@animalabs/agent-framework 0.23.1 → 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/README.md +212 -42
- package/dist/src/agent.d.ts +60 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +92 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +14 -12
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/code-execution/engine.d.ts +177 -0
- package/dist/src/code-execution/engine.d.ts.map +1 -0
- package/dist/src/code-execution/engine.js +494 -0
- package/dist/src/code-execution/engine.js.map +1 -0
- package/dist/src/code-execution/index.d.ts +5 -10
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js +5 -9
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/isolate/calls.d.ts +114 -0
- package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/calls.js +145 -0
- package/dist/src/code-execution/isolate/calls.js.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
- package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-entry.js +5 -0
- package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
- package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
- package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child-main.js +132 -0
- package/dist/src/code-execution/isolate/child-main.js.map +1 -0
- package/dist/src/code-execution/isolate/child.d.ts +44 -0
- package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/child.js +315 -0
- package/dist/src/code-execution/isolate/child.js.map +1 -0
- package/dist/src/code-execution/isolate/frames.d.ts +80 -0
- package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/frames.js +210 -0
- package/dist/src/code-execution/isolate/frames.js.map +1 -0
- package/dist/src/code-execution/isolate/output.d.ts +68 -0
- package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/output.js +143 -0
- package/dist/src/code-execution/isolate/output.js.map +1 -0
- package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
- package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/prelude.js +401 -0
- package/dist/src/code-execution/isolate/prelude.js.map +1 -0
- package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
- package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/protocol.js +150 -0
- package/dist/src/code-execution/isolate/protocol.js.map +1 -0
- package/dist/src/code-execution/isolate/render.d.ts +57 -0
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/render.js +214 -0
- package/dist/src/code-execution/isolate/render.js.map +1 -0
- package/dist/src/code-execution/isolate/runner.d.ts +250 -0
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/runner.js +854 -0
- package/dist/src/code-execution/isolate/runner.js.map +1 -0
- package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
- package/dist/src/code-execution/isolate/scripts.js +357 -0
- package/dist/src/code-execution/isolate/scripts.js.map +1 -0
- package/dist/src/code-execution/quickjs.wasm +0 -0
- package/dist/src/code-execution/tool-definition.d.ts +93 -43
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +176 -142
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/deadline-timer.d.ts +3 -0
- package/dist/src/deadline-timer.d.ts.map +1 -1
- package/dist/src/deadline-timer.js +1 -1
- package/dist/src/deadline-timer.js.map +1 -1
- package/dist/src/framework.d.ts +1036 -440
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4131 -2228
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +28 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +88 -10
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -3
- package/dist/src/held-writes.d.ts.map +1 -1
- package/dist/src/held-writes.js +16 -3
- package/dist/src/held-writes.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +471 -75
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1673 -337
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +12 -7
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +12 -7
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +9 -5
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +50 -0
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +130 -0
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -2
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +6 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +8 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +15 -6
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +168 -53
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/prose-grammar.d.ts +4 -5
- package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
- package/dist/src/mcpl/prose-grammar.js +4 -5
- package/dist/src/mcpl/prose-grammar.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +97 -10
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +173 -16
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +132 -14
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +227 -50
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +6 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +4 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +4 -4
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +50 -21
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +158 -74
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +24 -0
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +33 -0
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +55 -7
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +163 -34
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +22 -6
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +4 -2
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +100 -13
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +8 -13
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +27 -4
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/history/semantic.d.ts +15 -3
- package/dist/src/modules/history/semantic.d.ts.map +1 -1
- package/dist/src/modules/history/semantic.js +27 -5
- package/dist/src/modules/history/semantic.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/unknown-tool.d.ts +4 -1
- package/dist/src/modules/unknown-tool.d.ts.map +1 -1
- package/dist/src/modules/unknown-tool.js +4 -1
- package/dist/src/modules/unknown-tool.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +3 -1
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
- package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-plan.js +794 -0
- package/dist/src/modules/workspace/edit-plan.js.map +1 -0
- package/dist/src/modules/workspace/edit-text.d.ts +117 -0
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
- package/dist/src/modules/workspace/edit-text.js +584 -0
- package/dist/src/modules/workspace/edit-text.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +5 -4
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +78 -25
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +484 -61
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2173 -645
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +213 -38
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +416 -36
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +15 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/text.d.ts +32 -0
- package/dist/src/modules/workspace/text.d.ts.map +1 -0
- package/dist/src/modules/workspace/text.js +67 -0
- package/dist/src/modules/workspace/text.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +210 -6
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +3 -2
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/operator-log.d.ts +52 -24
- package/dist/src/operator-log.d.ts.map +1 -1
- package/dist/src/operator-log.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/recovery/offline-branch.d.ts +26 -1
- package/dist/src/recovery/offline-branch.d.ts.map +1 -1
- package/dist/src/recovery/offline-branch.js +164 -39
- package/dist/src/recovery/offline-branch.js.map +1 -1
- package/dist/src/recovery/recover-cli.js +23 -4
- package/dist/src/recovery/recover-cli.js.map +1 -1
- package/dist/src/send-tools.d.ts +11 -0
- package/dist/src/send-tools.d.ts.map +1 -0
- package/dist/src/send-tools.js +11 -0
- package/dist/src/send-tools.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +4 -3
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +5 -4
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +1 -1
- package/dist/src/tool-result-history.js +1 -1
- package/dist/src/tune-out/coordinator.d.ts +21 -0
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +139 -37
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +4 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +62 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +62 -58
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +40 -8
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +111 -10
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/types.d.ts +16 -0
- package/dist/src/usage/types.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.d.ts +14 -1
- package/dist/src/usage/usage-tracker.d.ts.map +1 -1
- package/dist/src/usage/usage-tracker.js +81 -41
- package/dist/src/usage/usage-tracker.js.map +1 -1
- package/package.json +6 -5
- package/src/agent.ts +93 -12
- package/src/api/server.ts +14 -12
- package/src/code-execution/engine.ts +595 -0
- package/src/code-execution/index.ts +5 -17
- package/src/code-execution/isolate/calls.ts +199 -0
- package/src/code-execution/isolate/child-entry.ts +5 -0
- package/src/code-execution/isolate/child-main.ts +132 -0
- package/src/code-execution/isolate/child.ts +327 -0
- package/src/code-execution/isolate/frames.ts +209 -0
- package/src/code-execution/isolate/output.ts +161 -0
- package/src/code-execution/isolate/prelude.ts +409 -0
- package/src/code-execution/isolate/protocol.ts +240 -0
- package/src/code-execution/isolate/render.ts +230 -0
- package/src/code-execution/isolate/runner.ts +911 -0
- package/src/code-execution/isolate/scripts.ts +479 -0
- package/src/code-execution/quickjs.wasm +0 -0
- package/src/code-execution/quickjs.wasm.LICENSES +149 -0
- package/src/code-execution/tool-definition.ts +236 -167
- package/src/deadline-timer.ts +1 -1
- package/src/framework.ts +4507 -2406
- package/src/gate/event-gate.ts +78 -11
- package/src/held-writes.ts +17 -3
- package/src/index.ts +30 -22
- package/src/mcpl/channel-registry.ts +1825 -368
- package/src/mcpl/conversation-router.ts +12 -7
- package/src/mcpl/convert-block.ts +9 -5
- package/src/mcpl/inbound-source.ts +131 -0
- package/src/mcpl/index.ts +4 -2
- package/src/mcpl/log-value.ts +9 -0
- package/src/mcpl/modern-connection.ts +162 -53
- package/src/mcpl/prose-grammar.ts +4 -5
- package/src/mcpl/protocol-family.ts +199 -21
- package/src/mcpl/push-coalescer.ts +306 -62
- package/src/mcpl/push-handler.ts +10 -2
- package/src/mcpl/reference-fetcher.ts +4 -4
- package/src/mcpl/server-connection.ts +180 -79
- package/src/mcpl/tags.ts +36 -0
- package/src/mcpl/tool-result-normalize.ts +2 -1
- package/src/mcpl/transport.ts +162 -33
- package/src/mcpl/types.ts +23 -6
- package/src/module-registry.ts +92 -13
- package/src/modules/discord/index.ts +8 -13
- package/src/modules/history/index.ts +25 -3
- package/src/modules/history/semantic.ts +29 -6
- package/src/modules/index.ts +1 -0
- package/src/modules/unknown-tool.ts +4 -1
- package/src/modules/workspace/branch-intent.ts +3 -1
- package/src/modules/workspace/edit-plan.ts +954 -0
- package/src/modules/workspace/edit-text.ts +618 -0
- package/src/modules/workspace/effects.ts +70 -19
- package/src/modules/workspace/index.ts +2247 -642
- package/src/modules/workspace/observe.ts +465 -56
- package/src/modules/workspace/reconcile.ts +14 -7
- package/src/modules/workspace/text.ts +68 -0
- package/src/modules/workspace/types.ts +211 -6
- package/src/modules/workspace/watcher.ts +3 -2
- package/src/operator-log.ts +47 -19
- package/src/recovery/discord-awareness-outbox.ts +1500 -356
- package/src/recovery/offline-branch.ts +189 -55
- package/src/recovery/recover-cli.ts +25 -4
- package/src/send-tools.ts +11 -0
- package/src/tool-result-guard.ts +6 -5
- package/src/tool-result-history.ts +1 -1
- package/src/tune-out/coordinator.ts +163 -42
- package/src/types/agent.ts +4 -0
- package/src/types/events.ts +62 -9
- package/src/types/framework.ts +62 -58
- package/src/types/module.ts +40 -8
- package/src/types/trace.ts +121 -15
- package/src/usage/types.ts +16 -0
- package/src/usage/usage-tracker.ts +86 -43
- package/dist/src/code-execution/py-runner.d.ts +0 -191
- package/dist/src/code-execution/py-runner.d.ts.map +0 -1
- package/dist/src/code-execution/py-runner.js +0 -655
- package/dist/src/code-execution/py-runner.js.map +0 -1
- package/dist/src/code-execution/runtime-py.d.ts +0 -50
- package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
- package/dist/src/code-execution/runtime-py.js +0 -677
- package/dist/src/code-execution/runtime-py.js.map +0 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
- package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
- package/dist/src/mcpl/prose-stream-router.js +0 -291
- package/dist/src/mcpl/prose-stream-router.js.map +0 -1
- package/src/code-execution/py-runner.ts +0 -791
- package/src/code-execution/runtime-py.ts +0 -677
- package/src/mcpl/prose-stream-router.ts +0 -268
|
@@ -32,8 +32,12 @@
|
|
|
32
32
|
* a directory is checked by its canonical path and identity, not held open:
|
|
33
33
|
* a parent replaced concurrently in the window between a check and its use
|
|
34
34
|
* (or replaced and restored around it) can still redirect a listing or
|
|
35
|
-
* lookup there. File contents are bound
|
|
36
|
-
* through a descriptor
|
|
35
|
+
* lookup there. File contents are bound more tightly: a file is read only
|
|
36
|
+
* through a descriptor whose identity matches the file its path named, as a
|
|
37
|
+
* check before the open found it (hashAt) or as the path's realpath after
|
|
38
|
+
* the open finds it (openContainedFile), though a link put back after that
|
|
39
|
+
* realpath, leading to the opened file itself, still passes the check
|
|
40
|
+
* (sameLevels).
|
|
37
41
|
*/
|
|
38
42
|
|
|
39
43
|
import { createHash } from 'node:crypto';
|
|
@@ -45,7 +49,11 @@ import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } fr
|
|
|
45
49
|
import { sameRootIdentity, type Fingerprint, type RootIdentity } from './disk-agreement.js';
|
|
46
50
|
|
|
47
51
|
export interface MountView {
|
|
48
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* The directory its readers read beneath: the mount's root as configured,
|
|
54
|
+
* or, for a direct mount, the root its view resolved (diskView), so a
|
|
55
|
+
* walk opens what it judges beneath one root.
|
|
56
|
+
*/
|
|
49
57
|
root: string;
|
|
50
58
|
/** The mount's name, which a tool puts before a path in the mount. */
|
|
51
59
|
name: string;
|
|
@@ -58,33 +66,35 @@ export interface MountView {
|
|
|
58
66
|
/** Text above this size is not ingested. */
|
|
59
67
|
maxFileSize: number;
|
|
60
68
|
ignore: string[];
|
|
69
|
+
/** The mount's `include`: files and links it doesn't cover are outside the mount. Absent, everything is inside. */
|
|
70
|
+
include?: readonly string[];
|
|
61
71
|
/**
|
|
62
72
|
* Other mounts' directories nested inside this one, mount-relative. Every
|
|
63
73
|
* pass passes over them, named or not, as another mount's directory.
|
|
64
74
|
*/
|
|
65
75
|
others: readonly string[];
|
|
66
76
|
/**
|
|
67
|
-
* The same mounts, each by its directory, its name
|
|
68
|
-
* its real root (NestedMount), for a refusal that names
|
|
69
|
-
* leads into: through a symlink this mount doesn't follow
|
|
70
|
-
* through one it follows, or a root configured through one
|
|
71
|
-
* Without it, no path is refused for where its links lead,
|
|
72
|
-
* refusal names no mount.
|
|
77
|
+
* The same mounts, each by its directory, its name, its own include and,
|
|
78
|
+
* where it has one, its real root (NestedMount), for a refusal that names
|
|
79
|
+
* the mount a path leads into: through a symlink this mount doesn't follow
|
|
80
|
+
* (linkWords), or through one it follows, or a root configured through one
|
|
81
|
+
* (nestedAtReal). Without it, no path is refused for where its links lead,
|
|
82
|
+
* and a link's refusal names no mount.
|
|
73
83
|
*/
|
|
74
84
|
nested?: readonly NestedMount[];
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
/**
|
|
78
88
|
* A mount nested in another: its directory in one of its spellings,
|
|
79
|
-
* mount-relative and `/`-separated, its name,
|
|
80
|
-
*
|
|
81
|
-
* (the nearest existing directory's, where the root
|
|
82
|
-
* the module's start has read it, and the root as
|
|
83
|
-
* There's none where it's this mount's own or holds
|
|
84
|
-
* by its configured path alone, since through its
|
|
85
|
-
* this mount's every path.
|
|
89
|
+
* mount-relative and `/`-separated, its name, its real root, which every
|
|
90
|
+
* spelling shares, and its own include. The real root is the root as
|
|
91
|
+
* realpath resolves it (the nearest existing directory's, where the root
|
|
92
|
+
* isn't made yet) once the module's start has read it, and the root as
|
|
93
|
+
* configured until then. There's none where it's this mount's own or holds
|
|
94
|
+
* it: the mount is nested by its configured path alone, since through its
|
|
95
|
+
* real root it would claim this mount's every path.
|
|
86
96
|
*/
|
|
87
|
-
export type NestedMount = { dir: string; name: string; real?: string };
|
|
97
|
+
export type NestedMount = { dir: string; name: string; real?: string; include?: readonly string[] };
|
|
88
98
|
|
|
89
99
|
export type StatFingerprint = Omit<Fingerprint, 'hashedAt'>;
|
|
90
100
|
|
|
@@ -247,9 +257,12 @@ export async function linkRefusal(
|
|
|
247
257
|
|
|
248
258
|
/**
|
|
249
259
|
* Where a symlink the walk found leads, for its refusal: to a path inside
|
|
250
|
-
* the mount (mount-relative, and whether it's a
|
|
251
|
-
* mount, or nowhere it can be resolved to. One
|
|
252
|
-
* only when refusing.
|
|
260
|
+
* the mount (mount-relative, as disk spells it, and whether it's a
|
|
261
|
+
* directory), outside the mount, or nowhere it can be resolved to. One
|
|
262
|
+
* realpath, one stat and the respelling, made only when refusing. realpath
|
|
263
|
+
* keeps the spelling the link's target gives, which on a disk that folds
|
|
264
|
+
* names need not be disk's, so the target is respelled before anything
|
|
265
|
+
* judges or names it (diskSpelling).
|
|
253
266
|
*/
|
|
254
267
|
export type LinkLeads = { kind: 'inside'; target: string; directory: boolean } | { kind: 'outside' } | { kind: 'nowhere' };
|
|
255
268
|
|
|
@@ -258,7 +271,7 @@ export async function linkLeads(root: string, rootReal: string, link: string): P
|
|
|
258
271
|
if (leads === null) return { kind: 'nowhere' };
|
|
259
272
|
if (!contained(rootReal, leads)) return { kind: 'outside' };
|
|
260
273
|
const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
|
|
261
|
-
return { kind: 'inside', target: relative(rootReal, leads).split(sep).join('/'), directory };
|
|
274
|
+
return { kind: 'inside', target: await diskSpelling(rootReal, relative(rootReal, leads).split(sep).join('/')), directory };
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
/**
|
|
@@ -266,12 +279,119 @@ export async function linkLeads(root: string, rootReal: string, link: string): P
|
|
|
266
279
|
* innermost where nested mounts nest, as #460 picks one; undefined when none
|
|
267
280
|
* does.
|
|
268
281
|
*/
|
|
269
|
-
export function nestedHolder
|
|
282
|
+
export function nestedHolder<N extends { dir: string; name: string }>(target: string, nested: readonly N[]): N | undefined {
|
|
270
283
|
return nested
|
|
271
284
|
.filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
|
|
272
285
|
.sort((a, b) => b.dir.length - a.dir.length)[0];
|
|
273
286
|
}
|
|
274
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Whether a link's refusal names `holder`, the nested mount whose directory
|
|
290
|
+
* holds the link's target. The target's path is one of that mount's own, so
|
|
291
|
+
* its include decides: the refusal names a directory, or a file the include
|
|
292
|
+
* covers. A refusal's code follows the same test (linkReadError's
|
|
293
|
+
* `other_mount`), so the code says no more than the message.
|
|
294
|
+
*/
|
|
295
|
+
export function namesHolder(leads: { target: string; directory: boolean }, holder: { dir: string; include?: readonly string[] }): boolean {
|
|
296
|
+
return namesNestedPlace(leads.target.slice(holder.dir.length + 1), holder.include, leads.directory);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Whether a refusal names a place in a nested mount's directory, `rest`
|
|
301
|
+
* beneath that mount's root, as a place of that mount's: a directory, or a
|
|
302
|
+
* file its include covers. The place is one of that mount's own paths, so
|
|
303
|
+
* its include decides, and a refusal's code follows the same test, so the
|
|
304
|
+
* code says no more than the message. Anywhere else the place is no mount's:
|
|
305
|
+
* the outer mount passes over the nested directory, and the nested mount's
|
|
306
|
+
* include leaves it out. So a link the outer mount doesn't follow is refused
|
|
307
|
+
* as one it doesn't follow (namesHolder), and a path that reaches the place
|
|
308
|
+
* through one it follows is refused as one that leaves the mount, in the
|
|
309
|
+
* words each such refusal already has (nestedAtReal's callers).
|
|
310
|
+
*/
|
|
311
|
+
export function namesNestedPlace(rest: string, include: readonly string[] | undefined, directory: boolean): boolean {
|
|
312
|
+
return directory || insideMount(rest, include);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Whether a path of a mount is one its passes decide and its listings may
|
|
317
|
+
* name: inside its include, and, in a nested mount's directory, a place
|
|
318
|
+
* that mount names (namesNestedPlace), its directory included. History the
|
|
319
|
+
* mount took in before another was nested there, which that mount's
|
|
320
|
+
* include leaves out, is no mount's, so no pass decides it, and no listing
|
|
321
|
+
* names it or a directory only it implies.
|
|
322
|
+
*/
|
|
323
|
+
export function withinIncludes(view: Pick<MountView, 'include' | 'nested'>, path: string): boolean {
|
|
324
|
+
if (!insideMount(path, view.include)) return false;
|
|
325
|
+
const holder = nestedHolder(path, view.nested ?? []);
|
|
326
|
+
return holder === undefined || path === holder.dir || namesNestedPlace(path.slice(holder.dir.length + 1), holder.include, false);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The path a mount's include judges for `given`, one of the mount's paths,
|
|
331
|
+
* which landed at `real`, its canonical path, inside the mount and in no
|
|
332
|
+
* nested mount's directory. Where realpath couldn't resolve it, as for a loop
|
|
333
|
+
* or a directory this process can't search, `real` is the path as
|
|
334
|
+
* path.resolve reads it, which this judges as named, so a caller refuses such
|
|
335
|
+
* a path first, as landingRefusal does (realBeneath's `unresolved`). The
|
|
336
|
+
* include is the operator's statement of which files' content the workspace
|
|
337
|
+
* holds, and a link is a path, not a grant of content, so where a followed
|
|
338
|
+
* path lands is judged as where it's named, as a nested mount's place is
|
|
339
|
+
* (namesNestedPlace). A path that meets no link lands where it's named, and
|
|
340
|
+
* is judged as named. realpath can spell such a path otherwise only as a disk
|
|
341
|
+
* that folds names does, with disk's stored names on macOS and Windows where
|
|
342
|
+
* a path keeps a workspace entry's spelling, so where the spellings differ
|
|
343
|
+
* only so, the path is judged as named where it reaches the landing itself,
|
|
344
|
+
* level by level (sameLevels). Otherwise the landing is judged, as disk
|
|
345
|
+
* spells it, since realpath keeps the spelling a link's target gives, which
|
|
346
|
+
* on a disk that folds names need not be disk's (diskSpelling).
|
|
347
|
+
*/
|
|
348
|
+
export async function judgedAt(rootReal: string, real: string, given: string): Promise<string> {
|
|
349
|
+
const landed = relative(rootReal, real).split(sep).join('/');
|
|
350
|
+
if (landed === given) return given;
|
|
351
|
+
if (foldedLoosely(landed) === foldedLoosely(given) && await sameLevels(rootReal, given, landed)) return given;
|
|
352
|
+
return diskSpelling(rootReal, landed);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Whether `given` reaches `landed`, two spellings of a path beneath
|
|
357
|
+
* `rootReal`, level by level: each directory on the way and the file itself
|
|
358
|
+
* are, as `lstat` finds them, one and the same on both sides, so the path as
|
|
359
|
+
* named passes through no link of its own that the landing doesn't. Beneath
|
|
360
|
+
* the last level both sides have, where neither has the next, a file a write
|
|
361
|
+
* would create there lands where it's named. A level only one side has, or
|
|
362
|
+
* one this can't ask, is no such path. It asks each level once, in turn: on a
|
|
363
|
+
* disk that folds names a level both spellings share is one entry, so a link
|
|
364
|
+
* disk puts there after the realpath meets both alike, and a name removed
|
|
365
|
+
* while this asks and put back after reads as a file a write would create.
|
|
366
|
+
* Where a read follows, its check of the opened file catches a link to
|
|
367
|
+
* another file, but not one back to the opened file itself; that race, like
|
|
368
|
+
* one where the spellings match, isn't closed here.
|
|
369
|
+
*/
|
|
370
|
+
async function sameLevels(rootReal: string, given: string, landed: string): Promise<boolean> {
|
|
371
|
+
const named = given.split('/');
|
|
372
|
+
const landing = landed.split('/');
|
|
373
|
+
const level = (segments: string[], i: number) => lstat(join(rootReal, ...segments.slice(0, i)), { bigint: true })
|
|
374
|
+
.then((st) => st, (err: unknown) => (errno(err) === 'ENOENT' || errno(err) === 'ENOTDIR' ? null : undefined));
|
|
375
|
+
for (let i = 1; i <= named.length; i++) {
|
|
376
|
+
const [a, b] = await Promise.all([level(named, i), level(landing, i)]);
|
|
377
|
+
if (a === undefined || b === undefined) return false;
|
|
378
|
+
if (a === null && b === null) return true;
|
|
379
|
+
if (a === null || b === null || !sameFile(a, b)) return false;
|
|
380
|
+
}
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Whether a mount's own include covers the file one of its paths, `given`,
|
|
386
|
+
* lands on at `real` (judgedAt): a file it leaves out there is no file of
|
|
387
|
+
* this mount's, and a path that reaches it through a link is refused as one
|
|
388
|
+
* that leaves the mount, in the words each such refusal already has
|
|
389
|
+
* (nestedAtReal's callers). A mount without an include covers every file.
|
|
390
|
+
*/
|
|
391
|
+
export async function coversLanding(view: Pick<MountView, 'include'>, rootReal: string, real: string, given: string): Promise<boolean> {
|
|
392
|
+
return view.include === undefined || insideMount(await judgedAt(rootReal, real, given), view.include);
|
|
393
|
+
}
|
|
394
|
+
|
|
275
395
|
/**
|
|
276
396
|
* Why a path is refused for what linkOnTheWay found, given where a link
|
|
277
397
|
* leads (linkLeads). For a link, the link is the subject, with where it
|
|
@@ -284,9 +404,15 @@ export function nestedHolder(target: string, nested: readonly { dir: string; nam
|
|
|
284
404
|
* path to use there in place of the link, and offers no setting, since
|
|
285
405
|
* following the link would only reach into another mount's directory. For a
|
|
286
406
|
* directory the walk couldn't ask, which one, and the answer it got. Paths
|
|
287
|
-
* are spelled as the tools take them, from the mount's name.
|
|
407
|
+
* are spelled as the tools take them, from the mount's name. On a mount with
|
|
408
|
+
* an include, an inside target is named only where a tool could name it: a
|
|
409
|
+
* file the include covers, or a directory, which a listing names whatever
|
|
410
|
+
* lies beneath it. Anywhere else the refusal says only that the link isn't
|
|
411
|
+
* followed, so no tool names what lies outside the include. A link the
|
|
412
|
+
* include doesn't cover is outside the mount itself, so its refusal tells
|
|
413
|
+
* nothing of where it leads (shownLeads gives null for it).
|
|
288
414
|
*/
|
|
289
|
-
export function linkWords(name: string, found: LinkFound, leads: LinkLeads | null, nested: readonly
|
|
415
|
+
export function linkWords(name: string, found: LinkFound, leads: LinkLeads | null, nested: readonly NestedMount[] = [], include?: readonly string[]): string {
|
|
290
416
|
const spell = (rel: string) => `"${rel === '' ? name : `${name}/${rel}`}"`;
|
|
291
417
|
if (!('link' in found)) return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${found.code})`;
|
|
292
418
|
const link = `${spell(found.link)} is a symlink`;
|
|
@@ -294,33 +420,52 @@ export function linkWords(name: string, found: LinkFound, leads: LinkLeads | nul
|
|
|
294
420
|
if (leads.kind === 'outside') return `${link} leading outside the mount, which this mount does not follow`;
|
|
295
421
|
const holder = nestedHolder(leads.target, nested);
|
|
296
422
|
if (holder) {
|
|
423
|
+
if (!namesHolder(leads, holder)) return `${link}, which this mount does not follow`;
|
|
297
424
|
const rest = leads.target.slice(holder.dir.length + 1);
|
|
298
425
|
return `${link} to ${spell(leads.target)}, which this mount does not follow; it lies in mount "${holder.name}", ` +
|
|
299
426
|
`nested in mount "${name}": use "${rest ? `${holder.name}/${rest}` : holder.name}" in place of ${spell(found.link)}`;
|
|
300
427
|
}
|
|
301
|
-
|
|
428
|
+
if (!leads.directory && !insideMount(leads.target, include)) return `${link}, which this mount does not follow`;
|
|
429
|
+
// Following it serves only what the include covers where it leads (judgedAt).
|
|
430
|
+
const setting = !leads.directory ? '' : include === undefined ? "; the mount's host can set followSymlinks: true"
|
|
431
|
+
: "; the mount's host can set followSymlinks: true, for what the include covers where it leads";
|
|
302
432
|
return `${link} to ${spell(leads.target)}, which this mount does not follow${setting}`;
|
|
303
433
|
}
|
|
304
434
|
|
|
305
|
-
/**
|
|
306
|
-
|
|
307
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Where a link linkOnTheWay found leads, as its refusal may tell it
|
|
437
|
+
* (linkLeads), or null where the refusal tells nothing of it: where the walk
|
|
438
|
+
* found a directory it couldn't ask rather than a link, and where the mount's
|
|
439
|
+
* include doesn't cover the link, which is then outside the mount, so its
|
|
440
|
+
* refusal shows only that a link stands on the path. A refusal's code
|
|
441
|
+
* follows the same answer (linkReadError), so the code says no more than the
|
|
442
|
+
* message.
|
|
443
|
+
*/
|
|
444
|
+
export async function shownLeads(root: string, rootReal: string, found: LinkFound, include: readonly string[] | undefined): Promise<LinkLeads | null> {
|
|
445
|
+
return 'link' in found && insideMount(found.link, include) ? linkLeads(root, rootReal, found.link) : null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
|
|
449
|
+
export async function linkReason(view: Pick<MountView, 'root' | 'name' | 'nested' | 'include'>, rootReal: string, found: LinkFound): Promise<string> {
|
|
450
|
+
return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
|
|
308
451
|
}
|
|
309
452
|
|
|
310
453
|
/**
|
|
311
454
|
* The nested mount whose root holds `real`, a canonical path, the innermost,
|
|
312
|
-
* with the path to use there
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
455
|
+
* with the path to use there, the path beneath its root (`rest`) and its
|
|
456
|
+
* include; undefined when none does. A path the lexical checks pass
|
|
457
|
+
* (inOtherMount, the tools' holderOf) reaches another mount's directory this
|
|
458
|
+
* way through a symlink the mount follows, so each check that resolves a
|
|
459
|
+
* path canonically asks this too, and names the mount only where
|
|
460
|
+
* namesNestedPlace says the place is that mount's.
|
|
316
461
|
*/
|
|
317
|
-
export function nestedAtReal(real: string, nested: readonly NestedMount[] = []): { name: string; use: string } | undefined {
|
|
462
|
+
export function nestedAtReal(real: string, nested: readonly NestedMount[] = []): { name: string; use: string; rest: string; include?: readonly string[] } | undefined {
|
|
318
463
|
const holder = nested
|
|
319
|
-
.flatMap((n) => (n.real !== undefined && contained(n.real, real) ? [{ name: n.name, real: n.real }] : []))
|
|
464
|
+
.flatMap((n) => (n.real !== undefined && contained(n.real, real) ? [{ name: n.name, real: n.real, include: n.include }] : []))
|
|
320
465
|
.sort((a, b) => b.real.length - a.real.length)[0];
|
|
321
466
|
if (!holder) return undefined;
|
|
322
467
|
const rest = relative(holder.real, real).split(sep).join('/');
|
|
323
|
-
return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name };
|
|
468
|
+
return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, rest, include: holder.include };
|
|
324
469
|
}
|
|
325
470
|
|
|
326
471
|
/**
|
|
@@ -471,6 +616,20 @@ function entryName(paths: readonly string[], prefix: string, segment: string): s
|
|
|
471
616
|
return found;
|
|
472
617
|
}
|
|
473
618
|
|
|
619
|
+
/**
|
|
620
|
+
* Whether a path's segments begin with a directory's: exactly, or, with
|
|
621
|
+
* `loosely`, with each segment compared as loosely as listedName compares
|
|
622
|
+
* names (foldedLoosely), as a disk that folds names may compare them. That
|
|
623
|
+
* errs toward the directory: on a disk that tells some of those spellings
|
|
624
|
+
* apart, such as case, or `ß` from `ss`, a distinct directory spelled one of
|
|
625
|
+
* them matches too, and resolveAbsolutePath, the one caller that compares
|
|
626
|
+
* loosely, gives null for a path beneath it.
|
|
627
|
+
*/
|
|
628
|
+
export function beginsWithDir(segments: readonly string[], dir: readonly string[], loosely: boolean): boolean {
|
|
629
|
+
if (segments.length < dir.length) return false;
|
|
630
|
+
return dir.every((d, i) => segments[i] === d || (loosely && foldedLoosely(segments[i]!) === foldedLoosely(d)));
|
|
631
|
+
}
|
|
632
|
+
|
|
474
633
|
/**
|
|
475
634
|
* The name `dir` lists for `segment`: `segment` itself, unless disk folds it
|
|
476
635
|
* there and lists it otherwise. `dir` is listed only where a variant of
|
|
@@ -770,9 +929,14 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
770
929
|
return { kind: 'unobserved', reason: OUTSIDE_PARENT, outside: true };
|
|
771
930
|
} else {
|
|
772
931
|
// Through a link this mount follows, a directory can be a nested
|
|
773
|
-
// mount's, which the mount neither reads nor writes.
|
|
932
|
+
// mount's, which the mount neither reads nor writes. A place there that
|
|
933
|
+
// mount's include leaves out is no mount's, and holds nothing beneath it,
|
|
934
|
+
// as at any file (namesNestedPlace).
|
|
774
935
|
const holder = nestedAtReal(real, view.nested);
|
|
775
|
-
if (holder)
|
|
936
|
+
if (holder) {
|
|
937
|
+
const directory = holder.include !== undefined && await stat(real).then((info) => info.isDirectory(), () => false);
|
|
938
|
+
return namesNestedPlace(holder.rest, holder.include, directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
|
|
939
|
+
}
|
|
776
940
|
}
|
|
777
941
|
const checked = await checkedAt(view, rel, real);
|
|
778
942
|
if (checked) {
|
|
@@ -997,19 +1161,24 @@ export async function filesystemTrustsCtime(root: string): Promise<boolean> {
|
|
|
997
1161
|
// ===========================================================================
|
|
998
1162
|
|
|
999
1163
|
/**
|
|
1000
|
-
* Where a path's parent directory canonically lies: inside the mount,
|
|
1001
|
-
* (absence is then proven separately), or somewhere a
|
|
1002
|
-
* still reach through — O_NOFOLLOW guards only the
|
|
1003
|
-
* on a mount that doesn't follow symlinks is any
|
|
1164
|
+
* Where a path's parent directory canonically lies: inside the mount, with
|
|
1165
|
+
* that real path, gone (absence is then proven separately), or somewhere a
|
|
1166
|
+
* no-follow open would still reach through — O_NOFOLLOW guards only the
|
|
1167
|
+
* final component — which on a mount that doesn't follow symlinks is any
|
|
1168
|
+
* symlink on the way.
|
|
1004
1169
|
*/
|
|
1005
|
-
async function parentPlacement(view: MountView, rootReal: string, relativePath: string): Promise<
|
|
1170
|
+
async function parentPlacement(view: MountView, rootReal: string, relativePath: string): Promise<{ inside: string } | 'gone' | { reason: string }> {
|
|
1006
1171
|
try {
|
|
1007
1172
|
const parent = await realpath(dirname(join(view.root, relativePath)));
|
|
1008
1173
|
const found = await linkRefusal(view, rootReal, relativePath, parent, 'parents');
|
|
1009
1174
|
if (found !== null) return { reason: await linkReason(view, rootReal, found) };
|
|
1010
1175
|
if (!contained(rootReal, parent)) return { reason: OUTSIDE_PARENT };
|
|
1011
1176
|
const holder = nestedAtReal(parent, view.nested);
|
|
1012
|
-
|
|
1177
|
+
if (!holder) return { inside: parent };
|
|
1178
|
+
// A place there that the nested mount's include leaves out is no mount's,
|
|
1179
|
+
// and holds nothing beneath it, as beneath any file (namesNestedPlace).
|
|
1180
|
+
const directory = holder.include !== undefined && await stat(parent).then((info) => info.isDirectory(), () => false);
|
|
1181
|
+
return namesNestedPlace(holder.rest, holder.include, directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
|
|
1013
1182
|
} catch (err) {
|
|
1014
1183
|
const code = errno(err);
|
|
1015
1184
|
if (code === 'ENOENT' || code === 'ENOTDIR') return 'gone';
|
|
@@ -1031,7 +1200,7 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1031
1200
|
const lexical = join(view.root, relativePath);
|
|
1032
1201
|
const placement = await parentPlacement(view, rootReal, relativePath);
|
|
1033
1202
|
if (placement === 'gone') return absence(view, rootReal, relativePath);
|
|
1034
|
-
if (
|
|
1203
|
+
if ('reason' in placement) return { kind: 'unobserved', reason: placement.reason };
|
|
1035
1204
|
let info;
|
|
1036
1205
|
try {
|
|
1037
1206
|
info = await lstat(lexical, { bigint: true });
|
|
@@ -1056,15 +1225,27 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1056
1225
|
}
|
|
1057
1226
|
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
|
|
1058
1227
|
const holder = nestedAtReal(real, view.nested);
|
|
1059
|
-
|
|
1228
|
+
// A place a nested mount's include leaves out is no mount's, so the link
|
|
1229
|
+
// leaves this one (namesNestedPlace).
|
|
1230
|
+
if (holder) {
|
|
1231
|
+
return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, target.isDirectory()) ? leadsInto(view.name, relativePath, holder) : 'a symlink that leaves the mount' };
|
|
1232
|
+
}
|
|
1060
1233
|
if (!target.isFile()) {
|
|
1061
1234
|
return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : 'not a regular file' };
|
|
1062
1235
|
}
|
|
1236
|
+
// A file this mount's own include leaves out where the link lands is no
|
|
1237
|
+
// file of this mount's (coversLanding). It's judged here, and not only
|
|
1238
|
+
// where the file is opened, since a pass takes a copy whose fingerprint
|
|
1239
|
+
// the stat vouches for without opening it.
|
|
1240
|
+
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
|
|
1063
1241
|
info = target;
|
|
1064
1242
|
} else if (info.isDirectory()) {
|
|
1065
1243
|
return { kind: 'unobserved', reason: 'a directory' };
|
|
1066
1244
|
} else if (!info.isFile()) {
|
|
1067
1245
|
return { kind: 'unobserved', reason: 'not a regular file' };
|
|
1246
|
+
} else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
|
|
1247
|
+
// So is one reached through a link among its directories.
|
|
1248
|
+
return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
|
|
1068
1249
|
}
|
|
1069
1250
|
return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 0o777n), fp: statFingerprint(info) };
|
|
1070
1251
|
}
|
|
@@ -1117,11 +1298,14 @@ async function openContained(
|
|
|
1117
1298
|
const found = await linkRefusal(view, rootReal, relativePath, real);
|
|
1118
1299
|
if (found !== null) return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
|
|
1119
1300
|
if (!info.isFile()) return { kind: 'unobserved', reason: info.isDirectory() ? 'a directory' : 'not a regular file' };
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1301
|
+
const leaves = view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT;
|
|
1302
|
+
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: leaves };
|
|
1123
1303
|
const holder = nestedAtReal(real, view.nested);
|
|
1124
|
-
|
|
1304
|
+
// A place a nested mount's include leaves out is no mount's. Reached
|
|
1305
|
+
// through a link this mount doesn't follow, the link is what's refused.
|
|
1306
|
+
if (holder) return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1307
|
+
// So is a file this mount's own include leaves out (coversLanding).
|
|
1308
|
+
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1125
1309
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
1126
1310
|
if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
|
|
1127
1311
|
return { kind: 'unobserved', reason: 'the file changed while it was read' };
|
|
@@ -1245,6 +1429,66 @@ export async function readPath(
|
|
|
1245
1429
|
}
|
|
1246
1430
|
}
|
|
1247
1431
|
|
|
1432
|
+
/** A file's bytes as readBytes read them, none hashed. */
|
|
1433
|
+
export interface BytesResult {
|
|
1434
|
+
kind: 'bytes';
|
|
1435
|
+
/**
|
|
1436
|
+
* Its size in bytes: as read, for a file within the limit; as its fstat
|
|
1437
|
+
* says, for one over it, and at least one past the limit where the reads
|
|
1438
|
+
* found it grown past it.
|
|
1439
|
+
*/
|
|
1440
|
+
size: number;
|
|
1441
|
+
/** The whole file, when it is at most the limit both at its fstat and as read; null otherwise. */
|
|
1442
|
+
bytes: Buffer | null;
|
|
1443
|
+
/** Its first bytes (at most HEAD_BYTES), for binary detection and MIME sniffing. */
|
|
1444
|
+
head: Buffer;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* A mount-relative file's bytes, through the same located descriptor as
|
|
1449
|
+
* readPath, for a direct mount's tools, which keep nothing: at most `limit`
|
|
1450
|
+
* of them, and none hashed. A file over `limit` at its fstat has only its
|
|
1451
|
+
* head read, and one that grew past it since is read no further than it.
|
|
1452
|
+
* What the open refuses gets the reason a listing gives it, such as a link
|
|
1453
|
+
* to a directory, except a regular file it couldn't open, which keeps the
|
|
1454
|
+
* open's.
|
|
1455
|
+
*/
|
|
1456
|
+
export async function readBytes(
|
|
1457
|
+
view: MountView,
|
|
1458
|
+
rootReal: string,
|
|
1459
|
+
relativePath: string,
|
|
1460
|
+
limit: number,
|
|
1461
|
+
): Promise<BytesResult | Exclude<Observation, { kind: 'file' }>> {
|
|
1462
|
+
const opened = await openContained(view, rootReal, relativePath);
|
|
1463
|
+
if (!('handle' in opened)) {
|
|
1464
|
+
// A walk takes a link to a directory as an entry, where the mount follows
|
|
1465
|
+
// links, and the open may refuse it as a directory or, where it can't
|
|
1466
|
+
// be opened, by its errno: either way it's named as a listing names it
|
|
1467
|
+
// (observePath).
|
|
1468
|
+
if (opened.kind === 'unobserved') {
|
|
1469
|
+
const seen = await observePath(view, rootReal, relativePath);
|
|
1470
|
+
return seen.kind === 'file' ? opened : seen;
|
|
1471
|
+
}
|
|
1472
|
+
return opened;
|
|
1473
|
+
}
|
|
1474
|
+
const { handle, info } = opened;
|
|
1475
|
+
try {
|
|
1476
|
+
const size = Number(info.size);
|
|
1477
|
+
if (size > limit) {
|
|
1478
|
+
const head = Buffer.alloc(Math.min(HEAD_BYTES, size));
|
|
1479
|
+
const { bytesRead } = await handle.read(head, 0, head.length, 0);
|
|
1480
|
+
return { kind: 'bytes', size, bytes: null, head: head.subarray(0, bytesRead) };
|
|
1481
|
+
}
|
|
1482
|
+
const { bytes, more } = await readPrefix(handle, limit, size);
|
|
1483
|
+
if (more) return { kind: 'bytes', size: Math.max(Number((await handle.stat({ bigint: true })).size), bytes.length + 1), bytes: null, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1484
|
+
return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
return { kind: 'unobserved', reason: `cannot read (${errno(err) ?? String(err)})` };
|
|
1487
|
+
} finally {
|
|
1488
|
+
await handle.close();
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1248
1492
|
/**
|
|
1249
1493
|
* The first bytes of a mount-relative file and its size, through the same
|
|
1250
1494
|
* located descriptor as readPath — for listing a file nothing tracks, which
|
|
@@ -1310,6 +1554,15 @@ export interface Walk {
|
|
|
1310
1554
|
* file name): present, unproven.
|
|
1311
1555
|
*/
|
|
1312
1556
|
others: Map<string, string>;
|
|
1557
|
+
/**
|
|
1558
|
+
* The entries in `others` that the mount's include doesn't cover and that
|
|
1559
|
+
* are neither directories nor links (a regular file, a FIFO, a socket or a
|
|
1560
|
+
* device): present, unproven, and holding nothing beneath them. Only provenAbsent
|
|
1561
|
+
* reads it, for a directory listed whole. A path beneath a name passed
|
|
1562
|
+
* over by design is never decided (reconcile), so those need no such
|
|
1563
|
+
* record.
|
|
1564
|
+
*/
|
|
1565
|
+
otherFiles: Set<string>;
|
|
1313
1566
|
/**
|
|
1314
1567
|
* Directories whose listing the walk read whole, and that were still the
|
|
1315
1568
|
* directory at their path once it had: every name in one is in `files`,
|
|
@@ -1395,6 +1648,84 @@ export function coveredByIgnore(relativePath: string, patterns: string[]): boole
|
|
|
1395
1648
|
return false;
|
|
1396
1649
|
}
|
|
1397
1650
|
|
|
1651
|
+
/** Why a walk passes over a file or link the mount's `include` doesn't cover, and a push leaves one alone. */
|
|
1652
|
+
export const OUTSIDE_INCLUDE = "outside the mount's include";
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* The segments of a path after its mount's name, as the workspace reads them
|
|
1656
|
+
* everywhere: split at `/`, and at `\` too where that's the platform's
|
|
1657
|
+
* separator, with no empty or `.` segment. A `..` segment stays, for the
|
|
1658
|
+
* parse to take back the segment before it or refuse as a climb.
|
|
1659
|
+
*/
|
|
1660
|
+
export function pathSegments(path: string, platformSep: string = sep): string[] {
|
|
1661
|
+
return (platformSep === '\\' ? path.replaceAll('\\', '/') : path).split('/').filter((s) => s !== '' && s !== '.');
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* Whether a mount-relative path is spelled as the workspace's parsing spells
|
|
1666
|
+
* one: segments between single `/`, none empty, `.` or `..`, with no `\`
|
|
1667
|
+
* where that's the separator, or '' for the root. A walk and the watcher name
|
|
1668
|
+
* paths this way. An earlier version stored a path as it was written, so the
|
|
1669
|
+
* store can hold a key such as `a//b.txt` or `c/`, which no spelling names.
|
|
1670
|
+
*/
|
|
1671
|
+
export function parsedSpelling(relativePath: string, platformSep: string = sep): boolean {
|
|
1672
|
+
if (relativePath === '') return true;
|
|
1673
|
+
const segments = pathSegments(relativePath, platformSep);
|
|
1674
|
+
return !segments.includes('..') && segments.join('/') === relativePath;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Whether a mount-relative path is inside its mount: spelled as parsing spells
|
|
1679
|
+
* one (parsedSpelling), and covered by the mount's `include` when it has one.
|
|
1680
|
+
* The mount's root, '', is inside. A store key no spelling names is outside,
|
|
1681
|
+
* as an entry the include doesn't cover is: nothing lists, counts, syncs or
|
|
1682
|
+
* writes it, and history keeps it as it was.
|
|
1683
|
+
*/
|
|
1684
|
+
export function insideMount(relativePath: string, include: readonly string[] | undefined): boolean {
|
|
1685
|
+
if (!parsedSpelling(relativePath)) return false;
|
|
1686
|
+
if (include === undefined || relativePath === '') return true;
|
|
1687
|
+
return coveredByInclude(relativePath, include);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Whether the include's matcher can read `pattern`: a name or an `*.ext` glob,
|
|
1692
|
+
* which covers a file by its own name, or a `dir/**` glob, with no empty, `.`
|
|
1693
|
+
* or `..` segment, since those name no entry a path reaches. Where `\` is the
|
|
1694
|
+
* platform's separator, as on Windows, a pattern holding one can't name a
|
|
1695
|
+
* file either, since every path is read with `\` as a separator
|
|
1696
|
+
* (pathSegments). A pattern the matcher can't read would match nothing, and
|
|
1697
|
+
* take every file out of the mount unsaid, so the module refuses it.
|
|
1698
|
+
*/
|
|
1699
|
+
export function includePatternReadable(pattern: unknown, platformSep: string = sep): boolean {
|
|
1700
|
+
const segment = (s: string): boolean => s.length > 0 && s !== '.' && s !== '..' && !(platformSep === '\\' && s.includes('\\'));
|
|
1701
|
+
const name = (s: string): boolean => segment(s) && !s.includes('/') &&
|
|
1702
|
+
(!s.includes('*') || (s.startsWith('*.') && s.length > 2 && !s.slice(1).includes('*')));
|
|
1703
|
+
return typeof pattern === 'string' &&
|
|
1704
|
+
(pattern.endsWith('/**') ? pattern.length > 3 && pattern.slice(0, -3).split('/').every((s) => segment(s) && !s.includes('*')) : name(pattern));
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* Whether a mount's include covers a mount-relative path. Its patterns take
|
|
1709
|
+
* `ignore`'s forms, read for files: a name or `*.ext` pattern covers a path by
|
|
1710
|
+
* its own last segment, at any depth, and `dir/**` covers what lies beneath
|
|
1711
|
+
* `dir`, and not a file named `dir`. Unlike `ignore`, a pattern that matches
|
|
1712
|
+
* a directory's name covers nothing beneath it: each file is covered by its
|
|
1713
|
+
* own name, so `gate.json` covers `x/gate.json` and not `x/gate.json/run.sh`,
|
|
1714
|
+
* and a subtree is named with `dir/**`. A directory is no file, so it needs
|
|
1715
|
+
* no covering: where one is a scope, refusesAsOutside (index.ts) decides.
|
|
1716
|
+
*/
|
|
1717
|
+
function coveredByInclude(relativePath: string, include: readonly string[]): boolean {
|
|
1718
|
+
const name = relativePath.slice(relativePath.lastIndexOf('/') + 1);
|
|
1719
|
+
for (const pattern of include) {
|
|
1720
|
+
if (pattern.endsWith('/**')) {
|
|
1721
|
+
if (relativePath.startsWith(pattern.slice(0, -2))) return true;
|
|
1722
|
+
} else if (pattern === name || (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))) {
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1398
1729
|
/**
|
|
1399
1730
|
* The ignore patterns a walk that names `scope` reads beneath it: a pattern
|
|
1400
1731
|
* covering the scope itself, or a directory above it, doesn't apply beneath
|
|
@@ -1424,9 +1755,18 @@ export const IGNORED = 'ignored by the mount';
|
|
|
1424
1755
|
/** The walk's reason for an entry `EDITOR_FILE` names. */
|
|
1425
1756
|
const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
1426
1757
|
|
|
1758
|
+
/**
|
|
1759
|
+
* The walk's reason for a link it doesn't enter. On a mount with an include,
|
|
1760
|
+
* a link the include's names don't cover is passed over with it, by design,
|
|
1761
|
+
* since the walk takes no name the include leaves out as a file: on a mount
|
|
1762
|
+
* that follows links, one that leads to a directory, and on one that
|
|
1763
|
+
* doesn't, every one.
|
|
1764
|
+
*/
|
|
1765
|
+
const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
1766
|
+
|
|
1427
1767
|
/** Whether a reason the walk gave an entry is one it passes over by design. */
|
|
1428
1768
|
export function passedOverByDesign(why: string | undefined): boolean {
|
|
1429
|
-
return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT;
|
|
1769
|
+
return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT || why === OUTSIDE_INCLUDE || why === LINK_NOT_WALKED;
|
|
1430
1770
|
}
|
|
1431
1771
|
|
|
1432
1772
|
/** Why a pass passes over a directory that another mount nested in this one holds, and what lies beneath it. */
|
|
@@ -1548,7 +1888,7 @@ export async function walkScope(
|
|
|
1548
1888
|
opts: { named?: boolean; resumeAfter?: string } = {},
|
|
1549
1889
|
): Promise<Walk> {
|
|
1550
1890
|
const walk: Walk = {
|
|
1551
|
-
files: new Set(), seen: new Set(), othersTaken: new Set(), dirs: new Set(), others: new Map(), complete: new Set(), missing: new Set(),
|
|
1891
|
+
files: new Set(), seen: new Set(), othersTaken: new Set(), dirs: new Set(), others: new Map(), otherFiles: new Set(), complete: new Set(), missing: new Set(),
|
|
1552
1892
|
incomplete: [],
|
|
1553
1893
|
};
|
|
1554
1894
|
// A walk that isn't named passes over a scope the mount passes over by
|
|
@@ -1638,6 +1978,7 @@ export async function walkScope(
|
|
|
1638
1978
|
// The links, ignored names and other entries those listings showed, and
|
|
1639
1979
|
// the regions they gave rise to, described the directory that was there.
|
|
1640
1980
|
for (const [path] of walk.others) if (beneath(path) && !walk.othersTaken.has(path)) walk.others.delete(path);
|
|
1981
|
+
for (const path of walk.otherFiles) if (!walk.others.has(path)) walk.otherFiles.delete(path);
|
|
1641
1982
|
for (let i = walk.incomplete.length - 1; i >= 0; i--) {
|
|
1642
1983
|
const region = walk.incomplete[i]!;
|
|
1643
1984
|
if ((region.kind === 'symlink' || region.kind === 'ignored') && beneath(region.path)) walk.incomplete.splice(i, 1);
|
|
@@ -1685,18 +2026,55 @@ export async function walkScope(
|
|
|
1685
2026
|
return null;
|
|
1686
2027
|
}
|
|
1687
2028
|
entries.sort((a, b) => compareNames(a.name, b.name));
|
|
2029
|
+
// A scope named through a link is listed from where the link leads, so
|
|
2030
|
+
// a file in it is judged where it lies as well as by its path, as
|
|
2031
|
+
// coversLanding judges where a path lands (judgedAt, once for the
|
|
2032
|
+
// listing).
|
|
2033
|
+
const judgedDir = view.include === undefined ? rel : await judgedAt(rootReal, dir.real, rel);
|
|
1688
2034
|
const leaves: Array<[string, boolean]> = [];
|
|
1689
2035
|
const subdirs: string[] = [];
|
|
1690
2036
|
const others: Array<[string, string]> = [];
|
|
2037
|
+
const otherFiles: string[] = [];
|
|
1691
2038
|
const regions: Walk['incomplete'] = [];
|
|
1692
2039
|
for (const entry of entries) {
|
|
1693
2040
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1694
|
-
|
|
2041
|
+
// Another mount's directory is passed over by its real path too, so a
|
|
2042
|
+
// walk that reaches one through a link to a directory above it lists
|
|
2043
|
+
// none of that mount's names (nestedAtReal).
|
|
2044
|
+
const passedOver = passedOverBecause(childRel, entry.name, ignore, view.others)
|
|
2045
|
+
?? (entry.isDirectory() && nestedAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
|
|
1695
2046
|
if (passedOver !== undefined) {
|
|
1696
2047
|
others.push([childRel, passedOver]);
|
|
1697
2048
|
regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
|
|
1698
2049
|
continue;
|
|
1699
2050
|
}
|
|
2051
|
+
// A file or link the mount's include doesn't cover is outside the
|
|
2052
|
+
// mount, a named scope's too. A directory is walked, since a name or an
|
|
2053
|
+
// extension glob in include can match beneath it. Anything but a link
|
|
2054
|
+
// (a regular file, a FIFO, a socket or a device) is noted as a file,
|
|
2055
|
+
// since nothing can lie beneath it.
|
|
2056
|
+
if (!entry.isDirectory() && (!insideMount(childRel, view.include) || (judgedDir !== rel && !insideMount(judgedDir === '' ? entry.name : `${judgedDir}/${entry.name}`, view.include)))) {
|
|
2057
|
+
// The walk enters no link. A link gets the reason a mount without an
|
|
2058
|
+
// include gives it, a symlink the walk doesn't enter, where that's
|
|
2059
|
+
// what matters about it. On a mount that follows links, that's a link
|
|
2060
|
+
// that leads to a directory: a name or an extension glob can cover
|
|
2061
|
+
// what lies beneath it, as beneath a directory, and what's read
|
|
2062
|
+
// through it is judged by its path and where it lands, so its own
|
|
2063
|
+
// name doesn't put what lies there outside. On one that doesn't, it's
|
|
2064
|
+
// every link, since nothing reads through one there, and such a mount
|
|
2065
|
+
// resolves none to say where it leads. The link's name is no covered
|
|
2066
|
+
// file's, so the walk passes it over as it does one, rather than
|
|
2067
|
+
// taking it.
|
|
2068
|
+
if (entry.isSymbolicLink() && (!view.followSymlinks || await stat(dir.at(entry.name)).then((info) => info.isDirectory(), () => false))) {
|
|
2069
|
+
others.push([childRel, LINK_NOT_WALKED]);
|
|
2070
|
+
regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'ignored' });
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
others.push([childRel, OUTSIDE_INCLUDE]);
|
|
2074
|
+
if (!entry.isSymbolicLink()) otherFiles.push(childRel);
|
|
2075
|
+
regions.push({ path: childRel, reason: OUTSIDE_INCLUDE, kind: 'ignored' });
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
1700
2078
|
if (entry.isDirectory()) {
|
|
1701
2079
|
subdirs.push(childRel);
|
|
1702
2080
|
} else if (entry.isFile()) {
|
|
@@ -1705,7 +2083,7 @@ export async function walkScope(
|
|
|
1705
2083
|
const symlink = entry.isSymbolicLink();
|
|
1706
2084
|
leaves.push([childRel, false]);
|
|
1707
2085
|
others.push([childRel, symlink ? 'a symlink' : 'not a regular file']);
|
|
1708
|
-
if (symlink) regions.push({ path: childRel, reason:
|
|
2086
|
+
if (symlink) regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
|
|
1709
2087
|
}
|
|
1710
2088
|
}
|
|
1711
2089
|
// A listing of a directory that was moved or replaced meanwhile says
|
|
@@ -1717,6 +2095,7 @@ export async function walkScope(
|
|
|
1717
2095
|
for (const [p, isFile] of leaves) if (isFile) walk.seen.add(p);
|
|
1718
2096
|
for (const d of subdirs) walk.dirs.add(d);
|
|
1719
2097
|
for (const [p, why] of others) walk.others.set(p, why);
|
|
2098
|
+
for (const p of otherFiles) walk.otherFiles.add(p);
|
|
1720
2099
|
walk.incomplete.push(...regions);
|
|
1721
2100
|
walk.complete.add(rel);
|
|
1722
2101
|
const listing = { leaves, subdirs, ...dir.identity() };
|
|
@@ -1733,6 +2112,22 @@ export async function walkScope(
|
|
|
1733
2112
|
return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
|
|
1734
2113
|
};
|
|
1735
2114
|
|
|
2115
|
+
// Each nested root by which directory it is, read once, when the walk
|
|
2116
|
+
// first descends: a directory the walk reaches through a link spelled
|
|
2117
|
+
// otherwise than disk spells it, as a disk that folds names lets one, has
|
|
2118
|
+
// a real path the check by path doesn't know, and is still another
|
|
2119
|
+
// mount's directory.
|
|
2120
|
+
let nestedRoots: Array<{ dev: string; ino: string }> | undefined;
|
|
2121
|
+
const isNestedRoot = async (dir: CheckedDir): Promise<boolean> => {
|
|
2122
|
+
if (!view.nested?.length) return false;
|
|
2123
|
+
nestedRoots ??= (await Promise.all([...new Set(view.nested.flatMap((n) => (n.real === undefined ? [] : [n.real])))].map(async (real) => {
|
|
2124
|
+
const at = await stat(real, { bigint: true }).catch(() => null);
|
|
2125
|
+
return at === null ? [] : [{ dev: String(at.dev), ino: String(at.ino) }];
|
|
2126
|
+
}))).flat();
|
|
2127
|
+
const { dev, ino } = dir.identity();
|
|
2128
|
+
return nestedRoots.some((r) => r.dev === dev && r.ino === ino);
|
|
2129
|
+
};
|
|
2130
|
+
|
|
1736
2131
|
const visit = async (dir: CheckedDir, stretch: Stretch): Promise<void> => {
|
|
1737
2132
|
visited.add(dir.rel);
|
|
1738
2133
|
const listing = await list(dir);
|
|
@@ -1764,6 +2159,16 @@ export async function walkScope(
|
|
|
1764
2159
|
// list() drops it from one that isn't.
|
|
1765
2160
|
walk.missing.delete(sub);
|
|
1766
2161
|
if (child.kind === 'checked') {
|
|
2162
|
+
if (await isNestedRoot(child.dir)) {
|
|
2163
|
+
// Passed over as another mount's directory, as the listing passes
|
|
2164
|
+
// over one it knows by path.
|
|
2165
|
+
walk.dirs.delete(sub);
|
|
2166
|
+
if (!walk.others.has(sub)) {
|
|
2167
|
+
walk.others.set(sub, OTHER_MOUNT);
|
|
2168
|
+
walk.incomplete.push({ path: sub, reason: OTHER_MOUNT, kind: 'ignored' });
|
|
2169
|
+
}
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
1767
2172
|
await visit(child.dir, stretch);
|
|
1768
2173
|
continue;
|
|
1769
2174
|
}
|
|
@@ -1813,9 +2218,12 @@ export async function walkScope(
|
|
|
1813
2218
|
|
|
1814
2219
|
/**
|
|
1815
2220
|
* Whether the walk proves nothing exists at `path`: some ancestor directory
|
|
1816
|
-
* is missing,
|
|
1817
|
-
* to it
|
|
1818
|
-
*
|
|
2221
|
+
* is missing, a completely listed ancestor holds no entry that could lead
|
|
2222
|
+
* to it, or an ancestor is a file the mount's include doesn't cover (anything
|
|
2223
|
+
* but a directory or a link), which the walk passed over and which holds
|
|
2224
|
+
* nothing beneath it.
|
|
2225
|
+
* Ancestors outside the walked scope are passed through, so a walk of `a/b`
|
|
2226
|
+
* decides paths beneath `a/b` and nothing else.
|
|
1819
2227
|
*/
|
|
1820
2228
|
export function provenAbsent(walk: Walk, path: string): boolean {
|
|
1821
2229
|
const parts = path.split('/');
|
|
@@ -1824,6 +2232,7 @@ export function provenAbsent(walk: Walk, path: string): boolean {
|
|
|
1824
2232
|
if (walk.missing.has(dir)) return true;
|
|
1825
2233
|
const child = dir ? `${dir}/${parts[i]}` : parts[i]!;
|
|
1826
2234
|
if (walk.complete.has(dir)) {
|
|
2235
|
+
if (i < parts.length - 1 && walk.otherFiles.has(child)) return true; // a file: nothing beneath it exists
|
|
1827
2236
|
if (walk.others.has(child) || walk.seen.has(child)) return false; // present, but unproven
|
|
1828
2237
|
if (i === parts.length - 1) return !walk.files.has(child) && !walk.dirs.has(child);
|
|
1829
2238
|
if (!walk.dirs.has(child)) return true; // no directory there: nothing beneath can exist
|