@effected/walker 0.22.0 → 0.24.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/ConfineReal.js +62 -2
- package/README.md +2 -2
- package/index.d.ts +67 -1
- package/index.js +2 -2
- package/internal/upwardMachine.js +16 -3
- package/package.json +3 -3
- package/tsdoc-metadata.json +1 -1
package/ConfineReal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PathEscapeError } from "./Confine.js";
|
|
2
2
|
import { runUpward, runUpwardSync } from "./internal/upwardDrivers.js";
|
|
3
3
|
import { confineRealMachine } from "./internal/upwardMachine.js";
|
|
4
|
-
import { Effect, Path, Result } from "effect";
|
|
4
|
+
import { Effect, Option, Path, Result } from "effect";
|
|
5
5
|
|
|
6
6
|
// src/ConfineReal.ts
|
|
7
7
|
/** The typed error a refusal describes — the one conversion both forms share. */
|
|
@@ -119,5 +119,65 @@ const confineReal = Effect.fn("Walker.confineReal")(function* (base, candidate)
|
|
|
119
119
|
* @public
|
|
120
120
|
*/
|
|
121
121
|
const confineRealSync = (base, candidate, ops) => toResult(candidate, runUpwardSync(confineRealMachine("Walker.confineRealSync", base, candidate, ops.path), ops.fileSystem));
|
|
122
|
+
const within = (options) => ({ requireExisting: options?.missing === "refuse" });
|
|
123
|
+
/**
|
|
124
|
+
* The posix, base-relative path of an absolute path that lies inside a base
|
|
125
|
+
* directory PHYSICALLY, or `None` when it does not.
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* The question `confineReal` does not answer: where, under `base`, does this
|
|
129
|
+
* absolute path sit? It resolves links the way `confineReal` does, and a path
|
|
130
|
+
* or tail that does not exist yet is resolved lexically from its deepest
|
|
131
|
+
* existing ancestor, so a destination can be placed before it is created.
|
|
132
|
+
* The base itself answers `""`. `None` covers every refusal: a relative or
|
|
133
|
+
* NUL-bearing candidate, a real location outside the base, and a resolution
|
|
134
|
+
* that cannot be verified (a permission error, a loop, a dangling link) —
|
|
135
|
+
* containment that cannot be proved is never assumed. A relative `base` is a
|
|
136
|
+
* **defect**, as it is for `confineReal`.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* import { relativeWithin } from "@effected/walker";
|
|
141
|
+
* import { Effect } from "effect";
|
|
142
|
+
*
|
|
143
|
+
* const inside = relativeWithin("/repo", "/repo/docs/new.md");
|
|
144
|
+
* // Requires `FileSystem` and `Path`; succeeds with Option.some("docs/new.md").
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @param base - The absolute directory the path must stay inside.
|
|
148
|
+
* @param absolute - The absolute path to place under `base`.
|
|
149
|
+
* @param options - See {@link RelativeWithinOptions}.
|
|
150
|
+
* @returns An `Effect` succeeding with the base-relative posix path, or `None`.
|
|
151
|
+
*
|
|
152
|
+
* @public
|
|
153
|
+
*/
|
|
154
|
+
const relativeWithin = Effect.fn("Walker.relativeWithin")(function* (base, absolute, options) {
|
|
155
|
+
const path = yield* Path.Path;
|
|
156
|
+
const outcome = yield* runUpward(confineRealMachine("Walker.relativeWithin", base, absolute, path, within(options)));
|
|
157
|
+
return outcome._tag === "Ok" ? Option.some(outcome.value) : Option.none();
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* {@link relativeWithin}, synchronously, over a caller-supplied port.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { relativeWithinSync } from "@effected/walker";
|
|
165
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
166
|
+
*
|
|
167
|
+
* const inside = relativeWithinSync("/repo", "/repo/docs/new.md", nodeWalkerSyncOps);
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @param base - The absolute directory the path must stay inside.
|
|
171
|
+
* @param absolute - The absolute path to place under `base`.
|
|
172
|
+
* @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
|
|
173
|
+
* @param options - See {@link RelativeWithinOptions}.
|
|
174
|
+
* @returns The base-relative posix path, or `None`.
|
|
175
|
+
*
|
|
176
|
+
* @public
|
|
177
|
+
*/
|
|
178
|
+
const relativeWithinSync = (base, absolute, ops, options) => {
|
|
179
|
+
const outcome = runUpwardSync(confineRealMachine("Walker.relativeWithinSync", base, absolute, ops.path, within(options)), ops.fileSystem);
|
|
180
|
+
return outcome._tag === "Ok" ? Option.some(outcome.value) : Option.none();
|
|
181
|
+
};
|
|
122
182
|
|
|
123
|
-
export { confineReal, confineRealSync };
|
|
183
|
+
export { confineReal, confineRealSync, relativeWithin, relativeWithinSync };
|
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
Path traversal as Effect primitives. `Walker.ascend` gives you the directory chain from a starting path to the filesystem root; `Walker.findUpward` returns the nearest existing file among per-directory candidates; `Walker.findRoot` returns the nearest directory a marker predicate accepts. Every probe absorbs its own failure, so a single unreadable ancestor cannot hide a valid `.git` or `pnpm-workspace.yaml` above it. Going the other way, `descend` expands a compiled [`@effected/glob`](https://www.npmjs.com/package/@effected/glob) pattern under a directory into the matching file paths — sorted, symlink-safe, and typed about unreadable subtrees instead of silently swallowing them. `FileSystem` and `Path` arrive from `effect` core, so no platform package is pulled in — not even in tests.
|
|
9
9
|
|
|
10
10
|
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
|
|
11
|
-
> Effect v4 (`effect`
|
|
11
|
+
> Effect v4 (`effect` `~4.1.0`) and still in `0.x` development. Stable Effect
|
|
12
12
|
> makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
|
|
13
13
|
> `@effect/*` versions on the line the kit is built and tested against, install
|
|
14
|
-
> [`@effected/pnpm-plugin-
|
|
14
|
+
> [`@effected/pnpm-plugin-effected`](https://www.npmjs.com/package/@effected/pnpm-plugin-effected).
|
|
15
15
|
>
|
|
16
16
|
> **Stability: unstable.** This package's API surface is not yet considered
|
|
17
17
|
> complete and may change across `0.x` releases. Pin an exact version — even a
|
package/index.d.ts
CHANGED
|
@@ -707,6 +707,72 @@ export declare const confineReal: (base: string, candidate: string) => Effect.Ef
|
|
|
707
707
|
* @public
|
|
708
708
|
*/
|
|
709
709
|
export declare const confineRealSync: (base: string, candidate: string, ops: WalkerSyncOps) => Result.Result<string, PathEscapeError>;
|
|
710
|
+
/**
|
|
711
|
+
* Options for {@link relativeWithin}.
|
|
712
|
+
*
|
|
713
|
+
* @public
|
|
714
|
+
*/
|
|
715
|
+
interface RelativeWithinOptions {
|
|
716
|
+
/**
|
|
717
|
+
* What a path that does not exist yet means. `"ancestor"` (the default)
|
|
718
|
+
* resolves the missing tail from the deepest existing ancestor, so a
|
|
719
|
+
* destination can be placed before it is created; `"refuse"` answers `None`
|
|
720
|
+
* for any path that does not exist.
|
|
721
|
+
*/
|
|
722
|
+
readonly missing?: "ancestor" | "refuse";
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* The posix, base-relative path of an absolute path that lies inside a base
|
|
726
|
+
* directory PHYSICALLY, or `None` when it does not.
|
|
727
|
+
*
|
|
728
|
+
* @remarks
|
|
729
|
+
* The question `confineReal` does not answer: where, under `base`, does this
|
|
730
|
+
* absolute path sit? It resolves links the way `confineReal` does, and a path
|
|
731
|
+
* or tail that does not exist yet is resolved lexically from its deepest
|
|
732
|
+
* existing ancestor, so a destination can be placed before it is created.
|
|
733
|
+
* The base itself answers `""`. `None` covers every refusal: a relative or
|
|
734
|
+
* NUL-bearing candidate, a real location outside the base, and a resolution
|
|
735
|
+
* that cannot be verified (a permission error, a loop, a dangling link) —
|
|
736
|
+
* containment that cannot be proved is never assumed. A relative `base` is a
|
|
737
|
+
* **defect**, as it is for `confineReal`.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* ```ts
|
|
741
|
+
* import { relativeWithin } from "@effected/walker";
|
|
742
|
+
* import { Effect } from "effect";
|
|
743
|
+
*
|
|
744
|
+
* const inside = relativeWithin("/repo", "/repo/docs/new.md");
|
|
745
|
+
* // Requires `FileSystem` and `Path`; succeeds with Option.some("docs/new.md").
|
|
746
|
+
* ```
|
|
747
|
+
*
|
|
748
|
+
* @param base - The absolute directory the path must stay inside.
|
|
749
|
+
* @param absolute - The absolute path to place under `base`.
|
|
750
|
+
* @param options - See {@link RelativeWithinOptions}.
|
|
751
|
+
* @returns An `Effect` succeeding with the base-relative posix path, or `None`.
|
|
752
|
+
*
|
|
753
|
+
* @public
|
|
754
|
+
*/
|
|
755
|
+
export declare const relativeWithin: (base: string, absolute: string, options?: RelativeWithinOptions) => Effect.Effect<Option.Option<string>, never, FileSystem.FileSystem | Path.Path>;
|
|
756
|
+
/**
|
|
757
|
+
* {@link relativeWithin}, synchronously, over a caller-supplied port.
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* ```ts
|
|
761
|
+
* import { relativeWithinSync } from "@effected/walker";
|
|
762
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
763
|
+
*
|
|
764
|
+
* const inside = relativeWithinSync("/repo", "/repo/docs/new.md", nodeWalkerSyncOps);
|
|
765
|
+
* ```
|
|
766
|
+
*
|
|
767
|
+
* @param base - The absolute directory the path must stay inside.
|
|
768
|
+
* @param absolute - The absolute path to place under `base`.
|
|
769
|
+
* @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
|
|
770
|
+
* @param options - See {@link RelativeWithinOptions}.
|
|
771
|
+
* @returns The base-relative posix path, or `None`.
|
|
772
|
+
*
|
|
773
|
+
* @public
|
|
774
|
+
*/
|
|
775
|
+
export declare const relativeWithinSync: (base: string, absolute: string, ops: WalkerSyncOps, options?: RelativeWithinOptions) => Option.Option<string>;
|
|
710
776
|
/**
|
|
711
777
|
* Options for {@link compileAndExpand}: every {@link DescendOptions} field,
|
|
712
778
|
* plus the glob options the pattern compiles under.
|
|
@@ -1165,5 +1231,5 @@ export declare class Walker {
|
|
|
1165
1231
|
*/
|
|
1166
1232
|
static findUpwardAllSync(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>, ops: WalkerSyncOps): Option.Option<UpwardMatches>;
|
|
1167
1233
|
}
|
|
1168
|
-
export type { AscendOptions, AscendSyncOptions, AscendWithinSyncOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, UnreadableDirectory, UpwardMatches, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
|
|
1234
|
+
export type { AscendOptions, AscendSyncOptions, AscendWithinSyncOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, RelativeWithinOptions, UnreadableDirectory, UpwardMatches, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
|
|
1169
1235
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PathEscapeError, PathEscapeReason, confineRelative, confineRelativeResult } from "./Confine.js";
|
|
2
|
-
import { confineReal, confineRealSync } from "./ConfineReal.js";
|
|
2
|
+
import { confineReal, confineRealSync, relativeWithin, relativeWithinSync } from "./ConfineReal.js";
|
|
3
3
|
import { DescendError, descend, descendEntries } from "./Descend.js";
|
|
4
4
|
import { descendEntriesSync, descendSync } from "./DescendSync.js";
|
|
5
5
|
import { GlobExpansionError, compileAndExpand } from "./Expand.js";
|
|
6
6
|
import { Walker } from "./Walker.js";
|
|
7
7
|
|
|
8
|
-
export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineReal, confineRealSync, confineRelative, confineRelativeResult, descend, descendEntries, descendEntriesSync, descendSync };
|
|
8
|
+
export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineReal, confineRealSync, confineRelative, confineRelativeResult, descend, descendEntries, descendEntriesSync, descendSync, relativeWithin, relativeWithinSync };
|
|
@@ -147,7 +147,7 @@ const isWin32Reserved = (segment) => segment.includes(":") || DOTS_AND_SPACES.te
|
|
|
147
147
|
* two requests) is unresolvable, its target unverified. Any failure other
|
|
148
148
|
* than `NotFound` makes the path unresolvable too. Fail closed throughout.
|
|
149
149
|
*/
|
|
150
|
-
function* resolvePhysical(absolute, path, win32, checkSegments) {
|
|
150
|
+
function* resolvePhysical(absolute, path, win32, checkSegments, requireExisting = false) {
|
|
151
151
|
let root = absolute;
|
|
152
152
|
for (let parent = path.dirname(root); parent !== root; parent = path.dirname(root)) root = parent;
|
|
153
153
|
const segments = (absolute.startsWith(root) ? absolute.slice(root.length) : absolute).split(win32 ? /[\\/]/ : "/").filter((segment) => segment !== "");
|
|
@@ -198,6 +198,10 @@ function* resolvePhysical(absolute, path, win32, checkSegments) {
|
|
|
198
198
|
_tag: "Unresolvable",
|
|
199
199
|
cause: answer.cause
|
|
200
200
|
};
|
|
201
|
+
if (requireExisting) return {
|
|
202
|
+
_tag: "Refused",
|
|
203
|
+
reason: "outside"
|
|
204
|
+
};
|
|
201
205
|
current = next;
|
|
202
206
|
}
|
|
203
207
|
return {
|
|
@@ -209,7 +213,7 @@ function* resolvePhysical(absolute, path, win32, checkSegments) {
|
|
|
209
213
|
* `confineReal`: the candidate's real path when it lies strictly inside the
|
|
210
214
|
* base's real path, or why not. A relative `base` is a wiring defect.
|
|
211
215
|
*/
|
|
212
|
-
function* confineRealMachine(name, base, candidate, path) {
|
|
216
|
+
function* confineRealMachine(name, base, candidate, path, within = false) {
|
|
213
217
|
if (base.includes("\0") || !isPinnedAbsolute(base, path)) return defect(`${name}: base must be an absolute path, received ${JSON.stringify(base)}`);
|
|
214
218
|
const refuse = (reason) => done({
|
|
215
219
|
_tag: "Refused",
|
|
@@ -217,6 +221,7 @@ function* confineRealMachine(name, base, candidate, path) {
|
|
|
217
221
|
});
|
|
218
222
|
if (candidate.includes("\0")) return refuse("nulByte");
|
|
219
223
|
if (candidate === "") return refuse("empty");
|
|
224
|
+
if (within && !path.isAbsolute(candidate)) return refuse("absolute");
|
|
220
225
|
const win32 = path.sep === "\\";
|
|
221
226
|
const isSep = (character) => character === path.sep || win32 && character === "/";
|
|
222
227
|
if (win32 && DRIVE.test(candidate) && !path.isAbsolute(candidate)) return refuse("absolute");
|
|
@@ -224,12 +229,20 @@ function* confineRealMachine(name, base, candidate, path) {
|
|
|
224
229
|
const full = path.isAbsolute(candidate) ? candidate : `${trimTrailing(base, isSep)}${path.sep}${candidate}`;
|
|
225
230
|
const baseReal = yield* resolvePhysical(base, path, win32, false);
|
|
226
231
|
if (baseReal._tag !== "Ok") return done(baseReal);
|
|
227
|
-
const candidateReal = yield* resolvePhysical(full, path, win32, true);
|
|
232
|
+
const candidateReal = yield* resolvePhysical(full, path, win32, true, within !== false && within.requireExisting);
|
|
228
233
|
if (candidateReal._tag !== "Ok") return done(candidateReal);
|
|
229
234
|
const root = trimTrailing(baseReal.value, isSep);
|
|
230
235
|
const resolved = trimTrailing(candidateReal.value, isSep);
|
|
236
|
+
if (within && resolved === root) return done({
|
|
237
|
+
_tag: "Ok",
|
|
238
|
+
value: ""
|
|
239
|
+
});
|
|
231
240
|
if (resolved === root) return refuse("empty");
|
|
232
241
|
if (!resolved.startsWith(`${root}${path.sep}`)) return refuse("outside");
|
|
242
|
+
if (within) return done({
|
|
243
|
+
_tag: "Ok",
|
|
244
|
+
value: resolved.slice(root.length + 1).split(path.sep).join("/")
|
|
245
|
+
});
|
|
233
246
|
return done({
|
|
234
247
|
_tag: "Ok",
|
|
235
248
|
value: candidateReal.value
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/walker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Path traversal as Effect primitives: ascend a directory chain to the first matching candidate, or descend a tree expanding a glob against the filesystem.",
|
|
6
6
|
"keywords": [
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"./package.json": "./package.json"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@effected/glob": "^0.
|
|
46
|
-
"effect": "
|
|
45
|
+
"@effected/glob": "^0.17.0",
|
|
46
|
+
"effect": "~4.1.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=24.11.0"
|