@poe-platform/safe-bash 0.1.656 → 0.1.658

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 (129) hide show
  1. package/dist/safe-bash/chunks/{chunk-43HWIHXB.js → chunk-Z4UKD5FF.js} +192 -61
  2. package/dist/safe-bash/chunks/chunk-Z4UKD5FF.js.map +7 -0
  3. package/dist/safe-bash/chunks/{xz-U4QDTPZL.js → xz-4V5FXSPG.js} +555 -514
  4. package/dist/safe-bash/chunks/xz-4V5FXSPG.js.map +7 -0
  5. package/dist/safe-bash/commands/archive/index.d.ts +1 -1
  6. package/dist/safe-bash/commands/archive/index.d.ts.map +1 -1
  7. package/dist/safe-bash/commands/archive/internal.d.ts +32 -0
  8. package/dist/safe-bash/commands/archive/internal.d.ts.map +1 -1
  9. package/dist/safe-bash/commands/archive/internal.js.map +1 -1
  10. package/dist/safe-bash/commands/archive/unzip/arguments.d.ts +3 -0
  11. package/dist/safe-bash/commands/archive/unzip/arguments.d.ts.map +1 -1
  12. package/dist/safe-bash/commands/archive/unzip/arguments.js +31 -5
  13. package/dist/safe-bash/commands/archive/unzip/arguments.js.map +1 -1
  14. package/dist/safe-bash/commands/archive/unzip/safety.d.ts +4 -0
  15. package/dist/safe-bash/commands/archive/unzip/safety.d.ts.map +1 -1
  16. package/dist/safe-bash/commands/archive/unzip/safety.js +3 -0
  17. package/dist/safe-bash/commands/archive/unzip/safety.js.map +1 -1
  18. package/dist/safe-bash/commands/archive/unzip.d.ts.map +1 -1
  19. package/dist/safe-bash/commands/archive/unzip.js +159 -112
  20. package/dist/safe-bash/commands/archive/unzip.js.map +1 -1
  21. package/dist/safe-bash/commands/archive/zip/aes-primitives-unavailable.d.ts +2 -0
  22. package/dist/safe-bash/commands/archive/zip/aes-primitives-unavailable.d.ts.map +1 -0
  23. package/dist/safe-bash/commands/archive/zip/aes-primitives-unavailable.js +3 -0
  24. package/dist/safe-bash/commands/archive/zip/aes-primitives-unavailable.js.map +1 -0
  25. package/dist/safe-bash/commands/archive/zip/aes-primitives.d.ts +2 -0
  26. package/dist/safe-bash/commands/archive/zip/aes-primitives.d.ts.map +1 -0
  27. package/dist/safe-bash/commands/archive/zip/aes-primitives.js +3 -0
  28. package/dist/safe-bash/commands/archive/zip/aes-primitives.js.map +1 -0
  29. package/dist/safe-bash/commands/archive/zip/aes.d.ts +18 -0
  30. package/dist/safe-bash/commands/archive/zip/aes.d.ts.map +1 -0
  31. package/dist/safe-bash/commands/archive/zip/aes.js +157 -0
  32. package/dist/safe-bash/commands/archive/zip/aes.js.map +1 -0
  33. package/dist/safe-bash/commands/archive/zip/crypto.d.ts +16 -0
  34. package/dist/safe-bash/commands/archive/zip/crypto.d.ts.map +1 -1
  35. package/dist/safe-bash/commands/archive/zip/crypto.js +85 -0
  36. package/dist/safe-bash/commands/archive/zip/crypto.js.map +1 -1
  37. package/dist/safe-bash/commands/archive/zip/grow.d.ts +4 -0
  38. package/dist/safe-bash/commands/archive/zip/grow.d.ts.map +1 -0
  39. package/dist/safe-bash/commands/archive/zip/grow.js +3 -0
  40. package/dist/safe-bash/commands/archive/zip/grow.js.map +1 -0
  41. package/dist/safe-bash/commands/archive/zip/help.d.ts +4 -2
  42. package/dist/safe-bash/commands/archive/zip/help.d.ts.map +1 -1
  43. package/dist/safe-bash/commands/archive/zip/help.js +98 -3
  44. package/dist/safe-bash/commands/archive/zip/help.js.map +1 -1
  45. package/dist/safe-bash/commands/archive/zip/line-endings.d.ts +1 -0
  46. package/dist/safe-bash/commands/archive/zip/line-endings.d.ts.map +1 -1
  47. package/dist/safe-bash/commands/archive/zip/line-endings.js +158 -0
  48. package/dist/safe-bash/commands/archive/zip/line-endings.js.map +1 -1
  49. package/dist/safe-bash/commands/archive/zip/log.d.ts +23 -0
  50. package/dist/safe-bash/commands/archive/zip/log.d.ts.map +1 -0
  51. package/dist/safe-bash/commands/archive/zip/log.js +159 -0
  52. package/dist/safe-bash/commands/archive/zip/log.js.map +1 -0
  53. package/dist/safe-bash/commands/archive/zip/lzma.d.ts +10 -0
  54. package/dist/safe-bash/commands/archive/zip/lzma.d.ts.map +1 -0
  55. package/dist/safe-bash/commands/archive/zip/lzma.js +41 -0
  56. package/dist/safe-bash/commands/archive/zip/lzma.js.map +1 -0
  57. package/dist/safe-bash/commands/archive/zip/names.d.ts +3 -0
  58. package/dist/safe-bash/commands/archive/zip/names.d.ts.map +1 -0
  59. package/dist/safe-bash/commands/archive/zip/names.js +34 -0
  60. package/dist/safe-bash/commands/archive/zip/names.js.map +1 -0
  61. package/dist/safe-bash/commands/archive/zip/options.d.ts +7 -0
  62. package/dist/safe-bash/commands/archive/zip/options.d.ts.map +1 -1
  63. package/dist/safe-bash/commands/archive/zip/options.js +88 -17
  64. package/dist/safe-bash/commands/archive/zip/options.js.map +1 -1
  65. package/dist/safe-bash/commands/archive/zip/repair.d.ts +11 -0
  66. package/dist/safe-bash/commands/archive/zip/repair.d.ts.map +1 -0
  67. package/dist/safe-bash/commands/archive/zip/repair.js +392 -0
  68. package/dist/safe-bash/commands/archive/zip/repair.js.map +1 -0
  69. package/dist/safe-bash/commands/archive/zip/safety.d.ts +23 -4
  70. package/dist/safe-bash/commands/archive/zip/safety.d.ts.map +1 -1
  71. package/dist/safe-bash/commands/archive/zip/safety.js +73 -23
  72. package/dist/safe-bash/commands/archive/zip/safety.js.map +1 -1
  73. package/dist/safe-bash/commands/archive/zip/sfx.d.ts +5 -0
  74. package/dist/safe-bash/commands/archive/zip/sfx.d.ts.map +1 -0
  75. package/dist/safe-bash/commands/archive/zip/sfx.js +56 -0
  76. package/dist/safe-bash/commands/archive/zip/sfx.js.map +1 -0
  77. package/dist/safe-bash/commands/archive/zip/test-command.d.ts +6 -0
  78. package/dist/safe-bash/commands/archive/zip/test-command.d.ts.map +1 -0
  79. package/dist/safe-bash/commands/archive/zip/test-command.js +101 -0
  80. package/dist/safe-bash/commands/archive/zip/test-command.js.map +1 -0
  81. package/dist/safe-bash/commands/archive/zip/volumes.d.ts +22 -0
  82. package/dist/safe-bash/commands/archive/zip/volumes.d.ts.map +1 -0
  83. package/dist/safe-bash/commands/archive/zip/volumes.js +265 -0
  84. package/dist/safe-bash/commands/archive/zip/volumes.js.map +1 -0
  85. package/dist/safe-bash/commands/archive/zip/zip64.d.ts +20 -1
  86. package/dist/safe-bash/commands/archive/zip/zip64.d.ts.map +1 -1
  87. package/dist/safe-bash/commands/archive/zip/zip64.js +127 -17
  88. package/dist/safe-bash/commands/archive/zip/zip64.js.map +1 -1
  89. package/dist/safe-bash/commands/archive/zip-format.d.ts +30 -5
  90. package/dist/safe-bash/commands/archive/zip-format.d.ts.map +1 -1
  91. package/dist/safe-bash/commands/archive/zip-format.js +489 -91
  92. package/dist/safe-bash/commands/archive/zip-format.js.map +1 -1
  93. package/dist/safe-bash/commands/archive/zip.d.ts.map +1 -1
  94. package/dist/safe-bash/commands/archive/zip.js +729 -128
  95. package/dist/safe-bash/commands/archive/zip.js.map +1 -1
  96. package/dist/safe-bash/commands/bytes/compression/bounded-codec.d.ts +7 -0
  97. package/dist/safe-bash/commands/bytes/compression/bounded-codec.d.ts.map +1 -1
  98. package/dist/safe-bash/commands/bytes/compression/bounded-codec.js.map +1 -1
  99. package/dist/safe-bash/commands/bytes/compression/codec-loader.d.ts.map +1 -1
  100. package/dist/safe-bash/commands/bytes/compression/codec-loader.js +8 -1
  101. package/dist/safe-bash/commands/bytes/compression/codec-loader.js.map +1 -1
  102. package/dist/safe-bash/commands/bytes/compression/native/generated/xz.mjs +1 -1
  103. package/dist/safe-bash/commands/bytes/compression/native/sources.json +2 -2
  104. package/dist/safe-bash/commands/bytes/compression/native/types.d.ts +1 -0
  105. package/dist/safe-bash/commands/bytes/compression/native/types.d.ts.map +1 -1
  106. package/dist/safe-bash/commands/python/cache.d.ts +11 -0
  107. package/dist/safe-bash/commands/python/cache.d.ts.map +1 -0
  108. package/dist/safe-bash/commands/python/cache.js +41 -0
  109. package/dist/safe-bash/commands/python/cache.js.map +1 -0
  110. package/dist/safe-bash/commands/python/index.browser.js +7 -1
  111. package/dist/safe-bash/commands/python/index.d.ts +6 -2
  112. package/dist/safe-bash/commands/python/index.d.ts.map +1 -1
  113. package/dist/safe-bash/commands/python/index.js +16 -10
  114. package/dist/safe-bash/commands/python/index.js.map +1 -1
  115. package/dist/safe-bash/commands/python/manifest.d.ts +23 -0
  116. package/dist/safe-bash/commands/python/manifest.d.ts.map +1 -0
  117. package/dist/safe-bash/commands/python/manifest.js +48 -0
  118. package/dist/safe-bash/commands/python/manifest.js.map +1 -0
  119. package/dist/safe-bash/commands/python/provisioning.d.ts +17 -13
  120. package/dist/safe-bash/commands/python/provisioning.d.ts.map +1 -1
  121. package/dist/safe-bash/commands/python/provisioning.js +88 -46
  122. package/dist/safe-bash/commands/python/provisioning.js.map +1 -1
  123. package/dist/safe-bash/commands/text.js +1 -1
  124. package/dist/safe-bash/commands/text.js.map +1 -1
  125. package/dist/safe-bash/core.browser.js +3113 -718
  126. package/dist/safe-bash/core.browser.js.map +4 -4
  127. package/package.json +11 -3
  128. package/dist/safe-bash/chunks/chunk-43HWIHXB.js.map +0 -7
  129. package/dist/safe-bash/chunks/xz-U4QDTPZL.js.map +0 -7
