@patrickdk77/archive-defuse 0.0.0-stage → 0.1.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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +345 -2
  3. package/dist/aes7z.d.ts +16 -0
  4. package/dist/aes7z.js +131 -0
  5. package/dist/alone.d.ts +21 -0
  6. package/dist/alone.js +65 -0
  7. package/dist/bcj2.d.ts +21 -0
  8. package/dist/bcj2.js +140 -0
  9. package/dist/bzip2.d.ts +21 -0
  10. package/dist/bzip2.js +688 -0
  11. package/dist/cab.d.ts +63 -0
  12. package/dist/cab.js +493 -0
  13. package/dist/checks.d.ts +23 -0
  14. package/dist/checks.js +123 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +364 -0
  17. package/dist/codecs.d.ts +39 -0
  18. package/dist/codecs.js +124 -0
  19. package/dist/coders.d.ts +58 -0
  20. package/dist/coders.js +392 -0
  21. package/dist/crc32.d.ts +8 -0
  22. package/dist/crc32.js +59 -0
  23. package/dist/encoder.d.ts +44 -0
  24. package/dist/encoder.js +114 -0
  25. package/dist/engine.d.ts +44 -0
  26. package/dist/engine.js +753 -0
  27. package/dist/errors.d.ts +64 -0
  28. package/dist/errors.js +98 -0
  29. package/dist/filters.d.ts +34 -0
  30. package/dist/filters.js +385 -0
  31. package/dist/findings.d.ts +26 -0
  32. package/dist/findings.js +176 -0
  33. package/dist/formats.d.ts +14 -0
  34. package/dist/formats.js +97 -0
  35. package/dist/index.d.ts +8 -0
  36. package/dist/index.js +50 -0
  37. package/dist/index.mjs +3 -0
  38. package/dist/inflate64.d.ts +6 -0
  39. package/dist/inflate64.js +388 -0
  40. package/dist/io.d.ts +78 -0
  41. package/dist/io.js +379 -0
  42. package/dist/lzma-formats.d.ts +75 -0
  43. package/dist/lzma-formats.js +373 -0
  44. package/dist/lzma-sniff.d.ts +20 -0
  45. package/dist/lzma-sniff.js +73 -0
  46. package/dist/lzma-writers.d.ts +35 -0
  47. package/dist/lzma-writers.js +276 -0
  48. package/dist/lzma1/LICENSE +201 -0
  49. package/dist/lzma1/NOTICE +36 -0
  50. package/dist/lzma1/crc32.d.ts +4 -0
  51. package/dist/lzma1/crc32.js +19 -0
  52. package/dist/lzma1/hash234.d.ts +22 -0
  53. package/dist/lzma1/hash234.js +75 -0
  54. package/dist/lzma1/hc4.d.ts +30 -0
  55. package/dist/lzma1/hc4.js +142 -0
  56. package/dist/lzma1/header.d.ts +32 -0
  57. package/dist/lzma1/header.js +73 -0
  58. package/dist/lzma1/lz-decoder.d.ts +41 -0
  59. package/dist/lzma1/lz-decoder.js +128 -0
  60. package/dist/lzma1/lz-encoder.d.ts +87 -0
  61. package/dist/lzma1/lz-encoder.js +166 -0
  62. package/dist/lzma1/lzma-coder.d.ts +107 -0
  63. package/dist/lzma1/lzma-coder.js +162 -0
  64. package/dist/lzma1/lzma-decoder.d.ts +36 -0
  65. package/dist/lzma1/lzma-decoder.js +484 -0
  66. package/dist/lzma1/lzma-encoder-fast.d.ts +13 -0
  67. package/dist/lzma1/lzma-encoder-fast.js +114 -0
  68. package/dist/lzma1/lzma-encoder.d.ts +105 -0
  69. package/dist/lzma1/lzma-encoder.js +497 -0
  70. package/dist/lzma1/lzma2-decoder.d.ts +85 -0
  71. package/dist/lzma1/lzma2-decoder.js +249 -0
  72. package/dist/lzma1/range-coder.d.ts +18 -0
  73. package/dist/lzma1/range-coder.js +23 -0
  74. package/dist/lzma1/range-decoder.d.ts +41 -0
  75. package/dist/lzma1/range-decoder.js +78 -0
  76. package/dist/lzma1/range-encoder.d.ts +53 -0
  77. package/dist/lzma1/range-encoder.js +269 -0
  78. package/dist/lzma1-stream.d.ts +60 -0
  79. package/dist/lzma1-stream.js +202 -0
  80. package/dist/model.d.ts +141 -0
  81. package/dist/model.js +16 -0
  82. package/dist/names.d.ts +13 -0
  83. package/dist/names.js +66 -0
  84. package/dist/pack.d.ts +72 -0
  85. package/dist/pack.js +514 -0
  86. package/dist/plugins.d.ts +13 -0
  87. package/dist/plugins.js +55 -0
  88. package/dist/rules.d.ts +25 -0
  89. package/dist/rules.js +403 -0
  90. package/dist/score.d.ts +17 -0
  91. package/dist/score.js +58 -0
  92. package/dist/seven.d.ts +75 -0
  93. package/dist/seven.js +492 -0
  94. package/dist/sevenzip.d.ts +133 -0
  95. package/dist/sevenzip.js +529 -0
  96. package/dist/single.d.ts +103 -0
  97. package/dist/single.js +453 -0
  98. package/dist/sniff.d.ts +20 -0
  99. package/dist/sniff.js +224 -0
  100. package/dist/stream-meter.d.ts +39 -0
  101. package/dist/stream-meter.js +54 -0
  102. package/dist/tar.d.ts +102 -0
  103. package/dist/tar.js +765 -0
  104. package/dist/types.d.ts +284 -0
  105. package/dist/types.js +133 -0
  106. package/dist/unpack.d.ts +67 -0
  107. package/dist/unpack.js +194 -0
  108. package/dist/xz.d.ts +25 -0
  109. package/dist/xz.js +333 -0
  110. package/dist/zip-lzma.d.ts +15 -0
  111. package/dist/zip-lzma.js +137 -0
  112. package/dist/zip-writer.d.ts +41 -0
  113. package/dist/zip-writer.js +263 -0
  114. package/dist/zip.d.ts +99 -0
  115. package/dist/zip.js +901 -0
  116. package/dist/zipcrypt.d.ts +47 -0
  117. package/dist/zipcrypt.js +233 -0
  118. package/dist/zstd.d.ts +25 -0
  119. package/dist/zstd.js +160 -0
  120. package/package.json +94 -4
