@poe-platform/safe-bash 0.1.711 → 0.1.713
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 +15 -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 +20 -3
- package/dist/safe-bash/playwright/private-target-transport.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,308 +0,0 @@
|
|
|
1
|
-
import { Resources } from "./resources.js";
|
|
2
|
-
import { exiftoolRegistry } from "./registry.js";
|
|
3
|
-
import { isNumericShift } from "./shifts.js";
|
|
4
|
-
const signature = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5
|
-
const bitDepths = Object.freeze({
|
|
6
|
-
0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16],
|
|
7
|
-
});
|
|
8
|
-
function checksum(bytes) {
|
|
9
|
-
let crc = 0xffffffff;
|
|
10
|
-
for (const byte of bytes) {
|
|
11
|
-
crc ^= byte;
|
|
12
|
-
for (let bit = 0; bit < 8; bit++)
|
|
13
|
-
crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
|
|
14
|
-
}
|
|
15
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
16
|
-
}
|
|
17
|
-
/** Qualified fixed-width timestamp policy; no host clock or timezone conversion. */
|
|
18
|
-
function encodePngTime(value, resources) {
|
|
19
|
-
resources.admit("work", value.length * 2 + 32);
|
|
20
|
-
const invalid = () => { throw new Error("PNG timestamp syntax/range not yet supported"); };
|
|
21
|
-
const decimal = (start, end) => {
|
|
22
|
-
let number = 0;
|
|
23
|
-
for (let index = start; index < end; index++) {
|
|
24
|
-
const digit = value.charCodeAt(index) - 48;
|
|
25
|
-
if (!(digit >= 0 && digit <= 9))
|
|
26
|
-
invalid();
|
|
27
|
-
number = number * 10 + digit;
|
|
28
|
-
}
|
|
29
|
-
return number;
|
|
30
|
-
};
|
|
31
|
-
if (value.length < 19 || ![":", "-"].includes(value[4]) || value[7] !== value[4] ||
|
|
32
|
-
![" ", "T"].includes(value[10]) || value[13] !== ":" || value[16] !== ":")
|
|
33
|
-
invalid();
|
|
34
|
-
const year = decimal(0, 4), month = decimal(5, 7), day = decimal(8, 10);
|
|
35
|
-
const hour = decimal(11, 13), minute = decimal(14, 16), second = decimal(17, 19);
|
|
36
|
-
if (!year || month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59)
|
|
37
|
-
invalid();
|
|
38
|
-
let index = 19;
|
|
39
|
-
if (value[index] === ".") {
|
|
40
|
-
const start = ++index;
|
|
41
|
-
while (index < value.length && value.charCodeAt(index) >= 48 && value.charCodeAt(index) <= 57)
|
|
42
|
-
index++;
|
|
43
|
-
if (start === index)
|
|
44
|
-
invalid();
|
|
45
|
-
}
|
|
46
|
-
if (value[index] === "Z")
|
|
47
|
-
index++;
|
|
48
|
-
else if (value[index] === "+" || value[index] === "-") {
|
|
49
|
-
if (value.length - index !== 6 || value[index + 3] !== ":" || decimal(index + 1, index + 3) > 23 || decimal(index + 4, index + 6) > 59)
|
|
50
|
-
invalid();
|
|
51
|
-
index += 6;
|
|
52
|
-
}
|
|
53
|
-
if (index !== value.length)
|
|
54
|
-
invalid();
|
|
55
|
-
// Native tIME ValueConvInv stores local fields, discarding fractions/offset.
|
|
56
|
-
resources.admit("retained", 26);
|
|
57
|
-
return new Uint8Array([year >>> 8, year & 255, month, day, hour, minute, second]);
|
|
58
|
-
}
|
|
59
|
-
/** Chunk constructor has its own explicit extent bound for SDK callers. */
|
|
60
|
-
export function pngChunk(type, data, maxBytes = 16_777_216) {
|
|
61
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0 || data.length + 12 > maxBytes)
|
|
62
|
-
throw new RangeError("PNG output budget exceeded");
|
|
63
|
-
if (type.length !== 4 || [...type].some(character => !((character >= "A" && character <= "Z") || (character >= "a" && character <= "z"))))
|
|
64
|
-
throw new TypeError("Invalid PNG chunk type");
|
|
65
|
-
const bytes = new Uint8Array(data.length + 12);
|
|
66
|
-
const view = new DataView(bytes.buffer);
|
|
67
|
-
view.setUint32(0, data.length);
|
|
68
|
-
for (let index = 0; index < 4; index++)
|
|
69
|
-
bytes[index + 4] = type.charCodeAt(index);
|
|
70
|
-
bytes.set(data, 8);
|
|
71
|
-
view.setUint32(bytes.length - 4, checksum(bytes.subarray(4, bytes.length - 4)));
|
|
72
|
-
return bytes;
|
|
73
|
-
}
|
|
74
|
-
function parse(input, resources) {
|
|
75
|
-
resources.admit("input", input.length);
|
|
76
|
-
resources.admit("retained", input.length);
|
|
77
|
-
resources.admit("work", input.length * 10);
|
|
78
|
-
const bytes = new Uint8Array(input);
|
|
79
|
-
if (bytes.length < 8 || signature.some((byte, index) => bytes[index] !== byte))
|
|
80
|
-
throw new Error("Not a PNG file");
|
|
81
|
-
const view = new DataView(bytes.buffer);
|
|
82
|
-
const chunks = [];
|
|
83
|
-
const tags = [];
|
|
84
|
-
const instances = new Map();
|
|
85
|
-
let offset = 8;
|
|
86
|
-
let ended = false;
|
|
87
|
-
let imageData = false;
|
|
88
|
-
while (offset < bytes.length) {
|
|
89
|
-
resources.signal.throwIfAborted();
|
|
90
|
-
resources.admit("retained", 128);
|
|
91
|
-
if (ended)
|
|
92
|
-
throw new Error("PNG trailing data not yet supported");
|
|
93
|
-
if (bytes.length - offset < 12)
|
|
94
|
-
throw new Error("PNG truncated chunk");
|
|
95
|
-
const size = view.getUint32(offset);
|
|
96
|
-
if (size > bytes.length - offset - 12)
|
|
97
|
-
throw new Error("PNG truncated chunk data");
|
|
98
|
-
const end = offset + size + 12;
|
|
99
|
-
const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
|
|
100
|
-
if (checksum(bytes.subarray(offset + 4, end - 4)) !== view.getUint32(end - 4))
|
|
101
|
-
throw new Error("PNG CRC mismatch");
|
|
102
|
-
const data = bytes.subarray(offset + 8, end - 4);
|
|
103
|
-
if (offset === 8 && type !== "IHDR")
|
|
104
|
-
throw new Error("PNG missing initial IHDR");
|
|
105
|
-
if (type === "IHDR") {
|
|
106
|
-
if (offset !== 8 || size !== 13)
|
|
107
|
-
throw new Error("Invalid PNG IHDR");
|
|
108
|
-
const header = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
109
|
-
if (!header.getUint32(0) || !header.getUint32(4) || header.getUint32(0) > 0x7fffffff || header.getUint32(4) > 0x7fffffff)
|
|
110
|
-
throw new Error("Invalid PNG dimensions");
|
|
111
|
-
if (!bitDepths[data[9]]?.includes(data[8]) || data[10] !== 0 || data[11] !== 0 || data[12] > 1)
|
|
112
|
-
throw new Error("Invalid PNG IHDR encoding fields");
|
|
113
|
-
}
|
|
114
|
-
if (type === "IDAT")
|
|
115
|
-
imageData = true;
|
|
116
|
-
if (type === "IEND" && !imageData)
|
|
117
|
-
throw new Error("PNG missing IDAT");
|
|
118
|
-
let tag;
|
|
119
|
-
let rawName, name = "", value = "";
|
|
120
|
-
let raw;
|
|
121
|
-
let start = 0;
|
|
122
|
-
if (type === "zTXt" || type === "eXIf")
|
|
123
|
-
throw new Error("PNG " + type + " metadata not yet supported");
|
|
124
|
-
if (type === "tEXt" || type === "iTXt") {
|
|
125
|
-
const separator = data.indexOf(0);
|
|
126
|
-
if (separator < 1 || separator > 79)
|
|
127
|
-
throw new Error("Invalid PNG text keyword");
|
|
128
|
-
resources.admit("decoded", data.length * 2);
|
|
129
|
-
resources.admit("work", data.length * 8);
|
|
130
|
-
const keyword = Array.from(data.subarray(0, separator), byte => String.fromCharCode(byte)).join("");
|
|
131
|
-
start = separator + 1;
|
|
132
|
-
if (type === "iTXt") {
|
|
133
|
-
if (start + 2 > data.length)
|
|
134
|
-
throw new Error("PNG truncated iTXt header");
|
|
135
|
-
if (data[start] !== 0)
|
|
136
|
-
throw new Error("PNG compressed iTXt not yet supported");
|
|
137
|
-
if (data[start + 1] !== 0)
|
|
138
|
-
throw new Error("Invalid PNG iTXt compression method");
|
|
139
|
-
const languageEnd = data.indexOf(0, start + 2);
|
|
140
|
-
const translatedEnd = languageEnd < 0 ? -1 : data.indexOf(0, languageEnd + 1);
|
|
141
|
-
if (translatedEnd < 0)
|
|
142
|
-
throw new Error("PNG truncated iTXt language fields");
|
|
143
|
-
// Language-qualified and structured metadata require namespace-aware controls.
|
|
144
|
-
if (languageEnd !== start + 2 || translatedEnd !== languageEnd + 1 || keyword === "XML:com.adobe.xmp")
|
|
145
|
-
throw new Error("PNG qualified/XMP iTXt not yet supported");
|
|
146
|
-
start = translatedEnd + 1;
|
|
147
|
-
}
|
|
148
|
-
raw = data.subarray(start);
|
|
149
|
-
// Includes transient per-byte strings/array slots in the Latin-1 decoder,
|
|
150
|
-
// owned raw bytes and interpreted UTF-16 strings, in addition to the input.
|
|
151
|
-
resources.admit("retained", raw.length + data.length * 40 + 128);
|
|
152
|
-
value = type === "tEXt" ? Array.from(raw, byte => String.fromCharCode(byte)).join("") : new TextDecoder("utf-8", { ignoreBOM: true }).decode(raw);
|
|
153
|
-
name = exiftoolRegistry.tags.find(name => name.toLowerCase() === keyword.toLowerCase()) ?? keyword;
|
|
154
|
-
rawName = keyword;
|
|
155
|
-
}
|
|
156
|
-
if (type === "tIME") {
|
|
157
|
-
if (size !== 7)
|
|
158
|
-
throw new Error("Invalid PNG tIME length");
|
|
159
|
-
resources.admit("decoded", 50);
|
|
160
|
-
resources.admit("retained", 256);
|
|
161
|
-
resources.admit("work", 128);
|
|
162
|
-
rawName = "tIME";
|
|
163
|
-
name = "ModifyDate";
|
|
164
|
-
raw = data;
|
|
165
|
-
const fields = [(data[0] << 8) | data[1], ...data.subarray(2)];
|
|
166
|
-
const parts = fields.map((field, index) => String(field).padStart(index === 0 ? 4 : 2, "0"));
|
|
167
|
-
value = parts.slice(0, 3).join(":") + " " + parts.slice(3).join(":");
|
|
168
|
-
}
|
|
169
|
-
if (rawName !== undefined && raw !== undefined) {
|
|
170
|
-
const instance = instances.get(name) ?? 0;
|
|
171
|
-
instances.set(name, instance + 1);
|
|
172
|
-
tag = Object.freeze({ name, rawName, chunkType: type, index: chunks.length, value, raw: new Uint8Array(raw), instance, offset: offset + 8 + start, group: "PNG" });
|
|
173
|
-
tags.push(tag);
|
|
174
|
-
}
|
|
175
|
-
chunks.push({ type, bytes: bytes.subarray(offset, end), ...(tag ? { tag } : {}) });
|
|
176
|
-
if (type === "IEND") {
|
|
177
|
-
if (size)
|
|
178
|
-
throw new Error("Invalid PNG IEND");
|
|
179
|
-
ended = true;
|
|
180
|
-
}
|
|
181
|
-
offset = end;
|
|
182
|
-
}
|
|
183
|
-
if (!ended)
|
|
184
|
-
throw new Error("PNG truncated: missing IEND");
|
|
185
|
-
return { chunks, tags };
|
|
186
|
-
}
|
|
187
|
-
export function inspectPng(input, options) {
|
|
188
|
-
const resources = options instanceof Resources ? options : new Resources(options);
|
|
189
|
-
return Object.freeze({ tags: Object.freeze(parse(input, resources).tags) });
|
|
190
|
-
}
|
|
191
|
-
/** Selected scalar writes only. Unknown metadata and every other chunk stay exact. */
|
|
192
|
-
export function editPng(input, assignments, options) {
|
|
193
|
-
const resources = options instanceof Resources ? options : new Resources(options);
|
|
194
|
-
const { chunks } = parse(input, resources);
|
|
195
|
-
resources.admit("retained", assignments.length * 128);
|
|
196
|
-
resources.admit("work", assignments.length * (chunks.length + 1));
|
|
197
|
-
const all = assignments.length === 1 && assignments[0].name.toLowerCase() === "all" && assignments[0].operation === "set" && assignments[0].value === "";
|
|
198
|
-
if (all && chunks.some(chunk => !["IHDR", "PLTE", "IDAT", "IEND", "tEXt", "iTXt", "tIME"].includes(chunk.type)))
|
|
199
|
-
throw new Error("PNG all deletion with unqualified chunks is not yet supported");
|
|
200
|
-
const operations = new Map();
|
|
201
|
-
for (const assignment of assignments) {
|
|
202
|
-
resources.admit("decoded", (assignment.name.length + assignment.value.length) * 2);
|
|
203
|
-
resources.admit("work", assignment.name.length * 5 + assignment.value.length * 3);
|
|
204
|
-
resources.admit("work", (chunks.length + assignments.length) * (assignment.value.length + assignment.name.length + 1));
|
|
205
|
-
resources.admit("retained", assignment.name.length * 10 + assignment.value.length * 6);
|
|
206
|
-
if (all)
|
|
207
|
-
continue;
|
|
208
|
-
const name = exiftoolRegistry.tags.find(name => name.toLowerCase() === assignment.name.toLowerCase());
|
|
209
|
-
if (!name)
|
|
210
|
-
throw new Error("Tag write not yet supported: " + assignment.name);
|
|
211
|
-
if (name === "ModifyDate" && assignment.operation !== "set")
|
|
212
|
-
throw new Error("Temporal shifts are not yet supported for ModifyDate");
|
|
213
|
-
if (assignment.operation === "add") {
|
|
214
|
-
const group = exiftoolRegistry.scalarShiftErrorGroups[name];
|
|
215
|
-
if (group && !isNumericShift(assignment.value))
|
|
216
|
-
throw new Error("Shift value for " + group + ":" + name + " is not a number");
|
|
217
|
-
throw new Error("Scalar numeric shifts are not yet supported for " + name);
|
|
218
|
-
}
|
|
219
|
-
if (assignment.operation !== "set" && assignment.operation !== "remove")
|
|
220
|
-
throw new Error("Invalid tag operation");
|
|
221
|
-
const prior = assignment.operation === "set" ? [] : operations.get(name) ?? [];
|
|
222
|
-
prior.push(assignment);
|
|
223
|
-
operations.set(name, prior);
|
|
224
|
-
}
|
|
225
|
-
const output = [signature];
|
|
226
|
-
let outputSize = 8;
|
|
227
|
-
resources.admit("output", 8);
|
|
228
|
-
const append = (bytes) => { resources.admit("output", bytes.length); outputSize += bytes.length; output.push(bytes); };
|
|
229
|
-
let inserted = false;
|
|
230
|
-
for (const chunk of chunks) {
|
|
231
|
-
if (all && chunk.tag)
|
|
232
|
-
continue;
|
|
233
|
-
const ops = chunk.tag && exiftoolRegistry.writeChunks[chunk.tag.name]?.includes(chunk.type) ? operations.get(chunk.tag.name) : undefined;
|
|
234
|
-
if (ops?.some(op => op.operation === "set" || op.value === chunk.tag.value))
|
|
235
|
-
continue;
|
|
236
|
-
if (chunk.type === "IDAT" && !inserted) {
|
|
237
|
-
inserted = true;
|
|
238
|
-
for (const [name, ops] of operations) {
|
|
239
|
-
const set = ops.find(op => op.operation === "set");
|
|
240
|
-
if (!set?.value || ops.some(op => op.operation === "remove" && op.value === set.value))
|
|
241
|
-
continue;
|
|
242
|
-
if (name === "ModifyDate") {
|
|
243
|
-
const data = encodePngTime(set.value, resources);
|
|
244
|
-
resources.admit("work", 128);
|
|
245
|
-
resources.admit("retained", 19);
|
|
246
|
-
resources.admit("output", 19);
|
|
247
|
-
outputSize += 19;
|
|
248
|
-
output.push(pngChunk("tIME", data, 19));
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
// Default native UTF-8 policy uses iTXt for every non-ASCII value,
|
|
252
|
-
// including characters that could also be represented in Latin-1.
|
|
253
|
-
resources.admit("work", set.value.length * 2);
|
|
254
|
-
let valueBytes = 0;
|
|
255
|
-
let international = false;
|
|
256
|
-
for (let index = 0; index < set.value.length; index++) {
|
|
257
|
-
const code = set.value.charCodeAt(index);
|
|
258
|
-
international ||= code > 127;
|
|
259
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
260
|
-
const next = set.value.charCodeAt(++index);
|
|
261
|
-
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
262
|
-
throw new Error("Unpaired surrogate in PNG text write");
|
|
263
|
-
valueBytes += 4;
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
if (code >= 0xdc00 && code <= 0xdfff)
|
|
267
|
-
throw new Error("Unpaired surrogate in PNG text write");
|
|
268
|
-
valueBytes += code < 128 ? 1 : code < 2048 ? 2 : 3;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
const prefix = name + (international ? "\0\0\0\0\0" : "\0");
|
|
272
|
-
const dataSize = prefix.length + valueBytes;
|
|
273
|
-
const size = dataSize + 12;
|
|
274
|
-
resources.admit("work", dataSize * 12);
|
|
275
|
-
resources.admit("retained", prefix.length * 2 + valueBytes + dataSize + size);
|
|
276
|
-
resources.admit("output", size);
|
|
277
|
-
const data = new Uint8Array(dataSize);
|
|
278
|
-
for (let index = 0; index < prefix.length; index++)
|
|
279
|
-
data[index] = prefix.charCodeAt(index);
|
|
280
|
-
data.set(new TextEncoder().encode(set.value), prefix.length);
|
|
281
|
-
outputSize += size;
|
|
282
|
-
output.push(pngChunk(international ? "iTXt" : "tEXt", data, size));
|
|
283
|
-
// Native PNG scalar writes replace each existing text instance rather
|
|
284
|
-
// than collapsing duplicates. A missing tag creates one new instance.
|
|
285
|
-
let instances = 0;
|
|
286
|
-
for (const existing of chunks) {
|
|
287
|
-
resources.signal.throwIfAborted();
|
|
288
|
-
resources.admit("work", 1);
|
|
289
|
-
if (existing.tag?.name === name && exiftoolRegistry.writeChunks[name]?.includes(existing.type))
|
|
290
|
-
instances++;
|
|
291
|
-
}
|
|
292
|
-
for (let instance = 1; instance < instances; instance++)
|
|
293
|
-
append(output[output.length - 1]);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
append(chunk.bytes);
|
|
297
|
-
}
|
|
298
|
-
resources.admit("retained", outputSize);
|
|
299
|
-
resources.admit("work", outputSize);
|
|
300
|
-
const result = new Uint8Array(outputSize);
|
|
301
|
-
let position = 0;
|
|
302
|
-
for (const bytes of output) {
|
|
303
|
-
result.set(bytes, position);
|
|
304
|
-
position += bytes.length;
|
|
305
|
-
}
|
|
306
|
-
resources.signal.throwIfAborted();
|
|
307
|
-
return result;
|
|
308
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { FsError } from "../safe-bash-contracts/errors.js";
|
|
2
|
-
export class Publication {
|
|
3
|
-
#context;
|
|
4
|
-
#pending = new Set();
|
|
5
|
-
#stages = [];
|
|
6
|
-
#closed = false;
|
|
7
|
-
#closing;
|
|
8
|
-
#serial = 0;
|
|
9
|
-
constructor(context) { this.#context = context; }
|
|
10
|
-
async track(operation) {
|
|
11
|
-
if (this.#closed)
|
|
12
|
-
throw new Error("ExifTool invocation closed");
|
|
13
|
-
this.#context.signal.throwIfAborted();
|
|
14
|
-
const pending = Promise.resolve().then(operation);
|
|
15
|
-
this.#pending.add(pending);
|
|
16
|
-
try {
|
|
17
|
-
return await pending;
|
|
18
|
-
}
|
|
19
|
-
finally {
|
|
20
|
-
this.#pending.delete(pending);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
close() {
|
|
24
|
-
if (this.#closing)
|
|
25
|
-
return this.#closing;
|
|
26
|
-
this.#closed = true;
|
|
27
|
-
this.#closing = (async () => {
|
|
28
|
-
await Promise.allSettled([...this.#pending]);
|
|
29
|
-
const failures = [];
|
|
30
|
-
// Cleanup must still run after invocation cancellation, on its own signal.
|
|
31
|
-
const signal = new AbortController().signal;
|
|
32
|
-
for (const staging of this.#stages.splice(0).reverse()) {
|
|
33
|
-
try {
|
|
34
|
-
await this.#context.fs.removeStagedFile(staging, { signal });
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
failures.push(error);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (failures.length)
|
|
41
|
-
throw new AggregateError(failures, "ExifTool staging cleanup failed");
|
|
42
|
-
})();
|
|
43
|
-
return this.#closing;
|
|
44
|
-
}
|
|
45
|
-
async publish(path, bytes, expected, inPlace) {
|
|
46
|
-
const { fs, signal } = this.#context;
|
|
47
|
-
const slash = path.lastIndexOf("/");
|
|
48
|
-
const directory = path.slice(0, slash) || "/";
|
|
49
|
-
const parent = await this.track(() => fs.stat(directory, { signal }));
|
|
50
|
-
const capabilities = fs.capabilitiesFor ? await this.track(() => fs.capabilitiesFor(path, { signal })) : fs.capabilities;
|
|
51
|
-
if (inPlace) {
|
|
52
|
-
if (!expected || !fs.writeFileConditional || !capabilities.atomicFileMutation)
|
|
53
|
-
throw new Error("VFS atomic in-place publication not supported");
|
|
54
|
-
await this.track(() => fs.writeFileConditional(path, bytes, { expected, parent, signal }));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (expected && expected.nlink !== 1)
|
|
58
|
-
throw new Error("VFS hardlink replacement is not yet supported");
|
|
59
|
-
if (!fs.createStagedFile || !fs.publishStagedFile || !fs.removeStagedFile || !capabilities.atomicFileStaging)
|
|
60
|
-
throw new Error("VFS atomic replacement publication not supported");
|
|
61
|
-
let stage;
|
|
62
|
-
for (let attempt = 0; attempt < 32; attempt++) {
|
|
63
|
-
try {
|
|
64
|
-
stage = await this.track(async () => {
|
|
65
|
-
const owned = await fs.createStagedFile(directory + (directory === "/" ? "" : "/") + ".exiftool-" + ++this.#serial, "output", { type: "file", data: bytes }, { parent, signal, mode: expected ? expected.mode & 0o7777 : 0o666 });
|
|
66
|
-
this.#stages.push(owned);
|
|
67
|
-
return owned;
|
|
68
|
-
});
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
if (!(error instanceof FsError) || error.code !== "EEXIST")
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (!stage)
|
|
77
|
-
throw new Error("ExifTool staging collision limit exceeded");
|
|
78
|
-
await this.track(() => fs.publishStagedFile(stage, path, { destination: expected, parent, signal }));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/** Independently admitted format/tag subset; this is not the full upstream catalog. */
|
|
2
|
-
const textChunks = Object.freeze(["tEXt", "iTXt"]);
|
|
3
|
-
const writeChunks = Object.freeze({
|
|
4
|
-
Title: textChunks, Author: textChunks, Description: textChunks,
|
|
5
|
-
Comment: textChunks, Copyright: textChunks, ModifyDate: Object.freeze(["tIME"]),
|
|
6
|
-
});
|
|
7
|
-
export const exiftoolRegistry = Object.freeze({
|
|
8
|
-
version: 2,
|
|
9
|
-
source: Object.freeze({ version: "13.59", commit: "2200871d9cef988051d2a99d67df3bda6cbb30a8", archiveSha256: "e1e2ad6c6fbf568afee5993ef8b2b91ab013d21698c9304e079e633ad82776f5" }),
|
|
10
|
-
formats: Object.freeze({ PNG: Object.freeze({ reader: "uncompressed-text-time", writer: "selected-text-time" }) }),
|
|
11
|
-
tags: Object.freeze(Object.keys(writeChunks)),
|
|
12
|
-
writeChunks,
|
|
13
|
-
scalarShiftErrorGroups: Object.freeze({ Title: "XMP-xmp" }),
|
|
14
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export const exiftoolLimits = Object.freeze({
|
|
2
|
-
maxInputBytes: 16_777_216, maxDecodedBytes: 8_388_608,
|
|
3
|
-
maxRetainedBytes: 33_554_432, maxOutputBytes: 16_777_216, maxWork: 268_435_456,
|
|
4
|
-
});
|
|
5
|
-
export class ResourceLimitError extends RangeError {
|
|
6
|
-
}
|
|
7
|
-
/** Cumulative admission for a single operation. No recursive parser is admitted. */
|
|
8
|
-
export class Resources {
|
|
9
|
-
limits;
|
|
10
|
-
signal;
|
|
11
|
-
usage = { input: 0, decoded: 0, retained: 0, output: 0, work: 0 };
|
|
12
|
-
constructor(options) {
|
|
13
|
-
this.signal = options.signal;
|
|
14
|
-
this.signal.throwIfAborted();
|
|
15
|
-
this.limits = Object.freeze({ ...exiftoolLimits, ...options });
|
|
16
|
-
for (const key of Object.keys(exiftoolLimits)) {
|
|
17
|
-
if (!Number.isSafeInteger(this.limits[key]) || this.limits[key] < 0)
|
|
18
|
-
throw new RangeError("Invalid resource limit: " + key);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
admit(kind, amount) {
|
|
22
|
-
this.signal.throwIfAborted();
|
|
23
|
-
const limits = { input: "maxInputBytes", decoded: "maxDecodedBytes", retained: "maxRetainedBytes", output: "maxOutputBytes", work: "maxWork" };
|
|
24
|
-
const total = this.usage[kind] + amount;
|
|
25
|
-
if (!Number.isSafeInteger(amount) || amount < 0 || !Number.isSafeInteger(total) || total > this.limits[limits[kind]])
|
|
26
|
-
throw new ResourceLimitError("ExifTool " + kind + " budget exceeded");
|
|
27
|
-
this.usage[kind] = total;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
const jsonEscapes = Object.freeze({ 9: "\\t", 10: "\\n", 13: "\\r", 34: '\\"', 92: "\\\\" });
|
|
2
|
-
function admit(value, options) {
|
|
3
|
-
options.signal?.throwIfAborted();
|
|
4
|
-
const limits = [options.maxDecodedBytes ?? 2_097_152, options.maxOutputBytes ?? 6_291_458, options.maxWork ?? 4_194_304, options.maxRetainedBytes ?? 14_680_068];
|
|
5
|
-
if (limits.some(limit => !Number.isSafeInteger(limit) || limit < 0))
|
|
6
|
-
throw new RangeError("Invalid scalar limit");
|
|
7
|
-
if (value.length * 2 > limits[0])
|
|
8
|
-
throw new RangeError("Scalar decoded bytes exceeded");
|
|
9
|
-
if (value.length * 4 > limits[2])
|
|
10
|
-
throw new RangeError("Scalar work exceeded");
|
|
11
|
-
if (4 + value.length * 14 > limits[3])
|
|
12
|
-
throw new RangeError("Scalar retained bytes exceeded");
|
|
13
|
-
}
|
|
14
|
-
function numericSpelling(value) {
|
|
15
|
-
// The pinned CLI end anchor also matches immediately before a final LF.
|
|
16
|
-
const length = value.endsWith("\n") ? value.length - 1 : value.length;
|
|
17
|
-
let index = value[0] === "-" ? 1 : 0;
|
|
18
|
-
const start = index;
|
|
19
|
-
const digit = () => index < length && value.charCodeAt(index) >= 48 && value.charCodeAt(index) <= 57;
|
|
20
|
-
while (digit())
|
|
21
|
-
index++;
|
|
22
|
-
const integers = index - start;
|
|
23
|
-
if (!integers || integers > 15 || (integers > 1 && value[start] === "0"))
|
|
24
|
-
return false;
|
|
25
|
-
if (value[index] === ".") {
|
|
26
|
-
const fraction = ++index;
|
|
27
|
-
while (digit())
|
|
28
|
-
index++;
|
|
29
|
-
if (index === fraction || index - fraction > 16)
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
if (value[index] === "e" || value[index] === "E") {
|
|
33
|
-
index++;
|
|
34
|
-
if (value[index] === "+" || value[index] === "-")
|
|
35
|
-
index++;
|
|
36
|
-
const exponent = index;
|
|
37
|
-
while (digit())
|
|
38
|
-
index++;
|
|
39
|
-
if (index === exponent || index - exponent > 3)
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
return index === length;
|
|
43
|
-
}
|
|
44
|
-
/** ExifTool 13.59 scalar JSON policy. Never converts lexical numbers to Number. */
|
|
45
|
-
export function encodeJsonScalar(value, options = {}) {
|
|
46
|
-
admit(value, options);
|
|
47
|
-
const limit = options.maxOutputBytes ?? 6_291_458;
|
|
48
|
-
if (!options.quoteScalars) {
|
|
49
|
-
const lower = value.toLowerCase();
|
|
50
|
-
const boolean = lower.endsWith("\n") ? lower.slice(0, -1) : lower;
|
|
51
|
-
const token = boolean === "true" || boolean === "false" ? lower : numericSpelling(value) ? value : undefined;
|
|
52
|
-
if (token !== undefined) {
|
|
53
|
-
if (token.length > limit)
|
|
54
|
-
throw new RangeError("Scalar output bytes exceeded");
|
|
55
|
-
return token;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// Reserve the maximum encoded extent before building strings. This deliberately
|
|
59
|
-
// conservative bound also accounts for UTF-8 and surrogate repair.
|
|
60
|
-
if (2 + value.length * 6 > limit)
|
|
61
|
-
throw new RangeError("Scalar output bytes exceeded");
|
|
62
|
-
let output = '"';
|
|
63
|
-
for (let index = 0; index < value.length; index++) {
|
|
64
|
-
const code = value.charCodeAt(index);
|
|
65
|
-
if (code === 0)
|
|
66
|
-
continue;
|
|
67
|
-
const escape = jsonEscapes[code];
|
|
68
|
-
if (escape)
|
|
69
|
-
output += escape;
|
|
70
|
-
else if (code < 32 || code === 127)
|
|
71
|
-
output += "\\u" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
72
|
-
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
73
|
-
const next = value.charCodeAt(index + 1);
|
|
74
|
-
if (next >= 0xdc00 && next <= 0xdfff)
|
|
75
|
-
output += value[index] + value[++index];
|
|
76
|
-
else
|
|
77
|
-
output += "\uFFFD";
|
|
78
|
-
}
|
|
79
|
-
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
80
|
-
output += "\uFFFD";
|
|
81
|
-
else
|
|
82
|
-
output += value[index];
|
|
83
|
-
}
|
|
84
|
-
options.signal?.throwIfAborted();
|
|
85
|
-
return output + '"';
|
|
86
|
-
}
|
|
87
|
-
/** Plain text display only; do not use to reconstruct stored metadata. */
|
|
88
|
-
export function printable(value, options = {}) {
|
|
89
|
-
admit(value, options);
|
|
90
|
-
if (value.length * 3 > (options.maxOutputBytes ?? 6_291_458))
|
|
91
|
-
throw new RangeError("Scalar output bytes exceeded");
|
|
92
|
-
let end = value.length;
|
|
93
|
-
// NUL disappears; other controls become dots and therefore survive trimming.
|
|
94
|
-
while (end && (value.charCodeAt(end - 1) === 0 || value[end - 1] === " "))
|
|
95
|
-
end--;
|
|
96
|
-
let output = "";
|
|
97
|
-
for (let index = 0; index < end; index++) {
|
|
98
|
-
const code = value.charCodeAt(index);
|
|
99
|
-
if (code)
|
|
100
|
-
output += code < 32 || code === 127 ? "." : value[index];
|
|
101
|
-
}
|
|
102
|
-
return output;
|
|
103
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { createCommandArguments } from "../safe-bash-contracts/command.js";
|
|
2
|
-
import { parseArguments } from "./arguments.js";
|
|
3
|
-
import { exiftoolRegistry } from "./registry.js";
|
|
4
|
-
import { Resources } from "./resources.js";
|
|
5
|
-
/** Build bounded, branded argv for direct CommandDefinition SDK invocation.
|
|
6
|
-
* Operands are always literal; option values never acquire flag authority.
|
|
7
|
-
* Argument construction is a separate bounded operation from command execution.
|
|
8
|
-
*/
|
|
9
|
-
export function createExiftoolArguments(options, engine) {
|
|
10
|
-
const resources = new Resources(engine);
|
|
11
|
-
const supported = ["files", "tags", "assignments", "format", "style", "duplicates", "missing", "numeric", "quoteScalars", "groupFamily", "overwrite", "destination"];
|
|
12
|
-
resources.admit("retained", 256);
|
|
13
|
-
for (const key in options) {
|
|
14
|
-
if (!Object.hasOwn(options, key))
|
|
15
|
-
continue;
|
|
16
|
-
resources.admit("work", key.length * 2 + supported.length);
|
|
17
|
-
resources.admit("retained", key.length * 2 + 32);
|
|
18
|
-
if (!supported.includes(key))
|
|
19
|
-
throw new TypeError("Unsupported ExifTool SDK option: " + key);
|
|
20
|
-
}
|
|
21
|
-
const count = options.files.length + (options.tags?.length ?? 0) + (options.assignments?.length ?? 0);
|
|
22
|
-
if (!Number.isSafeInteger(count) || count > 4096)
|
|
23
|
-
throw new RangeError("ExifTool argument count exceeded");
|
|
24
|
-
resources.admit("retained", 256);
|
|
25
|
-
const args = [];
|
|
26
|
-
const append = (...parts) => {
|
|
27
|
-
if (args.length >= 4096)
|
|
28
|
-
throw new RangeError("ExifTool argument count exceeded");
|
|
29
|
-
let length = 0;
|
|
30
|
-
for (const part of parts) {
|
|
31
|
-
if (typeof part !== "string")
|
|
32
|
-
throw new TypeError("ExifTool option values must be strings");
|
|
33
|
-
length += part.length;
|
|
34
|
-
}
|
|
35
|
-
resources.admit("decoded", length * 2);
|
|
36
|
-
resources.admit("retained", length * 8 + 128);
|
|
37
|
-
resources.admit("work", length * 4 + 32);
|
|
38
|
-
args.push(parts.join(""));
|
|
39
|
-
};
|
|
40
|
-
const tagName = (name, assignment) => {
|
|
41
|
-
if (typeof name !== "string")
|
|
42
|
-
throw new TypeError("ExifTool tag names must be strings");
|
|
43
|
-
resources.admit("work", name.length * 4 + 32);
|
|
44
|
-
resources.admit("retained", name.length * 4 + 64);
|
|
45
|
-
const selector = !assignment && name.endsWith("#") ? name.slice(0, -1) : name;
|
|
46
|
-
if (!exiftoolRegistry.tags.some(tag => tag.toLowerCase() === selector.toLowerCase()) && selector !== "MissingTag" && !(assignment && selector === "all")) {
|
|
47
|
-
throw new Error("ExifTool option/tag not yet supported: " + name);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
if (options.format !== undefined && options.format !== "text") {
|
|
51
|
-
const flag = { json: "-j", csv: "-csv", binary: "-b" }[options.format];
|
|
52
|
-
if (!flag)
|
|
53
|
-
throw new TypeError("Unsupported ExifTool output format");
|
|
54
|
-
append(flag);
|
|
55
|
-
}
|
|
56
|
-
if (options.style !== undefined) {
|
|
57
|
-
const flag = { short: "-s", compact: "-S", values: "-s3" }[options.style];
|
|
58
|
-
if (!flag)
|
|
59
|
-
throw new TypeError("Unsupported ExifTool output style");
|
|
60
|
-
append(flag);
|
|
61
|
-
}
|
|
62
|
-
if (options.duplicates)
|
|
63
|
-
append("-a");
|
|
64
|
-
if (options.missing)
|
|
65
|
-
append("-f");
|
|
66
|
-
if (options.numeric)
|
|
67
|
-
append("-n");
|
|
68
|
-
if (options.quoteScalars) {
|
|
69
|
-
append("-api");
|
|
70
|
-
append("StructFormat=JSONQ");
|
|
71
|
-
}
|
|
72
|
-
if (options.groupFamily !== undefined) {
|
|
73
|
-
if (options.groupFamily !== 4)
|
|
74
|
-
throw new TypeError("Unsupported ExifTool group family");
|
|
75
|
-
append("-G4");
|
|
76
|
-
}
|
|
77
|
-
if (options.overwrite !== undefined && options.overwrite !== "backup") {
|
|
78
|
-
const flag = { replace: "-overwrite_original", "in-place": "-overwrite_original_in_place" }[options.overwrite];
|
|
79
|
-
if (!flag)
|
|
80
|
-
throw new TypeError("Unsupported ExifTool overwrite policy");
|
|
81
|
-
append(flag);
|
|
82
|
-
}
|
|
83
|
-
if (options.destination !== undefined) {
|
|
84
|
-
append("-o");
|
|
85
|
-
append(options.destination);
|
|
86
|
-
}
|
|
87
|
-
for (const tag of options.tags ?? []) {
|
|
88
|
-
tagName(tag, false);
|
|
89
|
-
append("-", tag);
|
|
90
|
-
}
|
|
91
|
-
for (const assignment of options.assignments ?? []) {
|
|
92
|
-
tagName(assignment.name, true);
|
|
93
|
-
const suffix = { set: "=", remove: "-=", add: "+=" }[assignment.operation];
|
|
94
|
-
if (!suffix)
|
|
95
|
-
throw new TypeError("Unsupported ExifTool assignment operation");
|
|
96
|
-
append("-", assignment.name, suffix, assignment.value);
|
|
97
|
-
}
|
|
98
|
-
append("--");
|
|
99
|
-
for (const file of options.files)
|
|
100
|
-
append(file);
|
|
101
|
-
// Keep unsupported combination and stdin policy identical to CLI admission.
|
|
102
|
-
resources.admit("retained", args.length * 128);
|
|
103
|
-
resources.admit("work", resources.usage.decoded * 4 + args.length * 64);
|
|
104
|
-
parseArguments(args, resources.limits);
|
|
105
|
-
return createCommandArguments(args, {
|
|
106
|
-
assertOpen() { engine.signal.throwIfAborted(); },
|
|
107
|
-
reserve(bytes, slots) {
|
|
108
|
-
resources.admit("retained", bytes + slots * 16);
|
|
109
|
-
// Admission counts cumulative allocations, rather than live heap usage.
|
|
110
|
-
return { commit() { }, release() { } };
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|