@effected/walker 0.20.1 → 0.22.0

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.
package/Confine.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { resolveSegments } from "./internal/segments.js";
2
+ import { trimTrailing } from "./internal/trimTrailing.js";
2
3
  import { Effect, Result, Schema } from "effect";
3
4
 
4
5
  // src/Confine.ts
@@ -22,6 +23,12 @@ import { Effect, Result, Schema } from "effect";
22
23
  * reserved device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`,
23
24
  * `LPT1`–`LPT9`), in any case and with or without an extension
24
25
  * (`nul.txt`).
26
+ * - `outside` — `confineReal` only: the candidate's real path, links
27
+ * resolved, lies outside the base's real path.
28
+ * - `unresolvable` — `confineReal` only: resolving the candidate or the base
29
+ * failed for a reason other than absence (a permission error, a link loop,
30
+ * a file used as a directory, a dangling link), so containment cannot be
31
+ * verified. The failure rides in the error's `cause`.
25
32
  *
26
33
  * @public
27
34
  */
@@ -30,7 +37,9 @@ const PathEscapeReason = Schema.Literals([
30
37
  "absolute",
31
38
  "traversal",
32
39
  "empty",
33
- "reservedName"
40
+ "reservedName",
41
+ "outside",
42
+ "unresolvable"
34
43
  ]);
35
44
  /**
36
45
  * An untrusted relative path would leave, or does not name anything under,
@@ -49,7 +58,9 @@ var PathEscapeError = class extends Schema.TaggedError()("PathEscapeError", {
49
58
  /** The candidate path, exactly as the caller passed it. */
50
59
  candidate: Schema.String,
51
60
  /** Why the candidate was refused. */
52
- reason: PathEscapeReason
61
+ reason: PathEscapeReason,
62
+ /** The resolution failure, present when `reason` is `"unresolvable"`. */
63
+ cause: Schema.optionalKey(Schema.Defect())
53
64
  }) {
54
65
  get message() {
55
66
  return `the path ${JSON.stringify(this.candidate)} ${REASON_TEXT[this.reason]}`;
@@ -61,7 +72,9 @@ const REASON_TEXT = {
61
72
  absolute: "is absolute, so it is not confined to the base directory",
62
73
  traversal: "climbs above the base directory",
63
74
  empty: "names the base directory itself, not an entry under it",
64
- reservedName: "has a segment win32 reserves or reads as something else"
75
+ reservedName: "has a segment win32 reserves or reads as something else",
76
+ outside: "resolves, links followed, to a path outside the base directory",
77
+ unresolvable: "could not be resolved, so its containment cannot be verified"
65
78
  };
66
79
  /** A win32 drive prefix: `C:\x`, `C:/x`, and the drive-relative `C:x`. */
67
80
  const DRIVE_PREFIX = /^[A-Za-z]:/;
@@ -71,12 +84,8 @@ const DOTS_AND_SPACES = /^[. ]+$/;
71
84
  const DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9\u00b9\u00b2\u00b3]|lpt[0-9\u00b9\u00b2\u00b3]|conin\$|conout\$) *(?:\..*)?$/i;
72
85
  /** Whether win32 reads a normalised segment as something other than a plain entry. */
73
86
  const isReservedSegment = (segment) => segment.includes(":") || DOTS_AND_SPACES.test(segment) || DEVICE_NAME.test(segment);
74
- /** `path` without trailing `/` or `\`: a loop, because `/[\\/]+$/` backtracks quadratically on a long separator run. */
75
- const stripTrailingSeparators = (path) => {
76
- let end = path.length;
77
- while (end > 0 && (path[end - 1] === "/" || path[end - 1] === "\\")) end -= 1;
78
- return path.slice(0, end);
79
- };
87
+ /** `path` without trailing `/` or `\`, by a loop: `/[\\/]+$/` backtracks quadratically on a long separator run. */
88
+ const stripTrailingSeparators = (path) => trimTrailing(path, (character) => character === "/" || character === "\\");
80
89
  /**
81
90
  * Confine an untrusted relative path to a base directory, synchronously.
82
91
  *
@@ -107,7 +116,7 @@ const stripTrailingSeparators = (path) => {
107
116
  * base yields the normalised relative path alone. The check is lexical, not
108
117
  * physical, and symlinks are out of scope: a symlink already inside the
109
118
  * base can still point out of it, so a caller extracting into a directory
110
- * it does not own resolves links separately.
119
+ * it does not own checks the result with `confineReal` as well.
111
120
  *
112
121
  * {@link confineRelative} is defined in terms of this function and adds
113
122
  * only the tracing span.
package/ConfineReal.js ADDED
@@ -0,0 +1,123 @@
1
+ import { PathEscapeError } from "./Confine.js";
2
+ import { runUpward, runUpwardSync } from "./internal/upwardDrivers.js";
3
+ import { confineRealMachine } from "./internal/upwardMachine.js";
4
+ import { Effect, Path, Result } from "effect";
5
+
6
+ // src/ConfineReal.ts
7
+ /** The typed error a refusal describes — the one conversion both forms share. */
8
+ const toResult = (candidate, outcome) => {
9
+ if (outcome._tag === "Ok") return Result.succeed(outcome.value);
10
+ if (outcome._tag === "Unresolvable") return Result.fail(new PathEscapeError({
11
+ candidate,
12
+ reason: "unresolvable",
13
+ cause: outcome.cause
14
+ }));
15
+ return Result.fail(new PathEscapeError({
16
+ candidate,
17
+ reason: outcome.reason
18
+ }));
19
+ };
20
+ /**
21
+ * Confine a path to a base directory PHYSICALLY: resolve links in both, and
22
+ * succeed with the candidate's real path only when it lies strictly inside
23
+ * the base's real path.
24
+ *
25
+ * @remarks
26
+ * Reach for it where `confineRelative` stops: a link inside the base that
27
+ * points out of it (a symlinked `dist`, a planted `node_modules` entry), an
28
+ * absolute path an agent or a tool handed over, a `descend` match — a
29
+ * matched link is answered even when its target lies outside `cwd`.
30
+ *
31
+ * The candidate may be absolute (checked as it is) or relative (taken
32
+ * against `base`). It is resolved the way the operating system resolves it:
33
+ * segment by segment, so a `..` after a link climbs out of the link's
34
+ * TARGET — `base/link/../x` is outside when `link` points outside — rather
35
+ * than being collapsed lexically first, as `path.resolve` and node's JS
36
+ * `fs.realpath` would. A path, or the tail of one, that does not exist yet
37
+ * is resolved lexically from its real existing ancestor, so a write target
38
+ * can be checked before it is created — but only once absence is proved: a
39
+ * segment whose real path is not found is checked again without following
40
+ * links, because a dangling link is "not found" too, and a write through one
41
+ * creates its target wherever it points. The base is resolved the same way.
42
+ *
43
+ * Refused, typed as {@link PathEscapeError}: a NUL character (`nulByte`);
44
+ * the empty string or any path whose real location IS the base (`empty`,
45
+ * the same ruling `confineRelative` makes — a caller confining a path names
46
+ * an entry under the base); on win32 a drive-relative `C:x` or a driveless
47
+ * rooted `\x`, which names the current drive (`absolute`), or a segment
48
+ * win32 reads as something else, such as a device name or `.. `
49
+ * (`reservedName`); a real path outside the base's (`outside`); and a
50
+ * resolution that fails for a reason other than absence, such as a
51
+ * permission error, a link loop, or a dangling link — a link whose target
52
+ * does not exist, refused even when that target would lie inside the base
53
+ * (`unresolvable`, with the failure in `cause`) — containment that cannot be
54
+ * verified is refused, never assumed.
55
+ *
56
+ * A relative `base` — the empty string included — is a **defect**: it would
57
+ * be resolved against the process working directory, the silent wrong-root
58
+ * walk `ascend` refuses for a relative `stopAt`. Absoluteness is judged by
59
+ * the injected `Path`, so win32 accepts `C:\repo` and `\\server\share`;
60
+ * a win32 `\repo`, rooted on whichever drive the process is on, is a defect
61
+ * too.
62
+ *
63
+ * Use the returned path, not the candidate: it is the location that was
64
+ * checked. Comparison is exact on the real paths, so on a case-insensitive
65
+ * filesystem a base and a candidate spelled in different case may be refused
66
+ * — the check fails closed. As with any check-then-use on a live filesystem,
67
+ * a link swapped in between the check and the use is not covered.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { confineReal } from "@effected/walker";
72
+ * import { Effect } from "effect";
73
+ *
74
+ * const target = confineReal("/repo", "packages/app/dist").pipe(
75
+ * Effect.catchTag("PathEscapeError", (error) => Effect.die(error)),
76
+ * );
77
+ * // Requires `FileSystem` and `Path` from the platform layer.
78
+ * ```
79
+ *
80
+ * @param base - The absolute directory the candidate must stay under.
81
+ * @param candidate - The path to check: absolute, or relative to `base`.
82
+ * @returns An `Effect` succeeding with the candidate's real path, or failing
83
+ * with {@link PathEscapeError}.
84
+ *
85
+ * @public
86
+ */
87
+ const confineReal = Effect.fn("Walker.confineReal")(function* (base, candidate) {
88
+ const path = yield* Path.Path;
89
+ const outcome = yield* runUpward(confineRealMachine("Walker.confineReal", base, candidate, path));
90
+ return yield* Effect.fromResult(toResult(candidate, outcome));
91
+ });
92
+ /**
93
+ * {@link confineReal}, synchronously, over a caller-supplied port.
94
+ *
95
+ * @remarks
96
+ * The same resolution from the same code, through the port's `realPath`; a
97
+ * `realPath` throwing `code: "ENOENT"` is absence only when the port's
98
+ * `lstat` throws `ENOENT` too (otherwise a dangling link is there); anything
99
+ * else is `unresolvable`, with the thrown value as the `cause`. A relative `base`
100
+ * throws. For a caller with no Effect runtime; inside Effect, use
101
+ * `confineReal`.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * import { confineRealSync } from "@effected/walker";
106
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
107
+ * import { Result } from "effect";
108
+ *
109
+ * const checked = confineRealSync("/repo", "/repo/packages/app/dist", nodeWalkerSyncOps);
110
+ * if (Result.isFailure(checked)) console.error(checked.failure.message);
111
+ * ```
112
+ *
113
+ * @param base - The absolute directory the candidate must stay under.
114
+ * @param candidate - The path to check: absolute, or relative to `base`.
115
+ * @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
116
+ * @returns A `Result` succeeding with the candidate's real path, or failing
117
+ * with {@link PathEscapeError}.
118
+ *
119
+ * @public
120
+ */
121
+ const confineRealSync = (base, candidate, ops) => toResult(candidate, runUpwardSync(confineRealMachine("Walker.confineRealSync", base, candidate, ops.path), ops.fileSystem));
122
+
123
+ export { confineReal, confineRealSync };
package/Descend.js CHANGED
@@ -1,4 +1,4 @@
1
- import { resolveSegments } from "./internal/segments.js";
1
+ import { descendMachine, failureOf } from "./internal/descendMachine.js";
2
2
  import { Effect, FileSystem, Path, Schema } from "effect";
