@animalabs/agent-framework 0.23.3 → 0.24.1
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 +65 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +98 -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/clock-rows.d.ts +115 -0
- package/dist/src/clock-rows.d.ts.map +1 -0
- package/dist/src/clock-rows.js +222 -0
- package/dist/src/clock-rows.js.map +1 -0
- 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 +1120 -457
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +4635 -2505
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +28 -4
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +89 -13
- 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 +8 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +483 -115
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +1735 -425
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +19 -39
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +27 -59
- 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 +174 -17
- 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 +9 -20
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +7 -49
- 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 +493 -62
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +2216 -652
- 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/timezone.d.ts +4 -2
- package/dist/src/timezone.d.ts.map +1 -1
- package/dist/src/timezone.js +4 -2
- package/dist/src/timezone.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts +46 -7
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +60 -7
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +14 -11
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +17 -14
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +3 -2
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +3 -2
- package/dist/src/tool-result-history.js.map +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 +142 -40
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +31 -6
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +66 -9
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +69 -62
- 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 -8
- package/src/agent.ts +99 -12
- package/src/api/server.ts +14 -12
- package/src/clock-rows.ts +302 -0
- 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 +4978 -2671
- package/src/gate/event-gate.ts +79 -17
- package/src/held-writes.ts +17 -3
- package/src/index.ts +32 -22
- package/src/mcpl/channel-registry.ts +1892 -471
- package/src/mcpl/conversation-router.ts +26 -72
- 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 +200 -22
- package/src/mcpl/push-coalescer.ts +306 -62
- package/src/mcpl/push-handler.ts +12 -54
- 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 +2293 -650
- 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/timezone.ts +4 -2
- package/src/tool-image-ledger.ts +62 -8
- package/src/tool-result-guard.ts +18 -15
- package/src/tool-result-history.ts +3 -2
- package/src/tune-out/coordinator.ts +166 -45
- package/src/types/agent.ts +32 -6
- package/src/types/events.ts +66 -9
- package/src/types/framework.ts +69 -62
- 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
|
import { createHash } from 'node:crypto';
|
|
39
43
|
import { constants as fsConstants } from 'node:fs';
|
|
@@ -154,7 +158,7 @@ export async function linkLeads(root, rootReal, link) {
|
|
|
154
158
|
if (!contained(rootReal, leads))
|
|
155
159
|
return { kind: 'outside' };
|
|
156
160
|
const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
|
|
157
|
-
return { kind: 'inside', target: relative(rootReal, leads).split(sep).join('/'), directory };
|
|
161
|
+
return { kind: 'inside', target: await diskSpelling(rootReal, relative(rootReal, leads).split(sep).join('/')), directory };
|
|
158
162
|
}
|
|
159
163
|
/**
|
|
160
164
|
* The nested mount whose directory holds `target` (mount-relative), the
|
|
@@ -166,6 +170,113 @@ export function nestedHolder(target, nested) {
|
|
|
166
170
|
.filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
|
|
167
171
|
.sort((a, b) => b.dir.length - a.dir.length)[0];
|
|
168
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether a link's refusal names `holder`, the nested mount whose directory
|
|
175
|
+
* holds the link's target. The target's path is one of that mount's own, so
|
|
176
|
+
* its include decides: the refusal names a directory, or a file the include
|
|
177
|
+
* covers. A refusal's code follows the same test (linkReadError's
|
|
178
|
+
* `other_mount`), so the code says no more than the message.
|
|
179
|
+
*/
|
|
180
|
+
export function namesHolder(leads, holder) {
|
|
181
|
+
return namesNestedPlace(leads.target.slice(holder.dir.length + 1), holder.include, leads.directory);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Whether a refusal names a place in a nested mount's directory, `rest`
|
|
185
|
+
* beneath that mount's root, as a place of that mount's: a directory, or a
|
|
186
|
+
* file its include covers. The place is one of that mount's own paths, so
|
|
187
|
+
* its include decides, and a refusal's code follows the same test, so the
|
|
188
|
+
* code says no more than the message. Anywhere else the place is no mount's:
|
|
189
|
+
* the outer mount passes over the nested directory, and the nested mount's
|
|
190
|
+
* include leaves it out. So a link the outer mount doesn't follow is refused
|
|
191
|
+
* as one it doesn't follow (namesHolder), and a path that reaches the place
|
|
192
|
+
* through one it follows is refused as one that leaves the mount, in the
|
|
193
|
+
* words each such refusal already has (nestedAtReal's callers).
|
|
194
|
+
*/
|
|
195
|
+
export function namesNestedPlace(rest, include, directory) {
|
|
196
|
+
return directory || insideMount(rest, include);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether a path of a mount is one its passes decide and its listings may
|
|
200
|
+
* name: inside its include, and, in a nested mount's directory, a place
|
|
201
|
+
* that mount names (namesNestedPlace), its directory included. History the
|
|
202
|
+
* mount took in before another was nested there, which that mount's
|
|
203
|
+
* include leaves out, is no mount's, so no pass decides it, and no listing
|
|
204
|
+
* names it or a directory only it implies.
|
|
205
|
+
*/
|
|
206
|
+
export function withinIncludes(view, path) {
|
|
207
|
+
if (!insideMount(path, view.include))
|
|
208
|
+
return false;
|
|
209
|
+
const holder = nestedHolder(path, view.nested ?? []);
|
|
210
|
+
return holder === undefined || path === holder.dir || namesNestedPlace(path.slice(holder.dir.length + 1), holder.include, false);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The path a mount's include judges for `given`, one of the mount's paths,
|
|
214
|
+
* which landed at `real`, its canonical path, inside the mount and in no
|
|
215
|
+
* nested mount's directory. Where realpath couldn't resolve it, as for a loop
|
|
216
|
+
* or a directory this process can't search, `real` is the path as
|
|
217
|
+
* path.resolve reads it, which this judges as named, so a caller refuses such
|
|
218
|
+
* a path first, as landingRefusal does (realBeneath's `unresolved`). The
|
|
219
|
+
* include is the operator's statement of which files' content the workspace
|
|
220
|
+
* holds, and a link is a path, not a grant of content, so where a followed
|
|
221
|
+
* path lands is judged as where it's named, as a nested mount's place is
|
|
222
|
+
* (namesNestedPlace). A path that meets no link lands where it's named, and
|
|
223
|
+
* is judged as named. realpath can spell such a path otherwise only as a disk
|
|
224
|
+
* that folds names does, with disk's stored names on macOS and Windows where
|
|
225
|
+
* a path keeps a workspace entry's spelling, so where the spellings differ
|
|
226
|
+
* only so, the path is judged as named where it reaches the landing itself,
|
|
227
|
+
* level by level (sameLevels). Otherwise the landing is judged, as disk
|
|
228
|
+
* spells it, since realpath keeps the spelling a link's target gives, which
|
|
229
|
+
* on a disk that folds names need not be disk's (diskSpelling).
|
|
230
|
+
*/
|
|
231
|
+
export async function judgedAt(rootReal, real, given) {
|
|
232
|
+
const landed = relative(rootReal, real).split(sep).join('/');
|
|
233
|
+
if (landed === given)
|
|
234
|
+
return given;
|
|
235
|
+
if (foldedLoosely(landed) === foldedLoosely(given) && await sameLevels(rootReal, given, landed))
|
|
236
|
+
return given;
|
|
237
|
+
return diskSpelling(rootReal, landed);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Whether `given` reaches `landed`, two spellings of a path beneath
|
|
241
|
+
* `rootReal`, level by level: each directory on the way and the file itself
|
|
242
|
+
* are, as `lstat` finds them, one and the same on both sides, so the path as
|
|
243
|
+
* named passes through no link of its own that the landing doesn't. Beneath
|
|
244
|
+
* the last level both sides have, where neither has the next, a file a write
|
|
245
|
+
* would create there lands where it's named. A level only one side has, or
|
|
246
|
+
* one this can't ask, is no such path. It asks each level once, in turn: on a
|
|
247
|
+
* disk that folds names a level both spellings share is one entry, so a link
|
|
248
|
+
* disk puts there after the realpath meets both alike, and a name removed
|
|
249
|
+
* while this asks and put back after reads as a file a write would create.
|
|
250
|
+
* Where a read follows, its check of the opened file catches a link to
|
|
251
|
+
* another file, but not one back to the opened file itself; that race, like
|
|
252
|
+
* one where the spellings match, isn't closed here.
|
|
253
|
+
*/
|
|
254
|
+
async function sameLevels(rootReal, given, landed) {
|
|
255
|
+
const named = given.split('/');
|
|
256
|
+
const landing = landed.split('/');
|
|
257
|
+
const level = (segments, i) => lstat(join(rootReal, ...segments.slice(0, i)), { bigint: true })
|
|
258
|
+
.then((st) => st, (err) => (errno(err) === 'ENOENT' || errno(err) === 'ENOTDIR' ? null : undefined));
|
|
259
|
+
for (let i = 1; i <= named.length; i++) {
|
|
260
|
+
const [a, b] = await Promise.all([level(named, i), level(landing, i)]);
|
|
261
|
+
if (a === undefined || b === undefined)
|
|
262
|
+
return false;
|
|
263
|
+
if (a === null && b === null)
|
|
264
|
+
return true;
|
|
265
|
+
if (a === null || b === null || !sameFile(a, b))
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Whether a mount's own include covers the file one of its paths, `given`,
|
|
272
|
+
* lands on at `real` (judgedAt): a file it leaves out there is no file of
|
|
273
|
+
* this mount's, and a path that reaches it through a link is refused as one
|
|
274
|
+
* that leaves the mount, in the words each such refusal already has
|
|
275
|
+
* (nestedAtReal's callers). A mount without an include covers every file.
|
|
276
|
+
*/
|
|
277
|
+
export async function coversLanding(view, rootReal, real, given) {
|
|
278
|
+
return view.include === undefined || insideMount(await judgedAt(rootReal, real, given), view.include);
|
|
279
|
+
}
|
|
169
280
|
/**
|
|
170
281
|
* Why a path is refused for what linkOnTheWay found, given where a link
|
|
171
282
|
* leads (linkLeads). For a link, the link is the subject, with where it
|
|
@@ -178,9 +289,15 @@ export function nestedHolder(target, nested) {
|
|
|
178
289
|
* path to use there in place of the link, and offers no setting, since
|
|
179
290
|
* following the link would only reach into another mount's directory. For a
|
|
180
291
|
* directory the walk couldn't ask, which one, and the answer it got. Paths
|
|
181
|
-
* are spelled as the tools take them, from the mount's name.
|
|
292
|
+
* are spelled as the tools take them, from the mount's name. On a mount with
|
|
293
|
+
* an include, an inside target is named only where a tool could name it: a
|
|
294
|
+
* file the include covers, or a directory, which a listing names whatever
|
|
295
|
+
* lies beneath it. Anywhere else the refusal says only that the link isn't
|
|
296
|
+
* followed, so no tool names what lies outside the include. A link the
|
|
297
|
+
* include doesn't cover is outside the mount itself, so its refusal tells
|
|
298
|
+
* nothing of where it leads (shownLeads gives null for it).
|
|
182
299
|
*/
|
|
183
|
-
export function linkWords(name, found, leads, nested = []) {
|
|
300
|
+
export function linkWords(name, found, leads, nested = [], include) {
|
|
184
301
|
const spell = (rel) => `"${rel === '' ? name : `${name}/${rel}`}"`;
|
|
185
302
|
if (!('link' in found))
|
|
186
303
|
return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${found.code})`;
|
|
@@ -191,32 +308,52 @@ export function linkWords(name, found, leads, nested = []) {
|
|
|
191
308
|
return `${link} leading outside the mount, which this mount does not follow`;
|
|
192
309
|
const holder = nestedHolder(leads.target, nested);
|
|
193
310
|
if (holder) {
|
|
311
|
+
if (!namesHolder(leads, holder))
|
|
312
|
+
return `${link}, which this mount does not follow`;
|
|
194
313
|
const rest = leads.target.slice(holder.dir.length + 1);
|
|
195
314
|
return `${link} to ${spell(leads.target)}, which this mount does not follow; it lies in mount "${holder.name}", ` +
|
|
196
315
|
`nested in mount "${name}": use "${rest ? `${holder.name}/${rest}` : holder.name}" in place of ${spell(found.link)}`;
|
|
197
316
|
}
|
|
198
|
-
|
|
317
|
+
if (!leads.directory && !insideMount(leads.target, include))
|
|
318
|
+
return `${link}, which this mount does not follow`;
|
|
319
|
+
// Following it serves only what the include covers where it leads (judgedAt).
|
|
320
|
+
const setting = !leads.directory ? '' : include === undefined ? "; the mount's host can set followSymlinks: true"
|
|
321
|
+
: "; the mount's host can set followSymlinks: true, for what the include covers where it leads";
|
|
199
322
|
return `${link} to ${spell(leads.target)}, which this mount does not follow${setting}`;
|
|
200
323
|
}
|
|
201
|
-
/**
|
|
324
|
+
/**
|
|
325
|
+
* Where a link linkOnTheWay found leads, as its refusal may tell it
|
|
326
|
+
* (linkLeads), or null where the refusal tells nothing of it: where the walk
|
|
327
|
+
* found a directory it couldn't ask rather than a link, and where the mount's
|
|
328
|
+
* include doesn't cover the link, which is then outside the mount, so its
|
|
329
|
+
* refusal shows only that a link stands on the path. A refusal's code
|
|
330
|
+
* follows the same answer (linkReadError), so the code says no more than the
|
|
331
|
+
* message.
|
|
332
|
+
*/
|
|
333
|
+
export async function shownLeads(root, rootReal, found, include) {
|
|
334
|
+
return 'link' in found && insideMount(found.link, include) ? linkLeads(root, rootReal, found.link) : null;
|
|
335
|
+
}
|
|
336
|
+
/** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
|
|
202
337
|
export async function linkReason(view, rootReal, found) {
|
|
203
|
-
return linkWords(view.name, found,
|
|
338
|
+
return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
|
|
204
339
|
}
|
|
205
340
|
/**
|
|
206
341
|
* The nested mount whose root holds `real`, a canonical path, the innermost,
|
|
207
|
-
* with the path to use there
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
342
|
+
* with the path to use there, the path beneath its root (`rest`) and its
|
|
343
|
+
* include; undefined when none does. A path the lexical checks pass
|
|
344
|
+
* (inOtherMount, the tools' holderOf) reaches another mount's directory this
|
|
345
|
+
* way through a symlink the mount follows, so each check that resolves a
|
|
346
|
+
* path canonically asks this too, and names the mount only where
|
|
347
|
+
* namesNestedPlace says the place is that mount's.
|
|
211
348
|
*/
|
|
212
349
|
export function nestedAtReal(real, nested = []) {
|
|
213
350
|
const holder = nested
|
|
214
|
-
.flatMap((n) => (n.real !== undefined && contained(n.real, real) ? [{ name: n.name, real: n.real }] : []))
|
|
351
|
+
.flatMap((n) => (n.real !== undefined && contained(n.real, real) ? [{ name: n.name, real: n.real, include: n.include }] : []))
|
|
215
352
|
.sort((a, b) => b.real.length - a.real.length)[0];
|
|
216
353
|
if (!holder)
|
|
217
354
|
return undefined;
|
|
218
355
|
const rest = relative(holder.real, real).split(sep).join('/');
|
|
219
|
-
return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name };
|
|
356
|
+
return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, rest, include: holder.include };
|
|
220
357
|
}
|
|
221
358
|
/**
|
|
222
359
|
* Why a mount-relative path that resolves into a nested mount's directory is
|
|
@@ -345,6 +482,20 @@ function entryName(paths, prefix, segment) {
|
|
|
345
482
|
}
|
|
346
483
|
return found;
|
|
347
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Whether a path's segments begin with a directory's: exactly, or, with
|
|
487
|
+
* `loosely`, with each segment compared as loosely as listedName compares
|
|
488
|
+
* names (foldedLoosely), as a disk that folds names may compare them. That
|
|
489
|
+
* errs toward the directory: on a disk that tells some of those spellings
|
|
490
|
+
* apart, such as case, or `ß` from `ss`, a distinct directory spelled one of
|
|
491
|
+
* them matches too, and resolveAbsolutePath, the one caller that compares
|
|
492
|
+
* loosely, gives null for a path beneath it.
|
|
493
|
+
*/
|
|
494
|
+
export function beginsWithDir(segments, dir, loosely) {
|
|
495
|
+
if (segments.length < dir.length)
|
|
496
|
+
return false;
|
|
497
|
+
return dir.every((d, i) => segments[i] === d || (loosely && foldedLoosely(segments[i]) === foldedLoosely(d)));
|
|
498
|
+
}
|
|
348
499
|
/**
|
|
349
500
|
* The name `dir` lists for `segment`: `segment` itself, unless disk folds it
|
|
350
501
|
* there and lists it otherwise. `dir` is listed only where a variant of
|
|
@@ -657,10 +808,14 @@ async function locateDir(view, rootReal, rel) {
|
|
|
657
808
|
}
|
|
658
809
|
else {
|
|
659
810
|
// Through a link this mount follows, a directory can be a nested
|
|
660
|
-
// mount's, which the mount neither reads nor writes.
|
|
811
|
+
// mount's, which the mount neither reads nor writes. A place there that
|
|
812
|
+
// mount's include leaves out is no mount's, and holds nothing beneath it,
|
|
813
|
+
// as at any file (namesNestedPlace).
|
|
661
814
|
const holder = nestedAtReal(real, view.nested);
|
|
662
|
-
if (holder)
|
|
663
|
-
|
|
815
|
+
if (holder) {
|
|
816
|
+
const directory = holder.include !== undefined && await stat(real).then((info) => info.isDirectory(), () => false);
|
|
817
|
+
return namesNestedPlace(holder.rest, holder.include, directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
|
|
818
|
+
}
|
|
664
819
|
}
|
|
665
820
|
const checked = await checkedAt(view, rel, real);
|
|
666
821
|
if (checked) {
|
|
@@ -895,10 +1050,11 @@ export async function filesystemTrustsCtime(root) {
|
|
|
895
1050
|
// Paths and files
|
|
896
1051
|
// ===========================================================================
|
|
897
1052
|
/**
|
|
898
|
-
* Where a path's parent directory canonically lies: inside the mount,
|
|
899
|
-
* (absence is then proven separately), or somewhere a
|
|
900
|
-
* still reach through — O_NOFOLLOW guards only the
|
|
901
|
-
* on a mount that doesn't follow symlinks is any
|
|
1053
|
+
* Where a path's parent directory canonically lies: inside the mount, with
|
|
1054
|
+
* that real path, gone (absence is then proven separately), or somewhere a
|
|
1055
|
+
* no-follow open would still reach through — O_NOFOLLOW guards only the
|
|
1056
|
+
* final component — which on a mount that doesn't follow symlinks is any
|
|
1057
|
+
* symlink on the way.
|
|
902
1058
|
*/
|
|
903
1059
|
async function parentPlacement(view, rootReal, relativePath) {
|
|
904
1060
|
try {
|
|
@@ -909,7 +1065,12 @@ async function parentPlacement(view, rootReal, relativePath) {
|
|
|
909
1065
|
if (!contained(rootReal, parent))
|
|
910
1066
|
return { reason: OUTSIDE_PARENT };
|
|
911
1067
|
const holder = nestedAtReal(parent, view.nested);
|
|
912
|
-
|
|
1068
|
+
if (!holder)
|
|
1069
|
+
return { inside: parent };
|
|
1070
|
+
// A place there that the nested mount's include leaves out is no mount's,
|
|
1071
|
+
// and holds nothing beneath it, as beneath any file (namesNestedPlace).
|
|
1072
|
+
const directory = holder.include !== undefined && await stat(parent).then((info) => info.isDirectory(), () => false);
|
|
1073
|
+
return namesNestedPlace(holder.rest, holder.include, directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
|
|
913
1074
|
}
|
|
914
1075
|
catch (err) {
|
|
915
1076
|
const code = errno(err);
|
|
@@ -934,7 +1095,7 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
934
1095
|
const placement = await parentPlacement(view, rootReal, relativePath);
|
|
935
1096
|
if (placement === 'gone')
|
|
936
1097
|
return absence(view, rootReal, relativePath);
|
|
937
|
-
if (
|
|
1098
|
+
if ('reason' in placement)
|
|
938
1099
|
return { kind: 'unobserved', reason: placement.reason };
|
|
939
1100
|
let info;
|
|
940
1101
|
try {
|
|
@@ -966,11 +1127,20 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
966
1127
|
if (!contained(rootReal, real))
|
|
967
1128
|
return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
|
|
968
1129
|
const holder = nestedAtReal(real, view.nested);
|
|
969
|
-
|
|
970
|
-
|
|
1130
|
+
// A place a nested mount's include leaves out is no mount's, so the link
|
|
1131
|
+
// leaves this one (namesNestedPlace).
|
|
1132
|
+
if (holder) {
|
|
1133
|
+
return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, target.isDirectory()) ? leadsInto(view.name, relativePath, holder) : 'a symlink that leaves the mount' };
|
|
1134
|
+
}
|
|
971
1135
|
if (!target.isFile()) {
|
|
972
1136
|
return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : 'not a regular file' };
|
|
973
1137
|
}
|
|
1138
|
+
// A file this mount's own include leaves out where the link lands is no
|
|
1139
|
+
// file of this mount's (coversLanding). It's judged here, and not only
|
|
1140
|
+
// where the file is opened, since a pass takes a copy whose fingerprint
|
|
1141
|
+
// the stat vouches for without opening it.
|
|
1142
|
+
if (!(await coversLanding(view, rootReal, real, relativePath)))
|
|
1143
|
+
return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
|
|
974
1144
|
info = target;
|
|
975
1145
|
}
|
|
976
1146
|
else if (info.isDirectory()) {
|
|
@@ -979,6 +1149,10 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
979
1149
|
else if (!info.isFile()) {
|
|
980
1150
|
return { kind: 'unobserved', reason: 'not a regular file' };
|
|
981
1151
|
}
|
|
1152
|
+
else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
|
|
1153
|
+
// So is one reached through a link among its directories.
|
|
1154
|
+
return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
|
|
1155
|
+
}
|
|
982
1156
|
return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 511n), fp: statFingerprint(info) };
|
|
983
1157
|
}
|
|
984
1158
|
/**
|
|
@@ -1032,12 +1206,17 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1032
1206
|
return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
|
|
1033
1207
|
if (!info.isFile())
|
|
1034
1208
|
return { kind: 'unobserved', reason: info.isDirectory() ? 'a directory' : 'not a regular file' };
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1209
|
+
const leaves = view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT;
|
|
1210
|
+
if (!contained(rootReal, real))
|
|
1211
|
+
return { kind: 'unobserved', reason: leaves };
|
|
1038
1212
|
const holder = nestedAtReal(real, view.nested);
|
|
1213
|
+
// A place a nested mount's include leaves out is no mount's. Reached
|
|
1214
|
+
// through a link this mount doesn't follow, the link is what's refused.
|
|
1039
1215
|
if (holder)
|
|
1040
|
-
return { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder) };
|
|
1216
|
+
return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1217
|
+
// So is a file this mount's own include leaves out (coversLanding).
|
|
1218
|
+
if (!(await coversLanding(view, rootReal, real, relativePath)))
|
|
1219
|
+
return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1041
1220
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
1042
1221
|
if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
|
|
1043
1222
|
return { kind: 'unobserved', reason: 'the file changed while it was read' };
|
|
@@ -1167,6 +1346,48 @@ export async function readPath(view, rootReal, relativePath, limit) {
|
|
|
1167
1346
|
await handle.close();
|
|
1168
1347
|
}
|
|
1169
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* A mount-relative file's bytes, through the same located descriptor as
|
|
1351
|
+
* readPath, for a direct mount's tools, which keep nothing: at most `limit`
|
|
1352
|
+
* of them, and none hashed. A file over `limit` at its fstat has only its
|
|
1353
|
+
* head read, and one that grew past it since is read no further than it.
|
|
1354
|
+
* What the open refuses gets the reason a listing gives it, such as a link
|
|
1355
|
+
* to a directory, except a regular file it couldn't open, which keeps the
|
|
1356
|
+
* open's.
|
|
1357
|
+
*/
|
|
1358
|
+
export async function readBytes(view, rootReal, relativePath, limit) {
|
|
1359
|
+
const opened = await openContained(view, rootReal, relativePath);
|
|
1360
|
+
if (!('handle' in opened)) {
|
|
1361
|
+
// A walk takes a link to a directory as an entry, where the mount follows
|
|
1362
|
+
// links, and the open may refuse it as a directory or, where it can't
|
|
1363
|
+
// be opened, by its errno: either way it's named as a listing names it
|
|
1364
|
+
// (observePath).
|
|
1365
|
+
if (opened.kind === 'unobserved') {
|
|
1366
|
+
const seen = await observePath(view, rootReal, relativePath);
|
|
1367
|
+
return seen.kind === 'file' ? opened : seen;
|
|
1368
|
+
}
|
|
1369
|
+
return opened;
|
|
1370
|
+
}
|
|
1371
|
+
const { handle, info } = opened;
|
|
1372
|
+
try {
|
|
1373
|
+
const size = Number(info.size);
|
|
1374
|
+
if (size > limit) {
|
|
1375
|
+
const head = Buffer.alloc(Math.min(HEAD_BYTES, size));
|
|
1376
|
+
const { bytesRead } = await handle.read(head, 0, head.length, 0);
|
|
1377
|
+
return { kind: 'bytes', size, bytes: null, head: head.subarray(0, bytesRead) };
|
|
1378
|
+
}
|
|
1379
|
+
const { bytes, more } = await readPrefix(handle, limit, size);
|
|
1380
|
+
if (more)
|
|
1381
|
+
return { kind: 'bytes', size: Math.max(Number((await handle.stat({ bigint: true })).size), bytes.length + 1), bytes: null, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1382
|
+
return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1383
|
+
}
|
|
1384
|
+
catch (err) {
|
|
1385
|
+
return { kind: 'unobserved', reason: `cannot read (${errno(err) ?? String(err)})` };
|
|
1386
|
+
}
|
|
1387
|
+
finally {
|
|
1388
|
+
await handle.close();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1170
1391
|
/**
|
|
1171
1392
|
* The first bytes of a mount-relative file and its size, through the same
|
|
1172
1393
|
* located descriptor as readPath — for listing a file nothing tracks, which
|
|
@@ -1271,6 +1492,83 @@ export function coveredByIgnore(relativePath, patterns) {
|
|
|
1271
1492
|
}
|
|
1272
1493
|
return false;
|
|
1273
1494
|
}
|
|
1495
|
+
/** Why a walk passes over a file or link the mount's `include` doesn't cover, and a push leaves one alone. */
|
|
1496
|
+
export const OUTSIDE_INCLUDE = "outside the mount's include";
|
|
1497
|
+
/**
|
|
1498
|
+
* The segments of a path after its mount's name, as the workspace reads them
|
|
1499
|
+
* everywhere: split at `/`, and at `\` too where that's the platform's
|
|
1500
|
+
* separator, with no empty or `.` segment. A `..` segment stays, for the
|
|
1501
|
+
* parse to take back the segment before it or refuse as a climb.
|
|
1502
|
+
*/
|
|
1503
|
+
export function pathSegments(path, platformSep = sep) {
|
|
1504
|
+
return (platformSep === '\\' ? path.replaceAll('\\', '/') : path).split('/').filter((s) => s !== '' && s !== '.');
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Whether a mount-relative path is spelled as the workspace's parsing spells
|
|
1508
|
+
* one: segments between single `/`, none empty, `.` or `..`, with no `\`
|
|
1509
|
+
* where that's the separator, or '' for the root. A walk and the watcher name
|
|
1510
|
+
* paths this way. An earlier version stored a path as it was written, so the
|
|
1511
|
+
* store can hold a key such as `a//b.txt` or `c/`, which no spelling names.
|
|
1512
|
+
*/
|
|
1513
|
+
export function parsedSpelling(relativePath, platformSep = sep) {
|
|
1514
|
+
if (relativePath === '')
|
|
1515
|
+
return true;
|
|
1516
|
+
const segments = pathSegments(relativePath, platformSep);
|
|
1517
|
+
return !segments.includes('..') && segments.join('/') === relativePath;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Whether a mount-relative path is inside its mount: spelled as parsing spells
|
|
1521
|
+
* one (parsedSpelling), and covered by the mount's `include` when it has one.
|
|
1522
|
+
* The mount's root, '', is inside. A store key no spelling names is outside,
|
|
1523
|
+
* as an entry the include doesn't cover is: nothing lists, counts, syncs or
|
|
1524
|
+
* writes it, and history keeps it as it was.
|
|
1525
|
+
*/
|
|
1526
|
+
export function insideMount(relativePath, include) {
|
|
1527
|
+
if (!parsedSpelling(relativePath))
|
|
1528
|
+
return false;
|
|
1529
|
+
if (include === undefined || relativePath === '')
|
|
1530
|
+
return true;
|
|
1531
|
+
return coveredByInclude(relativePath, include);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Whether the include's matcher can read `pattern`: a name or an `*.ext` glob,
|
|
1535
|
+
* which covers a file by its own name, or a `dir/**` glob, with no empty, `.`
|
|
1536
|
+
* or `..` segment, since those name no entry a path reaches. Where `\` is the
|
|
1537
|
+
* platform's separator, as on Windows, a pattern holding one can't name a
|
|
1538
|
+
* file either, since every path is read with `\` as a separator
|
|
1539
|
+
* (pathSegments). A pattern the matcher can't read would match nothing, and
|
|
1540
|
+
* take every file out of the mount unsaid, so the module refuses it.
|
|
1541
|
+
*/
|
|
1542
|
+
export function includePatternReadable(pattern, platformSep = sep) {
|
|
1543
|
+
const segment = (s) => s.length > 0 && s !== '.' && s !== '..' && !(platformSep === '\\' && s.includes('\\'));
|
|
1544
|
+
const name = (s) => segment(s) && !s.includes('/') &&
|
|
1545
|
+
(!s.includes('*') || (s.startsWith('*.') && s.length > 2 && !s.slice(1).includes('*')));
|
|
1546
|
+
return typeof pattern === 'string' &&
|
|
1547
|
+
(pattern.endsWith('/**') ? pattern.length > 3 && pattern.slice(0, -3).split('/').every((s) => segment(s) && !s.includes('*')) : name(pattern));
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Whether a mount's include covers a mount-relative path. Its patterns take
|
|
1551
|
+
* `ignore`'s forms, read for files: a name or `*.ext` pattern covers a path by
|
|
1552
|
+
* its own last segment, at any depth, and `dir/**` covers what lies beneath
|
|
1553
|
+
* `dir`, and not a file named `dir`. Unlike `ignore`, a pattern that matches
|
|
1554
|
+
* a directory's name covers nothing beneath it: each file is covered by its
|
|
1555
|
+
* own name, so `gate.json` covers `x/gate.json` and not `x/gate.json/run.sh`,
|
|
1556
|
+
* and a subtree is named with `dir/**`. A directory is no file, so it needs
|
|
1557
|
+
* no covering: where one is a scope, refusesAsOutside (index.ts) decides.
|
|
1558
|
+
*/
|
|
1559
|
+
function coveredByInclude(relativePath, include) {
|
|
1560
|
+
const name = relativePath.slice(relativePath.lastIndexOf('/') + 1);
|
|
1561
|
+
for (const pattern of include) {
|
|
1562
|
+
if (pattern.endsWith('/**')) {
|
|
1563
|
+
if (relativePath.startsWith(pattern.slice(0, -2)))
|
|
1564
|
+
return true;
|
|
1565
|
+
}
|
|
1566
|
+
else if (pattern === name || (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))) {
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1274
1572
|
/**
|
|
1275
1573
|
* The ignore patterns a walk that names `scope` reads beneath it: a pattern
|
|
1276
1574
|
* covering the scope itself, or a directory above it, doesn't apply beneath
|
|
@@ -1297,9 +1595,17 @@ const EDITOR_FILE = /^\..+\.sw[px]$|~$|\.subl.*\.tmp$/;
|
|
|
1297
1595
|
export const IGNORED = 'ignored by the mount';
|
|
1298
1596
|
/** The walk's reason for an entry `EDITOR_FILE` names. */
|
|
1299
1597
|
const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
1598
|
+
/**
|
|
1599
|
+
* The walk's reason for a link it doesn't enter. On a mount with an include,
|
|
1600
|
+
* a link the include's names don't cover is passed over with it, by design,
|
|
1601
|
+
* since the walk takes no name the include leaves out as a file: on a mount
|
|
1602
|
+
* that follows links, one that leads to a directory, and on one that
|
|
1603
|
+
* doesn't, every one.
|
|
1604
|
+
*/
|
|
1605
|
+
const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
1300
1606
|
/** Whether a reason the walk gave an entry is one it passes over by design. */
|
|
1301
1607
|
export function passedOverByDesign(why) {
|
|
1302
|
-
return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT;
|
|
1608
|
+
return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT || why === OUTSIDE_INCLUDE || why === LINK_NOT_WALKED;
|
|
1303
1609
|
}
|
|
1304
1610
|
/** Why a pass passes over a directory that another mount nested in this one holds, and what lies beneath it. */
|
|
1305
1611
|
export const OTHER_MOUNT = "another mount's directory";
|
|
@@ -1423,7 +1729,7 @@ async function checkChild(view, rootReal, parent, rel) {
|
|
|
1423
1729
|
*/
|
|
1424
1730
|
export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}) {
|
|
1425
1731
|
const walk = {
|
|
1426
|
-
files: new Set(), seen: new Set(), othersTaken: new Set(), dirs: new Set(), others: new Map(), complete: new Set(), missing: new Set(),
|
|
1732
|
+
files: new Set(), seen: new Set(), othersTaken: new Set(), dirs: new Set(), others: new Map(), otherFiles: new Set(), complete: new Set(), missing: new Set(),
|
|
1427
1733
|
incomplete: [],
|
|
1428
1734
|
};
|
|
1429
1735
|
// A walk that isn't named passes over a scope the mount passes over by
|
|
@@ -1516,6 +1822,9 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1516
1822
|
for (const [path] of walk.others)
|
|
1517
1823
|
if (beneath(path) && !walk.othersTaken.has(path))
|
|
1518
1824
|
walk.others.delete(path);
|
|
1825
|
+
for (const path of walk.otherFiles)
|
|
1826
|
+
if (!walk.others.has(path))
|
|
1827
|
+
walk.otherFiles.delete(path);
|
|
1519
1828
|
for (let i = walk.incomplete.length - 1; i >= 0; i--) {
|
|
1520
1829
|
const region = walk.incomplete[i];
|
|
1521
1830
|
if ((region.kind === 'symlink' || region.kind === 'ignored') && beneath(region.path))
|
|
@@ -1569,18 +1878,56 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1569
1878
|
return null;
|
|
1570
1879
|
}
|
|
1571
1880
|
entries.sort((a, b) => compareNames(a.name, b.name));
|
|
1881
|
+
// A scope named through a link is listed from where the link leads, so
|
|
1882
|
+
// a file in it is judged where it lies as well as by its path, as
|
|
1883
|
+
// coversLanding judges where a path lands (judgedAt, once for the
|
|
1884
|
+
// listing).
|
|
1885
|
+
const judgedDir = view.include === undefined ? rel : await judgedAt(rootReal, dir.real, rel);
|
|
1572
1886
|
const leaves = [];
|
|
1573
1887
|
const subdirs = [];
|
|
1574
1888
|
const others = [];
|
|
1889
|
+
const otherFiles = [];
|
|
1575
1890
|
const regions = [];
|
|
1576
1891
|
for (const entry of entries) {
|
|
1577
1892
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1578
|
-
|
|
1893
|
+
// Another mount's directory is passed over by its real path too, so a
|
|
1894
|
+
// walk that reaches one through a link to a directory above it lists
|
|
1895
|
+
// none of that mount's names (nestedAtReal).
|
|
1896
|
+
const passedOver = passedOverBecause(childRel, entry.name, ignore, view.others)
|
|
1897
|
+
?? (entry.isDirectory() && nestedAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
|
|
1579
1898
|
if (passedOver !== undefined) {
|
|
1580
1899
|
others.push([childRel, passedOver]);
|
|
1581
1900
|
regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
|
|
1582
1901
|
continue;
|
|
1583
1902
|
}
|
|
1903
|
+
// A file or link the mount's include doesn't cover is outside the
|
|
1904
|
+
// mount, a named scope's too. A directory is walked, since a name or an
|
|
1905
|
+
// extension glob in include can match beneath it. Anything but a link
|
|
1906
|
+
// (a regular file, a FIFO, a socket or a device) is noted as a file,
|
|
1907
|
+
// since nothing can lie beneath it.
|
|
1908
|
+
if (!entry.isDirectory() && (!insideMount(childRel, view.include) || (judgedDir !== rel && !insideMount(judgedDir === '' ? entry.name : `${judgedDir}/${entry.name}`, view.include)))) {
|
|
1909
|
+
// The walk enters no link. A link gets the reason a mount without an
|
|
1910
|
+
// include gives it, a symlink the walk doesn't enter, where that's
|
|
1911
|
+
// what matters about it. On a mount that follows links, that's a link
|
|
1912
|
+
// that leads to a directory: a name or an extension glob can cover
|
|
1913
|
+
// what lies beneath it, as beneath a directory, and what's read
|
|
1914
|
+
// through it is judged by its path and where it lands, so its own
|
|
1915
|
+
// name doesn't put what lies there outside. On one that doesn't, it's
|
|
1916
|
+
// every link, since nothing reads through one there, and such a mount
|
|
1917
|
+
// resolves none to say where it leads. The link's name is no covered
|
|
1918
|
+
// file's, so the walk passes it over as it does one, rather than
|
|
1919
|
+
// taking it.
|
|
1920
|
+
if (entry.isSymbolicLink() && (!view.followSymlinks || await stat(dir.at(entry.name)).then((info) => info.isDirectory(), () => false))) {
|
|
1921
|
+
others.push([childRel, LINK_NOT_WALKED]);
|
|
1922
|
+
regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'ignored' });
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
others.push([childRel, OUTSIDE_INCLUDE]);
|
|
1926
|
+
if (!entry.isSymbolicLink())
|
|
1927
|
+
otherFiles.push(childRel);
|
|
1928
|
+
regions.push({ path: childRel, reason: OUTSIDE_INCLUDE, kind: 'ignored' });
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1584
1931
|
if (entry.isDirectory()) {
|
|
1585
1932
|
subdirs.push(childRel);
|
|
1586
1933
|
}
|
|
@@ -1592,7 +1939,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1592
1939
|
leaves.push([childRel, false]);
|
|
1593
1940
|
others.push([childRel, symlink ? 'a symlink' : 'not a regular file']);
|
|
1594
1941
|
if (symlink)
|
|
1595
|
-
regions.push({ path: childRel, reason:
|
|
1942
|
+
regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
|
|
1596
1943
|
}
|
|
1597
1944
|
}
|
|
1598
1945
|
// A listing of a directory that was moved or replaced meanwhile says
|
|
@@ -1608,6 +1955,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1608
1955
|
walk.dirs.add(d);
|
|
1609
1956
|
for (const [p, why] of others)
|
|
1610
1957
|
walk.others.set(p, why);
|
|
1958
|
+
for (const p of otherFiles)
|
|
1959
|
+
walk.otherFiles.add(p);
|
|
1611
1960
|
walk.incomplete.push(...regions);
|
|
1612
1961
|
walk.complete.add(rel);
|
|
1613
1962
|
const listing = { leaves, subdirs, ...dir.identity() };
|
|
@@ -1621,6 +1970,22 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1621
1970
|
const at = subtreeAgainst(dir, after);
|
|
1622
1971
|
return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
|
|
1623
1972
|
};
|
|
1973
|
+
// Each nested root by which directory it is, read once, when the walk
|
|
1974
|
+
// first descends: a directory the walk reaches through a link spelled
|
|
1975
|
+
// otherwise than disk spells it, as a disk that folds names lets one, has
|
|
1976
|
+
// a real path the check by path doesn't know, and is still another
|
|
1977
|
+
// mount's directory.
|
|
1978
|
+
let nestedRoots;
|
|
1979
|
+
const isNestedRoot = async (dir) => {
|
|
1980
|
+
if (!view.nested?.length)
|
|
1981
|
+
return false;
|
|
1982
|
+
nestedRoots ??= (await Promise.all([...new Set(view.nested.flatMap((n) => (n.real === undefined ? [] : [n.real])))].map(async (real) => {
|
|
1983
|
+
const at = await stat(real, { bigint: true }).catch(() => null);
|
|
1984
|
+
return at === null ? [] : [{ dev: String(at.dev), ino: String(at.ino) }];
|
|
1985
|
+
}))).flat();
|
|
1986
|
+
const { dev, ino } = dir.identity();
|
|
1987
|
+
return nestedRoots.some((r) => r.dev === dev && r.ino === ino);
|
|
1988
|
+
};
|
|
1624
1989
|
const visit = async (dir, stretch) => {
|
|
1625
1990
|
visited.add(dir.rel);
|
|
1626
1991
|
const listing = await list(dir);
|
|
@@ -1659,6 +2024,16 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1659
2024
|
// list() drops it from one that isn't.
|
|
1660
2025
|
walk.missing.delete(sub);
|
|
1661
2026
|
if (child.kind === 'checked') {
|
|
2027
|
+
if (await isNestedRoot(child.dir)) {
|
|
2028
|
+
// Passed over as another mount's directory, as the listing passes
|
|
2029
|
+
// over one it knows by path.
|
|
2030
|
+
walk.dirs.delete(sub);
|
|
2031
|
+
if (!walk.others.has(sub)) {
|
|
2032
|
+
walk.others.set(sub, OTHER_MOUNT);
|
|
2033
|
+
walk.incomplete.push({ path: sub, reason: OTHER_MOUNT, kind: 'ignored' });
|
|
2034
|
+
}
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
1662
2037
|
await visit(child.dir, stretch);
|
|
1663
2038
|
continue;
|
|
1664
2039
|
}
|
|
@@ -1714,9 +2089,12 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1714
2089
|
}
|
|
1715
2090
|
/**
|
|
1716
2091
|
* Whether the walk proves nothing exists at `path`: some ancestor directory
|
|
1717
|
-
* is missing,
|
|
1718
|
-
* to it
|
|
1719
|
-
*
|
|
2092
|
+
* is missing, a completely listed ancestor holds no entry that could lead
|
|
2093
|
+
* to it, or an ancestor is a file the mount's include doesn't cover (anything
|
|
2094
|
+
* but a directory or a link), which the walk passed over and which holds
|
|
2095
|
+
* nothing beneath it.
|
|
2096
|
+
* Ancestors outside the walked scope are passed through, so a walk of `a/b`
|
|
2097
|
+
* decides paths beneath `a/b` and nothing else.
|
|
1720
2098
|
*/
|
|
1721
2099
|
export function provenAbsent(walk, path) {
|
|
1722
2100
|
const parts = path.split('/');
|
|
@@ -1726,6 +2104,8 @@ export function provenAbsent(walk, path) {
|
|
|
1726
2104
|
return true;
|
|
1727
2105
|
const child = dir ? `${dir}/${parts[i]}` : parts[i];
|
|
1728
2106
|
if (walk.complete.has(dir)) {
|
|
2107
|
+
if (i < parts.length - 1 && walk.otherFiles.has(child))
|
|
2108
|
+
return true; // a file: nothing beneath it exists
|
|
1729
2109
|
if (walk.others.has(child) || walk.seen.has(child))
|
|
1730
2110
|
return false; // present, but unproven
|
|
1731
2111
|
if (i === parts.length - 1)
|