@poe-platform/safe-bash 0.1.710 → 0.1.712
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/safe-bash/commands/fmt/index.d.ts +2 -0
- package/dist/safe-bash/commands/fmt/index.d.ts.map +1 -0
- package/dist/safe-bash/commands/fmt/index.js +2 -0
- package/dist/safe-bash/commands/fmt/index.js.map +1 -0
- package/dist/safe-bash/commands/fmt.d.ts +1 -2
- package/dist/safe-bash/commands/fmt.d.ts.map +1 -1
- package/dist/safe-bash/commands/fmt.js +1 -838
- package/dist/safe-bash/commands/fmt.js.map +1 -1
- package/dist/safe-bash/commands/time-env/printenv.d.ts +1 -1
- package/dist/safe-bash/commands/time-env/printenv.d.ts.map +1 -1
- package/dist/safe-bash/commands/time-env/sleep.d.ts +1 -1
- package/dist/safe-bash/commands/time-env/sleep.d.ts.map +1 -1
- package/dist/safe-bash/core.browser.js +155 -2099
- package/dist/safe-bash/core.browser.js.map +3 -3
- package/dist/safe-bash/playwright/private-target-transport.d.ts.map +1 -1
- package/dist/safe-bash/playwright/private-target-transport.js +18 -2
- package/dist/safe-bash/playwright/private-target-transport.js.map +1 -1
- package/dist/safe-bash/playwright/profile.d.ts +3 -1
- package/dist/safe-bash/playwright/profile.d.ts.map +1 -1
- package/dist/safe-bash/playwright/profile.js +3 -3
- package/dist/safe-bash/playwright/profile.js.map +1 -1
- package/dist/safe-bash-command-exiftool/index.js +1401 -6
- package/dist/safe-bash-command-exiftool/index.js.map +7 -0
- package/dist/safe-bash-command-fmt/arguments.d.ts +8 -0
- package/dist/safe-bash-command-fmt/command.d.ts +20 -0
- package/dist/safe-bash-command-fmt/contracts.d.ts +42 -0
- package/dist/safe-bash-command-fmt/engine.d.ts +20 -0
- package/dist/safe-bash-command-fmt/index.d.ts +5 -0
- package/dist/safe-bash-command-fmt/index.js +2419 -0
- package/dist/safe-bash-command-fmt/index.js.map +7 -0
- package/dist/safe-bash-command-fmt/sdk.d.ts +15 -0
- package/dist/safe-bash-command-wkhtmltopdf/index.js +1458 -7
- package/dist/safe-bash-command-wkhtmltopdf/index.js.map +7 -0
- package/dist/safe-bash-contracts/index.d.ts +8 -0
- package/dist/safe-bash-contracts/index.js +8 -0
- package/dist/safe-playwright-cloudflare/browser-run-code.js +15 -1
- package/package.json +6 -2
- package/dist/safe-bash-command-exiftool/argfiles.js +0 -163
- package/dist/safe-bash-command-exiftool/arguments.js +0 -128
- package/dist/safe-bash-command-exiftool/command.js +0 -324
- package/dist/safe-bash-command-exiftool/csv.js +0 -78
- package/dist/safe-bash-command-exiftool/paths.js +0 -18
- package/dist/safe-bash-command-exiftool/png.js +0 -308
- package/dist/safe-bash-command-exiftool/publication.js +0 -80
- package/dist/safe-bash-command-exiftool/registry.js +0 -14
- package/dist/safe-bash-command-exiftool/resources.js +0 -29
- package/dist/safe-bash-command-exiftool/scalar.js +0 -103
- package/dist/safe-bash-command-exiftool/sdk.js +0 -113
- package/dist/safe-bash-command-exiftool/shifts.js +0 -29
- package/dist/safe-bash-command-wkhtmltopdf/command.js +0 -373
- package/dist/safe-bash-command-wkhtmltopdf/engine.js +0 -99
- package/dist/safe-bash-command-wkhtmltopdf/errors.js +0 -11
- package/dist/safe-bash-command-wkhtmltopdf/limits.js +0 -69
- package/dist/safe-bash-command-wkhtmltopdf/outcome.js +0 -38
- package/dist/safe-bash-command-wkhtmltopdf/parser.js +0 -146
- package/dist/safe-bash-command-wkhtmltopdf/resources.js +0 -284
- package/dist/safe-bash-command-wkhtmltopdf/settings.js +0 -35
- package/dist/safe-bash-command-wkhtmltopdf/switches.js +0 -127
- package/dist/safe-bash-command-wkhtmltopdf/values.js +0 -66
|
@@ -1,6 +1,1401 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// packages/safe-bash-command-exiftool/dist/command.js
|
|
2
|
+
import { commandRuntimeIdentity, getCommandArguments } from "../safe-bash-contracts/command.js";
|
|
3
|
+
import { FsError as FsError2 } from "../safe-bash-contracts/errors.js";
|
|
4
|
+
import { readBytes as readBytes2, writeBytes } from "../safe-bash-contracts/io.js";
|
|
5
|
+
import { createOutputOperation } from "../safe-bash-contracts/output.js";
|
|
6
|
+
import { shellValueByteLength } from "../safe-bash-contracts/value.js";
|
|
7
|
+
|
|
8
|
+
// packages/safe-bash-command-exiftool/dist/registry.js
|
|
9
|
+
var textChunks = Object.freeze(["tEXt", "iTXt"]);
|
|
10
|
+
var writeChunks = Object.freeze({
|
|
11
|
+
Title: textChunks,
|
|
12
|
+
Author: textChunks,
|
|
13
|
+
Description: textChunks,
|
|
14
|
+
Comment: textChunks,
|
|
15
|
+
Copyright: textChunks,
|
|
16
|
+
ModifyDate: Object.freeze(["tIME"])
|
|
17
|
+
});
|
|
18
|
+
var exiftoolRegistry = Object.freeze({
|
|
19
|
+
version: 2,
|
|
20
|
+
source: Object.freeze({ version: "13.59", commit: "2200871d9cef988051d2a99d67df3bda6cbb30a8", archiveSha256: "e1e2ad6c6fbf568afee5993ef8b2b91ab013d21698c9304e079e633ad82776f5" }),
|
|
21
|
+
formats: Object.freeze({ PNG: Object.freeze({ reader: "uncompressed-text-time", writer: "selected-text-time" }) }),
|
|
22
|
+
tags: Object.freeze(Object.keys(writeChunks)),
|
|
23
|
+
writeChunks,
|
|
24
|
+
scalarShiftErrorGroups: Object.freeze({ Title: "XMP-xmp" })
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// packages/safe-bash-command-exiftool/dist/arguments.js
|
|
28
|
+
function parseArguments(args, limits) {
|
|
29
|
+
if (args.length > 4096)
|
|
30
|
+
throw new RangeError("ExifTool argument count exceeded");
|
|
31
|
+
let extent = 0;
|
|
32
|
+
for (const arg of args) {
|
|
33
|
+
extent += arg.length * 2;
|
|
34
|
+
if (extent > limits.maxDecodedBytes)
|
|
35
|
+
throw new RangeError("ExifTool argument decoded budget exceeded");
|
|
36
|
+
}
|
|
37
|
+
const result = {
|
|
38
|
+
files: [],
|
|
39
|
+
tags: [],
|
|
40
|
+
assignments: [],
|
|
41
|
+
json: false,
|
|
42
|
+
csv: false,
|
|
43
|
+
quoteScalars: false,
|
|
44
|
+
duplicates: false,
|
|
45
|
+
binary: false,
|
|
46
|
+
missing: false,
|
|
47
|
+
style: "short",
|
|
48
|
+
overwrite: "backup",
|
|
49
|
+
destination: void 0,
|
|
50
|
+
groupFamily: void 0
|
|
51
|
+
};
|
|
52
|
+
let literal = false;
|
|
53
|
+
let valueConvSelector = false;
|
|
54
|
+
for (let index = 0; index < args.length; index++) {
|
|
55
|
+
const arg = args[index];
|
|
56
|
+
const option = arg.toLowerCase();
|
|
57
|
+
const value = () => {
|
|
58
|
+
if (++index >= args.length)
|
|
59
|
+
throw new Error("Missing argument for " + arg);
|
|
60
|
+
return args[index];
|
|
61
|
+
};
|
|
62
|
+
if (literal || !arg.startsWith("-") || arg === "-") {
|
|
63
|
+
result.files.push(arg);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === "--") {
|
|
67
|
+
literal = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (option === "-config") {
|
|
71
|
+
if (value() !== "")
|
|
72
|
+
throw new Error("User configuration modules are not supported");
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (option === "-j" || option === "-json") {
|
|
76
|
+
result.json = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (option === "-csv") {
|
|
80
|
+
result.csv = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (arg === "-G4") {
|
|
84
|
+
result.groupFamily = 4;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (option === "-api") {
|
|
88
|
+
if (value().toLowerCase() !== "structformat=jsonq")
|
|
89
|
+
throw new Error("API option not yet supported");
|
|
90
|
+
result.quoteScalars = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (option === "-a") {
|
|
94
|
+
result.duplicates = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (option === "-b") {
|
|
98
|
+
result.binary = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (option === "-f") {
|
|
102
|
+
result.missing = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === "-S") {
|
|
106
|
+
result.style = "compact";
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (option === "-s" || option === "-s1") {
|
|
110
|
+
result.style = "short";
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (option === "-s3") {
|
|
114
|
+
result.style = "values";
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (option === "-n")
|
|
118
|
+
continue;
|
|
119
|
+
if (option === "-overwrite_original") {
|
|
120
|
+
result.overwrite = "replace";
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (option === "-overwrite_original_in_place") {
|
|
124
|
+
result.overwrite = "in-place";
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (option === "-o") {
|
|
128
|
+
result.destination = value();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (["-if", "-p", "-stay_open", "-@", "-common_args"].includes(option) || option.startsWith("-execute"))
|
|
132
|
+
throw new Error("ExifTool option not yet supported (no code evaluation or ambient polling): " + arg);
|
|
133
|
+
const equals = arg.indexOf("=");
|
|
134
|
+
let name = arg.slice(1, equals < 0 ? void 0 : equals);
|
|
135
|
+
const operation = name.endsWith("+") ? "add" : name.endsWith("-") ? "remove" : "set";
|
|
136
|
+
if (operation !== "set")
|
|
137
|
+
name = name.slice(0, -1);
|
|
138
|
+
if (name.endsWith("#")) {
|
|
139
|
+
if (equals < 0)
|
|
140
|
+
valueConvSelector = true;
|
|
141
|
+
name = name.slice(0, -1);
|
|
142
|
+
}
|
|
143
|
+
const known = exiftoolRegistry.tags.find((tag) => tag.toLowerCase() === name.toLowerCase());
|
|
144
|
+
if (equals >= 0)
|
|
145
|
+
result.assignments.push({ name: known ?? name, operation, value: arg.slice(equals + 1) });
|
|
146
|
+
else if (known || name === "MissingTag")
|
|
147
|
+
result.tags.push(known ?? name);
|
|
148
|
+
else
|
|
149
|
+
throw new Error("ExifTool option/tag not yet supported: " + arg);
|
|
150
|
+
}
|
|
151
|
+
if (!result.files.length)
|
|
152
|
+
throw new Error("No file specified");
|
|
153
|
+
if (result.files.filter((file) => file === "-").length > 1)
|
|
154
|
+
throw new Error("Repeated stdin operands are not supported");
|
|
155
|
+
if (result.files.includes("-") && result.assignments.length)
|
|
156
|
+
throw new Error("Writing stdin metadata is not yet supported");
|
|
157
|
+
if (result.files.length > 64)
|
|
158
|
+
throw new RangeError("ExifTool file count exceeded");
|
|
159
|
+
if (result.destination !== void 0 && (result.files.length !== 1 || !result.assignments.length))
|
|
160
|
+
throw new Error("-o requires a single file with tag assignments in this profile");
|
|
161
|
+
if (result.json && result.binary)
|
|
162
|
+
throw new Error("JSON binary policy not yet supported");
|
|
163
|
+
if (result.csv && (result.json || result.binary || result.assignments.length))
|
|
164
|
+
throw new Error("CSV combined output/import policy not yet supported");
|
|
165
|
+
if (result.csv && valueConvSelector)
|
|
166
|
+
throw new Error("CSV ValueConv-qualified headers not yet supported");
|
|
167
|
+
if (result.groupFamily !== void 0 && (!result.json || result.assignments.length))
|
|
168
|
+
throw new Error("Group-family qualification outside JSON not yet supported");
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// packages/safe-bash-command-exiftool/dist/resources.js
|
|
173
|
+
var exiftoolLimits = Object.freeze({
|
|
174
|
+
maxInputBytes: 16777216,
|
|
175
|
+
maxDecodedBytes: 8388608,
|
|
176
|
+
maxRetainedBytes: 33554432,
|
|
177
|
+
maxOutputBytes: 16777216,
|
|
178
|
+
maxWork: 268435456
|
|
179
|
+
});
|
|
180
|
+
var ResourceLimitError = class extends RangeError {
|
|
181
|
+
};
|
|
182
|
+
var Resources = class {
|
|
183
|
+
limits;
|
|
184
|
+
signal;
|
|
185
|
+
usage = { input: 0, decoded: 0, retained: 0, output: 0, work: 0 };
|
|
186
|
+
constructor(options) {
|
|
187
|
+
this.signal = options.signal;
|
|
188
|
+
this.signal.throwIfAborted();
|
|
189
|
+
this.limits = Object.freeze({ ...exiftoolLimits, ...options });
|
|
190
|
+
for (const key of Object.keys(exiftoolLimits)) {
|
|
191
|
+
if (!Number.isSafeInteger(this.limits[key]) || this.limits[key] < 0)
|
|
192
|
+
throw new RangeError("Invalid resource limit: " + key);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
admit(kind, amount) {
|
|
196
|
+
this.signal.throwIfAborted();
|
|
197
|
+
const limits = { input: "maxInputBytes", decoded: "maxDecodedBytes", retained: "maxRetainedBytes", output: "maxOutputBytes", work: "maxWork" };
|
|
198
|
+
const total = this.usage[kind] + amount;
|
|
199
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || !Number.isSafeInteger(total) || total > this.limits[limits[kind]])
|
|
200
|
+
throw new ResourceLimitError("ExifTool " + kind + " budget exceeded");
|
|
201
|
+
this.usage[kind] = total;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// packages/safe-bash-command-exiftool/dist/shifts.js
|
|
206
|
+
function isNumericShift(value) {
|
|
207
|
+
const end = value.endsWith("\n") ? value.length - 1 : value.length;
|
|
208
|
+
let index = value[0] === "+" || value[0] === "-" ? 1 : 0;
|
|
209
|
+
const digit = () => index < end && value.charCodeAt(index) >= 48 && value.charCodeAt(index) <= 57;
|
|
210
|
+
const start = index;
|
|
211
|
+
while (digit())
|
|
212
|
+
index++;
|
|
213
|
+
let digits = index - start;
|
|
214
|
+
if (value[index] === "." || value[index] === ",") {
|
|
215
|
+
const start2 = ++index;
|
|
216
|
+
while (digit())
|
|
217
|
+
index++;
|
|
218
|
+
digits += index - start2;
|
|
219
|
+
}
|
|
220
|
+
if (!digits)
|
|
221
|
+
return false;
|
|
222
|
+
if (value[index] === "e" || value[index] === "E") {
|
|
223
|
+
index++;
|
|
224
|
+
if (value[index] === "+" || value[index] === "-")
|
|
225
|
+
index++;
|
|
226
|
+
const start2 = index;
|
|
227
|
+
while (digit())
|
|
228
|
+
index++;
|
|
229
|
+
if (index === start2)
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return index === end;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// packages/safe-bash-command-exiftool/dist/png.js
|
|
236
|
+
var signature = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
237
|
+
var bitDepths = Object.freeze({
|
|
238
|
+
0: [1, 2, 4, 8, 16],
|
|
239
|
+
2: [8, 16],
|
|
240
|
+
3: [1, 2, 4, 8],
|
|
241
|
+
4: [8, 16],
|
|
242
|
+
6: [8, 16]
|
|
243
|
+
});
|
|
244
|
+
function checksum(bytes) {
|
|
245
|
+
let crc = 4294967295;
|
|
246
|
+
for (const byte of bytes) {
|
|
247
|
+
crc ^= byte;
|
|
248
|
+
for (let bit = 0; bit < 8; bit++)
|
|
249
|
+
crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
|
|
250
|
+
}
|
|
251
|
+
return (crc ^ 4294967295) >>> 0;
|
|
252
|
+
}
|
|
253
|
+
function encodePngTime(value, resources) {
|
|
254
|
+
resources.admit("work", value.length * 2 + 32);
|
|
255
|
+
const invalid = () => {
|
|
256
|
+
throw new Error("PNG timestamp syntax/range not yet supported");
|
|
257
|
+
};
|
|
258
|
+
const decimal = (start, end) => {
|
|
259
|
+
let number = 0;
|
|
260
|
+
for (let index2 = start; index2 < end; index2++) {
|
|
261
|
+
const digit = value.charCodeAt(index2) - 48;
|
|
262
|
+
if (!(digit >= 0 && digit <= 9))
|
|
263
|
+
invalid();
|
|
264
|
+
number = number * 10 + digit;
|
|
265
|
+
}
|
|
266
|
+
return number;
|
|
267
|
+
};
|
|
268
|
+
if (value.length < 19 || ![":", "-"].includes(value[4]) || value[7] !== value[4] || ![" ", "T"].includes(value[10]) || value[13] !== ":" || value[16] !== ":")
|
|
269
|
+
invalid();
|
|
270
|
+
const year = decimal(0, 4), month = decimal(5, 7), day = decimal(8, 10);
|
|
271
|
+
const hour = decimal(11, 13), minute = decimal(14, 16), second = decimal(17, 19);
|
|
272
|
+
if (!year || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59)
|
|
273
|
+
invalid();
|
|
274
|
+
let index = 19;
|
|
275
|
+
if (value[index] === ".") {
|
|
276
|
+
const start = ++index;
|
|
277
|
+
while (index < value.length && value.charCodeAt(index) >= 48 && value.charCodeAt(index) <= 57)
|
|
278
|
+
index++;
|
|
279
|
+
if (start === index)
|
|
280
|
+
invalid();
|
|
281
|
+
}
|
|
282
|
+
if (value[index] === "Z")
|
|
283
|
+
index++;
|
|
284
|
+
else if (value[index] === "+" || value[index] === "-") {
|
|
285
|
+
if (value.length - index !== 6 || value[index + 3] !== ":" || decimal(index + 1, index + 3) > 23 || decimal(index + 4, index + 6) > 59)
|
|
286
|
+
invalid();
|
|
287
|
+
index += 6;
|
|
288
|
+
}
|
|
289
|
+
if (index !== value.length)
|
|
290
|
+
invalid();
|
|
291
|
+
resources.admit("retained", 26);
|
|
292
|
+
return new Uint8Array([year >>> 8, year & 255, month, day, hour, minute, second]);
|
|
293
|
+
}
|
|
294
|
+
function pngChunk(type, data, maxBytes = 16777216) {
|
|
295
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0 || data.length + 12 > maxBytes)
|
|
296
|
+
throw new RangeError("PNG output budget exceeded");
|
|
297
|
+
if (type.length !== 4 || [...type].some((character) => !(character >= "A" && character <= "Z" || character >= "a" && character <= "z")))
|
|
298
|
+
throw new TypeError("Invalid PNG chunk type");
|
|
299
|
+
const bytes = new Uint8Array(data.length + 12);
|
|
300
|
+
const view = new DataView(bytes.buffer);
|
|
301
|
+
view.setUint32(0, data.length);
|
|
302
|
+
for (let index = 0; index < 4; index++)
|
|
303
|
+
bytes[index + 4] = type.charCodeAt(index);
|
|
304
|
+
bytes.set(data, 8);
|
|
305
|
+
view.setUint32(bytes.length - 4, checksum(bytes.subarray(4, bytes.length - 4)));
|
|
306
|
+
return bytes;
|
|
307
|
+
}
|
|
308
|
+
function parse(input, resources) {
|
|
309
|
+
resources.admit("input", input.length);
|
|
310
|
+
resources.admit("retained", input.length);
|
|
311
|
+
resources.admit("work", input.length * 10);
|
|
312
|
+
const bytes = new Uint8Array(input);
|
|
313
|
+
if (bytes.length < 8 || signature.some((byte, index) => bytes[index] !== byte))
|
|
314
|
+
throw new Error("Not a PNG file");
|
|
315
|
+
const view = new DataView(bytes.buffer);
|
|
316
|
+
const chunks = [];
|
|
317
|
+
const tags = [];
|
|
318
|
+
const instances = /* @__PURE__ */ new Map();
|
|
319
|
+
let offset = 8;
|
|
320
|
+
let ended = false;
|
|
321
|
+
let imageData = false;
|
|
322
|
+
while (offset < bytes.length) {
|
|
323
|
+
resources.signal.throwIfAborted();
|
|
324
|
+
resources.admit("retained", 128);
|
|
325
|
+
if (ended)
|
|
326
|
+
throw new Error("PNG trailing data not yet supported");
|
|
327
|
+
if (bytes.length - offset < 12)
|
|
328
|
+
throw new Error("PNG truncated chunk");
|
|
329
|
+
const size = view.getUint32(offset);
|
|
330
|
+
if (size > bytes.length - offset - 12)
|
|
331
|
+
throw new Error("PNG truncated chunk data");
|
|
332
|
+
const end = offset + size + 12;
|
|
333
|
+
const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
|
|
334
|
+
if (checksum(bytes.subarray(offset + 4, end - 4)) !== view.getUint32(end - 4))
|
|
335
|
+
throw new Error("PNG CRC mismatch");
|
|
336
|
+
const data = bytes.subarray(offset + 8, end - 4);
|
|
337
|
+
if (offset === 8 && type !== "IHDR")
|
|
338
|
+
throw new Error("PNG missing initial IHDR");
|
|
339
|
+
if (type === "IHDR") {
|
|
340
|
+
if (offset !== 8 || size !== 13)
|
|
341
|
+
throw new Error("Invalid PNG IHDR");
|
|
342
|
+
const header = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
343
|
+
if (!header.getUint32(0) || !header.getUint32(4) || header.getUint32(0) > 2147483647 || header.getUint32(4) > 2147483647)
|
|
344
|
+
throw new Error("Invalid PNG dimensions");
|
|
345
|
+
if (!bitDepths[data[9]]?.includes(data[8]) || data[10] !== 0 || data[11] !== 0 || data[12] > 1)
|
|
346
|
+
throw new Error("Invalid PNG IHDR encoding fields");
|
|
347
|
+
}
|
|
348
|
+
if (type === "IDAT")
|
|
349
|
+
imageData = true;
|
|
350
|
+
if (type === "IEND" && !imageData)
|
|
351
|
+
throw new Error("PNG missing IDAT");
|
|
352
|
+
let tag;
|
|
353
|
+
let rawName, name = "", value = "";
|
|
354
|
+
let raw;
|
|
355
|
+
let start = 0;
|
|
356
|
+
if (type === "zTXt" || type === "eXIf")
|
|
357
|
+
throw new Error("PNG " + type + " metadata not yet supported");
|
|
358
|
+
if (type === "tEXt" || type === "iTXt") {
|
|
359
|
+
const separator = data.indexOf(0);
|
|
360
|
+
if (separator < 1 || separator > 79)
|
|
361
|
+
throw new Error("Invalid PNG text keyword");
|
|
362
|
+
resources.admit("decoded", data.length * 2);
|
|
363
|
+
resources.admit("work", data.length * 8);
|
|
364
|
+
const keyword = Array.from(data.subarray(0, separator), (byte) => String.fromCharCode(byte)).join("");
|
|
365
|
+
start = separator + 1;
|
|
366
|
+
if (type === "iTXt") {
|
|
367
|
+
if (start + 2 > data.length)
|
|
368
|
+
throw new Error("PNG truncated iTXt header");
|
|
369
|
+
if (data[start] !== 0)
|
|
370
|
+
throw new Error("PNG compressed iTXt not yet supported");
|
|
371
|
+
if (data[start + 1] !== 0)
|
|
372
|
+
throw new Error("Invalid PNG iTXt compression method");
|
|
373
|
+
const languageEnd = data.indexOf(0, start + 2);
|
|
374
|
+
const translatedEnd = languageEnd < 0 ? -1 : data.indexOf(0, languageEnd + 1);
|
|
375
|
+
if (translatedEnd < 0)
|
|
376
|
+
throw new Error("PNG truncated iTXt language fields");
|
|
377
|
+
if (languageEnd !== start + 2 || translatedEnd !== languageEnd + 1 || keyword === "XML:com.adobe.xmp")
|
|
378
|
+
throw new Error("PNG qualified/XMP iTXt not yet supported");
|
|
379
|
+
start = translatedEnd + 1;
|
|
380
|
+
}
|
|
381
|
+
raw = data.subarray(start);
|
|
382
|
+
resources.admit("retained", raw.length + data.length * 40 + 128);
|
|
383
|
+
value = type === "tEXt" ? Array.from(raw, (byte) => String.fromCharCode(byte)).join("") : new TextDecoder("utf-8", { ignoreBOM: true }).decode(raw);
|
|
384
|
+
name = exiftoolRegistry.tags.find((name2) => name2.toLowerCase() === keyword.toLowerCase()) ?? keyword;
|
|
385
|
+
rawName = keyword;
|
|
386
|
+
}
|
|
387
|
+
if (type === "tIME") {
|
|
388
|
+
if (size !== 7)
|
|
389
|
+
throw new Error("Invalid PNG tIME length");
|
|
390
|
+
resources.admit("decoded", 50);
|
|
391
|
+
resources.admit("retained", 256);
|
|
392
|
+
resources.admit("work", 128);
|
|
393
|
+
rawName = "tIME";
|
|
394
|
+
name = "ModifyDate";
|
|
395
|
+
raw = data;
|
|
396
|
+
const fields = [data[0] << 8 | data[1], ...data.subarray(2)];
|
|
397
|
+
const parts = fields.map((field, index) => String(field).padStart(index === 0 ? 4 : 2, "0"));
|
|
398
|
+
value = parts.slice(0, 3).join(":") + " " + parts.slice(3).join(":");
|
|
399
|
+
}
|
|
400
|
+
if (rawName !== void 0 && raw !== void 0) {
|
|
401
|
+
const instance = instances.get(name) ?? 0;
|
|
402
|
+
instances.set(name, instance + 1);
|
|
403
|
+
tag = Object.freeze({ name, rawName, chunkType: type, index: chunks.length, value, raw: new Uint8Array(raw), instance, offset: offset + 8 + start, group: "PNG" });
|
|
404
|
+
tags.push(tag);
|
|
405
|
+
}
|
|
406
|
+
chunks.push({ type, bytes: bytes.subarray(offset, end), ...tag ? { tag } : {} });
|
|
407
|
+
if (type === "IEND") {
|
|
408
|
+
if (size)
|
|
409
|
+
throw new Error("Invalid PNG IEND");
|
|
410
|
+
ended = true;
|
|
411
|
+
}
|
|
412
|
+
offset = end;
|
|
413
|
+
}
|
|
414
|
+
if (!ended)
|
|
415
|
+
throw new Error("PNG truncated: missing IEND");
|
|
416
|
+
return { chunks, tags };
|
|
417
|
+
}
|
|
418
|
+
function inspectPng(input, options) {
|
|
419
|
+
const resources = options instanceof Resources ? options : new Resources(options);
|
|
420
|
+
return Object.freeze({ tags: Object.freeze(parse(input, resources).tags) });
|
|
421
|
+
}
|
|
422
|
+
function editPng(input, assignments, options) {
|
|
423
|
+
const resources = options instanceof Resources ? options : new Resources(options);
|
|
424
|
+
const { chunks } = parse(input, resources);
|
|
425
|
+
resources.admit("retained", assignments.length * 128);
|
|
426
|
+
resources.admit("work", assignments.length * (chunks.length + 1));
|
|
427
|
+
const all = assignments.length === 1 && assignments[0].name.toLowerCase() === "all" && assignments[0].operation === "set" && assignments[0].value === "";
|
|
428
|
+
if (all && chunks.some((chunk) => !["IHDR", "PLTE", "IDAT", "IEND", "tEXt", "iTXt", "tIME"].includes(chunk.type)))
|
|
429
|
+
throw new Error("PNG all deletion with unqualified chunks is not yet supported");
|
|
430
|
+
const operations = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const assignment of assignments) {
|
|
432
|
+
resources.admit("decoded", (assignment.name.length + assignment.value.length) * 2);
|
|
433
|
+
resources.admit("work", assignment.name.length * 5 + assignment.value.length * 3);
|
|
434
|
+
resources.admit("work", (chunks.length + assignments.length) * (assignment.value.length + assignment.name.length + 1));
|
|
435
|
+
resources.admit("retained", assignment.name.length * 10 + assignment.value.length * 6);
|
|
436
|
+
if (all)
|
|
437
|
+
continue;
|
|
438
|
+
const name = exiftoolRegistry.tags.find((name2) => name2.toLowerCase() === assignment.name.toLowerCase());
|
|
439
|
+
if (!name)
|
|
440
|
+
throw new Error("Tag write not yet supported: " + assignment.name);
|
|
441
|
+
if (name === "ModifyDate" && assignment.operation !== "set")
|
|
442
|
+
throw new Error("Temporal shifts are not yet supported for ModifyDate");
|
|
443
|
+
if (assignment.operation === "add") {
|
|
444
|
+
const group = exiftoolRegistry.scalarShiftErrorGroups[name];
|
|
445
|
+
if (group && !isNumericShift(assignment.value))
|
|
446
|
+
throw new Error("Shift value for " + group + ":" + name + " is not a number");
|
|
447
|
+
throw new Error("Scalar numeric shifts are not yet supported for " + name);
|
|
448
|
+
}
|
|
449
|
+
if (assignment.operation !== "set" && assignment.operation !== "remove")
|
|
450
|
+
throw new Error("Invalid tag operation");
|
|
451
|
+
const prior = assignment.operation === "set" ? [] : operations.get(name) ?? [];
|
|
452
|
+
prior.push(assignment);
|
|
453
|
+
operations.set(name, prior);
|
|
454
|
+
}
|
|
455
|
+
const output = [signature];
|
|
456
|
+
let outputSize = 8;
|
|
457
|
+
resources.admit("output", 8);
|
|
458
|
+
const append = (bytes) => {
|
|
459
|
+
resources.admit("output", bytes.length);
|
|
460
|
+
outputSize += bytes.length;
|
|
461
|
+
output.push(bytes);
|
|
462
|
+
};
|
|
463
|
+
let inserted = false;
|
|
464
|
+
for (const chunk of chunks) {
|
|
465
|
+
if (all && chunk.tag)
|
|
466
|
+
continue;
|
|
467
|
+
const ops = chunk.tag && exiftoolRegistry.writeChunks[chunk.tag.name]?.includes(chunk.type) ? operations.get(chunk.tag.name) : void 0;
|
|
468
|
+
if (ops?.some((op) => op.operation === "set" || op.value === chunk.tag.value))
|
|
469
|
+
continue;
|
|
470
|
+
if (chunk.type === "IDAT" && !inserted) {
|
|
471
|
+
inserted = true;
|
|
472
|
+
for (const [name, ops2] of operations) {
|
|
473
|
+
const set = ops2.find((op) => op.operation === "set");
|
|
474
|
+
if (!set?.value || ops2.some((op) => op.operation === "remove" && op.value === set.value))
|
|
475
|
+
continue;
|
|
476
|
+
if (name === "ModifyDate") {
|
|
477
|
+
const data2 = encodePngTime(set.value, resources);
|
|
478
|
+
resources.admit("work", 128);
|
|
479
|
+
resources.admit("retained", 19);
|
|
480
|
+
resources.admit("output", 19);
|
|
481
|
+
outputSize += 19;
|
|
482
|
+
output.push(pngChunk("tIME", data2, 19));
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
resources.admit("work", set.value.length * 2);
|
|
486
|
+
let valueBytes = 0;
|
|
487
|
+
let international = false;
|
|
488
|
+
for (let index = 0; index < set.value.length; index++) {
|
|
489
|
+
const code = set.value.charCodeAt(index);
|
|
490
|
+
international ||= code > 127;
|
|
491
|
+
if (code >= 55296 && code <= 56319) {
|
|
492
|
+
const next = set.value.charCodeAt(++index);
|
|
493
|
+
if (!(next >= 56320 && next <= 57343))
|
|
494
|
+
throw new Error("Unpaired surrogate in PNG text write");
|
|
495
|
+
valueBytes += 4;
|
|
496
|
+
} else {
|
|
497
|
+
if (code >= 56320 && code <= 57343)
|
|
498
|
+
throw new Error("Unpaired surrogate in PNG text write");
|
|
499
|
+
valueBytes += code < 128 ? 1 : code < 2048 ? 2 : 3;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const prefix = name + (international ? "\0\0\0\0\0" : "\0");
|
|
503
|
+
const dataSize = prefix.length + valueBytes;
|
|
504
|
+
const size = dataSize + 12;
|
|
505
|
+
resources.admit("work", dataSize * 12);
|
|
506
|
+
resources.admit("retained", prefix.length * 2 + valueBytes + dataSize + size);
|
|
507
|
+
resources.admit("output", size);
|
|
508
|
+
const data = new Uint8Array(dataSize);
|
|
509
|
+
for (let index = 0; index < prefix.length; index++)
|
|
510
|
+
data[index] = prefix.charCodeAt(index);
|
|
511
|
+
data.set(new TextEncoder().encode(set.value), prefix.length);
|
|
512
|
+
outputSize += size;
|
|
513
|
+
output.push(pngChunk(international ? "iTXt" : "tEXt", data, size));
|
|
514
|
+
let instances = 0;
|
|
515
|
+
for (const existing of chunks) {
|
|
516
|
+
resources.signal.throwIfAborted();
|
|
517
|
+
resources.admit("work", 1);
|
|
518
|
+
if (existing.tag?.name === name && exiftoolRegistry.writeChunks[name]?.includes(existing.type))
|
|
519
|
+
instances++;
|
|
520
|
+
}
|
|
521
|
+
for (let instance = 1; instance < instances; instance++)
|
|
522
|
+
append(output[output.length - 1]);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
append(chunk.bytes);
|
|
526
|
+
}
|
|
527
|
+
resources.admit("retained", outputSize);
|
|
528
|
+
resources.admit("work", outputSize);
|
|
529
|
+
const result = new Uint8Array(outputSize);
|
|
530
|
+
let position = 0;
|
|
531
|
+
for (const bytes of output) {
|
|
532
|
+
result.set(bytes, position);
|
|
533
|
+
position += bytes.length;
|
|
534
|
+
}
|
|
535
|
+
resources.signal.throwIfAborted();
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// packages/safe-bash-command-exiftool/dist/publication.js
|
|
540
|
+
import { FsError } from "../safe-bash-contracts/errors.js";
|
|
541
|
+
var Publication = class {
|
|
542
|
+
#context;
|
|
543
|
+
#pending = /* @__PURE__ */ new Set();
|
|
544
|
+
#stages = [];
|
|
545
|
+
#closed = false;
|
|
546
|
+
#closing;
|
|
547
|
+
#serial = 0;
|
|
548
|
+
constructor(context) {
|
|
549
|
+
this.#context = context;
|
|
550
|
+
}
|
|
551
|
+
async track(operation) {
|
|
552
|
+
if (this.#closed)
|
|
553
|
+
throw new Error("ExifTool invocation closed");
|
|
554
|
+
this.#context.signal.throwIfAborted();
|
|
555
|
+
const pending = Promise.resolve().then(operation);
|
|
556
|
+
this.#pending.add(pending);
|
|
557
|
+
try {
|
|
558
|
+
return await pending;
|
|
559
|
+
} finally {
|
|
560
|
+
this.#pending.delete(pending);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
close() {
|
|
564
|
+
if (this.#closing)
|
|
565
|
+
return this.#closing;
|
|
566
|
+
this.#closed = true;
|
|
567
|
+
this.#closing = (async () => {
|
|
568
|
+
await Promise.allSettled([...this.#pending]);
|
|
569
|
+
const failures = [];
|
|
570
|
+
const signal = new AbortController().signal;
|
|
571
|
+
for (const staging of this.#stages.splice(0).reverse()) {
|
|
572
|
+
try {
|
|
573
|
+
await this.#context.fs.removeStagedFile(staging, { signal });
|
|
574
|
+
} catch (error) {
|
|
575
|
+
failures.push(error);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (failures.length)
|
|
579
|
+
throw new AggregateError(failures, "ExifTool staging cleanup failed");
|
|
580
|
+
})();
|
|
581
|
+
return this.#closing;
|
|
582
|
+
}
|
|
583
|
+
async publish(path, bytes, expected, inPlace) {
|
|
584
|
+
const { fs, signal } = this.#context;
|
|
585
|
+
const slash = path.lastIndexOf("/");
|
|
586
|
+
const directory = path.slice(0, slash) || "/";
|
|
587
|
+
const parent = await this.track(() => fs.stat(directory, { signal }));
|
|
588
|
+
const capabilities = fs.capabilitiesFor ? await this.track(() => fs.capabilitiesFor(path, { signal })) : fs.capabilities;
|
|
589
|
+
if (inPlace) {
|
|
590
|
+
if (!expected || !fs.writeFileConditional || !capabilities.atomicFileMutation)
|
|
591
|
+
throw new Error("VFS atomic in-place publication not supported");
|
|
592
|
+
await this.track(() => fs.writeFileConditional(path, bytes, { expected, parent, signal }));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (expected && expected.nlink !== 1)
|
|
596
|
+
throw new Error("VFS hardlink replacement is not yet supported");
|
|
597
|
+
if (!fs.createStagedFile || !fs.publishStagedFile || !fs.removeStagedFile || !capabilities.atomicFileStaging)
|
|
598
|
+
throw new Error("VFS atomic replacement publication not supported");
|
|
599
|
+
let stage;
|
|
600
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
601
|
+
try {
|
|
602
|
+
stage = await this.track(async () => {
|
|
603
|
+
const owned = await fs.createStagedFile(directory + (directory === "/" ? "" : "/") + ".exiftool-" + ++this.#serial, "output", { type: "file", data: bytes }, { parent, signal, mode: expected ? expected.mode & 4095 : 438 });
|
|
604
|
+
this.#stages.push(owned);
|
|
605
|
+
return owned;
|
|
606
|
+
});
|
|
607
|
+
break;
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (!(error instanceof FsError) || error.code !== "EEXIST")
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (!stage)
|
|
614
|
+
throw new Error("ExifTool staging collision limit exceeded");
|
|
615
|
+
await this.track(() => fs.publishStagedFile(stage, path, { destination: expected, parent, signal }));
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// packages/safe-bash-command-exiftool/dist/scalar.js
|
|
620
|
+
var jsonEscapes = Object.freeze({ 9: "\\t", 10: "\\n", 13: "\\r", 34: '\\"', 92: "\\\\" });
|
|
621
|
+
function admit(value, options) {
|
|
622
|
+
options.signal?.throwIfAborted();
|
|
623
|
+
const limits = [options.maxDecodedBytes ?? 2097152, options.maxOutputBytes ?? 6291458, options.maxWork ?? 4194304, options.maxRetainedBytes ?? 14680068];
|
|
624
|
+
if (limits.some((limit) => !Number.isSafeInteger(limit) || limit < 0))
|
|
625
|
+
throw new RangeError("Invalid scalar limit");
|
|
626
|
+
if (value.length * 2 > limits[0])
|
|
627
|
+
throw new RangeError("Scalar decoded bytes exceeded");
|
|
628
|
+
if (value.length * 4 > limits[2])
|
|
629
|
+
throw new RangeError("Scalar work exceeded");
|
|
630
|
+
if (4 + value.length * 14 > limits[3])
|
|
631
|
+
throw new RangeError("Scalar retained bytes exceeded");
|
|
632
|
+
}
|
|
633
|
+
function numericSpelling(value) {
|
|
634
|
+
const length = value.endsWith("\n") ? value.length - 1 : value.length;
|
|
635
|
+
let index = value[0] === "-" ? 1 : 0;
|
|
636
|
+
const start = index;
|
|
637
|
+
const digit = () => index < length && value.charCodeAt(index) >= 48 && value.charCodeAt(index) <= 57;
|
|
638
|
+
while (digit())
|
|
639
|
+
index++;
|
|
640
|
+
const integers = index - start;
|
|
641
|
+
if (!integers || integers > 15 || integers > 1 && value[start] === "0")
|
|
642
|
+
return false;
|
|
643
|
+
if (value[index] === ".") {
|
|
644
|
+
const fraction = ++index;
|
|
645
|
+
while (digit())
|
|
646
|
+
index++;
|
|
647
|
+
if (index === fraction || index - fraction > 16)
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
if (value[index] === "e" || value[index] === "E") {
|
|
651
|
+
index++;
|
|
652
|
+
if (value[index] === "+" || value[index] === "-")
|
|
653
|
+
index++;
|
|
654
|
+
const exponent = index;
|
|
655
|
+
while (digit())
|
|
656
|
+
index++;
|
|
657
|
+
if (index === exponent || index - exponent > 3)
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
return index === length;
|
|
661
|
+
}
|
|
662
|
+
function encodeJsonScalar(value, options = {}) {
|
|
663
|
+
admit(value, options);
|
|
664
|
+
const limit = options.maxOutputBytes ?? 6291458;
|
|
665
|
+
if (!options.quoteScalars) {
|
|
666
|
+
const lower = value.toLowerCase();
|
|
667
|
+
const boolean = lower.endsWith("\n") ? lower.slice(0, -1) : lower;
|
|
668
|
+
const token = boolean === "true" || boolean === "false" ? lower : numericSpelling(value) ? value : void 0;
|
|
669
|
+
if (token !== void 0) {
|
|
670
|
+
if (token.length > limit)
|
|
671
|
+
throw new RangeError("Scalar output bytes exceeded");
|
|
672
|
+
return token;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (2 + value.length * 6 > limit)
|
|
676
|
+
throw new RangeError("Scalar output bytes exceeded");
|
|
677
|
+
let output = '"';
|
|
678
|
+
for (let index = 0; index < value.length; index++) {
|
|
679
|
+
const code = value.charCodeAt(index);
|
|
680
|
+
if (code === 0)
|
|
681
|
+
continue;
|
|
682
|
+
const escape = jsonEscapes[code];
|
|
683
|
+
if (escape)
|
|
684
|
+
output += escape;
|
|
685
|
+
else if (code < 32 || code === 127)
|
|
686
|
+
output += "\\u" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
687
|
+
else if (code >= 55296 && code <= 56319) {
|
|
688
|
+
const next = value.charCodeAt(index + 1);
|
|
689
|
+
if (next >= 56320 && next <= 57343)
|
|
690
|
+
output += value[index] + value[++index];
|
|
691
|
+
else
|
|
692
|
+
output += "\uFFFD";
|
|
693
|
+
} else if (code >= 56320 && code <= 57343)
|
|
694
|
+
output += "\uFFFD";
|
|
695
|
+
else
|
|
696
|
+
output += value[index];
|
|
697
|
+
}
|
|
698
|
+
options.signal?.throwIfAborted();
|
|
699
|
+
return output + '"';
|
|
700
|
+
}
|
|
701
|
+
function printable(value, options = {}) {
|
|
702
|
+
admit(value, options);
|
|
703
|
+
if (value.length * 3 > (options.maxOutputBytes ?? 6291458))
|
|
704
|
+
throw new RangeError("Scalar output bytes exceeded");
|
|
705
|
+
let end = value.length;
|
|
706
|
+
while (end && (value.charCodeAt(end - 1) === 0 || value[end - 1] === " "))
|
|
707
|
+
end--;
|
|
708
|
+
let output = "";
|
|
709
|
+
for (let index = 0; index < end; index++) {
|
|
710
|
+
const code = value.charCodeAt(index);
|
|
711
|
+
if (code)
|
|
712
|
+
output += code < 32 || code === 127 ? "." : value[index];
|
|
713
|
+
}
|
|
714
|
+
return output;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// packages/safe-bash-command-exiftool/dist/csv.js
|
|
718
|
+
var CsvTable = class {
|
|
719
|
+
resources;
|
|
720
|
+
missing;
|
|
721
|
+
headers = /* @__PURE__ */ new Map();
|
|
722
|
+
records = [];
|
|
723
|
+
sortHeaders = false;
|
|
724
|
+
constructor(resources, missing, names) {
|
|
725
|
+
this.resources = resources;
|
|
726
|
+
this.missing = missing;
|
|
727
|
+
for (const name of names) {
|
|
728
|
+
resources.admit("work", name.length * 2 + 1);
|
|
729
|
+
resources.admit("retained", name.length * 4 + 128);
|
|
730
|
+
const key = name.toLowerCase();
|
|
731
|
+
if (key !== "sourcefile" && !this.headers.has(key))
|
|
732
|
+
this.headers.set(key, { name, found: missing });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
add(file, tags) {
|
|
736
|
+
this.resources.admit("work", file.length + tags.length);
|
|
737
|
+
this.resources.admit("retained", file.length * 2 + 256);
|
|
738
|
+
const fields = /* @__PURE__ */ new Map();
|
|
739
|
+
for (const tag of tags) {
|
|
740
|
+
this.resources.admit("work", tag.name.length * 2 + tag.value.length + 1);
|
|
741
|
+
this.resources.admit("retained", (tag.name.length + tag.value.length) * 4 + 256);
|
|
742
|
+
const key = tag.name.toLowerCase();
|
|
743
|
+
if (key === "sourcefile")
|
|
744
|
+
continue;
|
|
745
|
+
if (!this.headers.has(key)) {
|
|
746
|
+
if (this.records.length)
|
|
747
|
+
this.sortHeaders = true;
|
|
748
|
+
this.headers.set(key, { name: tag.name, found: true });
|
|
749
|
+
} else {
|
|
750
|
+
this.headers.set(key, { name: tag.name, found: true });
|
|
751
|
+
}
|
|
752
|
+
fields.set(key, tag.value);
|
|
753
|
+
}
|
|
754
|
+
this.records.push({ file, fields });
|
|
755
|
+
}
|
|
756
|
+
render() {
|
|
757
|
+
const extent = [...this.headers.keys()].reduce((total, key) => total + key.length + 1, 0);
|
|
758
|
+
this.resources.admit("work", extent * (this.headers.size + 1) + this.records.length * (this.headers.size + 1));
|
|
759
|
+
this.resources.admit("retained", this.headers.size * 64 + this.records.length * 32);
|
|
760
|
+
const columns = [...this.headers].filter(([, header]) => header.found);
|
|
761
|
+
if (this.sortHeaders)
|
|
762
|
+
columns.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
763
|
+
const quote = (value) => {
|
|
764
|
+
this.resources.admit("work", value.length * 4 + 1);
|
|
765
|
+
this.resources.admit("retained", value.length * 8 + 16);
|
|
766
|
+
let quoted = false;
|
|
767
|
+
let text = "";
|
|
768
|
+
for (const character of value) {
|
|
769
|
+
if (character === '"') {
|
|
770
|
+
text += '""';
|
|
771
|
+
quoted = true;
|
|
772
|
+
} else
|
|
773
|
+
text += character;
|
|
774
|
+
if (character === "," || character === "\n" || character === "\r")
|
|
775
|
+
quoted = true;
|
|
776
|
+
}
|
|
777
|
+
const whitespace2 = (character) => " \n\r\f\v\x85\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000".includes(character);
|
|
778
|
+
if (value.length && (whitespace2(value[0]) || whitespace2(value[value.length - 1])))
|
|
779
|
+
quoted = true;
|
|
780
|
+
return quoted ? '"' + text + '"' : text;
|
|
781
|
+
};
|
|
782
|
+
const lines = [["SourceFile", ...columns.map(([, header]) => quote(header.name))].join(",")];
|
|
783
|
+
for (const record of this.records) {
|
|
784
|
+
this.resources.admit("retained", (columns.length + 1) * 16);
|
|
785
|
+
lines.push([quote(record.file), ...columns.map(([key]) => quote(record.fields.get(key) ?? (this.missing ? "-" : "")))].join(","));
|
|
786
|
+
}
|
|
787
|
+
const size = lines.reduce((total, line) => total + line.length + 1, 0);
|
|
788
|
+
this.resources.admit("work", size * 2);
|
|
789
|
+
this.resources.admit("retained", size * 4);
|
|
790
|
+
return lines.join("\n") + "\n";
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// packages/safe-bash-command-exiftool/dist/argfiles.js
|
|
795
|
+
import { readBytes } from "../safe-bash-contracts/io.js";
|
|
796
|
+
|
|
797
|
+
// packages/safe-bash-command-exiftool/dist/paths.js
|
|
798
|
+
function virtualPath(cwd, input, resources) {
|
|
799
|
+
if (input.includes("\0") || input === "-")
|
|
800
|
+
throw new Error("Invalid or unsupported ExifTool file path");
|
|
801
|
+
const extent = input.length + (input.startsWith("/") ? 0 : cwd.length + 1);
|
|
802
|
+
resources.admit("decoded", extent * 2);
|
|
803
|
+
resources.admit("retained", extent * 32 + 256);
|
|
804
|
+
resources.admit("work", extent * 24 + 16);
|
|
805
|
+
const parts = [];
|
|
806
|
+
for (const part of (input.startsWith("/") ? input : cwd + "/" + input).split("/")) {
|
|
807
|
+
if (part === "..")
|
|
808
|
+
parts.pop();
|
|
809
|
+
else if (part && part !== ".")
|
|
810
|
+
parts.push(part);
|
|
811
|
+
}
|
|
812
|
+
return "/" + parts.join("/");
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// packages/safe-bash-command-exiftool/dist/argfiles.js
|
|
816
|
+
var escapes = Object.freeze({ a: "\x07", b: "\b", f: "\f", n: "\n", r: "\r", t: " ", '"': '"', "\\": "\\" });
|
|
817
|
+
function whitespace(character) {
|
|
818
|
+
return character === " " || character === " " || character === "\n" || character === "\r" || character === "\f" || character === "\v";
|
|
819
|
+
}
|
|
820
|
+
function filterArgfileLine(line) {
|
|
821
|
+
const cstr = line.startsWith("#[CSTR]");
|
|
822
|
+
if (line.startsWith("#") && !cstr)
|
|
823
|
+
return void 0;
|
|
824
|
+
let start = cstr ? 7 : 0, end = line.length;
|
|
825
|
+
if (!cstr)
|
|
826
|
+
while (start < end && whitespace(line[start]))
|
|
827
|
+
start++;
|
|
828
|
+
while (end > start && (line[end - 1] === "\r" || line[end - 1] === "\n"))
|
|
829
|
+
end--;
|
|
830
|
+
const value = line.slice(start, end);
|
|
831
|
+
if (cstr) {
|
|
832
|
+
const parts = [];
|
|
833
|
+
for (let index2 = 0; index2 < value.length; index2++) {
|
|
834
|
+
const character = value[index2];
|
|
835
|
+
if (character === "\\" && index2 + 1 < value.length) {
|
|
836
|
+
const next = value[++index2];
|
|
837
|
+
parts.push(Object.hasOwn(escapes, next) ? escapes[next] : "\\" + next);
|
|
838
|
+
} else
|
|
839
|
+
parts.push(character);
|
|
840
|
+
}
|
|
841
|
+
return parts.join("");
|
|
842
|
+
}
|
|
843
|
+
if (!value)
|
|
844
|
+
return void 0;
|
|
845
|
+
if (value[0] !== "-")
|
|
846
|
+
return value;
|
|
847
|
+
let index = 1;
|
|
848
|
+
while (index < value.length) {
|
|
849
|
+
const character = value[index];
|
|
850
|
+
if (!(character === "-" || character === "_" || character === ":" || character >= "0" && character <= "9" || character >= "A" && character <= "Z" || character >= "a" && character <= "z"))
|
|
851
|
+
break;
|
|
852
|
+
index++;
|
|
853
|
+
}
|
|
854
|
+
if (index === 1)
|
|
855
|
+
return value;
|
|
856
|
+
if (value[index] === "#")
|
|
857
|
+
index++;
|
|
858
|
+
const nameEnd = index;
|
|
859
|
+
while (index < value.length && whitespace(value[index]))
|
|
860
|
+
index++;
|
|
861
|
+
const operationStart = index;
|
|
862
|
+
if (value[index] === "+" || value[index] === "-" || value[index] === "<")
|
|
863
|
+
index++;
|
|
864
|
+
if (value[index] !== "=")
|
|
865
|
+
return value;
|
|
866
|
+
index++;
|
|
867
|
+
const operationEnd = index;
|
|
868
|
+
if (value[index] === " ")
|
|
869
|
+
index++;
|
|
870
|
+
return value.slice(0, nameEnd) + value.slice(operationStart, operationEnd) + value.slice(index);
|
|
871
|
+
}
|
|
872
|
+
async function expandArgfiles(args, context, publication, resources) {
|
|
873
|
+
const frames = [{ args, index: 0 }];
|
|
874
|
+
const result = [];
|
|
875
|
+
let literal = false;
|
|
876
|
+
let takesValue = false;
|
|
877
|
+
while (frames.length) {
|
|
878
|
+
resources.signal.throwIfAborted();
|
|
879
|
+
const frame = frames[frames.length - 1];
|
|
880
|
+
if (frame.index >= frame.args.length) {
|
|
881
|
+
frames.pop();
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const arg = frame.args[frame.index++];
|
|
885
|
+
resources.admit("work", arg.length + 1);
|
|
886
|
+
const option = arg.toLowerCase();
|
|
887
|
+
if (!literal && !takesValue && frame.path && (option === "-config" || option === "-common_args"))
|
|
888
|
+
throw new Error("Configuration/common arguments are not allowed in argument files");
|
|
889
|
+
if (!literal && !takesValue && option === "-@") {
|
|
890
|
+
while (frames.length && frames[frames.length - 1].index >= frames[frames.length - 1].args.length)
|
|
891
|
+
frames.pop();
|
|
892
|
+
const owner = frames[frames.length - 1];
|
|
893
|
+
if (!owner)
|
|
894
|
+
throw new Error("Missing argument for -@");
|
|
895
|
+
const input = owner.args[owner.index++];
|
|
896
|
+
if (input === "-" || input.includes("\0"))
|
|
897
|
+
throw new Error("Argument-file stdin/invalid paths are not supported");
|
|
898
|
+
const path = virtualPath(context.cwd, input, resources);
|
|
899
|
+
if (frames.some((active) => active.path === path))
|
|
900
|
+
throw new Error("Argument-file cycle is not supported");
|
|
901
|
+
if (frames.length >= 16)
|
|
902
|
+
throw new Error("Argument-file nesting limit exceeded");
|
|
903
|
+
const stat = await publication.track(() => context.fs.lstat(path, { signal: context.signal }));
|
|
904
|
+
if (stat.type !== "file")
|
|
905
|
+
throw new Error("Only regular VFS argument files are admitted");
|
|
906
|
+
resources.admit("input", stat.size);
|
|
907
|
+
resources.admit("decoded", stat.size * 2);
|
|
908
|
+
resources.admit("retained", stat.size * 48 + 256);
|
|
909
|
+
resources.admit("work", stat.size * 24 + path.length);
|
|
910
|
+
const bytes = await publication.track(async () => {
|
|
911
|
+
if (!context.fs.readStream) {
|
|
912
|
+
const result2 = await context.fs.readFile(path, { signal: context.signal, maxBytes: stat.size });
|
|
913
|
+
context.signal.throwIfAborted();
|
|
914
|
+
if (result2.length > stat.size)
|
|
915
|
+
throw new RangeError("ExifTool argument file grew beyond admitted size");
|
|
916
|
+
return result2;
|
|
917
|
+
}
|
|
918
|
+
const chunks = [];
|
|
919
|
+
let extent = 0;
|
|
920
|
+
for await (const chunk of readBytes(context.fs.readStream(path, { signal: context.signal }), context.signal)) {
|
|
921
|
+
if (chunk.length > stat.size - extent)
|
|
922
|
+
throw new Error("Argument file grew beyond admitted size");
|
|
923
|
+
resources.admit("retained", chunk.length + 128);
|
|
924
|
+
resources.admit("work", chunk.length + 1);
|
|
925
|
+
chunks.push(new Uint8Array(chunk));
|
|
926
|
+
extent += chunk.length;
|
|
927
|
+
}
|
|
928
|
+
const owned = new Uint8Array(extent);
|
|
929
|
+
let offset = 0;
|
|
930
|
+
for (const chunk of chunks) {
|
|
931
|
+
owned.set(chunk, offset);
|
|
932
|
+
offset += chunk.length;
|
|
933
|
+
}
|
|
934
|
+
return owned;
|
|
935
|
+
});
|
|
936
|
+
if (bytes.length > stat.size)
|
|
937
|
+
throw new Error("Argument file grew beyond admitted size");
|
|
938
|
+
let text;
|
|
939
|
+
try {
|
|
940
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
941
|
+
} catch {
|
|
942
|
+
throw new Error("Only UTF-8 argument files are currently supported");
|
|
943
|
+
}
|
|
944
|
+
const expanded = [];
|
|
945
|
+
let start = 0;
|
|
946
|
+
for (let index = 0; index <= text.length; index++) {
|
|
947
|
+
if (index !== text.length && text[index] !== "\n")
|
|
948
|
+
continue;
|
|
949
|
+
const value = filterArgfileLine(text.slice(start, index === text.length ? index : index + 1));
|
|
950
|
+
start = index + 1;
|
|
951
|
+
if (value !== void 0) {
|
|
952
|
+
resources.admit("retained", value.length * 2 + 128);
|
|
953
|
+
if (expanded.length >= 4096)
|
|
954
|
+
throw new Error("Argument-file argument count exceeded");
|
|
955
|
+
expanded.push(value);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
frames.push({ args: expanded, index: 0, path });
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (!literal && !takesValue && arg === "--")
|
|
962
|
+
literal = true;
|
|
963
|
+
const wasValue = takesValue;
|
|
964
|
+
takesValue = !literal && !wasValue && ["-config", "-api", "-o", "-if", "-p", "-stay_open"].includes(option);
|
|
965
|
+
resources.admit("retained", arg.length * 2 + 128);
|
|
966
|
+
if (result.length >= 4096)
|
|
967
|
+
throw new Error("ExifTool argument count exceeded");
|
|
968
|
+
result.push(arg);
|
|
969
|
+
}
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// packages/safe-bash-command-exiftool/dist/command.js
|
|
974
|
+
function selected(tags, names, duplicates, resources) {
|
|
975
|
+
resources.admit("work", tags.length + names.length);
|
|
976
|
+
resources.admit("retained", tags.length * 32);
|
|
977
|
+
const requested = names.length ? names : [...new Set(tags.map((tag) => tag.name))];
|
|
978
|
+
const requestedExtent = requested.reduce((sum, name) => sum + name.length + 1, 0);
|
|
979
|
+
const tagExtent = tags.reduce((sum, tag) => sum + tag.name.length + 1, 0);
|
|
980
|
+
resources.admit("work", tags.length * requestedExtent + tagExtent * requested.length * 2);
|
|
981
|
+
resources.admit("retained", requested.length * (128 + tags.length * 16));
|
|
982
|
+
const values = [];
|
|
983
|
+
for (const name of requested) {
|
|
984
|
+
const matching = tags.filter((tag) => tag.name.toLowerCase() === name.toLowerCase());
|
|
985
|
+
if (duplicates)
|
|
986
|
+
values.push(...matching);
|
|
987
|
+
else if (matching.length)
|
|
988
|
+
values.push(matching[matching.length - 1]);
|
|
989
|
+
}
|
|
990
|
+
return values;
|
|
991
|
+
}
|
|
992
|
+
function createExiftoolCommand(options = {}) {
|
|
993
|
+
const limits = Object.freeze({ ...options.limits });
|
|
994
|
+
return Object.freeze({
|
|
995
|
+
name: "exiftool",
|
|
996
|
+
runtimeIdentity: commandRuntimeIdentity,
|
|
997
|
+
description: "Inspect and edit admitted PNG metadata through virtual files",
|
|
998
|
+
async execute(context) {
|
|
999
|
+
const publication = new Publication(context);
|
|
1000
|
+
let stdout, stderr;
|
|
1001
|
+
let failed = false, failure;
|
|
1002
|
+
let closing;
|
|
1003
|
+
const cleanup = () => {
|
|
1004
|
+
closing ??= (async () => {
|
|
1005
|
+
const results = await Promise.allSettled([publication.close(), stdout?.close(), stderr?.close()]);
|
|
1006
|
+
const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
1007
|
+
if (failures.length) {
|
|
1008
|
+
if (failed)
|
|
1009
|
+
failures.unshift(failure);
|
|
1010
|
+
throw new AggregateError(failures, "ExifTool invocation and cleanup failed");
|
|
1011
|
+
}
|
|
1012
|
+
})();
|
|
1013
|
+
return closing;
|
|
1014
|
+
};
|
|
1015
|
+
context.registerCleanup?.(cleanup);
|
|
1016
|
+
try {
|
|
1017
|
+
stdout = createOutputOperation(context, context.stdout);
|
|
1018
|
+
stderr = createOutputOperation(context, context.stderr);
|
|
1019
|
+
const resources = new Resources({ ...limits, signal: context.signal });
|
|
1020
|
+
const output = async (text, error = false) => {
|
|
1021
|
+
resources.admit("work", text.length * 2);
|
|
1022
|
+
resources.admit("output", text.length * 3);
|
|
1023
|
+
resources.admit("retained", text.length * 5);
|
|
1024
|
+
await writeBytes(error ? stderr.output : stdout.output, new TextEncoder().encode(text), context.signal);
|
|
1025
|
+
};
|
|
1026
|
+
let invocation;
|
|
1027
|
+
try {
|
|
1028
|
+
const carrier = getCommandArguments(context);
|
|
1029
|
+
for (let index = 0; index < carrier.values.length; index++) {
|
|
1030
|
+
const extent = shellValueByteLength(carrier.values[index]);
|
|
1031
|
+
resources.admit("decoded", context.args[index].length * 2);
|
|
1032
|
+
resources.admit("retained", extent + 128);
|
|
1033
|
+
resources.admit("work", extent * 4);
|
|
1034
|
+
const bytes = carrier.bytes(index);
|
|
1035
|
+
try {
|
|
1036
|
+
if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes) !== context.args[index])
|
|
1037
|
+
throw new Error("Argv Unicode mismatch");
|
|
1038
|
+
} catch {
|
|
1039
|
+
throw new Error("Only qualified UTF-8 argv is currently supported");
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
invocation = parseArguments(await expandArgfiles(context.args, context, publication, resources), resources.limits);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
context.signal.throwIfAborted();
|
|
1045
|
+
if (error instanceof ResourceLimitError)
|
|
1046
|
+
throw error;
|
|
1047
|
+
await output("Error: " + (error instanceof Error ? error.message : String(error)) + "\n", true);
|
|
1048
|
+
return { exitCode: 1 };
|
|
1049
|
+
}
|
|
1050
|
+
const json = [];
|
|
1051
|
+
const csv = invocation.csv ? new CsvTable(resources, invocation.missing, invocation.tags) : void 0;
|
|
1052
|
+
const assignment = invocation.assignments.length === 1 ? invocation.assignments[0] : void 0;
|
|
1053
|
+
if (assignment?.operation === "add" && Object.hasOwn(exiftoolRegistry.scalarShiftErrorGroups, assignment.name) && !isNumericShift(assignment.value)) {
|
|
1054
|
+
await output("Warning: Shift value for " + exiftoolRegistry.scalarShiftErrorGroups[assignment.name] + ":" + assignment.name + " is not a number\nNothing to do.\n", true);
|
|
1055
|
+
return { exitCode: 1 };
|
|
1056
|
+
}
|
|
1057
|
+
let errors = 0, updated = 0, created = 0, unchanged = 0, read = 0;
|
|
1058
|
+
for (const file of invocation.files) {
|
|
1059
|
+
let tags = [];
|
|
1060
|
+
try {
|
|
1061
|
+
const path = file === "-" ? "-" : virtualPath(context.cwd, file, resources);
|
|
1062
|
+
const original = file === "-" ? void 0 : await publication.track(() => context.fs.lstat(path, { signal: context.signal }));
|
|
1063
|
+
if (original && original.type !== "file")
|
|
1064
|
+
throw new Error("Only regular VFS files are admitted");
|
|
1065
|
+
if (original) {
|
|
1066
|
+
resources.admit("input", original.size);
|
|
1067
|
+
resources.admit("retained", original.size * 32 + 256);
|
|
1068
|
+
resources.admit("decoded", original.size * 2);
|
|
1069
|
+
resources.admit("work", original.size * 20);
|
|
1070
|
+
}
|
|
1071
|
+
const bytes = await publication.track(async () => {
|
|
1072
|
+
if (original && !context.fs.readStream) {
|
|
1073
|
+
const result2 = await context.fs.readFile(path, { signal: context.signal, maxBytes: original.size });
|
|
1074
|
+
context.signal.throwIfAborted();
|
|
1075
|
+
if (result2.length > original.size)
|
|
1076
|
+
throw new RangeError("ExifTool input grew beyond admitted size");
|
|
1077
|
+
return result2;
|
|
1078
|
+
}
|
|
1079
|
+
const chunks = [];
|
|
1080
|
+
let size = 0;
|
|
1081
|
+
const source = original ? context.fs.readStream(path, { signal: context.signal }) : context.stdin;
|
|
1082
|
+
for await (const chunk of readBytes2(source, context.signal)) {
|
|
1083
|
+
resources.admit("work", chunk.length + 64);
|
|
1084
|
+
if (original && chunk.length > original.size - size)
|
|
1085
|
+
throw new RangeError("ExifTool input grew beyond admitted size");
|
|
1086
|
+
if (!original) {
|
|
1087
|
+
resources.admit("input", chunk.length);
|
|
1088
|
+
resources.admit("retained", chunk.length * 32 + 256);
|
|
1089
|
+
resources.admit("decoded", chunk.length * 2);
|
|
1090
|
+
resources.admit("work", chunk.length * 20);
|
|
1091
|
+
}
|
|
1092
|
+
resources.admit("retained", chunk.length + 128);
|
|
1093
|
+
if (chunk.length)
|
|
1094
|
+
chunks.push(new Uint8Array(chunk));
|
|
1095
|
+
size += chunk.length;
|
|
1096
|
+
}
|
|
1097
|
+
resources.admit("retained", size);
|
|
1098
|
+
const result = new Uint8Array(size);
|
|
1099
|
+
let position = 0;
|
|
1100
|
+
for (const chunk of chunks) {
|
|
1101
|
+
result.set(chunk, position);
|
|
1102
|
+
position += chunk.length;
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1105
|
+
});
|
|
1106
|
+
if (!bytes.length)
|
|
1107
|
+
throw new Error("File is empty");
|
|
1108
|
+
const extension = file.slice(file.lastIndexOf(".") + 1).toUpperCase();
|
|
1109
|
+
if (invocation.assignments.length && ["DOCX", "PPTX", "XLSX"].includes(extension))
|
|
1110
|
+
throw new Error("Writing of " + extension + " files is not yet supported");
|
|
1111
|
+
if (extension === "PDF")
|
|
1112
|
+
throw new Error("PDF parser/writer not yet supported; metadata deletion retains historical revisions and never guarantees redaction");
|
|
1113
|
+
const png = bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
1114
|
+
if (invocation.assignments.length) {
|
|
1115
|
+
if (!original)
|
|
1116
|
+
throw new Error("Writing stdin metadata is not yet supported");
|
|
1117
|
+
if (!png)
|
|
1118
|
+
throw new Error("Format writer not yet supported");
|
|
1119
|
+
const edited = editPng(bytes, invocation.assignments, resources);
|
|
1120
|
+
resources.admit("retained", edited.length * 3);
|
|
1121
|
+
resources.admit("work", bytes.length);
|
|
1122
|
+
if (invocation.destination === void 0 && !invocation.assignments.some((op) => op.operation === "set" && op.value !== "") && edited.length === bytes.length && edited.every((byte, index) => byte === bytes[index])) {
|
|
1123
|
+
unchanged++;
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
resources.admit("output", edited.length);
|
|
1127
|
+
const inPlace = invocation.overwrite === "in-place";
|
|
1128
|
+
const destination = invocation.destination === void 0 ? path : virtualPath(context.cwd, invocation.destination, resources);
|
|
1129
|
+
if (invocation.destination === void 0) {
|
|
1130
|
+
await publication.track(() => context.fs.access(path, 2, { signal: context.signal }));
|
|
1131
|
+
if (!inPlace && original.nlink !== 1)
|
|
1132
|
+
throw new Error("VFS hardlink replacement is not yet supported");
|
|
1133
|
+
if (invocation.overwrite === "backup") {
|
|
1134
|
+
const backup = path + "_original";
|
|
1135
|
+
let existing = false;
|
|
1136
|
+
try {
|
|
1137
|
+
await publication.track(() => context.fs.lstat(backup, { signal: context.signal }));
|
|
1138
|
+
existing = true;
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
if (!(error instanceof FsError2) || error.code !== "ENOENT")
|
|
1141
|
+
throw error;
|
|
1142
|
+
}
|
|
1143
|
+
if (!existing) {
|
|
1144
|
+
resources.admit("output", bytes.length);
|
|
1145
|
+
await publication.publish(backup, bytes, null, false);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
await publication.publish(destination, edited, invocation.destination === void 0 ? original : null, invocation.destination === void 0 && inPlace);
|
|
1150
|
+
if (invocation.destination === void 0)
|
|
1151
|
+
updated++;
|
|
1152
|
+
else
|
|
1153
|
+
created++;
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (!png && ["XMP", "DOCX", "PPTX", "XLSX", "JPG", "JPEG", "TIF", "TIFF"].includes(extension))
|
|
1157
|
+
throw new Error(extension + " reader not yet supported");
|
|
1158
|
+
tags = png ? inspectPng(bytes, resources).tags : [];
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
context.signal.throwIfAborted();
|
|
1161
|
+
if (error instanceof ResourceLimitError)
|
|
1162
|
+
throw error;
|
|
1163
|
+
errors++;
|
|
1164
|
+
const message = error instanceof FsError2 && error.code === "ENOENT" ? "File not found" : error instanceof Error ? error.message : String(error);
|
|
1165
|
+
await output("Error: " + message + " - " + file + "\n", true);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
read++;
|
|
1169
|
+
const chosen = selected(tags, invocation.tags, invocation.json ? invocation.groupFamily === 4 : invocation.duplicates && !invocation.csv, resources);
|
|
1170
|
+
if (csv) {
|
|
1171
|
+
csv.add(file, chosen);
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
const present = /* @__PURE__ */ new Set();
|
|
1175
|
+
if (invocation.missing) {
|
|
1176
|
+
resources.admit("retained", chosen.length * 64);
|
|
1177
|
+
for (const tag of chosen) {
|
|
1178
|
+
resources.admit("work", tag.name.length + 1);
|
|
1179
|
+
present.add(tag.name);
|
|
1180
|
+
}
|
|
1181
|
+
resources.admit("work", invocation.tags.reduce((sum, name) => sum + name.length + 1, 0));
|
|
1182
|
+
}
|
|
1183
|
+
const scalarOptions = {
|
|
1184
|
+
quoteScalars: invocation.quoteScalars,
|
|
1185
|
+
signal: context.signal,
|
|
1186
|
+
maxDecodedBytes: resources.limits.maxDecodedBytes,
|
|
1187
|
+
maxOutputBytes: resources.limits.maxOutputBytes,
|
|
1188
|
+
maxWork: resources.limits.maxWork,
|
|
1189
|
+
maxRetainedBytes: resources.limits.maxRetainedBytes
|
|
1190
|
+
};
|
|
1191
|
+
if (invocation.json) {
|
|
1192
|
+
const entries = [' "SourceFile": ' + encodeJsonScalar(file, { ...scalarOptions, quoteScalars: true })];
|
|
1193
|
+
const tokens = /* @__PURE__ */ new Set(['"SourceFile"']);
|
|
1194
|
+
const primaryInstances = /* @__PURE__ */ new Map();
|
|
1195
|
+
if (invocation.groupFamily === 4)
|
|
1196
|
+
for (const tag of tags) {
|
|
1197
|
+
resources.admit("work", tag.name.length + 1);
|
|
1198
|
+
resources.admit("retained", tag.name.length * 2 + 64);
|
|
1199
|
+
primaryInstances.set(tag.name, Math.max(primaryInstances.get(tag.name) ?? 0, tag.instance));
|
|
1200
|
+
}
|
|
1201
|
+
for (const tag of chosen) {
|
|
1202
|
+
resources.admit("work", tag.name.length * 4);
|
|
1203
|
+
const name = invocation.groupFamily === 4 ? (tag.instance === primaryInstances.get(tag.name) ? "" : "Copy" + (tag.instance + 1)) + ":" + tag.name : tag.name;
|
|
1204
|
+
const token = encodeJsonScalar(name, { ...scalarOptions, quoteScalars: true });
|
|
1205
|
+
if (tokens.has(token))
|
|
1206
|
+
continue;
|
|
1207
|
+
tokens.add(token);
|
|
1208
|
+
resources.admit("work", tag.value.length * 4);
|
|
1209
|
+
entries.push(" " + token + ": " + encodeJsonScalar(tag.value, scalarOptions));
|
|
1210
|
+
}
|
|
1211
|
+
if (invocation.missing)
|
|
1212
|
+
for (const name of invocation.tags) {
|
|
1213
|
+
if (present.has(name))
|
|
1214
|
+
continue;
|
|
1215
|
+
resources.admit("work", name.length * 4 + 1);
|
|
1216
|
+
const token = encodeJsonScalar((invocation.groupFamily === 4 ? ":" : "") + name, { ...scalarOptions, quoteScalars: true });
|
|
1217
|
+
if (tokens.has(token))
|
|
1218
|
+
continue;
|
|
1219
|
+
tokens.add(token);
|
|
1220
|
+
entries.push(" " + token + ': "-"');
|
|
1221
|
+
}
|
|
1222
|
+
const record = "{\n" + entries.join(",\n") + "\n}";
|
|
1223
|
+
resources.admit("retained", record.length * 2);
|
|
1224
|
+
json.push(record);
|
|
1225
|
+
} else {
|
|
1226
|
+
for (const tag of chosen) {
|
|
1227
|
+
if (invocation.binary) {
|
|
1228
|
+
const converted = tag.chunkType !== "iTXt";
|
|
1229
|
+
const extent = converted ? tag.value.length * 3 : tag.raw.length;
|
|
1230
|
+
resources.admit("work", extent);
|
|
1231
|
+
resources.admit("output", extent);
|
|
1232
|
+
if (converted)
|
|
1233
|
+
resources.admit("retained", extent);
|
|
1234
|
+
await writeBytes(stdout.output, converted ? new TextEncoder().encode(tag.value) : tag.raw, context.signal);
|
|
1235
|
+
} else {
|
|
1236
|
+
resources.admit("work", (tag.name.length + tag.value.length) * 4);
|
|
1237
|
+
const value = printable(tag.value, scalarOptions);
|
|
1238
|
+
await output(invocation.style === "values" ? value + "\n" : (invocation.style === "compact" ? tag.name + ": " : tag.name.padEnd(32) + ": ") + value + "\n");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (invocation.missing && !invocation.binary)
|
|
1242
|
+
for (const name of invocation.tags) {
|
|
1243
|
+
if (!present.has(name))
|
|
1244
|
+
await output(invocation.style === "values" ? "-\n" : (invocation.style === "compact" ? name + ": " : name.padEnd(32) + ": ") + "-\n");
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (invocation.json && !invocation.assignments.length)
|
|
1249
|
+
await output("[" + json.join(",\n") + "]\n");
|
|
1250
|
+
if (csv) {
|
|
1251
|
+
await output(csv.render());
|
|
1252
|
+
if (invocation.files.length > 1)
|
|
1253
|
+
await output(String(read).padStart(5) + " image files read\n", true);
|
|
1254
|
+
}
|
|
1255
|
+
if (updated || unchanged)
|
|
1256
|
+
await output(String(updated).padStart(5) + " image files updated\n");
|
|
1257
|
+
if (created)
|
|
1258
|
+
await output(String(created).padStart(5) + " image files created\n");
|
|
1259
|
+
if (unchanged)
|
|
1260
|
+
await output(String(unchanged).padStart(5) + " image files unchanged\n");
|
|
1261
|
+
return { exitCode: errors ? 1 : 0 };
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
failed = true;
|
|
1264
|
+
failure = error;
|
|
1265
|
+
throw error;
|
|
1266
|
+
} finally {
|
|
1267
|
+
await cleanup();
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
var exiftoolCommand = createExiftoolCommand();
|
|
1273
|
+
function exiftoolCommands(options = {}) {
|
|
1274
|
+
const command = createExiftoolCommand(options);
|
|
1275
|
+
return { name: "exiftool", setup(host) {
|
|
1276
|
+
host.commands.register(command);
|
|
1277
|
+
} };
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// packages/safe-bash-command-exiftool/dist/sdk.js
|
|
1281
|
+
import { createCommandArguments } from "../safe-bash-contracts/command.js";
|
|
1282
|
+
function createExiftoolArguments(options, engine) {
|
|
1283
|
+
const resources = new Resources(engine);
|
|
1284
|
+
const supported = ["files", "tags", "assignments", "format", "style", "duplicates", "missing", "numeric", "quoteScalars", "groupFamily", "overwrite", "destination"];
|
|
1285
|
+
resources.admit("retained", 256);
|
|
1286
|
+
for (const key in options) {
|
|
1287
|
+
if (!Object.hasOwn(options, key))
|
|
1288
|
+
continue;
|
|
1289
|
+
resources.admit("work", key.length * 2 + supported.length);
|
|
1290
|
+
resources.admit("retained", key.length * 2 + 32);
|
|
1291
|
+
if (!supported.includes(key))
|
|
1292
|
+
throw new TypeError("Unsupported ExifTool SDK option: " + key);
|
|
1293
|
+
}
|
|
1294
|
+
const count = options.files.length + (options.tags?.length ?? 0) + (options.assignments?.length ?? 0);
|
|
1295
|
+
if (!Number.isSafeInteger(count) || count > 4096)
|
|
1296
|
+
throw new RangeError("ExifTool argument count exceeded");
|
|
1297
|
+
resources.admit("retained", 256);
|
|
1298
|
+
const args = [];
|
|
1299
|
+
const append = (...parts) => {
|
|
1300
|
+
if (args.length >= 4096)
|
|
1301
|
+
throw new RangeError("ExifTool argument count exceeded");
|
|
1302
|
+
let length = 0;
|
|
1303
|
+
for (const part of parts) {
|
|
1304
|
+
if (typeof part !== "string")
|
|
1305
|
+
throw new TypeError("ExifTool option values must be strings");
|
|
1306
|
+
length += part.length;
|
|
1307
|
+
}
|
|
1308
|
+
resources.admit("decoded", length * 2);
|
|
1309
|
+
resources.admit("retained", length * 8 + 128);
|
|
1310
|
+
resources.admit("work", length * 4 + 32);
|
|
1311
|
+
args.push(parts.join(""));
|
|
1312
|
+
};
|
|
1313
|
+
const tagName = (name, assignment) => {
|
|
1314
|
+
if (typeof name !== "string")
|
|
1315
|
+
throw new TypeError("ExifTool tag names must be strings");
|
|
1316
|
+
resources.admit("work", name.length * 4 + 32);
|
|
1317
|
+
resources.admit("retained", name.length * 4 + 64);
|
|
1318
|
+
const selector = !assignment && name.endsWith("#") ? name.slice(0, -1) : name;
|
|
1319
|
+
if (!exiftoolRegistry.tags.some((tag) => tag.toLowerCase() === selector.toLowerCase()) && selector !== "MissingTag" && !(assignment && selector === "all")) {
|
|
1320
|
+
throw new Error("ExifTool option/tag not yet supported: " + name);
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
if (options.format !== void 0 && options.format !== "text") {
|
|
1324
|
+
const flag = { json: "-j", csv: "-csv", binary: "-b" }[options.format];
|
|
1325
|
+
if (!flag)
|
|
1326
|
+
throw new TypeError("Unsupported ExifTool output format");
|
|
1327
|
+
append(flag);
|
|
1328
|
+
}
|
|
1329
|
+
if (options.style !== void 0) {
|
|
1330
|
+
const flag = { short: "-s", compact: "-S", values: "-s3" }[options.style];
|
|
1331
|
+
if (!flag)
|
|
1332
|
+
throw new TypeError("Unsupported ExifTool output style");
|
|
1333
|
+
append(flag);
|
|
1334
|
+
}
|
|
1335
|
+
if (options.duplicates)
|
|
1336
|
+
append("-a");
|
|
1337
|
+
if (options.missing)
|
|
1338
|
+
append("-f");
|
|
1339
|
+
if (options.numeric)
|
|
1340
|
+
append("-n");
|
|
1341
|
+
if (options.quoteScalars) {
|
|
1342
|
+
append("-api");
|
|
1343
|
+
append("StructFormat=JSONQ");
|
|
1344
|
+
}
|
|
1345
|
+
if (options.groupFamily !== void 0) {
|
|
1346
|
+
if (options.groupFamily !== 4)
|
|
1347
|
+
throw new TypeError("Unsupported ExifTool group family");
|
|
1348
|
+
append("-G4");
|
|
1349
|
+
}
|
|
1350
|
+
if (options.overwrite !== void 0 && options.overwrite !== "backup") {
|
|
1351
|
+
const flag = { replace: "-overwrite_original", "in-place": "-overwrite_original_in_place" }[options.overwrite];
|
|
1352
|
+
if (!flag)
|
|
1353
|
+
throw new TypeError("Unsupported ExifTool overwrite policy");
|
|
1354
|
+
append(flag);
|
|
1355
|
+
}
|
|
1356
|
+
if (options.destination !== void 0) {
|
|
1357
|
+
append("-o");
|
|
1358
|
+
append(options.destination);
|
|
1359
|
+
}
|
|
1360
|
+
for (const tag of options.tags ?? []) {
|
|
1361
|
+
tagName(tag, false);
|
|
1362
|
+
append("-", tag);
|
|
1363
|
+
}
|
|
1364
|
+
for (const assignment of options.assignments ?? []) {
|
|
1365
|
+
tagName(assignment.name, true);
|
|
1366
|
+
const suffix = { set: "=", remove: "-=", add: "+=" }[assignment.operation];
|
|
1367
|
+
if (!suffix)
|
|
1368
|
+
throw new TypeError("Unsupported ExifTool assignment operation");
|
|
1369
|
+
append("-", assignment.name, suffix, assignment.value);
|
|
1370
|
+
}
|
|
1371
|
+
append("--");
|
|
1372
|
+
for (const file of options.files)
|
|
1373
|
+
append(file);
|
|
1374
|
+
resources.admit("retained", args.length * 128);
|
|
1375
|
+
resources.admit("work", resources.usage.decoded * 4 + args.length * 64);
|
|
1376
|
+
parseArguments(args, resources.limits);
|
|
1377
|
+
return createCommandArguments(args, {
|
|
1378
|
+
assertOpen() {
|
|
1379
|
+
engine.signal.throwIfAborted();
|
|
1380
|
+
},
|
|
1381
|
+
reserve(bytes, slots) {
|
|
1382
|
+
resources.admit("retained", bytes + slots * 16);
|
|
1383
|
+
return { commit() {
|
|
1384
|
+
}, release() {
|
|
1385
|
+
} };
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
export {
|
|
1390
|
+
createExiftoolArguments,
|
|
1391
|
+
createExiftoolCommand,
|
|
1392
|
+
editPng,
|
|
1393
|
+
encodeJsonScalar,
|
|
1394
|
+
exiftoolCommand,
|
|
1395
|
+
exiftoolCommands,
|
|
1396
|
+
exiftoolLimits,
|
|
1397
|
+
exiftoolRegistry,
|
|
1398
|
+
inspectPng,
|
|
1399
|
+
printable
|
|
1400
|
+
};
|
|
1401
|
+
//# sourceMappingURL=index.js.map
|