3
3
 
4
4
  // src/Descend.ts
@@ -25,140 +25,74 @@ var DescendError = class extends Schema.TaggedError()("DescendError", {
25
25
  return this.reason === "depthExceeded" ? `glob descent for ${JSON.stringify(this.pattern)} descended past ${this.limit ?? "the depth cap"} levels below ${where}` : `glob descent for ${JSON.stringify(this.pattern)} could not read ${where}`;
26
26
  }
27
27
  };
28
- /** Directory names never descended into unless the caller overrides `prune`. */
29
- const DEFAULT_PRUNE = ["node_modules", ".git"];
28
+ const kindOf = (info) => info.type === "File" ? "File" : info.type === "Directory" ? "Directory" : "Other";
30
29
  /**
31
- * Whether a cwd-relative pattern path lexically climbs above `cwd` via `..`
32
- * segments. Walked paths never contain `..`, so such a pattern can never match
33
- * one — the answer is zero matches, and no filesystem access outside `cwd`
34
- * ever happens (a pattern must not enumerate the tree above its documented
35
- * root).
30
+ * The `Effect` driver of the shared walk (`internal/descendMachine.ts`): each
31
+ * request the walk yields is answered through core's `FileSystem`, joins
32
+ * through `Path`. Every rule lives in the machine; the sync driver in
33
+ * `DescendSync.ts` answers the same requests from a synchronous port.
36
34
  */
