@smthrs/std 1.0.0-rc.7 → 1.0.0-rc.9
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/CHANGELOG.md +14 -0
- package/README.md +40 -36
- package/dist/cjs/Checkpoints.d.ts +37 -0
- package/dist/cjs/Checkpoints.d.ts.map +1 -1
- package/dist/cjs/Checkpoints.js +75 -0
- package/dist/cjs/Checkpoints.js.map +2 -2
- package/dist/cjs/PortableSearch.d.ts +13 -1
- package/dist/cjs/PortableSearch.d.ts.map +1 -1
- package/dist/cjs/PortableSearch.js +8 -1
- package/dist/cjs/PortableSearch.js.map +2 -2
- package/dist/cjs/RemoteSearch.d.ts +54 -0
- package/dist/cjs/RemoteSearch.d.ts.map +1 -0
- package/dist/cjs/RemoteSearch.js +134 -0
- package/dist/cjs/RemoteSearch.js.map +7 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/cjs/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/cjs/internal/EnvelopePrecheck.js +235 -36
- package/dist/cjs/internal/EnvelopePrecheck.js.map +3 -3
- package/dist/cjs/internal/Exec.d.ts +12 -7
- package/dist/cjs/internal/Exec.d.ts.map +1 -1
- package/dist/cjs/internal/Exec.js +8 -4
- package/dist/cjs/internal/Exec.js.map +2 -2
- package/dist/cjs/internal/RemoteTree.d.ts +60 -0
- package/dist/cjs/internal/RemoteTree.d.ts.map +1 -0
- package/dist/cjs/internal/RemoteTree.js +123 -0
- package/dist/cjs/internal/RemoteTree.js.map +7 -0
- package/dist/cjs/internal/Tar.d.ts +23 -0
- package/dist/cjs/internal/Tar.d.ts.map +1 -0
- package/dist/cjs/internal/Tar.js +86 -0
- package/dist/cjs/internal/Tar.js.map +7 -0
- package/dist/esm/Checkpoints.d.ts +37 -0
- package/dist/esm/Checkpoints.d.ts.map +1 -1
- package/dist/esm/Checkpoints.js +85 -0
- package/dist/esm/Checkpoints.js.map +1 -1
- package/dist/esm/PortableSearch.d.ts +13 -1
- package/dist/esm/PortableSearch.d.ts.map +1 -1
- package/dist/esm/PortableSearch.js +16 -0
- package/dist/esm/PortableSearch.js.map +1 -1
- package/dist/esm/RemoteSearch.d.ts +54 -0
- package/dist/esm/RemoteSearch.d.ts.map +1 -0
- package/dist/esm/RemoteSearch.js +162 -0
- package/dist/esm/RemoteSearch.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/esm/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.js +321 -62
- package/dist/esm/internal/EnvelopePrecheck.js.map +1 -1
- package/dist/esm/internal/Exec.d.ts +12 -7
- package/dist/esm/internal/Exec.d.ts.map +1 -1
- package/dist/esm/internal/Exec.js +30 -9
- package/dist/esm/internal/Exec.js.map +1 -1
- package/dist/esm/internal/RemoteTree.d.ts +60 -0
- package/dist/esm/internal/RemoteTree.d.ts.map +1 -0
- package/dist/esm/internal/RemoteTree.js +132 -0
- package/dist/esm/internal/RemoteTree.js.map +1 -0
- package/dist/esm/internal/Tar.d.ts +23 -0
- package/dist/esm/internal/Tar.d.ts.map +1 -0
- package/dist/esm/internal/Tar.js +97 -0
- package/dist/esm/internal/Tar.js.map +1 -0
- package/docs/README.md +3 -1
- package/docs/api.md +48 -21
- package/docs/concepts/effects-and-capabilities.md +3 -1
- package/docs/guides/bind-the-standard-flows.md +3 -1
- package/docs/guides/pin-a-checkpoint.md +20 -0
- package/docs/guides/run-a-shell-command.md +4 -1
- package/docs/installation.md +13 -9
- package/docs/quickstart.md +1 -1
- package/docs/troubleshooting.md +20 -0
- package/package.json +16 -6
- package/src/Checkpoints.ts +137 -0
- package/src/PortableSearch.ts +20 -0
- package/src/RemoteSearch.ts +199 -0
- package/src/index.ts +3 -0
- package/src/internal/EnvelopePrecheck.ts +320 -64
- package/src/internal/Exec.ts +32 -8
- package/src/internal/RemoteTree.ts +159 -0
- package/src/internal/Tar.ts +95 -0
package/src/internal/Exec.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { elideMiddle } from "./Text.ts"
|
|
|
30
30
|
* @category models
|
|
31
31
|
* @since 1.0.0
|
|
32
32
|
*/
|
|
33
|
-
export const ExecErrorCode = Schema.Literals(["timeout", "spawn_error", "capture_overflow"])
|
|
33
|
+
export const ExecErrorCode = Schema.Literals(["timeout", "spawn_error", "arguments_too_long", "capture_overflow"])
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Failure codes a buffered execution can report.
|
|
@@ -44,8 +44,10 @@ export type ExecErrorCode = typeof ExecErrorCode.Type
|
|
|
44
44
|
* A command that never produced an exit code.
|
|
45
45
|
*
|
|
46
46
|
* A non-zero exit is a successful {@link ExecResult}, not a failure; this
|
|
47
|
-
* error is reserved for a command that could not start
|
|
48
|
-
*
|
|
47
|
+
* error is reserved for a command that could not start (`arguments_too_long`
|
|
48
|
+
* when the operating system refused it for the size of its arguments) or was
|
|
49
|
+
* cut off by its timeout, or whose captured output exceeded a refusing capture
|
|
50
|
+
* bound.
|
|
49
51
|
*
|
|
50
52
|
* @category errors
|
|
51
53
|
* @since 1.0.0
|
|
@@ -269,6 +271,23 @@ const capture = <E>(
|
|
|
269
271
|
? boundedEnds(stream, maxBytes)
|
|
270
272
|
: boundedTail(stream, maxBytes, overflow)
|
|
271
273
|
|
|
274
|
+
/**
|
|
275
|
+
* A command whose arguments exceed the operating system's limit fails `execve`
|
|
276
|
+
* with `E2BIG` before it starts: one argument over 128 KiB on Linux, all of
|
|
277
|
+
* them over 1 MiB on macOS. A heredoc in a command line, or a file's text
|
|
278
|
+
* passed as an argument, is one such argument. The platform error's own text
|
|
279
|
+
* repeats the whole command line, so this text is written here instead, and a
|
|
280
|
+
* caller may show it as it stands.
|
|
281
|
+
*/
|
|
282
|
+
const argumentsTooLong = (error: { readonly cause?: unknown }): ExecError | undefined =>
|
|
283
|
+
(error.cause as { readonly code?: unknown } | undefined)?.code === "E2BIG"
|
|
284
|
+
? new ExecError({
|
|
285
|
+
code: "arguments_too_long",
|
|
286
|
+
message:
|
|
287
|
+
"Command not started: its arguments exceed the operating system's size limit (E2BIG). Send long text on standard input or in a file, not in the command line or its arguments."
|
|
288
|
+
})
|
|
289
|
+
: undefined
|
|
290
|
+
|
|
272
291
|
const unbounded = (
|
|
273
292
|
command: string,
|
|
274
293
|
options: ExecOptions
|
|
@@ -327,7 +346,7 @@ const unbounded = (
|
|
|
327
346
|
}).pipe(
|
|
328
347
|
Effect.scoped,
|
|
329
348
|
Effect.mapError((error) =>
|
|
330
|
-
error instanceof ExecError ? error : new ExecError({
|
|
349
|
+
error instanceof ExecError ? error : argumentsTooLong(error) ?? new ExecError({
|
|
331
350
|
code: "spawn_error",
|
|
332
351
|
message: `exec: ${command}: ${error.message}`
|
|
333
352
|
})
|
|
@@ -368,20 +387,23 @@ export const exec = (
|
|
|
368
387
|
*
|
|
369
388
|
* A timeout keeps its own code, because "the command never finished" and "the
|
|
370
389
|
* command never started" are different things to a caller deciding whether to
|
|
371
|
-
* retry.
|
|
390
|
+
* retry. Arguments the operating system refused for their size are the
|
|
391
|
+
* caller's own input, which a shell flow publishes, so they read as
|
|
392
|
+
* `invalid_input`. Every other way of producing no exit code is a host failure.
|
|
372
393
|
*
|
|
373
394
|
* @category errors
|
|
374
395
|
* @since 1.0.0
|
|
375
396
|
*/
|
|
376
397
|
export const toStdErrorCode = (error: ExecError): StdError.Code =>
|
|
377
|
-
error.code === "timeout" ? "timeout" : "command_failed"
|
|
398
|
+
error.code === "timeout" ? "timeout" : error.code === "arguments_too_long" ? "invalid_input" : "command_failed"
|
|
378
399
|
|
|
379
400
|
/**
|
|
380
401
|
* The model-facing failure one execution error becomes.
|
|
381
402
|
*
|
|
382
403
|
* A timeout names the command, which is what the caller asked for and all a
|
|
383
|
-
* timeout has to say.
|
|
384
|
-
*
|
|
404
|
+
* timeout has to say. Arguments too long to start carry the reason and the
|
|
405
|
+
* fix, and nothing of the command. Any other failure carries the message the
|
|
406
|
+
* execution produced, which already names the command.
|
|
385
407
|
*
|
|
386
408
|
* @category errors
|
|
387
409
|
* @since 1.0.0
|
|
@@ -391,6 +413,8 @@ export const toStdError = (command: string, error: ExecError): StdError.StdError
|
|
|
391
413
|
code: toStdErrorCode(error),
|
|
392
414
|
message: error.code === "timeout"
|
|
393
415
|
? `Command timed out: ${command}`
|
|
416
|
+
: error.code === "arguments_too_long"
|
|
417
|
+
? error.message
|
|
394
418
|
: `Command failed to start: ${error.message}`,
|
|
395
419
|
...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })
|
|
396
420
|
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tree another machine listed, served as a read-only `FileSystem`.
|
|
3
|
+
*
|
|
4
|
+
* `RemoteSearch` lists a root on the machine with `find` and brings back the
|
|
5
|
+
* bytes the search reads with `tar`; this module holds what came back and
|
|
6
|
+
* answers what the walk, the globs, and the matcher ask of a filesystem, so
|
|
7
|
+
* `PortableSearch` can search the copy as it searches a host.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.0.0-rc.8
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type * as Path from "@smthrs/kernel/Path"
|
|
13
|
+
import { Effect, Option, Stream } from "effect"
|
|
14
|
+
import * as ByteSize from "effect/ByteSize"
|
|
15
|
+
import * as FileSystem from "effect/FileSystem"
|
|
16
|
+
import * as PlatformError from "effect/PlatformError"
|
|
17
|
+
import * as Tar from "./Tar.ts"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What one listed path is: `d` a directory, `u` a directory the machine's
|
|
21
|
+
* user may not list, `f` a file, `o` anything else, and a symbolic link to a
|
|
22
|
+
* directory (`D`), to a file (`F`), or to neither (`L`).
|
|
23
|
+
*
|
|
24
|
+
* @private
|
|
25
|
+
* @since 1.0.0-rc.8
|
|
26
|
+
*/
|
|
27
|
+
export type Kind = "d" | "u" | "f" | "o" | "D" | "F" | "L"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One machine's listing of a root, and the bytes it brought back, by
|
|
31
|
+
* absolute path.
|
|
32
|
+
*
|
|
33
|
+
* @private
|
|
34
|
+
* @since 1.0.0-rc.8
|
|
35
|
+
*/
|
|
36
|
+
export interface Tree {
|
|
37
|
+
readonly kinds: ReadonlyMap<string, Kind>
|
|
38
|
+
readonly children: ReadonlyMap<string, ReadonlyArray<string>>
|
|
39
|
+
readonly contents: ReadonlyMap<string, Uint8Array>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const kinds: ReadonlySet<string> = new Set(["d", "u", "f", "o", "D", "F", "L"])
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The member names `tar` wrote for absolute paths, which it strips of their
|
|
46
|
+
* leading `/`, as those paths again.
|
|
47
|
+
*
|
|
48
|
+
* @private
|
|
49
|
+
* @since 1.0.0-rc.8
|
|
50
|
+
*/
|
|
51
|
+
export const absolute = (members: ReadonlyMap<string, Uint8Array>): ReadonlyMap<string, Uint8Array> =>
|
|
52
|
+
new Map([...members].map(([name, bytes]) => [`/${name}`, bytes]))
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reads NUL-terminated `<kind><path>` records up to an `E` record, then the
|
|
56
|
+
* tar stream of `.gitignore` files after it. `root` is the listed root, which
|
|
57
|
+
* is nobody's child. `undefined` when the bytes are not that shape.
|
|
58
|
+
*
|
|
59
|
+
* @private
|
|
60
|
+
* @since 1.0.0-rc.8
|
|
61
|
+
*/
|
|
62
|
+
export const parse = (bytes: Uint8Array, root: string, path: Path.Path): Tree | undefined => {
|
|
63
|
+
const decoder = new TextDecoder()
|
|
64
|
+
const listed = new Map<string, Kind>()
|
|
65
|
+
const children = new Map<string, Array<string>>()
|
|
66
|
+
let offset = 0
|
|
67
|
+
for (;;) {
|
|
68
|
+
const end = bytes.indexOf(0, offset)
|
|
69
|
+
if (end === -1) return undefined
|
|
70
|
+
const record = decoder.decode(bytes.subarray(offset, end))
|
|
71
|
+
offset = end + 1
|
|
72
|
+
if (record === "E") break
|
|
73
|
+
const kind = record.slice(0, 1)
|
|
74
|
+
if (!kinds.has(kind)) return undefined
|
|
75
|
+
const entry = record.slice(1)
|
|
76
|
+
listed.set(entry, kind as Kind)
|
|
77
|
+
if (entry === root) continue
|
|
78
|
+
const parent = path.dirname(entry)
|
|
79
|
+
const siblings = children.get(parent)
|
|
80
|
+
if (siblings === undefined) children.set(parent, [path.basename(entry)])
|
|
81
|
+
else siblings.push(path.basename(entry))
|
|
82
|
+
}
|
|
83
|
+
const ignores = Tar.files(bytes.subarray(offset))
|
|
84
|
+
return ignores === undefined ? undefined : { kinds: listed, children, contents: absolute(ignores) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const refusal = (tag: PlatformError.SystemErrorTag, method: string, path: string): PlatformError.PlatformError =>
|
|
88
|
+
PlatformError.systemError({ _tag: tag, module: "FileSystem", method, pathOrDescriptor: path })
|
|
89
|
+
|
|
90
|
+
/** What `stat` says each kind is: links resolved, a dangling one absent. */
|
|
91
|
+
const types: Readonly<Record<Kind, FileSystem.File.Type | undefined>> = {
|
|
92
|
+
d: "Directory",
|
|
93
|
+
u: "Directory",
|
|
94
|
+
D: "Directory",
|
|
95
|
+
f: "File",
|
|
96
|
+
F: "File",
|
|
97
|
+
o: "Unknown",
|
|
98
|
+
L: undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const info = (type: FileSystem.File.Type, size: number): FileSystem.File.Info => ({
|
|
102
|
+
type,
|
|
103
|
+
size: ByteSize.bytes(BigInt(size)),
|
|
104
|
+
mtime: Option.none(),
|
|
105
|
+
atime: Option.none(),
|
|
106
|
+
birthtime: Option.none(),
|
|
107
|
+
dev: 0,
|
|
108
|
+
ino: Option.none(),
|
|
109
|
+
mode: 0,
|
|
110
|
+
nlink: Option.none(),
|
|
111
|
+
uid: Option.none(),
|
|
112
|
+
gid: Option.none(),
|
|
113
|
+
rdev: Option.none(),
|
|
114
|
+
blksize: Option.none(),
|
|
115
|
+
blocks: Option.none()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The tree as a read-only `FileSystem`, resolving paths with `path`. A file
|
|
120
|
+
* whose bytes did not come back is one the machine's user could not read,
|
|
121
|
+
* which the in-process peer skips exactly as it skips such a file on a host.
|
|
122
|
+
*
|
|
123
|
+
* @private
|
|
124
|
+
* @since 1.0.0-rc.8
|
|
125
|
+
*/
|
|
126
|
+
export const fileSystem = (tree: Tree, path: Path.Path): FileSystem.FileSystem => {
|
|
127
|
+
const kindOf = (value: string) => tree.kinds.get(path.resolve(value))
|
|
128
|
+
const readFile = (value: string) => {
|
|
129
|
+
const bytes = tree.contents.get(path.resolve(value))
|
|
130
|
+
if (bytes !== undefined) return Effect.succeed(bytes)
|
|
131
|
+
const type = types[kindOf(value) ?? "L"]
|
|
132
|
+
return Effect.fail(
|
|
133
|
+
refusal(type === "File" ? "PermissionDenied" : type === undefined ? "NotFound" : "BadResource", "readFile", value)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return FileSystem.makeNoop({
|
|
137
|
+
stat: (value) => {
|
|
138
|
+
const type = types[kindOf(value) ?? "L"]
|
|
139
|
+
return type === undefined
|
|
140
|
+
? Effect.fail(refusal("NotFound", "stat", value))
|
|
141
|
+
: Effect.succeed(info(type, tree.contents.get(path.resolve(value))?.length ?? 0))
|
|
142
|
+
},
|
|
143
|
+
readDirectory: (value) => {
|
|
144
|
+
const kind = kindOf(value)
|
|
145
|
+
if (kind === "d") return Effect.succeed([...tree.children.get(path.resolve(value)) ?? []])
|
|
146
|
+
const tag = kind === "u" ? "PermissionDenied" : kind === undefined || kind === "L" ? "NotFound" : "BadResource"
|
|
147
|
+
return Effect.fail(refusal(tag, "readDirectory", value))
|
|
148
|
+
},
|
|
149
|
+
readLink: (value) => {
|
|
150
|
+
const kind = kindOf(value)
|
|
151
|
+
return kind === "D" || kind === "F" || kind === "L"
|
|
152
|
+
? Effect.succeed("")
|
|
153
|
+
: Effect.fail(refusal(kind === undefined ? "NotFound" : "BadResource", "readLink", value))
|
|
154
|
+
},
|
|
155
|
+
readFile,
|
|
156
|
+
readFileString: (value) => Effect.map(readFile(value), (bytes) => new TextDecoder().decode(bytes)),
|
|
157
|
+
stream: (value) => Stream.fromEffect(readFile(value))
|
|
158
|
+
})
|
|
159
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The regular files of a tar stream.
|
|
3
|
+
*
|
|
4
|
+
* `RemoteSearch` brings a machine's files back with its own `tar`, because
|
|
5
|
+
* every image ships one and it frames each file with its name and size. GNU
|
|
6
|
+
* and BusyBox `tar` write GNU long names, `bsdtar` writes POSIX extended
|
|
7
|
+
* headers, and `xargs` may start `tar` more than once, so the stream can hold
|
|
8
|
+
* several archives one after another. This reads all of them.
|
|
9
|
+
*
|
|
10
|
+
* @since 1.0.0-rc.8
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const decoder = new TextDecoder()
|
|
14
|
+
|
|
15
|
+
/** Bytes up to the first NUL, as text. */
|
|
16
|
+
const text = (bytes: Uint8Array): string => {
|
|
17
|
+
const end = bytes.indexOf(0)
|
|
18
|
+
return decoder.decode(end === -1 ? bytes : bytes.subarray(0, end))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** An octal header field, or `undefined` when it holds anything else. */
|
|
22
|
+
const octal = (bytes: Uint8Array): number | undefined => {
|
|
23
|
+
const digits = text(bytes).trim()
|
|
24
|
+
return /^[0-7]+$/.test(digits) ? Number.parseInt(digits, 8) : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The header's checksum, computed with the checksum field read as spaces. */
|
|
28
|
+
const checksum = (header: Uint8Array): number => {
|
|
29
|
+
let sum = 0
|
|
30
|
+
for (let index = 0; index < 512; index++) sum += index >= 148 && index < 156 ? 32 : header[index]!
|
|
31
|
+
return sum
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The `path` record of a POSIX extended header: `<length> <key>=<value>\n` records. */
|
|
35
|
+
const paxPath = (data: Uint8Array): string | undefined => {
|
|
36
|
+
let offset = 0
|
|
37
|
+
let path: string | undefined
|
|
38
|
+
while (offset < data.length) {
|
|
39
|
+
const space = data.indexOf(32, offset)
|
|
40
|
+
const length = space === -1 ? Number.NaN : Number(decoder.decode(data.subarray(offset, space)))
|
|
41
|
+
if (!Number.isSafeInteger(length) || length <= space - offset || offset + length > data.length) return path
|
|
42
|
+
const record = decoder.decode(data.subarray(space + 1, offset + length - 1))
|
|
43
|
+
if (record.startsWith("path=")) path = record.slice("path=".length)
|
|
44
|
+
offset += length
|
|
45
|
+
}
|
|
46
|
+
return path
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every regular file the stream holds, by member name, or `undefined` when
|
|
51
|
+
* the stream is not whole tar archives: a header whose checksum does not
|
|
52
|
+
* hold, a member cut short, or a last archive with no end. A `tar` that
|
|
53
|
+
* stopped between two members wrote no end, so its missing files are not
|
|
54
|
+
* mistaken for files it could not read.
|
|
55
|
+
*
|
|
56
|
+
* @private
|
|
57
|
+
* @since 1.0.0-rc.8
|
|
58
|
+
*/
|
|
59
|
+
export const files = (stream: Uint8Array): ReadonlyMap<string, Uint8Array> | undefined => {
|
|
60
|
+
const found = new Map<string, Uint8Array>()
|
|
61
|
+
let longName: string | undefined
|
|
62
|
+
let extended: string | undefined
|
|
63
|
+
let ended = true
|
|
64
|
+
for (let offset = 0; offset + 512 <= stream.length;) {
|
|
65
|
+
const header = stream.subarray(offset, offset + 512)
|
|
66
|
+
offset += 512
|
|
67
|
+
// Zero blocks end one archive; `xargs` may have started another.
|
|
68
|
+
ended = header.every((byte) => byte === 0)
|
|
69
|
+
if (ended) continue
|
|
70
|
+
const size = octal(header.subarray(124, 136))
|
|
71
|
+
if (size === undefined || octal(header.subarray(148, 156)) !== checksum(header)) return undefined
|
|
72
|
+
if (offset + size > stream.length) return undefined
|
|
73
|
+
const data = stream.subarray(offset, offset + size)
|
|
74
|
+
offset += Math.ceil(size / 512) * 512
|
|
75
|
+
const type = String.fromCharCode(header[156]!)
|
|
76
|
+
if (type === "L") {
|
|
77
|
+
longName = text(data)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
if (type === "x") {
|
|
81
|
+
extended = paxPath(data) ?? extended
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
// A long link name and a global header describe no member of their own.
|
|
85
|
+
if (type === "K" || type === "g") continue
|
|
86
|
+
if (type === "0" || type === "\0" || type === "7") {
|
|
87
|
+
const prefix = decoder.decode(header.subarray(257, 263)) === "ustar\0" ? text(header.subarray(345, 500)) : ""
|
|
88
|
+
const short = text(header.subarray(0, 100))
|
|
89
|
+
found.set(extended ?? longName ?? (prefix === "" ? short : `${prefix}/${short}`), data.slice())
|
|
90
|
+
}
|
|
91
|
+
longName = undefined
|
|
92
|
+
extended = undefined
|
|
93
|
+
}
|
|
94
|
+
return ended ? found : undefined
|
|
95
|
+
}
|