@better-fs-tools/read 0.0.0-tegami-trusted-publish-setup → 0.2.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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +807 -6
  3. package/dist/authorize/ask-user.d.ts +7 -0
  4. package/dist/authorize/ask-user.js +25 -0
  5. package/dist/authorize/authorizers.d.ts +7 -0
  6. package/dist/authorize/authorizers.js +34 -0
  7. package/dist/authorize/deny-paths.d.ts +7 -0
  8. package/dist/authorize/deny-paths.js +25 -0
  9. package/dist/authorize/glob.d.ts +7 -0
  10. package/dist/authorize/glob.js +46 -0
  11. package/dist/authorize/index.d.ts +5 -0
  12. package/dist/authorize/index.js +5 -0
  13. package/dist/authorize/shared.d.ts +9 -0
  14. package/dist/authorize/shared.js +12 -0
  15. package/dist/authorize/size-ceiling.d.ts +10 -0
  16. package/dist/authorize/size-ceiling.js +34 -0
  17. package/dist/budget/chars-per-token.d.ts +9 -0
  18. package/dist/budget/chars-per-token.js +15 -0
  19. package/dist/budget/index.d.ts +2 -0
  20. package/dist/budget/index.js +1 -0
  21. package/dist/classifiers/binary.d.ts +5 -0
  22. package/dist/classifiers/binary.js +13 -0
  23. package/dist/classifiers/extension.d.ts +17 -0
  24. package/dist/classifiers/extension.js +45 -0
  25. package/dist/classifiers/image.d.ts +5 -0
  26. package/dist/classifiers/image.js +22 -0
  27. package/dist/classifiers/index.d.ts +16 -0
  28. package/dist/classifiers/index.js +31 -0
  29. package/dist/classifiers/notebook.d.ts +5 -0
  30. package/dist/classifiers/notebook.js +14 -0
  31. package/dist/classifiers/office.d.ts +5 -0
  32. package/dist/classifiers/office.js +30 -0
  33. package/dist/classifiers/pdf.d.ts +4 -0
  34. package/dist/classifiers/pdf.js +8 -0
  35. package/dist/classifiers/shared.d.ts +17 -0
  36. package/dist/classifiers/shared.js +57 -0
  37. package/dist/classifiers/utf8.d.ts +8 -0
  38. package/dist/classifiers/utf8.js +61 -0
  39. package/dist/contract/base.d.ts +126 -0
  40. package/dist/contract/base.js +0 -0
  41. package/dist/contract/classify.d.ts +50 -0
  42. package/dist/contract/classify.js +0 -0
  43. package/dist/contract/context.d.ts +13 -0
  44. package/dist/contract/context.js +0 -0
  45. package/dist/contract/deps.d.ts +60 -0
  46. package/dist/contract/deps.js +0 -0
  47. package/dist/contract/digest.d.ts +13 -0
  48. package/dist/contract/digest.js +0 -0
  49. package/dist/contract/extensions.d.ts +115 -0
  50. package/dist/contract/extensions.js +0 -0
  51. package/dist/contract/format.d.ts +16 -0
  52. package/dist/contract/format.js +0 -0
  53. package/dist/contract/input.d.ts +16 -0
  54. package/dist/contract/input.js +0 -0
  55. package/dist/contract/json.d.ts +6 -0
  56. package/dist/contract/json.js +0 -0
  57. package/dist/contract/limits.d.ts +20 -0
  58. package/dist/contract/limits.js +0 -0
  59. package/dist/contract/messages.d.ts +110 -0
  60. package/dist/contract/messages.js +0 -0
  61. package/dist/contract/result.d.ts +153 -0
  62. package/dist/contract/result.js +0 -0
  63. package/dist/contract/state.d.ts +36 -0
  64. package/dist/contract/state.js +0 -0
  65. package/dist/converters/directory-listing.d.ts +13 -0
  66. package/dist/converters/directory-listing.js +63 -0
  67. package/dist/converters/image.d.ts +7 -0
  68. package/dist/converters/image.js +25 -0
  69. package/dist/converters/index.d.ts +8 -0
  70. package/dist/converters/index.js +4 -0
  71. package/dist/converters/notebook.d.ts +12 -0
  72. package/dist/converters/notebook.js +106 -0
  73. package/dist/converters/shared.d.ts +5 -0
  74. package/dist/converters/shared.js +18 -0
  75. package/dist/converters/text.d.ts +12 -0
  76. package/dist/converters/text.js +24 -0
  77. package/dist/core/authorize.d.ts +8 -0
  78. package/dist/core/authorize.js +76 -0
  79. package/dist/core/budget.d.ts +7 -0
  80. package/dist/core/budget.js +23 -0
  81. package/dist/core/call-scope.d.ts +36 -0
  82. package/dist/core/call-scope.js +112 -0
  83. package/dist/core/classify.d.ts +9 -0
  84. package/dist/core/classify.js +27 -0
  85. package/dist/core/convert-source.d.ts +29 -0
  86. package/dist/core/convert-source.js +85 -0
  87. package/dist/core/convert.d.ts +23 -0
  88. package/dist/core/convert.js +132 -0
  89. package/dist/core/converted.d.ts +18 -0
  90. package/dist/core/converted.js +111 -0
  91. package/dist/core/create-read-tool.d.ts +7 -0
  92. package/dist/core/create-read-tool.js +18 -0
  93. package/dist/core/cursor.d.ts +14 -0
  94. package/dist/core/cursor.js +93 -0
  95. package/dist/core/deps.d.ts +6 -0
  96. package/dist/core/deps.js +121 -0
  97. package/dist/core/directory.d.ts +16 -0
  98. package/dist/core/directory.js +113 -0
  99. package/dist/core/extension-error.d.ts +9 -0
  100. package/dist/core/extension-error.js +18 -0
  101. package/dist/core/format.d.ts +8 -0
  102. package/dist/core/format.js +29 -0
  103. package/dist/core/hooks.d.ts +3 -0
  104. package/dist/core/hooks.js +140 -0
  105. package/dist/core/input.d.ts +10 -0
  106. package/dist/core/input.js +45 -0
  107. package/dist/core/limits.d.ts +9 -0
  108. package/dist/core/limits.js +43 -0
  109. package/dist/core/messages.d.ts +8 -0
  110. package/dist/core/messages.js +78 -0
  111. package/dist/core/notes.d.ts +14 -0
  112. package/dist/core/notes.js +94 -0
  113. package/dist/core/observation.d.ts +11 -0
  114. package/dist/core/observation.js +51 -0
  115. package/dist/core/open.d.ts +16 -0
  116. package/dist/core/open.js +33 -0
  117. package/dist/core/outcomes.d.ts +27 -0
  118. package/dist/core/outcomes.js +172 -0
  119. package/dist/core/pipeline.d.ts +4 -0
  120. package/dist/core/pipeline.js +175 -0
  121. package/dist/core/record.d.ts +3 -0
  122. package/dist/core/record.js +37 -0
  123. package/dist/core/resolve.d.ts +15 -0
  124. package/dist/core/resolve.js +43 -0
  125. package/dist/core/same.d.ts +2 -0
  126. package/dist/core/same.js +22 -0
  127. package/dist/core/sample.d.ts +6 -0
  128. package/dist/core/sample.js +24 -0
  129. package/dist/core/scan.d.ts +26 -0
  130. package/dist/core/scan.js +95 -0
  131. package/dist/core/scanner.d.ts +29 -0
  132. package/dist/core/scanner.js +124 -0
  133. package/dist/core/suggest.d.ts +6 -0
  134. package/dist/core/suggest.js +41 -0
  135. package/dist/core/text-outcome.d.ts +19 -0
  136. package/dist/core/text-outcome.js +72 -0
  137. package/dist/core/verify.d.ts +7 -0
  138. package/dist/core/verify.js +15 -0
  139. package/dist/digest/sha256.d.ts +7 -0
  140. package/dist/digest/sha256.js +131 -0
  141. package/dist/formats/deep-agents.d.ts +7 -0
  142. package/dist/formats/deep-agents.js +23 -0
  143. package/dist/formats/hashline.d.ts +6 -0
  144. package/dist/formats/hashline.js +16 -0
  145. package/dist/formats/hermes.d.ts +9 -0
  146. package/dist/formats/hermes.js +62 -0
  147. package/dist/formats/index.d.ts +4 -0
  148. package/dist/formats/index.js +4 -0
  149. package/dist/formats/opencode.d.ts +7 -0
  150. package/dist/formats/opencode.js +35 -0
  151. package/dist/formats/shared.d.ts +4 -0
  152. package/dist/formats/shared.js +9 -0
  153. package/dist/formatters/eof.d.ts +7 -0
  154. package/dist/formatters/eof.js +20 -0
  155. package/dist/formatters/file-hash.d.ts +7 -0
  156. package/dist/formatters/file-hash.js +13 -0
  157. package/dist/formatters/hashline.d.ts +11 -0
  158. package/dist/formatters/hashline.js +18 -0
  159. package/dist/formatters/index.d.ts +10 -0
  160. package/dist/formatters/index.js +7 -0
  161. package/dist/formatters/json.d.ts +17 -0
  162. package/dist/formatters/json.js +36 -0
  163. package/dist/formatters/layout.d.ts +15 -0
  164. package/dist/formatters/layout.js +33 -0
  165. package/dist/formatters/line-number.d.ts +11 -0
  166. package/dist/formatters/line-number.js +23 -0
  167. package/dist/formatters/plain.d.ts +7 -0
  168. package/dist/formatters/plain.js +14 -0
  169. package/dist/hooks/index.d.ts +4 -0
  170. package/dist/hooks/index.js +2 -0
  171. package/dist/hooks/redact.d.ts +14 -0
  172. package/dist/hooks/redact.js +53 -0
  173. package/dist/hooks/repeat-read-guard.d.ts +13 -0
  174. package/dist/hooks/repeat-read-guard.js +49 -0
  175. package/dist/index.d.ts +35 -0
  176. package/dist/index.js +15 -0
  177. package/dist/resolve/expand-home.d.ts +5 -0
  178. package/dist/resolve/expand-home.js +17 -0
  179. package/dist/resolve/index.d.ts +5 -0
  180. package/dist/resolve/index.js +5 -0
  181. package/dist/resolve/path-resolvers.d.ts +7 -0
  182. package/dist/resolve/path-resolvers.js +38 -0
  183. package/dist/resolve/reanchor-leading-slash.d.ts +9 -0
  184. package/dist/resolve/reanchor-leading-slash.js +23 -0
  185. package/dist/resolve/strip-prefixes.d.ts +6 -0
  186. package/dist/resolve/strip-prefixes.js +30 -0
  187. package/dist/resolve/unicode-repair.d.ts +9 -0
  188. package/dist/resolve/unicode-repair.js +38 -0
  189. package/dist/signature/contract.d.ts +18 -0
  190. package/dist/signature/contract.js +0 -0
  191. package/dist/signature/index.d.ts +4 -0
  192. package/dist/signature/index.js +3 -0
  193. package/dist/signature/line-range.d.ts +7 -0
  194. package/dist/signature/line-range.js +59 -0
  195. package/dist/signature/messages.d.ts +4 -0
  196. package/dist/signature/messages.js +9 -0
  197. package/dist/signature/offset-limit.d.ts +13 -0
  198. package/dist/signature/offset-limit.js +59 -0
  199. package/dist/signature/schema.d.ts +10 -0
  200. package/dist/signature/schema.js +80 -0
  201. package/dist/state/index.d.ts +15 -0
  202. package/dist/state/index.js +77 -0
  203. package/dist/suggest/canonicalize.d.ts +6 -0
  204. package/dist/suggest/canonicalize.js +12 -0
  205. package/dist/suggest/default-suggest.d.ts +3 -0
  206. package/dist/suggest/default-suggest.js +12 -0
  207. package/dist/suggest/index.d.ts +3 -0
  208. package/dist/suggest/index.js +3 -0
  209. package/dist/suggest/suggest-file-names.d.ts +2 -0
  210. package/dist/suggest/suggest-file-names.js +46 -0
  211. package/docs/architecture.md +250 -0
  212. package/docs/result-schema.md +222 -0
  213. package/package.json +51 -2