@@ -1,5 +1,13 @@
1
+ import { zipDosName } from "./zip/names.js";
2
+ import { readZipSfx } from "./zip/sfx.js";
3
+ import { adjustZipSfx, repairZip } from "./zip/repair.js";
4
+ import { zipGrowRecords } from "./zip/grow.js";
5
+ import { ZipLog } from "./zip/log.js";
6
+ import { parseZipTestCommand, testZipCommand } from "./zip/test-command.js";
1
7
  import { inspectZipMoveSource, removeZipSources } from "./zip/move.js";
2
- import { zipToCrlf } from "./zip/line-endings.js";
8
+ import { readZipPassword, ZipHostFailure } from "./zip/crypto.js";
9
+ import { zipEncryptionProfile } from "./zip/aes.js";
10
+ import { zipToCrlf, zipFromCrlf } from "./zip/line-endings.js";
3
11
  import { parseZipDate, zipDateMatches, zipLatestTime } from "./zip/dates.js";
4
12
  import { zipEnvironmentArguments } from "./zip/environment.js";
5
13
  import { readZipComment, ZipCommentInput } from "./zip/comments.js";
@@ -12,14 +20,25 @@ import { publicDiagnosticMessage } from "../../diagnostics.js";
12
20
  import { escapeText } from "../../escaping.js";
13
21
  import { Budget, checkPath, display, fail, hasIdentity, sameIdentity, settings, text, vfsPath } from "./internal.js";
14
22
  import { decodeZipEntry, makeZipEntry, readZipArchive, writeZipArchive, streamZipArchive, updateZipExtras, setZipEntryComment } from "./zip-format.js";
15
- import { zipHelp, zipExtendedHelp } from "./zip/help.js";
16
- import { publishZip, ZipScope } from "./zip/safety.js";
23
+ import { zipHelp, zipExtendedHelp, zipVersion, zipLicense } from "./zip/help.js";
24
+ import { publishZip, stageZip, ZipScope } from "./zip/safety.js";
25
+ import { splitSize, splitZipVolumes, resolveZipVolumes, publishZipVolumes, volumeName } from "./zip/volumes.js";
17
26
  import { Selection } from "./unzip/arguments.js";
18
- import { normalizeZipOption, ZipFailure } from "./zip/options.js";
27
+ import { normalizeZipOption, reservedZipShortOptions, ZipFailure, parseZipDotSize, zipLongOptions, zipNegatableOptions, zipDisplaySize, zipPublicText, zipPasswordArgument } from "./zip/options.js";
19
28
  const defaultStoreSuffixes = [".Z", ".zip", ".zoo", ".arc", ".lzh", ".arj"];
