Skip to content
File

Blob: src/node/internal/internal_fs_utils.ts

typescript922 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import {
27 ERR_BUFFER_OUT_OF_BOUNDS,
28 ERR_INVALID_ARG_TYPE,
29 ERR_INVALID_ARG_VALUE,
30} from 'node-internal:internal_errors';
31import {
32 validateAbortSignal,
33 validateObject,
34 validateBoolean,
35 validateInteger,
36 validateInt32,
37 validateUint32,
38 validateEncoding,
39 parseFileMode,
40} from 'node-internal:validators';
41import { isArrayBufferView } from 'node-internal:internal_types';
42import {
43 F_OK,
44 W_OK,
45 R_OK,
46 X_OK,
47 COPYFILE_EXCL,
48 COPYFILE_FICLONE,
49 O_RDONLY,
50 O_APPEND,
51 O_CREAT,
52 O_RDWR,
53 O_EXCL,
54 O_SYNC,
55 O_TRUNC,
56 O_WRONLY,
57 S_IFCHR,
58 S_IFDIR,
59 S_IFREG,
60 S_IFLNK,
61 S_IFMT,
62 S_IFSOCK,
63 S_IFIFO,
64 S_IFBLK,
65 UV_FS_COPYFILE_FICLONE_FORCE,
66} from 'node-internal:internal_fs_constants';
67 
68import { strictEqual } from 'node-internal:internal_assert';
69 
70import { Buffer } from 'node-internal:internal_buffer';
71import processImpl from 'node-internal:process';
72export type FilePath = string | URL | Buffer;
73 
74import type {
75 MakeDirectoryOptions,
76 OpenDirOptions,
77 ReadOptions,
78 RmDirOptions as NodeRmDirOptions,
79 RmOptions,
80 WriteFileOptions,
81} from 'node:fs';
82 
83// Extended RmDirOptions that includes deprecated properties we still support
84// eslint-disable-next-line @typescript-eslint/no-deprecated
85export type RmDirOptions = NodeRmDirOptions & {
86 /** @deprecated Use `fs.rm()` with `recursive` option instead */
87 maxRetries?: number | undefined;
88 /** @deprecated Use `fs.rm()` with `recursive` option instead */
89 recursive?: boolean | undefined;
90 /** @deprecated Use `fs.rm()` with `recursive` option instead */
91 retryDelay?: number | undefined;
92};
93 
94export type ValidEncoding = BufferEncoding | 'buffer' | null;
95 
96import type { Stat as InternalStat } from 'cloudflare-internal:filesystem';
97 
98// A non-public symbol used to ensure that certain constructors cannot
99// be called from user-code
100export const kBadge = Symbol('kBadge');
101export const kFileHandle = Symbol('kFileHandle');
102 
103export function isFileHandle(object: unknown): boolean {
104 if (typeof object !== 'object' || object === null) return false;
105 return Reflect.has(object, kFileHandle);
106}
107 
108export type RawTime = string | number | bigint;
109export type SymlinkType = 'dir' | 'file' | 'junction' | null | undefined;
110 
111// Normalizes the input time to a Date object.
112export function getDate(time: RawTime | Date): Date {
113 if (typeof time === 'number') {
114 return new Date(time);
115 } else if (typeof time === 'bigint') {
116 return new Date(Number(time));
117 } else if (typeof time === 'string') {
118 return new Date(time);
119 } else if (time instanceof Date) {
120 return time;
121 }
122 throw new ERR_INVALID_ARG_TYPE(
123 'time',
124 ['string', 'number', 'bigint', 'Date'],
125 time
126 );
127}
128 
129// Normalizes the input file path to a URL object.
130export function normalizePath(path: FilePath, encoding: string = 'utf8'): URL {
131 // We treat all of our virtual file system paths as file URLs
132 // as a way of normalizing them. Because our file system is
133 // fully virtual, we don't need to worry about a number of the
134 // issues that real file system paths have and don't need to
135 // worry quite as much about strictly checking for null bytes
136 // in the path. The URL parsing will take care of those details.
137 // We do, however, need to be sensitive to the fact that there
138 // are two different URL impls in the runtime that are selected
139 // based on compat flags. A worker that is using the legacy URL
140 // implementation will end up seeing slightly different behavior
141 // here but that's not something we need to worry about for now.
142 if (typeof path === 'string') {
143 // fallthrough for typical case
144 } else if (path instanceof URL) {
145 return path;
146 } else if (Buffer.isBuffer(path)) {
147 path = path.toString(encoding);
148 } else {
149 throw new ERR_INVALID_ARG_TYPE('path', ['string', 'Buffer', 'URL'], path);
150 }
151 
152 if (path.indexOf('\0') !== -1) {
153 throw new ERR_INVALID_ARG_VALUE(
154 'path',
155 path,
156 'must not contain null bytes'
157 );
158 }
159 
160 // In this case, we have a string path. Any ? or # characters in the
161 // path should be percent-encoded to ensure they are not treated as
162 // special characters in the URL.
163 path = path.replace(/[#?]/g, encodeURIComponent);
164 
165 // Node.js will also ignore empty path segments (e.g. `//` in the path).
166 // Let's normalize those out here as well.
167 path = path.replace(/\/\//g, '/');
168 
169 return new URL(
170 path,
171 path.startsWith('/') ? 'file://' : `file://${processImpl.getCwd()}/`
172 );
173}
174 
175export const kMaxUserId = 2 ** 32 - 1;
176 
177// In Node.js async callback APIs, input arguments are always validated
178// with input validation errors thrown synchronously. Only errors that
179// occur during the actual operation (e.g. file not found) are reported
180// via the callback. The validateAccessArgs function is used by both the
181// accessSync and access-with-callback APIs to validate the input args.
182export function validateAccessArgs(
183 rawPath: FilePath,
184 mode: number
185): { path: URL; mode: number } {
186 return {
187 path: normalizePath(rawPath),
188 mode: validateMode(mode),
189 };
190}
191 
192export function validateChownArgs(
193 pathOrFd: FilePath | number,
194 uid: number,
195 gid: number
196): { pathOrFd: URL | number; uid: number; gid: number } {
197 validateInteger(uid, 'uid', -1, kMaxUserId);
198 validateInteger(gid, 'gid', -1, kMaxUserId);
199 if (typeof pathOrFd === 'number') {
200 return {
201 pathOrFd: getValidatedFd(pathOrFd, 'fd'),
202 uid,
203 gid,
204 };
205 }
206 return {
207 pathOrFd: normalizePath(pathOrFd),
208 uid,
209 gid,
210 };
211}
212 
213export function validateStatArgs(
214 path: number | FilePath,
215 options: {
216 bigint?: boolean | undefined;
217 throwIfNoEntry?: boolean | undefined;
218 } = {},
219 isfstat = false
220): { pathOrFd: number | URL; bigint: boolean; throwIfNoEntry: boolean } {
221 validateObject(options, 'options');
222 const { bigint = false, throwIfNoEntry = true } = options;
223 validateBoolean(bigint, 'options.bigint');
224 validateBoolean(throwIfNoEntry, 'options.throwIfNoEntry');
225 if (typeof path === 'number') {
226 return {
227 pathOrFd: getValidatedFd(path, 'fd'),
228 bigint,
229 throwIfNoEntry,
230 };
231 }
232 if (isfstat) {
233 throw new ERR_INVALID_ARG_TYPE('fd', 'number', path);
234 }
235 return {
236 pathOrFd: normalizePath(path),
237 bigint,
238 throwIfNoEntry,
239 };
240}
241export function validateChmodArgs(
242 pathOrFd: FilePath | number,
243 mode: number | string
244): { pathOrFd: URL | number; mode: number } {
245 const actualMode = parseFileMode(mode, 'mode');
246 if (typeof pathOrFd === 'number') {
247 return {
248 pathOrFd: getValidatedFd(pathOrFd, 'fd'),
249 mode: actualMode,
250 };
251 }
252 return {
253 pathOrFd: normalizePath(pathOrFd),
254 mode: actualMode,
255 };
256}
257 
258export function validateMkDirArgs(
259 path: FilePath,
260 options: number | MakeDirectoryOptions
261): { path: URL; recursive: boolean } {
262 const { recursive = false, mode = 0o777 } = ((): MakeDirectoryOptions => {
263 if (typeof options === 'number') {
264 return { mode: options };
265 } else {
266 validateObject(options, 'options');
267 return options;
268 }
269 })();
270 
271 validateBoolean(recursive, 'options.recursive');
272 
273 // We don't implement the mode option in any meaningful way. We just validate it.
274 parseFileMode(mode, 'mode');
275 
276 return {
277 path: normalizePath(path),
278 recursive,
279 };
280}
281 
282export function validateRmArgs(
283 path: FilePath,
284 options: RmOptions
285): { path: URL; recursive: boolean; force: boolean } {
286 validateObject(options, 'options');
287 const {
288 force = false,
289 maxRetries = 0,
290 recursive = false,
291 retryDelay = 0,
292 } = options;
293 // We do not implement the maxRetries or retryDelay options in any meaningful
294 // way. We just validate them.
295 validateBoolean(force, 'options.force');
296 validateUint32(maxRetries, 'options.maxRetries');
297 validateBoolean(recursive, 'options.recursive');
298 validateUint32(retryDelay, 'options.retryDelay');
299 return {
300 path: normalizePath(path),
301 recursive,
302 force,
303 };
304}
305 
306export function validateRmDirArgs(
307 path: FilePath,
308 options: RmDirOptions
309): { path: URL; recursive: boolean } {
310 validateObject(options, 'options');
311 const { maxRetries = 0, recursive = false, retryDelay = 0 } = options; // eslint-disable-line @typescript-eslint/no-deprecated
312 // We do not implement the maxRetries or retryDelay options in any meaningful
313 // way. We just validate them.
314 validateUint32(maxRetries, 'options.maxRetries');
315 validateBoolean(recursive, 'options.recursive');
316 validateUint32(retryDelay, 'options.retryDelay');
317 return {
318 path: normalizePath(path),
319 recursive,
320 };
321}
322 
323// We could use the @types/node definition here but it's a bit overly
324// complex for our needs here.
325export type ReadDirOptions = {
326 encoding?: ValidEncoding | undefined;
327 withFileTypes?: boolean | undefined;
328 recursive?: boolean | undefined;
329};
330 
331export function validateReaddirArgs(
332 path: FilePath,
333 options: ReadDirOptions | ValidEncoding
334): {
335 path: URL;
336 encoding: ValidEncoding;
337 withFileTypes: boolean;
338 recursive: boolean;
339} {
340 if (typeof options === 'string' || options == null) {
341 options = { encoding: options };
342 }
343 validateObject(options, 'options');
344 const {
345 encoding = 'utf8',
346 withFileTypes = false,
347 recursive = false,
348 } = options;
349 if (encoding !== 'buffer' && !Buffer.isEncoding(encoding)) {
350 throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
351 }
352 validateBoolean(withFileTypes, 'options.withFileTypes');
353 validateBoolean(recursive, 'options.recursive');
354 return {
355 path: normalizePath(path),
356 encoding,
357 withFileTypes,
358 recursive,
359 };
360}
361 
362export function validateOpendirArgs(
363 path: FilePath,
364 options: OpenDirOptions
365): {
366 path: URL;
367 encoding: ValidEncoding;
368 recursive: boolean;
369} {
370 validateObject(options, 'options');
371 const { encoding = 'utf8', bufferSize = 32, recursive = false } = options;
372 if (!Buffer.isEncoding(encoding) && encoding !== 'buffer') {
373 throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
374 }
375 
376 // We don't implement the bufferSize option in any meaningful way but we
377 // do at least validate it.
378 validateUint32(bufferSize, 'options.bufferSize');
379 validateBoolean(recursive, 'options.recursive');
380 return {
381 path: normalizePath(path),
382 encoding,
383 recursive,
384 };
385}
386 
387export type WriteSyncOptions = {
388 offset?: number | undefined;
389 length?: number | undefined;
390 position?: Position | undefined;
391};
392 
393export function validateWriteArgs(
394 fd: number,
395 buffer: NodeJS.ArrayBufferView | string,
396 offsetOrOptions: WriteSyncOptions | Position | undefined,
397 length: number | ValidEncoding | undefined,
398 position: Position | undefined
399): { fd: number; buffer: Buffer[]; position: Position } {
400 fd = getValidatedFd(fd);
401 
402 let offset: number | undefined | null = offsetOrOptions as number;
403 if (isArrayBufferView(buffer)) {
404 if (typeof offsetOrOptions === 'object' && offsetOrOptions != null) {
405 ({
406 offset = 0,
407 length = buffer.byteLength,
408 position = null,
409 } = (offsetOrOptions as WriteSyncOptions | null) || {});
410 offset ??= 0;
411 validateInteger(offset, 'offset', 0);
412 offset += buffer.byteOffset;
413 } else {
414 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
415 if (offset != null) {
416 validateInteger(offset, 'offset', 0);
417 }
418 offset ??= 0;
419 offset += buffer.byteOffset;
420 length ??= buffer.byteLength;
421 position ??= null;
422 }
423 
424 validatePosition(position, 'position');
425 validateInteger(length, 'length', 0);
426 
427 // Validate that the offset + length do not exceed the buffer's byte length.
428 if (length > buffer.byteLength) {
429 throw new ERR_BUFFER_OUT_OF_BOUNDS('length');
430 }
431 if (offset > length) {
432 throw new ERR_BUFFER_OUT_OF_BOUNDS('offset');
433 }
434 
435 return {
436 fd,
437 buffer: [Buffer.from(buffer.buffer, offset, length)],
438 position,
439 };
440 }
441 
442 if (typeof buffer !== 'string') {
443 throw new ERR_INVALID_ARG_TYPE(
444 'buffer',
445 ['string', 'Buffer', 'TypedArray', 'DataView'],
446 buffer
447 );
448 }
449 
450 // In this case, offsetOrOptions must either be a number, bigint, or null.
451 validatePosition(offsetOrOptions, 'position');
452 position = offsetOrOptions;
453 
454 // In this instance, buffer is a string and the length arg specifies
455 // the encoding to use.
456 validateEncoding(buffer, length as string);
457 return {
458 fd,
459 buffer: [Buffer.from(buffer, length as string /* encoding */)],
460 position,
461 };
462}
463 
464export function validateWriteFileArgs(
465 path: number | FilePath,
466 data: string | ArrayBufferView,
467 options: ValidEncoding | WriteFileOptions
468): {
469 path: number | URL;
470 data: NodeJS.ArrayBufferView;
471 append: boolean;
472 exclusive: boolean;
473} {
474 if (typeof path === 'number') {
475 path = getValidatedFd(path);
476 } else {
477 path = normalizePath(path);
478 }
479 
480 if (typeof options === 'string' || options == null) {
481 options = { encoding: options as BufferEncoding | null };
482 }
483 
484 validateObject(options, 'options');
485 const {
486 encoding = 'utf8',
487 mode = 0o666,
488 flag = 'w',
489 flush = false,
490 } = options;
491 // @ts-expect-error TS2367 types does not overlap.
492 if (encoding !== 'buffer' && !Buffer.isEncoding(encoding)) {
493 throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
494 }
495 validateBoolean(flush, 'options.flush');
496 parseFileMode(mode, 'options.mode', 0o666);
497 const newFlag = stringToFlags(flag);
498 
499 const append = Boolean(newFlag & O_APPEND);
500 const write = Boolean(newFlag & O_WRONLY || newFlag & O_RDWR) || append;
501 const exclusive = Boolean(newFlag & O_EXCL);
502 
503 if (!write) {
504 throw new ERR_INVALID_ARG_VALUE(
505 'flag',
506 flag,
507 'must be indicate write or append'
508 );
509 }
510 
511 // We're not currently implementing the exclusive flag. We're validating
512 // it here just to use it so the compiler doesn't complain.
513 validateBoolean(exclusive, 'options.exclusive');
514 
515 if (typeof data === 'string') {
516 data = Buffer.from(data, encoding);
517 }
518 
519 if (!isArrayBufferView(data)) {
520 throw new ERR_INVALID_ARG_TYPE(
521 'data',
522 ['string', 'Buffer', 'TypedArray', 'DataView'],
523 data
524 );
525 }
526 
527 return {
528 path,
529 data: data as NodeJS.ArrayBufferView,
530 append,
531 exclusive,
532 };
533}
534 
535export function validateReadArgs(
536 fd: number,
537 buffer: NodeJS.ArrayBufferView,
538 offsetOrOptions: ReadOptions | number | null,
539 length: number | undefined,
540 position: Position | undefined
541): { fd: number; buffer: Buffer[]; length: number; position: Position } {
542 fd = getValidatedFd(fd);
543 
544 // Great fun with polymorphism here. We're going to normalize the arguments
545 // to match the first signature (fd, buffer, offset, length, position).
546 //
547 // If the third argument is an object, then we will pull the offset, length,
548 // and position from it, ignoring the remaining arguments. If the third
549 // argument is a number, then we will use it as the offset and pull the
550 // length and position from the fourth and fifth arguments. If the third
551 // position is any other type, then we will throw an error.
552 
553 if (!isArrayBufferView(buffer)) {
554 throw new ERR_INVALID_ARG_TYPE(
555 'buffer',
556 ['Buffer', 'TypedArray', 'DataView'],
557 buffer
558 );
559 }
560 
561 let actualOffset = buffer.byteOffset;
562 let actualLength = buffer.byteLength;
563 let actualPosition = position;
564 
565 // Handle the case where the third argument is an options object
566 if (offsetOrOptions != null && typeof offsetOrOptions === 'object') {
567 const {
568 offset = 0,
569 length = buffer.byteLength - offset,
570 position = null,
571 } = offsetOrOptions;
572 actualOffset = offset;
573 actualLength = length;
574 actualPosition = position;
575 }
576 // Handle the case where the third argument is a number (offset)
577 else if (typeof offsetOrOptions === 'number') {
578 actualOffset = offsetOrOptions;
579 actualLength = length ?? buffer.byteLength - actualOffset;
580 actualPosition = position;
581 } else {
582 throw new ERR_INVALID_ARG_TYPE(
583 'offset',
584 ['number', 'object'],
585 offsetOrOptions
586 );
587 }
588 
589 validateUint32(actualOffset, 'offset');
590 validateUint32(actualLength, 'length');
591 validatePosition(actualPosition, 'position');
592 
593 // The actualOffset plus actualLength must not exceed the buffer's byte length.
594 if (actualOffset + actualLength > buffer.byteLength) {
595 throw new ERR_INVALID_ARG_VALUE('offset', actualOffset, 'out of bounds');
596 }
597 
598 return {
599 fd,
600 buffer: [Buffer.from(buffer.buffer, actualOffset, actualLength)],
601 length: actualLength,
602 position: actualPosition,
603 };
604}
605 
606// Validate the mode argument for either copyFile or access operations.
607// The mode argument is a bitmask that can be used to specify the access
608// permissions for a file. The valid modes depend on the operation type:
609// - For copyFile, the valid modes are COPYFILE_EXCL, COPYFILE_FICLONE, and
610// COPYFILE_FICLONE_FORCE, with a default of 0.
611// - For access, the valid modes are F_OK, R_OK, W_OK, and X_OK, with a
612// default of F_OK.
613// In either case, the mode must be a valid bitmask integer within the
614// a given range. If the mode is not provided, the default value is used.
615// Throws ERR_INVALID_ARG_TYPE if the mode is not a valid integer, or
616// ERR_OUT_OF_RANGE if the mode is outside the valid range.
617function validateMode(
618 mode: number | undefined,
619 type: 'copyFile' | 'access' = 'access'
620): number {
621 // The access modes can be any of F_OK, R_OK, W_OK or X_OK. Some might not be
622 // available on specific systems. They can be used in combination as well
623 // (F_OK | R_OK | W_OK | X_OK).
624 let min = Math.min(F_OK, W_OK, R_OK, X_OK);
625 let max = F_OK | W_OK | R_OK | X_OK;
626 let def = F_OK;
627 if (type === 'copyFile') {
628 // The copy modes can be any of COPYFILE_EXCL, COPYFILE_FICLONE or
629 // COPYFILE_FICLONE_FORCE. They can be used in combination as well
630 // (COPYFILE_EXCL | COPYFILE_FICLONE | COPYFILE_FICLONE_FORCE).
631 min = Math.min(
632 0,
633 COPYFILE_EXCL,
634 COPYFILE_FICLONE,
635 UV_FS_COPYFILE_FICLONE_FORCE
636 );
637 max = COPYFILE_EXCL | COPYFILE_FICLONE | UV_FS_COPYFILE_FICLONE_FORCE;
638 def = mode || 0;
639 } else {
640 strictEqual(type, 'access');
641 }
642 mode ??= def;
643 validateInteger(mode, 'mode', min, max);
644 return mode;
645}
646 
647function assertEncoding(encoding: unknown): asserts encoding is string {
648 if (
649 encoding &&
650 encoding !== 'buffer' &&
651 !Buffer.isEncoding(encoding as string)
652 ) {
653 const reason = 'is invalid encoding';
654 throw new ERR_INVALID_ARG_VALUE('encoding', encoding, reason);
655 }
656}
657 
658export function getOptions(
659 options: string | Record<string, unknown> | null,
660 defaultOptions: Record<string, unknown> = {}
661): Record<string, unknown> {
662 if (options == null || typeof options === 'function') {
663 return defaultOptions;
664 }
665 
666 if (typeof options === 'string') {
667 defaultOptions = { ...defaultOptions };
668 defaultOptions.encoding = options;
669 options = defaultOptions;
670 } else if (typeof options !== 'object') {
671 throw new ERR_INVALID_ARG_TYPE('options', ['string', 'Object'], options);
672 }
673 
674 if (options.encoding !== 'buffer') assertEncoding(options.encoding);
675 
676 if (options.signal !== undefined) {
677 validateAbortSignal(options.signal, 'options.signal');
678 }
679 
680 return options;
681}
682 
683export function stringToFlags(
684 flags: number | null | undefined | string,
685 name: string = 'flags'
686): number {
687 if (typeof flags === 'number') {
688 validateInt32(flags, name);
689 return flags;
690 }
691 
692 if (flags == null) {
693 return O_RDONLY;
694 }
695 
696 switch (flags) {
697 case 'r':
698 return O_RDONLY;
699 case 'rs': // Fall through.
700 case 'sr':
701 return O_RDONLY | O_SYNC;
702 case 'r+':
703 return O_RDWR;
704 case 'rs+': // Fall through.
705 case 'sr+':
706 return O_RDWR | O_SYNC;
707 
708 case 'w':
709 return O_TRUNC | O_CREAT | O_WRONLY;
710 case 'wx': // Fall through.
711 case 'xw':
712 return O_TRUNC | O_CREAT | O_WRONLY | O_EXCL;
713 
714 case 'w+':
715 return O_TRUNC | O_CREAT | O_RDWR;
716 case 'wx+': // Fall through.
717 case 'xw+':
718 return O_TRUNC | O_CREAT | O_RDWR | O_EXCL;
719 
720 case 'a':
721 return O_APPEND | O_CREAT | O_WRONLY;
722 case 'ax': // Fall through.
723 case 'xa':
724 return O_APPEND | O_CREAT | O_WRONLY | O_EXCL;
725 case 'as': // Fall through.
726 case 'sa':
727 return O_APPEND | O_CREAT | O_WRONLY | O_SYNC;
728 
729 case 'a+':
730 return O_APPEND | O_CREAT | O_RDWR;
731 case 'ax+': // Fall through.
732 case 'xa+':
733 return O_APPEND | O_CREAT | O_RDWR | O_EXCL;
734 case 'as+': // Fall through.
735 case 'sa+':
736 return O_APPEND | O_CREAT | O_RDWR | O_SYNC;
737 }
738 
739 throw new ERR_INVALID_ARG_VALUE('flags', flags);
740}
741 
742export type Position = number | null | bigint;
743 
744export function validatePosition(
745 position: unknown,
746 name: string
747): asserts position is Position {
748 if (typeof position === 'number') {
749 validateUint32(position, name);
750 } else if (typeof position !== 'bigint' && position !== null) {
751 throw new ERR_INVALID_ARG_TYPE(
752 name,
753 ['integer', 'bigint', 'null'],
754 position
755 );
756 }
757}
758 
759export function getValidatedFd(fd: number, propName: string = 'fd'): number {
760 if (Object.is(fd, -0)) {
761 return 0;
762 }
763 
764 validateInt32(fd, propName, 0);
765 
766 return fd;
767}
768 
769export function validateBufferArray(
770 buffers: unknown,
771 propName: string = 'buffer'
772): ArrayBufferView[] {
773 if (!Array.isArray(buffers))
774 throw new ERR_INVALID_ARG_TYPE(propName, 'ArrayBufferView[]', buffers);
775 
776 for (let i = 0; i < buffers.length; i++) {
777 if (!isArrayBufferView(buffers[i]))
778 throw new ERR_INVALID_ARG_TYPE(propName, 'ArrayBufferView[]', buffers);
779 }
780 
781 return buffers as ArrayBufferView[];
782}
783 
784// Our implementation of the Stats class differs a bit from Node.js' in that
785// the one in Node.js uses the older function-style class. However, use of
786// new fs.Stats(...) has been deprecated in Node.js for quite some time and
787// users really aren't supposed to be trying to create their own Stats objects.
788// Therefore, we intentionally use a class-style object here and make it an
789// error to try to create your own Stats object using the constructor.
790export class Stats {
791 dev: number | bigint;
792 ino: number | bigint;
793 mode: number | bigint;
794 nlink: number | bigint;
795 uid: number | bigint;
796 gid: number | bigint;
797 rdev: number | bigint;
798 size: number | bigint;
799 blksize: number | bigint;
800 blocks: number | bigint;
801 atimeMs: number | bigint;
802 mtimeMs: number | bigint;
803 ctimeMs: number | bigint;
804 birthtimeMs: number | bigint;
805 atimeNs?: bigint;
806 mtimeNs?: bigint;
807 ctimeNs?: bigint;
808 birthtimeNs?: bigint;
809 atime: Date;
810 mtime: Date;
811 ctime: Date;
812 birthtime: Date;
813 
814 constructor(badge: symbol, stat: InternalStat, options: { bigint: boolean }) {
815 // The kBadge symbol is never exported for users. We use it as an internal
816 // marker to ensure that only internal code can create a Stats object using
817 // the constructor.
818 if (badge !== kBadge) {
819 throw new TypeError('Illegal constructor');
820 }
821 
822 // All nodes are always readable
823 this.mode = 0o444;
824 if (stat.writable) {
825 this.mode |= 0o222; // writable
826 }
827 
828 if (stat.device) {
829 this.mode |= S_IFCHR;
830 } else {
831 switch (stat.type) {
832 case 'file':
833 this.mode |= S_IFREG;
834 break;
835 case 'directory':
836 this.mode |= S_IFDIR;
837 break;
838 case 'symlink':
839 this.mode |= S_IFLNK;
840 break;
841 }
842 }
843 
844 if (options.bigint) {
845 this.dev = BigInt(stat.device);
846 this.size = BigInt(stat.size);
847 
848 this.mode = BigInt(this.mode);
849 this.atimeNs = 0n;
850 this.mtimeNs = stat.lastModified;
851 this.ctimeNs = stat.lastModified;
852 this.birthtimeNs = stat.created;
853 this.atimeMs = this.atimeNs / 1_000_000n;
854 this.mtimeMs = this.mtimeNs / 1_000_000n;
855 this.ctimeMs = this.ctimeNs / 1_000_000n;
856 this.birthtimeMs = this.birthtimeNs / 1_000_000n;
857 this.atime = new Date(Number(this.atimeMs));
858 this.mtime = new Date(Number(this.mtimeMs));
859 this.ctime = new Date(Number(this.ctimeMs));
860 this.birthtime = new Date(Number(this.birthtimeMs));
861 
862 // We have no meaningful definition of these values.
863 this.ino = 0n;
864 this.nlink = 1n;
865 this.uid = 0n;
866 this.gid = 0n;
867 this.rdev = 0n;
868 this.blksize = 0n;
869 this.blocks = 0n;
870 } else {
871 this.dev = Number(stat.device);
872 this.size = stat.size;
873 
874 this.atimeMs = 0;
875 this.mtimeMs = Number(stat.lastModified) / 1_000_000;
876 this.ctimeMs = Number(stat.lastModified) / 1_000_000;
877 this.birthtimeMs = Number(stat.created) / 1_000_000;
878 this.atime = new Date(this.atimeMs);
879 this.mtime = new Date(this.mtimeMs);
880 this.ctime = new Date(this.ctimeMs);
881 this.birthtime = new Date(this.birthtimeMs);
882 
883 // We have no meaningful definition of these values.
884 this.ino = 0;
885 this.nlink = 1;
886 this.uid = 0;
887 this.gid = 0;
888 this.rdev = 0;
889 this.blksize = 0;
890 this.blocks = 0;
891 }
892 }
893 
894 isBlockDevice(): boolean {
895 return (Number(this.mode) & S_IFMT) === S_IFBLK;
896 }
897 
898 isCharacterDevice(): boolean {
899 return (Number(this.mode) & S_IFMT) === S_IFCHR;
900 }
901 
902 isDirectory(): boolean {
903 return (Number(this.mode) & S_IFMT) === S_IFDIR;
904 }
905 
906 isFIFO(): boolean {
907 return (Number(this.mode) & S_IFMT) === S_IFIFO;
908 }
909 
910 isFile(): boolean {
911 return (Number(this.mode) & S_IFMT) === S_IFREG;
912 }
913 
914 isSocket(): boolean {
915 return (Number(this.mode) & S_IFMT) === S_IFSOCK;
916 }
917 
918 isSymbolicLink(): boolean {
919 return (Number(this.mode) & S_IFMT) === S_IFLNK;
920 }
921}