@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 +19 -10
- package/ConfineReal.js +123 -0
- package/Descend.js +60 -126
- package/DescendSync.js +79 -0
- package/Walker.js +193 -38
- package/index.d.ts +591 -19
- package/index.js +4 -2
- package/internal/absolute.js +19 -0
- package/internal/descendMachine.js +282 -0
- package/internal/trimTrailing.js +13 -0
- package/internal/upwardDrivers.js +71 -0
- package/internal/upwardMachine.js +239 -0
- package/node-sync.d.ts +102 -0
- package/node-sync.js +61 -0
- package/package.json +7 -2
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
29
|
-
const DEFAULT_PRUNE = ["node_modules", ".git"];
|
|
28
|
+
const kindOf = (info) => info.type === "File" ? "File" : info.type === "Directory" ? "Directory" : "Other";
|
|
30
29
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 };
|