File
Blob: src/node/internal/internal_fs_sync.ts
| 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 | |
| 27 | import { |
| 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'; |
| 56 | import { |
| 57 | parseFileMode, |
| 58 | validateBoolean, |
| 59 | validateObject, |
| 60 | validateOneOf, |
| 61 | validateString, |
| 62 | validateUint32, |
| 63 | } from 'node-internal:validators'; |
| 64 | import { |
| 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 | |
| 74 | import { |
| 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'; |
| 86 | import { Dir, Dirent } from 'node-internal:internal_fs'; |
| 87 | import { default as cffs } from 'cloudflare-internal:filesystem'; |
| 88 | |
| 89 | import { Buffer } from 'node-internal:internal_buffer'; |
| 90 | import { |
| 91 | expandBraces, |
| 92 | normalizePattern, |
| 93 | walkGlob, |
| 94 | compileExcludePatterns, |
| 95 | collapseGlobstars, |
| 96 | precompileSegmentRegexes, |
| 97 | type GlobResult, |
| 98 | } from 'node-internal:internal_fs_glob'; |
| 99 | import processImpl from 'node-internal:process'; |
| 100 | import 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'; |
| 113 | import type { RmDirOptions } from 'node-internal:internal_fs_utils'; |
| 114 | |
| 115 | export 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 | |
| 141 | export 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 | |
| 146 | export 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 | |
| 164 | export 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 | |
| 176 | export 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 | |
| 188 | export function closeSync(fd: number): void { |
| 189 | cffs.close(getValidatedFd(fd)); |
| 190 | } |
| 191 | |
| 192 | export 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 | |
| 215 | export 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 | |
| 290 | export 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 | |
| 304 | export 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 | |
| 317 | export 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 | |
| 329 | export 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 | |
| 338 | export type FStatOptions = { |
| 339 | bigint?: boolean | undefined; |
| 340 | }; |
| 341 | |
| 342 | export 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 | |
| 356 | export 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 | |
| 365 | export function ftruncateSync(fd: number, len: number = 0): void { |
| 366 | validateUint32(len, 'len'); |
| 367 | cffs.truncate(getValidatedFd(fd), len); |
| 368 | } |
| 369 | |
| 370 | export 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 | |
| 382 | export 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 | |
| 394 | export 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 | |
| 406 | export 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 | |
| 418 | export 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. |
| 426 | export type StatOptions = { |
| 427 | bigint?: boolean | undefined; |
| 428 | throwIfNoEntry?: boolean | undefined; |
| 429 | }; |
| 430 | |
| 431 | export 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 | |
| 452 | export 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 | |
| 461 | export type MkdirTempSyncOptions = { |
| 462 | encoding?: ValidEncoding | undefined; |
| 463 | }; |
| 464 | |
| 465 | export 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 | |
| 491 | export 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 | |
| 502 | export 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 | |
| 526 | export type ReadDirResult = string[] | Buffer[] | Dirent[]; |
| 527 | |
| 528 | export 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 | |
| 570 | export type ReadFileSyncOptions = { |
| 571 | encoding?: ValidEncoding | undefined; |
| 572 | flag?: string | number | undefined; |
| 573 | }; |
| 574 | |
| 575 | export 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 | |
| 609 | export type ReadLinkSyncOptions = { |
| 610 | encoding?: ValidEncoding | undefined; |
| 611 | }; |
| 612 | |
| 613 | export 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. |
| 640 | export 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 | |
| 661 | export 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 | |
| 676 | export 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 | |
| 697 | realpathSync.native = realpathSync; |
| 698 | |
| 699 | export function renameSync(src: FilePath, dest: FilePath): void { |
| 700 | cffs.renameOrCopy(normalizePath(src), normalizePath(dest), { copy: false }); |
| 701 | } |
| 702 | |
| 703 | export 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 | |
| 709 | export 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 | |
| 719 | export 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 | |
| 740 | export 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 | |
| 773 | export 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 | |
| 783 | export function truncateSync(path: FilePath, len: number = 0): void { |
| 784 | validateUint32(len, 'len'); |
| 785 | cffs.truncate(normalizePath(path), len); |
| 786 | } |
| 787 | |
| 788 | export function unlinkSync(path: FilePath): void { |
| 789 | cffs.unlink(normalizePath(path)); |
| 790 | } |
| 791 | |
| 792 | export 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 | |
| 804 | export 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 | |
| 819 | export 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 | |
| 835 | export 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 | |
| 851 | export 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 | |
| 935 | export interface OpenAsBlobOptions { |
| 936 | type?: string | undefined; |
| 937 | } |
| 938 | export 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]) |