@poe-platform/safe-fs 0.1.70 → 0.1.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,6 +61,16 @@ export interface FsOptions {
61
61
  export interface ReadFileOptions extends FsOptions {
62
62
  readonly maxBytes?: number;
63
63
  }
64
+ export interface ReadDirectoryOptions extends FsOptions {
65
+ /**
66
+ * Optional per-listing entry admission limit, a nonnegative safe integer.
67
+ * Zero permits an empty listing; overflow rejects with EFBIG, never truncates.
68
+ * Omission preserves the adapter's existing limits and ordering. Composed
69
+ * adapters may conservatively count distinct candidates before visibility
70
+ * filtering. This is not a global traversal, host-allocation or work quota.
71
+ */
72
+ readonly maxEntries?: number;
73
+ }
64
74
  export interface WriteFileOptions extends FsOptions {
65
75
  readonly flag?: "w" | "wx" | "a" | "ax";
66
76
  readonly mode?: number;
@@ -93,7 +103,7 @@ export interface FileSystem {
93
103
  stat(path: string, options?: FsOptions): Promise<FileStat>;
94
104
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
95
105
  compareEntry?(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
96
- readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
106
+ readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
97
107
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
98
108
  rm(path: string, options?: RemoveOptions): Promise<void>;
99
109
  rmdir?(path: string, options?: FsOptions): Promise<void>;
@@ -0,0 +1,3 @@
1
+ import type { ReadDirectoryOptions } from "../contracts/filesystem.js";
2
+ export declare function directoryEntryLimit(options: ReadDirectoryOptions, path: string): number | undefined;
3
+ export declare function admitDirectoryEntries(count: number, limit: number | undefined, path: string): void;
@@ -0,0 +1,14 @@
1
+ import { FsError } from "../contracts/errors.js";
2
+ export function directoryEntryLimit(options, path) {
3
+ options.signal?.throwIfAborted();
4
+ const limit = options.maxEntries;
5
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
6
+ throw new FsError("EINVAL", { syscall: "readdir", path, message: "directory entry limit must be a nonnegative safe integer" });
7
+ }
8
+ return limit;
9
+ }
10
+ export function admitDirectoryEntries(count, limit, path) {
11
+ if (limit !== undefined && count > limit) {
12
+ throw new FsError("EFBIG", { syscall: "readdir", path, message: "directory entry limit exceeded" });
13
+ }
14
+ }
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  export declare class MemoryFileSystem implements FileSystem {
4
4
  readonly capabilities: Readonly<{
@@ -60,7 +60,7 @@ export declare class MemoryFileSystem implements FileSystem {
60
60
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
61
61
  stat(path: string, options?: FsOptions): Promise<FileStat>;
62
62
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
63
- readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
63
+ readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
64
64
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
65
65
  rmdir(path: string, options?: FsOptions): Promise<void>;
66
66
  rm(path: string, options?: RemoveOptions): Promise<void>;
@@ -2,6 +2,7 @@ import { FsError } from "../../contracts/errors.js";
2
2
  import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
3
3
  import { getOwnedS3Entry } from "../s3/registry.js";
4
4
  import { getOwnedWebDavEntry } from "../webdav/resource-id.js";
5
+ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
5
6
  const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
6
7
  const ownedStats = new WeakMap();
7
8
  const ownedStores = new WeakMap();
@@ -285,11 +286,12 @@ export class MemoryFileSystem {
285
286
  return stat;
286
287
  }
287
288
  async readdir(path, options = {}) {
288
- options.signal?.throwIfAborted();
289
+ const limit = directoryEntryLimit(options, path);
289
290
  const node = this.resolve(path, "readdir").node;
290
291
  if (node.type !== "directory")
291
292
  this.fail("ENOTDIR", "readdir", path);
292
293
  this.permission(node, 4, "readdir", path);
294
+ admitDirectoryEntries(node.entries.size, limit, path);
293
295
  node.atimeMs = Date.now();
294
296
  return [...node.entries].map(([name, entry]) => ({ name, type: entry.type }))
295
297
  .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  export interface MountFileSystemOptions {
4
4
  readonly root: FileSystem;
@@ -24,7 +24,7 @@ export declare class MountFileSystem implements FileSystem {
24
24
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
25
25
  stat(path: string, options?: FsOptions): Promise<FileStat>;
26
26
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
27
- readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
27
+ readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
28
28
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
29
29
  rmdir(path: string, options?: FsOptions): Promise<void>;
30
30
  rm(path: string, options?: RemoveOptions): Promise<void>;
@@ -2,6 +2,7 @@ import { FsError, isFsError, toFsError } from "../../contracts/errors.js";
2
2
  import { readBytes } from "../../contracts/io.js";
3
3
  import { finishCleanup } from "../../contracts/cleanup.js";
4
4
  import { readOnlyCapabilities } from "../capabilities.js";
5
+ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
5
6
  import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
6
7
  import { compareIdentity } from "./identity.js";
7
8
  import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
@@ -338,15 +339,22 @@ export class MountFileSystem {
338
339
  }
339
340
  readdir(path, options = {}) {
340
341
  return this.operation("readdir", path, options, async () => {
342
+ const limit = directoryEntryLimit(options, path);
341
343
  const location = await this.resolve(path, options);
342
344
  if (location.stat?.type !== "directory")
343
345
  fail("ENOTDIR");
344
346
  const entries = new Map();
345
347
  if (!location.synthetic) {
346
- for (const entry of await location.mount.backend.readdir(location.local, options)) {
348
+ const children = await location.mount.backend.readdir(location.local, options);
349
+ options.signal?.throwIfAborted();
350
+ admitDirectoryEntries(children.length, limit, path);
351
+ for (const entry of children) {
352
+ options.signal?.throwIfAborted();
347
353
  const { name, type } = entry;
348
354
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
349
355
  fail("EIO");
356
+ if (!entries.has(name))
357
+ admitDirectoryEntries(entries.size + 1, limit, path);
350
358
  entries.set(name, { name, type });
351
359
  }
352
360
  }
@@ -354,6 +362,8 @@ export class MountFileSystem {
354
362
  if (mount.path !== location.path && within(location.path, mount.path)) {
355
363
  const suffix = mount.path.slice(location.path === "/" ? 1 : location.path.length + 1);
356
364
  const name = suffix.split("/")[0];
365
+ if (!entries.has(name))
366
+ admitDirectoryEntries(entries.size + 1, limit, path);
357
367
  entries.set(name, { name, type: "directory" });
358
368
  }
359
369
  }
@@ -1,5 +1,5 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
3
  export interface OverlayFileSystemOptions {
4
4
  readonly upper: FileSystem;
5
5
  readonly lower: FileSystem;
@@ -49,7 +49,7 @@ export declare class OverlayFileSystem implements FileSystem {
49
49
  stat(path: string, options?: FsOptions): Promise<FileStat>;
50
50
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
51
51
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
52
- readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
52
+ readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
53
53
  realpath(path: string, options?: FsOptions): Promise<string>;
54
54
  access(path: string, requestedMode?: number, options?: FsOptions): Promise<void>;
55
55
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
@@ -6,6 +6,7 @@ import { FsError, toFsError } from "../../contracts/errors.js";
6
6
  import { dirname, isPathWithin, normalizePath, validatePath } from "../../contracts/virtual-path.js";
7
7
  import { compareIdentity } from "../mount/identity.js";
8
8
  import { compareEntries, registerEntryView } from "../mount/comparison.js";
9
+ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
9
10
  function snapshotStat(stat) {
10
11
  const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
11
12
  return {
@@ -343,21 +344,33 @@ export class OverlayFileSystem {
343
344
  async required(path, options, followFinal = true) {
344
345
  return (await this.resolve(path, options, followFinal)).entry;
345
346
  }
346
- async listing(entry, options) {
347
+ async listing(entry, options, limit) {
347
348
  if (entry.stat.type !== "directory")
348
349
  fail("ENOTDIR", entry.path);
349
350
  this.permission(entry, 4);
350
351
  const names = new Set();
352
+ const admit = (name) => {
353
+ options.signal?.throwIfAborted();
354
+ if (!names.has(name))
355
+ admitDirectoryEntries(names.size + 1, limit, entry.path);
356
+ names.add(name);
357
+ };
351
358
  const upper = await this.maybeStat(this.#upper, entry.path, options);
352
359
  if (upper?.type === "directory") {
353
- for (const child of await this.#upper.readdir(entry.path, options))
354
- names.add(child.name);
360
+ const children = await this.#upper.readdir(entry.path, options);
361
+ options.signal?.throwIfAborted();
362
+ admitDirectoryEntries(children.length, limit, entry.path);
363
+ for (const child of children)
364
+ admit(child.name);
355
365
  }
356
366
  if (await this.lowerVisible(entry.path, options) && !this.opaque.has(entry.path)) {
357
367
  const lower = await this.maybeStat(this.#lower, entry.path, options);
358
368
  if (lower?.type === "directory") {
359
- for (const child of await this.#lower.readdir(entry.path, options))
360
- names.add(child.name);
369
+ const children = await this.#lower.readdir(entry.path, options);
370
+ options.signal?.throwIfAborted();
371
+ admitDirectoryEntries(children.length, limit, entry.path);
372
+ for (const child of children)
373
+ admit(child.name);
361
374
  }
362
375
  }
363
376
  const entries = [];
@@ -572,7 +585,8 @@ export class OverlayFileSystem {
572
585
  return compareEntries(this, path, peer, peerPath, options);
573
586
  }
574
587
  async readdir(path, options = {}) {
575
- return this.run(options, async () => this.listing(await this.required(path, options), options), false);
588
+ const limit = directoryEntryLimit(options, path);
589
+ return this.run(options, async () => this.listing(await this.required(path, options), options, limit), false);
576
590
  }
577
591
  async realpath(path, options = {}) {
578
592
  return this.run(options, async () => (await this.required(path, options)).path, false);
@@ -1,5 +1,5 @@
1
1
  import type { ByteSource } from "../../contracts/io.js";
2
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
3
  export declare class ReadOnlyFileSystem implements FileSystem {
4
4
  #private;
5
5
  constructor(filesystem: FileSystem);
@@ -9,7 +9,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
9
9
  stat(path: string, options?: FsOptions): Promise<FileStat>;
10
10
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
11
11
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
12
- readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
12
+ readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
13
13
  realpath(path: string, options?: FsOptions): Promise<string>;
14
14
  access(path: string, mode?: number, options?: FsOptions): Promise<void>;
15
15
  readlink(path: string, options?: FsOptions): Promise<string>;
@@ -3,6 +3,7 @@ import { FsError } from "../../contracts/errors.js";
3
3
  import { readBytes } from "../../contracts/io.js";
4
4
  import { compareEntries, registerEntryView } from "../mount/comparison.js";
5
5
  import { readOnlyCapabilities } from "../capabilities.js";
6
+ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
7
  function readOnly(syscall, path, dest) {
7
8
  throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
8
9
  }
@@ -60,7 +61,12 @@ export class ReadOnlyFileSystem {
60
61
  return compareEntries(this, path, peer, peerPath, options);
61
62
  }
62
63
  async readdir(path, options) {
63
- return (await this.#filesystem.readdir(path, options)).map((entry) => ({ name: entry.name, type: entry.type }));
64
+ const limit = options?.maxEntries === undefined ? undefined : directoryEntryLimit(options, path);
65
+ const entries = await this.#filesystem.readdir(path, options);
66
+ if (limit !== undefined)
67
+ options?.signal?.throwIfAborted();
68
+ admitDirectoryEntries(entries.length, limit, path);
69
+ return entries.map((entry) => ({ name: entry.name, type: entry.type }));
64
70
  }
65
71
  async realpath(path, options) {
66
72
  return this.#filesystem.realpath(path, options);
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
1
+ import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
2
2
  export interface RealFileSystemOptions {
3
3
  /** An existing, absolute host directory. It is never created implicitly. */
4
4
  readonly root: string;
@@ -55,7 +55,7 @@ export declare class RealFileSystem implements FileSystem {
55
55
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
56
56
  stat(path: string, options?: FsOptions): Promise<FileStat>;
57
57
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
58
- readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
58
+ readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
59
59
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
60
60
  rmdir(path: string, options?: FsOptions): Promise<void>;
61
61
  rm(path: string, options?: RemoveOptions): Promise<void>;
@@ -3,6 +3,7 @@ import * as native from "node:fs/promises";
3
3
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { nativeAllocatedBytes } from "./allocation.js";
5
5
  import { finishCleanup } from "../../contracts/cleanup.js";
6
+ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
7
  import { FsError, collectBytes, isErrnoCode, toByteSource, toFsError, validatePath, } from "../../contracts/index.js";
7
8
  function fileType(stats) {
8
9
  if (stats.isFile())
@@ -273,11 +274,35 @@ export class RealFileSystem {
273
274
  });
274
275
  }
275
276
  async readdir(path, options = {}) {
277
+ const limit = directoryEntryLimit(options, path);
276
278
  return this.operation("readdir", path, options, async () => {
277
279
  const target = await this.path(path, options);
278
280
  options.signal?.throwIfAborted();
279
- const entries = await native.readdir(target, { withFileTypes: true });
280
- return entries.map((entry) => ({ name: entry.name, type: fileType(entry) }));
281
+ if (limit === undefined) {
282
+ const entries = await native.readdir(target, { withFileTypes: true });
283
+ return entries.map((entry) => ({ name: entry.name, type: fileType(entry) }));
284
+ }
285
+ const handle = await native.opendir(target, { bufferSize: 1 });
286
+ let failed = false;
287
+ try {
288
+ const entries = [];
289
+ while (true) {
290
+ options.signal?.throwIfAborted();
291
+ const entry = await handle.read();
292
+ options.signal?.throwIfAborted();
293
+ if (entry === null)
294
+ return entries;
295
+ admitDirectoryEntries(entries.length + 1, limit, path);
296
+ entries.push({ name: entry.name, type: fileType(entry) });
297
+ }
298
+ }
299
+ catch (error) {
300
+ failed = true;
301
+ throw error;
302
+ }
303
+ finally {
304
+ await finishCleanup(() => handle.close(), failed);
305
+ }
281
306
  });
282
307
  }
283
308
  async mkdir(path, options = {}) {
@@ -1,5 +1,5 @@
1
1
  import { FsError } from "../../contracts/errors.js";
2
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
3
  import type { ByteSource } from "../../contracts/io.js";
4
4
  import type { S3Transport } from "./transport.js";
5
5
  export interface S3FileSystemOptions {
@@ -93,7 +93,7 @@ export declare class S3FileSystem implements FileSystem {
93
93
  private writeMetadata;
94
94
  appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
95
95
  mkdir(input: string, options?: MkdirOptions): Promise<void>;
96
- readdir(input: string, options?: FsOptions): Promise<DirectoryEntry[]>;
96
+ readdir(input: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
97
97
  private tree;
98
98
  rmdir(input: string, options?: FsOptions): Promise<void>;
99
99
  rm(input: string, options?: RemoveOptions): Promise<void>;
@@ -5,6 +5,7 @@ import { collectBytes, readBytes } from "../../contracts/io.js";
5
5
  import { compareEntries, registerEntryAuthority } from "../mount/comparison.js";
6
6
  import { compareOwnedS3Entries, queryS3Head, recordS3Stat, registerS3EntryOwner } from "./authority.js";
7
7
  import { encodeCopySource } from "./transport.js";
8
+ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
8
9
  export class S3RenameError extends FsError {
9
10
  phase;
10
11
  copiedKeys;
@@ -495,20 +496,29 @@ export class S3FileSystem {
495
496
  }, this.requestOptions(options)), "EEXIST");
496
497
  }
497
498
  async readdir(input, options = {}) {
499
+ let limit;
500
+ if (options.maxEntries !== undefined) {
501
+ this.checkAbort(options, "readdir", input);
502
+ limit = directoryEntryLimit(options, input);
503
+ }
498
504
  const path = this.path(input);
499
505
  if ((await this.stat(input, options)).type !== "directory")
500
506
  fail("ENOTDIR", "readdir", path);
501
507
  const prefix = this.directoryKey(path);
502
508
  const entries = new Map();
503
509
  const add = (name, type) => {
510
+ this.checkAbort(options, "readdir", path);
504
511
  if (!name || name.includes("/"))
505
512
  fail("EIO", "readdir", path, "invalid direct child in delimited listing");
506
513
  const existing = entries.get(name);
507
514
  if (existing && existing.type !== type)
508
515
  this.unsupported("file/prefix collisions", path);
516
+ if (!existing)
517
+ admitDirectoryEntries(entries.size + 1, limit, path);
509
518
  entries.set(name, { name, type });
510
519
  };
511
- for await (const page of this.pages(prefix, path, options, "/")) {
520
+ const maxKeys = limit === undefined ? this.pageSize : Math.min(this.pageSize, limit + 1);
521
+ for await (const page of this.pages(prefix, path, options, "/", maxKeys)) {
512
522
  for (const item of page.Contents ?? []) {
513
523
  if (item.Key === prefix)
514
524
  continue;
@@ -1,6 +1,6 @@
1
1
  import type { PlatformComparisonCallback } from "#safe-fs-platform";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
3
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
4
4
  export type WebDavFetch = (url: string, init: RequestInit) => Promise<Response>;
5
5
  export interface WebDavAtomicEmptyDirectoryRequest {
6
6
  readonly operation: "atomic-empty-rmdir/v1";
@@ -100,7 +100,7 @@ export declare class WebDavFileSystem implements FileSystem {
100
100
  stat(path: string, options?: FsOptions): Promise<FileStat>;
101
101
  private statWithAncestors;
102
102
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
103
- readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
103
+ readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
104
104
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
105
105
  readStream(path: string, options?: ReadStreamOptions): AsyncGenerator<Uint8Array>;
106
106
  private prepareWrite;
@@ -2,6 +2,7 @@ import { FsError, isErrnoCode, isFsError } from "../../contracts/errors.js";
2
2
  import { composeAbortSignals } from "../../contracts/abort.js";
3
3
  import { readBytes } from "../../contracts/io.js";
4
4
  import { davChild, davChildren, parseXml, scalar } from "./xml.js";
5
+ import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
5
6
  import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
6
7
  import { compareWebDavResources, ownedResponseIdentifier, recordOwnedResourceStat, registerResourceQuery, resourceIdentifier } from "./resource-id.js";
7
8
  const timestampNamespace = "urn:virtual-bash:metadata";
@@ -616,14 +617,16 @@ export class WebDavFileSystem {
616
617
  this.etags.set(stat, validator);
617
618
  return stat;
618
619
  }
619
- async entries(path, depth, options, collection = depth === "1") {
620
+ async entries(path, depth, options, collection = depth === "1", limit) {
620
621
  return this.request("PROPFIND", path, options, {
621
622
  headers: { Depth: depth, "Content-Type": "application/xml; charset=utf-8" }, body: propfindBody,
622
623
  }, async (response, signal) => {
623
624
  if (response.status !== 207)
624
625
  this.httpError(response.status, "PROPFIND", path);
625
626
  const result = new Map();
627
+ let children = 0;
626
628
  for (const element of await this.multistatus(response, signal)) {
629
+ signal.throwIfAborted();
627
630
  const href = davChild(element, "href");
628
631
  if (!href)
629
632
  throw new Error("missing DAV:href");
@@ -634,6 +637,8 @@ export class WebDavFileSystem {
634
637
  }
635
638
  if (result.has(member))
636
639
  throw new Error("duplicate response href");
640
+ if (member !== path)
641
+ admitDirectoryEntries(++children, limit, path);
637
642
  const stat = this.entryStat(element, member);
638
643
  recordOwnedResourceStat(response, this, member, stat);
639
644
  result.set(member, stat);
@@ -730,8 +735,14 @@ export class WebDavFileSystem {
730
735
  return this.stat(path, options);
731
736
  }
732
737
  async readdir(path, options = {}) {
738
+ let limit;
739
+ if (options.maxEntries !== undefined) {
740
+ if (options.signal?.aborted)
741
+ fail("ECANCELED", "readdir", path);
742
+ limit = directoryEntryLimit(options, path);
743
+ }
733
744
  const normalized = normalize(path);
734
- const entries = await this.entries(normalized, "1", options);
745
+ const entries = await this.entries(normalized, "1", options, true, limit);
735
746
  if (entries.get(normalized).type !== "directory")
736
747
  fail("ENOTDIR", "readdir", path);
737
748
  return [...entries].filter(([member]) => member !== normalized)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.70",
3
+ "version": "0.1.72",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",