@patrickdk77/archive-defuse 0.0.0-stage → 0.1.1

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -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/detect.d.ts +10 -0
  24. package/dist/detect.js +95 -0
  25. package/dist/encoder.d.ts +44 -0
  26. package/dist/encoder.js +114 -0
  27. package/dist/engine.d.ts +44 -0
  28. package/dist/engine.js +753 -0
  29. package/dist/errors.d.ts +64 -0
  30. package/dist/errors.js +98 -0
  31. package/dist/filters.d.ts +34 -0
  32. package/dist/filters.js +385 -0
  33. package/dist/findings.d.ts +26 -0
  34. package/dist/findings.js +176 -0
  35. package/dist/formats.d.ts +24 -0
  36. package/dist/formats.js +106 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.js +53 -0
  39. package/dist/index.mjs +3 -0
  40. package/dist/inflate64.d.ts +6 -0
  41. package/dist/inflate64.js +388 -0
  42. package/dist/io.d.ts +78 -0
  43. package/dist/io.js +379 -0
  44. package/dist/lzma-formats.d.ts +75 -0
  45. package/dist/lzma-formats.js +373 -0
  46. package/dist/lzma-sniff.d.ts +20 -0
  47. package/dist/lzma-sniff.js +73 -0
  48. package/dist/lzma-writers.d.ts +35 -0
  49. package/dist/lzma-writers.js +276 -0
  50. package/dist/lzma1/LICENSE +201 -0
  51. package/dist/lzma1/NOTICE +36 -0
  52. package/dist/lzma1/crc32.d.ts +4 -0
  53. package/dist/lzma1/crc32.js +19 -0
  54. package/dist/lzma1/hash234.d.ts +22 -0
  55. package/dist/lzma1/hash234.js +75 -0
  56. package/dist/lzma1/hc4.d.ts +30 -0
  57. package/dist/lzma1/hc4.js +142 -0
  58. package/dist/lzma1/header.d.ts +32 -0
  59. package/dist/lzma1/header.js +73 -0
  60. package/dist/lzma1/lz-decoder.d.ts +41 -0
  61. package/dist/lzma1/lz-decoder.js +128 -0
  62. package/dist/lzma1/lz-encoder.d.ts +87 -0
  63. package/dist/lzma1/lz-encoder.js +166 -0
  64. package/dist/lzma1/lzma-coder.d.ts +107 -0
  65. package/dist/lzma1/lzma-coder.js +162 -0
  66. package/dist/lzma1/lzma-decoder.d.ts +36 -0
  67. package/dist/lzma1/lzma-decoder.js +484 -0
  68. package/dist/lzma1/lzma-encoder-fast.d.ts +13 -0
  69. package/dist/lzma1/lzma-encoder-fast.js +114 -0
  70. package/dist/lzma1/lzma-encoder.d.ts +105 -0
  71. package/dist/lzma1/lzma-encoder.js +497 -0
  72. package/dist/lzma1/lzma2-decoder.d.ts +85 -0
  73. package/dist/lzma1/lzma2-decoder.js +249 -0
  74. package/dist/lzma1/range-coder.d.ts +18 -0
  75. package/dist/lzma1/range-coder.js +23 -0
  76. package/dist/lzma1/range-decoder.d.ts +41 -0
  77. package/dist/lzma1/range-decoder.js +78 -0
  78. package/dist/lzma1/range-encoder.d.ts +53 -0
  79. package/dist/lzma1/range-encoder.js +269 -0
  80. package/dist/lzma1-stream.d.ts +60 -0
  81. package/dist/lzma1-stream.js +202 -0
  82. package/dist/model.d.ts +141 -0
  83. package/dist/model.js +16 -0
  84. package/dist/names.d.ts +13 -0
  85. package/dist/names.js +66 -0
  86. package/dist/pack.d.ts +79 -0
  87. package/dist/pack.js +590 -0
  88. package/dist/plugins.d.ts +13 -0
  89. package/dist/plugins.js +56 -0
  90. package/dist/rules.d.ts +25 -0
  91. package/dist/rules.js +403 -0
  92. package/dist/score.d.ts +17 -0
  93. package/dist/score.js +58 -0
  94. package/dist/seven.d.ts +75 -0
  95. package/dist/seven.js +492 -0
  96. package/dist/sevenzip.d.ts +133 -0
  97. package/dist/sevenzip.js +529 -0
  98. package/dist/single.d.ts +103 -0
  99. package/dist/single.js +453 -0
  100. package/dist/sniff.d.ts +20 -0
  101. package/dist/sniff.js +224 -0
  102. package/dist/stream-meter.d.ts +39 -0
  103. package/dist/stream-meter.js +54 -0
  104. package/dist/tar.d.ts +102 -0
  105. package/dist/tar.js +765 -0
  106. package/dist/types.d.ts +311 -0
  107. package/dist/types.js +133 -0
  108. package/dist/unpack.d.ts +67 -0
  109. package/dist/unpack.js +194 -0
  110. package/dist/xz.d.ts +25 -0
  111. package/dist/xz.js +333 -0
  112. package/dist/zip-lzma.d.ts +15 -0
  113. package/dist/zip-lzma.js +137 -0
  114. package/dist/zip-writer.d.ts +51 -0
  115. package/dist/zip-writer.js +305 -0
  116. package/dist/zip.d.ts +99 -0
  117. package/dist/zip.js +901 -0
  118. package/dist/zipcrypt.d.ts +47 -0
  119. package/dist/zipcrypt.js +233 -0
  120. package/dist/zstd.d.ts +25 -0
  121. package/dist/zstd.js +160 -0
  122. package/package.json +94 -4
