@oh-my-pi/pi-utils 17.4.0 → 17.4.2

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 (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +22 -0
  3. package/THIRD-PARTY-NOTICES.txt +22909 -0
  4. package/dist/types/ar/arj.d.ts +5 -0
  5. package/dist/types/ar/asar.d.ts +7 -0
  6. package/dist/types/ar/bytes.d.ts +20 -0
  7. package/dist/types/ar/cab.d.ts +5 -0
  8. package/dist/types/ar/checksums.d.ts +10 -0
  9. package/dist/types/ar/codecs/bzip2.d.ts +4 -0
  10. package/dist/types/ar/codecs/gzip.d.ts +6 -0
  11. package/dist/types/ar/codecs/lzma.d.ts +6 -0
  12. package/dist/types/ar/codecs/lzw.d.ts +4 -0
  13. package/dist/types/ar/codecs/lzx.d.ts +7 -0
  14. package/dist/types/ar/codecs/xz.d.ts +4 -0
  15. package/dist/types/ar/codecs/zstd.d.ts +6 -0
  16. package/dist/types/ar/cpio.d.ts +7 -0
  17. package/dist/types/ar/deb.d.ts +5 -0
  18. package/dist/types/ar/entries.d.ts +22 -0
  19. package/dist/types/ar/error.d.ts +8 -0
  20. package/dist/types/ar/index.d.ts +11 -0
  21. package/dist/types/ar/iso.d.ts +5 -0
  22. package/dist/types/ar/limits.d.ts +33 -0
  23. package/dist/types/ar/lzh.d.ts +7 -0
  24. package/dist/types/ar/open.d.ts +45 -0
  25. package/dist/types/ar/paths.d.ts +18 -0
  26. package/dist/types/ar/rar/rar4-decoder.d.ts +6 -0
  27. package/dist/types/ar/rar/rar5-decoder.d.ts +6 -0
  28. package/dist/types/ar/rar.d.ts +5 -0
  29. package/dist/types/ar/reader.d.ts +28 -0
  30. package/dist/types/ar/registry.d.ts +17 -0
  31. package/dist/types/ar/rpm.d.ts +5 -0
  32. package/dist/types/ar/sevenzip/decode.d.ts +33 -0
  33. package/dist/types/ar/sevenzip.d.ts +5 -0
  34. package/dist/types/ar/source.d.ts +54 -0
  35. package/dist/types/ar/tar.d.ts +9 -0
  36. package/dist/types/ar/types.d.ts +99 -0
  37. package/dist/types/ar/unix-ar.d.ts +7 -0
  38. package/dist/types/ar/write.d.ts +10 -0
  39. package/dist/types/ar/zip.d.ts +10 -0
  40. package/dist/types/postmortem.d.ts +33 -0
  41. package/package.json +9 -3
  42. package/src/ar/arj.ts +314 -0
  43. package/src/ar/asar.ts +480 -0
  44. package/src/ar/bytes.ts +78 -0
  45. package/src/ar/cab.ts +377 -0
  46. package/src/ar/checksums.ts +62 -0
  47. package/src/ar/codecs/bzip2.ts +489 -0
  48. package/src/ar/codecs/gzip.ts +25 -0
  49. package/src/ar/codecs/lzma.ts +437 -0
  50. package/src/ar/codecs/lzw.ts +191 -0
  51. package/src/ar/codecs/lzx.ts +366 -0
  52. package/src/ar/codecs/xz.ts +522 -0
  53. package/src/ar/codecs/zstd.ts +25 -0
  54. package/src/ar/cpio.ts +389 -0
  55. package/src/ar/deb.ts +160 -0
  56. package/src/ar/entries.ts +97 -0
  57. package/src/ar/error.ts +11 -0
  58. package/src/ar/index.ts +16 -0
  59. package/src/ar/iso.ts +712 -0
  60. package/src/ar/limits.ts +80 -0
  61. package/src/ar/lzh.ts +659 -0
  62. package/src/ar/open.ts +225 -0
  63. package/src/ar/paths.ts +70 -0
  64. package/src/ar/rar/rar4-decoder.ts +459 -0
  65. package/src/ar/rar/rar5-decoder.ts +400 -0
  66. package/src/ar/rar.ts +735 -0
  67. package/src/ar/reader.ts +162 -0
  68. package/src/ar/registry.ts +208 -0
  69. package/src/ar/rpm.ts +320 -0
  70. package/src/ar/sevenzip/decode.ts +239 -0
  71. package/src/ar/sevenzip.ts +634 -0
  72. package/src/ar/source.ts +190 -0
  73. package/src/ar/tar.ts +771 -0
  74. package/src/ar/types.ts +131 -0
  75. package/src/ar/unix-ar.ts +312 -0
  76. package/src/ar/write.ts +55 -0
  77. package/src/ar/zip.ts +719 -0
  78. package/src/browsers.ts +2 -149
  79. package/src/docx/converter.ts +9 -9
  80. package/src/postmortem.ts +74 -0
  81. package/dist/types/docx/zip.d.ts +0 -6
  82. package/src/docx/zip.ts +0 -87
@@ -0,0 +1,131 @@
1
+ import type { ArchiveLimits } from "./limits";
2
+ import type { ByteSource } from "./source";
3
+
4
+ /** Archive container formats readable by the unified archive API. */
5
+ export type ArchiveFormat =
6
+ // Containers with their own member framing.
7
+ | "zip"
8
+ | "tar"
9
+ | "tar.gz"
10
+ | "tar.bz2"
11
+ | "tar.xz"
12
+ | "tar.zst"
13
+ | "tar.Z"
14
+ | "asar"
15
+ | "rar"
16
+ | "7z"
17
+ | "iso"
18
+ | "cab"
19
+ | "cpio"
20
+ | "rpm"
21
+ | "ar"
22
+ | "deb"
23
+ | "lzh"
24
+ | "arj"
25
+ // Single-stream compressors exposed as one-member pseudo-archives.
26
+ | "gz"
27
+ | "bz2"
28
+ | "xz"
29
+ | "zst"
30
+ | "Z"
31
+ | "lzma";
32
+
33
+ /** Archive formats the unified API can serialize. Everything else is read-only. */
34
+ export type WritableArchiveFormat = "zip" | "tar" | "tar.gz" | "tar.zst" | "asar";
35
+
36
+ /**
37
+ * Where to read an archive from: an extension-inferred filesystem path, a
38
+ * format-tagged filesystem path, in-memory bytes, or any caller-provided
39
+ * {@link ByteSource} (e.g. `httpByteSource` for ranged remote reads).
40
+ * File- and source-backed ZIP/ASAR/RAR/7z/ISO are read lazily.
41
+ */
42
+ export type ArchiveSource =
43
+ | string
44
+ | { bytes: Uint8Array; format: ArchiveFormat }
45
+ | { path: string; format: ArchiveFormat }
46
+ | { source: ByteSource; format: ArchiveFormat; path?: string };
47
+
48
+ /** Content for a member when packing or extracting an archive. */
49
+ export type ArchiveMemberContent = string | Uint8Array | Blob;
50
+
51
+ /** One `archive.ext:inner/path` split candidate (see `parseArchivePathCandidates`). */
52
+ export interface ArchivePathCandidate {
53
+ archivePath: string;
54
+ subPath: string;
55
+ }
56
+
57
+ /** A file or directory node visible through an `ArchiveReader`. */
58
+ export interface ArchiveNode {
59
+ path: string;
60
+ isDirectory: boolean;
61
+ size: number;
62
+ mtimeMs?: number;
63
+ /** Unix permission/type bits when the container records them. */
64
+ mode?: number;
65
+ }
66
+
67
+ /** An {@link ArchiveNode} with its name relative to the listed directory. */
68
+ export interface ArchiveDirectoryEntry extends ArchiveNode {
69
+ name: string;
70
+ }
71
+
72
+ /** An {@link ArchiveNode} with its extracted payload. */
73
+ export interface ExtractedArchiveFile extends ArchiveNode {
74
+ bytes: Uint8Array;
75
+ }
76
+
77
+ /**
78
+ * Format-owned handle that extracts one member's bytes on demand. Instances
79
+ * may share per-archive state (solid-block decoders, 7z folder caches).
80
+ * Prefer classes with prototype methods over closures: archives can index
81
+ * hundreds of thousands of members.
82
+ */
83
+ export interface MemberSource {
84
+ /**
85
+ * Read this member's bytes. `size` is the entry's declared uncompressed
86
+ * size (already bounds-checked); `memberPath` is for error messages.
87
+ * Implementations must verify the produced byte count (and checksums when
88
+ * the container records them) and throw {@link ArchiveError} on mismatch.
89
+ */
90
+ read(size: number, memberPath: string): Promise<Uint8Array>;
91
+ }
92
+
93
+ /**
94
+ * How an indexed entry's bytes are stored. `link` entries alias another
95
+ * archive path and are resolved lazily by the reader core; `member` entries
96
+ * defer to their format module.
97
+ */
98
+ export type EntryStorage =
99
+ | {
100
+ type: "link";
101
+ targetPath: string;
102
+ /** Follow before target kind is known (ASAR link records do not encode it). */
103
+ resolveTarget: boolean;
104
+ }
105
+ | { type: "member"; source: MemberSource };
106
+
107
+ /** One indexed entry as produced by a format reader, before core resolution. */
108
+ export interface ArchiveIndexEntry extends ArchiveNode {
109
+ storage?: EntryStorage;
110
+ }
111
+
112
+ /** Context passed to every format reader. */
113
+ export interface FormatReadOptions {
114
+ limits: ArchiveLimits;
115
+ /**
116
+ * Filesystem path of the archive when file-backed. Formats that reference
117
+ * sibling files use it (ASAR `.unpacked` payloads, multi-volume RAR).
118
+ */
119
+ archivePath?: string;
120
+ }
121
+
122
+ /**
123
+ * Contract implemented by every format module: index `source` into normalized
124
+ * entries without materializing member payloads unless the container forces
125
+ * it (tar streams, solid archives). Implementations must:
126
+ * - normalize paths via `normalizeArchiveEntryPath` and drop unrepresentable ones,
127
+ * - enforce `options.limits` before metadata-driven allocations,
128
+ * - throw {@link ArchiveError} for malformed, truncated, encrypted, or
129
+ * unsupported input — never a bare `Error`, and never process-fatal paths.
130
+ */
131
+ export type FormatReader = (source: ByteSource, options: FormatReadOptions) => Promise<ArchiveIndexEntry[]>;
@@ -0,0 +1,312 @@
1
+ import { bytesMatchAscii, UTF8_DECODER } from "./bytes";
2
+ import { ensureParentDirectories, upsertArchiveEntry } from "./entries";
3
+ import { ArchiveError } from "./error";
4
+ import { type ArchiveLimits, assertArchiveMemberSize, assertEntryCount, assertIndexSize } from "./limits";
5
+ import { assertArchivePathBytes, normalizeArchiveEntryPath } from "./paths";
6
+ import { type ByteSource, memoryByteSource, readMemoryRange } from "./source";
7
+ import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
8
+
9
+ const SIGNATURE = "!<arch>\n";
10
+ const HEADER_SIZE = 60;
11
+ const NAME_SIZE = 16;
12
+ const HEADER_TRAILER_OFFSET = 58;
13
+ const FILE_TYPE_MASK = 0o170000;
14
+ const DIRECTORY_TYPE = 0o040000;
15
+
16
+ interface RawArMember {
17
+ name: string;
18
+ nameByteLength: number;
19
+ dataOffset: number;
20
+ size: number;
21
+ mtimeSeconds?: number;
22
+ mode?: number;
23
+ }
24
+
25
+ class ArMemberSource implements MemberSource {
26
+ readonly #source: ByteSource;
27
+ readonly #offset: number;
28
+
29
+ constructor(source: ByteSource, offset: number) {
30
+ this.#source = source;
31
+ this.#offset = offset;
32
+ }
33
+
34
+ async read(size: number, memberPath: string): Promise<Uint8Array> {
35
+ try {
36
+ const bytes = await this.#source.read(this.#offset, this.#offset + size);
37
+ if (bytes.byteLength !== size) {
38
+ throw new ArchiveError(`Archive member '${memberPath}' is truncated`);
39
+ }
40
+ return bytes;
41
+ } catch (error) {
42
+ if (error instanceof ArchiveError) throw error;
43
+ throw new ArchiveError(error instanceof Error ? error.message : String(error));
44
+ }
45
+ }
46
+ }
47
+
48
+ function decodeAsciiField(bytes: Uint8Array, offset: number, length: number): string {
49
+ let end = offset + length;
50
+ while (end > offset && bytes[end - 1] === 0x20) end--;
51
+ let value = "";
52
+ for (let index = offset; index < end; index++) {
53
+ const byte = bytes[index]!;
54
+ if (byte < 0x20 || byte > 0x7e) throw new ArchiveError("Invalid ar archive header field");
55
+ value += String.fromCharCode(byte);
56
+ }
57
+ return value;
58
+ }
59
+
60
+ function parseOptionalNumber(value: string, radix: 8 | 10, field: string): number | undefined {
61
+ if (value === "" || value === "-1") return undefined;
62
+ const pattern = radix === 8 ? /^[0-7]+$/ : /^\d+$/;
63
+ if (!pattern.test(value)) throw new ArchiveError(`Invalid ar archive ${field}`);
64
+ const parsed = Number.parseInt(value, radix);
65
+ if (!Number.isSafeInteger(parsed)) throw new ArchiveError(`Invalid ar archive ${field}`);
66
+ return parsed;
67
+ }
68
+
69
+ function parseRequiredSize(value: string): number {
70
+ const parsed = parseOptionalNumber(value, 10, "member size");
71
+ if (parsed === undefined) throw new ArchiveError("Invalid ar archive member size");
72
+ return parsed;
73
+ }
74
+
75
+ function parseHeader(header: Uint8Array): {
76
+ rawName: string;
77
+ physicalSize: number;
78
+ mtimeSeconds?: number;
79
+ mode?: number;
80
+ bsdNameLength?: number;
81
+ } {
82
+ if (
83
+ header.byteLength !== HEADER_SIZE ||
84
+ header[HEADER_TRAILER_OFFSET] !== 0x60 ||
85
+ header[HEADER_TRAILER_OFFSET + 1] !== 0x0a
86
+ ) {
87
+ throw new ArchiveError("Invalid ar archive member header");
88
+ }
89
+ const rawName = decodeAsciiField(header, 0, NAME_SIZE);
90
+ const mtimeSeconds = parseOptionalNumber(decodeAsciiField(header, 16, 12), 10, "modification time");
91
+ parseOptionalNumber(decodeAsciiField(header, 28, 6), 10, "user id");
92
+ parseOptionalNumber(decodeAsciiField(header, 34, 6), 10, "group id");
93
+ const mode = parseOptionalNumber(decodeAsciiField(header, 40, 8), 8, "mode");
94
+ const physicalSize = parseRequiredSize(decodeAsciiField(header, 48, 10));
95
+ let bsdNameLength: number | undefined;
96
+ if (rawName.startsWith("#1/")) {
97
+ const encodedLength = rawName.slice(3);
98
+ if (!/^\d+$/.test(encodedLength)) throw new ArchiveError("Invalid ar archive BSD extended name length");
99
+ bsdNameLength = Number.parseInt(encodedLength, 10);
100
+ if (!Number.isSafeInteger(bsdNameLength) || bsdNameLength <= 0 || bsdNameLength > physicalSize) {
101
+ throw new ArchiveError("Invalid ar archive BSD extended name length");
102
+ }
103
+ }
104
+ return { rawName, physicalSize, mtimeSeconds, mode, bsdNameLength };
105
+ }
106
+
107
+ function decodeName(bytes: Uint8Array, limits: ArchiveLimits): string {
108
+ assertArchivePathBytes(bytes.byteLength, "member path", limits.maxPathBytes);
109
+ return UTF8_DECODER.decode(bytes);
110
+ }
111
+
112
+ function decodeBsdName(bytes: Uint8Array, limits: ArchiveLimits): { name: string; byteLength: number } {
113
+ const nul = bytes.indexOf(0);
114
+ const nameBytes = nul >= 0 ? bytes.subarray(0, nul) : bytes;
115
+ if (nameBytes.byteLength === 0) throw new ArchiveError("Invalid ar archive empty BSD extended name");
116
+ return { name: decodeName(nameBytes, limits), byteLength: nameBytes.byteLength };
117
+ }
118
+
119
+ function shortName(rawName: string): string {
120
+ if (rawName === "/" || rawName === "//" || rawName === "/SYM64/") return rawName;
121
+ return rawName.endsWith("/") ? rawName.slice(0, -1) : rawName;
122
+ }
123
+
124
+ function resolveLongName(
125
+ reference: string,
126
+ table: Uint8Array,
127
+ limits: ArchiveLimits,
128
+ ): { name: string; byteLength: number } {
129
+ const offsetText = reference.slice(1);
130
+ if (!/^\d+$/.test(offsetText)) throw new ArchiveError(`Invalid ar archive member name '${reference}'`);
131
+ const offset = Number.parseInt(offsetText, 10);
132
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset >= table.byteLength) {
133
+ throw new ArchiveError(`Invalid ar archive long-name offset '${reference}'`);
134
+ }
135
+ let end = offset;
136
+ while (end < table.byteLength && table[end] !== 0 && table[end] !== 0x0a) end++;
137
+ if (end === table.byteLength) throw new ArchiveError(`Unterminated ar archive long name at offset ${offset}`);
138
+ let nameEnd = end;
139
+ if (table[end] === 0x0a && nameEnd > offset && table[nameEnd - 1] === 0x2f) nameEnd--;
140
+ if (nameEnd === offset) throw new ArchiveError(`Empty ar archive long name at offset ${offset}`);
141
+ const nameBytes = table.subarray(offset, nameEnd);
142
+ return { name: decodeName(nameBytes, limits), byteLength: nameBytes.byteLength };
143
+ }
144
+
145
+ function isMetadataName(name: string): boolean {
146
+ return name === "/" || name === "//" || name === "/SYM64/" || name === "__.SYMDEF" || name === "__.SYMDEF SORTED";
147
+ }
148
+
149
+ function materializeEntries(
150
+ records: RawArMember[],
151
+ longNames: Uint8Array | undefined,
152
+ source: ByteSource,
153
+ options: FormatReadOptions,
154
+ ): ArchiveIndexEntry[] {
155
+ const entries = new Map<string, ArchiveIndexEntry>();
156
+ for (const record of records) {
157
+ let name = record.name;
158
+ let nameByteLength = record.nameByteLength;
159
+ if (/^\/\d+$/.test(name)) {
160
+ if (!longNames) throw new ArchiveError(`Ar archive member '${name}' references a missing long-name table`);
161
+ const resolved = resolveLongName(name, longNames, options.limits);
162
+ name = resolved.name;
163
+ nameByteLength = resolved.byteLength;
164
+ } else {
165
+ name = shortName(name);
166
+ }
167
+ if (isMetadataName(name)) continue;
168
+ assertArchivePathBytes(nameByteLength, "member path", options.limits.maxPathBytes);
169
+ assertArchiveMemberSize(record.size, name, options.limits);
170
+ const path = normalizeArchiveEntryPath(name);
171
+ if (!path) continue;
172
+ const isDirectory = record.mode !== undefined && (record.mode & FILE_TYPE_MASK) === DIRECTORY_TYPE;
173
+ const entry: ArchiveIndexEntry = {
174
+ path,
175
+ isDirectory,
176
+ size: isDirectory ? 0 : record.size,
177
+ ...(record.mtimeSeconds !== undefined ? { mtimeMs: record.mtimeSeconds * 1000 } : {}),
178
+ ...(record.mode !== undefined ? { mode: record.mode } : {}),
179
+ ...(!isDirectory
180
+ ? { storage: { type: "member" as const, source: new ArMemberSource(source, record.dataOffset) } }
181
+ : {}),
182
+ };
183
+ upsertArchiveEntry(entries, entry);
184
+ assertEntryCount(entries.size, options.limits);
185
+ }
186
+ ensureParentDirectories(entries, options.limits);
187
+ return [...entries.values()];
188
+ }
189
+
190
+ function readSignatureFromBuffer(bytes: Uint8Array): void {
191
+ if (!sniffUnixAr(bytes)) throw new ArchiveError("Invalid ar archive signature");
192
+ }
193
+
194
+ /** Parse a fully materialized Unix ar archive for composition by formats such as deb. */
195
+ export function readUnixArEntriesFromBuffer(bytes: Uint8Array, options: FormatReadOptions): ArchiveIndexEntry[] {
196
+ readSignatureFromBuffer(bytes);
197
+ const records: RawArMember[] = [];
198
+ let longNames: Uint8Array | undefined;
199
+ let metadataSize = 0;
200
+ for (let position = SIGNATURE.length; position < bytes.byteLength; ) {
201
+ if (bytes.byteLength - position < HEADER_SIZE)
202
+ throw new ArchiveError("Invalid ar archive: truncated member header");
203
+ const header = parseHeader(readMemoryRange(bytes, position, position + HEADER_SIZE));
204
+ metadataSize += HEADER_SIZE;
205
+ assertIndexSize(metadataSize, options.limits, "index");
206
+ const payloadOffset = position + HEADER_SIZE;
207
+ const payloadEnd = payloadOffset + header.physicalSize;
208
+ if (!Number.isSafeInteger(payloadEnd) || payloadEnd > bytes.byteLength) {
209
+ throw new ArchiveError("Invalid ar archive: truncated member data");
210
+ }
211
+ let name = header.rawName;
212
+ let nameByteLength = Buffer.byteLength(name, "utf-8");
213
+ let dataOffset = payloadOffset;
214
+ let size = header.physicalSize;
215
+ if (header.bsdNameLength !== undefined) {
216
+ metadataSize += header.bsdNameLength;
217
+ assertIndexSize(metadataSize, options.limits, "index");
218
+ const nameBytes = readMemoryRange(bytes, payloadOffset, payloadOffset + header.bsdNameLength);
219
+ const decoded = decodeBsdName(nameBytes, options.limits);
220
+ name = decoded.name;
221
+ nameByteLength = decoded.byteLength;
222
+ dataOffset += header.bsdNameLength;
223
+ size -= header.bsdNameLength;
224
+ } else if (header.rawName === "//") {
225
+ metadataSize += header.physicalSize;
226
+ assertIndexSize(metadataSize, options.limits, "index");
227
+ longNames = readMemoryRange(bytes, payloadOffset, payloadEnd);
228
+ }
229
+ records.push({ name, nameByteLength, dataOffset, size, mtimeSeconds: header.mtimeSeconds, mode: header.mode });
230
+ assertEntryCount(records.length, options.limits);
231
+ position = payloadEnd + (header.physicalSize & 1);
232
+ if (position > bytes.byteLength) throw new ArchiveError("Invalid ar archive: missing alignment byte");
233
+ }
234
+ return materializeEntries(records, longNames, memoryByteSource(bytes), options);
235
+ }
236
+
237
+ async function readExact(source: ByteSource, start: number, end: number, what: string): Promise<Uint8Array> {
238
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > source.size) {
239
+ throw new ArchiveError(`Invalid ar archive: truncated ${what}`);
240
+ }
241
+ try {
242
+ const bytes = await source.read(start, end);
243
+ if (bytes.byteLength !== end - start) throw new ArchiveError(`Invalid ar archive: truncated ${what}`);
244
+ return bytes;
245
+ } catch (error) {
246
+ if (error instanceof ArchiveError) throw error;
247
+ throw new ArchiveError(error instanceof Error ? error.message : String(error));
248
+ }
249
+ }
250
+
251
+ async function readUnixArImpl(source: ByteSource, options: FormatReadOptions): Promise<ArchiveIndexEntry[]> {
252
+ if (!Number.isSafeInteger(source.size) || source.size < SIGNATURE.length)
253
+ throw new ArchiveError("Invalid ar archive signature");
254
+ readSignatureFromBuffer(await readExact(source, 0, SIGNATURE.length, "signature"));
255
+ const records: RawArMember[] = [];
256
+ let longNames: Uint8Array | undefined;
257
+ let metadataSize = 0;
258
+ for (let position = SIGNATURE.length; position < source.size; ) {
259
+ const headerBytes = await readExact(source, position, position + HEADER_SIZE, "member header");
260
+ const header = parseHeader(headerBytes);
261
+ metadataSize += HEADER_SIZE;
262
+ assertIndexSize(metadataSize, options.limits, "index");
263
+ const payloadOffset = position + HEADER_SIZE;
264
+ const payloadEnd = payloadOffset + header.physicalSize;
265
+ if (!Number.isSafeInteger(payloadEnd) || payloadEnd > source.size) {
266
+ throw new ArchiveError("Invalid ar archive: truncated member data");
267
+ }
268
+ let name = header.rawName;
269
+ let nameByteLength = Buffer.byteLength(name, "utf-8");
270
+ let dataOffset = payloadOffset;
271
+ let size = header.physicalSize;
272
+ if (header.bsdNameLength !== undefined) {
273
+ metadataSize += header.bsdNameLength;
274
+ assertIndexSize(metadataSize, options.limits, "index");
275
+ const nameBytes = await readExact(
276
+ source,
277
+ payloadOffset,
278
+ payloadOffset + header.bsdNameLength,
279
+ "BSD member name",
280
+ );
281
+ const decoded = decodeBsdName(nameBytes, options.limits);
282
+ name = decoded.name;
283
+ nameByteLength = decoded.byteLength;
284
+ dataOffset += header.bsdNameLength;
285
+ size -= header.bsdNameLength;
286
+ } else if (header.rawName === "//") {
287
+ metadataSize += header.physicalSize;
288
+ assertIndexSize(metadataSize, options.limits, "index");
289
+ longNames = await readExact(source, payloadOffset, payloadEnd, "long-name table");
290
+ }
291
+ records.push({ name, nameByteLength, dataOffset, size, mtimeSeconds: header.mtimeSeconds, mode: header.mode });
292
+ assertEntryCount(records.length, options.limits);
293
+ position = payloadEnd + (header.physicalSize & 1);
294
+ if (position > source.size) throw new ArchiveError("Invalid ar archive: missing alignment byte");
295
+ }
296
+ return materializeEntries(records, longNames, source, options);
297
+ }
298
+
299
+ /** Read a Unix ar, static-library, or COFF import-library container. */
300
+ export const readUnixAr: FormatReader = async (source, options) => {
301
+ try {
302
+ return await readUnixArImpl(source, options);
303
+ } catch (error) {
304
+ if (error instanceof ArchiveError) throw error;
305
+ throw new ArchiveError(error instanceof Error ? error.message : String(error));
306
+ }
307
+ };
308
+
309
+ /** Detect the Unix ar global header. */
310
+ export function sniffUnixAr(bytes: Uint8Array): boolean {
311
+ return bytesMatchAscii(bytes, 0, SIGNATURE);
312
+ }
@@ -0,0 +1,55 @@
1
+ import { encodeAsar } from "./asar";
2
+ import { gzipCompress } from "./codecs/gzip";
3
+ import { zstdCompress } from "./codecs/zstd";
4
+ import { memberContentToBytes } from "./open";
5
+ import { encodeTar } from "./tar";
6
+ import type { ArchiveFormat, ArchiveMemberContent, WritableArchiveFormat } from "./types";
7
+ import { encodeZip } from "./zip";
8
+
9
+ const WRITABLE_FORMATS: Record<WritableArchiveFormat, true> = {
10
+ zip: true,
11
+ tar: true,
12
+ "tar.gz": true,
13
+ "tar.zst": true,
14
+ asar: true,
15
+ };
16
+
17
+ /** Whether `format` can be serialized by {@link writeArchive} (rest are read-only). */
18
+ export function isWritableArchiveFormat(format: ArchiveFormat): format is WritableArchiveFormat {
19
+ return format in WRITABLE_FORMATS;
20
+ }
21
+
22
+ /**
23
+ * Serialize `entries` into an archive of `format` in memory. String members
24
+ * are encoded as UTF-8; member names are normalized to forward slashes.
25
+ */
26
+ export async function encodeArchive(
27
+ format: WritableArchiveFormat,
28
+ entries: Iterable<readonly [string, ArchiveMemberContent]>,
29
+ ): Promise<Uint8Array> {
30
+ const members: (readonly [string, Uint8Array])[] = [];
31
+ for (const [name, content] of entries) {
32
+ members.push([name.replace(/\\/g, "/"), await memberContentToBytes(content)] as const);
33
+ }
34
+ switch (format) {
35
+ case "zip":
36
+ return encodeZip(members);
37
+ case "asar":
38
+ return encodeAsar(members);
39
+ case "tar":
40
+ return encodeTar(members);
41
+ case "tar.gz":
42
+ return gzipCompress(await encodeTar(members));
43
+ case "tar.zst":
44
+ return zstdCompress(await encodeTar(members));
45
+ }
46
+ }
47
+
48
+ /** {@link encodeArchive}, written to `destPath` (parent directories auto-created). */
49
+ export async function writeArchive(
50
+ destPath: string,
51
+ format: WritableArchiveFormat,
52
+ entries: Iterable<readonly [string, ArchiveMemberContent]>,
53
+ ): Promise<void> {
54
+ await Bun.write(destPath, await encodeArchive(format, entries));
55
+ }