@@ -0,0 +1,175 @@
1
+ import { authorizeRead } from "./authorize.js";
2
+ import { CallScope } from "./call-scope.js";
3
+ import { classificationInfo, classifySample, encodingRefusal } from "./classify.js";
4
+ import { convertFile, selectFileConverter } from "./convert.js";
5
+ import { AbortReadError, ByteCursor } from "./cursor.js";
6
+ import { convertDirectory } from "./directory.js";
7
+ import { formatResult } from "./format.js";
8
+ import { runHooks } from "./hooks.js";
9
+ import { clampedLimit, parseReadInput } from "./input.js";
10
+ import { fileInfo, openFile } from "./open.js";
11
+ import { ReadStop, aborted, invalidInput, ioError, unsupportedBackend, unsupportedOutcome, } from "./outcomes.js";
12
+ import { recordOutcome } from "./record.js";
13
+ import { resolvePath } from "./resolve.js";
14
+ import { takeSample } from "./sample.js";
15
+ import { scanText } from "./scan.js";
16
+ import { missOutcome } from "./suggest.js";
17
+ import { textOutcome } from "./text-outcome.js";
18
+ import { checkSize, verifyHandle } from "./verify.js";
19
+ export async function runRead(deps, input, call) {
20
+ return formatResult(deps, call, await readOutcome(deps, input, call));
21
+ }
22
+ async function readOutcome(deps, input, call) {
23
+ let request;
24
+ try {
25
+ request = parseReadInput(input, deps.limits);
26
+ }
27
+ catch (error) {
28
+ return invalidInput(deps.messages, input, error);
29
+ }
30
+ const scope = new CallScope(deps, request, call);
31
+ const clampNote = clampedLimitNote(deps, input, request);
32
+ let resolveNote = null;
33
+ const finish = (outcome) => withNotes(outcome, [
34
+ ...(clampNote === null ? [] : [clampNote]),
35
+ ...scope.allowNotes,
36
+ ...(resolveNote === null ? [] : [resolveNote]),
37
+ ]);
38
+ let outcome;
39
+ try {
40
+ outcome = await readStages(deps, request, scope, (note) => {
41
+ resolveNote = note;
42
+ });
43
+ }
44
+ catch (error) {
45
+ outcome = outcomeFromError(deps, request, scope, error);
46
+ }
47
+ outcome = finish(outcome);
48
+ if (isAborted(outcome))
49
+ return outcome;
50
+ try {
51
+ const hooked = await runHooks(scope, outcome);
52
+ await recordOutcome(scope, hooked);
53
+ return hooked;
54
+ }
55
+ catch (error) {
56
+ return finish(outcomeFromError(deps, request, scope, error));
57
+ }
58
+ }
59
+ async function readStages(deps, request, scope, onResolveNote) {
60
+ scope.checkAbort();
61
+ scope.enter("open");
62
+ const fs = scope.fileSystem();
63
+ scope.enter("resolve");
64
+ const resolved = await resolvePath(deps.resolve, request, fs, scope);
65
+ onResolveNote(resolved.note);
66
+ if (resolved.kind === "not-found") {
67
+ return missOutcome(deps, request, fs, scope, request.path, null);
68
+ }
69
+ scope.enter("open");
70
+ const opened = await openFile(deps, fs, request, resolved.path, scope);
71
+ if (opened.kind === "directory") {
72
+ const { error, converter } = opened;
73
+ return convertDirectory({
74
+ fs,
75
+ request,
76
+ openedPath: resolved.path,
77
+ error,
78
+ resolvedFrom: resolved.resolvedFrom,
79
+ converter,
80
+ scope,
81
+ });
82
+ }
83
+ const { handle } = opened;
84
+ try {
85
+ const file = fileInfo(fs, request, handle.info, resolved.resolvedFrom);
86
+ await authorizeRead(deps.authorize, request, file, scope);
87
+ return await readOpenFile(deps, fs, request, file, handle, scope);
88
+ }
89
+ finally {
90
+ await handle.close().catch(() => { });
91
+ }
92
+ }
93
+ function outcomeFromError(deps, request, scope, error) {
94
+ if (error instanceof ReadStop)
95
+ return error.outcome;
96
+ if (error instanceof AbortReadError)
97
+ return aborted(deps.messages, request, scope.phase);
98
+ return ioError(deps.messages, request, scope.phase, error);
99
+ }
100
+ function clampedLimitNote(deps, input, request) {
101
+ const requested = clampedLimit(input, request);
102
+ if (requested === null)
103
+ return null;
104
+ const max = deps.limits.maxLines;
105
+ return {
106
+ code: "clamped",
107
+ severity: "info",
108
+ message: deps.messages.limitClamped({ requested, max }),
109
+ data: { param: "limit", requested, max },
110
+ };
111
+ }
112
+ function isAborted(outcome) {
113
+ return outcome.status === "error" && outcome.error.code === "ABORTED";
114
+ }
115
+ function withNotes(outcome, notes) {
116
+ return notes.length === 0 ? outcome : { ...outcome, notes: [...outcome.notes, ...notes] };
117
+ }
118
+ async function readOpenFile(deps, fs, request, file, handle, scope) {
119
+ const { limits, messages, classifiers } = deps;
120
+ const cursor = new ByteCursor(handle.bytes());
121
+ try {
122
+ scope.enter("sampling");
123
+ const sample = await takeSample(cursor, limits, file, scope);
124
+ if (sample.complete)
125
+ checkSize(messages, request, file, sample.bytes.byteLength);
126
+ const decision = classifySample(classifiers, sample);
127
+ if (decision === null)
128
+ return unsupportedBackend(messages, request, "sampling", file, null);
129
+ const converter = selectFileConverter(scope, decision, sample);
130
+ if (converter !== null) {
131
+ return await convertFile({
132
+ deps,
133
+ fs,
134
+ request,
135
+ file,
136
+ handle,
137
+ cursor,
138
+ sample,
139
+ decision,
140
+ converter,
141
+ scope,
142
+ });
143
+ }
144
+ if (decision.classification.kind === "unsupported") {
145
+ return unsupportedOutcome(request, file, decision.classifier, decision.classification);
146
+ }
147
+ scope.enter("scan");
148
+ const scan = await scanText({ cursor, sample, request, limits, digest: deps.digest, scope });
149
+ if (scan.kind === "invalid-utf8") {
150
+ const refusal = encodingRefusal(classifiers, sample);
151
+ if (refusal === null) {
152
+ return unsupportedBackend(messages, request, "scan", file, "invalid utf-8 with no encoding refusal");
153
+ }
154
+ return unsupportedOutcome(request, file, refusal.classifier, refusal.classification);
155
+ }
156
+ scope.enter("verification");
157
+ if (scan.reachedEof)
158
+ checkSize(messages, request, file, scan.scannedBytes);
159
+ await verifyHandle(handle, messages, request, file, scope);
160
+ return textOutcome({
161
+ deps,
162
+ fs,
163
+ request,
164
+ file,
165
+ classification: classificationInfo(decision),
166
+ conversion: null,
167
+ scan,
168
+ contentId: scan.contentId,
169
+ notes: decision.classification.notes ?? [],
170
+ });
171
+ }
172
+ finally {
173
+ await cursor.close();
174
+ }
175
+ }
@@ -0,0 +1,3 @@
1
+ import type { ReadReport } from "../contract/result.ts";
2
+ import type { CallScope } from "./call-scope.ts";
3
+ export declare function recordOutcome<THost>(scope: CallScope<THost>, outcome: ReadReport): Promise<void>;
@@ -0,0 +1,37 @@
1
+ export async function recordOutcome(scope, outcome) {
2
+ if (outcome.status !== "ok" && outcome.status !== "media")
3
+ return;
4
+ const { observation } = outcome;
5
+ const { digest } = scope.deps;
6
+ if (observation === null || digest === null)
7
+ return;
8
+ const store = scope.stateStore();
9
+ if (store === null)
10
+ return;
11
+ try {
12
+ await store.put(outcome.file.resolvedPath, toRecord(outcome, observation, digest.id));
13
+ }
14
+ catch {
15
+ // Session state is a cache: a failing store never fails the read.
16
+ }
17
+ }
18
+ /** Copies fields one by one so nothing from the host `call` reaches the store. */
19
+ function toRecord(outcome, observation, digest) {
20
+ const { file, request } = outcome;
21
+ return {
22
+ schema: 2,
23
+ origin: "read",
24
+ observationId: observation.id,
25
+ resolvedPath: file.resolvedPath,
26
+ identity: file.identity,
27
+ version: file.version,
28
+ digest,
29
+ contentId: observation.contentId,
30
+ viewId: observation.viewId,
31
+ observedAt: observation.observedAt,
32
+ wholeFileVisible: observation.wholeFileVisible,
33
+ // Media is only returned from a whole source, so its source totals are exact.
34
+ totalsExact: outcome.status === "ok" ? outcome.totals.exact : true,
35
+ request: { offset: request.offset, limit: request.limit },
36
+ };
37
+ }
@@ -0,0 +1,15 @@
1
+ import type { FileSystem } from "@better-fs-tools/fs";
2
+ import type { PathResolver } from "../contract/extensions.ts";
3
+ import type { ReadRequest } from "../contract/input.ts";
4
+ import type { ReadNote } from "../contract/result.ts";
5
+ import type { CallScope } from "./call-scope.ts";
6
+ export type ResolvedPath = {
7
+ readonly kind: "path";
8
+ readonly path: string;
9
+ readonly resolvedFrom: string | null;
10
+ readonly note: ReadNote | null;
11
+ } | {
12
+ readonly kind: "not-found";
13
+ readonly note: ReadNote | null;
14
+ };
15
+ export declare function resolvePath<THost>(resolver: PathResolver<THost> | null, request: ReadRequest, fs: FileSystem, scope: CallScope<THost>): Promise<ResolvedPath>;
@@ -0,0 +1,43 @@
1
+ import { extensionId } from "./extension-error.js";
2
+ import { isRecord } from "./input.js";
3
+ import { isNote } from "./outcomes.js";
4
+ export async function resolvePath(resolver, request, fs, scope) {
5
+ if (resolver === null)
6
+ return { kind: "path", path: request.path, resolvedFrom: null, note: null };
7
+ const ctx = {
8
+ ...scope.hookContext(),
9
+ paths: fs.paths,
10
+ list: (dir) => scope.list("resolver", dir),
11
+ };
12
+ let outcome;
13
+ try {
14
+ outcome = await resolver.resolve(request.path, ctx);
15
+ }
16
+ catch (error) {
17
+ scope.checkAbort();
18
+ scope.throwHeld();
19
+ throw scope.extensionFailure("resolve", extensionId(resolver, error));
20
+ }
21
+ scope.checkAbort();
22
+ scope.throwHeld();
23
+ const malformed = () => scope.extensionFailure("resolve", extensionId(resolver));
24
+ if (!isRecord(outcome))
25
+ throw malformed();
26
+ const { note } = outcome;
27
+ if (note !== undefined && !isNote(note))
28
+ throw malformed();
29
+ if (outcome.kind === "not-found")
30
+ return { kind: "not-found", note: note ?? null };
31
+ const { path } = outcome;
32
+ if (outcome.kind !== "path" || !isPath(path))
33
+ throw malformed();
34
+ return {
35
+ kind: "path",
36
+ path,
37
+ resolvedFrom: path === request.path ? null : request.path,
38
+ note: note ?? null,
39
+ };
40
+ }
41
+ function isPath(value) {
42
+ return typeof value === "string" && value.trim() !== "" && !value.includes("\0");
43
+ }
@@ -0,0 +1,2 @@
1
+ /** Structural equality for plain objects, arrays, byte arrays, and primitives. */
2
+ export declare function same(a: unknown, b: unknown): boolean;
@@ -0,0 +1,22 @@
1
+ /** Structural equality for plain objects, arrays, byte arrays, and primitives. */
2
+ export function same(a, b) {
3
+ if (Object.is(a, b))
4
+ return true;
5
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object")
6
+ return false;
7
+ if (a instanceof Uint8Array || b instanceof Uint8Array) {
8
+ if (!(a instanceof Uint8Array && b instanceof Uint8Array))
9
+ return false;
10
+ return a.byteLength === b.byteLength && a.every((byte, index) => byte === b[index]);
11
+ }
12
+ if (Array.isArray(a) || Array.isArray(b)) {
13
+ if (!(Array.isArray(a) && Array.isArray(b)) || a.length !== b.length)
14
+ return false;
15
+ return a.every((entry, index) => same(entry, b[index]));
16
+ }
17
+ const keys = Object.keys(a);
18
+ if (keys.length !== Object.keys(b).length)
19
+ return false;
20
+ return keys.every((key) => Object.hasOwn(b, key) &&
21
+ same(a[key], b[key]));
22
+ }
@@ -0,0 +1,6 @@
1
+ import type { ClassificationSample } from "../contract/classify.ts";
2
+ import type { ReadLimits } from "../contract/limits.ts";
3
+ import type { FileInfo } from "../contract/result.ts";
4
+ import type { CallScope } from "./call-scope.ts";
5
+ import type { ByteCursor } from "./cursor.ts";
6
+ export declare function takeSample<THost>(cursor: ByteCursor, limits: Readonly<ReadLimits>, file: FileInfo, scope: CallScope<THost>): Promise<ClassificationSample>;
@@ -0,0 +1,24 @@
1
+ export async function takeSample(cursor, limits, file, scope) {
2
+ const buffer = new Uint8Array(limits.sampleBytes);
3
+ let length = 0;
4
+ let reachedEof = false;
5
+ while (length < limits.sampleBytes) {
6
+ scope.checkAbort();
7
+ const item = await cursor.next(scope.signal);
8
+ if (item.done) {
9
+ reachedEof = true;
10
+ break;
11
+ }
12
+ const accepted = Math.min(limits.sampleBytes - length, item.value.byteLength);
13
+ buffer.set(item.value.subarray(0, accepted), length);
14
+ length += accepted;
15
+ if (accepted < item.value.byteLength)
16
+ cursor.unshift(item.value.subarray(accepted));
17
+ }
18
+ return {
19
+ bytes: buffer.subarray(0, length),
20
+ complete: reachedEof,
21
+ path: file.displayPath,
22
+ mimeType: file.mimeType,
23
+ };
24
+ }
@@ -0,0 +1,26 @@
1
+ import type { ClassificationSample } from "../contract/classify.ts";
2
+ import type { Digest } from "../contract/digest.ts";
3
+ import type { ReadRequest } from "../contract/input.ts";
4
+ import type { ReadLimits } from "../contract/limits.ts";
5
+ import type { CallScope } from "./call-scope.ts";
6
+ import type { ByteCursor } from "./cursor.ts";
7
+ import { LineScanner } from "./scanner.ts";
8
+ export type ScanOutcome = {
9
+ readonly kind: "scanned";
10
+ readonly scanner: LineScanner;
11
+ readonly scannedBytes: number;
12
+ readonly scanCapped: boolean;
13
+ readonly reachedEof: boolean;
14
+ readonly contentId: string | null;
15
+ } | {
16
+ readonly kind: "invalid-utf8";
17
+ };
18
+ export interface ScanInput<THost> {
19
+ readonly cursor: ByteCursor;
20
+ readonly sample: ClassificationSample;
21
+ readonly request: ReadRequest;
22
+ readonly limits: Readonly<ReadLimits>;
23
+ readonly digest: Digest | null;
24
+ readonly scope: CallScope<THost>;
25
+ }
26
+ export declare function scanText<THost>(input: ScanInput<THost>): Promise<ScanOutcome>;
@@ -0,0 +1,95 @@
1
+ import { scanBudget } from "./budget.js";
2
+ import { LineScanner } from "./scanner.js";
3
+ const ABORT_YIELD_BYTES = 1024 * 1024;
4
+ class DecoderFailureError extends Error {
5
+ }
6
+ export async function scanText(input) {
7
+ const { cursor, sample, request, limits, digest, scope } = input;
8
+ const scanner = new LineScanner(request, limits, scanBudget(scope));
9
+ const decoder = new TextDecoder("utf-8", { fatal: true });
10
+ const contentHash = digest === null ? null : digest.create();
11
+ let scannedBytes = 0;
12
+ let scanCapped = false;
13
+ let unscannedBytesKnown = false;
14
+ let reachedEof = sample.complete;
15
+ let nextAbortYield = ABORT_YIELD_BYTES;
16
+ const consume = (chunk) => {
17
+ const available = limits.maxScanBytes - scannedBytes;
18
+ if (available <= 0) {
19
+ scanCapped = chunk.byteLength > 0;
20
+ unscannedBytesKnown ||= chunk.byteLength > 0;
21
+ return;
22
+ }
23
+ const accepted = chunk.subarray(0, available);
24
+ if (accepted.byteLength > 0) {
25
+ contentHash?.update(accepted);
26
+ scannedBytes += accepted.byteLength;
27
+ scanner.push(decodeChunk(decoder, accepted));
28
+ }
29
+ if (accepted.byteLength < chunk.byteLength) {
30
+ scanCapped = true;
31
+ unscannedBytesKnown = true;
32
+ }
33
+ };
34
+ try {
35
+ consume(sample.bytes);
36
+ while (!scanCapped && !reachedEof) {
37
+ scope.checkAbort();
38
+ if (scope.signal !== undefined && scannedBytes >= nextAbortYield) {
39
+ await yieldForAbort();
40
+ nextAbortYield = scannedBytes + ABORT_YIELD_BYTES;
41
+ scope.checkAbort();
42
+ }
43
+ const item = await cursor.next(scope.signal);
44
+ if (item.done) {
45
+ reachedEof = true;
46
+ break;
47
+ }
48
+ consume(item.value);
49
+ }
50
+ scope.checkAbort();
51
+ if (reachedEof) {
52
+ const trailing = finishDecoder(decoder);
53
+ if (trailing)
54
+ scanner.push(trailing);
55
+ scanner.finish();
56
+ }
57
+ else {
58
+ scanner.stopAtScanLimit(unscannedBytesKnown);
59
+ }
60
+ }
61
+ catch (error) {
62
+ if (error instanceof DecoderFailureError)
63
+ return { kind: "invalid-utf8" };
64
+ throw error;
65
+ }
66
+ return {
67
+ kind: "scanned",
68
+ scanner,
69
+ scannedBytes,
70
+ scanCapped,
71
+ reachedEof,
72
+ contentId: reachedEof ? (contentHash?.digest() ?? null) : null,
73
+ };
74
+ }
75
+ function decodeChunk(decoder, chunk) {
76
+ try {
77
+ return decoder.decode(chunk, { stream: true });
78
+ }
79
+ catch (error) {
80
+ throw new DecoderFailureError("Invalid UTF-8", { cause: error });
81
+ }
82
+ }
83
+ function finishDecoder(decoder) {
84
+ try {
85
+ return decoder.decode();
86
+ }
87
+ catch (error) {
88
+ throw new DecoderFailureError("Invalid UTF-8", { cause: error });
89
+ }
90
+ }
91
+ function yieldForAbort() {
92
+ return new Promise((resolve) => {
93
+ setTimeout(resolve, 0);
94
+ });
95
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReadRequest } from "../contract/input.ts";
2
+ import type { ReadLimits } from "../contract/limits.ts";
3
+ import type { ReadLine } from "../contract/result.ts";
4
+ import type { ScanBudget } from "./budget.ts";
5
+ export type SelectionStop = "lines" | "bytes" | "budget" | null;
6
+ export declare class LineScanner {
7
+ private readonly request;
8
+ private readonly limits;
9
+ private readonly budget;
10
+ readonly lines: ReadLine[];
11
+ totalLines: number;
12
+ viewBytes: number;
13
+ selectionStop: SelectionStop;
14
+ firstUnshown: number | null;
15
+ private budgetUsed;
16
+ private linePrefix;
17
+ private lineChars;
18
+ private lineHasContent;
19
+ private pendingCr;
20
+ constructor(request: ReadRequest, limits: Readonly<ReadLimits>, budget?: ScanBudget | null);
21
+ push(text: string): void;
22
+ finish(): void;
23
+ stopAtScanLimit(unscannedBytesKnown: boolean): void;
24
+ get clampedLines(): readonly number[];
25
+ get viewText(): string;
26
+ private retainingCurrentLine;
27
+ private admitByBudget;
28
+ private emitLine;
29
+ }
@@ -0,0 +1,124 @@
1
+ const ENCODER = new TextEncoder();
2
+ export class LineScanner {
3
+ request;
4
+ limits;
5
+ budget;
6
+ lines = [];
7
+ totalLines = 0;
8
+ viewBytes = 0;
9
+ selectionStop = null;
10
+ firstUnshown = null;
11
+ budgetUsed = 0;
12
+ linePrefix = "";
13
+ lineChars = 0;
14
+ lineHasContent = false;
15
+ pendingCr = false;
16
+ constructor(request, limits, budget = null) {
17
+ this.request = request;
18
+ this.limits = limits;
19
+ this.budget = budget;
20
+ }
21
+ push(text) {
22
+ for (const character of text) {
23
+ if (this.pendingCr) {
24
+ this.emitLine();
25
+ this.pendingCr = false;
26
+ if (character === "\n")
27
+ continue;
28
+ }
29
+ if (character === "\r") {
30
+ this.pendingCr = true;
31
+ }
32
+ else if (character === "\n") {
33
+ this.emitLine();
34
+ }
35
+ else {
36
+ this.lineHasContent = true;
37
+ this.lineChars += 1;
38
+ if (this.lineChars <= this.limits.maxCharsPerLine && this.retainingCurrentLine()) {
39
+ this.linePrefix += character;
40
+ }
41
+ }
42
+ }
43
+ }
44
+ finish() {
45
+ if (this.pendingCr) {
46
+ this.emitLine();
47
+ this.pendingCr = false;
48
+ }
49
+ else if (this.lineHasContent) {
50
+ this.emitLine();
51
+ }
52
+ }
53
+ stopAtScanLimit(unscannedBytesKnown) {
54
+ if (this.pendingCr) {
55
+ this.emitLine();
56
+ this.pendingCr = false;
57
+ if (unscannedBytesKnown && this.firstUnshown === null) {
58
+ const nextLine = this.totalLines + 1;
59
+ if (nextLine >= this.request.offset)
60
+ this.firstUnshown = nextLine;
61
+ }
62
+ return;
63
+ }
64
+ if ((this.lineHasContent || unscannedBytesKnown) && this.firstUnshown === null) {
65
+ const currentLine = this.totalLines + 1;
66
+ if (currentLine >= this.request.offset)
67
+ this.firstUnshown = currentLine;
68
+ }
69
+ }
70
+ get clampedLines() {
71
+ return this.lines.filter((line) => line.clamped).map((line) => line.number);
72
+ }
73
+ get viewText() {
74
+ return this.lines.map((line) => line.text).join("\n");
75
+ }
76
+ retainingCurrentLine() {
77
+ return this.totalLines + 1 >= this.request.offset && this.selectionStop === null;
78
+ }
79
+ admitByBudget() {
80
+ if (this.budget === null)
81
+ return true;
82
+ const cost = this.budget.measure(this.linePrefix);
83
+ // The first line is always admitted, so a budget alone never gives an empty view.
84
+ if (this.lines.length > 0 && this.budgetUsed + cost > this.budget.max)
85
+ return false;
86
+ this.budgetUsed += cost;
87
+ return true;
88
+ }
89
+ emitLine() {
90
+ this.totalLines += 1;
91
+ const number = this.totalLines;
92
+ if (number >= this.request.offset && this.selectionStop === null) {
93
+ if (this.lines.length >= this.request.limit) {
94
+ this.selectionStop = "lines";
95
+ this.firstUnshown = number;
96
+ }
97
+ else {
98
+ const clamped = this.lineChars > this.limits.maxCharsPerLine;
99
+ const separatorBytes = this.lines.length === 0 ? 0 : 1;
100
+ const candidateBytes = ENCODER.encode(this.linePrefix).byteLength + separatorBytes;
101
+ if (this.viewBytes + candidateBytes > this.limits.maxViewBytes) {
102
+ this.selectionStop = "bytes";
103
+ this.firstUnshown = number;
104
+ }
105
+ else if (!this.admitByBudget()) {
106
+ this.selectionStop = "budget";
107
+ this.firstUnshown = number;
108
+ }
109
+ else {
110
+ this.lines.push({
111
+ number,
112
+ text: this.linePrefix,
113
+ clamped,
114
+ sourceChars: clamped ? this.lineChars : null,
115
+ });
116
+ this.viewBytes += candidateBytes;
117
+ }
118
+ }
119
+ }
120
+ this.linePrefix = "";
121
+ this.lineChars = 0;
122
+ this.lineHasContent = false;
123
+ }
124
+ }
@@ -0,0 +1,6 @@
1
+ import type { FileSystem, FileSystemError } from "@better-fs-tools/fs";
2
+ import type { ReadDependencies } from "../contract/deps.ts";
3
+ import type { ReadRequest } from "../contract/input.ts";
4
+ import type { ReadFailure } from "../contract/result.ts";
5
+ import type { CallScope } from "./call-scope.ts";
6
+ export declare function missOutcome<THost>(deps: ReadDependencies<THost>, request: ReadRequest, fs: FileSystem, scope: CallScope<THost>, missed: string, error: FileSystemError | null): Promise<ReadFailure>;
@@ -0,0 +1,41 @@
1
+ import { errorData, notFound } from "./outcomes.js";
2
+ export async function missOutcome(deps, request, fs, scope, missed, error) {
3
+ const data = error === null ? {} : errorData(error);
4
+ const found = deps.suggest === null
5
+ ? null
6
+ : await suggestNames(deps.suggest, fs, scope, missed, deps.limits.maxSuggestions);
7
+ scope.checkAbort();
8
+ if (found === null)
9
+ return notFound(deps.messages, request, [], data);
10
+ const { suggestions, entriesTruncated } = found;
11
+ return notFound(deps.messages, request, suggestions, {
12
+ ...data,
13
+ ...(suggestions.length === 0 ? {} : { suggestions: [...suggestions] }),
14
+ ...(entriesTruncated ? { entriesTruncated } : {}),
15
+ });
16
+ }
17
+ async function suggestNames(suggest, fs, scope, missed, max) {
18
+ const listed = await scope.list("open", fs.paths.dirname(missed));
19
+ scope.throwHeld();
20
+ if (!listed.ok)
21
+ return null;
22
+ let names;
23
+ try {
24
+ names = suggest({
25
+ path: missed,
26
+ name: fs.paths.basename(missed),
27
+ entries: listed.entries,
28
+ entriesTruncated: listed.truncated,
29
+ max,
30
+ call: scope.call,
31
+ });
32
+ }
33
+ catch {
34
+ scope.checkAbort();
35
+ throw scope.extensionFailure("suggest");
36
+ }
37
+ if (!Array.isArray(names) || !names.every((name) => typeof name === "string")) {
38
+ throw scope.extensionFailure("suggest");
39
+ }
40
+ return { suggestions: names.slice(0, max), entriesTruncated: listed.truncated };
41
+ }