package/dist/rules.js ADDED
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nameDetails = nameDetails;
4
+ exports.applyRules = applyRules;
5
+ const names_1 = require("./names");
6
+ const types_1 = require("./types");
7
+ const SEPARATOR = /[/\\]/;
8
+ /** A ".." part, also with the dots and spaces Windows drops from the end of a name. */
9
+ const DOTDOT = /^\.\.[. ]*$/;
10
+ const DRIVE = /^(?:[/\\]{2}[?.][/\\])?[A-Za-z]:/;
11
+ const ABSOLUTE = /^[/\\]/;
12
+ const CONTROL_TEST = new RegExp(names_1.CONTROL.source, 'u');
13
+ const utf8Length = (s) => Buffer.byteLength(s, 'utf8');
14
+ /** The most parts a link target may have and still be followed. */
15
+ const MAX_LINK_PARTS = 4096;
16
+ /** `s` without the characters in `chars` at its start. */
17
+ function trimStart(s, chars) {
18
+ let start = 0;
19
+ while (start < s.length && chars.includes(s[start]))
20
+ start++;
21
+ return s.slice(start);
22
+ }
23
+ /** The parts of a name between separators. A folder's name ends in "/", which is not an empty part. */
24
+ const partsOf = (name) => (/[/\\]$/.test(name) ? name.slice(0, -1) : name).split(SEPARATOR);
25
+ /** A name without the slashes that end a folder's name. */
26
+ const exactKey = (n) => (0, names_1.trimEnd)(n, '/');
27
+ /** Equal for names a file system that normalizes Unicode, including compatibility forms such as ligatures, takes as one. */
28
+ const normalKey = (n) => exactKey(n).normalize('NFKD');
29
+ /** Equal for names a file system that ignores case takes as one. Upper-casing first turns a sharp s into "SS". */
30
+ const foldKey = (n) => normalKey(n).toUpperCase().toLowerCase();
31
+ /** Applies every name rule whose action is strip, in a fixed order, to `name`. Each one changes only what it reports. */
32
+ function fixName(name, dir, strips) {
33
+ // The drive letter is set aside, so the other rules fix the rest of the name, and comes back unless DRIVE_LETTER strips.
34
+ const drive = DRIVE.exec(name)?.[0] ?? '';
35
+ let n = name.slice(drive.length);
36
+ if (strips(types_1.ArchiveDetail.Absolute))
37
+ n = trimStart(n, '/\\');
38
+ // Split into parts with the separator after each, so a rule that keeps backslashes keeps them where they were.
39
+ const tokens = n.split(/([/\\])/);
40
+ const parts = [];
41
+ for (let i = 0; i < tokens.length; i += 2)
42
+ parts.push({ part: tokens[i], sep: tokens[i + 1] ?? '' });
43
+ const lead = Math.max(0, parts.findIndex(p => p.part !== ''));
44
+ let kept = parts.filter((p, i) => {
45
+ if (strips(types_1.ArchiveDetail.Traversal) && DOTDOT.test(p.part))
46
+ return false;
47
+ // A leading empty part is the absolute path's, which ABSOLUTE decides, and a trailing one a folder's slash.
48
+ if (strips(types_1.ArchiveDetail.EmptySegment) && (p.part === '.' || (p.part === '' && i > lead && i < parts.length - 1)))
49
+ return false;
50
+ return true;
51
+ });
52
+ if (strips(types_1.ArchiveDetail.ControlCharacter))
53
+ kept = kept.map(p => ({ ...p, part: p.part.replace(new RegExp(names_1.CONTROL.source, 'gu'), '_') }));
54
+ if (strips(types_1.ArchiveDetail.WindowsReserved))
55
+ kept = kept.map(p => (DOTDOT.test(p.part) ? p : { ...p, part: (0, names_1.windowsSafe)(p.part) }));
56
+ n = kept.map((p, i) => p.part + (i < kept.length - 1 ? p.sep || '/' : '')).join('');
57
+ if (!strips(types_1.ArchiveDetail.DriveLetter))
58
+ n = drive + n;
59
+ if (strips(types_1.ArchiveDetail.Backslash))
60
+ n = n.replace(/\\/g, '/');
61
+ // A name the rewrite ends in "/" is a folder to every reader, so only a folder keeps its slash.
62
+ n = (0, names_1.trimEnd)(n, '/');
63
+ if (n === '')
64
+ return '';
65
+ return dir ? `${n}/` : n;
66
+ }
67
+ /**
68
+ * The name rules a name breaks, in the order they report. `applyRules` reports these, and `createArchive` refuses a
69
+ * name that breaks any but TOO_LONG. A ".." part is TRAVERSAL's alone, and empty parts at the start belong to an
70
+ * absolute or UNC path, which ABSOLUTE reports.
71
+ */
72
+ function nameDetails(name, backslashIsSeparator) {
73
+ const parts = partsOf(name.replace(DRIVE, ''));
74
+ const found = [];
75
+ if (name === '' || name === '/')
76
+ found.push(types_1.ArchiveDetail.Empty);
77
+ if (DRIVE.test(name))
78
+ found.push(types_1.ArchiveDetail.DriveLetter);
79
+ if (ABSOLUTE.test(name) || ABSOLUTE.test(name.replace(DRIVE, '')))
80
+ found.push(types_1.ArchiveDetail.Absolute);
81
+ if (parts.some(p => DOTDOT.test(p)))
82
+ found.push(types_1.ArchiveDetail.Traversal);
83
+ const lead = parts.findIndex(p => p !== '');
84
+ if (parts.some((p, j) => p === '.' || (p === '' && j > lead && lead >= 0)))
85
+ found.push(types_1.ArchiveDetail.EmptySegment);
86
+ if (backslashIsSeparator && name.includes('\\'))
87
+ found.push(types_1.ArchiveDetail.Backslash);
88
+ if (CONTROL_TEST.test(name))
89
+ found.push(types_1.ArchiveDetail.ControlCharacter);
90
+ if (parts.some(p => (0, names_1.windowsReserved)(p) && !DOTDOT.test(p)))
91
+ found.push(types_1.ArchiveDetail.WindowsReserved);
92
+ if (utf8Length(name) > 4096 || parts.some(p => utf8Length(p) > 255))
93
+ found.push(types_1.ArchiveDetail.TooLong);
94
+ return found;
95
+ }
96
+ /** A tree of path parts. Maps, not objects, so a part named "__proto__" is only a name. */
97
+ class PathTree {
98
+ children = new Map();
99
+ /** A symbolic link's target, at the node the link names. */
100
+ link;
101
+ explicitDir = false;
102
+ /** The long names in this folder, by the 8.3 alias they could get. */
103
+ aliases;
104
+ child(part, create) {
105
+ let c = this.children.get(part);
106
+ if (!c && create) {
107
+ c = new PathTree();
108
+ this.children.set(part, c);
109
+ }
110
+ return c;
111
+ }
112
+ }
113
+ /** The node for `parts`, created when `create` is set. */
114
+ function walk(root, parts, create) {
115
+ let node = root;
116
+ for (const p of parts) {
117
+ node = node.child(p, create);
118
+ if (!node)
119
+ return undefined;
120
+ }
121
+ return node;
122
+ }
123
+ /**
124
+ * Where a symbolic link's target leads, from the folder the link sits in, following the archive's other links by their
125
+ * case-folded names. Undefined when it leaves the extraction folder, when it is too long to follow, or when links lead
126
+ * to each other more than 40 times in all.
127
+ */
128
+ function resolveLink(from, target, links, budget) {
129
+ if (ABSOLUTE.test(target) || DRIVE.test(target))
130
+ return undefined;
131
+ const parts = target.split(SEPARATOR);
132
+ if (parts.length > MAX_LINK_PARTS)
133
+ return undefined;
134
+ // The path so far, and the tree node of each prefix of it, so a ".." steps back without walking from the root.
135
+ const path = [...from];
136
+ const nodesOf = (p) => {
137
+ const nodes = [links];
138
+ for (const x of p)
139
+ nodes.push(nodes[nodes.length - 1]?.child(x, false));
140
+ return nodes;
141
+ };
142
+ let nodes = nodesOf(path);
143
+ for (const part of parts) {
144
+ if (part === '' || part === '.')
145
+ continue;
146
+ if (DOTDOT.test(part)) {
147
+ if (!path.length)
148
+ return undefined;
149
+ path.pop();
150
+ nodes.pop();
151
+ continue;
152
+ }
153
+ const key = foldKey(part);
154
+ path.push(key);
155
+ const node = nodes[nodes.length - 1]?.child(key, false);
156
+ nodes.push(node);
157
+ if (node?.link === undefined)
158
+ continue;
159
+ if (--budget.hops < 0)
160
+ return undefined;
161
+ const resolved = resolveLink(path.slice(0, -1), node.link, links, budget);
162
+ if (resolved === undefined)
163
+ return undefined;
164
+ path.length = 0;
165
+ path.push(...resolved);
166
+ nodes = nodesOf(path);
167
+ }
168
+ return path;
169
+ }
170
+ /** The parts of the 8.3 alias Windows could give a long name: the first six characters of its base and three of its extension. */
171
+ function aliasKey(name) {
172
+ const dot = name.lastIndexOf('.');
173
+ const rawBase = dot > 0 ? name.slice(0, dot) : name;
174
+ const rawExt = dot > 0 ? name.slice(dot + 1) : '';
175
+ // A name that already fits 8.3 is its own short name and gets no alias.
176
+ if (rawBase.length <= 8 && rawExt.length <= 3 && /^[A-Za-z0-9_^$~!#%&{}@`'()-]*$/.test(rawBase + rawExt))
177
+ return undefined;
178
+ const clean = (s) => s
179
+ .toUpperCase()
180
+ .replace(/[ .]/g, '')
181
+ .replace(/[+,;=[\]]/g, '_');
182
+ return `${clean(rawBase).slice(0, 6)}|${clean(rawExt).slice(0, 3)}`;
183
+ }
184
+ /** The prefix and extension of a name in the form of an alias, such as PROGRA~1. */
185
+ const ALIAS = /^([^~.\\/]{1,6})~[1-9][0-9]{0,5}(?:\.([^.\\/]{0,3}))?$/;
186
+ /** Runs the rules over the entries of an archive. Single-file formats with no name skip the name rules. */
187
+ function applyRules(entries, factory, emit, options) {
188
+ const strips = (d, c = types_1.ArchiveCategory.Name) => factory.strips(c, d);
189
+ const decisions = entries.map(e => ({ remove: false, name: e.name, mode: e.mode, dropCapabilities: false }));
190
+ // Names, each rule on the name as the archive gives it.
191
+ if (options.names) {
192
+ for (const [i, e] of entries.entries()) {
193
+ const name = e.name;
194
+ const found = nameDetails(name, options.backslashIsSeparator);
195
+ const tooLong = found.includes(types_1.ArchiveDetail.TooLong);
196
+ // A drive letter is DRIVE_LETTER's. The rest of the name, "..\\x" in "C:..\\x", is what the other rules read.
197
+ for (const detail of found) {
198
+ if (detail === types_1.ArchiveDetail.Empty)
199
+ emit(types_1.ArchiveCategory.Name, detail, e);
200
+ else if (detail === types_1.ArchiveDetail.TooLong)
201
+ emit(types_1.ArchiveCategory.Name, detail, e, { name, bytes: utf8Length(name), parts: partsOf(name.replace(DRIVE, '')).length });
202
+ else
203
+ emit(types_1.ArchiveCategory.Name, detail, e, { name });
204
+ }
205
+ const d = decisions[i];
206
+ d.name = fixName(name, e.kind === 'directory', x => (x === types_1.ArchiveDetail.Backslash && !options.backslashIsSeparator ? false : strips(x)));
207
+ if (d.name === '' && (name !== '' || strips(types_1.ArchiveDetail.Empty)))
208
+ d.remove = true;
209
+ if (tooLong && strips(types_1.ArchiveDetail.TooLong))
210
+ d.remove = true;
211
+ }
212
+ }
213
+ // Collisions, on the names the output would have.
214
+ const groups = (key) => {
215
+ const m = new Map();
216
+ for (const [i, d] of decisions.entries()) {
217
+ if (d.name === '')
218
+ continue;
219
+ const k = key(d.name);
220
+ const list = m.get(k);
221
+ if (list)
222
+ list.push(i);
223
+ else
224
+ m.set(k, [i]);
225
+ }
226
+ return m;
227
+ };
228
+ const notDir = (i) => entries[i].kind !== 'directory';
229
+ for (const list of groups(exactKey).values()) {
230
+ const files = list.filter(notDir);
231
+ if (files.length < 2)
232
+ continue;
233
+ // Readers that overwrite keep the last copy, and that is the one the rewrite keeps.
234
+ for (const i of files.slice(0, -1)) {
235
+ emit(types_1.ArchiveCategory.Name, types_1.ArchiveDetail.Duplicate, entries[i], { name: entries[i].name });
236
+ if (strips(types_1.ArchiveDetail.Duplicate))
237
+ decisions[i].remove = true;
238
+ }
239
+ }
240
+ const collide = (key, inner, detail) => {
241
+ for (const list of groups(key).values()) {
242
+ if (list.length < 2 || list.every(i => !notDir(i)))
243
+ continue;
244
+ const last = list[list.length - 1];
245
+ for (const i of list) {
246
+ if (inner(decisions[i].name) === inner(decisions[last].name))
247
+ continue;
248
+ emit(types_1.ArchiveCategory.Name, detail, entries[i], { name: entries[i].name, other: entries[last].name });
249
+ if (strips(detail))
250
+ decisions[i].remove = true;
251
+ }
252
+ }
253
+ };
254
+ collide(normalKey, exactKey, types_1.ArchiveDetail.NormalizationCollision);
255
+ collide(foldKey, normalKey, types_1.ArchiveDetail.CaseCollision);
256
+ // The tree of the output's names, as written and case-folded: the folders the archive lists, every folder a name
257
+ // needs, and the symbolic links.
258
+ const tree = new PathTree();
259
+ const folded = new PathTree();
260
+ const finalParts = decisions.map(d => (d.name === '' ? [] : exactKey(d.name).split('/')));
261
+ for (const [i, parts] of finalParts.entries()) {
262
+ if (!parts.length)
263
+ continue;
264
+ const node = walk(tree, parts, true);
265
+ if (node && entries[i].kind === 'directory')
266
+ node.explicitDir = true;
267
+ const f = walk(folded, parts.map(p => foldKey(p)), true);
268
+ if (f && entries[i].kind === 'symlink' && entries[i].linkTarget !== undefined)
269
+ f.link = entries[i].linkTarget;
270
+ }
271
+ // A file where a folder is needed: a folder the archive lists under the same name, or a folder some other entry
272
+ // sits in. A link counts when the archive lists a folder of its name; a link other entries sit under is THROUGH_LINK's.
273
+ for (const [i, parts] of finalParts.entries()) {
274
+ const e = entries[i];
275
+ if (!parts.length || e.kind === 'directory')
276
+ continue;
277
+ const node = walk(tree, parts, false);
278
+ if (!node || !(node.explicitDir || (node.children.size > 0 && e.kind !== 'symlink')))
279
+ continue;
280
+ emit(types_1.ArchiveCategory.Name, types_1.ArchiveDetail.FileDirectoryClash, e, { name: e.name });
281
+ if (strips(types_1.ArchiveDetail.FileDirectoryClash))
282
+ decisions[i].remove = true;
283
+ }
284
+ // A name in the form of an 8.3 alias that Windows could give a long name in the same folder. Each folder node of the
285
+ // case-folded tree keeps the long names in it by the alias they could get.
286
+ for (const parts of finalParts) {
287
+ let node = folded;
288
+ for (const part of parts) {
289
+ if (!node)
290
+ break;
291
+ const k = aliasKey(part);
292
+ if (k !== undefined) {
293
+ node.aliases ??= new Map();
294
+ const names = node.aliases.get(k) ?? new Set();
295
+ node.aliases.set(k, names);
296
+ names.add(part.toUpperCase());
297
+ }
298
+ node = node.child(foldKey(part), false);
299
+ }
300
+ }
301
+ for (const [i, parts] of finalParts.entries()) {
302
+ let node = folded;
303
+ for (const part of parts) {
304
+ if (!node)
305
+ break;
306
+ const m = ALIAS.exec(part);
307
+ const longNames = m ? node.aliases?.get(`${m[1].toUpperCase()}|${(m[2] ?? '').toUpperCase()}`) : undefined;
308
+ if (longNames && [...longNames].some(n => n !== part.toUpperCase())) {
309
+ emit(types_1.ArchiveCategory.Name, types_1.ArchiveDetail.ShortNameCollision, entries[i], { name: entries[i].name, alias: part });
310
+ if (strips(types_1.ArchiveDetail.ShortNameCollision))
311
+ decisions[i].remove = true;
312
+ break;
313
+ }
314
+ node = node.child(foldKey(part), false);
315
+ }
316
+ }
317
+ // Links.
318
+ const byName = new Map();
319
+ for (const [i, e] of entries.entries()) {
320
+ const d = decisions[i];
321
+ if (e.kind === 'symlink') {
322
+ emit(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.Symlink, e, { name: e.name, target: e.linkTarget ?? '' });
323
+ if (strips(types_1.ArchiveDetail.Symlink, types_1.ArchiveCategory.Link))
324
+ d.remove = true;
325
+ const from = finalParts[i].slice(0, -1).map(p => foldKey(p));
326
+ if (e.linkTarget === undefined || resolveLink(from, e.linkTarget, folded, { hops: 40 }) === undefined) {
327
+ emit(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.Outside, e, { name: e.name, target: e.linkTarget ?? '' });
328
+ if (strips(types_1.ArchiveDetail.Outside, types_1.ArchiveCategory.Link))
329
+ d.remove = true;
330
+ }
331
+ }
332
+ else if (e.kind === 'hardlink') {
333
+ // A tar hard link names its target from the top of the archive, not from the folder the link sits in.
334
+ const target = e.linkTarget ?? '';
335
+ emit(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.Hardlink, e, { name: e.name, target });
336
+ if (strips(types_1.ArchiveDetail.Hardlink, types_1.ArchiveCategory.Link))
337
+ d.remove = true;
338
+ if (ABSOLUTE.test(target) || DRIVE.test(target) || target.split(SEPARATOR).some(p => DOTDOT.test(p))) {
339
+ emit(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.Outside, e, { name: e.name, target });
340
+ if (strips(types_1.ArchiveDetail.Outside, types_1.ArchiveCategory.Link))
341
+ d.remove = true;
342
+ }
343
+ const to = byName.get(target);
344
+ if (to === undefined || entries[to].kind === 'directory') {
345
+ emit(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.Dangling, e, { name: e.name, target });
346
+ if (strips(types_1.ArchiveDetail.Dangling, types_1.ArchiveCategory.Link))
347
+ d.remove = true;
348
+ }
349
+ else
350
+ d.linkIndex = to;
351
+ }
352
+ byName.set(e.name, i);
353
+ }
354
+ // An entry under a symbolic link is written wherever the link points.
355
+ for (const [i, parts] of finalParts.entries()) {
356
+ let node = folded;
357
+ for (let j = 0; j < parts.length - 1 && node; j++) {
358
+ node = node.child(foldKey(parts[j]), false);
359
+ if (node?.link === undefined)
360
+ continue;
361
+ emit(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.ThroughLink, entries[i], { name: entries[i].name, link: parts.slice(0, j + 1).join('/') });
362
+ if (strips(types_1.ArchiveDetail.ThroughLink, types_1.ArchiveCategory.Link))
363
+ decisions[i].remove = true;
364
+ break;
365
+ }
366
+ }
367
+ // Special files.
368
+ for (const [i, e] of entries.entries()) {
369
+ if (e.anti) {
370
+ emit(types_1.ArchiveCategory.Special, types_1.ArchiveDetail.AntiItem, e, { name: e.name });
371
+ if (strips(types_1.ArchiveDetail.AntiItem, types_1.ArchiveCategory.Special))
372
+ decisions[i].remove = true;
373
+ }
374
+ const detail = e.kind === 'device' ? types_1.ArchiveDetail.Device : e.kind === 'fifo' ? types_1.ArchiveDetail.Fifo : e.kind === 'socket' ? types_1.ArchiveDetail.Socket : e.kind === 'other' ? types_1.ArchiveDetail.UnknownType : undefined;
375
+ if (detail === undefined)
376
+ continue;
377
+ emit(types_1.ArchiveCategory.Special, detail, e, { name: e.name, ...(e.typeCode ? { type: e.typeCode } : {}) });
378
+ if (strips(detail, types_1.ArchiveCategory.Special))
379
+ decisions[i].remove = true;
380
+ }
381
+ // Permissions: setuid, setgid and sticky bits on any entry, folders included.
382
+ const bits = [
383
+ [0o4000, types_1.ArchiveDetail.Setuid],
384
+ [0o2000, types_1.ArchiveDetail.Setgid],
385
+ [0o1000, types_1.ArchiveDetail.Sticky],
386
+ ];
387
+ for (const [i, e] of entries.entries()) {
388
+ const d = decisions[i];
389
+ for (const [bit, detail] of bits) {
390
+ if (e.mode === undefined || !(e.mode & bit))
391
+ continue;
392
+ emit(types_1.ArchiveCategory.Permissions, detail, e, { name: e.name, mode: `0${e.mode.toString(8)}` });
393
+ if (strips(detail, types_1.ArchiveCategory.Permissions))
394
+ d.mode = (d.mode ?? 0) & ~bit;
395
+ }
396
+ if (e.capabilities?.length) {
397
+ emit(types_1.ArchiveCategory.Permissions, types_1.ArchiveDetail.Capability, e, { name: e.name, attribute: e.capabilities.join(', ') });
398
+ if (strips(types_1.ArchiveDetail.Capability, types_1.ArchiveCategory.Permissions))
399
+ d.dropCapabilities = true;
400
+ }
401
+ }
402
+ return decisions;
403
+ }
@@ -0,0 +1,17 @@
1
+ import { type ArchiveOptions, ArchiveRisk, type ArchiveScoreBands, ArchiveCategory as C, ArchiveDetail as D, type DefuseFinding } from './types';
2
+ export declare const DEFAULT_BANDS: ArchiveScoreBands;
3
+ interface Combination {
4
+ name: string;
5
+ weight: number;
6
+ test(has: (c: C, d?: D) => boolean): boolean;
7
+ }
8
+ export declare const COMBINATIONS: Combination[];
9
+ /** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
10
+ export declare function weightOf(f: DefuseFinding, options: ArchiveOptions): number;
11
+ /** Highest weight in full, a quarter of each other kind's weight, plus combinations, capped at 100. */
12
+ export declare function scoreFindings(findings: DefuseFinding[], options: ArchiveOptions): {
13
+ score: number;
14
+ risk: ArchiveRisk;
15
+ };
16
+ export declare function riskFor(score: number, bands: ArchiveScoreBands): ArchiveRisk;
17
+ export {};
package/dist/score.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMBINATIONS = exports.DEFAULT_BANDS = void 0;
4
+ exports.weightOf = weightOf;
5
+ exports.scoreFindings = scoreFindings;
6
+ exports.riskFor = riskFor;
7
+ // Scoring as pdf-defuse src/score.ts does it, with archive-defuse's own combinations.
8
+ const findings_1 = require("./findings");
9
+ const types_1 = require("./types");
10
+ exports.DEFAULT_BANDS = { low: 1, medium: 25, high: 50, critical: 80 };
11
+ exports.COMBINATIONS = [
12
+ {
13
+ name: 'a name that leaves the folder with a link that leaves it',
14
+ weight: 20,
15
+ test: has => (has(types_1.ArchiveCategory.Name, types_1.ArchiveDetail.Traversal) || has(types_1.ArchiveCategory.Name, types_1.ArchiveDetail.Absolute) || has(types_1.ArchiveCategory.Name, types_1.ArchiveDetail.DriveLetter)) && (has(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.Outside) || has(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.ThroughLink)),
16
+ },
17
+ { name: 'setuid or setgid with a link out of the folder', weight: 15, test: has => (has(types_1.ArchiveCategory.Permissions, types_1.ArchiveDetail.Setuid) || has(types_1.ArchiveCategory.Permissions, types_1.ArchiveDetail.Setgid)) && has(types_1.ArchiveCategory.Link, types_1.ArchiveDetail.Outside) },
18
+ { name: 'a polyglot with data hidden inside', weight: 10, test: has => has(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.Polyglot) && (has(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.HiddenData) || has(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.Overlap)) },
19
+ ];
20
+ /** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
21
+ function weightOf(f, options) {
22
+ const w = options.scoreWeights ?? [];
23
+ const exact = w.find(x => x.category === f.category && x.detail === f.detail);
24
+ if (exact)
25
+ return exact.weight;
26
+ const cat = w.find(x => x.category === f.category && x.detail === undefined);
27
+ if (cat)
28
+ return cat.weight;
29
+ return (0, findings_1.findingSpec)(f.category, f.detail)?.weight ?? f.weight ?? 0;
30
+ }
31
+ /** Highest weight in full, a quarter of each other kind's weight, plus combinations, capped at 100. */
32
+ function scoreFindings(findings, options) {
33
+ // Each kind of finding counts once, so twenty copies of one minor finding do not add up to a critical file.
34
+ const byKind = new Map();
35
+ for (const f of findings) {
36
+ const k = `${f.category}/${f.detail}`;
37
+ byKind.set(k, Math.max(byKind.get(k) ?? 0, weightOf(f, options)));
38
+ }
39
+ const weights = [...byKind.values()].sort((a, b) => b - a);
40
+ let score = weights.length ? weights[0] + weights.slice(1).reduce((s, w) => s + w / 4, 0) : 0;
41
+ const has = (c, d) => findings.some(f => f.category === c && (d === undefined || f.detail === d) && weightOf(f, options) > 0);
42
+ for (const combo of exports.COMBINATIONS)
43
+ if (combo.test(has))
44
+ score += combo.weight;
45
+ score = Math.min(100, Math.round(score));
46
+ return { score, risk: riskFor(score, options.scoreBands ?? exports.DEFAULT_BANDS) };
47
+ }
48
+ function riskFor(score, bands) {
49
+ if (score >= bands.critical)
50
+ return types_1.ArchiveRisk.Critical;
51
+ if (score >= bands.high)
52
+ return types_1.ArchiveRisk.High;
53
+ if (score >= bands.medium)
54
+ return types_1.ArchiveRisk.Medium;
55
+ if (score >= bands.low)
56
+ return types_1.ArchiveRisk.Low;
57
+ return types_1.ArchiveRisk.None;
58
+ }
@@ -0,0 +1,75 @@
1
+ import type { Reader } from './io';
2
+ import type { Note } from './model';
3
+ import { type FileInfo, type StreamsInfo } from './sevenzip';
4
+ import type { Limits, Totals } from './stream-meter';
5
+ import { type ByteSource } from './types';
6
+ /** What kind of thing an entry is. */
7
+ export type EntryKind = 'file' | 'dir' | 'symlink' | 'device' | 'fifo' | 'socket';
8
+ /** One entry of a 7z archive. */
9
+ export interface SevenEntry {
10
+ index: number;
11
+ info: FileInfo;
12
+ kind: EntryKind;
13
+ /** Decoded size the header declares. */
14
+ size: number;
15
+ crc?: number;
16
+ /** The folder the entry's data sits in, if it has data. */
17
+ folder?: number;
18
+ /** Where the entry's data starts in its folder's output. */
19
+ start: number;
20
+ }
21
+ export interface SevenArchive {
22
+ /** Where the signature header starts: above 0 when other data comes first. */
23
+ offset: number;
24
+ entries: SevenEntry[];
25
+ streams?: StreamsInfo;
26
+ notes: Note[];
27
+ /** The header was encrypted and a password opened it. */
28
+ headerEncrypted: boolean;
29
+ /** Offset of the first byte after the last structure, when the file goes on past it. */
30
+ trailingAt?: number;
31
+ /** Absolute offset of each packed stream. */
32
+ packOffsets: number[];
33
+ /** Index of each folder's first packed stream. */
34
+ folderPack: number[];
35
+ }
36
+ export interface SevenEnv {
37
+ source: ByteSource;
38
+ reader: Reader;
39
+ size: number;
40
+ limits: Limits;
41
+ totals: Totals;
42
+ deadline?: number;
43
+ /** Passwords to try, in order. */
44
+ passwords: string[];
45
+ }
46
+ /** The Unix mode stored in the high 16 bits of the attributes, when the low bits say one is there. */
47
+ export declare function unixMode(attrib: number | undefined): number | undefined;
48
+ /**
49
+ * Reads a 7z archive's signature and headers, decoding an encoded header, and checks that its structures fit the file
50
+ * and each other. `offset` is where the signature was found.
51
+ */
52
+ export declare function openSevenZip(env: SevenEnv, offset: number): Promise<SevenArchive>;
53
+ /**
54
+ * The content of each entry, decoded folder by folder. Entries of one folder read in order share one decode; an entry
55
+ * before the last one read starts the folder again.
56
+ */
57
+ export declare class SevenReader {
58
+ private readonly env;
59
+ private readonly archive;
60
+ private open?;
61
+ /**
62
+ * The password that opened each encrypted folder. A folder opens with one password, the first the engine tries that
63
+ * opens it, so an entry that fails its CRC never sends the reader back to the start of the folder with another.
64
+ */
65
+ private readonly chosen;
66
+ /** Passwords that did not open a folder, by folder and password. */
67
+ private readonly refused;
68
+ constructor(env: SevenEnv, archive: SevenArchive);
69
+ /** True when the folder of `entry` is not encrypted, or `password` opens it. */
70
+ unlock(entry: SevenEntry, password: string): Promise<boolean>;
71
+ private folder;
72
+ /** The decoded content of an entry with data. It throws when the folder cannot decode it or its CRC does not match. */
73
+ content(entry: SevenEntry, password?: string): AsyncGenerator<Uint8Array>;
74
+ close(): Promise<void>;
75
+ }