@openclaw/fs-safe 0.8.1 → 0.8.2
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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -0
- package/dist/archive-gzip-tail.d.ts +18 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -0
- package/dist/archive-gzip-tail.js +74 -0
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +18 -60
- package/dist/archive-tar-extract.d.ts +11 -0
- package/dist/archive-tar-extract.d.ts.map +1 -0
- package/dist/archive-tar-extract.js +49 -0
- package/dist/archive-tar-stream.d.ts +18 -0
- package/dist/archive-tar-stream.d.ts.map +1 -0
- package/dist/archive-tar-stream.js +95 -0
- package/dist/archive-tar-wasm.d.ts +18 -0
- package/dist/archive-tar-wasm.d.ts.map +1 -0
- package/dist/archive-tar-wasm.js +102 -0
- package/dist/archive-tar.d.ts +0 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +0 -22
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +2 -109
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -2
- package/dist/directory-mode-owner.d.ts.map +1 -1
- package/dist/directory-mode-owner.js +3 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +2 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -0
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +3 -0
- package/docs/archive.md +66 -61
- package/docs/contributing.md +35 -1
- package/docs/install.md +1 -1
- package/docs/native-helper.md +5 -0
- package/docs/native.md +16 -10
- package/docs/secret-file.md +5 -0
- package/package.json +19 -16
- package/dist/archive-tar-admission.d.ts +0 -7
- package/dist/archive-tar-admission.d.ts.map +0 -1
- package/dist/archive-tar-admission.js +0 -43
- package/dist/archive-tar-gnu.d.ts +0 -2
- package/dist/archive-tar-gnu.d.ts.map +0 -1
- package/dist/archive-tar-gnu.js +0 -20
- package/dist/archive-tar-header.d.ts +0 -8
- package/dist/archive-tar-header.d.ts.map +0 -1
- package/dist/archive-tar-header.js +0 -47
- package/dist/archive-tar-meta.d.ts +0 -34
- package/dist/archive-tar-meta.d.ts.map +0 -1
- package/dist/archive-tar-meta.js +0 -277
- package/dist/archive-tar-pax.d.ts +0 -8
- package/dist/archive-tar-pax.d.ts.map +0 -1
- package/dist/archive-tar-pax.js +0 -100
- package/dist/archive-tar-runtime.d.ts +0 -49
- package/dist/archive-tar-runtime.d.ts.map +0 -1
- package/dist/archive-tar-runtime.js +0 -22
package/dist/archive.js
CHANGED
|
@@ -9,7 +9,6 @@ import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWit
|
|
|
9
9
|
import { resolveArchiveKind } from "./archive-kind.js";
|
|
10
10
|
import { prepareArchiveDestinationDir, preparePrivateArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
11
11
|
import { mergePlannedArchiveIntoDestination } from "./archive-merge.js";
|
|
12
|
-
import { createTarEntryPreflightChecker, readTarEntryInfo, } from "./archive-tar.js";
|
|
13
12
|
import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
14
13
|
import { isZipSymlinkEntry, zipEntryDeclaredSize, zipEntryMode, } from "./archive-zip-entry.js";
|
|
15
14
|
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
@@ -19,9 +18,7 @@ import { extractNativeArchive } from "./archive-native.js";
|
|
|
19
18
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
20
19
|
import { getNativeBinding } from "./native.js";
|
|
21
20
|
import { resolveArchiveEntryMode, resolveArchiveFilteredEntryPolicy, shouldExtractArchiveEntry, } from "./archive-policy.js";
|
|
22
|
-
import {
|
|
23
|
-
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
24
|
-
import { createTarAdmissionPlan } from "./archive-tar-admission.js";
|
|
21
|
+
import { extractWasmTar } from "./archive-tar-extract.js";
|
|
25
22
|
import { writeSiblingTempFile } from "./sibling-temp.js";
|
|
26
23
|
export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
27
24
|
export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
|
|
@@ -230,117 +227,13 @@ export async function extractArchive(params) {
|
|
|
230
227
|
}
|
|
231
228
|
if (kind === "tar") {
|
|
232
229
|
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
|
|
233
|
-
const tar = await importOptionalTar();
|
|
234
230
|
const stagedArchive = await stageArchiveFileForExtraction({
|
|
235
231
|
archivePath: params.archivePath,
|
|
236
232
|
limits,
|
|
237
233
|
deadline,
|
|
238
234
|
});
|
|
239
235
|
try {
|
|
240
|
-
|
|
241
|
-
await preflightTarMetadata({
|
|
242
|
-
archivePath: stagedArchive.path,
|
|
243
|
-
limits: tarLimits,
|
|
244
|
-
signal: deadline.signal,
|
|
245
|
-
onMember: (entry) => { manifest.push(entry); },
|
|
246
|
-
});
|
|
247
|
-
deadline.check();
|
|
248
|
-
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
|
|
249
|
-
await withStagedArchiveDestination({
|
|
250
|
-
destinationRealDir,
|
|
251
|
-
run: async (stagingDir) => {
|
|
252
|
-
deadline.check();
|
|
253
|
-
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
|
|
254
|
-
const checkTarEntrySafety = createTarEntryPreflightChecker({
|
|
255
|
-
rootDir: destinationRealDir,
|
|
256
|
-
stripComponents: params.stripComponents,
|
|
257
|
-
limits,
|
|
258
|
-
entryFilter: params.entryFilter,
|
|
259
|
-
onFiltered,
|
|
260
|
-
});
|
|
261
|
-
const admission = createTarAdmissionPlan(manifest, (entry) => {
|
|
262
|
-
deadline.check();
|
|
263
|
-
return checkTarEntrySafety(entry);
|
|
264
|
-
}, strip);
|
|
265
|
-
const acceptedEntries = [];
|
|
266
|
-
// Extract privately, then merge through the safe-open boundary.
|
|
267
|
-
const extractor = tar.x({
|
|
268
|
-
cwd: stagingDir,
|
|
269
|
-
// fs-safe owns stripping: node-tar counts the `.` and empty path
|
|
270
|
-
// components that stripArchivePath() drops, so the two disagree.
|
|
271
|
-
strip: 0,
|
|
272
|
-
gzip: params.tarGzip,
|
|
273
|
-
signal: deadline.signal,
|
|
274
|
-
preservePaths: false,
|
|
275
|
-
noChmod: true,
|
|
276
|
-
preserveOwner: false,
|
|
277
|
-
noMtime: true,
|
|
278
|
-
dmode: 0o700,
|
|
279
|
-
strict: true,
|
|
280
|
-
maxMetaEntrySize: tarLimits.maxMetaEntryBytes,
|
|
281
|
-
maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
|
|
282
|
-
filter(_entryPath, entry) {
|
|
283
|
-
try {
|
|
284
|
-
const info = readTarEntryInfo(entry);
|
|
285
|
-
const relPath = admission.consume(info);
|
|
286
|
-
if (!relPath) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
const kind = info.type === "Directory" || info.type === "GNUDumpDir" ? "directory" : "file";
|
|
290
|
-
// Keep archived modes before changing node-tar's creation mode.
|
|
291
|
-
entry.path = relPath;
|
|
292
|
-
acceptedEntries.push({
|
|
293
|
-
path: relPath,
|
|
294
|
-
kind,
|
|
295
|
-
mode: resolveArchiveEntryMode({
|
|
296
|
-
kind,
|
|
297
|
-
archivedMode: info.mode,
|
|
298
|
-
policy: params.entryModes,
|
|
299
|
-
}),
|
|
300
|
-
});
|
|
301
|
-
entry.mode = kind === "directory" ? 0o700 : 0o600;
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
catch (error) {
|
|
305
|
-
// Abort through the parser so pipeline tears down both the
|
|
306
|
-
// archive reader and unpacker instead of leaving a paused
|
|
307
|
-
// stream behind after a policy rejection.
|
|
308
|
-
this.abort(error instanceof Error ? error : new Error(String(error)));
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
onReadEntry(entry) {
|
|
313
|
-
try {
|
|
314
|
-
deadline.check();
|
|
315
|
-
}
|
|
316
|
-
catch (err) {
|
|
317
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
318
|
-
// EventEmitter binds `this` to tar.Unpack, exposing abort().
|
|
319
|
-
const emitter = this;
|
|
320
|
-
emitter.abort?.(error);
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
try {
|
|
325
|
-
await pipeline(fsSync.createReadStream(stagedArchive.path), extractor, {
|
|
326
|
-
signal: deadline.signal,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
|
|
331
|
-
}
|
|
332
|
-
admission.finish();
|
|
333
|
-
deadline.check();
|
|
334
|
-
await mergePlannedArchiveIntoDestination({
|
|
335
|
-
entries: acceptedEntries,
|
|
336
|
-
sourceDir: stagingDir,
|
|
337
|
-
destinationDir: params.destDir,
|
|
338
|
-
destinationRealDir,
|
|
339
|
-
deadline,
|
|
340
|
-
});
|
|
341
|
-
deadline.check();
|
|
342
|
-
},
|
|
343
|
-
});
|
|
236
|
+
await extractWasmTar({ archivePath: stagedArchive.path, options: { ...params, onFiltered }, limits, tarLimits, deadline });
|
|
344
237
|
}
|
|
345
238
|
finally {
|
|
346
239
|
await stagedArchive.cleanup();
|
package/dist/device-path.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type UnsafeDeviceReadPathOptions = {
|
|
|
8
8
|
platform?: NodeJS.Platform;
|
|
9
9
|
};
|
|
10
10
|
export declare const WINDOWS_RESERVED_DEVICE_NAMES: ReadonlySet<string>;
|
|
11
|
+
export declare function isWindowsReservedDeviceName(name: string): boolean;
|
|
11
12
|
export declare function matchUnsafeDeviceReadPath(filePath: string, options?: UnsafeDeviceReadPathOptions): UnsafeDeviceReadPathMatch | undefined;
|
|
12
13
|
export declare function isUnsafeDeviceReadPath(filePath: string, options?: UnsafeDeviceReadPathOptions): boolean;
|
|
13
14
|
export declare function assertNoUnsafeDeviceReadPath(filePath: string, options?: UnsafeDeviceReadPathOptions): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-path.d.ts","sourceRoot":"","sources":["../src/device-path.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,0BAA0B,GAClC,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAcF,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAgC5D,CAAC;
|
|
1
|
+
{"version":3,"file":"device-path.d.ts","sourceRoot":"","sources":["../src/device-path.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,0BAA0B,GAClC,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAcF,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAgC5D,CAAC;AAsEH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAcD,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,2BAAgC,GACxC,yBAAyB,GAAG,SAAS,CAWvC;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAET;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAON"}
|
package/dist/device-path.js
CHANGED
|
@@ -98,8 +98,11 @@ function normalizeWindowsDeviceBaseName(filePath) {
|
|
|
98
98
|
const normalized = trimTrailingWindowsSeparators(filePath.replace(/\//g, "\\"));
|
|
99
99
|
const lastSegment = normalized.split("\\").filter(Boolean).at(-1) ?? normalized;
|
|
100
100
|
const withoutStream = lastSegment.split(":")[0] ?? lastSegment;
|
|
101
|
-
const
|
|
102
|
-
return (
|
|
101
|
+
const stem = withoutStream.split(".")[0] ?? withoutStream;
|
|
102
|
+
return trimTrailingWindowsIgnoredChars(stem).toUpperCase();
|
|
103
|
+
}
|
|
104
|
+
export function isWindowsReservedDeviceName(name) {
|
|
105
|
+
return name.length > 0 && WINDOWS_RESERVED_DEVICE_NAMES.has(normalizeWindowsDeviceBaseName(name));
|
|
103
106
|
}
|
|
104
107
|
function matchWindowsDeviceReadPath(filePath) {
|
|
105
108
|
const normalized = filePath.replace(/\//g, "\\");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"directory-mode-owner.d.ts","sourceRoot":"","sources":["../src/directory-mode-owner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAIlD,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC,CAAC;AACF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,GAAG,WAAW,EAAE,MAAM,EAAE,KAAK,GAAG,WAAW,GAAG,IAAI,CAOrG;AAED,8FAA8F;AAC9F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"directory-mode-owner.d.ts","sourceRoot":"","sources":["../src/directory-mode-owner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAIlD,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC,CAAC;AACF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,GAAG,WAAW,EAAE,MAAM,EAAE,KAAK,GAAG,WAAW,GAAG,IAAI,CAOrG;AAED,8FAA8F;AAC9F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,kBAAkB,CAuDrB"}
|
|
@@ -26,6 +26,9 @@ export function ownDirectoryMode(params) {
|
|
|
26
26
|
check?.();
|
|
27
27
|
}),
|
|
28
28
|
apply: (mode, checks = {}) => enqueue(async () => {
|
|
29
|
+
// inspect() reports permission bits only; chmod ignores file-type bits,
|
|
30
|
+
// so tolerate raw stat modes (e.g. S_IFDIR | 0o755) by masking up front.
|
|
31
|
+
mode &= 0o7777;
|
|
29
32
|
checks.check?.();
|
|
30
33
|
const currentMode = await params.inspect();
|
|
31
34
|
checks.check?.();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE5G,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,EACrE,OAAO,EAAE,MAAM,GACd,IAAI,CAgBN;AA2BD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CA4B1C;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE5G,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,EACrE,OAAO,EAAE,MAAM,GACd,IAAI,CAgBN;AA2BD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CA4B1C;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAeP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CA0BzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CA0BxC"}
|
|
@@ -101,7 +101,8 @@ export function applyDirectoryModeSync(params) {
|
|
|
101
101
|
const fd = params.fsModule.openSync(params.dirPath, directoryOpenFlags());
|
|
102
102
|
try {
|
|
103
103
|
assertSameDirectory(expected, params.fsModule.fstatSync(fd), params.dirPath);
|
|
104
|
-
|
|
104
|
+
// chmod ignores file-type bits; mask so raw stat modes are tolerated.
|
|
105
|
+
params.fchmodSync?.(fd, params.mode & 0o7777);
|
|
105
106
|
}
|
|
106
107
|
finally {
|
|
107
108
|
params.fsModule.closeSync(fd);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAyER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAYF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAuBD;AAuDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
|
package/dist/secret-file.js
CHANGED
|
@@ -7,6 +7,7 @@ import { normalizeMaxBytes } from "./byte-budget.js";
|
|
|
7
7
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
8
|
import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
|
|
9
9
|
import { assertOwnedDirectory } from "./directory-mode-owner.js";
|
|
10
|
+
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
10
11
|
import { FsSafeError } from "./errors.js";
|
|
11
12
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
12
13
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
@@ -37,6 +38,7 @@ export function readSecretFileSync(filePath, label, options = {}) {
|
|
|
37
38
|
}
|
|
38
39
|
let previewStat;
|
|
39
40
|
try {
|
|
41
|
+
assertNoUnsafeDeviceReadPath(resolvedPath);
|
|
40
42
|
previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
41
43
|
}
|
|
42
44
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AASA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAgEjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
|
|
@@ -2,6 +2,7 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
4
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
5
|
+
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
5
6
|
import { FsSafeError } from "./errors.js";
|
|
6
7
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
7
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
@@ -26,6 +27,7 @@ export async function readSecretFile(filePath, label, options = {}) {
|
|
|
26
27
|
}
|
|
27
28
|
let previewStat;
|
|
28
29
|
try {
|
|
30
|
+
assertNoUnsafeDeviceReadPath(resolvedPath);
|
|
29
31
|
previewStat = await inspectFileIdentity(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
30
32
|
}
|
|
31
33
|
catch (error) {
|
|
@@ -36,6 +38,7 @@ export async function readSecretFile(filePath, label, options = {}) {
|
|
|
36
38
|
let raw;
|
|
37
39
|
try {
|
|
38
40
|
const realPath = await fs.realpath(resolvedPath);
|
|
41
|
+
assertNoUnsafeDeviceReadPath(realPath);
|
|
39
42
|
handle = await fs.open(realPath, resolveReadOpenFlags());
|
|
40
43
|
const openedHandle = handle;
|
|
41
44
|
const openedStat = await inspectFileIdentity(async () => {
|
package/docs/archive.md
CHANGED
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`@openclaw/fs-safe/archive` extracts ZIP and TAR archives behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets. When the native binding is available for the current platform, Rust streams ZIP, TAR, gzip, zstd, and bzip2 while TypeScript remains the sole policy owner; every accepted output is created fd-relative in a private staging root. Extraction then merges through the same safe-open boundary used by direct writes — a symlinked entry can't trick the merge into following an out-of-tree path.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
that
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
(`--no-optional`, `--omit=optional`, or equivalent). If an archive helper throws
|
|
12
|
-
that an optional archive dependency is not installed, install `jszip` and/or
|
|
13
|
-
`tar` explicitly in the consuming package.
|
|
5
|
+
TAR admission uses one Rust core compiled into both the native binding and a
|
|
6
|
+
bundled, import-free WebAssembly module. The guarded JavaScript fallback uses
|
|
7
|
+
that module for TAR/gzip and optional `jszip` for ZIP. TAR needs no optional
|
|
8
|
+
parser dependency, runtime download, install script, or consumer Rust toolchain.
|
|
9
|
+
Installs omitting optional dependencies can import every public subpath and use
|
|
10
|
+
TAR/gzip in `auto` or `off`; ZIP fallback still requires `jszip`.
|
|
14
11
|
|
|
15
12
|
```ts
|
|
16
13
|
import { extractArchive, resolveArchiveKind } from "@openclaw/fs-safe/archive";
|
|
@@ -68,12 +65,12 @@ directories; ZIP UNIX creator records with zero attributes are explicit zero,
|
|
|
68
65
|
while non-UNIX ZIP records use the absent-metadata defaults.
|
|
69
66
|
|
|
70
67
|
TAR mode fields containing only NUL/ASCII-space padding use absent defaults.
|
|
71
|
-
|
|
72
|
-
within
|
|
73
|
-
Malformed or unsupported mode
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
Both backends recognize GNU binary modes, including signed values,
|
|
69
|
+
within JavaScript's safe-integer range before masking permission bits.
|
|
70
|
+
Malformed or unsupported mode fields consistently fall back to zero, matching
|
|
71
|
+
the former native behavior. This replaces JavaScript's decoder-dependent octal
|
|
72
|
+
prefix parsing, defaulting, or rejection for malformed fields. Ordinary octal,
|
|
73
|
+
absent, explicit zero, and supported GNU binary fields retain their behavior.
|
|
77
74
|
|
|
78
75
|
Final modes remain separate from private working staging permissions: files
|
|
79
76
|
stay `0o600` and directories `0o700` until publication. Files receive their final
|
|
@@ -112,8 +109,8 @@ normalizing separators. For example, `./pkg/hello.txt` with
|
|
|
112
109
|
`stripComponents: 1` extracts to `hello.txt` on both backends. Entries with no
|
|
113
110
|
remaining components are skipped before the filter callback, but still count
|
|
114
111
|
toward `maxEntries` and undergo traversal validation. JavaScript TAR extraction
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
copies the admitted payload range to this accepted output path, so depth checks,
|
|
113
|
+
collision checks, writes, and mode application agree.
|
|
117
114
|
|
|
118
115
|
An `entryFilter` sees the validated **canonical effective archive path before
|
|
119
116
|
stripping**, entry kind, and declared size. On every JavaScript and native
|
|
@@ -127,6 +124,12 @@ Unicode Path names use the same canonicalization.
|
|
|
127
124
|
Raw paths undergo traversal, absolute/drive-path, and NUL validation **before**
|
|
128
125
|
canonicalization; normalization cannot turn an unsafe path into an accepted
|
|
129
126
|
one. Stripping and output collision checks use this same canonical identity.
|
|
127
|
+
On Windows, archive admission also rejects reserved device segments such as
|
|
128
|
+
`NUL`, `CON.txt`, and `nul .txt` with `ArchiveSecurityError("entry-path")`,
|
|
129
|
+
before extraction or bounded member reads. Ignored trailing spaces in the stem
|
|
130
|
+
before an extension do not bypass this check. Ordinary members with these names
|
|
131
|
+
remain valid on POSIX; this is a host-specific device guard, not a portable-name
|
|
132
|
+
restriction.
|
|
130
133
|
Filters that compare exact strings should use canonical pre-strip paths,
|
|
131
134
|
including directory names without a trailing `/`.
|
|
132
135
|
Returning `"skip"` rejects the whole archive unless `onFiltered` is
|
|
@@ -162,11 +165,10 @@ If skipping was not explicitly part of the restore contract, omit
|
|
|
162
165
|
`onFiltered`; the first `"skip"` then rejects the complete archive with
|
|
163
166
|
`ArchiveSecurityError("entry-filtered")`.
|
|
164
167
|
|
|
165
|
-
Both TAR
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
instead of leaving a paused parser to drain indefinitely.
|
|
168
|
+
Both TAR routes finish bounded admission before TypeScript policy evaluation,
|
|
169
|
+
so a rejected plan never starts extraction. The JavaScript path owns its input,
|
|
170
|
+
decoder, and WASM parser streams, joining their teardown on validation, write,
|
|
171
|
+
or timeout failure.
|
|
170
172
|
|
|
171
173
|
TAR character devices, block devices, and FIFOs are presented to the filter as
|
|
172
174
|
`kind: "other"`. Accepted entries of these types reject with
|
|
@@ -183,8 +185,7 @@ and output collision checks in physical order. Each remaining record reaches
|
|
|
183
185
|
`entryFilter` once with its canonical pre-strip path, `kind: "other"`, and
|
|
184
186
|
declared effective size. A filter skip rejects with `"entry-filtered"` unless
|
|
185
187
|
`onFiltered: "skip-entry"` is explicit. Accepted unsupported records are safely
|
|
186
|
-
omitted and do not consume output payload budgets.
|
|
187
|
-
underlying TAR parser suppresses the record. GNU long names describe one such
|
|
188
|
+
omitted and do not consume output payload budgets. The shared core admits these records explicitly. GNU long names describe one such
|
|
188
189
|
record and are then cleared; local PAX on unsupported types and GNU sparse
|
|
189
190
|
`S` records retain their existing fail-closed format policy.
|
|
190
191
|
|
|
@@ -219,7 +220,7 @@ type ArchiveExtractLimits = {
|
|
|
219
220
|
};
|
|
220
221
|
```
|
|
221
222
|
|
|
222
|
-
Defaults exist for each (`DEFAULT_MAX_ARCHIVE_BYTES_ZIP`, `DEFAULT_MAX_ENTRIES`, `DEFAULT_MAX_EXTRACTED_BYTES`, `DEFAULT_MAX_ENTRY_BYTES`, `DEFAULT_MAX_META_ENTRY_BYTES`, `DEFAULT_MAX_ENTRY_PATH_COMPONENTS`). An explicit zero remains zero rather than selecting the default. `maxEntries` counts every archive entry, including entries removed by `stripComponents` or an explicit filter. The path-component default is 256. It is evaluated after `stripComponents` and before TypeScript accepts an entry for either JavaScript or native extraction, so rejected entries cannot cause implicit parent-directory creation. The 1 MiB metadata default
|
|
223
|
+
Defaults exist for each (`DEFAULT_MAX_ARCHIVE_BYTES_ZIP`, `DEFAULT_MAX_ENTRIES`, `DEFAULT_MAX_EXTRACTED_BYTES`, `DEFAULT_MAX_ENTRY_BYTES`, `DEFAULT_MAX_META_ENTRY_BYTES`, `DEFAULT_MAX_ENTRY_PATH_COMPONENTS`). An explicit zero remains zero rather than selecting the default. `maxEntries` counts every archive entry, including entries removed by `stripComponents` or an explicit filter. The path-component default is 256. It is evaluated after `stripComponents` and before TypeScript accepts an entry for either JavaScript or native extraction, so rejected entries cannot cause implicit parent-directory creation. The same resolved 1 MiB metadata default applies to the native and WASM core.
|
|
223
224
|
|
|
224
225
|
A limit violation throws `ArchiveLimitError`. Its constant and string code are:
|
|
225
226
|
|
|
@@ -262,17 +263,17 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
|
|
|
262
263
|
- **TOCTOU during merge:** extraction first writes to a private temp dir, then merges into `destDir` using the same boundary checks as `root().write()`. Destination symlink swaps are checked with the selected platform mechanism; non-Linux routes retain the best-effort race window documented in the [security model](security-model.md#containment-guarantees-by-platform).
|
|
263
264
|
- **Zip bombs:** `maxExtractedBytes` and `maxEntryBytes` apply to *post-decompression* bytes, so highly-compressed payloads hit the cap before they exhaust disk.
|
|
264
265
|
- **Corrupt ZIP payloads:** streamed output must match both the central-directory CRC and declared uncompressed size before it can leave private staging.
|
|
265
|
-
- **
|
|
266
|
+
- **Gzip container integrity:** every concatenated gzip member must have a complete valid header, body, CRC32, and ISIZE trailer. A completed member may be followed by all-zero compressed-container padding (including system-tar stdout padding), bounded by the original archive-byte limit. The padding must remain zero through physical EOF; nonzero bytes or another member after padding reject. Truncation and corruption reject before publication or selected bytes return on both backends. Compressed padding is separate from decoded TAR EOF and does not bypass its checks.
|
|
266
267
|
- **Slow-loris archives:** `timeoutMs` is a hard wall-clock budget for non-mutating work. Extraction is aborted on overrun; if a destination mutation is already in flight, that mutation and rollback are joined before rejection so archive-controlled publication cannot continue afterward.
|
|
267
|
-
- **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before
|
|
268
|
+
- **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before buffering their bodies. It understands octal and base-256 fixed sizes and validates bounded local PAX bodies before using their size overrides for member framing. Original archive bytes remain unchanged.
|
|
268
269
|
|
|
269
270
|
### Raw TAR framing
|
|
270
271
|
|
|
271
272
|
Extraction and bounded reads admit the complete decoded TAR stream through the
|
|
272
|
-
|
|
273
|
+
shared Rust core. This applies to plain TAR,
|
|
273
274
|
gzip, and native-supported zstd/bzip2, without changing native-mode availability
|
|
274
|
-
or fallback policy. The
|
|
275
|
-
framing rules
|
|
275
|
+
or fallback policy. The native and WASM builds enforce the same
|
|
276
|
+
framing rules:
|
|
276
277
|
|
|
277
278
|
- Every nonzero header must have a valid unsigned octal checksum, delimited
|
|
278
279
|
within its field. Checksum validation precedes metadata allocation and member
|
|
@@ -301,22 +302,28 @@ Missing linknames on links and nonempty linknames on non-links still use the
|
|
|
301
302
|
format error. PAX `x` and GNU long-name/long-link `L`/`K` payloads retain their
|
|
302
303
|
existing support and metadata limits; the zero-body rule is not applied to all
|
|
303
304
|
non-regular types.
|
|
304
|
-
PAX effective sizes
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
field's NUL terminator reject
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
305
|
+
PAX effective sizes determine regular-member framing. Admission preserves input
|
|
306
|
+
bytes and emits an ordered manifest with exact effective names, types, modes,
|
|
307
|
+
sizes, and decoded payload offsets. TypeScript owns filtering, stripping,
|
|
308
|
+
collisions, permissions, and accepted-output limits. Executors replay admitted
|
|
309
|
+
ranges from the immutable staged input; no second TAR parser interprets PAX,
|
|
310
|
+
GNU names, or payload lengths. Native writes remain descriptor-relative;
|
|
311
|
+
JavaScript writes use the shared guarded private staging and pinned-write helpers.
|
|
312
|
+
|
|
313
|
+
Original member names and USTAR prefixes are validated even when overridden.
|
|
314
|
+
Non-padding bytes after a fixed path field's NUL terminator reject. The core
|
|
315
|
+
enforces the 255-byte component ceiling under NFC and NFD, including Hangul
|
|
316
|
+
expansion. Every replay drains and validates physical EOF before publication or
|
|
317
|
+
returning selected bytes. Unrequested, filtered, and stripped members cannot
|
|
318
|
+
bypass validation. Decompression remains streaming; no complete decoded archive
|
|
319
|
+
is retained in memory or written to a decoded spool.
|
|
320
|
+
|
|
321
|
+
The WASM transport has a fixed 64 KiB input buffer, one pending member event,
|
|
322
|
+
and a 256 MiB maximum linear memory per isolated parser instance. Metadata is
|
|
323
|
+
bounded before allocation; allocation failure rejects. Stream backpressure
|
|
324
|
+
bounds queued chunks, and completion/error destroys the instance's parser
|
|
325
|
+
state. The manifest retains the existing charged budget below; linear memory
|
|
326
|
+
is an additional execution resource bound, not a new public limit option.
|
|
320
327
|
|
|
321
328
|
The raw meter enforces `maxEntries` before consuming each logical member's body,
|
|
322
329
|
including members later skipped by filtering or stripping. PAX/GNU metadata
|
|
@@ -343,9 +350,7 @@ archive overhead with safe addition. Ordinary limits, including
|
|
|
343
350
|
zero and the existing defaulting/rounding rules, retain their behavior.
|
|
344
351
|
There is no new public option. This is an absolute decoded admission
|
|
345
352
|
cap, not a decompression-ratio policy; bounded stream/codec read-ahead remains.
|
|
346
|
-
|
|
347
|
-
independent ratio threshold so it cannot reject data that the native backend
|
|
348
|
-
accepts within the same absolute limits.
|
|
353
|
+
There is no independent TAR parser decompression-ratio threshold.
|
|
349
354
|
|
|
350
355
|
### Bounded local PAX support
|
|
351
356
|
|
|
@@ -359,13 +364,14 @@ permits link creation. Effective sizes drive framing, filters, and the existing
|
|
|
359
364
|
budgets; `maxEntries` still counts members, not their metadata headers.
|
|
360
365
|
|
|
361
366
|
Records must have exact byte lengths, ASCII keys, a final newline, and no
|
|
362
|
-
duplicate keys
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
duplicate keys or unconsumed bytes. `path` and `linkpath` must be nonempty strict
|
|
368
|
+
UTF-8 without NUL. Unicode, a leading BOM, numeric-looking names, and embedded
|
|
369
|
+
newlines preserve their exact spelling; newlines inside a byte-counted value
|
|
370
|
+
are data. Windows filesystem filename restrictions still apply during creation.
|
|
371
|
+
Ownership names retain the existing nonempty printable-ASCII contract. Raw name,
|
|
372
|
+
USTAR prefix, and link fields still require strict UTF-8 and NUL padding even
|
|
373
|
+
when metadata overrides them. Raw link targets must be present only on links.
|
|
374
|
+
`size`, `uid`, and `gid`
|
|
369
375
|
must be canonical unsigned decimal safe integers (zero is valid; signs, leading
|
|
370
376
|
zeros, fractions, and exponents are not). Padded member sizes must also fit the
|
|
371
377
|
safe integer range. Raw and effective directory/link sizes must both be zero;
|
|
@@ -378,8 +384,7 @@ with optional fractional digits, within JavaScript's Date range), `uid`, `gid`,
|
|
|
378
384
|
destination. `LIBARCHIVE.xattr.*` and `SCHILY.xattr.*` with nonempty ASCII
|
|
379
385
|
alphanumeric/dot/underscore/hyphen suffixes are also accepted as inert metadata,
|
|
380
386
|
never restored as extended attributes. Their values are byte-counted and may
|
|
381
|
-
contain NUL
|
|
382
|
-
newlines are rejected because they can disrupt downstream record parsing.
|
|
387
|
+
contain NUL, non-UTF8 bytes, or newlines, including macOS provenance metadata.
|
|
383
388
|
|
|
384
389
|
Global `g`, old `X`, old GNU `N`, empty/dangling/repeated local headers, mixed
|
|
385
390
|
PAX/GNU extension chains, unknown keys, charset declarations, ACL extensions,
|
|
@@ -393,11 +398,11 @@ chains without introducing a new limit or changing defaults.
|
|
|
393
398
|
|
|
394
399
|
### Bounded GNU long names and links
|
|
395
400
|
|
|
396
|
-
|
|
397
|
-
`maxMetaEntryBytes
|
|
401
|
+
The shared core buffers GNU long-name `L` and long-link `K` bodies within
|
|
402
|
+
`maxMetaEntryBytes`. A body must contain a nonempty
|
|
398
403
|
UTF-8 name, with either no NUL or exactly one terminal NUL. Embedded NULs,
|
|
399
404
|
additional terminal NULs, bytes after a NUL, and invalid UTF-8 reject with
|
|
400
|
-
`ArchiveFormatError("archive-header-invalid")`. The
|
|
405
|
+
`ArchiveFormatError("archive-header-invalid")`. The core preserves original
|
|
401
406
|
archive bytes, including the optional terminator and block padding.
|
|
402
407
|
|
|
403
408
|
One logical member may have at most one `L` and one `K`, in either order.
|
package/docs/contributing.md
CHANGED
|
@@ -19,7 +19,16 @@ manager version declared in `package.json`.
|
|
|
19
19
|
pnpm build
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Runs
|
|
22
|
+
Runs TypeScript compilation and builds the portable Rust TAR parser for
|
|
23
|
+
`wasm32-unknown-unknown`. Contributors need Rust (the native crate's declared
|
|
24
|
+
minimum or newer) and `rustup target add wasm32-unknown-unknown`; Alpine's
|
|
25
|
+
packaged toolchain uses `rust-wasm`. `pnpm archive:wasm` rebuilds just the parser.
|
|
26
|
+
The import-free asset lands at `dist/archive-parser.wasm`; source tests and
|
|
27
|
+
compiled consumers both resolve that generated artifact. Run `pnpm build`
|
|
28
|
+
before source tests in a fresh checkout. Do not commit `dist/` or built WASM.
|
|
29
|
+
Consumers receive the asset in the npm package and need no compiler.
|
|
30
|
+
|
|
31
|
+
Output lands in `dist/`. The package's `prepack` hook re-runs the build before publishing — manual `pnpm build` is only required when you want to inspect the output or run a freshly-built copy locally.
|
|
23
32
|
|
|
24
33
|
## Test
|
|
25
34
|
|
|
@@ -59,8 +68,33 @@ pnpm check
|
|
|
59
68
|
This runs the filesystem boundary checks, build, tests, and package
|
|
60
69
|
tarball/import validation.
|
|
61
70
|
|
|
71
|
+
### Real TAR producers
|
|
72
|
+
|
|
73
|
+
After installing the freshly packed root (and optionally its freshly built host
|
|
74
|
+
binding) in a disposable consumer, run:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pnpm archive:producer-smoke ./consumer off
|
|
78
|
+
pnpm archive:producer-smoke ./consumer require
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This uses a child bound to canonical cwd/device/inode running `/usr/bin/tar -czf - .`
|
|
82
|
+
with unchanged stdout, and npm tar, on synthetic Unicode/newline/long-name files,
|
|
83
|
+
then the installed package API for exact payload hashes and bounded reads.
|
|
84
|
+
It also rejects a valid PAX override attached to an invalid raw UTF-8 field.
|
|
85
|
+
The `require` command must resolve the freshly packed native binding; the
|
|
86
|
+
`off` command uses the installed WASM asset. No live user files are read.
|
|
87
|
+
|
|
62
88
|
### Native consumer installs
|
|
63
89
|
|
|
90
|
+
The CI Node 24 and native jobs also run `node scripts/device-path-proof.mjs off`
|
|
91
|
+
and `node scripts/device-path-proof.mjs require` against the built package.
|
|
92
|
+
This extracts real ZIP files and checks bounded member reads, preserving reserved
|
|
93
|
+
device-like names on POSIX while rejecting them and ignored-space aliases on
|
|
94
|
+
Windows. It also verifies ordinary secret reads and typed device-path rejection
|
|
95
|
+
without replacing filesystem functions. Run after `pnpm build`, and build the
|
|
96
|
+
host binding with `pnpm native:build` before the `require` case.
|
|
97
|
+
|
|
64
98
|
After `pnpm build` and a fresh `pnpm native:build`, run `pnpm package:smoke`.
|
|
65
99
|
It packs the real root and host binding, then runs root-only npm and the
|
|
66
100
|
declared pnpm version against a disposable loopback registry. The root's exact
|
package/docs/install.md
CHANGED
|
@@ -85,7 +85,7 @@ Use the main entry for the common surface, or the focused subpaths when you want
|
|
|
85
85
|
|
|
86
86
|
## Runtime dependencies
|
|
87
87
|
|
|
88
|
-
`@openclaw/fs-safe`
|
|
88
|
+
`@openclaw/fs-safe` bundles an import-free WASM build of its Rust TAR parser for guarded JavaScript TAR/gzip [archive extraction](archive.md), including installs with optional dependencies omitted. ZIP fallback uses lazily loaded optional `jszip` and reports a missing-dependency error without it. Public subpaths remain safe to import with all optional dependencies omitted, but imports do not prove native availability.
|
|
89
89
|
|
|
90
90
|
There are no peer dependencies. Exact-version optional packages carry the seven
|
|
91
91
|
native targets and npm-compatible OS, CPU, and Linux libc filters install only
|
package/docs/native-helper.md
CHANGED
|
@@ -29,6 +29,11 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
|
|
|
29
29
|
| `off` | Do not load a native package. Use the guarded JavaScript path deterministically. |
|
|
30
30
|
| `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
|
|
31
31
|
|
|
32
|
+
TAR/gzip in the guarded JavaScript path uses a bundled, import-free WASM build
|
|
33
|
+
of the same Rust parser used by native. `off` still disables native filesystem
|
|
34
|
+
code; it does not disable this portable parser. ZIP fallback still requires
|
|
35
|
+
optional `jszip`, and zstd/bzip2 remain native-only.
|
|
36
|
+
|
|
32
37
|
Configure the mode once during startup. Loading is lazy and cached; changing from `auto` to `require` after a failed load changes failure policy but does not repeatedly probe the binary.
|
|
33
38
|
|
|
34
39
|
[`tempWorkspace()` and its scoped/sync variants](temp.md#private-temp-workspaces)
|