20
- async function parse(scope, limits) {
29
+ async function parse(scope, limits, defaults) {
21
30
  const context = scope.context;
22
31
  const args = zipEnvironmentArguments(context.env, context.args, limits);
32
+ const rawArguments = context.argumentValues ? getCommandArguments(context) : undefined;
33
+ const passwordArguments = new Set();
34
+ const defaultsCount = args.length - context.args.length;
35
+ const validateArgumentText = () => {
36
+ if (rawArguments)
37
+ for (const [index, value] of rawArguments.values.entries()) {
38
+ if (!passwordArguments.has(index))
39
+ text(shellValueBytes(value));
40
+ }
41
+ };
23
42
  if (args.length > limits.maxArgumentBytes)
24
43
  fail("argument count limit exceeded");
25
44
  let bytes = 0;
@@ -38,10 +57,11 @@ async function parse(scope, limits) {
38
57
  fail("argument byte limit exceeded");
39
58
  rawBytes += size;
40
59
  }
41
- for (const value of argumentsValue.values)
42
- text(shellValueBytes(value));
43
60
  }
44
61
  let archive;
62
+ let password;
63
+ let encrypt = defaults?.encryption !== undefined;
64
+ let aes = zipEncryptionProfile(defaults?.encryption ?? "zipcrypto");
45
65
  let output;
46
66
  let action = "add";
47
67
  let recursive = false;
@@ -49,24 +69,55 @@ async function parse(scope, limits) {
49
69
  let noWild = false;
50
70
  let stopAtDirectories = false;
51
71
  let quiet = false;
72
+ let verbose = false;
73
+ let showFiles;
74
+ let showCommand = false;
75
+ let showOptions = false;
76
+ let debug = false;
77
+ let displayBytes = false;
78
+ let displayCounts = false;
79
+ let displayUsize = false;
80
+ let displayVolume = false;
81
+ let dotSize = 0;
82
+ let dotsSet = false;
83
+ let globalDots = false;
84
+ const commandOptions = [];
85
+ const commandPaths = [];
52
86
  let junkPaths = false;
87
+ let dosNames = false;
88
+ let fifo = false;
53
89
  let omitDirectories = false;
54
90
  let storeLinks = false;
55
91
  let test = false;
92
+ let testCommand;
93
+ let difference = false;
94
+ let grow = false;
95
+ let tempPath;
96
+ let junkSfx = false;
97
+ let repair;
98
+ let adjust = false;
99
+ let logPath;
100
+ let logAppend = false;
101
+ let logInfo = false;
56
102
  let mustMatch = false;
57
103
  let filesync = false;
58
104
  let archiveComment = false;
59
105
  let entryComments = false;
60
106
  let latestTime = false;
61
107
  let toCrlf = false;
108
+ let fromCrlf = false;
62
109
  let move = false;
63
110
  let fromDate;
64
111
  let beforeDate;
65
112
  let descriptors = false;
113
+ let split;
114
+ let splitPause = false, splitVerbose = false, splitBell = false;
66
115
  let zip64;
67
116
  let metadata = "default";
68
117
  let level = 6;
69
- let method = "deflate";
118
+ let method = defaults?.compression ?? "deflate";
119
+ if (method !== "store" && method !== "deflate" && method !== "bzip2" && method !== "lzma")
120
+ fail("ZIP unsupported compression method");
70
121
  let suffixes = defaultStoreSuffixes;
71
122
  let stdinNames = false;
72
123
  let literal = false;
@@ -75,25 +126,212 @@ async function parse(scope, limits) {
75
126
  const includes = [];
76
127
  const excludes = [];
77
128
  for (let index = 0; index < args.length; index++) {
129
+ const startIndex = index;
130
+ let listOption = false;
78
131
  const original = args[index];
79
132
  checkPath(original, limits);
133
+ if (!literal && (original === "--encryption" || original.startsWith("--encryption="))) {
134
+ const value = original === "--encryption" ? args[++index] : original.slice(13);
135
+ if (value === undefined)
136
+ fail("ZIP encryption option requires a profile");
137
+ aes = zipEncryptionProfile(value);
138
+ encrypt = true;
139
+ commandOptions.push(...args.slice(startIndex, index + 1));
140
+ continue;
141
+ }
80
142
  const argument = literal ? original : normalizeZipOption(original);
81
143
  if (!literal && argument === "--") {
82
144
  if (archive === undefined)
83
145
  throw new ZipFailure(16, "Invalid command arguments", "can't use -- before archive name");
84
146
  literal = true;
147
+ commandPaths.push(original);
85
148
  }
86
149
  else if (!literal && argument.startsWith("-") && argument !== "-") {
150
+ commandOptions.push(original);
151
+ if (argument === "--version") {
152
+ validateArgumentText();
153
+ return { information: "version" };
154
+ }
87
155
  for (let offset = 1; offset < argument.length; offset++) {
156
+ if (reservedZipShortOptions.has(argument.slice(offset, offset + 2))) {
157
+ throw new ZipFailure(16, "Invalid command arguments", `unsupported option: -${argument.slice(offset, offset + 2)}`);
158
+ }
88
159
  const flag = argument[offset];
89
- if (flag === "h") {
160
+ const pair = argument.slice(offset, offset + 2);
161
+ if (pair === "sp" || pair === "sv" || pair === "sb") {
162
+ if (pair === "sp") {
163
+ splitPause = true;
164
+ descriptors = true;
165
+ }
166
+ if (pair === "sv")
167
+ splitVerbose = true;
168
+ if (pair === "sb")
169
+ splitBell = true;
170
+ offset++;
171
+ }
172
+ else if (flag === "s" && !["sc", "sd", "sf", "so"].includes(pair)) {
173
+ let value = argument.slice(offset + 1) || args[++index];
174
+ if (value?.startsWith("="))
175
+ value = value.slice(1);
176
+ if (!value)
177
+ throw new ZipFailure(16, "Invalid command arguments", "split size requires a value");
178
+ split = splitSize(value);
179
+ break;
180
+ }
181
+ else if (pair === "FI") {
182
+ fifo = argument[offset + 2] !== "-";
183
+ offset += fifo ? 1 : 2;
184
+ }
185
+ else if (pair === "RE") {
186
+ // Unix bracket lists are already enabled in the bounded glob matcher.
187
+ if (argument[offset + 2] === "-")
188
+ throw new ZipFailure(16, "Invalid command arguments", "option RE is not negatable");
189
+ offset++;
190
+ }
191
+ else if (flag === "k") {
192
+ if (argument[offset + 1] === "-")
193
+ throw new ZipFailure(16, "Invalid command arguments", "option k is not negatable");
194
+ dosNames = true;
195
+ }
196
+ else if (pair === "DF") {
197
+ difference = true;
198
+ offset++;
199
+ }
200
+ else if (pair === "la" || pair === "li") {
201
+ if (pair === "la")
202
+ logAppend = true;
203
+ else
204
+ logInfo = true;
205
+ offset++;
206
+ }
207
+ else if (pair === "TT" || pair === "lf" || flag === "b") {
208
+ const option = flag === "b" ? "b" : pair;
209
+ if (option !== "b")
210
+ offset++;
211
+ let value = argument.slice(offset + 1) || args[++index];
212
+ if (value?.startsWith("="))
213
+ value = value.slice(1);
214
+ if (!value)
215
+ throw new ZipFailure(16, "Invalid command arguments", `option ${option} requires a value`);
216
+ if (option === "b")
217
+ tempPath = value;
218
+ else if (option === "lf")
219
+ logPath = value;
220
+ else
221
+ testCommand = parseZipTestCommand(value);
222
+ break;
223
+ }
224
+ else if (flag === "g")
225
+ grow = true;
226
+ else if (flag === "J")
227
+ junkSfx = true;
228
+ else if (flag === "A")
229
+ adjust = true;
230
+ else if (flag === "F" && pair !== "FS" && pair !== "FI") {
231
+ repair = pair === "FF" ? "FF" : "F";
232
+ if (pair === "FF")
233
+ offset++;
234
+ }
235
+ else if (flag === "P") {
236
+ let value = argument.slice(offset + 1);
237
+ const attached = value.length > 0;
238
+ if (!value) {
239
+ value = args[++index];
240
+ if (value === undefined)
241
+ throw new ZipFailure(16, "Invalid command arguments", "password option requires a value");
242
+ }
243
+ else if (value.startsWith("="))
244
+ value = value.slice(1);
245
+ const rawIndex = index - defaultsCount;
246
+ passwordArguments.add(rawIndex);
247
+ const raw = rawArguments?.bytes(rawIndex);
248
+ const prefix = attached ? original.startsWith("--") ? original.indexOf("=") + 1 : offset + 1 + (original[offset + 1] === "=" ? 1 : 0) : 0;
249
+ password = raw ? new Uint8Array(raw.subarray(prefix)) : Buffer.from(value);
250
+ if (password.includes(0))
251
+ fail("ZIP invalid password bytes");
252
+ if (!password.length)
253
+ throw new ZipFailure(16, "Invalid command arguments", "zero length password not allowed");
254
+ encrypt = true;
255
+ break;
256
+ }
257
+ else if (flag === "e")
258
+ encrypt = true;
259
+ else if (["db", "dc", "dd", "dg", "du", "dv", "ds", "sc", "sd", "sf", "so"].includes(pair)) {
260
+ offset++;
261
+ const negated = argument[offset + 1] === "-";
262
+ if (negated && !zipNegatableOptions.has(pair))
263
+ throw new ZipFailure(16, "Invalid command arguments", `option ${pair} is not negatable`);
264
+ if (negated)
265
+ offset++;
266
+ if (pair === "ds") {
267
+ let value = argument.slice(offset + 1) || args[++index];
268
+ if (value === undefined)
269
+ throw new ZipFailure(16, "Invalid command arguments", "option ds requires a value");
270
+ if (value.startsWith("="))
271
+ value = value.slice(1);
272
+ dotSize = parseZipDotSize(value);
273
+ dotsSet = true;
274
+ break;
275
+ }
276
+ if (pair === "db")
277
+ displayBytes = !negated;
278
+ else if (pair === "dc")
279
+ displayCounts = !negated;
280
+ else if (pair === "du")
281
+ displayUsize = !negated;
282
+ else if (pair === "dv")
283
+ displayVolume = !negated;
284
+ else if (pair === "sf")
285
+ showFiles = !negated;
286
+ else if (pair === "sc")
287
+ showCommand = true;
288
+ else if (pair === "sd")
289
+ debug = true;
290
+ else if (pair === "so")
291
+ showOptions = true;
292
+ else if (pair === "dd") {
293
+ globalDots = false;
294
+ if (negated)
295
+ dotsSet = false;
296
+ else {
297
+ if (!dotsSet)
298
+ dotSize = 10 * 1024 ** 2;
299
+ dotsSet = true;
300
+ }
301
+ }
302
+ else if (pair === "dg") {
303
+ globalDots = !negated;
304
+ if (!negated) {
305
+ if (!dotsSet)
306
+ dotSize = 10 * 1024 ** 2;
307
+ dotsSet = true;
308
+ }
309
+ }
310
+ }
311
+ else if (flag === "L" || flag === "v") {
312
+ if (argument[offset + 1] === "-")
313
+ throw new ZipFailure(16, "Invalid command arguments", `option ${flag} is not negatable`);
314
+ if (flag === "L") {
315
+ validateArgumentText();
316
+ return { information: "license" };
317
+ }
318
+ if (args.length === 1 && original === "-v") {
319
+ validateArgumentText();
320
+ return { information: "version" };
321
+ }
322
+ verbose = true;
323
+ quiet = false;
324
+ }
325
+ else if (flag === "h") {
90
326
  if (argument[offset + 1] === "-")
91
327
  throw new ZipFailure(16, "Invalid command arguments", "option h is not negatable");
92
328
  if (argument[offset + 1] === "2") {
93
329
  if (argument[offset + 2] === "-")
94
330
  throw new ZipFailure(16, "Invalid command arguments", "option h2 is not negatable");
331
+ validateArgumentText();
95
332
  return { information: "more-help" };
96
333
  }
334
+ validateArgumentText();
97
335
  return { information: "help" };
98
336
  }
99
337
  else if (flag === "r" || flag === "R") {
@@ -150,9 +388,10 @@ async function parse(scope, limits) {
150
388
  else if (flag === "o")
151
389
  latestTime = true;
152
390
  else if (flag === "l") {
153
- if (argument[offset + 1] === "l")
154
- throw new ZipFailure(16, "Invalid command arguments", `unsupported option: ${argument}`);
155
- toCrlf = true;
391
+ fromCrlf = argument[offset + 1] === "l";
392
+ toCrlf = !fromCrlf;
393
+ if (fromCrlf)
394
+ offset++;
156
395
  }
157
396
  else if (flag === "m")
158
397
  move = true;
@@ -223,15 +462,16 @@ async function parse(scope, limits) {
223
462
  fail("suffix count limit exceeded");
224
463
  }
225
464
  else {
226
- const matches = ["store", "deflate", "bzip2"].filter(name => name.startsWith(value.toLowerCase()));
465
+ const matches = ["store", "deflate", "bzip2", "lzma"].filter(name => name.startsWith(value.toLowerCase()));
227
466
  const selected = matches.length === 1 ? matches[0] : undefined;
228
467
  if (!selected)
229
468
  throw new ZipFailure(16, "Invalid command arguments", "Option -Z (--compression-method): unknown method");
230
- method = selected === "store" ? "store" : selected === "bzip2" ? "bzip2" : "deflate";
469
+ method = selected;
231
470
  }
232
471
  break;
233
472
  }
234
473
  else if (flag === "i" || flag === "x") {
474
+ listOption = offset + 1 === argument.length;
235
475
  const patterns = flag === "i" ? includes : excludes;
236
476
  const before = patterns.length;
237
477
  const append = (pattern) => {
@@ -239,7 +479,7 @@ async function parse(scope, limits) {
239
479
  checkPath(pattern, limits);
240
480
  if (includes.length + excludes.length >= limits.maxMembers)
241
481
  fail("pattern count limit exceeded");
242
- patterns.push(pattern);
482
+ patterns.push(dosNames ? zipDosName(pattern) : pattern);
243
483
  };
244
484
  const attached = offset + 1 < argument.length;
245
485
  if (attached) {
@@ -262,19 +502,38 @@ async function parse(scope, limits) {
262
502
  break;
263
503
  }
264
504
  else
265
- throw new ZipFailure(16, "Invalid command arguments", `unsupported option: ${argument}`);
505
+ throw new ZipFailure(16, "Invalid command arguments", `unsupported option: -${flag}`);
266
506
  }
507
+ commandOptions.push(...args.slice(startIndex + 1, index + 1));
508
+ if (listOption && args[index] !== "@")
509
+ commandOptions.push("@");
267
510
  }
268
- else if (archive === undefined)
511
+ else if (archive === undefined) {
269
512
  archive = argument;
513
+ commandPaths.push(original);
514
+ }
270
515
  else {
271
516
  if (operands.length >= limits.maxMembers)
272
517
  fail("operand limit exceeded");
273
518
  if (firstOperand < 0)
274
519
  firstOperand = index;
275
520
  operands.push(argument);
521
+ commandPaths.push(original);
276
522
  }
277
523
  }
524
+ validateArgumentText();
525
+ if (repair || adjust) {
526
+ if (repair && adjust || junkSfx || grow || move || split !== undefined || operands.length || stdinNames || includes.length || excludes.length || action !== "add" || archiveComment || entryComments || latestTime || filesync || encrypt || showFiles !== undefined || difference || toCrlf || fromCrlf || recursive || recursivePatterns)
527
+ throw new ZipFailure(16, "Invalid command arguments", "repair/adjust cannot be combined with archive modification or selection");
528
+ if (!archive || archive === "-" || repair && (!output || output === "-"))
529
+ throw new ZipFailure(16, "Invalid command arguments", "recovery requires a named source and a separate --out file");
530
+ }
531
+ if (showCommand)
532
+ return { information: "command", command: [context.command, ...commandOptions, ...commandPaths], debug };
533
+ if (showOptions)
534
+ return { information: "options", debug };
535
+ if (verbose && !dotsSet && !dotSize)
536
+ dotSize = 10 * 1024 ** 2;
278
537
  if (archive === undefined) {
279
538
  if (includes.length || excludes.length)
280
539
  throw new ZipFailure(16, "Invalid command arguments", "nothing to select from");
@@ -294,7 +553,9 @@ async function parse(scope, limits) {
294
553
  if (action === "add" && archive !== "-" && !operands.length && !stdinNames)
295
554
  action = "copy";
296
555
  }
297
- if (action === "copy" && output === undefined)
556
+ if (showFiles !== undefined && !operands.length && !stdinNames && (includes.length || excludes.length))
557
+ throw new ZipFailure(16, "Invalid command arguments", "nothing to select from");
558
+ if (action === "copy" && output === undefined && showFiles === undefined)
298
559
  throw new ZipFailure(16, "Invalid command arguments", "-U (--copy) requires -O (--out)");
299
560
  const names = [];
300
561
  if (stdinNames) {
@@ -325,9 +586,16 @@ async function parse(scope, limits) {
325
586
  }
326
587
  if (recursivePatterns && !names.length && !operands.length)
327
588
  throw new ZipFailure(16, "Invalid command arguments", "nothing to select from");
328
- if (filesync && action !== "add")
589
+ if (difference && (archive === "-" || output === undefined || action === "copy" || action === "delete"))
590
+ throw new ZipFailure(16, "Invalid command arguments", "difference archive requires separate output and cannot use delete or copy");
591
+ if (logPath !== undefined && !logPath.slice(logPath.lastIndexOf("/") + 1).includes("."))
592
+ logPath += ".log";
593
+ if (filesync && (action !== "add" || grow))
329
594
  throw new ZipFailure(16, "Invalid command arguments", "can't use -d, -f, -u, -U, or -g with filesync -FS\n");
330
- return { args, action, archive, output, recursive, recursivePatterns, noWild, stopAtDirectories, quiet, junkPaths, omitDirectories, storeLinks, test, mustMatch, filesync, archiveComment, entryComments, latestTime, toCrlf, move, fromDate, beforeDate, descriptors, zip64, metadata, includes, excludes, level, method, suffixes, operands: [...names, ...operands], firstOperand };
595
+ const diagnosticArgs = args.map((value, index) => passwordArguments.has(index - defaultsCount) ? "[redacted]" : zipPublicText(value));
596
+ if (split && (archive === "-" || grow))
597
+ throw new ZipFailure(16, "Invalid command arguments", "split output requires a file and cannot grow");
598
+ return { repair, adjust, split, splitPause, splitVerbose, splitBell, password, encrypt, ...(aes ? { aes } : {}), args: diagnosticArgs, action, archive, output, recursive, recursivePatterns, noWild, stopAtDirectories, quiet, verbose, showFiles, debug, displayBytes, displayCounts, displayUsize, displayVolume, dotSize, globalDots, junkPaths, dosNames, fifo, omitDirectories, storeLinks: storeLinks && !dosNames, test, testCommand, difference, grow, tempPath, junkSfx, logPath, logAppend, logInfo, mustMatch, filesync, archiveComment, entryComments, latestTime, toCrlf, fromCrlf, move, fromDate, beforeDate, descriptors, zip64, metadata, includes, excludes, level, method, suffixes, operands: [...names, ...operands], firstOperand };
331
599
  }
332
600
  function memberName(path, limits) {
333
601
  if (path.startsWith("//") && !path.startsWith("///"))
@@ -374,13 +642,23 @@ async function filterName(name, selection, includeCount) {
374
642
  }
375
643
  return included;
376
644
  }
377
- async function prepare(scope, parsed, budget) {
645
+ async function prepare(scope, parsed, budget, log, host) {
378
646
  const context = scope.context;
379
647
  const limits = budget.limits;
380
648
  if (parsed.archive === "-" && parsed.action !== "add")
381
649
  throw new ZipFailure(16, "Invalid command arguments", "can't use -d, -f, -u, -U, or -g on stdout\n");
650
+ let temporary;
651
+ if (parsed.tempPath !== undefined && parsed.showFiles === undefined) {
652
+ const name = vfsPath(context.cwd, parsed.tempPath);
653
+ checkPath(name, limits);
654
+ const parent = await scope.operation(() => context.fs.realpath(name, { signal: context.signal }));
655
+ const parentStat = await scope.operation(() => context.fs.lstat(parent, { signal: context.signal }));
656
+ if (parentStat.type !== "directory" || !hasIdentity(parentStat))
657
+ fail("ZIP temporary path requires a directory with known backing identity");
658
+ temporary = { name, parent, parentStat };
659
+ }
382
660
  let publication;
383
- if (parsed.archive !== "-") {
661
+ if (parsed.archive !== "-" && parsed.showFiles === undefined) {
384
662
  const outputName = vfsPath(context.cwd, parsed.output ?? parsed.archive);
385
663
  const parentName = dirname(outputName);
386
664
  const parent = await scope.operation(() => context.fs.realpath(parentName, { signal: context.signal }));
@@ -389,11 +667,17 @@ async function prepare(scope, parsed, budget) {
389
667
  checkPath(output, limits);
390
668
  const existing = await scope.stat(output);
391
669
  publication = { output, parentName, parent, parentStat, existing };
670
+ if (temporary)
671
+ publication = { ...publication, stagingName: temporary.name, stagingParent: temporary.parent, stagingParentStat: temporary.parentStat };
392
672
  }
393
- const output = publication?.output;
673
+ const output = publication?.output ?? (parsed.output === undefined ? undefined : vfsPath(context.cwd, parsed.output));
394
674
  let input = publication?.output;
395
675
  let existing = publication?.existing;
396
- if (parsed.output !== undefined) {
676
+ if (parsed.showFiles !== undefined && parsed.archive !== "-") {
677
+ input = vfsPath(context.cwd, parsed.archive);
678
+ existing = await scope.stat(input);
679
+ }
680
+ if (parsed.output !== undefined && parsed.showFiles === undefined) {
397
681
  if (parsed.archive === "-") {
398
682
  input = undefined;
399
683
  existing = undefined;
@@ -415,7 +699,7 @@ async function prepare(scope, parsed, budget) {
415
699
  throw new ZipFailure(16, "Invalid command arguments", `--out path must be different than in path: ${parsed.archive}`);
416
700
  }
417
701
  }
418
- if ((parsed.action === "copy" || parsed.output !== undefined && parsed.archive !== "-") && !existing)
702
+ if ((parsed.action === "copy" || parsed.showFiles !== undefined && !parsed.operands.length || parsed.output !== undefined && parsed.archive !== "-") && !existing)
419
703
  throw new ZipFailure(18, "File not found or no read permission", parsed.archive);
420
704
  if (publication?.existing && (publication.existing.type !== "file" || !hasIdentity(publication.existing) || publication.existing.nlink !== 1))
421
705
  fail("output archive requires a regular, single-link file with known backing identity");
@@ -425,46 +709,110 @@ async function prepare(scope, parsed, budget) {
425
709
  fail("updating archive requires a regular, single-link file with known backing identity; archive aliases are unsupported");
426
710
  let archive = { entries: [], comment: new Uint8Array() };
427
711
  let originalBytes;
712
+ let inputPaths = [];
713
+ let inputSplit;
428
714
  if (existing && input) {
429
715
  if (!Number.isSafeInteger(existing.size) || existing.size < 0 || existing.size > limits.maxArchiveBytes)
430
716
  fail("archive byte limit exceeded");
431
717
  const bytes = await collectBytes(scope.input(input), { maxBytes: limits.maxArchiveBytes, signal: context.signal });
432
718
  if (bytes.length !== existing.size)
433
719
  fail("archive changed while reading");
434
- archive = await readZipArchive(bytes, limits, context.signal);
435
- if (parsed.latestTime || parsed.test)
720
+ const resolved = parsed.repair || parsed.adjust ? { bytes, paths: [input] } : await resolveZipVolumes(scope, input, bytes, host);
721
+ inputPaths = resolved.paths;
722
+ await log?.protect(inputPaths);
723
+ if (resolved.disks && resolved.disks.starts.length > 1)
724
+ inputSplit = Math.max(65536, ...resolved.disks.lengths.slice(0, -1));
725
+ if (publication && parsed.output !== undefined)
726
+ for (const path of inputPaths) {
727
+ const stat = await scope.stat(path);
728
+ if (publication.output === path || stat && publication.existing && sameIdentity(stat, publication.existing))
729
+ fail("ZIP output aliases an input volume");
730
+ }
731
+ if ((parsed.split || resolved.disks) && parsed.output === undefined && parsed.showFiles === undefined)
732
+ throw new ZipFailure(16, "Invalid command arguments", "split archives require a separate --out destination");
733
+ if (parsed.repair || parsed.adjust) {
734
+ if (!publication)
735
+ fail("ZIP repair requires a file destination");
736
+ let outputBytes, partial = false;
737
+ if (parsed.repair) {
738
+ const recovered = await repairZip(bytes, parsed.repair, limits, context.signal, parsed.password);
739
+ partial = recovered.partial;
740
+ outputBytes = await writeZipArchive(recovered.archive, limits, context.signal);
741
+ const verified = await readZipArchive(outputBytes, limits, context.signal);
742
+ for (const entry of verified.entries)
743
+ for await (const chunk of decodeZipEntry(entry, limits, context.signal, parsed.password)) {
744
+ void chunk;
745
+ }
746
+ }
747
+ else {
748
+ archive = await readZipArchive(bytes, limits, context.signal, { prefix: true });
749
+ outputBytes = await adjustZipSfx(bytes, archive, limits, context.signal, parsed.password);
750
+ }
751
+ const current = await scope.stat(input);
752
+ if (!current || !unchanged(existing, current))
753
+ fail("archive changed while reading");
754
+ const progress = [];
755
+ if (!parsed.quiet)
756
+ progress.push(parsed.repair ? `Fix archive (-${parsed.repair}) - verified recovered members\n` : "Zip entry offsets adjusted\n");
757
+ if (partial)
758
+ progress.push("\tzip warning: partial recovery; some entries were not recovered\n");
759
+ return { kind: "file", inputSplit, inputPaths, testRequired: parsed.test, publication, bytes: outputBytes, progress, exitCode: 0, moves: [] };
760
+ }
761
+ archive = parsed.junkSfx ? await readZipSfx(bytes, limits, context.signal, parsed.password) : await readZipArchive(resolved.bytes, limits, context.signal, { grow: parsed.grow, ...(resolved.disks ? { disks: resolved.disks } : {}) });
762
+ if (!resolved.disks && (parsed.latestTime || parsed.test))
436
763
  originalBytes = bytes;
437
764
  const current = await scope.stat(input);
438
765
  if (!current || !unchanged(existing, current))
439
766
  fail("archive changed while reading");
440
767
  }
768
+ const logStat = parsed.logPath === undefined ? undefined : await scope.stat(vfsPath(context.cwd, parsed.logPath));
441
769
  const old = new Map(archive.entries.map(entry => [entry.name, entry]));
770
+ const liveProfile = !parsed.split && !existing && !parsed.toCrlf && !parsed.fromCrlf && !parsed.archiveComment && !parsed.entryComments
771
+ && !parsed.test && !parsed.latestTime && parsed.tempPath === undefined;
442
772
  if (!archive.entries.length && !parsed.quiet && (parsed.action === "update" || parsed.action === "freshen"))
443
- await budget.output(`\tzip warning: ${parsed.archive} not found or empty\n`);
773
+ await budget.output(`\tzip warning: ${zipPublicText(parsed.archive)} not found or empty\n`);
774
+ const dosConvertedNames = new Set();
444
775
  const selected = new Map();
776
+ const conversionWarnings = new Map();
777
+ const listed = new Map();
445
778
  const moves = new Map();
446
779
  const deleted = new Set();
447
780
  const synchronized = new Map();
448
781
  const commentNames = new Set();
449
782
  const selection = new Selection([...parsed.includes, ...parsed.excludes], limits, context.signal, { noWild: parsed.noWild, stopAtDirectories: parsed.stopAtDirectories });
450
- const recursiveSelection = parsed.recursivePatterns ? new Selection(parsed.operands, limits, context.signal, { noWild: parsed.noWild, stopAtDirectories: parsed.stopAtDirectories, trailingComponents: true }) : undefined;
783
+ const recursiveSelection = parsed.recursivePatterns ? new Selection(parsed.dosNames ? parsed.operands.map(zipDosName) : parsed.operands, limits, context.signal, { noWild: parsed.noWild, stopAtDirectories: parsed.stopAtDirectories, trailingComponents: true }) : undefined;
451
784
  const ancestors = [];
452
785
  let visits = 0;
453
786
  let work = 0;
454
787
  let compressedBytes = 0;
455
- const encodeSelected = async (source, name, bytes, attributes) => {
788
+ const encodeSelected = async (source, name, input, attributes, expectedSize) => {
789
+ const live = !(input instanceof Uint8Array);
790
+ let bytes = live ? new Uint8Array() : input;
791
+ const sourceSize = live ? expectedSize ?? 0 : bytes.length;
792
+ const originalBytes = bytes;
456
793
  const store = parsed.method === "store" || parsed.level !== 9 && parsed.suffixes.some(suffix => name.endsWith(suffix));
457
794
  if (!store && parsed.level === 0 && bytes.length && !attributes.directory && !attributes.symlink)
458
795
  throw new ZipFailure(5, "Internal logic error", "bad pack level");
459
- if (parsed.toCrlf && !attributes.directory && !attributes.symlink) {
796
+ if ((parsed.toCrlf || parsed.fromCrlf) && !attributes.directory && !attributes.symlink) {
797
+ if (parsed.fromCrlf && parsed.method === "bzip2" && !store)
798
+ fail("from-crlf BZIP2 native read profile unsupported");
460
799
  const originalSize = bytes.length;
461
- bytes = await zipToCrlf(bytes, store, Math.min(limits.maxEntryBytes, limits.maxTotalBytes - budget.totalBytes + originalSize), context.signal);
800
+ bytes = parsed.fromCrlf ? await zipFromCrlf(bytes, store, context.signal, parsed.level) : await zipToCrlf(bytes, store, Math.min(limits.maxEntryBytes, limits.maxTotalBytes - budget.totalBytes + originalSize), context.signal);
462
801
  budget.totalBytes += bytes.length - originalSize;
463
802
  }
464
803
  const level = store ? 0 : parsed.level;
465
- let entry = await makeZipEntry(name, bytes, attributes, limits, context.signal, level, !store && (parsed.archive === "-" || parsed.descriptors && bytes.length > 0), parsed.method === "bzip2" ? "bzip2" : "deflate");
804
+ let entry = await makeZipEntry(name, bytes, attributes, limits, context.signal, level, !store && (parsed.archive === "-" && parsed.tempPath === undefined || parsed.descriptors && bytes.length > 0), parsed.method === "store" ? "deflate" : parsed.method);
805
+ if (live) {
806
+ entry = { ...entry, data: new Uint8Array(), size: expectedSize ?? 0, source: input, level,
807
+ method: store || expectedSize === 0 && parsed.archive !== "-" ? 0 : parsed.method === "lzma" ? 14 : parsed.method === "bzip2" ? 12 : 8, ...(parsed.method === "lzma" && !store && !(expectedSize === 0 && parsed.archive !== "-") ? { flags: 0x802 } : {}), ...(expectedSize === undefined ? {} : { expectedSize }) };
808
+ }
809
+ if (parsed.fromCrlf && sourceSize > 0 && !store && entry.internalAttributes === 0 && !attributes.directory && !attributes.symlink && !parsed.quiet) {
810
+ conversionWarnings.set(name, `\tzip warning: ${bytes === originalBytes ? "has binary so -ll ignored" : "-ll used on binary file - corrupted?"}\n`);
811
+ }
466
812
  if (parsed.descriptors)
467
813
  entry.descriptors = true;
814
+ if (parsed.zip64 === false)
815
+ entry.zip64 = false;
468
816
  if (parsed.zip64 === true || parsed.zip64 === undefined && source === "-")
469
817
  entry.zip64 = true;
470
818
  const prior = old.get(name);
@@ -475,7 +823,12 @@ async function prepare(scope, parsed, budget) {
475
823
  if (entry.data.length > limits.maxArchiveBytes - compressedBytes)
476
824
  fail("archive byte limit exceeded");
477
825
  compressedBytes += entry.data.length;
478
- selected.set(name, { entry, source });
826
+ if (dosConvertedNames.has(name)) {
827
+ entry = { ...entry, mode: attributes.directory ? 0o040755 : 0o100644, versionMadeBy: 30, externalAttributes: attributes.directory ? 16 : 0, flags: (entry.flags ?? 0) & ~0x800 | (prior?.comment?.some(byte => byte >= 128) ? (prior.flags ?? 0) & 0x800 : 0) };
828
+ }
829
+ if (parsed.encryption && !entry.directory)
830
+ entry.encryption = parsed.encryption;
831
+ selected.set(name, { entry, source, sourceSize });
479
832
  if (parsed.entryComments)
480
833
  commentNames.add(name);
481
834
  };
@@ -490,7 +843,7 @@ async function prepare(scope, parsed, budget) {
490
843
  if (depth > limits.maxDepth)
491
844
  fail("archive recursion depth limit exceeded");
492
845
  if (source === "-") {
493
- const previous = selected.get(name);
846
+ const previous = selected.get(name) ?? listed.get(name);
494
847
  if (previous && previous.source !== source)
495
848
  throw new ZipFailure(16, "Invalid command arguments", "cannot repeat names in zip file");
496
849
  if (previous || !await filterName(name, selection, parsed.includes.length))
@@ -508,8 +861,14 @@ async function prepare(scope, parsed, budget) {
508
861
  }
509
862
  if (parsed.action === "freshen" && !prior || prior && (parsed.action === "update" || parsed.action === "freshen") && Math.floor(modified.getTime() / 1000) <= Math.floor(prior.modified.getTime() / 1000))
510
863
  return;
511
- const bytes = await collectBytes(scope.stdin, { maxBytes: Math.min(limits.maxEntryBytes, limits.maxTotalBytes - budget.totalBytes), signal: context.signal });
512
- await budget.member(bytes.length);
864
+ if (parsed.showFiles !== undefined) {
865
+ await budget.member(0);
866
+ listed.set(name, { name, size: 0, source });
867
+ return;
868
+ }
869
+ const live = liveProfile;
870
+ const bytes = live ? scope.stdin : await collectBytes(scope.stdin, { maxBytes: Math.min(limits.maxEntryBytes, limits.maxTotalBytes - budget.totalBytes), signal: context.signal });
871
+ await budget.member(bytes instanceof Uint8Array ? bytes.length : 0);
513
872
  await encodeSelected(source, name, bytes, { modified, mode: 0o010660, directory: false, symlink: false });
514
873
  return;
515
874
  }
@@ -545,20 +904,21 @@ async function prepare(scope, parsed, budget) {
545
904
  commentNames.add(name);
546
905
  if (parsed.mustMatch && !storedName) {
547
906
  if (!parsed.quiet)
548
- await budget.output(`\tzip warning: name not matched: ${source}\n`);
907
+ await budget.output(`\tzip warning: name not matched: ${zipPublicText(source)}\n`);
549
908
  throw new ZipFailure(18, "File not found or no read permission", source);
550
909
  }
551
910
  if (!parsed.quiet && !storedName)
552
- await budget.output(`\tzip warning: name not matched: ${source}\n`);
911
+ await budget.output(`\tzip warning: name not matched: ${zipPublicText(source)}\n`);
553
912
  return;
554
913
  }
555
914
  checkPath(canonical, limits);
556
915
  if (canonical === output || canonical === input || (existing && sameIdentity(existing, stat)) || (publication?.existing && sameIdentity(publication.existing, stat)))
557
916
  return;
558
917
  const symlink = parsed.storeLinks && stat.type === "symlink";
559
- if (stat.type !== "file" && stat.type !== "directory" && !symlink) {
918
+ const fifoSource = (stat.mode & 0o170000) === 0o010000;
919
+ if (fifoSource && !parsed.fifo || stat.type !== "file" && stat.type !== "directory" && !symlink && !fifoSource) {
560
920
  if (!parsed.quiet)
561
- await budget.output(`\tzip warning: ignoring special file: ${source}\n`);
921
+ await budget.output(`\tzip warning: ignoring special file: ${zipPublicText(source)}\n`);
562
922
  return;
563
923
  }
564
924
  if ((existing || publication?.existing) && !hasIdentity(stat))
@@ -569,63 +929,101 @@ async function prepare(scope, parsed, budget) {
569
929
  const dateIncluded = zipDateMatches(new Date(stat.mtimeMs), parsed.fromDate, parsed.beforeDate);
570
930
  const nameIncluded = (dateIncluded || parsed.entryComments) && await filterName(name, selection, parsed.includes.length) && (!recursiveSelection || await recursiveSelection.matches(name, true));
571
931
  const included = dateIncluded && nameIncluded;
932
+ if (logStat && included && (!hasIdentity(stat) || sameIdentity(logStat, stat))) {
933
+ if (log)
934
+ log.failed = true;
935
+ throw new ZipFailure(16, "Invalid command arguments", "ZIP log may not alias a selected source");
936
+ }
572
937
  const sourceName = name;
573
938
  if (parsed.junkPaths && !storedName)
574
939
  name = directory ? "" : name.slice(name.lastIndexOf("/") + 1);
940
+ if (parsed.dosNames && !storedName && name && included) {
941
+ name = zipDosName(name);
942
+ if (!name || (directory ? name.slice(0, -1) : name).split("/").some(component => !component))
943
+ throw new ZipFailure(16, "Invalid command arguments", "DOS conversion produces an empty component");
944
+ dosConvertedNames.add(name);
945
+ }
575
946
  const prior = old.get(name);
576
947
  if (parsed.entryComments && prior && nameIncluded)
577
948
  commentNames.add(name);
578
- if (parsed.filesync && name && included && !(directory && parsed.omitDirectories)) {
949
+ if ((parsed.filesync || parsed.difference) && name && included && !(directory && parsed.omitDirectories)) {
579
950
  if (synchronized.has(name) && synchronized.get(name) !== source)
580
951
  throw new ZipFailure(16, "Invalid command arguments", "cannot repeat names in zip file");
581
952
  synchronized.set(name, source);
582
953
  }
583
- if (parsed.move && name && included && !(directory && parsed.omitDirectories) && (prior || parsed.action !== "freshen")) {
584
- if (!moves.has(path))
585
- moves.set(path, await inspectZipMoveSource(scope, path, source));
586
- }
587
- const current = parsed.filesync && prior && prior.size === (directory ? 0 : stat.size)
954
+ const current = !fifoSource && (parsed.filesync || parsed.difference) && prior && prior.size === (directory ? 0 : stat.size)
588
955
  && Math.ceil(Math.floor(stat.mtimeMs / 1000) / 2) === Math.ceil(Math.floor(prior.modified.getTime() / 1000) / 2);
589
956
  const eligible = parsed.action !== "update" && parsed.action !== "freshen"
590
957
  || (prior ? Math.floor(stat.mtimeMs / 1000) > Math.floor(prior.modified.getTime() / 1000) : parsed.action === "update");
591
- if (name && included && eligible && !current && !(directory && parsed.omitDirectories)) {
958
+ if (parsed.showFiles === undefined && parsed.move && (!parsed.difference || !current && eligible) && name && included && !(directory && parsed.omitDirectories) && (prior || parsed.action !== "freshen")) {
959
+ if (!moves.has(path))
960
+ moves.set(path, await inspectZipMoveSource(scope, path, source));
961
+ }
962
+ if (name && included && eligible && (!current || parsed.showFiles !== undefined) && !(directory && parsed.omitDirectories)) {
592
963
  checkPath(name, limits);
593
- const previous = selected.get(name);
964
+ const previous = selected.get(name) ?? listed.get(name);
594
965
  if (previous && previous.source !== source) {
595
966
  if (!parsed.quiet)
596
- await budget.output(`\tzip warning: first full name: ${previous.source}\n second full name: ${source}\n name in zip file repeated: ${name}\n`);
967
+ await budget.output(`\tzip warning: first full name: ${zipPublicText(previous.source)}\n second full name: ${zipPublicText(source)}\n name in zip file repeated: ${zipPublicText(name)}\n`);
597
968
  throw new ZipFailure(16, "Invalid command arguments", "cannot repeat names in zip file");
598
969
  }
599
970
  if (!previous) {
600
- await budget.member(directory ? 0 : stat.size);
601
- let bytes;
602
- if (directory)
603
- bytes = new Uint8Array();
604
- else if (symlink) {
605
- if (!context.fs.readlink)
606
- fail("filesystem does not support reading symbolic links");
607
- const target = await scope.operation(() => context.fs.readlink(path, { signal: context.signal }));
608
- if (Buffer.byteLength(target) > stat.size)
609
- fail(`source changed while reading: ${source}`);
610
- bytes = Buffer.from(target);
971
+ if (fifoSource && parsed.move)
972
+ fail("FIFO move is unsupported: producer streams have no removable file snapshot");
973
+ await budget.member(directory || fifoSource ? 0 : stat.size);
974
+ if (parsed.showFiles !== undefined) {
975
+ listed.set(name, { name, size: directory ? 0 : stat.size, source });
611
976
  }
612
977
  else {
613
- try {
614
- bytes = await collectBytes(scope.input(path), { maxBytes: stat.size, signal: context.signal });
978
+ const store = parsed.method === "store" || parsed.level !== 9 && parsed.suffixes.some(suffix => name.endsWith(suffix));
979
+ if (fifoSource) {
980
+ const bytes = await collectBytes(scope.input(path, true), { maxBytes: Math.min(limits.maxEntryBytes, limits.maxTotalBytes - budget.totalBytes), signal: context.signal });
981
+ const current = await inspectSource(scope, path, parsed.storeLinks);
982
+ if (current.canonical !== canonical || !unchanged(stat, current.stat))
983
+ fail(`source changed while reading: ${source}`);
984
+ budget.totalBytes += bytes.length;
985
+ await encodeSelected(source, name, bytes, { modified: new Date(stat.mtimeMs), mode: 0o100644, directory: false, symlink: false });
986
+ return;
987
+ }
988
+ if (liveProfile && !directory && !symlink && (parsed.archive === "-" || store || parsed.descriptors)) {
989
+ const input = (async function* () {
990
+ yield* scope.input(path);
991
+ const current = await inspectSource(scope, path, parsed.storeLinks);
992
+ if (current.canonical !== canonical || !unchanged(stat, current.stat))
993
+ fail(`source changed while reading: ${source}`);
994
+ })();
995
+ await encodeSelected(source, name, input, { modified: new Date(stat.mtimeMs), mode: stat.mode, directory, symlink }, stat.size);
996
+ return;
997
+ }
998
+ let bytes;
999
+ if (directory)
1000
+ bytes = new Uint8Array();
1001
+ else if (symlink) {
1002
+ if (!context.fs.readlink)
1003
+ fail("filesystem does not support reading symbolic links");
1004
+ const target = await scope.operation(() => context.fs.readlink(path, { signal: context.signal }));
1005
+ if (Buffer.byteLength(target) > stat.size)
1006
+ fail(`source changed while reading: ${source}`);
1007
+ bytes = Buffer.from(target);
615
1008
  }
616
- catch (error) {
617
- context.signal.throwIfAborted();
618
- if (parsed.mustMatch && typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "EACCES"))
619
- throw new ZipFailure(18, "File not found or no read permission", `was zipping ${source}`);
620
- throw error;
1009
+ else {
1010
+ try {
1011
+ bytes = await collectBytes(scope.input(path), { maxBytes: stat.size, signal: context.signal });
1012
+ }
1013
+ catch (error) {
1014
+ context.signal.throwIfAborted();
1015
+ if (parsed.mustMatch && typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "EACCES"))
1016
+ throw new ZipFailure(18, "File not found or no read permission", `was zipping ${source}`);
1017
+ throw error;
1018
+ }
621
1019
  }
1020
+ if (!directory && bytes.length !== stat.size)
1021
+ fail(`source changed while reading: ${source}`);
1022
+ const current = await inspectSource(scope, path, parsed.storeLinks);
1023
+ if (current.canonical !== canonical || !unchanged(stat, current.stat))
1024
+ fail(`source changed while reading: ${source}`);
1025
+ await encodeSelected(source, name, bytes, { modified: new Date(stat.mtimeMs), mode: stat.mode, directory, symlink });
622
1026
  }
623
- if (!directory && bytes.length !== stat.size)
624
- fail(`source changed while reading: ${source}`);
625
- const current = await inspectSource(scope, path, parsed.storeLinks);
626
- if (current.canonical !== canonical || !unchanged(stat, current.stat))
627
- fail(`source changed while reading: ${source}`);
628
- await encodeSelected(source, name, bytes, { modified: new Date(stat.mtimeMs), mode: stat.mode, directory, symlink });
629
1027
  }
630
1028
  }
631
1029
  if (directory && !storedName && (parsed.recursive || parsed.recursivePatterns)) {
@@ -657,7 +1055,7 @@ async function prepare(scope, parsed, budget) {
657
1055
  for (const entry of archive.entries) {
658
1056
  if (await operands.matches(entry.name) && await filterName(entry.name, selection, parsed.includes.length) && zipDateMatches(entry.modified, parsed.fromDate, parsed.beforeDate)) {
659
1057
  await budget.member(entry.size);
660
- selected.set(entry.name, { entry, source: entry.name });
1058
+ selected.set(entry.name, { entry, source: entry.name, sourceSize: entry.size });
661
1059
  }
662
1060
  }
663
1061
  }
@@ -665,7 +1063,7 @@ async function prepare(scope, parsed, budget) {
665
1063
  if (!parsed.quiet && (parsed.recursive || parsed.archiveComment || parsed.entryComments || parsed.move))
666
1064
  await budget.output("\tzip warning: invalid option(s) used with -d; ignored.\n");
667
1065
  if (!parsed.quiet && !archive.entries.length)
668
- await budget.output(`\tzip warning: ${parsed.archive} not found or empty\n`);
1066
+ await budget.output(`\tzip warning: ${zipPublicText(parsed.archive)} not found or empty\n`);
669
1067
  const operands = new Selection(parsed.recursivePatterns ? [] : parsed.operands, limits, context.signal, { noWild: parsed.noWild, stopAtDirectories: parsed.stopAtDirectories });
670
1068
  if (parsed.operands.length && !parsed.recursivePatterns) {
671
1069
  for (const entry of archive.entries) {
@@ -682,18 +1080,32 @@ async function prepare(scope, parsed, budget) {
682
1080
  if (!parsed.quiet) {
683
1081
  for (const [index, operand] of parsed.operands.entries()) {
684
1082
  if (!operands.matched.has(index))
685
- await budget.output(`\tzip warning: name not matched: ${operand}\n`);
1083
+ await budget.output(`\tzip warning: name not matched: ${zipPublicText(operand)}\n`);
686
1084
  }
687
1085
  }
688
1086
  }
689
1087
  else {
690
- const operands = parsed.operands.length || parsed.action === "add" ? parsed.operands : archive.entries.map(entry => entry.name);
1088
+ const operands = parsed.showFiles !== undefined && !parsed.operands.length ? [] : parsed.operands.length || parsed.action === "add" ? parsed.operands : archive.entries.map(entry => entry.name);
691
1089
  if (parsed.recursivePatterns)
692
1090
  await visit(".", "", 0);
693
1091
  else
694
1092
  for (const operand of operands)
695
1093
  await visit(operand, memberName(operand, limits), 0);
696
1094
  }
1095
+ if (!publication || !(parsed.split ?? inputSplit))
1096
+ await log?.start();
1097
+ if (parsed.showFiles !== undefined) {
1098
+ const contains = !parsed.operands.length && !parsed.includes.length && !parsed.excludes.length;
1099
+ const listing = contains ? archive.entries : parsed.action === "delete" ? archive.entries.filter(entry => deleted.has(entry.name)) : parsed.action === "copy" ? [...selected.values()].map(item => item.entry) : [...listed.values()];
1100
+ const title = contains ? "Archive contains" : parsed.action === "delete" ? "Would Delete" : parsed.action === "freshen" ? "Would Freshen" : parsed.action === "copy" ? "Would Copy" : "Would Add/Update";
1101
+ if (!parsed.quiet && parsed.showFiles) {
1102
+ await budget.output(`${title}:\n`);
1103
+ for (const entry of listing)
1104
+ await budget.output(` ${escapeText(zipPublicText(entry.name), "display")}\n`);
1105
+ }
1106
+ await budget.output(`Total ${listing.length} entries (${listing.reduce((total, entry) => total + entry.size, 0)} bytes)\n`);
1107
+ return { kind: "current", exitCode: 0, moves: [] };
1108
+ }
697
1109
  if (parsed.filesync) {
698
1110
  if (!synchronized.size)
699
1111
  throw new ZipFailure(12, "Nothing to do!", parsed.archive);
@@ -708,15 +1120,16 @@ async function prepare(scope, parsed, budget) {
708
1120
  }
709
1121
  }
710
1122
  const changed = selected.size > 0 || deleted.size > 0;
1123
+ const testRequired = parsed.test && !(parsed.filesync && !changed);
711
1124
  let exitCode = 0;
712
- if (!selected.size && !((editComment || editEntries || parsed.test) && archive.entries.length) && (parsed.action === "freshen" || parsed.action === "update" && (existing || !parsed.includes.length))) {
1125
+ if (!parsed.difference && !selected.size && !((editComment || editEntries || parsed.junkSfx || parsed.test) && archive.entries.length) && (parsed.action === "freshen" || parsed.action === "update" && (existing || !parsed.includes.length))) {
713
1126
  if (!parsed.latestTime || !archive.entries.length)
714
1127
  return moves.size ? { kind: "current", exitCode: 12, moves: [...moves.values()] } : undefined;
715
1128
  exitCode = 12;
716
1129
  }
717
1130
  if (parsed.latestTime && !changed && !archive.entries.length && parsed.archive !== "-")
718
1131
  throw new ZipFailure(13, "Missing or empty zip file", parsed.archive);
719
- if (!selected.size && !deleted.size && !((editComment || editEntries || parsed.latestTime || parsed.test) && archive.entries.length) && (parsed.action === "delete" || parsed.action === "copy" || parsed.recursivePatterns || parsed.fromDate !== undefined || parsed.beforeDate !== undefined || !parsed.includes.length)) {
1132
+ if (!parsed.difference && !selected.size && !deleted.size && !((editComment || editEntries || parsed.junkSfx || parsed.latestTime || parsed.test) && archive.entries.length) && (parsed.action === "delete" || parsed.action === "copy" || parsed.recursivePatterns || parsed.fromDate !== undefined || parsed.beforeDate !== undefined || !parsed.includes.length)) {
720
1133
  const detail = parsed.action !== "delete" && parsed.recursive && parsed.firstOperand >= 0
721
1134
  ? `try: zip ${parsed.args.slice(0, parsed.firstOperand).join(" ")} . -i ${parsed.args.slice(parsed.firstOperand).join(" ")}`
722
1135
  : parsed.archive;
@@ -734,22 +1147,49 @@ async function prepare(scope, parsed, budget) {
734
1147
  progressBytes += size;
735
1148
  progress.push(message);
736
1149
  };
1150
+ const counted = parsed.action === "delete" ? archive.entries.filter(entry => deleted.has(entry.name)) : [...selected.values()].map(item => item.entry);
1151
+ let remainingBytes = counted.reduce((total, entry) => total + (parsed.action === "delete" || parsed.action === "copy" ? entry.data.length : selected.get(entry.name)?.sourceSize ?? entry.size), 0);
1152
+ let doneBytes = 0;
1153
+ let done = 0;
1154
+ const stats = (entry) => {
1155
+ let prefix = parsed.displayVolume ? "1>1: " : "";
1156
+ if (parsed.displayCounts)
1157
+ prefix += `${String(done).padStart(3)}/${String(counted.length - done).padStart(3)} `;
1158
+ if (parsed.displayBytes)
1159
+ prefix += `[${zipDisplaySize(doneBytes).padStart(4)}/${zipDisplaySize(remainingBytes).padStart(4)}] `;
1160
+ const size = parsed.action === "delete" || parsed.action === "copy" ? entry.data.length : selected.get(entry.name)?.sourceSize ?? entry.size;
1161
+ done++;
1162
+ doneBytes += size;
1163
+ remainingBytes -= size;
1164
+ return prefix;
1165
+ };
737
1166
  const append = (entry, update) => {
738
- const percentage = entry.size ? Math.trunc((Math.trunc(200 * (entry.size - entry.data.length) / entry.size) + 1) / 2) : 0;
739
- queue(`${update ? parsed.action === "freshen" ? "freshening:" : "updating:" : " adding:"} ${entry.name} (${entry.method === 12 ? `bzipped ${percentage}%` : entry.method === 8 ? `deflated ${percentage}%` : "stored 0%"})\n`);
1167
+ if (parsed.quiet)
1168
+ return;
1169
+ const compressedSize = entry.compressedSize ?? entry.data.length;
1170
+ const percentage = entry.size ? Math.trunc((Math.trunc(200 * (entry.size - compressedSize) / entry.size) + 1) / 2) : 0;
1171
+ const prefix = stats(entry);
1172
+ const usize = parsed.displayUsize ? ` (${zipDisplaySize(selected.get(entry.name)?.sourceSize ?? entry.size)})` : "";
1173
+ const verbose = parsed.verbose ? `${entry.method === 0 ? "" : " "}\t(in=${entry.size}) (out=${compressedSize})` : "";
1174
+ const dotCount = !parsed.globalDots && parsed.dotSize ? Math.floor(entry.size / parsed.dotSize) : 0;
1175
+ if (dotCount > limits.maxTextBytes - budget.textBytes - progressBytes)
1176
+ fail("text output limit exceeded");
1177
+ const dots = ".".repeat(dotCount);
1178
+ const warning = conversionWarnings.get(entry.name);
1179
+ queue(`${prefix}${update ? parsed.action === "freshen" ? "freshening:" : "updating:" : " adding:"} ${escapeText(zipPublicText(entry.name), "display")}${usize}${verbose}${warning ? `\n${warning}` : ""} ${dots ? `${dots} ` : ""}(${entry.method === 14 ? `lzma ${percentage}%` : entry.method === 12 ? `bzipped ${percentage}%` : entry.method === 8 ? `deflated ${percentage}%` : "stored 0%"})\n`);
740
1180
  };
741
1181
  for (const entry of archive.entries) {
742
1182
  if (++work > limits.maxPatternSteps)
743
1183
  fail("archive work limit exceeded");
744
1184
  if (deleted.has(entry.name)) {
745
- queue(`deleting: ${entry.name}\n`);
1185
+ queue(`${stats(entry)}deleting: ${escapeText(zipPublicText(entry.name), "display")}\n`);
746
1186
  continue;
747
1187
  }
748
1188
  const replacement = selected.get(entry.name);
749
1189
  if (parsed.action === "copy") {
750
1190
  if (replacement) {
751
1191
  entries.push(replacement.entry);
752
- queue(` copying: ${entry.name}\n`);
1192
+ queue(`${stats(entry)} copying: ${escapeText(zipPublicText(entry.name), "display")}\n`);
753
1193
  selected.delete(entry.name);
754
1194
  }
755
1195
  continue;
@@ -759,15 +1199,39 @@ async function prepare(scope, parsed, budget) {
759
1199
  append(replacement.entry, true);
760
1200
  selected.delete(entry.name);
761
1201
  }
762
- else {
1202
+ else if (!parsed.difference) {
763
1203
  await budget.member(entry.size);
764
1204
  entries.push(entry);
765
1205
  }
766
1206
  }
1207
+ if (parsed.grow && (parsed.zip64 !== undefined || deleted.size || archive.entries.some(entry => !entries.includes(entry)))) {
1208
+ for (const entry of archive.entries)
1209
+ zipGrowRecords.delete(entry);
1210
+ }
1211
+ const pendingProgress = [];
767
1212
  for (const { entry } of selected.values()) {
768
1213
  entries.push(entry);
769
- append(entry, false);
1214
+ if (entry.source)
1215
+ pendingProgress.push(entry);
1216
+ else
1217
+ append(entry, false);
770
1218
  }
1219
+ const summary = () => {
1220
+ if (!parsed.verbose || parsed.quiet)
1221
+ return;
1222
+ const size = entries.reduce((total, entry) => total + entry.size, 0);
1223
+ const compressed = entries.reduce((total, entry) => total + (entry.compressedSize ?? entry.data.length), 0);
1224
+ queue(`total bytes=${size}, compressed=${compressed} -> ${size ? Math.round(100 * (size - compressed) / size) : 0}% savings\n`);
1225
+ };
1226
+ const finishProgress = () => {
1227
+ if (!pendingProgress.length)
1228
+ return;
1229
+ for (const entry of pendingProgress.splice(0))
1230
+ append(entry, false);
1231
+ summary();
1232
+ };
1233
+ if (!pendingProgress.length)
1234
+ summary();
771
1235
  if (!entries.length)
772
1236
  queue("\tzip warning: zip file empty\n");
773
1237
  let comment = archive.comment;
@@ -783,7 +1247,7 @@ async function prepare(scope, parsed, budget) {
783
1247
  if (!commentNames.has(entry.name))
784
1248
  continue;
785
1249
  if (!parsed.quiet)
786
- await budget.output(`Enter comment for ${entry.name}:\n`);
1250
+ await budget.output(`Enter comment for ${zipPublicText(entry.name)}:\n`);
787
1251
  const line = await commentInput.readLine();
788
1252
  if (line !== undefined)
789
1253
  entries[index] = setZipEntryComment(entry, line.at(-1) === 10 ? line.subarray(0, -1) : line, limits);
@@ -802,7 +1266,9 @@ async function prepare(scope, parsed, budget) {
802
1266
  comment = await readZipComment(commentInput, limits, context.signal);
803
1267
  }
804
1268
  if (!publication)
805
- return { kind: "stream", archive: { entries, comment }, progress, moves: [...moves.values()] };
1269
+ return { kind: "stream", temporary, archive: { entries, comment }, progress, finishProgress, moves: [...moves.values()] };
1270
+ if (entries.some(entry => entry.source))
1271
+ return { kind: "staged-stream", publication, archive: { entries, comment }, progress, finishProgress, exitCode, moves: [...moves.values()] };
806
1272
  let latestWarning;
807
1273
  if (parsed.latestTime) {
808
1274
  const mtimeMs = await zipLatestTime(entries, context.signal);
@@ -813,44 +1279,37 @@ async function prepare(scope, parsed, budget) {
813
1279
  else
814
1280
  publication = { ...publication, mtimeMs };
815
1281
  }
816
- const bytes = originalBytes && !changed && !editComment && !editEntries ? originalBytes
817
- : await writeZipArchive({ entries, comment }, limits, context.signal, false, parsed.zip64 === true);
818
- if (parsed.test && parsed.archive !== "-" && !(parsed.filesync && !changed)) {
1282
+ const bytes = originalBytes && !changed && !editComment && !editEntries && !parsed.junkSfx && !parsed.difference ? originalBytes
1283
+ : await writeZipArchive({ entries, comment }, limits, context.signal, false, parsed.zip64 === true, parsed.zip64 !== false);
1284
+ if (testRequired && parsed.archive !== "-") {
819
1285
  for (const message of progress)
820
1286
  await budget.output(message);
821
1287
  progress.length = 0;
822
1288
  progressBytes = 0;
823
- try {
824
- const tested = await readZipArchive(bytes, limits, context.signal);
825
- if (!tested.entries.length)
826
- fail("empty ZIP archive");
827
- let decoded = 0;
828
- for (const entry of tested.entries) {
829
- for await (const chunk of decodeZipEntry(entry, limits, context.signal)) {
830
- if (chunk.length > limits.maxTotalBytes - decoded)
831
- fail("actual decompressed byte limit exceeded");
832
- decoded += chunk.length;
833
- }
834
- }
835
- }
836
- catch {
837
- context.signal.throwIfAborted();
838
- if (!parsed.quiet)
839
- await budget.output(`test of ${parsed.archive} FAILED\n`);
840
- throw new ZipFailure(8, "Zip file invalid, could not spawn unzip, or wrong unzip", "original files unmodified");
841
- }
842
- if (!parsed.quiet) {
843
- queue(`test of ${parsed.archive} OK\n`);
844
- }
845
1289
  }
846
1290
  if (latestWarning !== undefined)
847
1291
  queue(latestWarning);
848
- if (!changed && originalBytes && !editComment && !editEntries && (parsed.output === undefined || parsed.action === "copy" && parsed.test) && (latestWarning !== undefined || parsed.test && !parsed.latestTime)) {
1292
+ if (!changed && originalBytes && !editComment && !editEntries && !parsed.junkSfx && !parsed.difference && (parsed.output === undefined || parsed.action === "copy" && parsed.test) && (latestWarning !== undefined || parsed.test && !parsed.latestTime)) {
1293
+ if (testRequired && input)
1294
+ await testZipCommand(scope, parsed.testCommand, input, budget, parsed.archive, parsed.quiet, parsed.password);
849
1295
  for (const message of progress)
850
1296
  await budget.output(message);
851
1297
  return { kind: "current", exitCode, moves: [...moves.values()] };
852
1298
  }
853
- return { kind: "file", publication, bytes, progress, exitCode, moves: [...moves.values()] };
1299
+ const sourcePaths = [];
1300
+ if ((parsed.split ?? inputSplit) && parsed.action !== "copy" && parsed.action !== "delete")
1301
+ for (const value of selected.values()) {
1302
+ if (value.source === "-")
1303
+ continue;
1304
+ const path = vfsPath(context.cwd, value.source);
1305
+ if (value.entry.symlink) {
1306
+ const parent = await scope.operation(() => context.fs.realpath(dirname(path), { signal: context.signal }));
1307
+ sourcePaths.push(`${parent === "/" ? "" : parent}/${path.slice(path.lastIndexOf("/") + 1)}`);
1308
+ }
1309
+ else
1310
+ sourcePaths.push(await scope.operation(() => context.fs.realpath(path, { signal: context.signal })));
1311
+ }
1312
+ return { kind: "file", inputSplit, inputPaths: [...inputPaths, ...sourcePaths], testRequired, publication, bytes, progress, exitCode, moves: [...moves.values()] };
854
1313
  }
855
1314
  export function createZipCommand(options = {}) {
856
1315
  const limits = settings(options);
@@ -859,54 +1318,196 @@ export function createZipCommand(options = {}) {
859
1318
  const scope = new ZipScope(original, limits);
860
1319
  const context = scope.context;
861
1320
  let budget = new Budget(context, limits);
1321
+ let log;
862
1322
  try {
863
- const parsed = await parse(scope, limits);
1323
+ let parsed = await parse(scope, limits, options.zip);
864
1324
  if ("information" in parsed) {
865
- await budget.output(parsed.information === "more-help" ? zipExtendedHelp : zipHelp);
1325
+ if (parsed.debug)
1326
+ await budget.output("sd: Command line read\n");
1327
+ if (parsed.information === "command") {
1328
+ await budget.output("command line:\n");
1329
+ let redactNext = false;
1330
+ for (const argument of parsed.command ?? []) {
1331
+ const value = zipPublicText(redactNext ? "[redacted]" : argument);
1332
+ redactNext = zipPasswordArgument(argument) === "separate";
1333
+ const escaped = escapeText(value, "display", size => { if (size > limits.maxTextBytes - budget.textBytes)
1334
+ fail("text output limit exceeded"); }).split("'").join("'\\''");
1335
+ await budget.output(`'${escaped}' `);
1336
+ }
1337
+ await budget.output("\n\nzip error: Interrupted (show command line)\n");
1338
+ return { exitCode: 9 };
1339
+ }
1340
+ if (parsed.information === "options") {
1341
+ await budget.output("available options:\nVirtual implementation; unsupported native options are omitted.\n sh long val neg\n");
1342
+ for (const [name, short] of Object.entries(zipLongOptions)) {
1343
+ const value = ["i", "x"].includes(short) ? "list" : ["s", "P", "n", "Z", "t", "tt", "O", "ds", "b", "lf", "TT"].includes(short) ? "req" : "";
1344
+ await budget.output(` ${(short === "version" ? "" : short).padEnd(3)} ${name.padEnd(20)} ${value.padEnd(4)} ${zipNegatableOptions.has(short) ? "neg" : ""}\n`);
1345
+ }
1346
+ }
1347
+ else
1348
+ await budget.output(parsed.information === "more-help" ? zipExtendedHelp : parsed.information === "version" ? zipVersion : parsed.information === "license" ? zipLicense : zipHelp);
866
1349
  return { exitCode: 0 };
867
1350
  }
868
- if (parsed.archive === "-")
869
- budget = new Budget({ ...context, stdout: context.stderr }, limits);
870
- const prepared = await prepare(scope, parsed, budget);
1351
+ if (parsed.logPath !== undefined) {
1352
+ log = new ZipLog(scope, parsed.logInfo);
1353
+ await log.open(parsed.logPath, parsed.logAppend, [parsed.archive, parsed.output, ...(parsed.action === "copy" || parsed.action === "delete" ? [] : parsed.operands)].filter((path) => path !== undefined));
1354
+ }
1355
+ const quietDisplay = parsed.quiet;
1356
+ if (log && quietDisplay)
1357
+ parsed = { ...parsed, quiet: false };
1358
+ const progressContext = log ? { ...context, stdout: log.sink(context.stdout, quietDisplay), stderr: log.sink(context.stderr, quietDisplay) } : context;
1359
+ budget = new Budget(parsed.archive === "-" ? { ...progressContext, stdout: log ? log.sink(context.stderr, quietDisplay) : context.stderr } : progressContext, limits);
1360
+ if (parsed.encrypt) {
1361
+ if (parsed.password === undefined)
1362
+ parsed.password = await scope.operation(() => readZipPassword(options.zipHost, limits.maxArgumentBytes, context.signal, true));
1363
+ if (log)
1364
+ log.password = parsed.password;
1365
+ parsed.encryption = { ...(parsed.aes ? { aes: parsed.aes } : {}), password: parsed.password, entropy: (length, signal) => scope.operation(() => {
1366
+ if (!options.zipHost?.entropy)
1367
+ throw new ZipHostFailure("entropy");
1368
+ return options.zipHost.entropy(length, signal);
1369
+ }) };
1370
+ }
1371
+ if (parsed.debug)
1372
+ await budget.output("sd: Command line read\nsd: Reading virtual archive and selecting files\n");
1373
+ if (parsed.split && parsed.splitPause && !options.zipHost?.volumePrompt)
1374
+ throw new ZipFailure(16, "Invalid command arguments", "split pause requires an explicit volume prompt capability");
1375
+ const prepared = await prepare(scope, parsed, budget, log, options.zipHost);
871
1376
  if (!prepared)
872
1377
  return { exitCode: 12 };
873
1378
  if (prepared.kind === "current") {
874
1379
  await removeZipSources(scope, prepared.moves, budget, parsed.quiet);
875
1380
  return { exitCode: prepared.exitCode };
876
1381
  }
1382
+ if (parsed.debug)
1383
+ await budget.output("sd: Writing virtual archive\n");
1384
+ let globalBytes = 0;
1385
+ const dots = async (size) => {
1386
+ if (!parsed.globalDots || !parsed.dotSize)
1387
+ return;
1388
+ if (!globalBytes)
1389
+ await budget.output(" ");
1390
+ const count = Math.floor((globalBytes + size) / parsed.dotSize) - Math.floor(globalBytes / parsed.dotSize);
1391
+ globalBytes += size;
1392
+ for (let index = 0; index < count; index++)
1393
+ await budget.output(".");
1394
+ };
877
1395
  if (prepared.kind === "file") {
878
1396
  const publication = prepared.publication;
879
1397
  if (!publication)
880
1398
  fail("ZIP missing file publication");
881
- await writeFileOutput(context, prepared.bytes, () => scope.operation(() => publishZip(scope, { ...publication, bytes: prepared.bytes })));
1399
+ await dots(prepared.bytes.length);
1400
+ const split = parsed.split ?? prepared.inputSplit;
1401
+ if (split) {
1402
+ if (parsed.splitPause && !options.zipHost?.volumePrompt)
1403
+ throw new ZipFailure(16, "Invalid command arguments", "split pause requires an explicit volume prompt capability");
1404
+ const parts = await splitZipVolumes(prepared.bytes, split, limits, context.signal);
1405
+ await log?.protect(parts.map((_, disk) => volumeName(publication.output, disk, parts.length)));
1406
+ await log?.start();
1407
+ if (prepared.testRequired) {
1408
+ await scope.operation(() => stageZip(scope, { ...publication, bytes: prepared.bytes, reservedPath: publication.output }, staging => testZipCommand(scope, parsed.testCommand, staging.file.path, budget, parsed.archive, parsed.quiet, parsed.password)));
1409
+ }
1410
+ await scope.operation(() => publishZipVolumes(scope, publication, parts, prepared.inputPaths, async (path, disk) => {
1411
+ if (parsed.splitVerbose)
1412
+ await budget.output(`split ${disk + 1}/${parts.length}: ${zipPublicText(path)} (${parts[disk].length} bytes)\n`);
1413
+ if (disk && parsed.splitPause) {
1414
+ if (parsed.splitBell)
1415
+ await budget.output("\u0007");
1416
+ if (!await scope.operation(() => options.zipHost.volumePrompt({ path, disk, disks: parts.length, signal: context.signal })))
1417
+ fail("ZIP split volume prompt cancelled");
1418
+ }
1419
+ }));
1420
+ }
1421
+ else
1422
+ await writeFileOutput(context, prepared.bytes, () => scope.operation(() => publishZip(scope, { ...publication, bytes: prepared.bytes, ...(prepared.testRequired ? { validate: (path) => testZipCommand(scope, parsed.testCommand, path, budget, parsed.archive, parsed.quiet, parsed.password) } : {}) })));
882
1423
  for (const message of prepared.progress)
883
1424
  await budget.output(message);
884
1425
  }
885
- else {
1426
+ else if (prepared.kind === "staged-stream") {
1427
+ const source = (async function* () {
1428
+ for await (const chunk of streamZipArchive(prepared.archive, limits, context.signal, true, parsed.zip64 === true, parsed.zip64 !== false)) {
1429
+ yield chunk;
1430
+ try {
1431
+ await dots(chunk.length);
1432
+ }
1433
+ catch (error) {
1434
+ void scope.closeInputs().catch(() => { });
1435
+ throw error;
1436
+ }
1437
+ }
1438
+ prepared.finishProgress();
1439
+ })();
1440
+ await scope.operation(() => publishZip(scope, { ...prepared.publication, source }));
1441
+ prepared.finishProgress();
886
1442
  for (const message of prepared.progress)
887
1443
  await budget.output(message);
1444
+ }
1445
+ else {
888
1446
  const output = createOutputOperation(context, context.stdout);
889
1447
  try {
890
- for await (const chunk of streamZipArchive(prepared.archive, limits, output.signal, true, parsed.zip64 === true)) {
891
- await writeBytes(output.output, chunk, output.signal);
1448
+ const source = streamZipArchive(prepared.archive, limits, output.signal, !prepared.temporary || parsed.descriptors, parsed.zip64 === true, parsed.zip64 !== false);
1449
+ const emit = async (input) => {
1450
+ for await (const chunk of input) {
1451
+ try {
1452
+ await writeBytes(output.output, chunk, output.signal);
1453
+ await dots(chunk.length);
1454
+ }
1455
+ catch (error) {
1456
+ void scope.closeInputs().catch(() => { });
1457
+ throw error;
1458
+ }
1459
+ }
1460
+ };
1461
+ if (prepared.temporary) {
1462
+ const temporary = prepared.temporary;
1463
+ await scope.operation(() => stageZip(scope, { ...temporary, source }, async (staging) => {
1464
+ const current = await scope.operation(() => context.fs.realpath(temporary.name, { signal: context.signal }));
1465
+ if (current !== temporary.parent)
1466
+ fail("ZIP temporary path changed before output");
1467
+ await emit(scope.input(staging.file.path));
1468
+ }));
892
1469
  }
1470
+ else
1471
+ await emit(source);
893
1472
  }
894
1473
  finally {
895
1474
  await output.close();
896
1475
  }
1476
+ prepared.finishProgress();
1477
+ for (const message of prepared.progress)
1478
+ await budget.output(message);
897
1479
  }
1480
+ if (parsed.globalDots)
1481
+ await budget.output("\n");
1482
+ if (parsed.debug)
1483
+ await budget.output("sd: Virtual archive complete\n");
898
1484
  await removeZipSources(scope, prepared.moves, budget, parsed.quiet);
899
- return { exitCode: prepared.kind === "file" ? prepared.exitCode : 0 };
1485
+ return { exitCode: prepared.kind === "file" || prepared.kind === "staged-stream" ? prepared.exitCode : 0 };
900
1486
  }
901
1487
  catch (error) {
902
1488
  original.signal.throwIfAborted();
903
1489
  context.signal.throwIfAborted();
1490
+ // Incomplete selection has not proved the log is disjoint from sources.
1491
+ // Report to the screen rather than mutating an unvisited source file.
1492
+ if (log && !log.started)
1493
+ log.failed = true;
1494
+ if (error instanceof ZipHostFailure && (error.code === "password-empty" || error.code === "password-confirm")) {
1495
+ await budget.output(`\nzip error: Invalid command arguments (${error.message})\n`);
1496
+ return { exitCode: 16 };
1497
+ }
904
1498
  if (error instanceof ZipFailure) {
905
- await budget.output(`\nzip error: ${error.label} (${error.message})\n`);
1499
+ const diagnostic = `\nzip error: ${error.label} (${zipPublicText(error.message)})\n`;
1500
+ if (log?.failed)
1501
+ await writeBytes(context.stderr, Buffer.from(diagnostic).subarray(0, limits.maxDiagnosticBytes), context.signal);
1502
+ else
1503
+ await budget.output(diagnostic);
906
1504
  return { exitCode: error.status };
907
1505
  }
908
- const message = escapeText(display(publicDiagnosticMessage(error, context.onInternalError).slice(0, 1024)), "diagnostic");
909
- await writeBytes(context.stderr, Buffer.from(`zip: ${message}\n`).subarray(0, limits.maxDiagnosticBytes), context.signal);
1506
+ if (log?.failed)
1507
+ budget = new Budget(context, limits);
1508
+ const detail = display(publicDiagnosticMessage(error, context.onInternalError).slice(0, 1024));
1509
+ const message = escapeText(error instanceof ZipHostFailure ? detail : zipPublicText(detail), "diagnostic");
1510
+ await writeBytes(log && !log.failed ? log.sink(context.stderr) : context.stderr, Buffer.from(`zip: ${message}\n`).subarray(0, limits.maxDiagnosticBytes), context.signal);
910
1511
  return { exitCode: 2 };
911
1512
  }
912
1513
  finally {