Skip to content
File

Blob: src/node/internal/internal_fs_sync.ts

typescript1014 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/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unnecessary-condition */
26 
27import {
28 stringToFlags,
29 getValidatedFd,
30 validateBufferArray,
31 normalizePath,
32 getDate,
33 Stats,
34 kBadge,
35 type FilePath,
36 type Position,
37 type RawTime,
38 type SymlinkType,
39 type ReadDirOptions,
40 type WriteSyncOptions,
41 type ValidEncoding,
42 validatePosition,
43 validateAccessArgs,
44 validateChownArgs,
45 validateChmodArgs,
46 validateStatArgs,
47 validateMkDirArgs,
48 validateRmArgs,
49 validateRmDirArgs,
50 validateReaddirArgs,
51 validateReadArgs,
52 validateWriteArgs,
53 validateWriteFileArgs,
54 validateOpendirArgs,
55} from 'node-internal:internal_fs_utils';
56import {
57 parseFileMode,
58 validateBoolean,
59 validateObject,
60 validateOneOf,
61 validateString,
62 validateUint32,
63} from 'node-internal:validators';
64import {
65 ERR_INVALID_ARG_TYPE,
66 ERR_INVALID_ARG_VALUE,
67 ERR_ENOENT,
68 ERR_EBADF,
69 ERR_EINVAL,
70 ERR_EEXIST,
71 ERR_UNSUPPORTED_OPERATION,
72} from 'node-internal:internal_errors';
73 
74import {
75 F_OK,
76 X_OK,
77 W_OK,
78 O_WRONLY,
79 O_RDWR,
80 O_APPEND,
81 O_EXCL,
82 COPYFILE_EXCL,
83 COPYFILE_FICLONE,
84 COPYFILE_FICLONE_FORCE,
85} from 'node-internal:internal_fs_constants';
86import { Dir, Dirent } from 'node-internal:internal_fs';
87import { default as cffs } from 'cloudflare-internal:filesystem';
88 
89import { Buffer } from 'node-internal:internal_buffer';
90import {
91 expandBraces,
92 normalizePattern,
93 walkGlob,
94 compileExcludePatterns,
95 collapseGlobstars,
96 precompileSegmentRegexes,
97 type GlobResult,
98} from 'node-internal:internal_fs_glob';
99import processImpl from 'node-internal:process';
100import type {
101 BigIntStatsFs,
102 CopySyncOptions,
103 GlobOptions,
104 GlobOptionsWithFileTypes,
105 GlobOptionsWithoutFileTypes,
106 MakeDirectoryOptions,
107 OpenDirOptions,
108 ReadOptions,
109 RmOptions,
110 StatsFs,
111 WriteFileOptions,
112} from 'node:fs';
113import type { RmDirOptions } from 'node-internal:internal_fs_utils';
114 
115export function accessSyncImpl(
116 path: URL,
117 mode: number,
118 followSymlinks: boolean
119): void {
120 // Input validation should have already been done by the caller.
121 
122 // If the X_OK flag is set we will always throw because we don't
123 // support executable files.
124 if (mode & X_OK) {
125 throw new ERR_ENOENT(path.pathname, { syscall: 'access' });
126 }
127 
128 const stat = cffs.stat(path, { followSymlinks });
129 
130 // Similar to node.js, we make no differentiation between the file
131 // not existing and the file existing but not being accessible.
132 if (stat == null || (mode & W_OK && !stat.writable)) {
133 // Not found... or not writable
134 throw new ERR_ENOENT(path.pathname, { syscall: 'access' });
135 }
136 
137 // We always assume that files are readable, so if we get here the
138 // path is accessible.
139}
140 
141export function accessSync(path: FilePath, mode: number = F_OK): void {
142 const { path: actualPath, mode: actualMode } = validateAccessArgs(path, mode);
143 accessSyncImpl(actualPath, actualMode, true);
144}
145 
146export function appendFileSync(
147 path: number | FilePath,
148 data: string | ArrayBufferView,
149 options: ValidEncoding | WriteFileOptions = {}
150): number {
151 if (typeof options === 'string' || options == null) {
152 options = { encoding: options as BufferEncoding };
153 }
154 const {
155 encoding = 'utf8',
156 mode = 0o666,
157 flag = 'a',
158 flush = false,
159 } = options ?? {};
160 // We defer to writeFileSync for the actual implementation and validation
161 return writeFileSync(path, data, { encoding, mode, flag, flush });
162}
163 
164export function chmodSync(path: FilePath, mode: string | number): void {
165 const { pathOrFd } = validateChmodArgs(path, mode);
166 if (cffs.stat(pathOrFd as URL, { followSymlinks: true }) == null) {
167 throw new ERR_ENOENT((pathOrFd as URL).pathname, { syscall: 'chmod' });
168 }
169 // We do not implement chmod in any meaningful way as our filesystem
170 // has no concept of user-defined permissions. Once we validate the inputs
171 // we just return as a non-op.
172 // The reason we call accessSync is to ensure, at the very least, that
173 // the path exists and would otherwise be accessible.
174}
175 
176export function chownSync(path: FilePath, uid: number, gid: number): void {
177 const { pathOrFd } = validateChownArgs(path, uid, gid);
178 if (cffs.stat(pathOrFd as URL, { followSymlinks: true }) == null) {
179 throw new ERR_ENOENT((pathOrFd as URL).pathname, { syscall: 'chown' });
180 }
181 // We do not implement chown in any meaningful way as our filesystem
182 // has no concept of ownership. Once we validate the inputs we just
183 // return as a non-op.
184 // The reason we call accessSync is to ensure, at the very least, that
185 // the path exists and would otherwise be accessible.
186}
187 
188export function closeSync(fd: number): void {
189 cffs.close(getValidatedFd(fd));
190}
191 
192export function copyFileSync(
193 src: FilePath,
194 dest: FilePath,
195 mode: number = 0
196): void {
197 validateOneOf(mode, 'mode', [
198 0,
199 COPYFILE_EXCL,
200 COPYFILE_FICLONE_FORCE,
201 COPYFILE_FICLONE,
202 ]);
203 if (mode & COPYFILE_FICLONE_FORCE) {
204 throw new ERR_UNSUPPORTED_OPERATION();
205 }
206 if (mode & COPYFILE_EXCL && existsSync(dest)) {
207 throw new ERR_EEXIST({
208 syscall: 'copyFile',
209 path: normalizePath(dest).pathname,
210 });
211 }
212 cffs.renameOrCopy(normalizePath(src), normalizePath(dest), { copy: true });
213}
214 
215export function cpSync(
216 src: FilePath,
217 dest: FilePath,
218 options: CopySyncOptions = {}
219): void {
220 validateObject(options, 'options');
221 const {
222 dereference = false,
223 errorOnExist = false,
224 force = true,
225 mode = 0,
226 preserveTimestamps = false,
227 recursive = false,
228 verbatimSymlinks = false,
229 } = options;
230 
231 validateBoolean(dereference, 'options.dereference');
232 validateBoolean(errorOnExist, 'options.errorOnExist');
233 validateBoolean(force, 'options.force');
234 validateBoolean(preserveTimestamps, 'options.preserveTimestamps');
235 validateBoolean(recursive, 'options.recursive');
236 validateBoolean(verbatimSymlinks, 'options.verbatimSymlinks');
237 validateUint32(mode, 'options.mode');
238 
239 if (mode & COPYFILE_FICLONE_FORCE) {
240 throw new ERR_INVALID_ARG_VALUE(
241 'options.mode',
242 'COPYFILE_FICLONE_FORCE is not supported'
243 );
244 }
245 
246 // We do not implement the filter option currently. There's a bug in the Node.js
247 // implementation of fs.cp and the option.filter in which non-UTF-8 encoded file
248 // names are not handled correctly and the option.filter fails when the src or
249 // dest is passed in as a Buffer. Fixing this bug in Node.js will require a breaking
250 // change to the API or a new API that appropriately handles Buffer inputs and non
251 // UTF-8 encoded names. We want to avoid implementing the filter option for now
252 // until Node.js settles on a better implementation and API.
253 if (options.filter !== undefined) {
254 if (typeof options.filter !== 'function') {
255 throw new ERR_INVALID_ARG_TYPE(
256 'options.filter',
257 'function',
258 options.filter
259 );
260 }
261 throw new ERR_UNSUPPORTED_OPERATION();
262 }
263 
264 const exclusive = Boolean(mode & COPYFILE_EXCL);
265 // We're not current implementing the exclusive flag. We're validating
266 // it here just to use it so the compiler doesn't complain.
267 validateBoolean(exclusive, '');
268 
269 // We're not currently implementing verbatimSymlinks in any meaningful way.
270 // Our symlinks are always fully qualfied. That is, they always point to
271 // an absolute path and never to a relative path, so there is no distinction
272 // between verbatimSymlinks and non-verbatimSymlinks. We validate the option
273 // value above but otherwise we ignore it.
274 
275 // We're also not currently implementing the preserveTimestamps option.
276 // Timestamps in our virtual filesystem aren't super meaningful given
277 // that most files in the current implementation are either created
278 // at startup and use the EPOCH as their timestamp, or are temporary files
279 // that are deleted when the request completes.
280 // TODO(node-fs): Decide if we want to implement preserveTimestamps in the future.
281 
282 cffs.cp(normalizePath(src), normalizePath(dest), {
283 deferenceSymlinks: dereference,
284 recursive,
285 force,
286 errorOnExist,
287 });
288}
289 
290export function existsSync(path: FilePath): boolean {
291 try {
292 // The existsSync function follows symlinks. If the symlink is broken,
293 // it will return false.
294 accessSync(path);
295 return true;
296 } catch {
297 // It's always odd to swallow errors but this is how Node.js does it.
298 // The existsSync function never throws and returns false if any error
299 // occurs.
300 return false;
301 }
302}
303 
304export function fchmodSync(fd: number, mode: string | number): void {
305 fd = getValidatedFd(fd);
306 parseFileMode(mode, 'mode');
307 if (cffs.stat(fd, { followSymlinks: true }) == null) {
308 throw new ERR_EBADF({ syscall: 'fchmod' });
309 }
310 // We do not implement chmod in any meaningful way as our filesystem
311 // has no concept of user-defined permissions. Once we validate the inputs
312 // we just return as a non-op.
313 // The reason we call cffs.stat is to ensure, at the very least, that
314 // the fd is valid and would otherwise be accessible.
315}
316 
317export function fchownSync(fd: number, uid: number, gid: number): void {
318 const { pathOrFd } = validateChownArgs(fd, uid, gid);
319 if (cffs.stat(pathOrFd as number, { followSymlinks: true }) == null) {
320 throw new ERR_EBADF({ syscall: 'fchown' });
321 }
322 // We do not implement chown in any meaningful way as our filesystem
323 // has no concept of ownership. Once we validate the inputs we just
324 // return as a non-op.
325 // The reason we call accessSync is to ensure, at the very least, that
326 // the path exists and would otherwise be accessible.
327}
328 
329export function fdatasyncSync(fd: number): void {
330 fd = getValidatedFd(fd);
331 // We do not implement fdatasync in any meaningful way. At most we
332 // will validate that the fd is valid and would otherwise be accessible.
333 if (cffs.stat(fd, { followSymlinks: true }) == null) {
334 throw new ERR_EBADF({ syscall: 'datasync' });
335 }
336}
337 
338export type FStatOptions = {
339 bigint?: boolean | undefined;
340};
341 
342export function fstatSync(fd: number, options: FStatOptions = {}): Stats {
343 validateObject(options, 'options');
344 const { pathOrFd: validatedFd, bigint } = validateStatArgs(
345 fd,
346 options,
347 true /* is fstat */
348 );
349 const stat = cffs.stat(validatedFd as number, { followSymlinks: true });
350 if (stat == null) {
351 throw new ERR_EBADF({ syscall: 'stat' });
352 }
353 return new Stats(kBadge, stat, { bigint });
354}
355 
356export function fsyncSync(fd: number): void {
357 fd = getValidatedFd(fd);
358 // We do not implement fdatasync in any meaningful way. At most we
359 // will validate that the fd is valid and would otherwise be accessible.
360 if (cffs.stat(fd, { followSymlinks: true }) == null) {
361 throw new ERR_EBADF({ syscall: 'sync' });
362 }
363}
364 
365export function ftruncateSync(fd: number, len: number = 0): void {
366 validateUint32(len, 'len');
367 cffs.truncate(getValidatedFd(fd), len);
368}
369 
370export function futimesSync(
371 fd: number,
372 atime: RawTime | Date,
373 mtime: RawTime | Date
374): void {
375 // We do not actually make use of access time in our filesystem. We just
376 // validate the inputs here.
377 atime = getDate(atime);
378 mtime = getDate(mtime);
379 cffs.setLastModified(getValidatedFd(fd), mtime, {});
380}
381 
382export function lchmodSync(path: FilePath, mode: string | number): void {
383 const { pathOrFd } = validateChmodArgs(path, mode);
384 if (cffs.stat(pathOrFd as URL, { followSymlinks: false }) == null) {
385 throw new ERR_ENOENT((pathOrFd as URL).pathname, { syscall: 'lchmod' });
386 }
387 // We do not implement chmod in any meaningful way as our filesystem
388 // has no concept of user-defined permissions. Once we validate the inputs
389 // we just return as a non-op.
390 // The reason we call cffs.stat is to ensure, at the very least, that
391 // the fd is valid and would otherwise be accessible.
392}
393 
394export function lchownSync(path: FilePath, uid: number, gid: number): void {
395 const { pathOrFd } = validateChownArgs(path, uid, gid);
396 if (cffs.stat(pathOrFd as URL, { followSymlinks: false }) == null) {
397 throw new ERR_ENOENT((path as URL).pathname, { syscall: 'lchown' });
398 }
399 // We do not implement chown in any meaningful way as our filesystem
400 // has no concept of user-defined permissions. Once we validate the inputs
401 // we just return as a non-op.
402 // The reason we call cffs.stat is to ensure, at the very least, that
403 // the fd is valid and would otherwise be accessible.
404}
405 
406export function lutimesSync(
407 path: FilePath,
408 atime: RawTime | Date,
409 mtime: RawTime | Date
410): void {
411 // We do not actually make use of access time in our filesystem. We just
412 // validate the inputs here.
413 atime = getDate(atime);
414 mtime = getDate(mtime);
415 cffs.setLastModified(normalizePath(path), mtime, { followSymlinks: false });
416}
417 
418export function linkSync(existingPath: FilePath, newPath: FilePath): void {
419 cffs.link(normalizePath(newPath), normalizePath(existingPath), {
420 symbolic: false,
421 });
422}
423 
424// We could use the StatSyncOptions from @types:node here but the definition
425// of that in @types:node is bit overly complex for our use here.
426export type StatOptions = {
427 bigint?: boolean | undefined;
428 throwIfNoEntry?: boolean | undefined;
429};
430 
431export function lstatSync(
432 path: FilePath,
433 options: StatOptions = {}
434): Stats | undefined {
435 const {
436 pathOrFd: validatedPath,
437 bigint,
438 throwIfNoEntry,
439 } = validateStatArgs(path, options);
440 const stat = cffs.stat(validatedPath as URL, { followSymlinks: false });
441 if (stat == null) {
442 if (throwIfNoEntry) {
443 throw new ERR_ENOENT((validatedPath as URL).pathname, {
444 syscall: 'lstat',
445 });
446 }
447 return undefined;
448 }
449 return new Stats(kBadge, stat, { bigint });
450}
451 
452export function mkdirSync(
453 path: FilePath,
454 options: number | MakeDirectoryOptions = {}
455): string | undefined {
456 const { path: normalizedPath, recursive } = validateMkDirArgs(path, options);
457 
458 return cffs.mkdir(normalizedPath, { recursive, tmp: false });
459}
460 
461export type MkdirTempSyncOptions = {
462 encoding?: ValidEncoding | undefined;
463};
464 
465export function mkdtempSync(
466 prefix: FilePath,
467 options: ValidEncoding | MkdirTempSyncOptions = {}
468): string {
469 if (typeof options === 'string' || options == null) {
470 options = { encoding: options };
471 }
472 validateObject(options, 'options');
473 const { encoding = 'utf8' } = options;
474 if (!Buffer.isEncoding(encoding) && encoding !== 'buffer') {
475 throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
476 }
477 prefix = normalizePath(prefix, encoding);
478 const ret = cffs.mkdir(normalizePath(prefix), {
479 recursive: false,
480 tmp: true,
481 });
482 if (ret === undefined) {
483 // If mkdir failed it should throw a meaningful error. If we get
484 // here, it means something else went wrong and we're just going
485 // to throw a generic EINVAL error.
486 throw new ERR_EINVAL({ syscall: 'mkdtemp' });
487 }
488 return ret;
489}
490 
491export function opendirSync(path: FilePath, options: OpenDirOptions = {}): Dir {
492 const {
493 path: validatedPath,
494 encoding,
495 recursive,
496 } = validateOpendirArgs(path, options);
497 
498 const handles = cffs.readdir(validatedPath, { recursive });
499 return new Dir(handles, path, { encoding });
500}
501 
502export function openSync(
503 path: FilePath,
504 flags: string | number = 'r',
505 mode: string | number = 0o666
506): number {
507 // We don't actually the the mode in any meaningful way. We just validate it.
508 parseFileMode(mode, 'mode', 0o666);
509 const newFlags = stringToFlags(flags);
510 
511 const read = !(newFlags & O_WRONLY) || Boolean(newFlags & O_RDWR);
512 const write = Boolean(newFlags & O_WRONLY) || Boolean(newFlags & O_RDWR);
513 const append = Boolean(newFlags & O_APPEND);
514 const exclusive = Boolean(newFlags & O_EXCL);
515 const followSymlinks = true;
516 
517 return cffs.open(normalizePath(path), {
518 read,
519 write,
520 append,
521 exclusive,
522 followSymlinks,
523 });
524}
525 
526export type ReadDirResult = string[] | Buffer[] | Dirent[];
527 
528export function readdirSync(
529 path: FilePath,
530 options: ValidEncoding | ReadDirOptions = {}
531): ReadDirResult {
532 if (typeof options === 'string' || options == null) {
533 options = { encoding: options };
534 }
535 const {
536 path: normalizedPath,
537 encoding,
538 withFileTypes,
539 recursive,
540 } = validateReaddirArgs(path, options);
541 
542 const handles = cffs.readdir(normalizedPath, { recursive });
543 
544 if (withFileTypes) {
545 if ((encoding as string) === 'buffer') {
546 return handles.map((handle) => {
547 return new Dirent(
548 Buffer.from(handle.name),
549 handle.type,
550 handle.parentPath
551 );
552 });
553 }
554 return handles.map((handle) => {
555 return new Dirent(handle.name, handle.type, handle.parentPath);
556 });
557 }
558 
559 if ((encoding as string) === 'buffer') {
560 return handles.map((handle) => {
561 return Buffer.from(handle.name);
562 });
563 }
564 
565 return handles.map((handle) => {
566 return Buffer.from(handle.name).toString(encoding as string);
567 });
568}
569 
570export type ReadFileSyncOptions = {
571 encoding?: ValidEncoding | undefined;
572 flag?: string | number | undefined;
573};
574 
575export function readFileSync(
576 pathOrFd: number | FilePath,
577 options: ValidEncoding | ReadFileSyncOptions = {}
578): string | Buffer {
579 if (typeof options === 'string' || options == null) {
580 options = { encoding: options };
581 }
582 validateObject(options, 'options');
583 const { encoding = null, flag = 'r' } = options;
584 if (
585 encoding !== null &&
586 encoding !== 'buffer' &&
587 !Buffer.isEncoding(encoding)
588 ) {
589 throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
590 }
591 stringToFlags(flag);
592 
593 // TODO(node:fs): We are currently ignoring flags on readFileSync.
594 
595 const u8 = ((): Uint8Array => {
596 if (typeof pathOrFd === 'number') {
597 return cffs.readAll(getValidatedFd(pathOrFd));
598 }
599 return cffs.readAll(normalizePath(pathOrFd));
600 })();
601 
602 const buf = Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength);
603 if (typeof encoding === 'string') {
604 return buf.toString(encoding);
605 }
606 return buf;
607}
608 
609export type ReadLinkSyncOptions = {
610 encoding?: ValidEncoding | undefined;
611};
612 
613export function readlinkSync(
614 path: FilePath,
615 options: ValidEncoding | ReadLinkSyncOptions = {}
616): string | Buffer {
617 if (typeof options === 'string' || options == null) {
618 options = { encoding: options };
619 }
620 validateObject(options, 'options');
621 const { encoding = 'utf8' } = options;
622 if (!Buffer.isEncoding(encoding) && encoding !== 'buffer') {
623 throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
624 }
625 const dest = Buffer.from(
626 cffs.readLink(normalizePath(path), { failIfNotSymlink: true })
627 );
628 if (typeof encoding === 'string') {
629 return dest.toString(encoding);
630 }
631 return dest;
632}
633 
634// readSync is overloaded to support two different signatures:
635// fs.readSync(fd, buffer, offset, length, position)
636// fs.readSync(fd, buffer, options)
637//
638// fd is always a number, buffer is an ArrayBufferView, offset and length
639// are numbers, and position is either a number, bigint, or null.
640export function readSync(
641 fd: number,
642 buffer: NodeJS.ArrayBufferView,
643 offsetOrOptions: ReadOptions | number = {},
644 length?: number,
645 position: Position = null
646): number {
647 const {
648 fd: validatedFd,
649 buffer: actualBuffer,
650 length: actualLength,
651 position: actualPosition,
652 } = validateReadArgs(fd, buffer, offsetOrOptions, length, position);
653 
654 if (actualLength === 0 || buffer.byteLength === 0) {
655 return 0;
656 }
657 
658 return readvSync(validatedFd, actualBuffer, actualPosition);
659}
660 
661export function readvSync(
662 fd: number,
663 buffers: NodeJS.ArrayBufferView[],
664 position: Position = null
665): number {
666 fd = getValidatedFd(fd);
667 validateBufferArray(buffers);
668 validatePosition(position, 'position');
669 
670 if (buffers.length === 0) {
671 return 0;
672 }
673 return cffs.read(fd, buffers, { position });
674}
675 
676export function realpathSync(
677 p: FilePath,
678 options: ValidEncoding | ReadLinkSyncOptions = {}
679): string | Buffer {
680 if (typeof options === 'string' || options == null) {
681 options = { encoding: options };
682 }
683 validateObject(options, 'options');
684 const { encoding = 'utf8' } = options;
685 if (!Buffer.isEncoding(encoding) && encoding !== 'buffer') {
686 throw new ERR_INVALID_ARG_VALUE('options.encoding', encoding);
687 }
688 const dest = Buffer.from(
689 cffs.readLink(normalizePath(p), { failIfNotSymlink: false })
690 );
691 if (typeof encoding === 'string') {
692 return dest.toString(encoding);
693 }
694 return dest;
695}
696 
697realpathSync.native = realpathSync;
698 
699export function renameSync(src: FilePath, dest: FilePath): void {
700 cffs.renameOrCopy(normalizePath(src), normalizePath(dest), { copy: false });
701}
702 
703export function rmdirSync(path: FilePath, options: RmDirOptions = {}): void {
704 const { path: normalizedPath, recursive } = validateRmDirArgs(path, options);
705 
706 cffs.rm(normalizedPath, { recursive, force: false, dironly: true });
707}
708 
709export function rmSync(path: FilePath, options: RmOptions = {}): void {
710 const {
711 path: normalizedPath,
712 recursive,
713 force,
714 } = validateRmArgs(path, options);
715 
716 cffs.rm(normalizedPath, { recursive, force, dironly: false });
717}
718 
719export function statSync(
720 path: FilePath,
721 options: StatOptions = {}
722): Stats | undefined {
723 const {
724 pathOrFd: validatedPath,
725 bigint,
726 throwIfNoEntry,
727 } = validateStatArgs(path, options);
728 const stat = cffs.stat(validatedPath as URL, { followSymlinks: true });
729 if (stat == null) {
730 if (throwIfNoEntry) {
731 throw new ERR_ENOENT((validatedPath as URL).pathname, {
732 syscall: 'stat',
733 });
734 }
735 return undefined;
736 }
737 return new Stats(kBadge, stat, { bigint });
738}
739 
740export function statfsSync(
741 path: FilePath,
742 options: { bigint?: boolean | undefined } = {}
743): StatsFs | BigIntStatsFs {
744 normalizePath(path);
745 validateObject(options, 'options');
746 const { bigint = false } = options;
747 validateBoolean(bigint, 'options.bigint');
748 // We don't implement statfs in any meaningful way. Nor will we actually
749 // validate that the path exists. We just return a non-op dummy object.
750 if (bigint) {
751 return {
752 type: 0n,
753 bsize: 0n,
754 blocks: 0n,
755 bfree: 0n,
756 bavail: 0n,
757 files: 0n,
758 ffree: 0n,
759 };
760 } else {
761 return {
762 type: 0,
763 bsize: 0,
764 blocks: 0,
765 bfree: 0,
766 bavail: 0,
767 files: 0,
768 ffree: 0,
769 };
770 }
771}
772 
773export function symlinkSync(
774 target: FilePath,
775 path: FilePath,
776 type: SymlinkType = null
777): void {
778 // We don't implement type in any meaningful way but we do validate it.
779 validateOneOf(type, 'type', ['dir', 'file', 'junction', null]);
780 cffs.link(normalizePath(path), normalizePath(target), { symbolic: true });
781}
782 
783export function truncateSync(path: FilePath, len: number = 0): void {
784 validateUint32(len, 'len');
785 cffs.truncate(normalizePath(path), len);
786}
787 
788export function unlinkSync(path: FilePath): void {
789 cffs.unlink(normalizePath(path));
790}
791 
792export function utimesSync(
793 path: FilePath,
794 atime: RawTime | Date,
795 mtime: RawTime | Date
796): void {
797 // We do not actually make use of access time in our filesystem. We just
798 // validate the inputs here.
799 atime = getDate(atime);
800 mtime = getDate(mtime);
801 cffs.setLastModified(normalizePath(path), mtime, { followSymlinks: true });
802}
803 
804export function writeFileSync(
805 path: number | FilePath,
806 data: string | ArrayBufferView,
807 options: ValidEncoding | WriteFileOptions = {}
808): number {
809 const {
810 path: validatedPath,
811 data: actualData,
812 append,
813 exclusive,
814 } = validateWriteFileArgs(path, data, options);
815 
816 return cffs.writeAll(validatedPath, actualData, { append, exclusive });
817}
818 
819export function writeSync(
820 fd: number,
821 buffer: NodeJS.ArrayBufferView | string,
822 offsetOrOptions: WriteSyncOptions | Position = null,
823 length?: number | ValidEncoding,
824 position?: Position
825): number {
826 const {
827 fd: validatedFd,
828 buffer: actualBuffer,
829 position: actualPosition,
830 } = validateWriteArgs(fd, buffer, offsetOrOptions, length, position);
831 
832 return writevSync(validatedFd, actualBuffer, actualPosition);
833}
834 
835export function writevSync(
836 fd: number,
837 buffers: NodeJS.ArrayBufferView[],
838 position: Position = null
839): number {
840 fd = getValidatedFd(fd);
841 validateBufferArray(buffers);
842 validatePosition(position, 'position');
843 
844 if (buffers.length === 0) {
845 return 0;
846 }
847 
848 return cffs.write(fd, buffers, { position });
849}
850 
851export function globSync(
852 pattern: string | readonly string[],
853 options:
854 | GlobOptions
855 | GlobOptionsWithFileTypes
856 | GlobOptionsWithoutFileTypes = {}
857): string[] | Dirent[] {
858 // Normalize pattern to array
859 const patterns: string[] =
860 typeof pattern === 'string' ? [pattern] : [...pattern];
861 for (const p of patterns) {
862 validateString(p, 'pattern');
863 }
864 
865 if (typeof options !== 'object' || options === null) {
866 validateObject(options, 'options');
867 }
868 
869 const cwdOption = (options as GlobOptions).cwd;
870 const cwd: string =
871 cwdOption instanceof URL
872 ? cwdOption.pathname
873 : ((cwdOption as string | undefined) ?? processImpl.getCwd());
874 validateString(cwd, 'options.cwd');
875 
876 const withFileTypes: boolean =
877 (options as GlobOptionsWithFileTypes).withFileTypes ?? false;
878 
879 // Exclude can be a user function or an array of glob patterns.
880 // Keep them as separate variables to preserve proper type signatures.
881 const excludeOption = (options as GlobOptions).exclude;
882 let excludeUserFn: ((path: string | Dirent) => boolean) | undefined;
883 let excludePatternFn: ((path: string) => boolean) | undefined;
884 if (typeof excludeOption === 'function') {
885 excludeUserFn = excludeOption as (path: string | Dirent) => boolean;
886 } else if (Array.isArray(excludeOption)) {
887 excludePatternFn = compileExcludePatterns(excludeOption as string[]);
888 }
889 
890 // Pattern-driven directory walk
891 const results = new Map<string, GlobResult>();
892 const dirCache = new Map<
893 string,
894 import('cloudflare-internal:filesystem').DirEntryHandle[]
895 >();
896 
897 for (const p of patterns) {
898 for (const expanded of expandBraces(p)) {
899 const normalized = normalizePattern(expanded);
900 const segments = collapseGlobstars(
901 normalized.split('/').filter((s) => s !== '')
902 );
903 if (segments.length === 0) continue;
904 const segmentRegexes = precompileSegmentRegexes(segments);
905 walkGlob(cwd, segments, 0, cwd, '', results, dirCache, segmentRegexes);
906 }
907 }
908 
909 // Build final results, applying exclude filter
910 const stringResults: string[] = [];
911 const direntResults: Dirent[] = [];
912 
913 for (const [relPath, entry] of results) {
914 if (!relPath) continue; // skip empty paths
915 
916 if (excludePatternFn && excludePatternFn(relPath)) continue;
917 
918 if (withFileTypes) {
919 const parts = relPath.split('/');
920 const name = parts.pop() ?? '';
921 const parentPath = cwd + (parts.length ? '/' + parts.join('/') : '');
922 const type = entry.handle?.type ?? 0;
923 const dirent = new Dirent(name, type, parentPath);
924 if (excludeUserFn && excludeUserFn(dirent)) continue;
925 direntResults.push(dirent);
926 } else {
927 if (excludeUserFn && excludeUserFn(relPath)) continue;
928 stringResults.push(relPath);
929 }
930 }
931 
932 return withFileTypes ? direntResults : stringResults;
933}
934 
935export interface OpenAsBlobOptions {
936 type?: string | undefined;
937}
938export function openAsBlob(
939 path: FilePath,
940 options: OpenAsBlobOptions = {}
941): Blob {
942 // TODO(node-fs): We do not yet implement the openAsBlob API. We will implement
943 // this soon.
944 normalizePath(path);
945 validateObject(options, 'options');
946 const { type = '' } = options;
947 validateString(type, 'options.type');
948 
949 return cffs.openAsBlob(normalizePath(path), { type });
950}
951 
952// An API is considered stubbed if it is not implemented by the function
953// exists with the correct signature and throws an error if called. If
954// a function exists that does not have the correct signature, it is
955// not considered fully stubbed.
956// An API is considered optimized if the API has been implemented and
957// tested and then optimized for performance.
958//
959// (S == Stubbed, I == Implemented, T == Tested, O == Optimized, V = Verified)
960// For T, 1 == basic tests, 2 == node.js tests ported
961// Verified means that the behavior or the API has been verified to be
962// consistent with the node.js API. This does not mean that the behaviors.
963// We can only determine verification status once the node.js tests are
964// ported and verified to work correctly.
965// match exactly, just that they are consistent.
966// S I T V O
967// [x][x][2][x][x] fs.accessSync(path[, mode])
968// [x][x][2][x][x] fs.existsSync(path)
969// [x][x][2][x][x] fs.chmodSync(path, mode)
970// [x][x][2][x][x] fs.chownSync(path, uid, gid)
971// [x][x][2][x][x] fs.closeSync(fd)
972// [x][x][2][x][x] fs.fchmodSync(fd, mode)
973// [x][x][2][x][x] fs.fchownSync(fd, uid, gid)
974// [x][x][2][x][x] fs.lchmodSync(path, mode)
975// [x][x][2][x][x] fs.lchownSync(path, uid, gid)
976// [x][x][2][x][x] fs.futimesSync(fd, atime, mtime)
977// [x][x][2][x][x] fs.lutimesSync(path, atime, mtime)
978// [x][x][2][x][x] fs.utimesSync(path, atime, mtime)
979// [x][x][2][x][x] fs.fstatSync(fd[, options])
980// [x][x][2][x][x] fs.lstatSync(path[, options])
981// [x][x][2][x][x] fs.statSync(path[, options])
982// [x][x][2][x][x] fs.statfsSync(path[, options])
983// [x][x][2][x][x] fs.fdatasyncSync(fd)
984// [x][x][2][x][x] fs.fsyncSync(fd)
985// [x][x][2][x][x] fs.linkSync(existingPath, newPath)
986// [x][x][2][x][x] fs.readlinkSync(path[, options])
987// [x][x][2][x][x] fs.realpathSync(path[, options])
988// [x][x][2][x][x] fs.realpathSync.native(path[, options])
989// [x][x][2][x][x] fs.symlinkSync(target, path[, type])
990// [x][x][2][x][x] fs.unlinkSync(path)
991// [x][x][2][x][x] fs.mkdirSync(path[, options])
992// [x][x][2][x][x] fs.mkdtempSync(prefix[, options])
993// [x][x][2][x][x] fs.rmdirSync(path[, options])
994// [x][x][2][x][x] fs.rmSync(path[, options])
995// [x][x][2][x][x] fs.ftruncateSync(fd[, len])
996// [x][x][2][x][x] fs.truncateSync(path[, len])
997// [x][x][2][x][x] fs.openSync(path[, flags[, mode]])
998// [x][x][2][x][x] fs.readdirSync(path[, options])
999// [x][x][2][x][x] fs.readFileSync(path[, options])
1000// [x][x][2][x][x] fs.readSync(fd, buffer, offset, length[, position])
1001// [x][x][2][x][x] fs.readSync(fd, buffer[, options])
1002// [x][x][2][x][x] fs.readvSync(fd, buffers[, position])
1003// [x][x][2][x][x] fs.renameSync(oldPath, newPath)
1004// [x][x][2][x][x] fs.writeFileSync(file, data[, options])
1005// [x][x][2][x][x] fs.writeSync(fd, buffer, offset[, length[, position]])
1006// [x][x][2][x][x] fs.writeSync(fd, buffer[, options])
1007// [x][x][2][x][x] fs.writeSync(fd, string[, position[, encoding]])
1008// [x][x][2][x][x] fs.writevSync(fd, buffers[, position])
1009// [x][x][2][x][x] fs.appendFileSync(path, data[, options])
1010// [x][x][2][x][x] fs.copyFileSync(src, dest[, mode])
1011// [x][x][2][x][x] fs.opendirSync(path[, options])
1012// [x][x][2][x][x] fs.cpSync(src, dest[, options])
1013// [x][x][2][x][x] fs.globSync(pattern[, options])