37
- const escapesCwd = (relative) => resolveSegments(relative.split("/")) === void 0;
38
- /** Shared empty ancestor chain: frames under `followSymlinks: false` never consult theirs. */
39
- const NO_ANCESTORS = Object.freeze([]);
40
- /** Not exported — `descend`'s overloads below carry the public documentation. */
41
- const descendImpl = Effect.fn("Walker.descend")(function* (pattern, options) {
35
+ const walk = (pattern, options) => Effect.gen(function* () {
42
36
  const fs = yield* FileSystem.FileSystem;
43
37
  const path = yield* Path.Path;
44
- const maxDepth = options.maxDepth ?? 256;
45
- if (!Number.isInteger(maxDepth) || maxDepth < 1) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.descend: maxDepth must be a positive integer, received ${maxDepth}`));
46
- const prune = new Set(options.prune ?? DEFAULT_PRUNE);
47
- const onUnreadable = options.onUnreadable ?? "fail";
48
- const followSymlinks = options.followSymlinks ?? false;
49
- const unreadable = [];
50
- /** The stat-resolved type of `absolute`, or `undefined` when it does not resolve (missing, dangling symlink, unstatable). */
51
- const typeOf = (absolute) => fs.stat(absolute).pipe(Effect.map((info) => info.type), Effect.orElseSucceed(() => void 0));
52
- /** Whether `absolute` is itself a symlink. `readLink` succeeds only on links; any failure means "not one". */
53
- const isSymbolicLink = (absolute) => fs.readLink(absolute).pipe(Effect.map(() => true), Effect.orElseSucceed(() => false));
54
- /**
55
- * The real path of a directory the cycle guard must identify — the walk
56
- * base, or a symlinked directory — or `undefined` when the walk must not
57
- * enter it. A link the resolver cannot resolve is one the guard cannot
58
- * reason about: recording a stand-in (the link's own path) would make
59
- * every hop around a loop look unseen, so the subtree is never queued.
60
- * Whether the caller HEARS about it follows `onUnreadable` exactly as a
61
- * failed `readDirectory` does: a `NotFound` is the benign vanished-target
62
- * race (the subtree would have read as empty anyway) and stays silent in
63
- * every mode; anything else — `EACCES` on a path component, above all —
64
- * is a subtree this walk was asked to enumerate and cannot, so the
65
- * default fails typed, `"record"` keeps the path and the cause, and
66
- * `"skip"` forgets it. A silent skip here would hand a caller a smaller
67
- * answer with nothing reporting it, which is the failure `"fail"` exists
68
- * to prevent.
69
- */
70
- const realPathOf = (absolute, relative) => fs.realPath(absolute).pipe(Effect.map((real) => real), Effect.catch((error) => {
71
- if (error.reason._tag === "NotFound") return Effect.succeed(void 0);
72
- if (onUnreadable === "fail") return Effect.fail(new DescendError({
73
- pattern: pattern.source,
74
- reason: "unreadableDirectory",
75
- path: relative
76
- }));
77
- if (onUnreadable === "record") unreadable.push({
78
- path: relative,
79
- cause: error
80
- });
81
- return Effect.succeed(void 0);
82
- }));
83
- /** Wrap the walk's success value per `onUnreadable`: a plain array unless "record" asked for the pair. */
84
- const finish = (matches) => onUnreadable === "record" ? {
38
+ const read = (effect) => effect.pipe(Effect.map((value) => ({
39
+ _tag: "Ok",
40
+ value
41
+ })), Effect.catch((error) => Effect.succeed(error.reason._tag === "NotFound" ? { _tag: "NotFound" } : {
42
+ _tag: "Failed",
43
+ cause: error
44
+ })));
45
+ const answer = (request) => {
46
+ switch (request._tag) {
47
+ case "Stat": return fs.stat(request.path).pipe(Effect.map((info) => ({
48
+ kind: kindOf(info),
49
+ info
50
+ })), Effect.orElseSucceed(() => void 0));
51
+ case "Lstat": return fs.lstat(request.path).pipe(Effect.map((info) => info.type === "SymbolicLink" ? { kind: "SymbolicLink" } : {
52
+ kind: kindOf(info),
53
+ info
54
+ }), Effect.orElseSucceed(() => void 0));
55
+ case "RealPath": return read(fs.realPath(request.path));
56
+ case "ReadDirectory": return read(fs.readDirectory(request.path));
57
+ }
58
+ };
59
+ const machine = descendMachine(pattern, options, {
60
+ sep: path.sep,
61
+ join: (a, b) => path.join(a, b),
62
+ isAbsolute: (p) => path.isAbsolute(p)
63
+ });
64
+ let step = machine.next();
65
+ while (step.done !== true) step = machine.next(yield* answer(step.value));
66
+ const result = step.value;
67
+ if (result._tag === "Defect") return yield* Effect.die(new Error(result.message));
68
+ if (result._tag === "Fail") return yield* new DescendError(failureOf(pattern, result));
69
+ return {
70
+ entries: result.entries,
71
+ unreadable: result.unreadable
72
+ };
73
+ });
74
+ /** Not exported — `descend`'s overloads below carry the public documentation. */
75
+ const descendImpl = Effect.fn("Walker.descend")(function* (pattern, options) {
76
+ const { entries, unreadable } = yield* walk(pattern, options);
77
+ const matches = entries.map((entry) => entry.path);
78
+ return options.onUnreadable === "record" ? {
85
79
  matches,
86
80
  unreadable
87
81
  } : matches;
88
- if (!pattern.hasMagic && !pattern.negated) {
89
- if (escapesCwd(pattern.source)) return finish([]);
90
- return finish((yield* typeOf(path.join(options.cwd, pattern.source))) === "File" ? [pattern.source] : []);
91
- }
92
- const base = pattern.negated ? "" : pattern.enumerationPrefix.replace(/\/+$/, "");
93
- if (escapesCwd(base)) return finish([]);
94
- const absoluteBase = base === "" ? options.cwd : path.join(options.cwd, base);
95
- if ((yield* typeOf(absoluteBase)) !== "Directory") return finish([]);
96
- let baseAncestors = NO_ANCESTORS;
97
- if (followSymlinks) {
98
- const real = yield* realPathOf(absoluteBase, base);
99
- if (real === void 0) return finish([]);
100
- baseAncestors = [real];
101
- }
102
- const deep = pattern.crossesSegments || pattern.negated;
103
- const results = [];
104
- const frames = [{
105
- relative: base,
106
- absolute: absoluteBase,
107
- depth: 0,
108
- ancestors: baseAncestors
109
- }];
110
- for (let head = 0; head < frames.length; head += 1) {
111
- const frame = frames[head];
112
- if (frame === void 0) break;
113
- const entries = yield* fs.readDirectory(frame.absolute).pipe(Effect.catch((error) => {
114
- if (error.reason._tag === "NotFound") return Effect.succeed([]);
115
- if (onUnreadable === "fail") return Effect.fail(new DescendError({
116
- pattern: pattern.source,
117
- reason: "unreadableDirectory",
118
- path: frame.relative
119
- }));
120
- if (onUnreadable === "record") unreadable.push({
121
- path: frame.relative,
122
- cause: error
123
- });
124
- return Effect.succeed([]);
125
- }));
126
- for (const entry of entries) {
127
- const relative = frame.relative === "" ? entry : `${frame.relative}/${entry}`;
128
- const absolute = path.join(frame.absolute, entry);
129
- const kind = yield* typeOf(absolute);
130
- if (kind === "File") {
131
- if (pattern.matches(relative)) results.push(relative);
132
- continue;
133
- }
134
- if (kind !== "Directory" || !deep) continue;
135
- if (prune.has(entry)) continue;
136
- let ancestors = frame.ancestors;
137
- if (followSymlinks) {
138
- const parentReal = frame.ancestors[frame.ancestors.length - 1] ?? frame.absolute;
139
- const real = (yield* isSymbolicLink(absolute)) ? yield* realPathOf(absolute, relative) : path.join(parentReal, entry);
140
- if (real === void 0 || frame.ancestors.includes(real)) continue;
141
- ancestors = [...frame.ancestors, real];
142
- } else if (yield* isSymbolicLink(absolute)) continue;
143
- if (frame.depth + 1 > maxDepth) return yield* new DescendError({
144
- pattern: pattern.source,
145
- reason: "depthExceeded",
146
- path: frame.relative,
147
- limit: maxDepth
148
- });
149
- frames.push({
150
- relative,
151
- absolute,
152
- depth: frame.depth + 1,
153
- ancestors
154
- });
155
- }
156
- }
157
- results.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
158
- return finish(results);
82
+ });
83
+ /** Not exported — `descendEntries`' overloads below carry the public documentation. */
84
+ const descendEntriesImpl = Effect.fn("Walker.descendEntries")(function* (pattern, options) {
85
+ const { entries, unreadable } = yield* walk(pattern, options);
86
+ return options.onUnreadable === "record" ? {
87
+ matches: entries,
88
+ unreadable
89
+ } : entries;
159
90
  });
160
91
  function descend(pattern, options) {
161
92
  return descendImpl(pattern, options);
162
93
  }
94
+ function descendEntries(pattern, options) {
95
+ return descendEntriesImpl(pattern, options);
96
+ }
163
97
 
164
- export { DescendError, descend };
98
+ export { DescendError, descend, descendEntries };
package/DescendSync.js ADDED
@@ -0,0 +1,79 @@
1
+ import { descendMachine, failureOf } from "./internal/descendMachine.js";
2
+ import { DescendError } from "./Descend.js";
3
+ import { Result } from "effect";
4
+
5
+ // src/DescendSync.ts
6
+ const isNotFound = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
7
+ const kindOf = (stats) => stats.isFile() ? "File" : stats.isDirectory() ? "Directory" : "Other";
8
+ /** The sync driver of the shared walk. Throws only for a defect (an invalid `maxDepth`). */
9
+ const walkSync = (pattern, options) => {
10
+ const { fileSystem, path } = options;
11
+ const read = (f) => {
12
+ try {
13
+ return {
14
+ _tag: "Ok",
15
+ value: f()
16
+ };
17
+ } catch (cause) {
18
+ return isNotFound(cause) ? { _tag: "NotFound" } : {
19
+ _tag: "Failed",
20
+ cause
21
+ };
22
+ }
23
+ };
24
+ const answer = (request) => {
25
+ switch (request._tag) {
26
+ case "Stat": try {
27
+ const info = fileSystem.stat(request.path);
28
+ return {
29
+ kind: kindOf(info),
30
+ info
31
+ };
32
+ } catch {
33
+ return;
34
+ }
35
+ case "Lstat": try {
36
+ const info = fileSystem.lstat(request.path);
37
+ return info.isSymbolicLink() ? { kind: "SymbolicLink" } : {
38
+ kind: kindOf(info),
39
+ info
40
+ };
41
+ } catch {
42
+ return;
43
+ }
44
+ case "RealPath": return read(() => fileSystem.realPath(request.path));
45
+ case "ReadDirectory": return read(() => fileSystem.readDirectory(request.path));
46
+ }
47
+ };
48
+ const machine = descendMachine(pattern, options, {
49
+ sep: path.sep,
50
+ join: (a, b) => path.join(a, b),
51
+ isAbsolute: (p) => path.isAbsolute(p)
52
+ });
53
+ let step = machine.next();
54
+ while (step.done !== true) step = machine.next(answer(step.value));
55
+ const result = step.value;
56
+ if (result._tag === "Defect") throw new Error(result.message);
57
+ if (result._tag === "Fail") return Result.fail(new DescendError(failureOf(pattern, result)));
58
+ return Result.succeed({
59
+ entries: result.entries,
60
+ unreadable: result.unreadable
61
+ });
62
+ };
63
+ function descendSync(pattern, options) {
64
+ return Result.map(walkSync(pattern, options), ({ entries, unreadable }) => {
65
+ const matches = entries.map((entry) => entry.path);
66
+ return options.onUnreadable === "record" ? {
67
+ matches,
68
+ unreadable
69
+ } : matches;
70
+ });
71
+ }
72
+ function descendEntriesSync(pattern, options) {
73
+ return Result.map(walkSync(pattern, options), ({ entries, unreadable }) => options.onUnreadable === "record" ? {
74
+ matches: entries,
75
+ unreadable
76
+ } : entries);
77
+ }
78
+
79
+ export { descendEntriesSync, descendSync };