File
Blob: src/node/internal/internal_fs_utils.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 | |
| 26 | import { |
| 27 | ERR_BUFFER_OUT_OF_BOUNDS, |
| 28 | ERR_INVALID_ARG_TYPE, |
| 29 | ERR_INVALID_ARG_VALUE, |
| 30 | } from 'node-internal:internal_errors'; |
| 31 | import { |
| 32 | validateAbortSignal, |
| 33 | validateObject, |
| 34 | validateBoolean, |
| 35 | validateInteger, |
| 36 | validateInt32, |
| 37 | validateUint32, |
| 38 | validateEncoding, |
| 39 | parseFileMode, |
| 40 | } from 'node-internal:validators'; |
| 41 | import { isArrayBufferView } from 'node-internal:internal_types'; |
| 42 | import { |
| 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 | |
| 68 | import { strictEqual } from 'node-internal:internal_assert'; |
| 69 | |
| 70 | import { Buffer } from 'node-internal:internal_buffer'; |
| 71 | import processImpl from 'node-internal:process'; |
| 72 | export type FilePath = string | URL | Buffer; |
| 73 | |
| 74 | import 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 |
| 85 | export 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 | |
| 94 | export type ValidEncoding = BufferEncoding | 'buffer' | null; |
| 95 | |
| 96 | import 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 |
| 100 | export const kBadge = Symbol('kBadge'); |
| 101 | export const kFileHandle = Symbol('kFileHandle'); |
| 102 | |
| 103 | export function isFileHandle(object: unknown): boolean { |
| 104 | if (typeof object !== 'object' || object === null) return false; |
| 105 | return Reflect.has(object, kFileHandle); |
| 106 | } |
| 107 | |
| 108 | export type RawTime = string | number | bigint; |
| 109 | export type SymlinkType = 'dir' | 'file' | 'junction' | null | undefined; |
| 110 | |
| 111 | // Normalizes the input time to a Date object. |
| 112 | export 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. |
| 130 | export 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 | |
| 175 | export 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. |
| 182 | export 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 | |
| 192 | export 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 | |
| 213 | export 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 | } |
| 241 | export 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 | |
| 258 | export 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 | |
| 282 | export 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 | |
| 306 | export 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. |
| 325 | export type ReadDirOptions = { |
| 326 | encoding?: ValidEncoding | undefined; |
| 327 | withFileTypes?: boolean | undefined; |
| 328 | recursive?: boolean | undefined; |
| 329 | }; |
| 330 | |
| 331 | export 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 | |
| 362 | export 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 | |
| 387 | export type WriteSyncOptions = { |
| 388 | offset?: number | undefined; |
| 389 | length?: number | undefined; |
| 390 | position?: Position | undefined; |
| 391 | }; |
| 392 | |
| 393 | export 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 | |
| 464 | export 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 | |
| 535 | export 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. |
| 617 | function 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 | |
| 647 | function 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 | |
| 658 | export 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 | |
| 683 | export 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 | |
| 742 | export type Position = number | null | bigint; |
| 743 | |
| 744 | export 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 | |
| 759 | export 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 | |
| 769 | export 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. |
| 790 | export 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 | } |