@animalabs/agent-framework 0.27.0 → 0.29.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 +145 -41
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +73 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +11 -5
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +5 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +9 -5
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +468 -125
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3651 -1111
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +133 -13
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +654 -59
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +173 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +202 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +778 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +133 -54
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +29 -11
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +43 -70
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +70 -30
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +150 -80
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +60 -30
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -55
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +220 -111
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +27 -35
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +313 -91
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +31 -13
- 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 +10 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +15 -9
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- 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 +30 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +26 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +138 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +59 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +67 -50
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +63 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +980 -547
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +125 -82
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +465 -241
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +156 -50
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- 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.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/notice-tags.d.ts +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +52 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/silent-heartbeat.d.ts +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +39 -4
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +63 -18
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +58 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -13
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +76 -12
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +11 -5
- package/src/auto-continue.ts +12 -5
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +3772 -1138
- package/src/gate/event-gate.ts +705 -60
- package/src/gate/index.ts +9 -1
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +176 -3
- package/src/heartbeat/heartbeat.ts +843 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +5 -1
- package/src/line-reader.ts +58 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +139 -53
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +45 -70
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +76 -34
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +149 -82
- package/src/mcpl/tool-signature.ts +64 -32
- package/src/mcpl/toolset-host.ts +212 -110
- package/src/mcpl/toolsets-module.ts +277 -91
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +32 -13
- package/src/module-registry.ts +14 -9
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +30 -18
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +151 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +59 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +69 -50
- package/src/modules/workspace/index.ts +966 -556
- package/src/modules/workspace/observe.ts +497 -242
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +184 -49
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/notice-tags.ts +84 -0
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +64 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +38 -4
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/coordinator.ts +25 -27
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +63 -18
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +59 -0
- package/src/types/module.ts +28 -13
- package/src/types/trace.ts +36 -1
|
@@ -44,6 +44,7 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
44
44
|
import { lstat, mkdir, open, readdir, readlink, realpath, stat, statfs } from 'node:fs/promises';
|
|
45
45
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
46
46
|
import { sameRootIdentity } from './disk-agreement.js';
|
|
47
|
+
import { OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoCode, systemWords } from './answers.js';
|
|
47
48
|
/**
|
|
48
49
|
* Opened with every open of a workspace path that may already be there, a
|
|
49
50
|
* read, a write in place or a directory's sync, where the platform has it:
|
|
@@ -61,15 +62,26 @@ export const HEAD_BYTES = 8192;
|
|
|
61
62
|
export const RACY_WINDOW_MS = 2000;
|
|
62
63
|
export const OUTSIDE_PARENT = 'a parent directory resolves outside the mount';
|
|
63
64
|
export const NOT_FOLLOWED = 'a symlink, which this mount does not follow';
|
|
65
|
+
export const DIRECTORY = 'a directory';
|
|
66
|
+
export const NOT_REGULAR = 'not a regular file';
|
|
67
|
+
export const LEAVES_MOUNT = 'a symlink that leaves the mount';
|
|
68
|
+
export const NOT_A_DIRECTORY = 'a parent path is not a directory';
|
|
64
69
|
export const ROOT_UNAVAILABLE = 'the mount root is unavailable';
|
|
65
70
|
export const ROOT_REPLACED = 'the mount root is not the directory disk last agreed with (an unmounted drive, or a replaced directory): ' +
|
|
66
71
|
'reconnect it; if it was replaced on purpose, sync the mount with acceptRoot to take it as it is now';
|
|
67
|
-
const CHANGED = 'it changed while it was observed';
|
|
68
|
-
const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
69
|
-
const
|
|
72
|
+
export const CHANGED = 'it changed while it was observed';
|
|
73
|
+
export const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
74
|
+
export const FILE_CHANGED = 'the file changed while it was read';
|
|
75
|
+
export const RESOLVES_OUTSIDE = 'the directory resolves outside the mount';
|
|
76
|
+
export const LINK_TO_DIRECTORY = 'a symlink to a directory, not walked';
|
|
77
|
+
export const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
|
|
70
78
|
export function errno(err) {
|
|
71
79
|
return err?.code;
|
|
72
80
|
}
|
|
81
|
+
function failedOn(what, err) {
|
|
82
|
+
const code = errno(err);
|
|
83
|
+
return { reason: `${what} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
|
|
84
|
+
}
|
|
73
85
|
export function contained(rootReal, candidate) {
|
|
74
86
|
return candidate === rootReal || candidate.startsWith(rootReal.endsWith(sep) ? rootReal : rootReal + sep);
|
|
75
87
|
}
|
|
@@ -152,9 +164,14 @@ export async function linkRefusal(view, rootReal, rel, real, through = 'path') {
|
|
|
152
164
|
return linkOnTheWay(view, rel, through);
|
|
153
165
|
}
|
|
154
166
|
export async function linkLeads(root, rootReal, link) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
let leads;
|
|
168
|
+
try {
|
|
169
|
+
leads = await realpath(join(root, link));
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
const code = errno(err);
|
|
173
|
+
return code === 'ENOENT' || code === 'ENOTDIR' || code === 'ELOOP' ? { kind: 'nowhere' } : null;
|
|
174
|
+
}
|
|
158
175
|
if (!contained(rootReal, leads))
|
|
159
176
|
return { kind: 'outside' };
|
|
160
177
|
const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
|
|
@@ -166,19 +183,19 @@ export async function linkLeads(root, rootReal, link) {
|
|
|
166
183
|
* does.
|
|
167
184
|
*/
|
|
168
185
|
export function nestedHolder(target, nested) {
|
|
169
|
-
return nested
|
|
170
|
-
.filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
|
|
171
|
-
.sort((a, b) => b.dir.length - a.dir.length)[0];
|
|
186
|
+
return nestedHolders(target, nested)[0];
|
|
172
187
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
function nestedHolders(target, nested) {
|
|
189
|
+
const segments = target.split('/');
|
|
190
|
+
return mayHold(nested.filter((n) => beneathNested(segments, n)), (n) => n.dir.split('/').length, (n) => !n.guessed && surelyBeneath(segments, n.dir.split('/')));
|
|
191
|
+
}
|
|
192
|
+
function mayHold(holders, depth, sure) {
|
|
193
|
+
const sorted = [...holders].sort((a, b) => depth(b) - depth(a) || Number(sure(b)) - Number(sure(a)));
|
|
194
|
+
const surely = sorted.findIndex(sure);
|
|
195
|
+
return surely === -1 ? sorted : sorted.slice(0, surely + 1);
|
|
196
|
+
}
|
|
197
|
+
export function namesHolder(leads, nested) {
|
|
198
|
+
return nestedHolders(leads.target, nested).every((holder) => namesNestedPlace(restBeneath(leads.target, holder), holder.include, leads.directory));
|
|
182
199
|
}
|
|
183
200
|
/**
|
|
184
201
|
* Whether a refusal names a place in a nested mount's directory, `rest`
|
|
@@ -195,19 +212,13 @@ export function namesHolder(leads, holder) {
|
|
|
195
212
|
export function namesNestedPlace(rest, include, directory) {
|
|
196
213
|
return directory || insideMount(rest, include);
|
|
197
214
|
}
|
|
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
215
|
export function withinIncludes(view, path) {
|
|
207
216
|
if (!insideMount(path, view.include))
|
|
208
217
|
return false;
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
return nestedHolders(path, view.nested ?? []).every((holder) => {
|
|
219
|
+
const rest = restBeneath(path, holder);
|
|
220
|
+
return rest === '' || namesNestedPlace(rest, holder.include, false);
|
|
221
|
+
});
|
|
211
222
|
}
|
|
212
223
|
/**
|
|
213
224
|
* The path a mount's include judges for `given`, one of the mount's paths,
|
|
@@ -279,16 +290,23 @@ export async function coversLanding(view, rootReal, real, given) {
|
|
|
279
290
|
}
|
|
280
291
|
/**
|
|
281
292
|
* Why a path is refused for what linkOnTheWay found, given where a link
|
|
282
|
-
* leads (linkLeads). For a link, the link is the subject,
|
|
283
|
-
* leads: for a link inside the mount, its target,
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
293
|
+
* leads (linkLeads), and the way on. For a link, the link is the subject,
|
|
294
|
+
* with where it leads: for a link inside the mount, its target, as the path
|
|
295
|
+
* to use in its place, and for a link to a directory there, the setting
|
|
296
|
+
* whoever runs the host can change too; for a link to something that isn't
|
|
297
|
+
* a directory, that nothing lies beneath it (the link stands among the
|
|
298
|
+
* path's directories, so following it can't make the path beneath it
|
|
299
|
+
* exist); for one leading outside the mount, that it does, and that
|
|
300
|
+
* whoever runs the host can mount what it leads to, or a directory that
|
|
301
|
+
* holds it; for one leading nowhere, that whoever runs the host can remove it or make
|
|
302
|
+
* it lead to what it stood for; for one whose target the refusal doesn't
|
|
303
|
+
* tell, what whoever runs the host can do wherever it leads
|
|
304
|
+
* (UNFOLLOWED_STEP).
|
|
305
|
+
* A target in a nested mount's directory (`nested`) is that mount's: the
|
|
306
|
+
* refusal names the mount and the path to use there in place of the link,
|
|
307
|
+
* and offers no setting, since following the link would only reach into
|
|
308
|
+
* another mount's directory. For a directory the walk couldn't ask, which
|
|
309
|
+
* one, the answer it got, and the errno's way on (errnoClause). Paths
|
|
292
310
|
* are spelled as the tools take them, from the mount's name. On a mount with
|
|
293
311
|
* an include, an inside target is named only where a tool could name it: a
|
|
294
312
|
* file the include covers, or a directory, which a listing names whatever
|
|
@@ -299,27 +317,37 @@ export async function coversLanding(view, rootReal, real, given) {
|
|
|
299
317
|
*/
|
|
300
318
|
export function linkWords(name, found, leads, nested = [], include) {
|
|
301
319
|
const spell = (rel) => `"${rel === '' ? name : `${name}/${rel}`}"`;
|
|
302
|
-
if (!('link' in found))
|
|
303
|
-
|
|
320
|
+
if (!('link' in found)) {
|
|
321
|
+
const code = errnoCode({ code: found.code });
|
|
322
|
+
return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(code, found.code)}); ${errnoClause(code, 'calling again', 'calling again then gets past it')}`;
|
|
323
|
+
}
|
|
304
324
|
const link = `${spell(found.link)} is a symlink`;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
const unknown = `${link}, which this mount does not follow; ${UNFOLLOWED_STEP}`;
|
|
326
|
+
if (leads === null)
|
|
327
|
+
return unknown;
|
|
328
|
+
if (leads.kind === 'nowhere') {
|
|
329
|
+
return `${link} that leads nowhere, which this mount does not follow; whoever runs this host can remove it or make it lead to what it stood for`;
|
|
330
|
+
}
|
|
331
|
+
if (leads.kind === 'outside') {
|
|
332
|
+
return `${link} leading outside the mount, which this mount does not follow; followed or not, ${OUTSIDE_STEP}`;
|
|
333
|
+
}
|
|
309
334
|
const holder = nestedHolder(leads.target, nested);
|
|
310
335
|
if (holder) {
|
|
311
|
-
if (!namesHolder(leads,
|
|
312
|
-
return
|
|
313
|
-
const rest = leads.target
|
|
314
|
-
|
|
315
|
-
|
|
336
|
+
if (!namesHolder(leads, nested))
|
|
337
|
+
return unknown;
|
|
338
|
+
const rest = restBeneath(leads.target, holder);
|
|
339
|
+
const guess = nestedGuess(name, leads.target.split('/'), holder);
|
|
340
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
|
|
341
|
+
`"${holder.name}", nested in mount "${name}": ${nestedStep({ name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, guess }, { after: ` in place of ${spell(found.link)}` })}`;
|
|
316
342
|
}
|
|
317
343
|
if (!leads.directory && !insideMount(leads.target, include))
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
344
|
+
return unknown;
|
|
345
|
+
if (!leads.directory)
|
|
346
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; that isn't a directory, so nothing lies beneath it`;
|
|
347
|
+
// Either way serves only what the include covers where it leads (judgedAt).
|
|
348
|
+
const covered = include === undefined ? '' : '; either way, the mount reaches only what its include covers there';
|
|
349
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; use ${spell(leads.target)} in place of ${spell(found.link)}, ` +
|
|
350
|
+
`or whoever runs this host can set followSymlinks: true on mount "${name}"${covered}`;
|
|
323
351
|
}
|
|
324
352
|
/**
|
|
325
353
|
* Where a link linkOnTheWay found leads, as its refusal may tell it
|
|
@@ -335,25 +363,83 @@ export async function shownLeads(root, rootReal, found, include) {
|
|
|
335
363
|
}
|
|
336
364
|
/** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
|
|
337
365
|
export async function linkReason(view, rootReal, found) {
|
|
338
|
-
return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
|
|
366
|
+
return { reason: linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include), ownStep: true };
|
|
367
|
+
}
|
|
368
|
+
export function realSegments(path) {
|
|
369
|
+
return path.split(sep).filter((segment, i) => i === 0 || segment !== '');
|
|
370
|
+
}
|
|
371
|
+
export function beneathRoot(path, root) {
|
|
372
|
+
const segments = realSegments(path);
|
|
373
|
+
const top = realSegments(root);
|
|
374
|
+
return segments.length > top.length && beginsWith(segments, top) ? segments.slice(top.length).join('/') : undefined;
|
|
375
|
+
}
|
|
376
|
+
export function sameRoot(path, root) {
|
|
377
|
+
const segments = realSegments(path);
|
|
378
|
+
const top = realSegments(root);
|
|
379
|
+
return segments.length === top.length && beginsWith(segments, top);
|
|
380
|
+
}
|
|
381
|
+
export function surelyAtRoot(path, root) {
|
|
382
|
+
return surelyBeneath(realSegments(path), realSegments(root));
|
|
383
|
+
}
|
|
384
|
+
export function nestedRootAtReal(real, nested = []) {
|
|
385
|
+
return nestedRootsAtReal(real, nested)[0];
|
|
386
|
+
}
|
|
387
|
+
function nestedRootsAtReal(real, nested) {
|
|
388
|
+
// realpath keeps the spelling it's given where disk folds it on Linux, as
|
|
389
|
+
// from a link's target, so a real root is compared as loosely as a path.
|
|
390
|
+
const segments = realSegments(real);
|
|
391
|
+
const holders = nested.flatMap((n) => {
|
|
392
|
+
if (n.real === undefined)
|
|
393
|
+
return [];
|
|
394
|
+
const root = realSegments(n.real);
|
|
395
|
+
return beginsWith(segments, root) ? [{ mount: n, depth: root.length, sure: surelyBeneath(segments, root) }] : [];
|
|
396
|
+
});
|
|
397
|
+
return mayHold(holders, (h) => h.depth, (h) => h.sure);
|
|
339
398
|
}
|
|
340
399
|
/**
|
|
341
400
|
* The nested mount whose root holds `real`, a canonical path, the innermost,
|
|
342
|
-
* with the path to use there
|
|
343
|
-
*
|
|
401
|
+
* with the path to use there and the path beneath its root (`rest`);
|
|
402
|
+
* undefined when none does. A path the lexical checks pass
|
|
344
403
|
* (inOtherMount, the tools' holderOf) reaches another mount's directory this
|
|
345
404
|
* way through a symlink the mount follows, so each check that resolves a
|
|
346
|
-
* path canonically asks this too
|
|
347
|
-
* namesNestedPlace says the place is that mount's.
|
|
405
|
+
* path canonically asks this too.
|
|
348
406
|
*/
|
|
349
|
-
export function nestedAtReal(real, nested = []) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
.sort((a, b) => b.real.length - a.real.length)[0];
|
|
407
|
+
export async function nestedAtReal(real, nested = [], at) {
|
|
408
|
+
const holders = nestedRootsAtReal(real, nested);
|
|
409
|
+
const holder = holders[0];
|
|
353
410
|
if (!holder)
|
|
354
411
|
return undefined;
|
|
355
|
-
const
|
|
356
|
-
|
|
412
|
+
const segments = realSegments(real);
|
|
413
|
+
// realpath keeps a link target's spelling beneath the root too, so the
|
|
414
|
+
// rest is read as disk spells it, for each mount's include to judge.
|
|
415
|
+
const rests = await Promise.all(holders.map(async (h) => {
|
|
416
|
+
const given = segments.slice(h.depth).join('/');
|
|
417
|
+
return given === '' ? '' : await diskSpelling(h.mount.real, given);
|
|
418
|
+
}));
|
|
419
|
+
const rest = rests[0];
|
|
420
|
+
const { name } = holder.mount;
|
|
421
|
+
const root = realSegments(holder.mount.real);
|
|
422
|
+
const found = {
|
|
423
|
+
name,
|
|
424
|
+
use: rest ? `${name}/${rest}` : name,
|
|
425
|
+
rest,
|
|
426
|
+
judgedByInclude: holders.some((h) => h.mount.include !== undefined),
|
|
427
|
+
names: (directory) => holders.every((h, i) => namesNestedPlace(rests[i], h.mount.include, directory)),
|
|
428
|
+
};
|
|
429
|
+
if (surelyBeneath(segments, root))
|
|
430
|
+
return found;
|
|
431
|
+
if (at === undefined)
|
|
432
|
+
return { ...found, guess: null };
|
|
433
|
+
const top = realSegments(at.rootReal);
|
|
434
|
+
if (segments.length <= top.length || root.length <= top.length || !top.every((t, i) => segments[i] === t))
|
|
435
|
+
return { ...found, guess: null };
|
|
436
|
+
const within = segments.slice(top.length);
|
|
437
|
+
const below = root.slice(top.length);
|
|
438
|
+
const guess = guessFor(at.name, within, below);
|
|
439
|
+
const leads = guess === undefined ? [at.name, ...within].join('/') : [guess.given, ...within.slice(below.length)].join('/');
|
|
440
|
+
if (!surelyBeneath(root.slice(0, top.length), top))
|
|
441
|
+
return { ...found, guess: { ...placedGuess(at.name, within.slice(0, below.length), guess), leads } };
|
|
442
|
+
return { ...found, guess: guess === undefined ? null : { ...guess, leads } };
|
|
357
443
|
}
|
|
358
444
|
/**
|
|
359
445
|
* Why a mount-relative path that resolves into a nested mount's directory is
|
|
@@ -361,7 +447,8 @@ export function nestedAtReal(real, nested = []) {
|
|
|
361
447
|
* place. Spelled as the tools take paths, from the mount's name.
|
|
362
448
|
*/
|
|
363
449
|
export function leadsInto(name, rel, holder) {
|
|
364
|
-
|
|
450
|
+
const verb = holder.guess === undefined ? 'leads' : 'may lead';
|
|
451
|
+
return `"${rel === '' ? name : `${name}/${rel}`}" ${verb} into mount "${holder.name}", nested in mount "${name}": ${nestedStep(holder, { after: ' in its place' })}`;
|
|
365
452
|
}
|
|
366
453
|
// ===========================================================================
|
|
367
454
|
// Disk's spelling of a path
|
|
@@ -371,9 +458,8 @@ const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
|
371
458
|
const otherCase = (c) => (c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase());
|
|
372
459
|
/**
|
|
373
460
|
* The name with each letter's case flipped, where its other case is one
|
|
374
|
-
* letter whose own other case is the letter again
|
|
375
|
-
*
|
|
376
|
-
* and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
461
|
+
* letter whose own other case is the letter again. `ß`, whose upper case is
|
|
462
|
+
* `SS`, and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
377
463
|
*/
|
|
378
464
|
const flippedCase = (name) => [...name].map((c) => {
|
|
379
465
|
const other = otherCase(c);
|
|
@@ -388,6 +474,15 @@ const folded = (name) => name.normalize('NFC').toLowerCase();
|
|
|
388
474
|
* the same-file check keeps that from naming another file.
|
|
389
475
|
*/
|
|
390
476
|
export const foldedLoosely = (name) => name.normalize('NFC').toLowerCase().toUpperCase().toLowerCase();
|
|
477
|
+
/**
|
|
478
|
+
* Linux's casefold, which ext4 and f2fs set per directory, drops
|
|
479
|
+
* default-ignorable code points (on every kernel but 6.12 to 6.12.4) and
|
|
480
|
+
* compares names decomposed, in full case folding, and HFS+ passes over some
|
|
481
|
+
* of those code points too. Case mapped on the decomposed name joins marks
|
|
482
|
+
* that come in another order around a combining ypogegrammeni (U+0345),
|
|
483
|
+
* whose upper case is a letter.
|
|
484
|
+
*/
|
|
485
|
+
const foldedFully = (name) => name.replace(/\p{Default_Ignorable_Code_Point}/gu, '').normalize('NFD').toLowerCase().toUpperCase().toLowerCase().normalize('NFD');
|
|
391
486
|
/**
|
|
392
487
|
* Whether `dir` folds case, told without writing: by looking up flipped a
|
|
393
488
|
* name it lists whose flipped spelling it doesn't list too (a name with no
|
|
@@ -482,19 +577,73 @@ function entryName(paths, prefix, segment) {
|
|
|
482
577
|
}
|
|
483
578
|
return found;
|
|
484
579
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
export function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return
|
|
580
|
+
function sameLevel(given, name) {
|
|
581
|
+
return given === name || (given !== '' && foldedFully(given) === foldedFully(name));
|
|
582
|
+
}
|
|
583
|
+
function beginsWith(segments, dir) {
|
|
584
|
+
return segments.length >= dir.length && dir.every((d, i) => sameLevel(segments[i], d));
|
|
585
|
+
}
|
|
586
|
+
export function beneathNested(segments, nested) {
|
|
587
|
+
return beginsWith(segments, nested.dir.split('/'));
|
|
588
|
+
}
|
|
589
|
+
export function restBeneath(path, nested) {
|
|
590
|
+
// By segments, since a name that matches can be another length, as `SS`
|
|
591
|
+
// is beside `ß`.
|
|
592
|
+
return path.split('/').slice(nested.dir.split('/').length).join('/');
|
|
593
|
+
}
|
|
594
|
+
function surelyBeneath(segments, dir) {
|
|
595
|
+
return segments.length >= dir.length && dir.every((d, i) => segments[i] === d);
|
|
596
|
+
}
|
|
597
|
+
function shownByCodePoints(name) {
|
|
598
|
+
return [...name].map((c) => {
|
|
599
|
+
const code = c.codePointAt(0);
|
|
600
|
+
if (c === '\\')
|
|
601
|
+
return '\\\\';
|
|
602
|
+
return code < 0x80 ? c : code > 0xffff ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, '0')}`;
|
|
603
|
+
}).join('');
|
|
604
|
+
}
|
|
605
|
+
function guessFor(mount, given, dir) {
|
|
606
|
+
if (surelyBeneath(given, dir))
|
|
607
|
+
return undefined;
|
|
608
|
+
const levels = given.slice(0, dir.length);
|
|
609
|
+
const plain = (a, b) => a === b || !/[^\x00-\x7f]/.test(a + b);
|
|
610
|
+
const shown = (name, other) => (plain(name, other) ? name : shownByCodePoints(name));
|
|
611
|
+
return {
|
|
612
|
+
given: [mount, ...levels.map((g, i) => shown(g, dir[i]))].join('/'),
|
|
613
|
+
dir: [mount, ...dir.map((d, i) => shown(d, levels[i]))].join('/'),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
export function nestedGuess(mount, segments, nested) {
|
|
617
|
+
const dir = nested.dir.split('/');
|
|
618
|
+
const guess = guessFor(mount, segments, dir);
|
|
619
|
+
return nested.guessed ? placedGuess(mount, segments.slice(0, dir.length), guess) : guess;
|
|
620
|
+
}
|
|
621
|
+
function placedGuess(mount, levels, guess) {
|
|
622
|
+
return guess === undefined ? { placed: [mount, ...levels].join('/') } : { placed: guess.given, dir: guess.dir };
|
|
623
|
+
}
|
|
624
|
+
export function nestedStep(holder, opts = {}) {
|
|
625
|
+
const { guess } = holder;
|
|
626
|
+
const after = opts.after ?? '';
|
|
627
|
+
if (guess === undefined)
|
|
628
|
+
return `use "${holder.use}"${after}`;
|
|
629
|
+
if (guess === null) {
|
|
630
|
+
return `this workspace can't tell whether where it leads is that mount's directory. ` +
|
|
631
|
+
`If you meant that mount's directory, use "${holder.use}"${after}; if not, ask whoever runs this workspace`;
|
|
632
|
+
}
|
|
633
|
+
const leads = guess.leads === undefined ? '' : `it leads to "${guess.leads}", and `;
|
|
634
|
+
if ('placed' in guess) {
|
|
635
|
+
const check = 'ask whoever runs this workspace to check where the two mounts are configured';
|
|
636
|
+
const outside = guess.dir === undefined ? `"${guess.placed}"` : `"${guess.placed}" and "${guess.dir}"`;
|
|
637
|
+
return `${leads}this workspace can't tell whether "${guess.placed}" is mount "${holder.name}"'s directory, since the two mounts' roots are configured in ` +
|
|
638
|
+
`spellings it can't tell apart. If you meant that mount's directory, use "${holder.use}"${after}. ` +
|
|
639
|
+
(opts.named ? `If you meant one of this mount's own, for a new file use a path outside ${outside}, and for one already there ${check}` : `If not, ${check}`);
|
|
640
|
+
}
|
|
641
|
+
const remedy = `rename one of "${guess.given}" and "${guess.dir}"`;
|
|
642
|
+
const own = opts.named
|
|
643
|
+
? `If you meant one of this mount's own, for a new file use a path outside "${guess.given}" and "${guess.dir}", and for one already there ask whoever runs this workspace to ${remedy}`
|
|
644
|
+
: `If not, ask whoever runs this workspace to ${remedy}`;
|
|
645
|
+
return `${leads}this workspace can't tell "${guess.given}" from "${guess.dir}", ` +
|
|
646
|
+
`mount "${holder.name}"'s directory. If you meant that mount's directory, use "${holder.use}"${after}. ${own}`;
|
|
498
647
|
}
|
|
499
648
|
/**
|
|
500
649
|
* The name `dir` lists for `segment`: `segment` itself, unless disk folds it
|
|
@@ -707,20 +856,26 @@ export class CheckedDir {
|
|
|
707
856
|
lexical;
|
|
708
857
|
dev;
|
|
709
858
|
ino;
|
|
859
|
+
born;
|
|
710
860
|
constructor(
|
|
711
861
|
/** Mount-relative path ('' for the root). */
|
|
712
862
|
rel,
|
|
713
863
|
/** Its canonical location when checked. */
|
|
714
|
-
real, lexical, dev, ino) {
|
|
864
|
+
real, lexical, dev, ino, born) {
|
|
715
865
|
this.rel = rel;
|
|
716
866
|
this.real = real;
|
|
717
867
|
this.lexical = lexical;
|
|
718
868
|
this.dev = dev;
|
|
719
869
|
this.ino = ino;
|
|
870
|
+
this.born = born;
|
|
720
871
|
}
|
|
721
872
|
/** Which directory it is. */
|
|
722
873
|
identity() {
|
|
723
|
-
return {
|
|
874
|
+
return {
|
|
875
|
+
dev: String(this.dev),
|
|
876
|
+
ino: String(this.ino),
|
|
877
|
+
...(this.born > 0n ? { born: String(this.born) } : {}),
|
|
878
|
+
};
|
|
724
879
|
}
|
|
725
880
|
/** A path that reaches `name` in this directory. */
|
|
726
881
|
at(name) {
|
|
@@ -764,12 +919,26 @@ export class CheckedDir {
|
|
|
764
919
|
}
|
|
765
920
|
}
|
|
766
921
|
}
|
|
922
|
+
/**
|
|
923
|
+
* What is at a mount root's path now, followed as the root is: for a
|
|
924
|
+
* directory, undefined; for anything else, ENOTDIR; and where the path
|
|
925
|
+
* can't be followed to anything, the errno its stat failed with, ENOENT
|
|
926
|
+
* when nothing is there.
|
|
927
|
+
*/
|
|
928
|
+
export async function rootAbsence(root) {
|
|
929
|
+
try {
|
|
930
|
+
return (await stat(root)).isDirectory() ? undefined : 'ENOTDIR';
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
return errno(err);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
767
936
|
/** Check the directory at a canonical location, if one is there. */
|
|
768
937
|
async function checkedAt(view, rel, real) {
|
|
769
938
|
const info = await lstat(real, { bigint: true }).catch(() => null);
|
|
770
939
|
if (!info?.isDirectory())
|
|
771
940
|
return null;
|
|
772
|
-
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino);
|
|
941
|
+
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs);
|
|
773
942
|
}
|
|
774
943
|
/**
|
|
775
944
|
* Where a mount directory is now: checked; missing — nothing there, or not a
|
|
@@ -784,19 +953,19 @@ async function locateDir(view, rootReal, rel) {
|
|
|
784
953
|
catch (err) {
|
|
785
954
|
const code = errno(err);
|
|
786
955
|
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
787
|
-
return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE } : { kind: 'missing' };
|
|
956
|
+
return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE, errno: code } : { kind: 'missing' };
|
|
788
957
|
// A link on the way that can't be resolved, such as a loop, is still a
|
|
789
958
|
// link this mount doesn't follow.
|
|
790
959
|
const found = rel === '' ? null : await linkOnTheWay(view, rel);
|
|
791
960
|
if (found !== null)
|
|
792
|
-
return { kind: 'unobserved',
|
|
793
|
-
return { kind: 'unobserved',
|
|
961
|
+
return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
962
|
+
return { kind: 'unobserved', ...failedOn('cannot resolve the directory', err) };
|
|
794
963
|
}
|
|
795
964
|
// A mount that doesn't follow symlinks goes through none on the way to a
|
|
796
965
|
// directory, inside the mount or out of it.
|
|
797
966
|
const found = await linkRefusal(view, rootReal, rel, real);
|
|
798
967
|
if (found !== null)
|
|
799
|
-
return { kind: 'unobserved',
|
|
968
|
+
return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
800
969
|
if (rel === '') {
|
|
801
970
|
// The root is whatever the configured path names, and it must still be
|
|
802
971
|
// the root the pass was given.
|
|
@@ -811,10 +980,10 @@ async function locateDir(view, rootReal, rel) {
|
|
|
811
980
|
// mount's, which the mount neither reads nor writes. A place there that
|
|
812
981
|
// mount's include leaves out is no mount's, and holds nothing beneath it,
|
|
813
982
|
// as at any file (namesNestedPlace).
|
|
814
|
-
const holder = nestedAtReal(real, view.nested);
|
|
983
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
815
984
|
if (holder) {
|
|
816
|
-
const directory = holder.
|
|
817
|
-
return
|
|
985
|
+
const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
|
|
986
|
+
return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder), ownStep: true } : { kind: 'missing' };
|
|
818
987
|
}
|
|
819
988
|
}
|
|
820
989
|
const checked = await checkedAt(view, rel, real);
|
|
@@ -842,7 +1011,10 @@ export async function checkRoot(view, rootReal) {
|
|
|
842
1011
|
const found = await locateDir(view, rootReal, '');
|
|
843
1012
|
if (found.kind === 'checked')
|
|
844
1013
|
return { dir: found.dir };
|
|
845
|
-
|
|
1014
|
+
const reason = found.kind === 'unobserved' ? found.reason : ROOT_UNAVAILABLE;
|
|
1015
|
+
// What is at the root's path now says whether the directory is gone from it.
|
|
1016
|
+
const code = found.kind === 'unobserved' && found.errno !== undefined ? found.errno : reason === ROOT_UNAVAILABLE ? await rootAbsence(view.root) : undefined;
|
|
1017
|
+
return { reason, ...(code !== undefined ? { errno: code } : {}) };
|
|
846
1018
|
}
|
|
847
1019
|
/**
|
|
848
1020
|
* Check the directory at a mount-relative path. With `create`, missing
|
|
@@ -858,7 +1030,7 @@ export async function checkDir(view, rootReal, rel, opts = {}) {
|
|
|
858
1030
|
if (opts.create)
|
|
859
1031
|
return createDir(view, rootReal, rel);
|
|
860
1032
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
861
|
-
return
|
|
1033
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
|
|
862
1034
|
}
|
|
863
1035
|
async function createDir(view, rootReal, rel) {
|
|
864
1036
|
const parts = rel.split('/');
|
|
@@ -879,16 +1051,16 @@ async function createDir(view, rootReal, rel) {
|
|
|
879
1051
|
const childRel = parts.slice(0, i + 1).join('/');
|
|
880
1052
|
// Nothing is made in a nested mount's directory, its root included where
|
|
881
1053
|
// it isn't made yet, by a path that reaches it through a link.
|
|
882
|
-
const holder = nestedAtReal(base.at(parts[i]), view.nested);
|
|
1054
|
+
const holder = await nestedAtReal(base.at(parts[i]), view.nested, { name: view.name, rootReal });
|
|
883
1055
|
if (holder)
|
|
884
|
-
return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
|
|
1056
|
+
return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder), ownStep: true };
|
|
885
1057
|
try {
|
|
886
1058
|
await mkdir(base.at(parts[i]));
|
|
887
1059
|
}
|
|
888
1060
|
catch (err) {
|
|
889
1061
|
const code = errno(err);
|
|
890
1062
|
if (code !== 'EEXIST')
|
|
891
|
-
return { kind: 'unobserved',
|
|
1063
|
+
return { kind: 'unobserved', ...failedOn(`cannot create the directory ${childRel}`, err) };
|
|
892
1064
|
}
|
|
893
1065
|
// Made, or there already (made meanwhile, or not a directory): it must
|
|
894
1066
|
// be a real directory now, never a symlink or file in its place.
|
|
@@ -898,8 +1070,8 @@ async function createDir(view, rootReal, rel) {
|
|
|
898
1070
|
// doesn't follow symlinks, is named as one.
|
|
899
1071
|
const there = view.followSymlinks ? null : await lstat(base.at(parts[i])).catch(() => null);
|
|
900
1072
|
if (there?.isSymbolicLink())
|
|
901
|
-
return { kind: 'unobserved',
|
|
902
|
-
return { kind: 'unobserved', reason:
|
|
1073
|
+
return { kind: 'unobserved', ...(await linkReason(view, rootReal, { link: childRel })) };
|
|
1074
|
+
return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
|
|
903
1075
|
}
|
|
904
1076
|
base = checked;
|
|
905
1077
|
}
|
|
@@ -923,14 +1095,14 @@ async function createDir(view, rootReal, rel) {
|
|
|
923
1095
|
export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
|
|
924
1096
|
const root = await locateDir(view, rootReal, '');
|
|
925
1097
|
if (root.kind !== 'checked')
|
|
926
|
-
return ROOT_UNAVAILABLE;
|
|
1098
|
+
return { reason: ROOT_UNAVAILABLE };
|
|
927
1099
|
let cur = root.dir;
|
|
928
1100
|
/** Every directory the walk went through, the root first. */
|
|
929
1101
|
const walked = [cur];
|
|
930
1102
|
const proven = async () => {
|
|
931
1103
|
for (const dir of walked)
|
|
932
1104
|
if (!(await dir.stillHere()))
|
|
933
|
-
return CHANGED;
|
|
1105
|
+
return { reason: CHANGED };
|
|
934
1106
|
return { by: cur.rel };
|
|
935
1107
|
};
|
|
936
1108
|
const parts = rel.split('/');
|
|
@@ -945,48 +1117,51 @@ export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
|
|
|
945
1117
|
catch (err) {
|
|
946
1118
|
const code = errno(err);
|
|
947
1119
|
if (code === 'ENOTDIR')
|
|
948
|
-
return CHANGED;
|
|
1120
|
+
return { reason: CHANGED };
|
|
949
1121
|
if (code !== 'ENOENT')
|
|
950
|
-
return
|
|
1122
|
+
return failedOn('cannot stat', err);
|
|
951
1123
|
// Absent from a directory that, like each above it, is still the one its path names.
|
|
952
1124
|
return proven();
|
|
953
1125
|
}
|
|
954
1126
|
if (info.isDirectory()) {
|
|
955
1127
|
if (last)
|
|
956
|
-
return CHANGED; // present after all
|
|
957
|
-
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino);
|
|
1128
|
+
return { reason: CHANGED }; // present after all
|
|
1129
|
+
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
|
|
958
1130
|
walked.push(cur);
|
|
959
1131
|
continue;
|
|
960
1132
|
}
|
|
961
1133
|
if (info.isSymbolicLink()) {
|
|
962
1134
|
if (last && target === 'entry')
|
|
963
|
-
return CHANGED; // present: the link itself
|
|
1135
|
+
return { reason: CHANGED }; // present: the link itself
|
|
964
1136
|
// A mount that doesn't follow symlinks goes through none on the way.
|
|
965
1137
|
if (!view.followSymlinks)
|
|
966
1138
|
return linkReason(view, rootReal, { link: childRel });
|
|
967
1139
|
const located = await locateDir(view, rootReal, childRel);
|
|
1140
|
+
if (located.kind === 'unobserved' && located.outside !== true) {
|
|
1141
|
+
return { reason: located.reason, ...(located.errno !== undefined ? { errno: located.errno } : {}), ...(located.ownStep === true ? { ownStep: true } : {}) };
|
|
1142
|
+
}
|
|
968
1143
|
if (located.kind !== 'checked')
|
|
969
|
-
return BENEATH_SYMLINK;
|
|
1144
|
+
return { reason: BENEATH_SYMLINK };
|
|
970
1145
|
if (last)
|
|
971
|
-
return CHANGED; // a directory there after all
|
|
1146
|
+
return { reason: CHANGED }; // a directory there after all
|
|
972
1147
|
cur = located.dir;
|
|
973
1148
|
walked.push(cur);
|
|
974
1149
|
continue;
|
|
975
1150
|
}
|
|
976
1151
|
if (last && target === 'entry')
|
|
977
|
-
return CHANGED; // present after all
|
|
1152
|
+
return { reason: CHANGED }; // present after all
|
|
978
1153
|
// A non-directory: nothing can exist beneath it, while it is still this
|
|
979
1154
|
// same one, in the directory still at its path.
|
|
980
1155
|
const again = await lstat(cur.at(name), { bigint: true }).catch(() => null);
|
|
981
1156
|
if (again === null || again.isDirectory() || again.isSymbolicLink() || again.dev !== info.dev || again.ino !== info.ino)
|
|
982
|
-
return CHANGED;
|
|
1157
|
+
return { reason: CHANGED };
|
|
983
1158
|
return proven();
|
|
984
1159
|
}
|
|
985
|
-
return CHANGED;
|
|
1160
|
+
return { reason: CHANGED };
|
|
986
1161
|
}
|
|
987
1162
|
async function absence(view, rootReal, rel) {
|
|
988
1163
|
const proof = await confirmAbsent(view, rootReal, rel);
|
|
989
|
-
return
|
|
1164
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent' };
|
|
990
1165
|
}
|
|
991
1166
|
// ===========================================================================
|
|
992
1167
|
// Fingerprints
|
|
@@ -1061,16 +1236,16 @@ async function parentPlacement(view, rootReal, relativePath) {
|
|
|
1061
1236
|
const parent = await realpath(dirname(join(view.root, relativePath)));
|
|
1062
1237
|
const found = await linkRefusal(view, rootReal, relativePath, parent, 'parents');
|
|
1063
1238
|
if (found !== null)
|
|
1064
|
-
return
|
|
1239
|
+
return linkReason(view, rootReal, found);
|
|
1065
1240
|
if (!contained(rootReal, parent))
|
|
1066
1241
|
return { reason: OUTSIDE_PARENT };
|
|
1067
|
-
const holder = nestedAtReal(parent, view.nested);
|
|
1242
|
+
const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
|
|
1068
1243
|
if (!holder)
|
|
1069
1244
|
return { inside: parent };
|
|
1070
1245
|
// A place there that the nested mount's include leaves out is no mount's,
|
|
1071
1246
|
// and holds nothing beneath it, as beneath any file (namesNestedPlace).
|
|
1072
|
-
const directory = holder.
|
|
1073
|
-
return
|
|
1247
|
+
const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
|
|
1248
|
+
return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder), ownStep: true } : 'gone';
|
|
1074
1249
|
}
|
|
1075
1250
|
catch (err) {
|
|
1076
1251
|
const code = errno(err);
|
|
@@ -1080,8 +1255,8 @@ async function parentPlacement(view, rootReal, relativePath) {
|
|
|
1080
1255
|
// link this mount doesn't follow.
|
|
1081
1256
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1082
1257
|
if (found !== null)
|
|
1083
|
-
return
|
|
1084
|
-
return
|
|
1258
|
+
return linkReason(view, rootReal, found);
|
|
1259
|
+
return failedOn('cannot resolve the parent directory', err);
|
|
1085
1260
|
}
|
|
1086
1261
|
}
|
|
1087
1262
|
/**
|
|
@@ -1096,7 +1271,7 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
1096
1271
|
if (placement === 'gone')
|
|
1097
1272
|
return absence(view, rootReal, relativePath);
|
|
1098
1273
|
if ('reason' in placement)
|
|
1099
|
-
return { kind: 'unobserved',
|
|
1274
|
+
return { kind: 'unobserved', ...placement };
|
|
1100
1275
|
let info;
|
|
1101
1276
|
try {
|
|
1102
1277
|
info = await lstat(lexical, { bigint: true });
|
|
@@ -1105,7 +1280,7 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
1105
1280
|
const code = errno(err);
|
|
1106
1281
|
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
1107
1282
|
return absence(view, rootReal, relativePath);
|
|
1108
|
-
return { kind: 'unobserved',
|
|
1283
|
+
return { kind: 'unobserved', ...failedOn('cannot stat', err) };
|
|
1109
1284
|
}
|
|
1110
1285
|
if (info.isSymbolicLink()) {
|
|
1111
1286
|
if (!view.followSymlinks)
|
|
@@ -1115,43 +1290,43 @@ export async function observePath(view, rootReal, relativePath) {
|
|
|
1115
1290
|
target = await stat(lexical, { bigint: true });
|
|
1116
1291
|
}
|
|
1117
1292
|
catch (err) {
|
|
1118
|
-
return { kind: 'unobserved',
|
|
1293
|
+
return { kind: 'unobserved', ...failedOn('a symlink whose target cannot be read', err) };
|
|
1119
1294
|
}
|
|
1120
1295
|
let real;
|
|
1121
1296
|
try {
|
|
1122
1297
|
real = await realpath(lexical);
|
|
1123
1298
|
}
|
|
1124
1299
|
catch (err) {
|
|
1125
|
-
return { kind: 'unobserved',
|
|
1300
|
+
return { kind: 'unobserved', ...failedOn('a symlink that cannot be resolved', err) };
|
|
1126
1301
|
}
|
|
1127
1302
|
if (!contained(rootReal, real))
|
|
1128
|
-
return { kind: 'unobserved', reason:
|
|
1129
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1303
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1304
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1130
1305
|
// A place a nested mount's include leaves out is no mount's, so the link
|
|
1131
1306
|
// leaves this one (namesNestedPlace).
|
|
1132
1307
|
if (holder) {
|
|
1133
|
-
return { kind: 'unobserved', reason:
|
|
1308
|
+
return holder.names(target.isDirectory()) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1134
1309
|
}
|
|
1135
1310
|
if (!target.isFile()) {
|
|
1136
|
-
return { kind: 'unobserved', reason: target.isDirectory() ?
|
|
1311
|
+
return { kind: 'unobserved', reason: target.isDirectory() ? LINK_TO_DIRECTORY : NOT_REGULAR };
|
|
1137
1312
|
}
|
|
1138
1313
|
// A file this mount's own include leaves out where the link lands is no
|
|
1139
1314
|
// file of this mount's (coversLanding). It's judged here, and not only
|
|
1140
1315
|
// where the file is opened, since a pass takes a copy whose fingerprint
|
|
1141
1316
|
// the stat vouches for without opening it.
|
|
1142
1317
|
if (!(await coversLanding(view, rootReal, real, relativePath)))
|
|
1143
|
-
return { kind: 'unobserved', reason:
|
|
1318
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1144
1319
|
info = target;
|
|
1145
1320
|
}
|
|
1146
1321
|
else if (info.isDirectory()) {
|
|
1147
|
-
return { kind: 'unobserved', reason:
|
|
1322
|
+
return { kind: 'unobserved', reason: DIRECTORY };
|
|
1148
1323
|
}
|
|
1149
1324
|
else if (!info.isFile()) {
|
|
1150
|
-
return { kind: 'unobserved', reason:
|
|
1325
|
+
return { kind: 'unobserved', reason: NOT_REGULAR };
|
|
1151
1326
|
}
|
|
1152
1327
|
else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
|
|
1153
1328
|
// So is one reached through a link among its directories.
|
|
1154
|
-
return { kind: 'unobserved', reason:
|
|
1329
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1155
1330
|
}
|
|
1156
1331
|
return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 511n), fp: statFingerprint(info) };
|
|
1157
1332
|
}
|
|
@@ -1184,18 +1359,18 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1184
1359
|
if (code === 'ELOOP' && !view.followSymlinks) {
|
|
1185
1360
|
// The link at the path, or a looping one on its way.
|
|
1186
1361
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1187
|
-
return { kind: 'unobserved', reason:
|
|
1362
|
+
return found === null ? { kind: 'unobserved', reason: NOT_FOLLOWED } : { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1188
1363
|
}
|
|
1189
1364
|
if (code === 'EISDIR')
|
|
1190
|
-
return { kind: 'unobserved', reason:
|
|
1191
|
-
return { kind: 'unobserved',
|
|
1365
|
+
return { kind: 'unobserved', reason: DIRECTORY };
|
|
1366
|
+
return { kind: 'unobserved', ...failedOn('cannot open', err) };
|
|
1192
1367
|
}
|
|
1193
1368
|
let keep = false;
|
|
1194
1369
|
try {
|
|
1195
1370
|
const info = await handle.stat({ bigint: true });
|
|
1196
1371
|
const real = await realpath(lexical).catch(() => null);
|
|
1197
1372
|
if (real === null)
|
|
1198
|
-
return { kind: 'unobserved', reason:
|
|
1373
|
+
return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1199
1374
|
// O_NOFOLLOW guards only the final component: a mount that doesn't
|
|
1200
1375
|
// follow symlinks goes through none among the directories on the way
|
|
1201
1376
|
// either. Asked of what the path resolved to after the open, which the
|
|
@@ -1203,23 +1378,23 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1203
1378
|
// descriptor holds, so a link gets its own answer whatever it leads to.
|
|
1204
1379
|
const found = await linkRefusal(view, rootReal, relativePath, real);
|
|
1205
1380
|
if (found !== null)
|
|
1206
|
-
return { kind: 'unobserved',
|
|
1381
|
+
return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1207
1382
|
if (!info.isFile())
|
|
1208
|
-
return { kind: 'unobserved', reason: info.isDirectory() ?
|
|
1209
|
-
const leaves = view.followSymlinks ?
|
|
1383
|
+
return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
|
|
1384
|
+
const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
|
|
1210
1385
|
if (!contained(rootReal, real))
|
|
1211
1386
|
return { kind: 'unobserved', reason: leaves };
|
|
1212
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1387
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1213
1388
|
// A place a nested mount's include leaves out is no mount's. Reached
|
|
1214
1389
|
// through a link this mount doesn't follow, the link is what's refused.
|
|
1215
1390
|
if (holder)
|
|
1216
|
-
return { kind: 'unobserved', reason:
|
|
1391
|
+
return holder.names(false) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1217
1392
|
// So is a file this mount's own include leaves out (coversLanding).
|
|
1218
1393
|
if (!(await coversLanding(view, rootReal, real, relativePath)))
|
|
1219
1394
|
return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1220
1395
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
1221
1396
|
if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
|
|
1222
|
-
return { kind: 'unobserved', reason:
|
|
1397
|
+
return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1223
1398
|
}
|
|
1224
1399
|
if (!view.followSymlinks) {
|
|
1225
1400
|
// Where no symlink is followed, the path itself must be the file the
|
|
@@ -1229,7 +1404,7 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1229
1404
|
if (entry?.isSymbolicLink())
|
|
1230
1405
|
return { kind: 'unobserved', reason: NOT_FOLLOWED };
|
|
1231
1406
|
if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) {
|
|
1232
|
-
return { kind: 'unobserved', reason:
|
|
1407
|
+
return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1233
1408
|
}
|
|
1234
1409
|
}
|
|
1235
1410
|
keep = true;
|
|
@@ -1239,7 +1414,7 @@ async function openContained(view, rootReal, relativePath) {
|
|
|
1239
1414
|
const code = errno(err);
|
|
1240
1415
|
if (code === 'ENOENT')
|
|
1241
1416
|
return absence(view, rootReal, relativePath);
|
|
1242
|
-
return { kind: 'unobserved',
|
|
1417
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1243
1418
|
}
|
|
1244
1419
|
finally {
|
|
1245
1420
|
if (!keep)
|
|
@@ -1340,7 +1515,7 @@ export async function readPath(view, rootReal, relativePath, limit) {
|
|
|
1340
1515
|
const code = errno(err);
|
|
1341
1516
|
if (code === 'ENOENT')
|
|
1342
1517
|
return absence(view, rootReal, relativePath);
|
|
1343
|
-
return { kind: 'unobserved',
|
|
1518
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1344
1519
|
}
|
|
1345
1520
|
finally {
|
|
1346
1521
|
await handle.close();
|
|
@@ -1382,7 +1557,7 @@ export async function readBytes(view, rootReal, relativePath, limit) {
|
|
|
1382
1557
|
return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1383
1558
|
}
|
|
1384
1559
|
catch (err) {
|
|
1385
|
-
return { kind: 'unobserved',
|
|
1560
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1386
1561
|
}
|
|
1387
1562
|
finally {
|
|
1388
1563
|
await handle.close();
|
|
@@ -1404,7 +1579,7 @@ export async function readHead(view, rootReal, relativePath) {
|
|
|
1404
1579
|
return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
|
|
1405
1580
|
}
|
|
1406
1581
|
catch (err) {
|
|
1407
|
-
return { kind: 'unobserved',
|
|
1582
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1408
1583
|
}
|
|
1409
1584
|
finally {
|
|
1410
1585
|
await handle.close();
|
|
@@ -1480,14 +1655,14 @@ function subtreeAgainst(dir, at) {
|
|
|
1480
1655
|
return 'holds';
|
|
1481
1656
|
}
|
|
1482
1657
|
/**
|
|
1483
|
-
* Whether a mount's ignore list covers a mount-relative path: the path itself
|
|
1484
|
-
* or any directory above it, as the walk's own test reads
|
|
1485
|
-
* path and
|
|
1658
|
+
* Whether a mount's ignore list covers a mount-relative path: the path itself,
|
|
1659
|
+
* a directory or not, or any directory above it, as the walk's own test reads
|
|
1660
|
+
* each entry by its path and kind.
|
|
1486
1661
|
*/
|
|
1487
|
-
export function coveredByIgnore(relativePath, patterns) {
|
|
1662
|
+
export function coveredByIgnore(relativePath, patterns, directory) {
|
|
1488
1663
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1489
1664
|
for (let i = 0; i < segments.length; i++) {
|
|
1490
|
-
if (isIgnored(segments.slice(0, i + 1).join('/'),
|
|
1665
|
+
if (isIgnored(segments.slice(0, i + 1).join('/'), patterns, i < segments.length - 1 || directory))
|
|
1491
1666
|
return true;
|
|
1492
1667
|
}
|
|
1493
1668
|
return false;
|
|
@@ -1530,44 +1705,90 @@ export function insideMount(relativePath, include) {
|
|
|
1530
1705
|
return true;
|
|
1531
1706
|
return coveredByInclude(relativePath, include);
|
|
1532
1707
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1708
|
+
const readings = new Map();
|
|
1709
|
+
function readPattern(pattern) {
|
|
1710
|
+
let reading = readings.get(pattern);
|
|
1711
|
+
if (reading === undefined) {
|
|
1712
|
+
reading = parsePattern(pattern);
|
|
1713
|
+
readings.set(pattern, reading);
|
|
1714
|
+
}
|
|
1715
|
+
return reading;
|
|
1716
|
+
}
|
|
1717
|
+
function parsePattern(pattern) {
|
|
1718
|
+
if (pattern.startsWith('!'))
|
|
1719
|
+
return 'a leading "!" makes an exception in a .gitignore, and this list has none, so leave it out';
|
|
1720
|
+
if (pattern.startsWith('#'))
|
|
1721
|
+
return 'a leading "#" marks a comment in a .gitignore, so leave comments out of the list';
|
|
1722
|
+
if (pattern.includes('\\'))
|
|
1723
|
+
return '"\\" is an escape in a .gitignore and a separator on Windows, so no pattern here holds one, and "?" stands for one in a name';
|
|
1724
|
+
if (pattern.endsWith(' '))
|
|
1725
|
+
return 'a .gitignore drops trailing spaces, so remove them';
|
|
1726
|
+
const anyDepth = pattern.startsWith('**/');
|
|
1727
|
+
let body = anyDepth ? pattern.slice(3) : pattern;
|
|
1728
|
+
const beneath = body.endsWith('/**');
|
|
1729
|
+
const directory = beneath || body.endsWith('/');
|
|
1730
|
+
body = body.slice(0, beneath ? -3 : directory ? -1 : undefined);
|
|
1731
|
+
const anchored = (beneath && !anyDepth) || body.includes('/');
|
|
1732
|
+
if (body.startsWith('/') && !anyDepth)
|
|
1733
|
+
body = body.slice(1);
|
|
1734
|
+
const segments = body.split('/');
|
|
1735
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..'))
|
|
1736
|
+
return 'it has an empty, "." or ".." segment, which names no entry';
|
|
1737
|
+
if (body.includes('**') || (anyDepth && anchored))
|
|
1738
|
+
return '"**" is read only as a leading "**/" before one name, or a trailing "/**"';
|
|
1739
|
+
const glob = (s) => /[*?]/.test(s)
|
|
1740
|
+
? new RegExp(`^${s.replace(/[.+^${}()|[\]*?]/g, (c) => (c === '*' ? '.*' : c === '?' ? '.' : `\\${c}`))}$`, 'su')
|
|
1741
|
+
: s;
|
|
1742
|
+
return { segments: segments.map(glob), anchored, directory };
|
|
1743
|
+
}
|
|
1744
|
+
function nameMatches(segment, name) {
|
|
1745
|
+
return typeof segment === 'string' ? segment === name : segment.test(name);
|
|
1746
|
+
}
|
|
1747
|
+
function matchesEntry(pattern, segments, directory) {
|
|
1748
|
+
if (pattern.directory && !directory)
|
|
1749
|
+
return false;
|
|
1750
|
+
if (!pattern.anchored)
|
|
1751
|
+
return nameMatches(pattern.segments[0], segments[segments.length - 1]);
|
|
1752
|
+
return segments.length === pattern.segments.length && pattern.segments.every((s, i) => nameMatches(s, segments[i]));
|
|
1753
|
+
}
|
|
1754
|
+
export function patternRefusal(pattern, list) {
|
|
1755
|
+
if (typeof pattern !== 'string')
|
|
1756
|
+
return "it isn't a string";
|
|
1757
|
+
const reading = readPattern(pattern);
|
|
1758
|
+
if (typeof reading === 'string')
|
|
1759
|
+
return reading;
|
|
1760
|
+
if (list === 'ignore' && pattern.includes('[')) {
|
|
1761
|
+
return '"[" starts a character class in a .gitignore, which this list doesn\'t read, so write each name out, with "?" for a "[" in it';
|
|
1762
|
+
}
|
|
1763
|
+
return undefined;
|
|
1764
|
+
}
|
|
1765
|
+
export function directoryAt(path, followSymlinks) {
|
|
1766
|
+
if (!followSymlinks)
|
|
1767
|
+
return lstat(path).then((s) => s.isDirectory(), () => undefined);
|
|
1768
|
+
return stat(path).then((s) => s.isDirectory(), () => lstat(path).then(() => false, () => undefined));
|
|
1548
1769
|
}
|
|
1549
1770
|
/**
|
|
1550
|
-
* Whether a mount's include covers a mount-relative path
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
* no covering: where one is a scope, refusesAsOutside (index.ts) decides.
|
|
1771
|
+
* Whether a mount's include covers a mount-relative path, read as a file: a
|
|
1772
|
+
* pattern that matches only directories covers what lies beneath a directory
|
|
1773
|
+
* it matches, and any other covers a file by its own name or path, and
|
|
1774
|
+
* nothing beneath a directory it matches. So `gate.json` covers `x/gate.json`
|
|
1775
|
+
* and not `x/gate.json/run.sh`, and a subtree is named with `dir/` or
|
|
1776
|
+
* `dir/**`. A directory is no file, so it needs no covering: where one is a
|
|
1777
|
+
* scope, refusesAsOutside (index.ts) decides.
|
|
1558
1778
|
*/
|
|
1559
1779
|
function coveredByInclude(relativePath, include) {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1780
|
+
const segments = relativePath.split('/');
|
|
1781
|
+
return include.some((p) => {
|
|
1782
|
+
const pattern = readPattern(p);
|
|
1783
|
+
if (typeof pattern === 'string')
|
|
1784
|
+
return false;
|
|
1785
|
+
if (!pattern.directory)
|
|
1786
|
+
return matchesEntry(pattern, segments, false);
|
|
1787
|
+
for (let k = 1; k < segments.length; k++)
|
|
1788
|
+
if (matchesEntry(pattern, segments.slice(0, k), true))
|
|
1564
1789
|
return true;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
return true;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return false;
|
|
1790
|
+
return false;
|
|
1791
|
+
});
|
|
1571
1792
|
}
|
|
1572
1793
|
/**
|
|
1573
1794
|
* The ignore patterns a walk that names `scope` reads beneath it: a pattern
|
|
@@ -1575,7 +1796,7 @@ function coveredByInclude(relativePath, include) {
|
|
|
1575
1796
|
* it (#276); one covering only something inside it does.
|
|
1576
1797
|
*/
|
|
1577
1798
|
export function ignoreBeneath(scope, patterns) {
|
|
1578
|
-
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern]));
|
|
1799
|
+
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern], true));
|
|
1579
1800
|
}
|
|
1580
1801
|
/**
|
|
1581
1802
|
* Names editors give the files they keep beside the one being edited: vim's
|
|
@@ -1594,7 +1815,7 @@ const EDITOR_FILE = /^\..+\.sw[px]$|~$|\.subl.*\.tmp$/;
|
|
|
1594
1815
|
/** The walk's reason for an entry the mount's ignore list covers. */
|
|
1595
1816
|
export const IGNORED = 'ignored by the mount';
|
|
1596
1817
|
/** The walk's reason for an entry `EDITOR_FILE` names. */
|
|
1597
|
-
const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
1818
|
+
export const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
1598
1819
|
/**
|
|
1599
1820
|
* The walk's reason for a link it doesn't enter. On a mount with an include,
|
|
1600
1821
|
* a link the include's names don't cover is passed over with it, by design,
|
|
@@ -1602,7 +1823,7 @@ const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
|
1602
1823
|
* that follows links, one that leads to a directory, and on one that
|
|
1603
1824
|
* doesn't, every one.
|
|
1604
1825
|
*/
|
|
1605
|
-
const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
1826
|
+
export const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
1606
1827
|
/** Whether a reason the walk gave an entry is one it passes over by design. */
|
|
1607
1828
|
export function passedOverByDesign(why) {
|
|
1608
1829
|
return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT || why === OUTSIDE_INCLUDE || why === LINK_NOT_WALKED;
|
|
@@ -1618,18 +1839,18 @@ export const OTHER_MOUNT = "another mount's directory";
|
|
|
1618
1839
|
* Every nested-mount check (a pass's, a walk's, a push's, grep's) asks this.
|
|
1619
1840
|
*/
|
|
1620
1841
|
export function inOtherMount(relativePath, others) {
|
|
1621
|
-
const
|
|
1622
|
-
return others.some((dir) =>
|
|
1842
|
+
const segments = posix.normalize(relativePath).replace(/\/+$/, '').split('/');
|
|
1843
|
+
return others.some((dir) => beneathNested(segments, { dir }));
|
|
1623
1844
|
}
|
|
1624
1845
|
/**
|
|
1625
|
-
* Why a walk passes over an entry by design, by its mount-relative path
|
|
1626
|
-
*
|
|
1627
|
-
* name. Undefined when none covers it.
|
|
1846
|
+
* Why a walk passes over an entry by design, by its mount-relative path, its
|
|
1847
|
+
* name and whether it's a directory: another mount's directory, the ignore
|
|
1848
|
+
* list, or an editor's file name. Undefined when none covers it.
|
|
1628
1849
|
*/
|
|
1629
|
-
function passedOverBecause(relativePath, name, patterns, others) {
|
|
1850
|
+
function passedOverBecause(relativePath, name, patterns, others, directory) {
|
|
1630
1851
|
if (inOtherMount(relativePath, others))
|
|
1631
1852
|
return OTHER_MOUNT;
|
|
1632
|
-
if (isIgnored(relativePath,
|
|
1853
|
+
if (isIgnored(relativePath, patterns, directory))
|
|
1633
1854
|
return IGNORED;
|
|
1634
1855
|
if (EDITOR_FILE.test(name))
|
|
1635
1856
|
return EDITOR_FILE_REASON;
|
|
@@ -1637,17 +1858,17 @@ function passedOverBecause(relativePath, name, patterns, others) {
|
|
|
1637
1858
|
}
|
|
1638
1859
|
/**
|
|
1639
1860
|
* Why a sync without a path, and the watcher, pass over a mount-relative
|
|
1640
|
-
* path: the path itself or a directory above it is passed
|
|
1641
|
-
* the walk reads each entry by its path and
|
|
1642
|
-
* directory is that mount's, whatever else covers a
|
|
1643
|
-
* Undefined when nothing covers it.
|
|
1861
|
+
* path, a directory or not: the path itself or a directory above it is passed
|
|
1862
|
+
* over by design, as the walk reads each entry by its path, name and kind. A
|
|
1863
|
+
* path in another mount's directory is that mount's, whatever else covers a
|
|
1864
|
+
* directory above it. Undefined when nothing covers it.
|
|
1644
1865
|
*/
|
|
1645
|
-
export function passedOverAt(relativePath, patterns, others) {
|
|
1866
|
+
export function passedOverAt(relativePath, patterns, others, directory) {
|
|
1646
1867
|
if (inOtherMount(relativePath, others))
|
|
1647
1868
|
return OTHER_MOUNT;
|
|
1648
1869
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1649
1870
|
for (let i = 0; i < segments.length; i++) {
|
|
1650
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
|
|
1871
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, i < segments.length - 1 || directory);
|
|
1651
1872
|
if (why !== undefined)
|
|
1652
1873
|
return why;
|
|
1653
1874
|
}
|
|
@@ -1663,28 +1884,19 @@ export function passedOverBetween(scope, relativePath, patterns, others) {
|
|
|
1663
1884
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1664
1885
|
const from = scope === '' ? 0 : scope.split('/').filter(Boolean).length;
|
|
1665
1886
|
for (let i = from; i < segments.length - 1; i++) {
|
|
1666
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
|
|
1887
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, true);
|
|
1667
1888
|
if (why !== undefined)
|
|
1668
1889
|
return why;
|
|
1669
1890
|
}
|
|
1670
1891
|
return undefined;
|
|
1671
1892
|
}
|
|
1672
|
-
/**
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
if (relativePath === prefix || relativePath.startsWith(prefix + '/'))
|
|
1680
|
-
return true;
|
|
1681
|
-
}
|
|
1682
|
-
if (!pattern.includes('/') && relativePath === pattern)
|
|
1683
|
-
return true;
|
|
1684
|
-
if (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))
|
|
1685
|
-
return true;
|
|
1686
|
-
}
|
|
1687
|
-
return false;
|
|
1893
|
+
/** Whether an ignore pattern matches the entry at a mount-relative path, a directory or not, itself. */
|
|
1894
|
+
function isIgnored(relativePath, patterns, directory) {
|
|
1895
|
+
const segments = relativePath.split('/');
|
|
1896
|
+
return patterns.some((p) => {
|
|
1897
|
+
const pattern = readPattern(p);
|
|
1898
|
+
return typeof pattern !== 'string' && matchesEntry(pattern, segments, directory);
|
|
1899
|
+
});
|
|
1688
1900
|
}
|
|
1689
1901
|
/**
|
|
1690
1902
|
* A subdirectory a checked directory's listing showed, checked in turn: still a
|
|
@@ -1699,13 +1911,13 @@ async function checkChild(view, rootReal, parent, rel) {
|
|
|
1699
1911
|
}
|
|
1700
1912
|
catch (err) {
|
|
1701
1913
|
if (errno(err) !== 'ENOENT')
|
|
1702
|
-
return { kind: 'unobserved',
|
|
1914
|
+
return { kind: 'unobserved', ...failedOn('cannot stat', err) };
|
|
1703
1915
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
1704
|
-
return
|
|
1916
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
|
|
1705
1917
|
}
|
|
1706
1918
|
if (!info.isDirectory())
|
|
1707
1919
|
return { kind: 'unobserved', reason: CHANGED_WALK };
|
|
1708
|
-
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino) };
|
|
1920
|
+
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs) };
|
|
1709
1921
|
}
|
|
1710
1922
|
/**
|
|
1711
1923
|
* List a directory scope ('' for the mount root), descending when recursive,
|
|
@@ -1734,12 +1946,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1734
1946
|
};
|
|
1735
1947
|
// A walk that isn't named passes over a scope the mount passes over by
|
|
1736
1948
|
// design, at it or a directory above it, as its parent's listing would pass
|
|
1737
|
-
// over its entry
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
//
|
|
1741
|
-
//
|
|
1742
|
-
// sync that names it, whose scope is walked (#276), below.
|
|
1949
|
+
// over its entry. So a listing, glob or grep there never starts tracking
|
|
1950
|
+
// what the mount leaves out, and a stored copy beneath it is unobserved, as
|
|
1951
|
+
// beneath any region passed over by design. Its region says how to take
|
|
1952
|
+
// what's there in: a sync that names it, whose scope is walked (#276),
|
|
1953
|
+
// below.
|
|
1743
1954
|
// Another mount's directory is passed over by every walk, named or not,
|
|
1744
1955
|
// and no sync of this mount takes it in, so its region gives no advice.
|
|
1745
1956
|
if (scope !== '' && inOtherMount(scope, view.others)) {
|
|
@@ -1748,7 +1959,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1748
1959
|
return walk;
|
|
1749
1960
|
}
|
|
1750
1961
|
if (!opts.named && scope !== '') {
|
|
1751
|
-
|
|
1962
|
+
let why = passedOverAt(scope, view.ignore, view.others, true);
|
|
1963
|
+
if (why !== undefined && passedOverAt(scope, view.ignore, view.others, false) === undefined) {
|
|
1964
|
+
if ((await directoryAt(join(rootReal, scope), view.followSymlinks)) === false)
|
|
1965
|
+
why = undefined;
|
|
1966
|
+
}
|
|
1752
1967
|
if (why !== undefined) {
|
|
1753
1968
|
walk.others.set(scope, why);
|
|
1754
1969
|
walk.incomplete.push({ path: scope, reason: `${why}: a sync that names this path takes in what's there, editors' files aside`, kind: 'ignored' });
|
|
@@ -1761,7 +1976,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1761
1976
|
return walk;
|
|
1762
1977
|
}
|
|
1763
1978
|
if (top.kind === 'unobserved') {
|
|
1764
|
-
walk.incomplete.push({ path: scope, reason: top.outside ?
|
|
1979
|
+
walk.incomplete.push({ path: scope, reason: top.outside ? RESOLVES_OUTSIDE : top.reason, kind: 'error', ...(top.errno !== undefined ? { errno: top.errno } : {}) });
|
|
1765
1980
|
return walk;
|
|
1766
1981
|
}
|
|
1767
1982
|
// A scope a path sync names is walked even where the ignore list covers
|
|
@@ -1786,11 +2001,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1786
2001
|
const visited = new Set();
|
|
1787
2002
|
/** Paths already reported as errors, so a second visit doesn't report them again. */
|
|
1788
2003
|
const failed = new Set();
|
|
1789
|
-
const fail = (path, reason) => {
|
|
2004
|
+
const fail = (path, reason, code) => {
|
|
1790
2005
|
if (failed.has(path))
|
|
1791
2006
|
return;
|
|
1792
2007
|
failed.add(path);
|
|
1793
|
-
walk.incomplete.push({ path, reason, kind: 'error' });
|
|
2008
|
+
walk.incomplete.push({ path, reason, kind: 'error', ...(code !== undefined ? { errno: code } : {}) });
|
|
1794
2009
|
};
|
|
1795
2010
|
let left = cap;
|
|
1796
2011
|
let lastTaken;
|
|
@@ -1866,6 +2081,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1866
2081
|
const proof = rel === '' ? ROOT_UNAVAILABLE : await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
1867
2082
|
if (typeof proof === 'string')
|
|
1868
2083
|
fail(rel, proof);
|
|
2084
|
+
else if ('reason' in proof)
|
|
2085
|
+
fail(rel, proof.reason, proof.errno);
|
|
1869
2086
|
else {
|
|
1870
2087
|
walk.missing.add(rel);
|
|
1871
2088
|
// Nothing was listed, so a later check that finds it again, on the
|
|
@@ -1874,7 +2091,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1874
2091
|
}
|
|
1875
2092
|
return null;
|
|
1876
2093
|
}
|
|
1877
|
-
|
|
2094
|
+
const listed = failedOn('cannot list', err);
|
|
2095
|
+
fail(rel, listed.reason, listed.errno);
|
|
1878
2096
|
return null;
|
|
1879
2097
|
}
|
|
1880
2098
|
entries.sort((a, b) => compareNames(a.name, b.name));
|
|
@@ -1892,9 +2110,14 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1892
2110
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1893
2111
|
// Another mount's directory is passed over by its real path too, so a
|
|
1894
2112
|
// walk that reaches one through a link to a directory above it lists
|
|
1895
|
-
// none of that mount's names (
|
|
1896
|
-
|
|
1897
|
-
?? (entry.
|
|
2113
|
+
// none of that mount's names (nestedRootAtReal).
|
|
2114
|
+
let passedOver = passedOverBecause(childRel, entry.name, ignore, view.others, entry.isDirectory())
|
|
2115
|
+
?? (!entry.isSymbolicLink() && nestedRootAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
|
|
2116
|
+
if (passedOver === undefined && view.followSymlinks && entry.isSymbolicLink()) {
|
|
2117
|
+
const asDirectory = passedOverBecause(childRel, entry.name, ignore, view.others, true);
|
|
2118
|
+
if (asDirectory !== undefined && (await directoryAt(dir.at(entry.name), true)) === true)
|
|
2119
|
+
passedOver = asDirectory;
|
|
2120
|
+
}
|
|
1898
2121
|
if (passedOver !== undefined) {
|
|
1899
2122
|
others.push([childRel, passedOver]);
|
|
1900
2123
|
regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
|
|
@@ -1937,7 +2160,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1937
2160
|
else {
|
|
1938
2161
|
const symlink = entry.isSymbolicLink();
|
|
1939
2162
|
leaves.push([childRel, false]);
|
|
1940
|
-
others.push([childRel, symlink ? 'a symlink' :
|
|
2163
|
+
others.push([childRel, symlink ? 'a symlink' : NOT_REGULAR]);
|
|
1941
2164
|
if (symlink)
|
|
1942
2165
|
regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
|
|
1943
2166
|
}
|
|
@@ -1971,10 +2194,10 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
1971
2194
|
return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
|
|
1972
2195
|
};
|
|
1973
2196
|
// Each nested root by which directory it is, read once, when the walk
|
|
1974
|
-
// first descends: a directory the walk reaches
|
|
1975
|
-
//
|
|
1976
|
-
// a
|
|
1977
|
-
// mount's directory.
|
|
2197
|
+
// first descends: a directory the walk reaches by a path that no
|
|
2198
|
+
// comparison of names places in the nested mount's directory, as through a
|
|
2199
|
+
// bind mount, or on a disk whose folding goes past the comparison's, is
|
|
2200
|
+
// still another mount's directory.
|
|
1978
2201
|
let nestedRoots;
|
|
1979
2202
|
const isNestedRoot = async (dir) => {
|
|
1980
2203
|
if (!view.nested?.length)
|
|
@@ -2043,7 +2266,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
2043
2266
|
if (child.kind === 'absent')
|
|
2044
2267
|
walk.missing.add(sub);
|
|
2045
2268
|
else
|
|
2046
|
-
fail(sub, child.reason);
|
|
2269
|
+
fail(sub, child.reason, child.errno);
|
|
2047
2270
|
}
|
|
2048
2271
|
};
|
|
2049
2272
|
if (after === undefined) {
|
|
@@ -2058,6 +2281,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
2058
2281
|
// directories a listing showed that weren't listed. A walk that doesn't
|
|
2059
2282
|
// descend leaves its subdirectories by design.
|
|
2060
2283
|
const capped = `the walk cap (${cap}) was reached`;
|
|
2284
|
+
const raise = "whoever runs this host can raise the mount's walkCap";
|
|
2061
2285
|
const holdingUntaken = new Set();
|
|
2062
2286
|
for (const f of walk.seen)
|
|
2063
2287
|
holdingUntaken.add(parentOf(f));
|
|
@@ -2065,7 +2289,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
2065
2289
|
if (!passedOverByDesign(why) && !walk.othersTaken.has(p))
|
|
2066
2290
|
holdingUntaken.add(parentOf(p));
|
|
2067
2291
|
for (const d of holdingUntaken) {
|
|
2068
|
-
walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken`, kind: 'cap' });
|
|
2292
|
+
walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken; naming a path beneath it walks only that path, and ${raise}`, kind: 'cap' });
|
|
2069
2293
|
}
|
|
2070
2294
|
let unlisted = 0;
|
|
2071
2295
|
if (recursive) {
|
|
@@ -2073,7 +2297,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
|
|
|
2073
2297
|
if (visited.has(d) || walk.missing.has(d) || failed.has(d))
|
|
2074
2298
|
continue;
|
|
2075
2299
|
unlisted++;
|
|
2076
|
-
walk.incomplete.push({ path: d, reason: `not visited: ${capped}`, kind: 'cap' });
|
|
2300
|
+
walk.incomplete.push({ path: d, reason: `not visited: ${capped}; naming this path walks only it, and ${raise}`, kind: 'cap' });
|
|
2077
2301
|
}
|
|
2078
2302
|
}
|
|
2079
2303
|
// The cap stopped the walk only if it left something: a walk whose last
|