package/dist/tar.js ADDED
@@ -0,0 +1,765 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isTar = exports.asciiFallback = exports.TarArchive = exports.RECORD = exports.BLOCK = void 0;
4
+ exports.numeric = numeric;
5
+ exports.parsePax = parsePax;
6
+ exports.paxRecord = paxRecord;
7
+ exports.header = header;
8
+ exports.split = split;
9
+ // tar archives: v7, ustar, star, GNU (long names, long links, old sparse files) and POSIX pax (extended and global
10
+ // headers, sparse files 0.0, 0.1 and 1.0). The writer writes ustar headers, with pax records for what they cannot hold.
11
+ const errors_1 = require("./errors");
12
+ const io_1 = require("./io");
13
+ const model_1 = require("./model");
14
+ const names_1 = require("./names");
15
+ const sniff_1 = require("./sniff");
16
+ const types_1 = require("./types");
17
+ exports.BLOCK = 512;
18
+ /** GNU tar's default record: the archive is padded with zeros to a multiple of it. */
19
+ exports.RECORD = 20 * exports.BLOCK;
20
+ /** The most a pax header, a long name or a sparse map may hold. */
21
+ const MAX_META = 8 * 1024 * 1024;
22
+ /** The most regions a sparse map may list. */
23
+ const MAX_REGIONS = 500_000;
24
+ /** Further headers after the end of the archive are searched for up to this many bytes. */
25
+ const HIDDEN_SCAN = 64 * 1024 * 1024;
26
+ const cstr = (b, at, length) => {
27
+ const end = b.subarray(at, at + length).indexOf(0);
28
+ return b.subarray(at, end < 0 ? at + length : at + end);
29
+ };
30
+ /**
31
+ * A numeric field: octal digits after any spaces or NULs, up to a space or NUL, or GNU's base-256 with the top bit of
32
+ * the first byte set. Undefined when the field holds anything else or a value beyond a safe integer.
33
+ */
34
+ function numeric(b, at, length) {
35
+ if (b[at] & 0x80) {
36
+ // Base-256, two's complement, with the top bit as the marker. 0xff marks a negative value.
37
+ const negative = b[at] === 0xff;
38
+ let v = negative ? -1n : 0n;
39
+ for (let i = 0; i < length; i++)
40
+ v = (v << 8n) | BigInt(i === 0 ? (negative ? 0xff : b[at] & 0x7f) : b[at + i]);
41
+ if (v > BigInt(Number.MAX_SAFE_INTEGER) || v < BigInt(Number.MIN_SAFE_INTEGER))
42
+ return undefined;
43
+ return Number(v);
44
+ }
45
+ let i = at;
46
+ const end = at + length;
47
+ while (i < end && (b[i] === 0x20 || b[i] === 0))
48
+ i++;
49
+ let v = 0;
50
+ let digits = 0;
51
+ for (; i < end && b[i] >= 0x30 && b[i] <= 0x37; i++, digits++) {
52
+ v = v * 8 + (b[i] - 0x30);
53
+ if (v > Number.MAX_SAFE_INTEGER)
54
+ return undefined;
55
+ }
56
+ for (; i < end; i++)
57
+ if (b[i] !== 0x20 && b[i] !== 0)
58
+ return undefined;
59
+ return digits === 0 ? 0 : v;
60
+ }
61
+ /** Parses pax records, "<length> <key>=<value>\n". Returns them in order, with the reason for any fault that stopped it. */
62
+ function parsePax(data) {
63
+ const records = [];
64
+ let p = 0;
65
+ while (p < data.length) {
66
+ // A trailing run of NULs pads the last block.
67
+ if (data[p] === 0)
68
+ break;
69
+ const space = data.indexOf(0x20, p);
70
+ if (space < 0 || space - p > 20)
71
+ return { records, malformed: 'a pax record has no length' };
72
+ const lenText = Buffer.from(data.subarray(p, space)).toString('latin1');
73
+ if (!/^[1-9][0-9]*$/.test(lenText))
74
+ return { records, malformed: 'a pax record has no length' };
75
+ const len = Number(lenText);
76
+ if (p + len > data.length || data[p + len - 1] !== 0x0a)
77
+ return { records, malformed: 'a pax record runs past its header' };
78
+ const body = data.subarray(space + 1, p + len - 1);
79
+ const eq = body.indexOf(0x3d);
80
+ if (eq <= 0)
81
+ return { records, malformed: 'a pax record has no key' };
82
+ const key = (0, names_1.utf8OrUndefined)(body.subarray(0, eq));
83
+ if (key === undefined)
84
+ return { records, malformed: 'a pax key is not UTF-8' };
85
+ records.push({ key, value: Buffer.from(body.subarray(eq + 1)) });
86
+ p += len;
87
+ }
88
+ return { records };
89
+ }
90
+ /** Keys the reader turns into header fields. Every other record is kept as it is. */
91
+ const STRUCTURAL = new Set(['path', 'linkpath', 'size', 'uid', 'gid', 'uname', 'gname', 'mtime', 'hdrcharset']);
92
+ const isStructural = (key) => STRUCTURAL.has(key) || key.startsWith('GNU.sparse.');
93
+ const CAPABILITY = /^(SCHILY|LIBARCHIVE)\.xattr\.security\.capability$/;
94
+ /** A name or a link target from bytes: UTF-8 when valid, and Latin-1 otherwise, which no reader agrees on. */
95
+ function text(bytes, notes, name) {
96
+ const u = (0, names_1.utf8OrUndefined)(bytes);
97
+ if (u !== undefined)
98
+ return u;
99
+ notes.push({ category: types_1.ArchiveCategory.Name, detail: types_1.ArchiveDetail.Encoding, data: { name: name ?? Buffer.from(bytes).toString('latin1') } });
100
+ return Buffer.from(bytes).toString('latin1');
101
+ }
102
+ /** True for a name that leaves the extraction folder: absolute, with a drive letter, or with a ".." part. */
103
+ const unsafePath = (p) => /^[/\\]/.test(p) || /^[a-zA-Z]:/.test(p) || p.split(/[/\\]/).includes('..');
104
+ const kinds = { '0': 'file', '\0': 'file', '7': 'file', '1': 'hardlink', '2': 'symlink', '3': 'device', '4': 'device', '5': 'directory', '6': 'fifo', D: 'directory' };
105
+ /** A tar archive read header by header. */
106
+ class TarArchive {
107
+ reader;
108
+ entries;
109
+ notes;
110
+ end;
111
+ trailingStart;
112
+ label;
113
+ format = 'tar';
114
+ single = false;
115
+ lateNotes = [];
116
+ comment;
117
+ constructor(reader, entries, notes,
118
+ /** Where the end-of-archive blocks start, or the archive's data ends. */
119
+ end,
120
+ /** Where the data after the archive starts. */
121
+ trailingStart,
122
+ /** The volume label, from a GNU 'V' header. */
123
+ label) {
124
+ this.reader = reader;
125
+ this.entries = entries;
126
+ this.notes = notes;
127
+ this.end = end;
128
+ this.trailingStart = trailingStart;
129
+ this.label = label;
130
+ }
131
+ static async open(source, limits, deadline) {
132
+ const size = await source.size();
133
+ const reader = new io_1.Reader(source, size);
134
+ const notes = [];
135
+ const entries = [];
136
+ let global = [];
137
+ let pending = { pax: [] };
138
+ let pos = 0;
139
+ let total = 0;
140
+ let comment;
141
+ let label;
142
+ let end = -1;
143
+ let trailingStart = size;
144
+ let headers = 0;
145
+ const meta = async (at, length, what) => {
146
+ if (length > MAX_META)
147
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Unparseable, `A ${what} is too large`, { reason: `a ${what} of ${length} bytes` });
148
+ if (at + length > size)
149
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Truncated, 'The archive ends inside a header', { what: what });
150
+ return Buffer.from(await reader.read(at, length));
151
+ };
152
+ for (;;) {
153
+ if (headers++ % 256 === 0)
154
+ (0, errors_1.checkTime)(deadline);
155
+ if (pos + exports.BLOCK > size) {
156
+ // The archive ends without its two zero blocks.
157
+ if (entries.length || pending.pax.length || pending.longName)
158
+ notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: 'the archive has no end-of-archive blocks' } });
159
+ end = Math.min(pos, size);
160
+ trailingStart = end;
161
+ break;
162
+ }
163
+ const block = await reader.read(pos, exports.BLOCK);
164
+ if (block.every(b => b === 0)) {
165
+ end = pos;
166
+ // Two zero blocks end the archive. Readers that stop at the first one and readers that go on past a lone one
167
+ // see different archives, so what follows the first is data after the archive either way.
168
+ const next = pos + exports.BLOCK + exports.BLOCK <= size ? await reader.read(pos + exports.BLOCK, exports.BLOCK) : undefined;
169
+ trailingStart = next?.every(b => b === 0) ? pos + 2 * exports.BLOCK : Math.min(size, pos + exports.BLOCK);
170
+ break;
171
+ }
172
+ if (!(0, sniff_1.isTarHeader)(block)) {
173
+ if (pos === 0)
174
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Unparseable, 'Not a tar archive', { reason: 'no tar header at the start' });
175
+ // A damaged header with more headers after it hides entries from some readers and not others.
176
+ if (await TarArchive.headerAfter(reader, pos + exports.BLOCK, deadline))
177
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Unparseable, 'A damaged header', { reason: 'a damaged header in the middle of the archive' });
178
+ notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: 'the archive has no end-of-archive blocks' } });
179
+ end = pos;
180
+ trailingStart = pos;
181
+ break;
182
+ }
183
+ const typeflag = String.fromCharCode(block[156]);
184
+ const ustar = Buffer.from(block.subarray(257, 263)).toString('latin1');
185
+ const posix = ustar === 'ustar\0';
186
+ const star = posix && Buffer.from(block.subarray(508, 512)).toString('latin1') === 'tar\0';
187
+ const entryNotes = [];
188
+ const num = (at, length, field) => {
189
+ const v = numeric(block, at, length);
190
+ if (v === undefined)
191
+ entryNotes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: `a ${field} field that is not a number` } });
192
+ return v ?? 0;
193
+ };
194
+ const sizeField = numeric(block, 124, 12);
195
+ if (sizeField === undefined || sizeField < 0)
196
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Unparseable, 'A header has no valid size', { reason: 'a header without a valid size' });
197
+ const dataOffset = pos + exports.BLOCK;
198
+ // Extended headers and long names describe the next header.
199
+ if (typeflag === 'x' || typeflag === 'X' || typeflag === 'g') {
200
+ const parsed = parsePax(await meta(dataOffset, sizeField, 'pax header'));
201
+ if (parsed.malformed)
202
+ notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: parsed.malformed } });
203
+ if (typeflag === 'g') {
204
+ // Later global records replace earlier ones of the same key.
205
+ const keys = new Set(parsed.records.map(r => r.key));
206
+ global = [...global.filter(r => !keys.has(r.key)), ...parsed.records];
207
+ const c = parsed.records.find(r => r.key === 'comment');
208
+ if (c)
209
+ comment = c.value.toString('utf8');
210
+ }
211
+ else
212
+ pending.pax.push(...parsed.records);
213
+ pos = dataOffset + Math.ceil(sizeField / exports.BLOCK) * exports.BLOCK;
214
+ continue;
215
+ }
216
+ if (typeflag === 'L' || typeflag === 'K') {
217
+ const value = cstr(await meta(dataOffset, sizeField, 'long name'), 0, sizeField);
218
+ if (typeflag === 'L')
219
+ pending.longName = value;
220
+ else
221
+ pending.longLink = value;
222
+ pos = dataOffset + Math.ceil(sizeField / exports.BLOCK) * exports.BLOCK;
223
+ continue;
224
+ }
225
+ if (typeflag === 'M')
226
+ throw new errors_1.StopError(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.MultiVolume, 'A GNU multi-volume continuation');
227
+ if (typeflag === 'V') {
228
+ label = Buffer.from(cstr(block, 0, 100)).toString('latin1');
229
+ pos = dataOffset + Math.ceil(sizeField / exports.BLOCK) * exports.BLOCK;
230
+ continue;
231
+ }
232
+ // The records that apply: global ones, then this entry's own, the later winning.
233
+ const records = [...global.filter(g => !pending.pax.some(r => r.key === g.key)), ...pending.pax];
234
+ const last = (key) => {
235
+ let found;
236
+ for (const r of records)
237
+ if (r.key === key)
238
+ found = r;
239
+ return found;
240
+ };
241
+ const binary = last('hdrcharset')?.value.toString('latin1') === 'BINARY';
242
+ const paxText = (key) => {
243
+ const r = last(key);
244
+ if (r === undefined)
245
+ return undefined;
246
+ // GNU tar, bsdtar and Python cut a pax value at its first NUL, so this reader does as well, and says so.
247
+ const nul = r.value.indexOf(0);
248
+ if (nul >= 0)
249
+ entryNotes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: `the pax ${key} holds a NUL byte, which readers cut at or keep` } });
250
+ const value = nul >= 0 ? r.value.subarray(0, nul) : r.value;
251
+ return binary ? text(value, entryNotes) : value.toString('utf8');
252
+ };
253
+ const paxNumber = (key) => {
254
+ const v = paxText(key);
255
+ if (v === undefined)
256
+ return undefined;
257
+ if (!/^-?[0-9]+(\.[0-9]*)?$/.test(v)) {
258
+ entryNotes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: `a pax ${key} that is not a number` } });
259
+ return undefined;
260
+ }
261
+ const n = Math.floor(Number(v));
262
+ return Number.isSafeInteger(n) ? n : undefined;
263
+ };
264
+ const owner = (v, field) => {
265
+ if (v >= 0)
266
+ return v;
267
+ entryNotes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: `a negative ${field}` } });
268
+ return 0;
269
+ };
270
+ const headerName = cstr(block, 0, 100);
271
+ const prefix = posix ? cstr(block, 345, star ? 131 : 155) : new Uint8Array(0);
272
+ const fallbackName = prefix.length ? Buffer.concat([prefix, Buffer.from('/'), headerName]) : Buffer.from(headerName);
273
+ const plainName = pending.longName ?? fallbackName;
274
+ let name = paxText('path') ?? text(plainName, entryNotes);
275
+ const linkBytes = pending.longLink ?? cstr(block, 157, 100);
276
+ const linkTarget = paxText('linkpath') ?? text(linkBytes, entryNotes);
277
+ // Readers without pax or GNU support take the names in the header itself.
278
+ const plain = text(fallbackName, []);
279
+ if (plain !== name && unsafePath(plain) && !unsafePath(name)) {
280
+ entryNotes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HeaderMismatch, data: { name, what: `readers without pax or GNU long names see the name ${plain}` } });
281
+ }
282
+ const paxSize = paxNumber('size');
283
+ // The header's own size field holds the size too unless it cannot: readers without pax support go by it, and
284
+ // a size that differs from the pax record's makes them read the archive another way.
285
+ const fieldCannotHold = paxSize !== undefined && paxSize > 0o77777777777 && (sizeField === 0 || sizeField === 0o77777777777 || (block[124] & 0x80) !== 0);
286
+ if (paxSize !== undefined && paxSize !== sizeField && !fieldCannotHold) {
287
+ notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HeaderMismatch, data: { name, what: 'the pax size and the header size differ' } });
288
+ }
289
+ const stored = paxSize ?? sizeField;
290
+ if (stored < 0)
291
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Unparseable, 'A negative size', { reason: 'a negative pax size' });
292
+ let kind = kinds[typeflag] ?? 'other';
293
+ // A v7 header marks a directory by the slash alone.
294
+ if (kind === 'file' && (typeflag === '\0' || typeflag === '0') && name.endsWith('/'))
295
+ kind = 'directory';
296
+ if (kind === 'directory' && !name.endsWith('/'))
297
+ name += '/';
298
+ const e = {
299
+ index: entries.length,
300
+ name,
301
+ kind,
302
+ mode: num(100, 8, 'mode') & 0o7777,
303
+ size: kind === 'file' || kind === 'other' ? stored : 0,
304
+ compressedSize: stored,
305
+ notes: entryNotes,
306
+ headerOffset: pos,
307
+ dataOffset,
308
+ stored,
309
+ typeflag,
310
+ uid: owner(paxNumber('uid') ?? num(108, 8, 'uid'), 'uid'),
311
+ gid: owner(paxNumber('gid') ?? num(116, 8, 'gid'), 'gid'),
312
+ uname: paxText('uname') ?? Buffer.from(cstr(block, 265, 32)).toString('latin1'),
313
+ gname: paxText('gname') ?? Buffer.from(cstr(block, 297, 32)).toString('latin1'),
314
+ mtime: paxNumber('mtime') ?? num(136, 12, 'mtime'),
315
+ devmajor: posix || ustar === 'ustar ' ? num(329, 8, 'devmajor') : 0,
316
+ devminor: posix || ustar === 'ustar ' ? num(337, 8, 'devminor') : 0,
317
+ // Global records apply to every entry after them, so each entry keeps its own copy. A global comment is the archive's.
318
+ pax: records.filter(r => !isStructural(r.key) && !(r.key === 'comment' && global.includes(r))),
319
+ };
320
+ if (kind === 'symlink' || kind === 'hardlink')
321
+ e.linkTarget = linkTarget;
322
+ if (kind === 'other')
323
+ e.typeCode = `tar type ${JSON.stringify(typeflag)}`;
324
+ if (kind === 'device')
325
+ e.typeCode = typeflag === '3' ? 'character device' : 'block device';
326
+ const ownComment = pending.pax.find(r => r.key === 'comment');
327
+ if (ownComment)
328
+ e.comment = ownComment.value.toString('utf8');
329
+ const caps = e.pax.filter(r => CAPABILITY.test(r.key)).map(r => r.key);
330
+ if (caps.length)
331
+ e.capabilities = caps;
332
+ if (typeflag === 'S' || records.some(r => r.key.startsWith('GNU.sparse.')))
333
+ await TarArchive.sparse(reader, e, block, records, paxText, pos);
334
+ if (e.dataOffset + stored > size)
335
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Truncated, 'The archive ends inside an entry', { what: 'entry' }, e.name);
336
+ if (kind !== 'file' && kind !== 'other' && stored > 0)
337
+ entryNotes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.NonFileData, data: { name, bytes: stored } });
338
+ if (entries.length >= limits.entries)
339
+ throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.EntryCount, 'Too many entries', { limit: limits.entries });
340
+ if (e.size > limits.decompressedBytes)
341
+ throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DecompressedSize, `${name} has ${e.size} bytes`, { size: e.size, limit: limits.decompressedBytes }, name);
342
+ total += e.size;
343
+ if (total > limits.totalDecompressedBytes)
344
+ throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.TotalDecompressedSize, 'The entries hold too many bytes', { limit: limits.totalDecompressedBytes });
345
+ entries.push(e);
346
+ pending = { pax: [] };
347
+ pos = e.dataOffset + Math.ceil(stored / exports.BLOCK) * exports.BLOCK;
348
+ // The last block of an entry's data may be cut short at the end of the file. Readers that pad it and readers
349
+ // that stop see the same entry, so only the end-of-archive check notes it.
350
+ if (pos > size)
351
+ pos = size;
352
+ }
353
+ // Data after the end of the archive. GNU tar pads with zeros to a whole record, which is not data.
354
+ if (trailingStart < size) {
355
+ let nonZero = false;
356
+ for (let at = trailingStart; at < size && !nonZero; at += 65536) {
357
+ (0, errors_1.checkTime)(deadline);
358
+ if ((await reader.read(at, Math.min(65536, size - at))).some(b => b !== 0))
359
+ nonZero = true;
360
+ }
361
+ if (nonZero) {
362
+ notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.TrailingBytes, data: { bytes: size - trailingStart } });
363
+ const first = await TarArchive.firstNonZero(reader, trailingStart);
364
+ const type = (0, sniff_1.sniffType)(await reader.read(first, Math.min(1024, size - first)));
365
+ // More tar after the end is hidden entries, which HIDDEN_DATA reports, not a second format.
366
+ if (type !== undefined && type !== 'application/x-tar')
367
+ notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.Polyglot, data: { type, where: 'after the archive' } });
368
+ if (await TarArchive.headerAfter(reader, trailingStart, deadline))
369
+ notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HiddenData, data: { reason: 'tar headers after the end of the archive, which some readers go on to read' } });
370
+ }
371
+ }
372
+ const archive = new TarArchive(reader, entries, notes, end, trailingStart, label);
373
+ if (comment !== undefined)
374
+ archive.comment = comment;
375
+ return archive;
376
+ }
377
+ /** The offset of the first byte that is not zero, from `from`. */
378
+ static async firstNonZero(reader, from) {
379
+ for (let at = from; at < reader.size; at += 65536) {
380
+ const b = await reader.read(at, Math.min(65536, reader.size - at));
381
+ const i = b.findIndex(x => x !== 0);
382
+ if (i >= 0)
383
+ return at + i;
384
+ }
385
+ return reader.size;
386
+ }
387
+ /** True when a tar header sits at a block boundary from `from` on. */
388
+ static async headerAfter(reader, from, deadline) {
389
+ const start = Math.ceil(from / exports.BLOCK) * exports.BLOCK;
390
+ for (let at = start; at + exports.BLOCK <= reader.size && at - start < HIDDEN_SCAN; at += exports.BLOCK) {
391
+ if ((at - start) % (256 * exports.BLOCK) === 0)
392
+ (0, errors_1.checkTime)(deadline);
393
+ if ((0, sniff_1.isTarHeader)(await reader.read(at, exports.BLOCK)))
394
+ return true;
395
+ }
396
+ return false;
397
+ }
398
+ /** Reads a sparse file's map: GNU's old header and its extensions, or pax 0.0, 0.1 or 1.0. */
399
+ static async sparse(reader, e, block, records, paxText, pos) {
400
+ const map = [];
401
+ const bad = (reason) => {
402
+ throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Unparseable, 'A sparse map that does not read', { reason }, e.name);
403
+ };
404
+ let realSize;
405
+ if (e.typeflag === 'S') {
406
+ const take = (b, start, count) => {
407
+ for (let i = 0; i < count; i++) {
408
+ const o = numeric(b, start + i * 24, 12);
409
+ const n = numeric(b, start + i * 24 + 12, 12);
410
+ if (o === undefined || n === undefined)
411
+ bad('a sparse entry that is not a number');
412
+ if (b[start + i * 24] === 0 && n === 0 && o === 0)
413
+ continue;
414
+ map.push([o ?? 0, n ?? 0]);
415
+ }
416
+ };
417
+ take(block, 386, 4);
418
+ realSize = numeric(block, 483, 12);
419
+ let extended = block[482] !== 0;
420
+ let at = pos + exports.BLOCK;
421
+ while (extended) {
422
+ if (map.length > MAX_REGIONS || at + exports.BLOCK > reader.size)
423
+ bad('a sparse map that does not end');
424
+ const ext = await reader.read(at, exports.BLOCK);
425
+ take(ext, 0, 21);
426
+ extended = ext[504] !== 0;
427
+ at += exports.BLOCK;
428
+ }
429
+ // The extension blocks come before the data.
430
+ e.dataOffset = at;
431
+ }
432
+ else {
433
+ const major = paxText('GNU.sparse.major');
434
+ if (major === '1') {
435
+ realSize = Number(paxText('GNU.sparse.realsize'));
436
+ const name = paxText('GNU.sparse.name');
437
+ if (name !== undefined)
438
+ e.name = name;
439
+ // The map heads the data, as decimal lines, padded to a block: the number of regions, then an offset and a
440
+ // length for each. The lines are read block by block, and what follows the last one is padding.
441
+ const numbers = [];
442
+ let at = e.dataOffset;
443
+ let carry = '';
444
+ const wanted = () => (numbers.length ? 1 + 2 * numbers[0] : 1);
445
+ while (numbers.length < wanted()) {
446
+ if (at - e.dataOffset >= MAX_META || at + exports.BLOCK > e.dataOffset + e.stored)
447
+ bad('a sparse map that does not end');
448
+ const lines = (carry + Buffer.from(await reader.read(at, exports.BLOCK)).toString('latin1')).split('\n');
449
+ at += exports.BLOCK;
450
+ carry = lines.pop() ?? '';
451
+ for (const line of lines) {
452
+ if (numbers.length >= wanted())
453
+ break;
454
+ if (!/^[0-9]{1,20}$/.test(line))
455
+ bad('a sparse map line that is not a number');
456
+ numbers.push(Number(line));
457
+ if (numbers.length === 1 && numbers[0] > MAX_REGIONS)
458
+ bad('a sparse map with too many regions');
459
+ }
460
+ if (numbers.length < wanted() && carry.length > 64)
461
+ bad('a sparse map line that is not a number');
462
+ }
463
+ for (let i = 0; i < numbers[0]; i++)
464
+ map.push([numbers[1 + 2 * i], numbers[2 + 2 * i]]);
465
+ e.sparseMapBytes = at - e.dataOffset;
466
+ }
467
+ else if (paxText('GNU.sparse.map') !== undefined) {
468
+ realSize = Number(paxText('GNU.sparse.size'));
469
+ const nums = (paxText('GNU.sparse.map') ?? '').split(',');
470
+ if (nums.length > 2 * MAX_REGIONS)
471
+ bad('a sparse map with too many regions');
472
+ if (nums.length % 2 !== 0 || nums.some(n => !/^[0-9]+$/.test(n)))
473
+ bad('a sparse map that does not read');
474
+ for (let i = 0; i + 1 < nums.length; i += 2)
475
+ map.push([Number(nums[i]), Number(nums[i + 1])]);
476
+ const name = paxText('GNU.sparse.name');
477
+ if (name !== undefined)
478
+ e.name = name;
479
+ }
480
+ else {
481
+ realSize = Number(paxText('GNU.sparse.size'));
482
+ let offset;
483
+ for (const r of records) {
484
+ if (r.key === 'GNU.sparse.offset')
485
+ offset = Number(r.value.toString('latin1'));
486
+ else if (r.key === 'GNU.sparse.numbytes') {
487
+ if (offset === undefined)
488
+ bad('a sparse length without an offset');
489
+ map.push([offset ?? 0, Number(r.value.toString('latin1'))]);
490
+ offset = undefined;
491
+ }
492
+ }
493
+ }
494
+ }
495
+ if (realSize === undefined || !Number.isSafeInteger(realSize) || realSize < 0)
496
+ bad('a sparse file without a valid size');
497
+ let covered = 0;
498
+ let data = 0;
499
+ for (const [o, n] of map) {
500
+ if (!Number.isSafeInteger(o) || !Number.isSafeInteger(n) || o < covered || o + n > (realSize ?? 0))
501
+ bad('sparse segments out of order or past the file');
502
+ covered = o + n;
503
+ data += n;
504
+ }
505
+ if (data !== e.stored - (e.sparseMapBytes ?? 0))
506
+ bad('sparse segments that do not add up to the data');
507
+ e.sparse = map;
508
+ e.size = realSize ?? 0;
509
+ e.kind = 'file';
510
+ }
511
+ async unlock() {
512
+ return false;
513
+ }
514
+ hasMetadata() {
515
+ return this.label !== undefined || this.comment !== undefined || this.entries.some(e => e.uid !== 0 || e.gid !== 0 || e.mtime !== 0 || e.uname !== '' || e.gname !== '' || e.pax.length > 0);
516
+ }
517
+ /** The entry's stored data. */
518
+ raw(e) {
519
+ return this.reader.chunks(e.dataOffset, e.stored);
520
+ }
521
+ async *read(e, meter) {
522
+ if (e.broken)
523
+ throw new errors_1.DataError(e.broken);
524
+ if (!e.sparse) {
525
+ let total = 0;
526
+ for await (const c of this.raw(e)) {
527
+ total += c.length;
528
+ meter.add(total, total);
529
+ yield c;
530
+ }
531
+ if (total !== e.stored)
532
+ throw new errors_1.DataError('the archive ends inside the entry', true);
533
+ return;
534
+ }
535
+ // A sparse file: zeros in the holes, the stored segments in between.
536
+ let at = e.dataOffset + (e.sparseMapBytes ?? 0);
537
+ let out = 0;
538
+ const zeros = Buffer.alloc(65536);
539
+ const hole = async function* (length) {
540
+ for (let left = length; left > 0; left -= zeros.length) {
541
+ const n = Math.min(left, zeros.length);
542
+ out += n;
543
+ // The holes are the size the header declared, which the limits already checked, so they count as stored.
544
+ meter.add(out, out);
545
+ yield zeros.subarray(0, n);
546
+ }
547
+ };
548
+ for (const [o, n] of e.sparse) {
549
+ yield* hole(o - out);
550
+ for await (const c of this.reader.chunks(at, n)) {
551
+ out += c.length;
552
+ meter.add(out, out);
553
+ yield c;
554
+ }
555
+ at += n;
556
+ }
557
+ yield* hole(e.size - out);
558
+ }
559
+ write(plan, sink, context) {
560
+ return writeTar(this, plan, sink, context);
561
+ }
562
+ trailing() {
563
+ return this.reader.chunks(this.trailingStart, this.reader.size - this.trailingStart);
564
+ }
565
+ }
566
+ exports.TarArchive = TarArchive;
567
+ // ---------------------------------------------------------------------------------------------------------------
568
+ // The writer.
569
+ /** One pax record, whose length counts its own digits. */
570
+ function paxRecord(key, value) {
571
+ const body = Buffer.concat([Buffer.from(` ${key}=`, 'utf8'), value, Buffer.from('\n')]);
572
+ let len = body.length + 1;
573
+ while (String(len).length + body.length !== len)
574
+ len = String(len).length + body.length;
575
+ return Buffer.concat([Buffer.from(String(len)), body]);
576
+ }
577
+ const octalField = (v, width) => `${v.toString(8).padStart(width - 1, '0')}\0`;
578
+ /** A header block. */
579
+ function header(f) {
580
+ const b = Buffer.alloc(exports.BLOCK);
581
+ f.name.copy(b, 0, 0, 100);
582
+ b.write(octalField(f.mode, 8), 100, 'latin1');
583
+ b.write(octalField(f.uid, 8), 108, 'latin1');
584
+ b.write(octalField(f.gid, 8), 116, 'latin1');
585
+ b.write(octalField(f.size, 12), 124, 'latin1');
586
+ b.write(octalField(f.mtime, 12), 136, 'latin1');
587
+ b.write(' ', 148, 'latin1');
588
+ b.write(f.typeflag, 156, 'latin1');
589
+ f.linkname.copy(b, 157, 0, 100);
590
+ b.write('ustar\x0000', 257, 'latin1');
591
+ b.write(f.uname.slice(0, 32), 265, 'latin1');
592
+ b.write(f.gname.slice(0, 32), 297, 'latin1');
593
+ b.write(octalField(f.devmajor, 8), 329, 'latin1');
594
+ b.write(octalField(f.devminor, 8), 337, 'latin1');
595
+ f.prefix.copy(b, 345, 0, 155);
596
+ let sum = 0;
597
+ for (const x of b)
598
+ sum += x;
599
+ b.write(`${sum.toString(8).padStart(6, '0')}\0 `, 148, 'latin1');
600
+ return b;
601
+ }
602
+ /** A name in ASCII for the header itself, for readers without pax support. The pax record holds the real one. */
603
+ const asciiFallback = (s) => Buffer.from(s.replace(/[^\x20-\x7e]/g, '_').slice(0, 100), 'latin1');
604
+ exports.asciiFallback = asciiFallback;
605
+ /** The name split across the ustar prefix and name fields, or undefined when it does not fit. */
606
+ function split(name) {
607
+ if (!/^[\x20-\x7e]*$/.test(name))
608
+ return undefined;
609
+ if (name.length <= 100)
610
+ return { prefix: Buffer.alloc(0), name: Buffer.from(name, 'latin1') };
611
+ for (let i = name.length - 1; i > 0; i--) {
612
+ if (name[i] !== '/')
613
+ continue;
614
+ if (i <= 155 && name.length - i - 1 <= 100 && name.length - i - 1 > 0)
615
+ return { prefix: Buffer.from(name.slice(0, i), 'latin1'), name: Buffer.from(name.slice(i + 1), 'latin1') };
616
+ }
617
+ return undefined;
618
+ }
619
+ const typeflagFor = (e) => {
620
+ if (e.kind === 'directory')
621
+ return '5';
622
+ if (e.kind === 'file')
623
+ return e.typeflag === '7' ? '7' : '0';
624
+ if (e.kind === 'other')
625
+ return e.typeflag;
626
+ if (e.kind === 'hardlink')
627
+ return '1';
628
+ if (e.kind === 'symlink')
629
+ return '2';
630
+ if (e.kind === 'fifo')
631
+ return '6';
632
+ return e.typeflag === '4' ? '4' : '3';
633
+ };
634
+ async function writeTar(tar, plan, sink, context) {
635
+ let written = 0;
636
+ const put = async (b) => {
637
+ await sink.write(b);
638
+ written += b.length;
639
+ };
640
+ const meta = !plan.stripMetadata;
641
+ if (meta && tar.label !== undefined) {
642
+ await put(header({
643
+ name: Buffer.from(tar.label.slice(0, 100), 'latin1'),
644
+ mode: 0,
645
+ uid: 0,
646
+ gid: 0,
647
+ size: 0,
648
+ mtime: 0,
649
+ typeflag: 'V',
650
+ linkname: Buffer.alloc(0),
651
+ uname: '',
652
+ gname: '',
653
+ prefix: Buffer.alloc(0),
654
+ devmajor: 0,
655
+ devminor: 0,
656
+ }));
657
+ }
658
+ if (meta && tar.comment !== undefined)
659
+ await writePax(put, 'g', [paxRecord('comment', Buffer.from(tar.comment, 'utf8'))]);
660
+ for (const k of plan.kept) {
661
+ context.meter().time();
662
+ await writeEntry(tar, k, plan, put, context);
663
+ }
664
+ await put(Buffer.alloc(2 * exports.BLOCK));
665
+ const pad = (exports.RECORD - (written % exports.RECORD)) % exports.RECORD;
666
+ if (pad)
667
+ await put(Buffer.alloc(pad));
668
+ if (plan.keepTrailing)
669
+ for await (const c of tar.trailing())
670
+ await put(c);
671
+ }
672
+ async function writePax(put, typeflag, records) {
673
+ const body = Buffer.concat(records);
674
+ await put(header({
675
+ name: Buffer.from(typeflag === 'g' ? 'pax_global_header' : '././@PaxHeader'),
676
+ mode: 0o644,
677
+ uid: 0,
678
+ gid: 0,
679
+ size: body.length,
680
+ mtime: 0,
681
+ typeflag,
682
+ linkname: Buffer.alloc(0),
683
+ uname: '',
684
+ gname: '',
685
+ prefix: Buffer.alloc(0),
686
+ devmajor: 0,
687
+ devminor: 0,
688
+ }));
689
+ await put(body);
690
+ const pad = (exports.BLOCK - (body.length % exports.BLOCK)) % exports.BLOCK;
691
+ if (pad)
692
+ await put(Buffer.alloc(pad));
693
+ }
694
+ async function writeEntry(tar, k, plan, put, context) {
695
+ const e = k.entry;
696
+ const meta = !plan.stripMetadata;
697
+ const records = [];
698
+ const name = e.kind === 'directory' && !k.name.endsWith('/') ? `${k.name}/` : k.name;
699
+ const parts = split(name);
700
+ if (!parts)
701
+ records.push(paxRecord('path', Buffer.from(name, 'utf8')));
702
+ const link = k.linkTarget ?? e.linkTarget ?? '';
703
+ const linkFits = /^[\x20-\x7e]*$/.test(link) && link.length <= 100;
704
+ if (!linkFits)
705
+ records.push(paxRecord('linkpath', Buffer.from(link, 'utf8')));
706
+ const hasData = e.kind === 'file' || e.kind === 'other';
707
+ const size = hasData ? (0, model_1.sizeOf)(k) : 0;
708
+ if (size > 0o77777777777)
709
+ records.push(paxRecord('size', Buffer.from(String(size))));
710
+ const uid = meta ? e.uid : 0;
711
+ const gid = meta ? e.gid : 0;
712
+ const uname = meta ? e.uname : '';
713
+ const gname = meta ? e.gname : '';
714
+ const mtime = meta ? e.mtime : 0;
715
+ const idFits = (v) => v >= 0 && v <= 0o7777777;
716
+ if (!idFits(uid))
717
+ records.push(paxRecord('uid', Buffer.from(String(uid))));
718
+ if (!idFits(gid))
719
+ records.push(paxRecord('gid', Buffer.from(String(gid))));
720
+ const nameFits = (s) => /^[\x20-\x7e]*$/.test(s) && s.length <= 32;
721
+ if (!nameFits(uname))
722
+ records.push(paxRecord('uname', Buffer.from(uname, 'utf8')));
723
+ if (!nameFits(gname))
724
+ records.push(paxRecord('gname', Buffer.from(gname, 'utf8')));
725
+ const timeFits = mtime >= 0 && mtime <= 0o77777777777;
726
+ if (!timeFits)
727
+ records.push(paxRecord('mtime', Buffer.from(String(mtime))));
728
+ if (meta)
729
+ for (const r of e.pax)
730
+ if (!(k.dropCapabilities && CAPABILITY.test(r.key)))
731
+ records.push(paxRecord(r.key, r.value));
732
+ if (records.length)
733
+ await writePax(put, 'x', records);
734
+ await put(header({
735
+ name: parts?.name ?? (0, exports.asciiFallback)(name),
736
+ prefix: parts?.prefix ?? Buffer.alloc(0),
737
+ mode: k.mode ?? (e.kind === 'directory' ? 0o755 : 0o644),
738
+ uid: idFits(uid) ? uid : 0,
739
+ gid: idFits(gid) ? gid : 0,
740
+ size: size > 0o77777777777 ? 0 : size,
741
+ mtime: timeFits ? mtime : 0,
742
+ typeflag: typeflagFor(e),
743
+ linkname: linkFits ? Buffer.from(link, 'latin1') : (0, exports.asciiFallback)(link),
744
+ uname: nameFits(uname) ? uname : '',
745
+ gname: nameFits(gname) ? gname : '',
746
+ devmajor: e.kind === 'device' ? e.devmajor : 0,
747
+ devminor: e.kind === 'device' ? e.devminor : 0,
748
+ }));
749
+ if (!hasData)
750
+ return;
751
+ let n = 0;
752
+ const chunks = k.replacement || e.sparse ? (0, model_1.contentOf)(tar, k, context) : tar.raw(e);
753
+ for await (const c of chunks) {
754
+ context.meter().time();
755
+ n += c.length;
756
+ await put(c);
757
+ }
758
+ if (n !== size)
759
+ throw new Error(`Entry ${k.name} has ${n} bytes, not ${size}`);
760
+ const pad = (exports.BLOCK - (size % exports.BLOCK)) % exports.BLOCK;
761
+ if (pad)
762
+ await put(Buffer.alloc(pad));
763
+ }
764
+ const isTar = (head) => head.length >= exports.BLOCK && (0, sniff_1.isTarHeader)(head.subarray(0, exports.BLOCK)) && (0, names_1.isAscii)(head.subarray(257, 262));
765
+ exports.isTar = isTar;