@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
|
@@ -47,6 +47,7 @@ import { lstat, mkdir, open, readdir, readlink, realpath, stat, statfs } from 'n
|
|
|
47
47
|
import type { FileHandle } from 'node:fs/promises';
|
|
48
48
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
49
49
|
import { sameRootIdentity, type Fingerprint, type RootIdentity } from './disk-agreement.js';
|
|
50
|
+
import { OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoCode, systemWords } from './answers.js';
|
|
50
51
|
|
|
51
52
|
export interface MountView {
|
|
52
53
|
/**
|
|
@@ -84,6 +85,8 @@ export interface MountView {
|
|
|
84
85
|
nested?: readonly NestedMount[];
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
export type NestedDir = { dir: string; guessed?: true };
|
|
89
|
+
|
|
87
90
|
/**
|
|
88
91
|
* A mount nested in another: its directory in one of its spellings,
|
|
89
92
|
* mount-relative and `/`-separated, its name, its real root, which every
|
|
@@ -94,7 +97,7 @@ export interface MountView {
|
|
|
94
97
|
* it: the mount is nested by its configured path alone, since through its
|
|
95
98
|
* real root it would claim this mount's every path.
|
|
96
99
|
*/
|
|
97
|
-
export type NestedMount =
|
|
100
|
+
export type NestedMount = NestedDir & { name: string; real?: string; include?: readonly string[] };
|
|
98
101
|
|
|
99
102
|
export type StatFingerprint = Omit<Fingerprint, 'hashedAt'>;
|
|
100
103
|
|
|
@@ -112,7 +115,17 @@ export type Observation =
|
|
|
112
115
|
| { kind: 'absent' }
|
|
113
116
|
/** `mode`: the file's permission bits (`st_mode & 0o777`). */
|
|
114
117
|
| { kind: 'file'; size: number; mtimeMs: number; mode: number; fp: StatFingerprint }
|
|
115
|
-
| { kind: 'unobserved'
|
|
118
|
+
| ({ kind: 'unobserved' } & NotShown);
|
|
119
|
+
|
|
120
|
+
export interface Cause {
|
|
121
|
+
errno?: string;
|
|
122
|
+
again?: true;
|
|
123
|
+
ownStep?: true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface NotShown extends Cause {
|
|
127
|
+
reason: string;
|
|
128
|
+
}
|
|
116
129
|
|
|
117
130
|
export interface ReadResult {
|
|
118
131
|
kind: 'read';
|
|
@@ -145,18 +158,30 @@ export const RACY_WINDOW_MS = 2000;
|
|
|
145
158
|
|
|
146
159
|
export const OUTSIDE_PARENT = 'a parent directory resolves outside the mount';
|
|
147
160
|
export const NOT_FOLLOWED = 'a symlink, which this mount does not follow';
|
|
161
|
+
export const DIRECTORY = 'a directory';
|
|
162
|
+
export const NOT_REGULAR = 'not a regular file';
|
|
163
|
+
export const LEAVES_MOUNT = 'a symlink that leaves the mount';
|
|
164
|
+
export const NOT_A_DIRECTORY = 'a parent path is not a directory';
|
|
148
165
|
export const ROOT_UNAVAILABLE = 'the mount root is unavailable';
|
|
149
166
|
export const ROOT_REPLACED =
|
|
150
167
|
'the mount root is not the directory disk last agreed with (an unmounted drive, or a replaced directory): ' +
|
|
151
168
|
'reconnect it; if it was replaced on purpose, sync the mount with acceptRoot to take it as it is now';
|
|
152
|
-
const CHANGED = 'it changed while it was observed';
|
|
153
|
-
const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
154
|
-
const
|
|
169
|
+
export const CHANGED = 'it changed while it was observed';
|
|
170
|
+
export const CHANGED_WALK = 'the directory changed while the walk ran';
|
|
171
|
+
export const FILE_CHANGED = 'the file changed while it was read';
|
|
172
|
+
export const RESOLVES_OUTSIDE = 'the directory resolves outside the mount';
|
|
173
|
+
export const LINK_TO_DIRECTORY = 'a symlink to a directory, not walked';
|
|
174
|
+
export const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
|
|
155
175
|
|
|
156
176
|
export function errno(err: unknown): string | undefined {
|
|
157
177
|
return (err as { code?: string } | null)?.code;
|
|
158
178
|
}
|
|
159
179
|
|
|
180
|
+
function failedOn(what: string, err: unknown): NotShown {
|
|
181
|
+
const code = errno(err);
|
|
182
|
+
return { reason: `${what} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
export function contained(rootReal: string, candidate: string): boolean {
|
|
161
186
|
return candidate === rootReal || candidate.startsWith(rootReal.endsWith(sep) ? rootReal : rootReal + sep);
|
|
162
187
|
}
|
|
@@ -258,7 +283,9 @@ export async function linkRefusal(
|
|
|
258
283
|
/**
|
|
259
284
|
* Where a symlink the walk found leads, for its refusal: to a path inside
|
|
260
285
|
* the mount (mount-relative, as disk spells it, and whether it's a
|
|
261
|
-
* directory), outside the mount, or nowhere
|
|
286
|
+
* directory), outside the mount, or nowhere (nothing is there, or the links
|
|
287
|
+
* loop); null where the realpath fails otherwise, as on a directory this
|
|
288
|
+
* process can't search, so where it leads isn't known. One
|
|
262
289
|
* realpath, one stat and the respelling, made only when refusing. realpath
|
|
263
290
|
* keeps the spelling the link's target gives, which on a disk that folds
|
|
264
291
|
* names need not be disk's, so the target is respelled before anything
|
|
@@ -266,9 +293,14 @@ export async function linkRefusal(
|
|
|
266
293
|
*/
|
|
267
294
|
export type LinkLeads = { kind: 'inside'; target: string; directory: boolean } | { kind: 'outside' } | { kind: 'nowhere' };
|
|
268
295
|
|
|
269
|
-
export async function linkLeads(root: string, rootReal: string, link: string): Promise<LinkLeads> {
|
|
270
|
-
|
|
271
|
-
|
|
296
|
+
export async function linkLeads(root: string, rootReal: string, link: string): Promise<LinkLeads | null> {
|
|
297
|
+
let leads: string;
|
|
298
|
+
try {
|
|
299
|
+
leads = await realpath(join(root, link));
|
|
300
|
+
} catch (err) {
|
|
301
|
+
const code = errno(err);
|
|
302
|
+
return code === 'ENOENT' || code === 'ENOTDIR' || code === 'ELOOP' ? { kind: 'nowhere' } : null;
|
|
303
|
+
}
|
|
272
304
|
if (!contained(rootReal, leads)) return { kind: 'outside' };
|
|
273
305
|
const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
|
|
274
306
|
return { kind: 'inside', target: await diskSpelling(rootReal, relative(rootReal, leads).split(sep).join('/')), directory };
|
|
@@ -279,21 +311,23 @@ export async function linkLeads(root: string, rootReal: string, link: string): P
|
|
|
279
311
|
* innermost where nested mounts nest, as #460 picks one; undefined when none
|
|
280
312
|
* does.
|
|
281
313
|
*/
|
|
282
|
-
export function nestedHolder<N extends
|
|
283
|
-
return nested
|
|
284
|
-
.filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
|
|
285
|
-
.sort((a, b) => b.dir.length - a.dir.length)[0];
|
|
314
|
+
export function nestedHolder<N extends NestedDir & { name: string }>(target: string, nested: readonly N[]): N | undefined {
|
|
315
|
+
return nestedHolders(target, nested)[0];
|
|
286
316
|
}
|
|
287
317
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return
|
|
318
|
+
function nestedHolders<N extends NestedDir>(target: string, nested: readonly N[]): N[] {
|
|
319
|
+
const segments = target.split('/');
|
|
320
|
+
return mayHold(nested.filter((n) => beneathNested(segments, n)), (n) => n.dir.split('/').length, (n) => !n.guessed && surelyBeneath(segments, n.dir.split('/')));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function mayHold<T>(holders: readonly T[], depth: (holder: T) => number, sure: (holder: T) => boolean): T[] {
|
|
324
|
+
const sorted = [...holders].sort((a, b) => depth(b) - depth(a) || Number(sure(b)) - Number(sure(a)));
|
|
325
|
+
const surely = sorted.findIndex(sure);
|
|
326
|
+
return surely === -1 ? sorted : sorted.slice(0, surely + 1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function namesHolder(leads: { target: string; directory: boolean }, nested: readonly NestedMount[]): boolean {
|
|
330
|
+
return nestedHolders(leads.target, nested).every((holder) => namesNestedPlace(restBeneath(leads.target, holder), holder.include, leads.directory));
|
|
297
331
|
}
|
|
298
332
|
|
|
299
333
|
/**
|
|
@@ -312,18 +346,12 @@ export function namesNestedPlace(rest: string, include: readonly string[] | unde
|
|
|
312
346
|
return directory || insideMount(rest, include);
|
|
313
347
|
}
|
|
314
348
|
|
|
315
|
-
/**
|
|
316
|
-
* Whether a path of a mount is one its passes decide and its listings may
|
|
317
|
-
* name: inside its include, and, in a nested mount's directory, a place
|
|
318
|
-
* that mount names (namesNestedPlace), its directory included. History the
|
|
319
|
-
* mount took in before another was nested there, which that mount's
|
|
320
|
-
* include leaves out, is no mount's, so no pass decides it, and no listing
|
|
321
|
-
* names it or a directory only it implies.
|
|
322
|
-
*/
|
|
323
349
|
export function withinIncludes(view: Pick<MountView, 'include' | 'nested'>, path: string): boolean {
|
|
324
350
|
if (!insideMount(path, view.include)) return false;
|
|
325
|
-
|
|
326
|
-
|
|
351
|
+
return nestedHolders(path, view.nested ?? []).every((holder) => {
|
|
352
|
+
const rest = restBeneath(path, holder);
|
|
353
|
+
return rest === '' || namesNestedPlace(rest, holder.include, false);
|
|
354
|
+
});
|
|
327
355
|
}
|
|
328
356
|
|
|
329
357
|
/**
|
|
@@ -394,16 +422,23 @@ export async function coversLanding(view: Pick<MountView, 'include'>, rootReal:
|
|
|
394
422
|
|
|
395
423
|
/**
|
|
396
424
|
* Why a path is refused for what linkOnTheWay found, given where a link
|
|
397
|
-
* leads (linkLeads). For a link, the link is the subject,
|
|
398
|
-
* leads: for a link inside the mount, its target,
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
425
|
+
* leads (linkLeads), and the way on. For a link, the link is the subject,
|
|
426
|
+
* with where it leads: for a link inside the mount, its target, as the path
|
|
427
|
+
* to use in its place, and for a link to a directory there, the setting
|
|
428
|
+
* whoever runs the host can change too; for a link to something that isn't
|
|
429
|
+
* a directory, that nothing lies beneath it (the link stands among the
|
|
430
|
+
* path's directories, so following it can't make the path beneath it
|
|
431
|
+
* exist); for one leading outside the mount, that it does, and that
|
|
432
|
+
* whoever runs the host can mount what it leads to, or a directory that
|
|
433
|
+
* holds it; for one leading nowhere, that whoever runs the host can remove it or make
|
|
434
|
+
* it lead to what it stood for; for one whose target the refusal doesn't
|
|
435
|
+
* tell, what whoever runs the host can do wherever it leads
|
|
436
|
+
* (UNFOLLOWED_STEP).
|
|
437
|
+
* A target in a nested mount's directory (`nested`) is that mount's: the
|
|
438
|
+
* refusal names the mount and the path to use there in place of the link,
|
|
439
|
+
* and offers no setting, since following the link would only reach into
|
|
440
|
+
* another mount's directory. For a directory the walk couldn't ask, which
|
|
441
|
+
* one, the answer it got, and the errno's way on (errnoClause). Paths
|
|
407
442
|
* are spelled as the tools take them, from the mount's name. On a mount with
|
|
408
443
|
* an include, an inside target is named only where a tool could name it: a
|
|
409
444
|
* file the include covers, or a directory, which a listing names whatever
|
|
@@ -414,22 +449,33 @@ export async function coversLanding(view: Pick<MountView, 'include'>, rootReal:
|
|
|
414
449
|
*/
|
|
415
450
|
export function linkWords(name: string, found: LinkFound, leads: LinkLeads | null, nested: readonly NestedMount[] = [], include?: readonly string[]): string {
|
|
416
451
|
const spell = (rel: string) => `"${rel === '' ? name : `${name}/${rel}`}"`;
|
|
417
|
-
if (!('link' in found))
|
|
452
|
+
if (!('link' in found)) {
|
|
453
|
+
const code = errnoCode({ code: found.code });
|
|
454
|
+
return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(code, found.code)}); ${errnoClause(code, 'calling again', 'calling again then gets past it')}`;
|
|
455
|
+
}
|
|
418
456
|
const link = `${spell(found.link)} is a symlink`;
|
|
419
|
-
|
|
420
|
-
if (leads
|
|
457
|
+
const unknown = `${link}, which this mount does not follow; ${UNFOLLOWED_STEP}`;
|
|
458
|
+
if (leads === null) return unknown;
|
|
459
|
+
if (leads.kind === 'nowhere') {
|
|
460
|
+
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`;
|
|
461
|
+
}
|
|
462
|
+
if (leads.kind === 'outside') {
|
|
463
|
+
return `${link} leading outside the mount, which this mount does not follow; followed or not, ${OUTSIDE_STEP}`;
|
|
464
|
+
}
|
|
421
465
|
const holder = nestedHolder(leads.target, nested);
|
|
422
466
|
if (holder) {
|
|
423
|
-
if (!namesHolder(leads,
|
|
424
|
-
const rest = leads.target
|
|
425
|
-
|
|
426
|
-
|
|
467
|
+
if (!namesHolder(leads, nested)) return unknown;
|
|
468
|
+
const rest = restBeneath(leads.target, holder);
|
|
469
|
+
const guess = nestedGuess(name, leads.target.split('/'), holder);
|
|
470
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
|
|
471
|
+
`"${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)}` })}`;
|
|
427
472
|
}
|
|
428
|
-
if (!leads.directory && !insideMount(leads.target, include)) return
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return `${link} to ${spell(leads.target)}, which this mount does not follow${
|
|
473
|
+
if (!leads.directory && !insideMount(leads.target, include)) return unknown;
|
|
474
|
+
if (!leads.directory) return `${link} to ${spell(leads.target)}, which this mount does not follow; that isn't a directory, so nothing lies beneath it`;
|
|
475
|
+
// Either way serves only what the include covers where it leads (judgedAt).
|
|
476
|
+
const covered = include === undefined ? '' : '; either way, the mount reaches only what its include covers there';
|
|
477
|
+
return `${link} to ${spell(leads.target)}, which this mount does not follow; use ${spell(leads.target)} in place of ${spell(found.link)}, ` +
|
|
478
|
+
`or whoever runs this host can set followSymlinks: true on mount "${name}"${covered}`;
|
|
433
479
|
}
|
|
434
480
|
|
|
435
481
|
/**
|
|
@@ -446,26 +492,85 @@ export async function shownLeads(root: string, rootReal: string, found: LinkFoun
|
|
|
446
492
|
}
|
|
447
493
|
|
|
448
494
|
/** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
|
|
449
|
-
export async function linkReason(view: Pick<MountView, 'root' | 'name' | 'nested' | 'include'>, rootReal: string, found: LinkFound): Promise<
|
|
450
|
-
return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
|
|
495
|
+
export async function linkReason(view: Pick<MountView, 'root' | 'name' | 'nested' | 'include'>, rootReal: string, found: LinkFound): Promise<NotShown> {
|
|
496
|
+
return { reason: linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include), ownStep: true };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function realSegments(path: string): string[] {
|
|
500
|
+
return path.split(sep).filter((segment, i) => i === 0 || segment !== '');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function beneathRoot(path: string, root: string): string | undefined {
|
|
504
|
+
const segments = realSegments(path);
|
|
505
|
+
const top = realSegments(root);
|
|
506
|
+
return segments.length > top.length && beginsWith(segments, top) ? segments.slice(top.length).join('/') : undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function sameRoot(path: string, root: string): boolean {
|
|
510
|
+
const segments = realSegments(path);
|
|
511
|
+
const top = realSegments(root);
|
|
512
|
+
return segments.length === top.length && beginsWith(segments, top);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function surelyAtRoot(path: string, root: string): boolean {
|
|
516
|
+
return surelyBeneath(realSegments(path), realSegments(root));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function nestedRootAtReal(real: string, nested: readonly NestedMount[] = []): { mount: NestedMount; depth: number } | undefined {
|
|
520
|
+
return nestedRootsAtReal(real, nested)[0];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function nestedRootsAtReal(real: string, nested: readonly NestedMount[]): Array<{ mount: NestedMount; depth: number }> {
|
|
524
|
+
// realpath keeps the spelling it's given where disk folds it on Linux, as
|
|
525
|
+
// from a link's target, so a real root is compared as loosely as a path.
|
|
526
|
+
const segments = realSegments(real);
|
|
527
|
+
const holders = nested.flatMap((n) => {
|
|
528
|
+
if (n.real === undefined) return [];
|
|
529
|
+
const root = realSegments(n.real);
|
|
530
|
+
return beginsWith(segments, root) ? [{ mount: n, depth: root.length, sure: surelyBeneath(segments, root) }] : [];
|
|
531
|
+
});
|
|
532
|
+
return mayHold(holders, (h) => h.depth, (h) => h.sure);
|
|
451
533
|
}
|
|
452
534
|
|
|
453
535
|
/**
|
|
454
536
|
* The nested mount whose root holds `real`, a canonical path, the innermost,
|
|
455
|
-
* with the path to use there
|
|
456
|
-
*
|
|
537
|
+
* with the path to use there and the path beneath its root (`rest`);
|
|
538
|
+
* undefined when none does. A path the lexical checks pass
|
|
457
539
|
* (inOtherMount, the tools' holderOf) reaches another mount's directory this
|
|
458
540
|
* way through a symlink the mount follows, so each check that resolves a
|
|
459
|
-
* path canonically asks this too
|
|
460
|
-
* namesNestedPlace says the place is that mount's.
|
|
541
|
+
* path canonically asks this too.
|
|
461
542
|
*/
|
|
462
|
-
export function nestedAtReal(real: string, nested: readonly NestedMount[] = []): { name: string; use: string; rest: string;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
.sort((a, b) => b.real.length - a.real.length)[0];
|
|
543
|
+
export async function nestedAtReal(real: string, nested: readonly NestedMount[] = [], at?: { name: string; rootReal: string }): Promise<{ name: string; use: string; rest: string; judgedByInclude: boolean; names: (directory: boolean) => boolean; guess?: NestedGuess } | undefined> {
|
|
544
|
+
const holders = nestedRootsAtReal(real, nested);
|
|
545
|
+
const holder = holders[0];
|
|
466
546
|
if (!holder) return undefined;
|
|
467
|
-
const
|
|
468
|
-
|
|
547
|
+
const segments = realSegments(real);
|
|
548
|
+
// realpath keeps a link target's spelling beneath the root too, so the
|
|
549
|
+
// rest is read as disk spells it, for each mount's include to judge.
|
|
550
|
+
const rests = await Promise.all(holders.map(async (h) => {
|
|
551
|
+
const given = segments.slice(h.depth).join('/');
|
|
552
|
+
return given === '' ? '' : await diskSpelling(h.mount.real!, given);
|
|
553
|
+
}));
|
|
554
|
+
const rest = rests[0]!;
|
|
555
|
+
const { name } = holder.mount;
|
|
556
|
+
const root = realSegments(holder.mount.real!);
|
|
557
|
+
const found = {
|
|
558
|
+
name,
|
|
559
|
+
use: rest ? `${name}/${rest}` : name,
|
|
560
|
+
rest,
|
|
561
|
+
judgedByInclude: holders.some((h) => h.mount.include !== undefined),
|
|
562
|
+
names: (directory: boolean) => holders.every((h, i) => namesNestedPlace(rests[i]!, h.mount.include, directory)),
|
|
563
|
+
};
|
|
564
|
+
if (surelyBeneath(segments, root)) return found;
|
|
565
|
+
if (at === undefined) return { ...found, guess: null };
|
|
566
|
+
const top = realSegments(at.rootReal);
|
|
567
|
+
if (segments.length <= top.length || root.length <= top.length || !top.every((t, i) => segments[i] === t)) return { ...found, guess: null };
|
|
568
|
+
const within = segments.slice(top.length);
|
|
569
|
+
const below = root.slice(top.length);
|
|
570
|
+
const guess = guessFor(at.name, within, below);
|
|
571
|
+
const leads = guess === undefined ? [at.name, ...within].join('/') : [guess.given, ...within.slice(below.length)].join('/');
|
|
572
|
+
if (!surelyBeneath(root.slice(0, top.length), top)) return { ...found, guess: { ...placedGuess(at.name, within.slice(0, below.length), guess), leads } };
|
|
573
|
+
return { ...found, guess: guess === undefined ? null : { ...guess, leads } };
|
|
469
574
|
}
|
|
470
575
|
|
|
471
576
|
/**
|
|
@@ -473,8 +578,9 @@ export function nestedAtReal(real: string, nested: readonly NestedMount[] = []):
|
|
|
473
578
|
* refused (nestedAtReal): what it leads into, and the path to use in its
|
|
474
579
|
* place. Spelled as the tools take paths, from the mount's name.
|
|
475
580
|
*/
|
|
476
|
-
export function leadsInto(name: string, rel: string, holder: { name: string; use: string }): string {
|
|
477
|
-
|
|
581
|
+
export function leadsInto(name: string, rel: string, holder: { name: string; use: string; guess?: NestedGuess }): string {
|
|
582
|
+
const verb = holder.guess === undefined ? 'leads' : 'may lead';
|
|
583
|
+
return `"${rel === '' ? name : `${name}/${rel}`}" ${verb} into mount "${holder.name}", nested in mount "${name}": ${nestedStep(holder, { after: ' in its place' })}`;
|
|
478
584
|
}
|
|
479
585
|
|
|
480
586
|
// ===========================================================================
|
|
@@ -488,9 +594,8 @@ const otherCase = (c: string): string => (c === c.toLowerCase() ? c.toUpperCase(
|
|
|
488
594
|
|
|
489
595
|
/**
|
|
490
596
|
* The name with each letter's case flipped, where its other case is one
|
|
491
|
-
* letter whose own other case is the letter again
|
|
492
|
-
*
|
|
493
|
-
* and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
597
|
+
* letter whose own other case is the letter again. `ß`, whose upper case is
|
|
598
|
+
* `SS`, and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
|
|
494
599
|
*/
|
|
495
600
|
const flippedCase = (name: string): string =>
|
|
496
601
|
[...name].map((c) => {
|
|
@@ -509,6 +614,17 @@ const folded = (name: string): string => name.normalize('NFC').toLowerCase();
|
|
|
509
614
|
*/
|
|
510
615
|
export const foldedLoosely = (name: string): string => name.normalize('NFC').toLowerCase().toUpperCase().toLowerCase();
|
|
511
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Linux's casefold, which ext4 and f2fs set per directory, drops
|
|
619
|
+
* default-ignorable code points (on every kernel but 6.12 to 6.12.4) and
|
|
620
|
+
* compares names decomposed, in full case folding, and HFS+ passes over some
|
|
621
|
+
* of those code points too. Case mapped on the decomposed name joins marks
|
|
622
|
+
* that come in another order around a combining ypogegrammeni (U+0345),
|
|
623
|
+
* whose upper case is a letter.
|
|
624
|
+
*/
|
|
625
|
+
const foldedFully = (name: string): string =>
|
|
626
|
+
name.replace(/\p{Default_Ignorable_Code_Point}/gu, '').normalize('NFD').toLowerCase().toUpperCase().toLowerCase().normalize('NFD');
|
|
627
|
+
|
|
512
628
|
/** What the workspace holds of a mount, as the spelling step reads it (diskSpelling). */
|
|
513
629
|
export interface WorkspaceEntries {
|
|
514
630
|
/**
|
|
@@ -616,18 +732,81 @@ function entryName(paths: readonly string[], prefix: string, segment: string): s
|
|
|
616
732
|
return found;
|
|
617
733
|
}
|
|
618
734
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
735
|
+
function sameLevel(given: string, name: string): boolean {
|
|
736
|
+
return given === name || (given !== '' && foldedFully(given) === foldedFully(name));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function beginsWith(segments: readonly string[], dir: readonly string[]): boolean {
|
|
740
|
+
return segments.length >= dir.length && dir.every((d, i) => sameLevel(segments[i]!, d));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function beneathNested(segments: readonly string[], nested: NestedDir): boolean {
|
|
744
|
+
return beginsWith(segments, nested.dir.split('/'));
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function restBeneath(path: string, nested: NestedDir): string {
|
|
748
|
+
// By segments, since a name that matches can be another length, as `SS`
|
|
749
|
+
// is beside `ß`.
|
|
750
|
+
return path.split('/').slice(nested.dir.split('/').length).join('/');
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function surelyBeneath(segments: readonly string[], dir: readonly string[]): boolean {
|
|
754
|
+
return segments.length >= dir.length && dir.every((d, i) => segments[i] === d);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function shownByCodePoints(name: string): string {
|
|
758
|
+
return [...name].map((c) => {
|
|
759
|
+
const code = c.codePointAt(0)!;
|
|
760
|
+
if (c === '\\') return '\\\\';
|
|
761
|
+
return code < 0x80 ? c : code > 0xffff ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, '0')}`;
|
|
762
|
+
}).join('');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export type NestedGuess = { given: string; dir: string; leads?: string } | { placed: string; dir?: string; leads?: string } | null;
|
|
766
|
+
|
|
767
|
+
function guessFor(mount: string, given: readonly string[], dir: readonly string[]): Extract<NestedGuess, { given: string }> | undefined {
|
|
768
|
+
if (surelyBeneath(given, dir)) return undefined;
|
|
769
|
+
const levels = given.slice(0, dir.length);
|
|
770
|
+
const plain = (a: string, b: string) => a === b || !/[^\x00-\x7f]/.test(a + b);
|
|
771
|
+
const shown = (name: string, other: string) => (plain(name, other) ? name : shownByCodePoints(name));
|
|
772
|
+
return {
|
|
773
|
+
given: [mount, ...levels.map((g, i) => shown(g, dir[i]!))].join('/'),
|
|
774
|
+
dir: [mount, ...dir.map((d, i) => shown(d, levels[i]!))].join('/'),
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export function nestedGuess(mount: string, segments: readonly string[], nested: NestedDir): NestedGuess | undefined {
|
|
779
|
+
const dir = nested.dir.split('/');
|
|
780
|
+
const guess = guessFor(mount, segments, dir);
|
|
781
|
+
return nested.guessed ? placedGuess(mount, segments.slice(0, dir.length), guess) : guess;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function placedGuess(mount: string, levels: readonly string[], guess: ReturnType<typeof guessFor>): Extract<NestedGuess, { placed: string }> {
|
|
785
|
+
return guess === undefined ? { placed: [mount, ...levels].join('/') } : { placed: guess.given, dir: guess.dir };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export function nestedStep(holder: { name: string; use: string; guess?: NestedGuess }, opts: { after?: string; named?: boolean } = {}): string {
|
|
789
|
+
const { guess } = holder;
|
|
790
|
+
const after = opts.after ?? '';
|
|
791
|
+
if (guess === undefined) return `use "${holder.use}"${after}`;
|
|
792
|
+
if (guess === null) {
|
|
793
|
+
return `this workspace can't tell whether where it leads is that mount's directory. ` +
|
|
794
|
+
`If you meant that mount's directory, use "${holder.use}"${after}; if not, ask whoever runs this workspace`;
|
|
795
|
+
}
|
|
796
|
+
const leads = guess.leads === undefined ? '' : `it leads to "${guess.leads}", and `;
|
|
797
|
+
if ('placed' in guess) {
|
|
798
|
+
const check = 'ask whoever runs this workspace to check where the two mounts are configured';
|
|
799
|
+
const outside = guess.dir === undefined ? `"${guess.placed}"` : `"${guess.placed}" and "${guess.dir}"`;
|
|
800
|
+
return `${leads}this workspace can't tell whether "${guess.placed}" is mount "${holder.name}"'s directory, since the two mounts' roots are configured in ` +
|
|
801
|
+
`spellings it can't tell apart. If you meant that mount's directory, use "${holder.use}"${after}. ` +
|
|
802
|
+
(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}`);
|
|
803
|
+
}
|
|
804
|
+
const remedy = `rename one of "${guess.given}" and "${guess.dir}"`;
|
|
805
|
+
const own = opts.named
|
|
806
|
+
? `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}`
|
|
807
|
+
: `If not, ask whoever runs this workspace to ${remedy}`;
|
|
808
|
+
return `${leads}this workspace can't tell "${guess.given}" from "${guess.dir}", ` +
|
|
809
|
+
`mount "${holder.name}"'s directory. If you meant that mount's directory, use "${holder.use}"${after}. ${own}`;
|
|
631
810
|
}
|
|
632
811
|
|
|
633
812
|
/**
|
|
@@ -825,8 +1004,8 @@ export type DirCheck =
|
|
|
825
1004
|
* holds the non-directory in its place.
|
|
826
1005
|
*/
|
|
827
1006
|
| { kind: 'absent'; by: string }
|
|
828
|
-
/** Unproven either way; `outside` when it resolves outside the mount. */
|
|
829
|
-
| { kind: 'unobserved';
|
|
1007
|
+
/** Unproven either way; `outside` when it resolves outside the mount, `errno` when a system call failed. */
|
|
1008
|
+
| ({ kind: 'unobserved'; outside?: true } & NotShown);
|
|
830
1009
|
|
|
831
1010
|
/** Errors meaning a directory can't be synced on this filesystem, not that syncing it failed. */
|
|
832
1011
|
const DIRECTORY_SYNC_UNSUPPORTED = new Set(['EINVAL', 'ENOTSUP']);
|
|
@@ -846,11 +1025,16 @@ export class CheckedDir {
|
|
|
846
1025
|
private readonly lexical: string,
|
|
847
1026
|
private readonly dev: bigint,
|
|
848
1027
|
private readonly ino: bigint,
|
|
1028
|
+
private readonly born: bigint,
|
|
849
1029
|
) {}
|
|
850
1030
|
|
|
851
1031
|
/** Which directory it is. */
|
|
852
1032
|
identity(): RootIdentity {
|
|
853
|
-
return {
|
|
1033
|
+
return {
|
|
1034
|
+
dev: String(this.dev),
|
|
1035
|
+
ino: String(this.ino),
|
|
1036
|
+
...(this.born > 0n ? { born: String(this.born) } : {}),
|
|
1037
|
+
};
|
|
854
1038
|
}
|
|
855
1039
|
|
|
856
1040
|
/** A path that reaches `name` in this directory. */
|
|
@@ -892,11 +1076,25 @@ export class CheckedDir {
|
|
|
892
1076
|
}
|
|
893
1077
|
}
|
|
894
1078
|
|
|
1079
|
+
/**
|
|
1080
|
+
* What is at a mount root's path now, followed as the root is: for a
|
|
1081
|
+
* directory, undefined; for anything else, ENOTDIR; and where the path
|
|
1082
|
+
* can't be followed to anything, the errno its stat failed with, ENOENT
|
|
1083
|
+
* when nothing is there.
|
|
1084
|
+
*/
|
|
1085
|
+
export async function rootAbsence(root: string): Promise<string | undefined> {
|
|
1086
|
+
try {
|
|
1087
|
+
return (await stat(root)).isDirectory() ? undefined : 'ENOTDIR';
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
return errno(err);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
895
1093
|
/** Check the directory at a canonical location, if one is there. */
|
|
896
1094
|
async function checkedAt(view: MountView, rel: string, real: string): Promise<CheckedDir | null> {
|
|
897
1095
|
const info = await lstat(real, { bigint: true }).catch(() => null);
|
|
898
1096
|
if (!info?.isDirectory()) return null;
|
|
899
|
-
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino);
|
|
1097
|
+
return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs);
|
|
900
1098
|
}
|
|
901
1099
|
|
|
902
1100
|
/**
|
|
@@ -910,17 +1108,17 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
910
1108
|
real = await realpath(lexical);
|
|
911
1109
|
} catch (err) {
|
|
912
1110
|
const code = errno(err);
|
|
913
|
-
if (code === 'ENOENT' || code === 'ENOTDIR') return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE } : { kind: 'missing' };
|
|
1111
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE, errno: code } : { kind: 'missing' };
|
|
914
1112
|
// A link on the way that can't be resolved, such as a loop, is still a
|
|
915
1113
|
// link this mount doesn't follow.
|
|
916
1114
|
const found = rel === '' ? null : await linkOnTheWay(view, rel);
|
|
917
|
-
if (found !== null) return { kind: 'unobserved',
|
|
918
|
-
return { kind: 'unobserved',
|
|
1115
|
+
if (found !== null) return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1116
|
+
return { kind: 'unobserved', ...failedOn('cannot resolve the directory', err) };
|
|
919
1117
|
}
|
|
920
1118
|
// A mount that doesn't follow symlinks goes through none on the way to a
|
|
921
1119
|
// directory, inside the mount or out of it.
|
|
922
1120
|
const found = await linkRefusal(view, rootReal, rel, real);
|
|
923
|
-
if (found !== null) return { kind: 'unobserved',
|
|
1121
|
+
if (found !== null) return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
924
1122
|
if (rel === '') {
|
|
925
1123
|
// The root is whatever the configured path names, and it must still be
|
|
926
1124
|
// the root the pass was given.
|
|
@@ -932,10 +1130,10 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
932
1130
|
// mount's, which the mount neither reads nor writes. A place there that
|
|
933
1131
|
// mount's include leaves out is no mount's, and holds nothing beneath it,
|
|
934
1132
|
// as at any file (namesNestedPlace).
|
|
935
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1133
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
936
1134
|
if (holder) {
|
|
937
|
-
const directory = holder.
|
|
938
|
-
return
|
|
1135
|
+
const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
|
|
1136
|
+
return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder), ownStep: true } : { kind: 'missing' };
|
|
939
1137
|
}
|
|
940
1138
|
}
|
|
941
1139
|
const checked = await checkedAt(view, rel, real);
|
|
@@ -955,16 +1153,19 @@ async function locateDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
955
1153
|
* The mount root as it is now: its identity if it is available (a directory,
|
|
956
1154
|
* the one the pass was given, and the one disk last agreed under), else why not.
|
|
957
1155
|
*/
|
|
958
|
-
export async function locateRoot(view: MountView, rootReal: string): Promise<{ identity: RootIdentity } | { reason: string }> {
|
|
1156
|
+
export async function locateRoot(view: MountView, rootReal: string): Promise<{ identity: RootIdentity } | { reason: string; errno?: string }> {
|
|
959
1157
|
const found = await checkRoot(view, rootReal);
|
|
960
1158
|
return 'dir' in found ? { identity: found.dir.identity() } : found;
|
|
961
1159
|
}
|
|
962
1160
|
|
|
963
1161
|
/** The mount root as locateRoot finds it, checked, so a caller can ask later whether it's still there. */
|
|
964
|
-
export async function checkRoot(view: MountView, rootReal: string): Promise<{ dir: CheckedDir } | { reason: string }> {
|
|
1162
|
+
export async function checkRoot(view: MountView, rootReal: string): Promise<{ dir: CheckedDir } | { reason: string; errno?: string }> {
|
|
965
1163
|
const found = await locateDir(view, rootReal, '');
|
|
966
1164
|
if (found.kind === 'checked') return { dir: found.dir };
|
|
967
|
-
|
|
1165
|
+
const reason = found.kind === 'unobserved' ? found.reason : ROOT_UNAVAILABLE;
|
|
1166
|
+
// What is at the root's path now says whether the directory is gone from it.
|
|
1167
|
+
const code = found.kind === 'unobserved' && found.errno !== undefined ? found.errno : reason === ROOT_UNAVAILABLE ? await rootAbsence(view.root) : undefined;
|
|
1168
|
+
return { reason, ...(code !== undefined ? { errno: code } : {}) };
|
|
968
1169
|
}
|
|
969
1170
|
|
|
970
1171
|
/**
|
|
@@ -979,7 +1180,7 @@ export async function checkDir(view: MountView, rootReal: string, rel: string, o
|
|
|
979
1180
|
if (found.kind !== 'missing') return found;
|
|
980
1181
|
if (opts.create) return createDir(view, rootReal, rel);
|
|
981
1182
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
982
|
-
return
|
|
1183
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
|
|
983
1184
|
}
|
|
984
1185
|
|
|
985
1186
|
async function createDir(view: MountView, rootReal: string, rel: string): Promise<DirCheck> {
|
|
@@ -999,13 +1200,13 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
999
1200
|
const childRel = parts.slice(0, i + 1).join('/');
|
|
1000
1201
|
// Nothing is made in a nested mount's directory, its root included where
|
|
1001
1202
|
// it isn't made yet, by a path that reaches it through a link.
|
|
1002
|
-
const holder = nestedAtReal(base.at(parts[i]!), view.nested);
|
|
1003
|
-
if (holder) return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
|
|
1203
|
+
const holder = await nestedAtReal(base.at(parts[i]!), view.nested, { name: view.name, rootReal });
|
|
1204
|
+
if (holder) return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder), ownStep: true };
|
|
1004
1205
|
try {
|
|
1005
1206
|
await mkdir(base.at(parts[i]!));
|
|
1006
1207
|
} catch (err) {
|
|
1007
1208
|
const code = errno(err);
|
|
1008
|
-
if (code !== 'EEXIST') return { kind: 'unobserved',
|
|
1209
|
+
if (code !== 'EEXIST') return { kind: 'unobserved', ...failedOn(`cannot create the directory ${childRel}`, err) };
|
|
1009
1210
|
}
|
|
1010
1211
|
// Made, or there already (made meanwhile, or not a directory): it must
|
|
1011
1212
|
// be a real directory now, never a symlink or file in its place.
|
|
@@ -1014,8 +1215,8 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1014
1215
|
// A symlink in the way, such as one that leads nowhere, on a mount that
|
|
1015
1216
|
// doesn't follow symlinks, is named as one.
|
|
1016
1217
|
const there = view.followSymlinks ? null : await lstat(base.at(parts[i]!)).catch(() => null);
|
|
1017
|
-
if (there?.isSymbolicLink()) return { kind: 'unobserved',
|
|
1018
|
-
return { kind: 'unobserved', reason:
|
|
1218
|
+
if (there?.isSymbolicLink()) return { kind: 'unobserved', ...(await linkReason(view, rootReal, { link: childRel })) };
|
|
1219
|
+
return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
|
|
1019
1220
|
}
|
|
1020
1221
|
base = checked;
|
|
1021
1222
|
}
|
|
@@ -1037,14 +1238,14 @@ async function createDir(view: MountView, rootReal: string, rel: string): Promis
|
|
|
1037
1238
|
* Returns the directory whose entries prove it, or why absence couldn't be
|
|
1038
1239
|
* shown.
|
|
1039
1240
|
*/
|
|
1040
|
-
export async function confirmAbsent(view: MountView, rootReal: string, rel: string, target: 'entry' | 'beneath' = 'entry'): Promise<{ by: string } |
|
|
1241
|
+
export async function confirmAbsent(view: MountView, rootReal: string, rel: string, target: 'entry' | 'beneath' = 'entry'): Promise<{ by: string } | NotShown> {
|
|
1041
1242
|
const root = await locateDir(view, rootReal, '');
|
|
1042
|
-
if (root.kind !== 'checked') return ROOT_UNAVAILABLE;
|
|
1243
|
+
if (root.kind !== 'checked') return { reason: ROOT_UNAVAILABLE };
|
|
1043
1244
|
let cur: CheckedDir = root.dir;
|
|
1044
1245
|
/** Every directory the walk went through, the root first. */
|
|
1045
1246
|
const walked: CheckedDir[] = [cur];
|
|
1046
|
-
const proven = async (): Promise<{ by: string } |
|
|
1047
|
-
for (const dir of walked) if (!(await dir.stillHere())) return CHANGED;
|
|
1247
|
+
const proven = async (): Promise<{ by: string } | NotShown> => {
|
|
1248
|
+
for (const dir of walked) if (!(await dir.stillHere())) return { reason: CHANGED };
|
|
1048
1249
|
return { by: cur.rel };
|
|
1049
1250
|
};
|
|
1050
1251
|
const parts = rel.split('/');
|
|
@@ -1057,41 +1258,44 @@ export async function confirmAbsent(view: MountView, rootReal: string, rel: stri
|
|
|
1057
1258
|
info = await lstat(cur.at(name), { bigint: true });
|
|
1058
1259
|
} catch (err) {
|
|
1059
1260
|
const code = errno(err);
|
|
1060
|
-
if (code === 'ENOTDIR') return CHANGED;
|
|
1061
|
-
if (code !== 'ENOENT') return
|
|
1261
|
+
if (code === 'ENOTDIR') return { reason: CHANGED };
|
|
1262
|
+
if (code !== 'ENOENT') return failedOn('cannot stat', err);
|
|
1062
1263
|
// Absent from a directory that, like each above it, is still the one its path names.
|
|
1063
1264
|
return proven();
|
|
1064
1265
|
}
|
|
1065
1266
|
if (info.isDirectory()) {
|
|
1066
|
-
if (last) return CHANGED; // present after all
|
|
1067
|
-
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino);
|
|
1267
|
+
if (last) return { reason: CHANGED }; // present after all
|
|
1268
|
+
cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
|
|
1068
1269
|
walked.push(cur);
|
|
1069
1270
|
continue;
|
|
1070
1271
|
}
|
|
1071
1272
|
if (info.isSymbolicLink()) {
|
|
1072
|
-
if (last && target === 'entry') return CHANGED; // present: the link itself
|
|
1273
|
+
if (last && target === 'entry') return { reason: CHANGED }; // present: the link itself
|
|
1073
1274
|
// A mount that doesn't follow symlinks goes through none on the way.
|
|
1074
1275
|
if (!view.followSymlinks) return linkReason(view, rootReal, { link: childRel });
|
|
1075
1276
|
const located = await locateDir(view, rootReal, childRel);
|
|
1076
|
-
if (located.kind
|
|
1077
|
-
|
|
1277
|
+
if (located.kind === 'unobserved' && located.outside !== true) {
|
|
1278
|
+
return { reason: located.reason, ...(located.errno !== undefined ? { errno: located.errno } : {}), ...(located.ownStep === true ? { ownStep: true } : {}) };
|
|
1279
|
+
}
|
|
1280
|
+
if (located.kind !== 'checked') return { reason: BENEATH_SYMLINK };
|
|
1281
|
+
if (last) return { reason: CHANGED }; // a directory there after all
|
|
1078
1282
|
cur = located.dir;
|
|
1079
1283
|
walked.push(cur);
|
|
1080
1284
|
continue;
|
|
1081
1285
|
}
|
|
1082
|
-
if (last && target === 'entry') return CHANGED; // present after all
|
|
1286
|
+
if (last && target === 'entry') return { reason: CHANGED }; // present after all
|
|
1083
1287
|
// A non-directory: nothing can exist beneath it, while it is still this
|
|
1084
1288
|
// same one, in the directory still at its path.
|
|
1085
1289
|
const again = await lstat(cur.at(name), { bigint: true }).catch(() => null);
|
|
1086
|
-
if (again === null || again.isDirectory() || again.isSymbolicLink() || again.dev !== info.dev || again.ino !== info.ino) return CHANGED;
|
|
1290
|
+
if (again === null || again.isDirectory() || again.isSymbolicLink() || again.dev !== info.dev || again.ino !== info.ino) return { reason: CHANGED };
|
|
1087
1291
|
return proven();
|
|
1088
1292
|
}
|
|
1089
|
-
return CHANGED;
|
|
1293
|
+
return { reason: CHANGED };
|
|
1090
1294
|
}
|
|
1091
1295
|
|
|
1092
1296
|
async function absence(view: MountView, rootReal: string, rel: string): Promise<Exclude<Observation, { kind: 'file' }>> {
|
|
1093
1297
|
const proof = await confirmAbsent(view, rootReal, rel);
|
|
1094
|
-
return
|
|
1298
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent' };
|
|
1095
1299
|
}
|
|
1096
1300
|
|
|
1097
1301
|
// ===========================================================================
|
|
@@ -1167,26 +1371,26 @@ export async function filesystemTrustsCtime(root: string): Promise<boolean> {
|
|
|
1167
1371
|
* final component — which on a mount that doesn't follow symlinks is any
|
|
1168
1372
|
* symlink on the way.
|
|
1169
1373
|
*/
|
|
1170
|
-
async function parentPlacement(view: MountView, rootReal: string, relativePath: string): Promise<{ inside: string } | 'gone' |
|
|
1374
|
+
async function parentPlacement(view: MountView, rootReal: string, relativePath: string): Promise<{ inside: string } | 'gone' | NotShown> {
|
|
1171
1375
|
try {
|
|
1172
1376
|
const parent = await realpath(dirname(join(view.root, relativePath)));
|
|
1173
1377
|
const found = await linkRefusal(view, rootReal, relativePath, parent, 'parents');
|
|
1174
|
-
if (found !== null) return
|
|
1378
|
+
if (found !== null) return linkReason(view, rootReal, found);
|
|
1175
1379
|
if (!contained(rootReal, parent)) return { reason: OUTSIDE_PARENT };
|
|
1176
|
-
const holder = nestedAtReal(parent, view.nested);
|
|
1380
|
+
const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
|
|
1177
1381
|
if (!holder) return { inside: parent };
|
|
1178
1382
|
// A place there that the nested mount's include leaves out is no mount's,
|
|
1179
1383
|
// and holds nothing beneath it, as beneath any file (namesNestedPlace).
|
|
1180
|
-
const directory = holder.
|
|
1181
|
-
return
|
|
1384
|
+
const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
|
|
1385
|
+
return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder), ownStep: true } : 'gone';
|
|
1182
1386
|
} catch (err) {
|
|
1183
1387
|
const code = errno(err);
|
|
1184
1388
|
if (code === 'ENOENT' || code === 'ENOTDIR') return 'gone';
|
|
1185
1389
|
// A link on the way that can't be resolved, such as a loop, is still a
|
|
1186
1390
|
// link this mount doesn't follow.
|
|
1187
1391
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1188
|
-
if (found !== null) return
|
|
1189
|
-
return
|
|
1392
|
+
if (found !== null) return linkReason(view, rootReal, found);
|
|
1393
|
+
return failedOn('cannot resolve the parent directory', err);
|
|
1190
1394
|
}
|
|
1191
1395
|
}
|
|
1192
1396
|
|
|
@@ -1200,14 +1404,14 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1200
1404
|
const lexical = join(view.root, relativePath);
|
|
1201
1405
|
const placement = await parentPlacement(view, rootReal, relativePath);
|
|
1202
1406
|
if (placement === 'gone') return absence(view, rootReal, relativePath);
|
|
1203
|
-
if ('reason' in placement) return { kind: 'unobserved',
|
|
1407
|
+
if ('reason' in placement) return { kind: 'unobserved', ...placement };
|
|
1204
1408
|
let info;
|
|
1205
1409
|
try {
|
|
1206
1410
|
info = await lstat(lexical, { bigint: true });
|
|
1207
1411
|
} catch (err) {
|
|
1208
1412
|
const code = errno(err);
|
|
1209
1413
|
if (code === 'ENOENT' || code === 'ENOTDIR') return absence(view, rootReal, relativePath);
|
|
1210
|
-
return { kind: 'unobserved',
|
|
1414
|
+
return { kind: 'unobserved', ...failedOn('cannot stat', err) };
|
|
1211
1415
|
}
|
|
1212
1416
|
if (info.isSymbolicLink()) {
|
|
1213
1417
|
if (!view.followSymlinks) return { kind: 'unobserved', reason: NOT_FOLLOWED };
|
|
@@ -1215,37 +1419,37 @@ export async function observePath(view: MountView, rootReal: string, relativePat
|
|
|
1215
1419
|
try {
|
|
1216
1420
|
target = await stat(lexical, { bigint: true });
|
|
1217
1421
|
} catch (err) {
|
|
1218
|
-
return { kind: 'unobserved',
|
|
1422
|
+
return { kind: 'unobserved', ...failedOn('a symlink whose target cannot be read', err) };
|
|
1219
1423
|
}
|
|
1220
1424
|
let real: string;
|
|
1221
1425
|
try {
|
|
1222
1426
|
real = await realpath(lexical);
|
|
1223
1427
|
} catch (err) {
|
|
1224
|
-
return { kind: 'unobserved',
|
|
1428
|
+
return { kind: 'unobserved', ...failedOn('a symlink that cannot be resolved', err) };
|
|
1225
1429
|
}
|
|
1226
|
-
if (!contained(rootReal, real)) return { kind: 'unobserved', reason:
|
|
1227
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1430
|
+
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1431
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1228
1432
|
// A place a nested mount's include leaves out is no mount's, so the link
|
|
1229
1433
|
// leaves this one (namesNestedPlace).
|
|
1230
1434
|
if (holder) {
|
|
1231
|
-
return { kind: 'unobserved', reason:
|
|
1435
|
+
return holder.names(target.isDirectory()) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1232
1436
|
}
|
|
1233
1437
|
if (!target.isFile()) {
|
|
1234
|
-
return { kind: 'unobserved', reason: target.isDirectory() ?
|
|
1438
|
+
return { kind: 'unobserved', reason: target.isDirectory() ? LINK_TO_DIRECTORY : NOT_REGULAR };
|
|
1235
1439
|
}
|
|
1236
1440
|
// A file this mount's own include leaves out where the link lands is no
|
|
1237
1441
|
// file of this mount's (coversLanding). It's judged here, and not only
|
|
1238
1442
|
// where the file is opened, since a pass takes a copy whose fingerprint
|
|
1239
1443
|
// the stat vouches for without opening it.
|
|
1240
|
-
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason:
|
|
1444
|
+
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1241
1445
|
info = target;
|
|
1242
1446
|
} else if (info.isDirectory()) {
|
|
1243
|
-
return { kind: 'unobserved', reason:
|
|
1447
|
+
return { kind: 'unobserved', reason: DIRECTORY };
|
|
1244
1448
|
} else if (!info.isFile()) {
|
|
1245
|
-
return { kind: 'unobserved', reason:
|
|
1449
|
+
return { kind: 'unobserved', reason: NOT_REGULAR };
|
|
1246
1450
|
} else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
|
|
1247
1451
|
// So is one reached through a link among its directories.
|
|
1248
|
-
return { kind: 'unobserved', reason:
|
|
1452
|
+
return { kind: 'unobserved', reason: LEAVES_MOUNT };
|
|
1249
1453
|
}
|
|
1250
1454
|
return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 0o777n), fp: statFingerprint(info) };
|
|
1251
1455
|
}
|
|
@@ -1280,35 +1484,35 @@ async function openContained(
|
|
|
1280
1484
|
if (code === 'ELOOP' && !view.followSymlinks) {
|
|
1281
1485
|
// The link at the path, or a looping one on its way.
|
|
1282
1486
|
const found = await linkOnTheWay(view, relativePath, 'parents');
|
|
1283
|
-
return { kind: 'unobserved', reason:
|
|
1487
|
+
return found === null ? { kind: 'unobserved', reason: NOT_FOLLOWED } : { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1284
1488
|
}
|
|
1285
|
-
if (code === 'EISDIR') return { kind: 'unobserved', reason:
|
|
1286
|
-
return { kind: 'unobserved',
|
|
1489
|
+
if (code === 'EISDIR') return { kind: 'unobserved', reason: DIRECTORY };
|
|
1490
|
+
return { kind: 'unobserved', ...failedOn('cannot open', err) };
|
|
1287
1491
|
}
|
|
1288
1492
|
let keep = false;
|
|
1289
1493
|
try {
|
|
1290
1494
|
const info = await handle.stat({ bigint: true });
|
|
1291
1495
|
const real = await realpath(lexical).catch(() => null);
|
|
1292
|
-
if (real === null) return { kind: 'unobserved', reason:
|
|
1496
|
+
if (real === null) return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1293
1497
|
// O_NOFOLLOW guards only the final component: a mount that doesn't
|
|
1294
1498
|
// follow symlinks goes through none among the directories on the way
|
|
1295
1499
|
// either. Asked of what the path resolved to after the open, which the
|
|
1296
1500
|
// check below holds to the descriptor's file, and before what the
|
|
1297
1501
|
// descriptor holds, so a link gets its own answer whatever it leads to.
|
|
1298
1502
|
const found = await linkRefusal(view, rootReal, relativePath, real);
|
|
1299
|
-
if (found !== null) return { kind: 'unobserved',
|
|
1300
|
-
if (!info.isFile()) return { kind: 'unobserved', reason: info.isDirectory() ?
|
|
1301
|
-
const leaves = view.followSymlinks ?
|
|
1503
|
+
if (found !== null) return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
|
|
1504
|
+
if (!info.isFile()) return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
|
|
1505
|
+
const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
|
|
1302
1506
|
if (!contained(rootReal, real)) return { kind: 'unobserved', reason: leaves };
|
|
1303
|
-
const holder = nestedAtReal(real, view.nested);
|
|
1507
|
+
const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
|
|
1304
1508
|
// A place a nested mount's include leaves out is no mount's. Reached
|
|
1305
1509
|
// through a link this mount doesn't follow, the link is what's refused.
|
|
1306
|
-
if (holder) return { kind: 'unobserved', reason:
|
|
1510
|
+
if (holder) return holder.names(false) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1307
1511
|
// So is a file this mount's own include leaves out (coversLanding).
|
|
1308
1512
|
if (!(await coversLanding(view, rootReal, real, relativePath))) return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
|
|
1309
1513
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
1310
1514
|
if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
|
|
1311
|
-
return { kind: 'unobserved', reason:
|
|
1515
|
+
return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1312
1516
|
}
|
|
1313
1517
|
if (!view.followSymlinks) {
|
|
1314
1518
|
// Where no symlink is followed, the path itself must be the file the
|
|
@@ -1317,7 +1521,7 @@ async function openContained(
|
|
|
1317
1521
|
const entry = await lstat(lexical, { bigint: true }).catch(() => null);
|
|
1318
1522
|
if (entry?.isSymbolicLink()) return { kind: 'unobserved', reason: NOT_FOLLOWED };
|
|
1319
1523
|
if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) {
|
|
1320
|
-
return { kind: 'unobserved', reason:
|
|
1524
|
+
return { kind: 'unobserved', reason: FILE_CHANGED };
|
|
1321
1525
|
}
|
|
1322
1526
|
}
|
|
1323
1527
|
keep = true;
|
|
@@ -1325,7 +1529,7 @@ async function openContained(
|
|
|
1325
1529
|
} catch (err) {
|
|
1326
1530
|
const code = errno(err);
|
|
1327
1531
|
if (code === 'ENOENT') return absence(view, rootReal, relativePath);
|
|
1328
|
-
return { kind: 'unobserved',
|
|
1532
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1329
1533
|
} finally {
|
|
1330
1534
|
if (!keep) await handle.close();
|
|
1331
1535
|
}
|
|
@@ -1423,7 +1627,7 @@ export async function readPath(
|
|
|
1423
1627
|
} catch (err) {
|
|
1424
1628
|
const code = errno(err);
|
|
1425
1629
|
if (code === 'ENOENT') return absence(view, rootReal, relativePath);
|
|
1426
|
-
return { kind: 'unobserved',
|
|
1630
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1427
1631
|
} finally {
|
|
1428
1632
|
await handle.close();
|
|
1429
1633
|
}
|
|
@@ -1483,7 +1687,7 @@ export async function readBytes(
|
|
|
1483
1687
|
if (more) return { kind: 'bytes', size: Math.max(Number((await handle.stat({ bigint: true })).size), bytes.length + 1), bytes: null, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1484
1688
|
return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
|
|
1485
1689
|
} catch (err) {
|
|
1486
|
-
return { kind: 'unobserved',
|
|
1690
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1487
1691
|
} finally {
|
|
1488
1692
|
await handle.close();
|
|
1489
1693
|
}
|
|
@@ -1507,7 +1711,7 @@ export async function readHead(
|
|
|
1507
1711
|
const { bytesRead } = await handle.read(head, 0, HEAD_BYTES, 0);
|
|
1508
1712
|
return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
|
|
1509
1713
|
} catch (err) {
|
|
1510
|
-
return { kind: 'unobserved',
|
|
1714
|
+
return { kind: 'unobserved', ...failedOn('cannot read', err) };
|
|
1511
1715
|
} finally {
|
|
1512
1716
|
await handle.close();
|
|
1513
1717
|
}
|
|
@@ -1579,7 +1783,7 @@ export interface Walk {
|
|
|
1579
1783
|
* `symlink` region is never walked; the pass reports the link itself by
|
|
1580
1784
|
* path.
|
|
1581
1785
|
*/
|
|
1582
|
-
incomplete: Array<{ path: string; reason: string; kind: 'error' | 'cap' | 'ignored' | 'symlink' }>;
|
|
1786
|
+
incomplete: Array<{ path: string; reason: string; kind: 'error' | 'cap' | 'ignored' | 'symlink'; errno?: string }>;
|
|
1583
1787
|
/**
|
|
1584
1788
|
* When the walk cap stopped the walk, the last entry it took. A walk that
|
|
1585
1789
|
* resumes after it takes the entries after it first, in the walk's order
|
|
@@ -1636,14 +1840,14 @@ function subtreeAgainst(dir: string, at: string): 'before' | 'holds' | 'after' {
|
|
|
1636
1840
|
}
|
|
1637
1841
|
|
|
1638
1842
|
/**
|
|
1639
|
-
* Whether a mount's ignore list covers a mount-relative path: the path itself
|
|
1640
|
-
* or any directory above it, as the walk's own test reads
|
|
1641
|
-
* path and
|
|
1843
|
+
* Whether a mount's ignore list covers a mount-relative path: the path itself,
|
|
1844
|
+
* a directory or not, or any directory above it, as the walk's own test reads
|
|
1845
|
+
* each entry by its path and kind.
|
|
1642
1846
|
*/
|
|
1643
|
-
export function coveredByIgnore(relativePath: string, patterns: string[]): boolean {
|
|
1847
|
+
export function coveredByIgnore(relativePath: string, patterns: readonly string[], directory: boolean): boolean {
|
|
1644
1848
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1645
1849
|
for (let i = 0; i < segments.length; i++) {
|
|
1646
|
-
if (isIgnored(segments.slice(0, i + 1).join('/'),
|
|
1850
|
+
if (isIgnored(segments.slice(0, i + 1).join('/'), patterns, i < segments.length - 1 || directory)) return true;
|
|
1647
1851
|
}
|
|
1648
1852
|
return false;
|
|
1649
1853
|
}
|
|
@@ -1688,42 +1892,89 @@ export function insideMount(relativePath: string, include: readonly string[] | u
|
|
|
1688
1892
|
}
|
|
1689
1893
|
|
|
1690
1894
|
/**
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
* (pathSegments). A pattern the matcher can't read would match nothing, and
|
|
1697
|
-
* take every file out of the mount unsaid, so the module refuses it.
|
|
1895
|
+
* A pattern of `ignore` or `include`, read as gitignore reads it: its
|
|
1896
|
+
* segments, each a name or a glob with `*` and `?` within it; whether it's a
|
|
1897
|
+
* path from the mount's root (a leading or inner `/`, or a trailing `/**`)
|
|
1898
|
+
* rather than a name at any depth; and whether it matches only directories (a
|
|
1899
|
+
* trailing `/` or `/**`).
|
|
1698
1900
|
*/
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1901
|
+
type Pattern = { segments: Array<string | RegExp>; anchored: boolean; directory: boolean };
|
|
1902
|
+
|
|
1903
|
+
const readings = new Map<string, Pattern | string>();
|
|
1904
|
+
|
|
1905
|
+
function readPattern(pattern: string): Pattern | string {
|
|
1906
|
+
let reading = readings.get(pattern);
|
|
1907
|
+
if (reading === undefined) {
|
|
1908
|
+
reading = parsePattern(pattern);
|
|
1909
|
+
readings.set(pattern, reading);
|
|
1910
|
+
}
|
|
1911
|
+
return reading;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function parsePattern(pattern: string): Pattern | string {
|
|
1915
|
+
if (pattern.startsWith('!')) return 'a leading "!" makes an exception in a .gitignore, and this list has none, so leave it out';
|
|
1916
|
+
if (pattern.startsWith('#')) return 'a leading "#" marks a comment in a .gitignore, so leave comments out of the list';
|
|
1917
|
+
if (pattern.includes('\\')) 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';
|
|
1918
|
+
if (pattern.endsWith(' ')) return 'a .gitignore drops trailing spaces, so remove them';
|
|
1919
|
+
const anyDepth = pattern.startsWith('**/');
|
|
1920
|
+
let body = anyDepth ? pattern.slice(3) : pattern;
|
|
1921
|
+
const beneath = body.endsWith('/**');
|
|
1922
|
+
const directory = beneath || body.endsWith('/');
|
|
1923
|
+
body = body.slice(0, beneath ? -3 : directory ? -1 : undefined);
|
|
1924
|
+
const anchored = (beneath && !anyDepth) || body.includes('/');
|
|
1925
|
+
if (body.startsWith('/') && !anyDepth) body = body.slice(1);
|
|
1926
|
+
const segments = body.split('/');
|
|
1927
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..')) return 'it has an empty, "." or ".." segment, which names no entry';
|
|
1928
|
+
if (body.includes('**') || (anyDepth && anchored)) return '"**" is read only as a leading "**/" before one name, or a trailing "/**"';
|
|
1929
|
+
const glob = (s: string): string | RegExp => /[*?]/.test(s)
|
|
1930
|
+
? new RegExp(`^${s.replace(/[.+^${}()|[\]*?]/g, (c) => (c === '*' ? '.*' : c === '?' ? '.' : `\\${c}`))}$`, 'su')
|
|
1931
|
+
: s;
|
|
1932
|
+
return { segments: segments.map(glob), anchored, directory };
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function nameMatches(segment: string | RegExp, name: string): boolean {
|
|
1936
|
+
return typeof segment === 'string' ? segment === name : segment.test(name);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function matchesEntry(pattern: Pattern, segments: readonly string[], directory: boolean): boolean {
|
|
1940
|
+
if (pattern.directory && !directory) return false;
|
|
1941
|
+
if (!pattern.anchored) return nameMatches(pattern.segments[0]!, segments[segments.length - 1]!);
|
|
1942
|
+
return segments.length === pattern.segments.length && pattern.segments.every((s, i) => nameMatches(s, segments[i]!));
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
export function patternRefusal(pattern: unknown, list: 'ignore' | 'include'): string | undefined {
|
|
1946
|
+
if (typeof pattern !== 'string') return "it isn't a string";
|
|
1947
|
+
const reading = readPattern(pattern);
|
|
1948
|
+
if (typeof reading === 'string') return reading;
|
|
1949
|
+
if (list === 'ignore' && pattern.includes('[')) {
|
|
1950
|
+
return '"[" starts a character class in a .gitignore, which this list doesn\'t read, so write each name out, with "?" for a "[" in it';
|
|
1951
|
+
}
|
|
1952
|
+
return undefined;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
export function directoryAt(path: string, followSymlinks: boolean): Promise<boolean | undefined> {
|
|
1956
|
+
if (!followSymlinks) return lstat(path).then((s) => s.isDirectory(), () => undefined);
|
|
1957
|
+
return stat(path).then((s) => s.isDirectory(), () => lstat(path).then(() => false, () => undefined));
|
|
1705
1958
|
}
|
|
1706
1959
|
|
|
1707
1960
|
/**
|
|
1708
|
-
* Whether a mount's include covers a mount-relative path
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
* no covering: where one is a scope, refusesAsOutside (index.ts) decides.
|
|
1961
|
+
* Whether a mount's include covers a mount-relative path, read as a file: a
|
|
1962
|
+
* pattern that matches only directories covers what lies beneath a directory
|
|
1963
|
+
* it matches, and any other covers a file by its own name or path, and
|
|
1964
|
+
* nothing beneath a directory it matches. So `gate.json` covers `x/gate.json`
|
|
1965
|
+
* and not `x/gate.json/run.sh`, and a subtree is named with `dir/` or
|
|
1966
|
+
* `dir/**`. A directory is no file, so it needs no covering: where one is a
|
|
1967
|
+
* scope, refusesAsOutside (index.ts) decides.
|
|
1716
1968
|
*/
|
|
1717
1969
|
function coveredByInclude(relativePath: string, include: readonly string[]): boolean {
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
}
|
|
1726
|
-
return false;
|
|
1970
|
+
const segments = relativePath.split('/');
|
|
1971
|
+
return include.some((p) => {
|
|
1972
|
+
const pattern = readPattern(p);
|
|
1973
|
+
if (typeof pattern === 'string') return false;
|
|
1974
|
+
if (!pattern.directory) return matchesEntry(pattern, segments, false);
|
|
1975
|
+
for (let k = 1; k < segments.length; k++) if (matchesEntry(pattern, segments.slice(0, k), true)) return true;
|
|
1976
|
+
return false;
|
|
1977
|
+
});
|
|
1727
1978
|
}
|
|
1728
1979
|
|
|
1729
1980
|
/**
|
|
@@ -1732,7 +1983,7 @@ function coveredByInclude(relativePath: string, include: readonly string[]): boo
|
|
|
1732
1983
|
* it (#276); one covering only something inside it does.
|
|
1733
1984
|
*/
|
|
1734
1985
|
export function ignoreBeneath(scope: string, patterns: string[]): string[] {
|
|
1735
|
-
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern]));
|
|
1986
|
+
return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern], true));
|
|
1736
1987
|
}
|
|
1737
1988
|
|
|
1738
1989
|
/**
|
|
@@ -1753,7 +2004,7 @@ const EDITOR_FILE = /^\..+\.sw[px]$|~$|\.subl.*\.tmp$/;
|
|
|
1753
2004
|
/** The walk's reason for an entry the mount's ignore list covers. */
|
|
1754
2005
|
export const IGNORED = 'ignored by the mount';
|
|
1755
2006
|
/** The walk's reason for an entry `EDITOR_FILE` names. */
|
|
1756
|
-
const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
2007
|
+
export const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
1757
2008
|
|
|
1758
2009
|
/**
|
|
1759
2010
|
* The walk's reason for a link it doesn't enter. On a mount with an include,
|
|
@@ -1762,7 +2013,7 @@ const EDITOR_FILE_REASON = "an editor's swap or backup file";
|
|
|
1762
2013
|
* that follows links, one that leads to a directory, and on one that
|
|
1763
2014
|
* doesn't, every one.
|
|
1764
2015
|
*/
|
|
1765
|
-
const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
2016
|
+
export const LINK_NOT_WALKED = 'a symlink, not walked';
|
|
1766
2017
|
|
|
1767
2018
|
/** Whether a reason the walk gave an entry is one it passes over by design. */
|
|
1768
2019
|
export function passedOverByDesign(why: string | undefined): boolean {
|
|
@@ -1781,34 +2032,34 @@ export const OTHER_MOUNT = "another mount's directory";
|
|
|
1781
2032
|
* Every nested-mount check (a pass's, a walk's, a push's, grep's) asks this.
|
|
1782
2033
|
*/
|
|
1783
2034
|
export function inOtherMount(relativePath: string, others: readonly string[]): boolean {
|
|
1784
|
-
const
|
|
1785
|
-
return others.some((dir) =>
|
|
2035
|
+
const segments = posix.normalize(relativePath).replace(/\/+$/, '').split('/');
|
|
2036
|
+
return others.some((dir) => beneathNested(segments, { dir }));
|
|
1786
2037
|
}
|
|
1787
2038
|
|
|
1788
2039
|
/**
|
|
1789
|
-
* Why a walk passes over an entry by design, by its mount-relative path
|
|
1790
|
-
*
|
|
1791
|
-
* name. Undefined when none covers it.
|
|
2040
|
+
* Why a walk passes over an entry by design, by its mount-relative path, its
|
|
2041
|
+
* name and whether it's a directory: another mount's directory, the ignore
|
|
2042
|
+
* list, or an editor's file name. Undefined when none covers it.
|
|
1792
2043
|
*/
|
|
1793
|
-
function passedOverBecause(relativePath: string, name: string, patterns: string[], others: readonly string[]): string | undefined {
|
|
2044
|
+
function passedOverBecause(relativePath: string, name: string, patterns: string[], others: readonly string[], directory: boolean): string | undefined {
|
|
1794
2045
|
if (inOtherMount(relativePath, others)) return OTHER_MOUNT;
|
|
1795
|
-
if (isIgnored(relativePath,
|
|
2046
|
+
if (isIgnored(relativePath, patterns, directory)) return IGNORED;
|
|
1796
2047
|
if (EDITOR_FILE.test(name)) return EDITOR_FILE_REASON;
|
|
1797
2048
|
return undefined;
|
|
1798
2049
|
}
|
|
1799
2050
|
|
|
1800
2051
|
/**
|
|
1801
2052
|
* Why a sync without a path, and the watcher, pass over a mount-relative
|
|
1802
|
-
* path: the path itself or a directory above it is passed
|
|
1803
|
-
* the walk reads each entry by its path and
|
|
1804
|
-
* directory is that mount's, whatever else covers a
|
|
1805
|
-
* Undefined when nothing covers it.
|
|
2053
|
+
* path, a directory or not: the path itself or a directory above it is passed
|
|
2054
|
+
* over by design, as the walk reads each entry by its path, name and kind. A
|
|
2055
|
+
* path in another mount's directory is that mount's, whatever else covers a
|
|
2056
|
+
* directory above it. Undefined when nothing covers it.
|
|
1806
2057
|
*/
|
|
1807
|
-
export function passedOverAt(relativePath: string, patterns: string[], others: readonly string[]): string | undefined {
|
|
2058
|
+
export function passedOverAt(relativePath: string, patterns: string[], others: readonly string[], directory: boolean): string | undefined {
|
|
1808
2059
|
if (inOtherMount(relativePath, others)) return OTHER_MOUNT;
|
|
1809
2060
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1810
2061
|
for (let i = 0; i < segments.length; i++) {
|
|
1811
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others);
|
|
2062
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others, i < segments.length - 1 || directory);
|
|
1812
2063
|
if (why !== undefined) return why;
|
|
1813
2064
|
}
|
|
1814
2065
|
return undefined;
|
|
@@ -1824,24 +2075,19 @@ export function passedOverBetween(scope: string, relativePath: string, patterns:
|
|
|
1824
2075
|
const segments = relativePath.split('/').filter(Boolean);
|
|
1825
2076
|
const from = scope === '' ? 0 : scope.split('/').filter(Boolean).length;
|
|
1826
2077
|
for (let i = from; i < segments.length - 1; i++) {
|
|
1827
|
-
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others);
|
|
2078
|
+
const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i]!, patterns, others, true);
|
|
1828
2079
|
if (why !== undefined) return why;
|
|
1829
2080
|
}
|
|
1830
2081
|
return undefined;
|
|
1831
2082
|
}
|
|
1832
2083
|
|
|
1833
|
-
/**
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
}
|
|
1841
|
-
if (!pattern.includes('/') && relativePath === pattern) return true;
|
|
1842
|
-
if (pattern.startsWith('*.') && name.endsWith(pattern.slice(1))) return true;
|
|
1843
|
-
}
|
|
1844
|
-
return false;
|
|
2084
|
+
/** Whether an ignore pattern matches the entry at a mount-relative path, a directory or not, itself. */
|
|
2085
|
+
function isIgnored(relativePath: string, patterns: readonly string[], directory: boolean): boolean {
|
|
2086
|
+
const segments = relativePath.split('/');
|
|
2087
|
+
return patterns.some((p) => {
|
|
2088
|
+
const pattern = readPattern(p);
|
|
2089
|
+
return typeof pattern !== 'string' && matchesEntry(pattern, segments, directory);
|
|
2090
|
+
});
|
|
1845
2091
|
}
|
|
1846
2092
|
|
|
1847
2093
|
/**
|
|
@@ -1855,12 +2101,12 @@ async function checkChild(view: MountView, rootReal: string, parent: CheckedDir,
|
|
|
1855
2101
|
try {
|
|
1856
2102
|
info = await lstat(real, { bigint: true });
|
|
1857
2103
|
} catch (err) {
|
|
1858
|
-
if (errno(err) !== 'ENOENT') return { kind: 'unobserved',
|
|
2104
|
+
if (errno(err) !== 'ENOENT') return { kind: 'unobserved', ...failedOn('cannot stat', err) };
|
|
1859
2105
|
const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
1860
|
-
return
|
|
2106
|
+
return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
|
|
1861
2107
|
}
|
|
1862
2108
|
if (!info.isDirectory()) return { kind: 'unobserved', reason: CHANGED_WALK };
|
|
1863
|
-
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino) };
|
|
2109
|
+
return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs) };
|
|
1864
2110
|
}
|
|
1865
2111
|
|
|
1866
2112
|
/**
|
|
@@ -1893,12 +2139,11 @@ export async function walkScope(
|
|
|
1893
2139
|
};
|
|
1894
2140
|
// A walk that isn't named passes over a scope the mount passes over by
|
|
1895
2141
|
// design, at it or a directory above it, as its parent's listing would pass
|
|
1896
|
-
// over its entry
|
|
1897
|
-
//
|
|
1898
|
-
//
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
// sync that names it, whose scope is walked (#276), below.
|
|
2142
|
+
// over its entry. So a listing, glob or grep there never starts tracking
|
|
2143
|
+
// what the mount leaves out, and a stored copy beneath it is unobserved, as
|
|
2144
|
+
// beneath any region passed over by design. Its region says how to take
|
|
2145
|
+
// what's there in: a sync that names it, whose scope is walked (#276),
|
|
2146
|
+
// below.
|
|
1902
2147
|
// Another mount's directory is passed over by every walk, named or not,
|
|
1903
2148
|
// and no sync of this mount takes it in, so its region gives no advice.
|
|
1904
2149
|
if (scope !== '' && inOtherMount(scope, view.others)) {
|
|
@@ -1907,7 +2152,10 @@ export async function walkScope(
|
|
|
1907
2152
|
return walk;
|
|
1908
2153
|
}
|
|
1909
2154
|
if (!opts.named && scope !== '') {
|
|
1910
|
-
|
|
2155
|
+
let why = passedOverAt(scope, view.ignore, view.others, true);
|
|
2156
|
+
if (why !== undefined && passedOverAt(scope, view.ignore, view.others, false) === undefined) {
|
|
2157
|
+
if ((await directoryAt(join(rootReal, scope), view.followSymlinks)) === false) why = undefined;
|
|
2158
|
+
}
|
|
1911
2159
|
if (why !== undefined) {
|
|
1912
2160
|
walk.others.set(scope, why);
|
|
1913
2161
|
walk.incomplete.push({ path: scope, reason: `${why}: a sync that names this path takes in what's there, editors' files aside`, kind: 'ignored' });
|
|
@@ -1920,7 +2168,7 @@ export async function walkScope(
|
|
|
1920
2168
|
return walk;
|
|
1921
2169
|
}
|
|
1922
2170
|
if (top.kind === 'unobserved') {
|
|
1923
|
-
walk.incomplete.push({ path: scope, reason: top.outside ?
|
|
2171
|
+
walk.incomplete.push({ path: scope, reason: top.outside ? RESOLVES_OUTSIDE : top.reason, kind: 'error', ...(top.errno !== undefined ? { errno: top.errno } : {}) });
|
|
1924
2172
|
return walk;
|
|
1925
2173
|
}
|
|
1926
2174
|
// A scope a path sync names is walked even where the ignore list covers
|
|
@@ -1948,10 +2196,10 @@ export async function walkScope(
|
|
|
1948
2196
|
const visited = new Set<string>();
|
|
1949
2197
|
/** Paths already reported as errors, so a second visit doesn't report them again. */
|
|
1950
2198
|
const failed = new Set<string>();
|
|
1951
|
-
const fail = (path: string, reason: string): void => {
|
|
2199
|
+
const fail = (path: string, reason: string, code?: string): void => {
|
|
1952
2200
|
if (failed.has(path)) return;
|
|
1953
2201
|
failed.add(path);
|
|
1954
|
-
walk.incomplete.push({ path, reason, kind: 'error' });
|
|
2202
|
+
walk.incomplete.push({ path, reason, kind: 'error', ...(code !== undefined ? { errno: code } : {}) });
|
|
1955
2203
|
};
|
|
1956
2204
|
let left = cap;
|
|
1957
2205
|
let lastTaken: string | undefined;
|
|
@@ -2014,6 +2262,7 @@ export async function walkScope(
|
|
|
2014
2262
|
// Gone since it was checked: absent beneath only if that can be shown.
|
|
2015
2263
|
const proof = rel === '' ? ROOT_UNAVAILABLE : await confirmAbsent(view, rootReal, rel, 'beneath');
|
|
2016
2264
|
if (typeof proof === 'string') fail(rel, proof);
|
|
2265
|
+
else if ('reason' in proof) fail(rel, proof.reason, proof.errno);
|
|
2017
2266
|
else {
|
|
2018
2267
|
walk.missing.add(rel);
|
|
2019
2268
|
// Nothing was listed, so a later check that finds it again, on the
|
|
@@ -2022,7 +2271,8 @@ export async function walkScope(
|
|
|
2022
2271
|
}
|
|
2023
2272
|
return null;
|
|
2024
2273
|
}
|
|
2025
|
-
|
|
2274
|
+
const listed = failedOn('cannot list', err);
|
|
2275
|
+
fail(rel, listed.reason, listed.errno);
|
|
2026
2276
|
return null;
|
|
2027
2277
|
}
|
|
2028
2278
|
entries.sort((a, b) => compareNames(a.name, b.name));
|
|
@@ -2040,9 +2290,13 @@ export async function walkScope(
|
|
|
2040
2290
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
2041
2291
|
// Another mount's directory is passed over by its real path too, so a
|
|
2042
2292
|
// walk that reaches one through a link to a directory above it lists
|
|
2043
|
-
// none of that mount's names (
|
|
2044
|
-
|
|
2045
|
-
?? (entry.
|
|
2293
|
+
// none of that mount's names (nestedRootAtReal).
|
|
2294
|
+
let passedOver = passedOverBecause(childRel, entry.name, ignore, view.others, entry.isDirectory())
|
|
2295
|
+
?? (!entry.isSymbolicLink() && nestedRootAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
|
|
2296
|
+
if (passedOver === undefined && view.followSymlinks && entry.isSymbolicLink()) {
|
|
2297
|
+
const asDirectory = passedOverBecause(childRel, entry.name, ignore, view.others, true);
|
|
2298
|
+
if (asDirectory !== undefined && (await directoryAt(dir.at(entry.name), true)) === true) passedOver = asDirectory;
|
|
2299
|
+
}
|
|
2046
2300
|
if (passedOver !== undefined) {
|
|
2047
2301
|
others.push([childRel, passedOver]);
|
|
2048
2302
|
regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
|
|
@@ -2082,7 +2336,7 @@ export async function walkScope(
|
|
|
2082
2336
|
} else {
|
|
2083
2337
|
const symlink = entry.isSymbolicLink();
|
|
2084
2338
|
leaves.push([childRel, false]);
|
|
2085
|
-
others.push([childRel, symlink ? 'a symlink' :
|
|
2339
|
+
others.push([childRel, symlink ? 'a symlink' : NOT_REGULAR]);
|
|
2086
2340
|
if (symlink) regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
|
|
2087
2341
|
}
|
|
2088
2342
|
}
|
|
@@ -2113,10 +2367,10 @@ export async function walkScope(
|
|
|
2113
2367
|
};
|
|
2114
2368
|
|
|
2115
2369
|
// Each nested root by which directory it is, read once, when the walk
|
|
2116
|
-
// first descends: a directory the walk reaches
|
|
2117
|
-
//
|
|
2118
|
-
// a
|
|
2119
|
-
// mount's directory.
|
|
2370
|
+
// first descends: a directory the walk reaches by a path that no
|
|
2371
|
+
// comparison of names places in the nested mount's directory, as through a
|
|
2372
|
+
// bind mount, or on a disk whose folding goes past the comparison's, is
|
|
2373
|
+
// still another mount's directory.
|
|
2120
2374
|
let nestedRoots: Array<{ dev: string; ino: string }> | undefined;
|
|
2121
2375
|
const isNestedRoot = async (dir: CheckedDir): Promise<boolean> => {
|
|
2122
2376
|
if (!view.nested?.length) return false;
|
|
@@ -2176,7 +2430,7 @@ export async function walkScope(
|
|
|
2176
2430
|
// directory there now: what the walk read of it describes another.
|
|
2177
2431
|
dropBeneath(sub);
|
|
2178
2432
|
if (child.kind === 'absent') walk.missing.add(sub);
|
|
2179
|
-
else fail(sub, child.reason);
|
|
2433
|
+
else fail(sub, child.reason, child.errno);
|
|
2180
2434
|
}
|
|
2181
2435
|
};
|
|
2182
2436
|
|
|
@@ -2191,18 +2445,19 @@ export async function walkScope(
|
|
|
2191
2445
|
// directories a listing showed that weren't listed. A walk that doesn't
|
|
2192
2446
|
// descend leaves its subdirectories by design.
|
|
2193
2447
|
const capped = `the walk cap (${cap}) was reached`;
|
|
2448
|
+
const raise = "whoever runs this host can raise the mount's walkCap";
|
|
2194
2449
|
const holdingUntaken = new Set<string>();
|
|
2195
2450
|
for (const f of walk.seen) holdingUntaken.add(parentOf(f));
|
|
2196
2451
|
for (const [p, why] of walk.others) if (!passedOverByDesign(why) && !walk.othersTaken.has(p)) holdingUntaken.add(parentOf(p));
|
|
2197
2452
|
for (const d of holdingUntaken) {
|
|
2198
|
-
walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken`, kind: 'cap' });
|
|
2453
|
+
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' });
|
|
2199
2454
|
}
|
|
2200
2455
|
let unlisted = 0;
|
|
2201
2456
|
if (recursive) {
|
|
2202
2457
|
for (const d of walk.dirs) {
|
|
2203
2458
|
if (visited.has(d) || walk.missing.has(d) || failed.has(d)) continue;
|
|
2204
2459
|
unlisted++;
|
|
2205
|
-
walk.incomplete.push({ path: d, reason: `not visited: ${capped}`, kind: 'cap' });
|
|
2460
|
+
walk.incomplete.push({ path: d, reason: `not visited: ${capped}; naming this path walks only it, and ${raise}`, kind: 'cap' });
|
|
2206
2461
|
}
|
|
2207
2462
|
}
|
|
2208
2463
|
// The cap stopped the walk only if it left something: a walk whose last
|