@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,128 +0,0 @@
|
|
|
1
|
-
import { exiftoolRegistry } from "./registry.js";
|
|
2
|
-
export function parseArguments(args, limits) {
|
|
3
|
-
if (args.length > 4096)
|
|
4
|
-
throw new RangeError("ExifTool argument count exceeded");
|
|
5
|
-
let extent = 0;
|
|
6
|
-
for (const arg of args) {
|
|
7
|
-
extent += arg.length * 2;
|
|
8
|
-
if (extent > limits.maxDecodedBytes)
|
|
9
|
-
throw new RangeError("ExifTool argument decoded budget exceeded");
|
|
10
|
-
}
|
|
11
|
-
const result = { files: [], tags: [], assignments: [], json: false, csv: false, quoteScalars: false, duplicates: false,
|
|
12
|
-
binary: false, missing: false, style: "short", overwrite: "backup", destination: undefined, groupFamily: undefined };
|
|
13
|
-
let literal = false;
|
|
14
|
-
let valueConvSelector = false;
|
|
15
|
-
for (let index = 0; index < args.length; index++) {
|
|
16
|
-
const arg = args[index];
|
|
17
|
-
const option = arg.toLowerCase();
|
|
18
|
-
const value = () => { if (++index >= args.length)
|
|
19
|
-
throw new Error("Missing argument for " + arg); return args[index]; };
|
|
20
|
-
if (literal || !arg.startsWith("-") || arg === "-") {
|
|
21
|
-
result.files.push(arg);
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (arg === "--") {
|
|
25
|
-
literal = true;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (option === "-config") {
|
|
29
|
-
if (value() !== "")
|
|
30
|
-
throw new Error("User configuration modules are not supported");
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (option === "-j" || option === "-json") {
|
|
34
|
-
result.json = true;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (option === "-csv") {
|
|
38
|
-
result.csv = true;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (arg === "-G4") {
|
|
42
|
-
result.groupFamily = 4;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (option === "-api") {
|
|
46
|
-
if (value().toLowerCase() !== "structformat=jsonq")
|
|
47
|
-
throw new Error("API option not yet supported");
|
|
48
|
-
result.quoteScalars = true;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (option === "-a") {
|
|
52
|
-
result.duplicates = true;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (option === "-b") {
|
|
56
|
-
result.binary = true;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (option === "-f") {
|
|
60
|
-
result.missing = true;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (arg === "-S") {
|
|
64
|
-
result.style = "compact";
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (option === "-s" || option === "-s1") {
|
|
68
|
-
result.style = "short";
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (option === "-s3") {
|
|
72
|
-
result.style = "values";
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (option === "-n")
|
|
76
|
-
continue; // admitted PNG text tags have no PrintConv
|
|
77
|
-
if (option === "-overwrite_original") {
|
|
78
|
-
result.overwrite = "replace";
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (option === "-overwrite_original_in_place") {
|
|
82
|
-
result.overwrite = "in-place";
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (option === "-o") {
|
|
86
|
-
result.destination = value();
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (["-if", "-p", "-stay_open", "-@", "-common_args"].includes(option) || option.startsWith("-execute"))
|
|
90
|
-
throw new Error("ExifTool option not yet supported (no code evaluation or ambient polling): " + arg);
|
|
91
|
-
const equals = arg.indexOf("=");
|
|
92
|
-
let name = arg.slice(1, equals < 0 ? undefined : equals);
|
|
93
|
-
const operation = name.endsWith("+") ? "add" : name.endsWith("-") ? "remove" : "set";
|
|
94
|
-
if (operation !== "set")
|
|
95
|
-
name = name.slice(0, -1);
|
|
96
|
-
if (name.endsWith("#")) {
|
|
97
|
-
if (equals < 0)
|
|
98
|
-
valueConvSelector = true;
|
|
99
|
-
name = name.slice(0, -1);
|
|
100
|
-
}
|
|
101
|
-
const known = exiftoolRegistry.tags.find(tag => tag.toLowerCase() === name.toLowerCase());
|
|
102
|
-
if (equals >= 0)
|
|
103
|
-
result.assignments.push({ name: known ?? name, operation, value: arg.slice(equals + 1) });
|
|
104
|
-
else if (known || name === "MissingTag")
|
|
105
|
-
result.tags.push(known ?? name);
|
|
106
|
-
else
|
|
107
|
-
throw new Error("ExifTool option/tag not yet supported: " + arg);
|
|
108
|
-
}
|
|
109
|
-
if (!result.files.length)
|
|
110
|
-
throw new Error("No file specified");
|
|
111
|
-
if (result.files.filter(file => file === "-").length > 1)
|
|
112
|
-
throw new Error("Repeated stdin operands are not supported");
|
|
113
|
-
if (result.files.includes("-") && result.assignments.length)
|
|
114
|
-
throw new Error("Writing stdin metadata is not yet supported");
|
|
115
|
-
if (result.files.length > 64)
|
|
116
|
-
throw new RangeError("ExifTool file count exceeded");
|
|
117
|
-
if (result.destination !== undefined && (result.files.length !== 1 || !result.assignments.length))
|
|
118
|
-
throw new Error("-o requires a single file with tag assignments in this profile");
|
|
119
|
-
if (result.json && result.binary)
|
|
120
|
-
throw new Error("JSON binary policy not yet supported");
|
|
121
|
-
if (result.csv && (result.json || result.binary || result.assignments.length))
|
|
122
|
-
throw new Error("CSV combined output/import policy not yet supported");
|
|
123
|
-
if (result.csv && valueConvSelector)
|
|
124
|
-
throw new Error("CSV ValueConv-qualified headers not yet supported");
|
|
125
|
-
if (result.groupFamily !== undefined && (!result.json || result.assignments.length))
|
|
126
|
-
throw new Error("Group-family qualification outside JSON not yet supported");
|
|
127
|
-
return result;
|
|
128
|
-
}
|
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import { commandRuntimeIdentity, getCommandArguments } from "../safe-bash-contracts/command.js";
|
|
2
|
-
import { FsError } from "../safe-bash-contracts/errors.js";
|
|
3
|
-
import { readBytes, writeBytes } from "../safe-bash-contracts/io.js";
|
|
4
|
-
import { createOutputOperation } from "../safe-bash-contracts/output.js";
|
|
5
|
-
import { shellValueByteLength } from "../safe-bash-contracts/value.js";
|
|
6
|
-
import { parseArguments } from "./arguments.js";
|
|
7
|
-
import { inspectPng, editPng } from "./png.js";
|
|
8
|
-
import { Publication } from "./publication.js";
|
|
9
|
-
import { Resources, ResourceLimitError } from "./resources.js";
|
|
10
|
-
import { encodeJsonScalar, printable } from "./scalar.js";
|
|
11
|
-
import { exiftoolRegistry } from "./registry.js";
|
|
12
|
-
import { isNumericShift } from "./shifts.js";
|
|
13
|
-
import { CsvTable } from "./csv.js";
|
|
14
|
-
import { expandArgfiles } from "./argfiles.js";
|
|
15
|
-
import { virtualPath } from "./paths.js";
|
|
16
|
-
function selected(tags, names, duplicates, resources) {
|
|
17
|
-
resources.admit("work", tags.length + names.length);
|
|
18
|
-
resources.admit("retained", tags.length * 32);
|
|
19
|
-
const requested = names.length ? names : [...new Set(tags.map(tag => tag.name))];
|
|
20
|
-
const requestedExtent = requested.reduce((sum, name) => sum + name.length + 1, 0);
|
|
21
|
-
const tagExtent = tags.reduce((sum, tag) => sum + tag.name.length + 1, 0);
|
|
22
|
-
// Includes per-predicate lowercasing/comparison and every transient matching
|
|
23
|
-
// array, plus selected output references. Admit before the multiplicative loop.
|
|
24
|
-
resources.admit("work", tags.length * requestedExtent + tagExtent * requested.length * 2);
|
|
25
|
-
resources.admit("retained", requested.length * (128 + tags.length * 16));
|
|
26
|
-
const values = [];
|
|
27
|
-
for (const name of requested) {
|
|
28
|
-
const matching = tags.filter(tag => tag.name.toLowerCase() === name.toLowerCase());
|
|
29
|
-
if (duplicates)
|
|
30
|
-
values.push(...matching);
|
|
31
|
-
else if (matching.length)
|
|
32
|
-
values.push(matching[matching.length - 1]);
|
|
33
|
-
}
|
|
34
|
-
return values;
|
|
35
|
-
}
|
|
36
|
-
export function createExiftoolCommand(options = {}) {
|
|
37
|
-
const limits = Object.freeze({ ...options.limits });
|
|
38
|
-
return Object.freeze({
|
|
39
|
-
name: "exiftool", runtimeIdentity: commandRuntimeIdentity,
|
|
40
|
-
description: "Inspect and edit admitted PNG metadata through virtual files",
|
|
41
|
-
async execute(context) {
|
|
42
|
-
const publication = new Publication(context);
|
|
43
|
-
let stdout, stderr;
|
|
44
|
-
let failed = false, failure;
|
|
45
|
-
let closing;
|
|
46
|
-
const cleanup = () => {
|
|
47
|
-
closing ??= (async () => {
|
|
48
|
-
const results = await Promise.allSettled([publication.close(), stdout?.close(), stderr?.close()]);
|
|
49
|
-
const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
|
|
50
|
-
if (failures.length) {
|
|
51
|
-
if (failed)
|
|
52
|
-
failures.unshift(failure);
|
|
53
|
-
throw new AggregateError(failures, "ExifTool invocation and cleanup failed");
|
|
54
|
-
}
|
|
55
|
-
})();
|
|
56
|
-
return closing;
|
|
57
|
-
};
|
|
58
|
-
context.registerCleanup?.(cleanup);
|
|
59
|
-
try {
|
|
60
|
-
stdout = createOutputOperation(context, context.stdout);
|
|
61
|
-
stderr = createOutputOperation(context, context.stderr);
|
|
62
|
-
const resources = new Resources({ ...limits, signal: context.signal });
|
|
63
|
-
const output = async (text, error = false) => {
|
|
64
|
-
resources.admit("work", text.length * 2);
|
|
65
|
-
resources.admit("output", text.length * 3);
|
|
66
|
-
resources.admit("retained", text.length * 5);
|
|
67
|
-
await writeBytes(error ? stderr.output : stdout.output, new TextEncoder().encode(text), context.signal);
|
|
68
|
-
};
|
|
69
|
-
let invocation;
|
|
70
|
-
try {
|
|
71
|
-
const carrier = getCommandArguments(context);
|
|
72
|
-
for (let index = 0; index < carrier.values.length; index++) {
|
|
73
|
-
const extent = shellValueByteLength(carrier.values[index]);
|
|
74
|
-
resources.admit("decoded", context.args[index].length * 2);
|
|
75
|
-
resources.admit("retained", extent + 128);
|
|
76
|
-
resources.admit("work", extent * 4);
|
|
77
|
-
const bytes = carrier.bytes(index);
|
|
78
|
-
try {
|
|
79
|
-
if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes) !== context.args[index])
|
|
80
|
-
throw new Error("Argv Unicode mismatch");
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
throw new Error("Only qualified UTF-8 argv is currently supported");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
invocation = parseArguments(await expandArgfiles(context.args, context, publication, resources), resources.limits);
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
context.signal.throwIfAborted();
|
|
90
|
-
if (error instanceof ResourceLimitError)
|
|
91
|
-
throw error;
|
|
92
|
-
await output("Error: " + (error instanceof Error ? error.message : String(error)) + "\n", true);
|
|
93
|
-
return { exitCode: 1 };
|
|
94
|
-
}
|
|
95
|
-
const json = [];
|
|
96
|
-
const csv = invocation.csv ? new CsvTable(resources, invocation.missing, invocation.tags) : undefined;
|
|
97
|
-
const assignment = invocation.assignments.length === 1 ? invocation.assignments[0] : undefined;
|
|
98
|
-
if (assignment?.operation === "add" && Object.hasOwn(exiftoolRegistry.scalarShiftErrorGroups, assignment.name) && !isNumericShift(assignment.value)) {
|
|
99
|
-
await output("Warning: Shift value for " + exiftoolRegistry.scalarShiftErrorGroups[assignment.name] + ":" + assignment.name + " is not a number\nNothing to do.\n", true);
|
|
100
|
-
return { exitCode: 1 };
|
|
101
|
-
}
|
|
102
|
-
let errors = 0, updated = 0, created = 0, unchanged = 0, read = 0;
|
|
103
|
-
for (const file of invocation.files) {
|
|
104
|
-
let tags = [];
|
|
105
|
-
try {
|
|
106
|
-
const path = file === "-" ? "-" : virtualPath(context.cwd, file, resources);
|
|
107
|
-
const original = file === "-" ? undefined : await publication.track(() => context.fs.lstat(path, { signal: context.signal }));
|
|
108
|
-
if (original && original.type !== "file")
|
|
109
|
-
throw new Error("Only regular VFS files are admitted");
|
|
110
|
-
if (original) {
|
|
111
|
-
resources.admit("input", original.size);
|
|
112
|
-
resources.admit("retained", original.size * 32 + 256);
|
|
113
|
-
resources.admit("decoded", original.size * 2);
|
|
114
|
-
resources.admit("work", original.size * 20);
|
|
115
|
-
}
|
|
116
|
-
const bytes = await publication.track(async () => {
|
|
117
|
-
if (original && !context.fs.readStream) {
|
|
118
|
-
const result = await context.fs.readFile(path, { signal: context.signal, maxBytes: original.size });
|
|
119
|
-
context.signal.throwIfAborted();
|
|
120
|
-
if (result.length > original.size)
|
|
121
|
-
throw new RangeError("ExifTool input grew beyond admitted size");
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
const chunks = [];
|
|
125
|
-
let size = 0;
|
|
126
|
-
const source = original ? context.fs.readStream(path, { signal: context.signal }) : context.stdin;
|
|
127
|
-
for await (const chunk of readBytes(source, context.signal)) {
|
|
128
|
-
resources.admit("work", chunk.length + 64);
|
|
129
|
-
if (original && chunk.length > original.size - size)
|
|
130
|
-
throw new RangeError("ExifTool input grew beyond admitted size");
|
|
131
|
-
if (!original) {
|
|
132
|
-
resources.admit("input", chunk.length);
|
|
133
|
-
resources.admit("retained", chunk.length * 32 + 256);
|
|
134
|
-
resources.admit("decoded", chunk.length * 2);
|
|
135
|
-
resources.admit("work", chunk.length * 20);
|
|
136
|
-
}
|
|
137
|
-
resources.admit("retained", chunk.length + 128);
|
|
138
|
-
if (chunk.length)
|
|
139
|
-
chunks.push(new Uint8Array(chunk));
|
|
140
|
-
size += chunk.length;
|
|
141
|
-
}
|
|
142
|
-
resources.admit("retained", size);
|
|
143
|
-
const result = new Uint8Array(size);
|
|
144
|
-
let position = 0;
|
|
145
|
-
for (const chunk of chunks) {
|
|
146
|
-
result.set(chunk, position);
|
|
147
|
-
position += chunk.length;
|
|
148
|
-
}
|
|
149
|
-
return result;
|
|
150
|
-
});
|
|
151
|
-
if (!bytes.length)
|
|
152
|
-
throw new Error("File is empty");
|
|
153
|
-
const extension = file.slice(file.lastIndexOf(".") + 1).toUpperCase();
|
|
154
|
-
if (invocation.assignments.length && ["DOCX", "PPTX", "XLSX"].includes(extension))
|
|
155
|
-
throw new Error("Writing of " + extension + " files is not yet supported");
|
|
156
|
-
if (extension === "PDF")
|
|
157
|
-
throw new Error("PDF parser/writer not yet supported; metadata deletion retains historical revisions and never guarantees redaction");
|
|
158
|
-
const png = bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
159
|
-
if (invocation.assignments.length) {
|
|
160
|
-
if (!original)
|
|
161
|
-
throw new Error("Writing stdin metadata is not yet supported");
|
|
162
|
-
if (!png)
|
|
163
|
-
throw new Error("Format writer not yet supported");
|
|
164
|
-
const edited = editPng(bytes, invocation.assignments, resources);
|
|
165
|
-
resources.admit("retained", edited.length * 3);
|
|
166
|
-
resources.admit("work", bytes.length);
|
|
167
|
-
if (invocation.destination === undefined && !invocation.assignments.some(op => op.operation === "set" && op.value !== "") && edited.length === bytes.length && edited.every((byte, index) => byte === bytes[index])) {
|
|
168
|
-
unchanged++;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
resources.admit("output", edited.length);
|
|
172
|
-
const inPlace = invocation.overwrite === "in-place";
|
|
173
|
-
const destination = invocation.destination === undefined ? path : virtualPath(context.cwd, invocation.destination, resources);
|
|
174
|
-
if (invocation.destination === undefined) {
|
|
175
|
-
await publication.track(() => context.fs.access(path, 2, { signal: context.signal }));
|
|
176
|
-
if (!inPlace && original.nlink !== 1)
|
|
177
|
-
throw new Error("VFS hardlink replacement is not yet supported");
|
|
178
|
-
if (invocation.overwrite === "backup") {
|
|
179
|
-
const backup = path + "_original";
|
|
180
|
-
let existing = false;
|
|
181
|
-
try {
|
|
182
|
-
await publication.track(() => context.fs.lstat(backup, { signal: context.signal }));
|
|
183
|
-
existing = true;
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
if (!(error instanceof FsError) || error.code !== "ENOENT")
|
|
187
|
-
throw error;
|
|
188
|
-
}
|
|
189
|
-
if (!existing) {
|
|
190
|
-
resources.admit("output", bytes.length);
|
|
191
|
-
await publication.publish(backup, bytes, null, false);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
await publication.publish(destination, edited, invocation.destination === undefined ? original : null, invocation.destination === undefined && inPlace);
|
|
196
|
-
if (invocation.destination === undefined)
|
|
197
|
-
updated++;
|
|
198
|
-
else
|
|
199
|
-
created++;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
// Unrecognized nonempty formats match the validated no-selected-tag control.
|
|
203
|
-
if (!png && ["XMP", "DOCX", "PPTX", "XLSX", "JPG", "JPEG", "TIF", "TIFF"].includes(extension))
|
|
204
|
-
throw new Error(extension + " reader not yet supported");
|
|
205
|
-
tags = png ? inspectPng(bytes, resources).tags : [];
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
context.signal.throwIfAborted();
|
|
209
|
-
if (error instanceof ResourceLimitError)
|
|
210
|
-
throw error;
|
|
211
|
-
errors++;
|
|
212
|
-
const message = error instanceof FsError && error.code === "ENOENT" ? "File not found" : error instanceof Error ? error.message : String(error);
|
|
213
|
-
await output("Error: " + message + " - " + file + "\n", true);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
read++;
|
|
217
|
-
const chosen = selected(tags, invocation.tags, invocation.json ? invocation.groupFamily === 4 : invocation.duplicates && !invocation.csv, resources);
|
|
218
|
-
if (csv) {
|
|
219
|
-
csv.add(file, chosen);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
const present = new Set();
|
|
223
|
-
if (invocation.missing) {
|
|
224
|
-
resources.admit("retained", chosen.length * 64);
|
|
225
|
-
for (const tag of chosen) {
|
|
226
|
-
resources.admit("work", tag.name.length + 1);
|
|
227
|
-
present.add(tag.name);
|
|
228
|
-
}
|
|
229
|
-
resources.admit("work", invocation.tags.reduce((sum, name) => sum + name.length + 1, 0));
|
|
230
|
-
}
|
|
231
|
-
const scalarOptions = { quoteScalars: invocation.quoteScalars, signal: context.signal,
|
|
232
|
-
maxDecodedBytes: resources.limits.maxDecodedBytes, maxOutputBytes: resources.limits.maxOutputBytes, maxWork: resources.limits.maxWork, maxRetainedBytes: resources.limits.maxRetainedBytes };
|
|
233
|
-
if (invocation.json) {
|
|
234
|
-
const entries = [' "SourceFile": ' + encodeJsonScalar(file, { ...scalarOptions, quoteScalars: true })];
|
|
235
|
-
const tokens = new Set(['"SourceFile"']);
|
|
236
|
-
const primaryInstances = new Map();
|
|
237
|
-
if (invocation.groupFamily === 4)
|
|
238
|
-
for (const tag of tags) {
|
|
239
|
-
resources.admit("work", tag.name.length + 1);
|
|
240
|
-
resources.admit("retained", tag.name.length * 2 + 64);
|
|
241
|
-
primaryInstances.set(tag.name, Math.max(primaryInstances.get(tag.name) ?? 0, tag.instance));
|
|
242
|
-
}
|
|
243
|
-
for (const tag of chosen) {
|
|
244
|
-
resources.admit("work", tag.name.length * 4);
|
|
245
|
-
const name = invocation.groupFamily === 4 ? (tag.instance === primaryInstances.get(tag.name) ? "" : "Copy" + (tag.instance + 1)) + ":" + tag.name : tag.name;
|
|
246
|
-
const token = encodeJsonScalar(name, { ...scalarOptions, quoteScalars: true });
|
|
247
|
-
if (tokens.has(token))
|
|
248
|
-
continue;
|
|
249
|
-
tokens.add(token);
|
|
250
|
-
resources.admit("work", tag.value.length * 4);
|
|
251
|
-
entries.push(" " + token + ": " + encodeJsonScalar(tag.value, scalarOptions));
|
|
252
|
-
}
|
|
253
|
-
if (invocation.missing)
|
|
254
|
-
for (const name of invocation.tags) {
|
|
255
|
-
if (present.has(name))
|
|
256
|
-
continue;
|
|
257
|
-
resources.admit("work", name.length * 4 + 1);
|
|
258
|
-
const token = encodeJsonScalar((invocation.groupFamily === 4 ? ":" : "") + name, { ...scalarOptions, quoteScalars: true });
|
|
259
|
-
if (tokens.has(token))
|
|
260
|
-
continue;
|
|
261
|
-
tokens.add(token);
|
|
262
|
-
entries.push(" " + token + ': "-"');
|
|
263
|
-
}
|
|
264
|
-
const record = "{\n" + entries.join(",\n") + "\n}";
|
|
265
|
-
resources.admit("retained", record.length * 2);
|
|
266
|
-
json.push(record);
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
for (const tag of chosen) {
|
|
270
|
-
if (invocation.binary) {
|
|
271
|
-
// -b suppresses PrintConv, not ValueConv. tEXt's stored Latin-1
|
|
272
|
-
// and tIME's packed fields therefore differ from output bytes.
|
|
273
|
-
const converted = tag.chunkType !== "iTXt";
|
|
274
|
-
const extent = converted ? tag.value.length * 3 : tag.raw.length;
|
|
275
|
-
resources.admit("work", extent);
|
|
276
|
-
resources.admit("output", extent);
|
|
277
|
-
if (converted)
|
|
278
|
-
resources.admit("retained", extent);
|
|
279
|
-
await writeBytes(stdout.output, converted ? new TextEncoder().encode(tag.value) : tag.raw, context.signal);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
resources.admit("work", (tag.name.length + tag.value.length) * 4);
|
|
283
|
-
const value = printable(tag.value, scalarOptions);
|
|
284
|
-
await output(invocation.style === "values" ? value + "\n" : (invocation.style === "compact" ? tag.name + ": " : tag.name.padEnd(32) + ": ") + value + "\n");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (invocation.missing && !invocation.binary)
|
|
288
|
-
for (const name of invocation.tags) {
|
|
289
|
-
if (!present.has(name))
|
|
290
|
-
await output(invocation.style === "values" ? "-\n" : (invocation.style === "compact" ? name + ": " : name.padEnd(32) + ": ") + "-\n");
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (invocation.json && !invocation.assignments.length)
|
|
295
|
-
await output("[" + json.join(",\n") + "]\n");
|
|
296
|
-
if (csv) {
|
|
297
|
-
await output(csv.render());
|
|
298
|
-
if (invocation.files.length > 1)
|
|
299
|
-
await output(String(read).padStart(5) + " image files read\n", true);
|
|
300
|
-
}
|
|
301
|
-
if (updated || unchanged)
|
|
302
|
-
await output(String(updated).padStart(5) + " image files updated\n");
|
|
303
|
-
if (created)
|
|
304
|
-
await output(String(created).padStart(5) + " image files created\n");
|
|
305
|
-
if (unchanged)
|
|
306
|
-
await output(String(unchanged).padStart(5) + " image files unchanged\n");
|
|
307
|
-
return { exitCode: errors ? 1 : 0 };
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
failed = true;
|
|
311
|
-
failure = error;
|
|
312
|
-
throw error;
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
await cleanup();
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
export const exiftoolCommand = createExiftoolCommand();
|
|
321
|
-
export function exiftoolCommands(options = {}) {
|
|
322
|
-
const command = createExiftoolCommand(options);
|
|
323
|
-
return { name: "exiftool", setup(host) { host.commands.register(command); } };
|
|
324
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/** Invocation-owned table. CSV values bypass Printable and retain NUL bytes. */
|
|
2
|
-
export class CsvTable {
|
|
3
|
-
resources;
|
|
4
|
-
missing;
|
|
5
|
-
headers = new Map();
|
|
6
|
-
records = [];
|
|
7
|
-
sortHeaders = false;
|
|
8
|
-
constructor(resources, missing, names) {
|
|
9
|
-
this.resources = resources;
|
|
10
|
-
this.missing = missing;
|
|
11
|
-
for (const name of names) {
|
|
12
|
-
resources.admit("work", name.length * 2 + 1);
|
|
13
|
-
resources.admit("retained", name.length * 4 + 128);
|
|
14
|
-
const key = name.toLowerCase();
|
|
15
|
-
if (key !== "sourcefile" && !this.headers.has(key))
|
|
16
|
-
this.headers.set(key, { name, found: missing });
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
add(file, tags) {
|
|
20
|
-
this.resources.admit("work", file.length + tags.length);
|
|
21
|
-
this.resources.admit("retained", file.length * 2 + 256);
|
|
22
|
-
const fields = new Map();
|
|
23
|
-
for (const tag of tags) {
|
|
24
|
-
this.resources.admit("work", tag.name.length * 2 + tag.value.length + 1);
|
|
25
|
-
this.resources.admit("retained", (tag.name.length + tag.value.length) * 4 + 256);
|
|
26
|
-
const key = tag.name.toLowerCase();
|
|
27
|
-
if (key === "sourcefile")
|
|
28
|
-
continue;
|
|
29
|
-
if (!this.headers.has(key)) {
|
|
30
|
-
if (this.records.length)
|
|
31
|
-
this.sortHeaders = true;
|
|
32
|
-
this.headers.set(key, { name: tag.name, found: true });
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
this.headers.set(key, { name: tag.name, found: true });
|
|
36
|
-
}
|
|
37
|
-
fields.set(key, tag.value);
|
|
38
|
-
}
|
|
39
|
-
this.records.push({ file, fields });
|
|
40
|
-
}
|
|
41
|
-
render() {
|
|
42
|
-
const extent = [...this.headers.keys()].reduce((total, key) => total + key.length + 1, 0);
|
|
43
|
-
this.resources.admit("work", extent * (this.headers.size + 1) + this.records.length * (this.headers.size + 1));
|
|
44
|
-
this.resources.admit("retained", this.headers.size * 64 + this.records.length * 32);
|
|
45
|
-
const columns = [...this.headers].filter(([, header]) => header.found);
|
|
46
|
-
if (this.sortHeaders)
|
|
47
|
-
columns.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
48
|
-
const quote = (value) => {
|
|
49
|
-
this.resources.admit("work", value.length * 4 + 1);
|
|
50
|
-
this.resources.admit("retained", value.length * 8 + 16);
|
|
51
|
-
let quoted = false;
|
|
52
|
-
let text = "";
|
|
53
|
-
for (const character of value) {
|
|
54
|
-
if (character === '"') {
|
|
55
|
-
text += '""';
|
|
56
|
-
quoted = true;
|
|
57
|
-
}
|
|
58
|
-
else
|
|
59
|
-
text += character;
|
|
60
|
-
if (character === "," || character === "\n" || character === "\r")
|
|
61
|
-
quoted = true;
|
|
62
|
-
}
|
|
63
|
-
const whitespace = (character) => " \t\n\r\f\v\u0085\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000".includes(character);
|
|
64
|
-
if (value.length && (whitespace(value[0]) || whitespace(value[value.length - 1])))
|
|
65
|
-
quoted = true;
|
|
66
|
-
return quoted ? '"' + text + '"' : text;
|
|
67
|
-
};
|
|
68
|
-
const lines = [["SourceFile", ...columns.map(([, header]) => quote(header.name))].join(",")];
|
|
69
|
-
for (const record of this.records) {
|
|
70
|
-
this.resources.admit("retained", (columns.length + 1) * 16);
|
|
71
|
-
lines.push([quote(record.file), ...columns.map(([key]) => quote(record.fields.get(key) ?? (this.missing ? "-" : "")))].join(","));
|
|
72
|
-
}
|
|
73
|
-
const size = lines.reduce((total, line) => total + line.length + 1, 0);
|
|
74
|
-
this.resources.admit("work", size * 2);
|
|
75
|
-
this.resources.admit("retained", size * 4);
|
|
76
|
-
return lines.join("\n") + "\n";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** Literal VFS resolution with scratch admission before splitting components. */
|
|
2
|
-
export function virtualPath(cwd, input, resources) {
|
|
3
|
-
if (input.includes("\0") || input === "-")
|
|
4
|
-
throw new Error("Invalid or unsupported ExifTool file path");
|
|
5
|
-
const extent = input.length + (input.startsWith("/") ? 0 : cwd.length + 1);
|
|
6
|
-
// Includes separator-only and dot-only inputs with short normalized results.
|
|
7
|
-
resources.admit("decoded", extent * 2);
|
|
8
|
-
resources.admit("retained", extent * 32 + 256);
|
|
9
|
-
resources.admit("work", extent * 24 + 16);
|
|
10
|
-
const parts = [];
|
|
11
|
-
for (const part of (input.startsWith("/") ? input : cwd + "/" + input).split("/")) {
|
|
12
|
-
if (part === "..")
|
|
13
|
-
parts.pop();
|
|
14
|
-
else if (part && part !== ".")
|
|
15
|
-
parts.push(part);
|
|
16
|
-
}
|
|
17
|
-
return "/" + parts.join("/");
|
|
18
|
-
}
|