@openclaw/fs-safe 0.1.0
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +12 -0
- package/dist/absolute-path.d.ts +19 -0
- package/dist/absolute-path.d.ts.map +1 -0
- package/dist/absolute-path.js +98 -0
- package/dist/advanced.d.ts +22 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +24 -0
- package/dist/archive-entry.d.ts +13 -0
- package/dist/archive-entry.d.ts.map +1 -0
- package/dist/archive-entry.js +47 -0
- package/dist/archive-kind.d.ts +8 -0
- package/dist/archive-kind.d.ts.map +1 -0
- package/dist/archive-kind.js +58 -0
- package/dist/archive-limits.d.ts +40 -0
- package/dist/archive-limits.d.ts.map +1 -0
- package/dist/archive-limits.js +92 -0
- package/dist/archive-staging.d.ts +26 -0
- package/dist/archive-staging.d.ts.map +1 -0
- package/dist/archive-staging.js +171 -0
- package/dist/archive-tar.d.ts +14 -0
- package/dist/archive-tar.d.ts.map +1 -0
- package/dist/archive-tar.js +52 -0
- package/dist/archive-utils.d.ts +3 -0
- package/dist/archive-utils.d.ts.map +1 -0
- package/dist/archive-utils.js +26 -0
- package/dist/archive-zip-preflight.d.ts +7 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -0
- package/dist/archive-zip-preflight.js +170 -0
- package/dist/archive.d.ts +24 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +251 -0
- package/dist/async-lock.d.ts +2 -0
- package/dist/async-lock.d.ts.map +1 -0
- package/dist/async-lock.js +17 -0
- package/dist/atomic.d.ts +5 -0
- package/dist/atomic.d.ts.map +1 -0
- package/dist/atomic.js +4 -0
- package/dist/boundary-file-read.d.ts +44 -0
- package/dist/boundary-file-read.d.ts.map +1 -0
- package/dist/boundary-file-read.js +129 -0
- package/dist/boundary-path.d.ts +39 -0
- package/dist/boundary-path.d.ts.map +1 -0
- package/dist/boundary-path.js +598 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/file-identity.d.ts +6 -0
- package/dist/file-identity.d.ts.map +1 -0
- package/dist/file-identity.js +14 -0
- package/dist/file-lock.d.ts +20 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +34 -0
- package/dist/file-store.d.ts +66 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +436 -0
- package/dist/file-url.d.ts +2 -0
- package/dist/file-url.d.ts.map +1 -0
- package/dist/file-url.js +1 -0
- package/dist/filename.d.ts +2 -0
- package/dist/filename.d.ts.map +1 -0
- package/dist/filename.js +25 -0
- package/dist/fs-pinned-path-helper.d.ts +7 -0
- package/dist/fs-pinned-path-helper.d.ts.map +1 -0
- package/dist/fs-pinned-path-helper.js +182 -0
- package/dist/fs-pinned-write-helper.d.ts +21 -0
- package/dist/fs-pinned-write-helper.d.ts.map +1 -0
- package/dist/fs-pinned-write-helper.js +263 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +30 -0
- package/dist/hardlink-guards.d.ts +7 -0
- package/dist/hardlink-guards.d.ts.map +1 -0
- package/dist/hardlink-guards.js +30 -0
- package/dist/home-dir.d.ts +22 -0
- package/dist/home-dir.d.ts.map +1 -0
- package/dist/home-dir.js +107 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/install-path.d.ts +20 -0
- package/dist/install-path.d.ts.map +1 -0
- package/dist/install-path.js +94 -0
- package/dist/install-safe-path.d.ts +20 -0
- package/dist/install-safe-path.d.ts.map +1 -0
- package/dist/install-safe-path.js +94 -0
- package/dist/json-document-store.d.ts +30 -0
- package/dist/json-document-store.d.ts.map +1 -0
- package/dist/json-document-store.js +68 -0
- package/dist/json-file.d.ts +3 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +123 -0
- package/dist/json-files.d.ts +20 -0
- package/dist/json-files.d.ts.map +1 -0
- package/dist/json-files.js +153 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.d.ts.map +1 -0
- package/dist/json-store.js +12 -0
- package/dist/json.d.ts +17 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +198 -0
- package/dist/local-file-access.d.ts +8 -0
- package/dist/local-file-access.d.ts.map +1 -0
- package/dist/local-file-access.js +73 -0
- package/dist/local-roots.d.ts +26 -0
- package/dist/local-roots.d.ts.map +1 -0
- package/dist/local-roots.js +150 -0
- package/dist/mode.d.ts +2 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +3 -0
- package/dist/move-path.d.ts +6 -0
- package/dist/move-path.d.ts.map +1 -0
- package/dist/move-path.js +18 -0
- package/dist/path-alias-guards.d.ts +19 -0
- package/dist/path-alias-guards.d.ts.map +1 -0
- package/dist/path-alias-guards.js +21 -0
- package/dist/path-guards.d.ts +7 -0
- package/dist/path-guards.d.ts.map +1 -0
- package/dist/path-guards.js +49 -0
- package/dist/path-policy.d.ts +25 -0
- package/dist/path-policy.d.ts.map +1 -0
- package/dist/path-policy.js +50 -0
- package/dist/path-safety.d.ts +12 -0
- package/dist/path-safety.d.ts.map +1 -0
- package/dist/path-safety.js +50 -0
- package/dist/path-scope.d.ts +2 -0
- package/dist/path-scope.d.ts.map +1 -0
- package/dist/path-scope.js +1 -0
- package/dist/path.d.ts +18 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +123 -0
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/permissions.d.ts +86 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +435 -0
- package/dist/pinned-helper.d.ts +9 -0
- package/dist/pinned-helper.d.ts.map +1 -0
- package/dist/pinned-helper.js +19 -0
- package/dist/pinned-open.d.ts +24 -0
- package/dist/pinned-open.d.ts.map +1 -0
- package/dist/pinned-open.js +71 -0
- package/dist/pinned-path.d.ts +7 -0
- package/dist/pinned-path.d.ts.map +1 -0
- package/dist/pinned-path.js +21 -0
- package/dist/pinned-python-config.d.ts +9 -0
- package/dist/pinned-python-config.d.ts.map +1 -0
- package/dist/pinned-python-config.js +39 -0
- package/dist/pinned-python.d.ts +12 -0
- package/dist/pinned-python.d.ts.map +1 -0
- package/dist/pinned-python.js +599 -0
- package/dist/pinned-write.d.ts +33 -0
- package/dist/pinned-write.d.ts.map +1 -0
- package/dist/pinned-write.js +198 -0
- package/dist/private-file-store.d.ts +60 -0
- package/dist/private-file-store.d.ts.map +1 -0
- package/dist/private-file-store.js +233 -0
- package/dist/private-temp-workspace.d.ts +39 -0
- package/dist/private-temp-workspace.d.ts.map +1 -0
- package/dist/private-temp-workspace.js +185 -0
- package/dist/regular-file.d.ts +38 -0
- package/dist/regular-file.d.ts.map +1 -0
- package/dist/regular-file.js +239 -0
- package/dist/replace-directory.d.ts +7 -0
- package/dist/replace-directory.d.ts.map +1 -0
- package/dist/replace-directory.js +32 -0
- package/dist/replace-file.d.ts +41 -0
- package/dist/replace-file.d.ts.map +1 -0
- package/dist/replace-file.js +325 -0
- package/dist/root-file.d.ts +44 -0
- package/dist/root-file.d.ts.map +1 -0
- package/dist/root-file.js +129 -0
- package/dist/root-path.d.ts +39 -0
- package/dist/root-path.d.ts.map +1 -0
- package/dist/root-path.js +598 -0
- package/dist/root-paths.d.ts +92 -0
- package/dist/root-paths.d.ts.map +1 -0
- package/dist/root-paths.js +338 -0
- package/dist/root.d.ts +108 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +1332 -0
- package/dist/safe-open-sync.d.ts +24 -0
- package/dist/safe-open-sync.d.ts.map +1 -0
- package/dist/safe-open-sync.js +71 -0
- package/dist/safe-root.d.ts +123 -0
- package/dist/safe-root.d.ts.map +1 -0
- package/dist/safe-root.js +1060 -0
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.d.ts.map +1 -0
- package/dist/secret-file.js +232 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +1 -0
- package/dist/secure-file.d.ts +32 -0
- package/dist/secure-file.d.ts.map +1 -0
- package/dist/secure-file.js +163 -0
- package/dist/secure-temp-dir.d.ts +27 -0
- package/dist/secure-temp-dir.d.ts.map +1 -0
- package/dist/secure-temp-dir.js +155 -0
- package/dist/secure-temp-workspace.d.ts +25 -0
- package/dist/secure-temp-workspace.d.ts.map +1 -0
- package/dist/secure-temp-workspace.js +136 -0
- package/dist/sibling-temp-file.d.ts +16 -0
- package/dist/sibling-temp-file.d.ts.map +1 -0
- package/dist/sibling-temp-file.js +73 -0
- package/dist/sibling-temp-write.d.ts +8 -0
- package/dist/sibling-temp-write.d.ts.map +1 -0
- package/dist/sibling-temp-write.js +40 -0
- package/dist/sibling-temp.d.ts +23 -0
- package/dist/sibling-temp.d.ts.map +1 -0
- package/dist/sibling-temp.js +119 -0
- package/dist/sidecar-lock.d.ts +50 -0
- package/dist/sidecar-lock.d.ts.map +1 -0
- package/dist/sidecar-lock.js +235 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +2 -0
- package/dist/string-coerce.d.ts +14 -0
- package/dist/string-coerce.d.ts.map +1 -0
- package/dist/string-coerce.js +72 -0
- package/dist/symlink-parents.d.ts +12 -0
- package/dist/symlink-parents.d.ts.map +1 -0
- package/dist/symlink-parents.js +78 -0
- package/dist/temp-cleanup.d.ts +6 -0
- package/dist/temp-cleanup.d.ts.map +1 -0
- package/dist/temp-cleanup.js +42 -0
- package/dist/temp-target.d.ts +28 -0
- package/dist/temp-target.d.ts.map +1 -0
- package/dist/temp-target.js +84 -0
- package/dist/temp.d.ts +3 -0
- package/dist/temp.d.ts.map +1 -0
- package/dist/temp.js +2 -0
- package/dist/test-hooks.d.ts +9 -0
- package/dist/test-hooks.d.ts.map +1 -0
- package/dist/test-hooks.js +13 -0
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -0
- package/dist/text-atomic.js +13 -0
- package/dist/timing.d.ts +6 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +24 -0
- package/dist/trash.d.ts +5 -0
- package/dist/trash.d.ts.map +1 -0
- package/dist/trash.js +127 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/walk.d.ts +26 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +169 -0
- package/dist/write-queue.d.ts +2 -0
- package/dist/write-queue.d.ts.map +1 -0
- package/dist/write-queue.js +18 -0
- package/package.json +117 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, resolveExtractLimits, } from "./archive-limits.js";
|
|
2
|
+
const ZIP_EOCD_SIGNATURE = 0x06054b50;
|
|
3
|
+
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
|
|
4
|
+
const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
|
|
5
|
+
const ZIP_EOCD_MIN_BYTES = 22;
|
|
6
|
+
const ZIP_EOCD_MAX_COMMENT_BYTES = 0xffff;
|
|
7
|
+
const ZIP64_ENTRY_COUNT_SENTINEL = 0xffff;
|
|
8
|
+
const ZIP64_UINT32_SENTINEL = 0xffffffff;
|
|
9
|
+
const ZIP_CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
|
|
10
|
+
const ZIP_CENTRAL_FILE_HEADER_MIN_BYTES = 46;
|
|
11
|
+
const ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET = 28;
|
|
12
|
+
const ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET = 30;
|
|
13
|
+
const ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET = 32;
|
|
14
|
+
const ZIP_EOCD_TOTAL_ENTRIES_OFFSET = 10;
|
|
15
|
+
const ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 12;
|
|
16
|
+
const ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 16;
|
|
17
|
+
const ZIP_EOCD_COMMENT_LENGTH_OFFSET = 20;
|
|
18
|
+
const ZIP64_EOCD_LOCATOR_BYTES = 20;
|
|
19
|
+
const ZIP64_EOCD_OFFSET_OFFSET = 8;
|
|
20
|
+
const ZIP64_EOCD_TOTAL_ENTRIES_OFFSET = 32;
|
|
21
|
+
const ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 40;
|
|
22
|
+
const ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 48;
|
|
23
|
+
function asBufferView(buffer) {
|
|
24
|
+
if (Buffer.isBuffer(buffer)) {
|
|
25
|
+
return buffer;
|
|
26
|
+
}
|
|
27
|
+
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
28
|
+
}
|
|
29
|
+
function readSafeUInt64LE(buffer, offset) {
|
|
30
|
+
const value = buffer.readBigUInt64LE(offset);
|
|
31
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
32
|
+
return Number.MAX_SAFE_INTEGER;
|
|
33
|
+
}
|
|
34
|
+
return Number(value);
|
|
35
|
+
}
|
|
36
|
+
function findZipEndOfCentralDirectory(buffer) {
|
|
37
|
+
if (buffer.byteLength < ZIP_EOCD_MIN_BYTES) {
|
|
38
|
+
return -1;
|
|
39
|
+
}
|
|
40
|
+
const minOffset = Math.max(0, buffer.byteLength - ZIP_EOCD_MIN_BYTES - ZIP_EOCD_MAX_COMMENT_BYTES);
|
|
41
|
+
for (let offset = buffer.byteLength - ZIP_EOCD_MIN_BYTES; offset >= minOffset; offset -= 1) {
|
|
42
|
+
if (buffer.readUInt32LE(offset) !== ZIP_EOCD_SIGNATURE) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const commentLength = buffer.readUInt16LE(offset + ZIP_EOCD_COMMENT_LENGTH_OFFSET);
|
|
46
|
+
if (offset + ZIP_EOCD_MIN_BYTES + commentLength === buffer.byteLength) {
|
|
47
|
+
return offset;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
function readZip64CentralDirectoryInfo(buffer, eocdOffset) {
|
|
53
|
+
const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_BYTES;
|
|
54
|
+
if (locatorOffset < 0 || buffer.readUInt32LE(locatorOffset) !== ZIP64_EOCD_LOCATOR_SIGNATURE) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const zip64EocdOffset = readSafeUInt64LE(buffer, locatorOffset + ZIP64_EOCD_OFFSET_OFFSET);
|
|
58
|
+
if (zip64EocdOffset < 0 ||
|
|
59
|
+
zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET + 8 > buffer.byteLength ||
|
|
60
|
+
buffer.readUInt32LE(zip64EocdOffset) !== ZIP64_EOCD_SIGNATURE) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
declaredEntryCount: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_TOTAL_ENTRIES_OFFSET),
|
|
65
|
+
centralDirectorySize: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET),
|
|
66
|
+
centralDirectoryOffset: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET),
|
|
67
|
+
endOfCentralDirectoryOffset: eocdOffset,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function readZipCentralDirectoryInfo(buffer) {
|
|
71
|
+
const eocdOffset = findZipEndOfCentralDirectory(buffer);
|
|
72
|
+
if (eocdOffset < 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const declaredEntryCount = buffer.readUInt16LE(eocdOffset + ZIP_EOCD_TOTAL_ENTRIES_OFFSET);
|
|
76
|
+
const centralDirectorySize = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET);
|
|
77
|
+
const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET);
|
|
78
|
+
const usesZip64 = declaredEntryCount === ZIP64_ENTRY_COUNT_SENTINEL ||
|
|
79
|
+
centralDirectorySize === ZIP64_UINT32_SENTINEL ||
|
|
80
|
+
centralDirectoryOffset === ZIP64_UINT32_SENTINEL;
|
|
81
|
+
if (usesZip64) {
|
|
82
|
+
return (readZip64CentralDirectoryInfo(buffer, eocdOffset) ?? {
|
|
83
|
+
declaredEntryCount,
|
|
84
|
+
centralDirectoryOffset,
|
|
85
|
+
centralDirectorySize,
|
|
86
|
+
endOfCentralDirectoryOffset: eocdOffset,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
declaredEntryCount,
|
|
91
|
+
centralDirectoryOffset,
|
|
92
|
+
centralDirectorySize,
|
|
93
|
+
endOfCentralDirectoryOffset: eocdOffset,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function countZipCentralDirectoryHeaders(buffer, info) {
|
|
97
|
+
const start = info.centralDirectoryOffset;
|
|
98
|
+
const declaredEnd = start + info.centralDirectorySize;
|
|
99
|
+
const scanEnd = info.endOfCentralDirectoryOffset;
|
|
100
|
+
if (!Number.isSafeInteger(start) ||
|
|
101
|
+
!Number.isSafeInteger(declaredEnd) ||
|
|
102
|
+
!Number.isSafeInteger(scanEnd) ||
|
|
103
|
+
start < 0 ||
|
|
104
|
+
declaredEnd < start ||
|
|
105
|
+
scanEnd < start ||
|
|
106
|
+
scanEnd > buffer.byteLength) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
let offset = start;
|
|
110
|
+
let count = 0;
|
|
111
|
+
while (offset < scanEnd) {
|
|
112
|
+
if (scanEnd - offset < ZIP_CENTRAL_FILE_HEADER_MIN_BYTES) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (buffer.readUInt32LE(offset) !== ZIP_CENTRAL_FILE_HEADER_SIGNATURE) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const nameLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET);
|
|
119
|
+
const extraLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET);
|
|
120
|
+
const commentLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET);
|
|
121
|
+
const nextOffset = offset + ZIP_CENTRAL_FILE_HEADER_MIN_BYTES + nameLength + extraLength + commentLength;
|
|
122
|
+
if (nextOffset <= offset || nextOffset > scanEnd) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
count += 1;
|
|
126
|
+
offset = nextOffset;
|
|
127
|
+
}
|
|
128
|
+
return count > 0 || info.declaredEntryCount === 0 ? count : null;
|
|
129
|
+
}
|
|
130
|
+
export function readZipCentralDirectoryEntryCount(buffer) {
|
|
131
|
+
const view = asBufferView(buffer);
|
|
132
|
+
const info = readZipCentralDirectoryInfo(view);
|
|
133
|
+
if (!info) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const countedEntryCount = countZipCentralDirectoryHeaders(view, info);
|
|
137
|
+
return countedEntryCount === null
|
|
138
|
+
? info.declaredEntryCount
|
|
139
|
+
: Math.max(info.declaredEntryCount, countedEntryCount);
|
|
140
|
+
}
|
|
141
|
+
export async function loadZipArchiveWithPreflight(buffer, limits) {
|
|
142
|
+
const resolvedLimits = resolveExtractLimits(limits);
|
|
143
|
+
if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
|
|
144
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
|
|
145
|
+
}
|
|
146
|
+
const entryCount = readZipCentralDirectoryEntryCount(buffer);
|
|
147
|
+
if (entryCount !== null) {
|
|
148
|
+
assertArchiveEntryCountWithinLimit(entryCount, resolvedLimits);
|
|
149
|
+
}
|
|
150
|
+
const JSZip = await importOptionalJsZip();
|
|
151
|
+
return await JSZip.loadAsync(buffer);
|
|
152
|
+
}
|
|
153
|
+
async function importOptionalJsZip() {
|
|
154
|
+
try {
|
|
155
|
+
const module = await import("jszip");
|
|
156
|
+
const candidate = typeof module === "function" ? module : module.default;
|
|
157
|
+
if ((typeof candidate !== "object" && typeof candidate !== "function") ||
|
|
158
|
+
candidate === null ||
|
|
159
|
+
typeof candidate.loadAsync !== "function") {
|
|
160
|
+
throw new Error('Optional archive dependency "jszip" does not expose loadAsync().');
|
|
161
|
+
}
|
|
162
|
+
return candidate;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
throw missingOptionalArchiveDependencyError("jszip", err);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function missingOptionalArchiveDependencyError(packageName, cause) {
|
|
169
|
+
return new Error(`Optional archive dependency "${packageName}" is not installed. Install it to use ZIP archive helpers from @openclaw/fs-safe/archive.`, { cause });
|
|
170
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ArchiveExtractLimits } from "./archive-limits.js";
|
|
2
|
+
import { type ArchiveKind } from "./archive-kind.js";
|
|
3
|
+
export type ArchiveLogger = {
|
|
4
|
+
info?: (message: string) => void;
|
|
5
|
+
warn?: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
8
|
+
export { resolveArchiveKind, resolvePackedRootDir, type ArchiveKind } from "./archive-kind.js";
|
|
9
|
+
export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, type ArchiveExtractLimits, type ArchiveLimitErrorCode, } from "./archive-limits.js";
|
|
10
|
+
export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-staging.js";
|
|
11
|
+
export { createArchiveSymlinkTraversalError, mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
12
|
+
export { createTarEntryPreflightChecker, type TarEntryInfo } from "./archive-tar.js";
|
|
13
|
+
export { loadZipArchiveWithPreflight, readZipCentralDirectoryEntryCount, type ZipArchiveWithFiles, } from "./archive-zip-preflight.js";
|
|
14
|
+
export declare function extractArchive(params: {
|
|
15
|
+
archivePath: string;
|
|
16
|
+
destDir: string;
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
kind?: ArchiveKind;
|
|
19
|
+
stripComponents?: number;
|
|
20
|
+
tarGzip?: boolean;
|
|
21
|
+
limits?: ArchiveExtractLimits;
|
|
22
|
+
logger?: ArchiveLogger;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAYA,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAgBzE,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAiOpC,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyEhB"}
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
8
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, } from "./archive-limits.js";
|
|
9
|
+
import { resolveArchiveKind } from "./archive-kind.js";
|
|
10
|
+
import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
11
|
+
import { createTarEntryPreflightChecker, readTarEntryInfo, } from "./archive-tar.js";
|
|
12
|
+
import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
13
|
+
import { isNotFoundPathError } from "./path.js";
|
|
14
|
+
import { withTimeout } from "./timing.js";
|
|
15
|
+
export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
16
|
+
export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
|
|
17
|
+
export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, } from "./archive-limits.js";
|
|
18
|
+
export { ArchiveSecurityError } from "./archive-staging.js";
|
|
19
|
+
export { createArchiveSymlinkTraversalError, mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
20
|
+
export { createTarEntryPreflightChecker } from "./archive-tar.js";
|
|
21
|
+
export { loadZipArchiveWithPreflight, readZipCentralDirectoryEntryCount, } from "./archive-zip-preflight.js";
|
|
22
|
+
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
23
|
+
const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
|
|
24
|
+
fsConstants.O_CREAT |
|
|
25
|
+
fsConstants.O_EXCL |
|
|
26
|
+
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
27
|
+
async function cleanupPartialRegularFile(filePath) {
|
|
28
|
+
let stat;
|
|
29
|
+
try {
|
|
30
|
+
stat = await fs.lstat(filePath);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (isNotFoundPathError(err)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
if (stat.isFile()) {
|
|
39
|
+
await fs.unlink(filePath).catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function buildArchiveAtomicTempPath(targetPath) {
|
|
43
|
+
return path.join(path.dirname(targetPath), `.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
44
|
+
}
|
|
45
|
+
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
|
46
|
+
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
|
47
|
+
function isZipSymlinkEntry(entry) {
|
|
48
|
+
return (typeof entry.unixPermissions === "number" &&
|
|
49
|
+
(entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE);
|
|
50
|
+
}
|
|
51
|
+
async function readZipEntryStream(entry) {
|
|
52
|
+
if (typeof entry.nodeStream === "function") {
|
|
53
|
+
return entry.nodeStream();
|
|
54
|
+
}
|
|
55
|
+
// Old JSZip: fall back to buffering, but still extract via a stream.
|
|
56
|
+
const buf = await entry.async("nodebuffer");
|
|
57
|
+
return Readable.from(buf);
|
|
58
|
+
}
|
|
59
|
+
function resolveZipOutputPath(params) {
|
|
60
|
+
validateArchiveEntryPath(params.entryPath);
|
|
61
|
+
const relPath = stripArchivePath(params.entryPath, params.strip);
|
|
62
|
+
if (!relPath) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
validateArchiveEntryPath(relPath);
|
|
66
|
+
return {
|
|
67
|
+
relPath,
|
|
68
|
+
outPath: resolveArchiveOutputPath({
|
|
69
|
+
rootDir: params.destinationDir,
|
|
70
|
+
relPath,
|
|
71
|
+
originalPath: params.entryPath,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function prepareZipOutputPath(params) {
|
|
76
|
+
await prepareArchiveOutputPath(params);
|
|
77
|
+
}
|
|
78
|
+
async function writeZipFileEntry(params) {
|
|
79
|
+
params.budget.startEntry();
|
|
80
|
+
const readable = await readZipEntryStream(params.entry);
|
|
81
|
+
const destinationPath = params.outPath;
|
|
82
|
+
let tempHandle = null;
|
|
83
|
+
let tempPath = null;
|
|
84
|
+
let handleClosedByStream = false;
|
|
85
|
+
try {
|
|
86
|
+
tempPath = buildArchiveAtomicTempPath(destinationPath);
|
|
87
|
+
tempHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o666);
|
|
88
|
+
const writable = tempHandle.createWriteStream();
|
|
89
|
+
writable.once("close", () => {
|
|
90
|
+
handleClosedByStream = true;
|
|
91
|
+
});
|
|
92
|
+
await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), writable);
|
|
93
|
+
if (!handleClosedByStream) {
|
|
94
|
+
await tempHandle.close().catch(() => undefined);
|
|
95
|
+
handleClosedByStream = true;
|
|
96
|
+
}
|
|
97
|
+
tempHandle = null;
|
|
98
|
+
await fs.rename(tempPath, destinationPath);
|
|
99
|
+
tempPath = null;
|
|
100
|
+
// Best-effort permission restore for zip entries created on unix.
|
|
101
|
+
if (typeof params.entry.unixPermissions === "number") {
|
|
102
|
+
const mode = params.entry.unixPermissions & 0o777;
|
|
103
|
+
if (mode !== 0) {
|
|
104
|
+
await fs.chmod(destinationPath, mode).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (tempPath) {
|
|
110
|
+
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
await cleanupPartialRegularFile(destinationPath).catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (tempHandle && !handleClosedByStream) {
|
|
119
|
+
await tempHandle.close().catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function extractZip(params) {
|
|
124
|
+
const limits = resolveExtractLimits(params.limits);
|
|
125
|
+
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
|
|
126
|
+
const stat = await fs.stat(params.archivePath);
|
|
127
|
+
if (stat.size > limits.maxArchiveBytes) {
|
|
128
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
|
|
129
|
+
}
|
|
130
|
+
const buffer = await fs.readFile(params.archivePath);
|
|
131
|
+
const zip = await loadZipArchiveWithPreflight(buffer, limits);
|
|
132
|
+
const entries = Object.values(zip.files);
|
|
133
|
+
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
|
|
134
|
+
assertArchiveEntryCountWithinLimit(entries.length, limits);
|
|
135
|
+
const budget = createByteBudgetTracker(limits);
|
|
136
|
+
await withStagedArchiveDestination({
|
|
137
|
+
destinationRealDir,
|
|
138
|
+
run: async (stagingDir) => {
|
|
139
|
+
const stagingRealDir = await fs.realpath(stagingDir);
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const output = resolveZipOutputPath({
|
|
142
|
+
entryPath: entry.name,
|
|
143
|
+
strip,
|
|
144
|
+
destinationDir: stagingRealDir,
|
|
145
|
+
});
|
|
146
|
+
if (!output) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
await prepareZipOutputPath({
|
|
150
|
+
destinationDir: stagingRealDir,
|
|
151
|
+
destinationRealDir: stagingRealDir,
|
|
152
|
+
relPath: output.relPath,
|
|
153
|
+
outPath: output.outPath,
|
|
154
|
+
originalPath: entry.name,
|
|
155
|
+
isDirectory: entry.dir,
|
|
156
|
+
});
|
|
157
|
+
if (entry.dir) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (isZipSymlinkEntry(entry)) {
|
|
161
|
+
throw new Error(`zip entry is a link: ${entry.name}`);
|
|
162
|
+
}
|
|
163
|
+
await writeZipFileEntry({
|
|
164
|
+
entry,
|
|
165
|
+
outPath: output.outPath,
|
|
166
|
+
budget,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
await mergeExtractedTreeIntoDestination({
|
|
170
|
+
sourceDir: stagingRealDir,
|
|
171
|
+
destinationDir: destinationRealDir,
|
|
172
|
+
destinationRealDir,
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
export async function extractArchive(params) {
|
|
178
|
+
const kind = params.kind ?? resolveArchiveKind(params.archivePath);
|
|
179
|
+
if (!kind) {
|
|
180
|
+
throw new Error(`unsupported archive: ${params.archivePath}`);
|
|
181
|
+
}
|
|
182
|
+
const label = kind === "zip" ? "extract zip" : "extract tar";
|
|
183
|
+
if (kind === "tar") {
|
|
184
|
+
await withTimeout((async () => {
|
|
185
|
+
const tar = await importOptionalTar();
|
|
186
|
+
const limits = resolveExtractLimits(params.limits);
|
|
187
|
+
const stat = await fs.stat(params.archivePath);
|
|
188
|
+
if (stat.size > limits.maxArchiveBytes) {
|
|
189
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
|
|
190
|
+
}
|
|
191
|
+
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
|
|
192
|
+
await withStagedArchiveDestination({
|
|
193
|
+
destinationRealDir,
|
|
194
|
+
run: async (stagingDir) => {
|
|
195
|
+
const checkTarEntrySafety = createTarEntryPreflightChecker({
|
|
196
|
+
rootDir: destinationRealDir,
|
|
197
|
+
stripComponents: params.stripComponents,
|
|
198
|
+
limits,
|
|
199
|
+
});
|
|
200
|
+
// A canonical cwd is not enough here: tar can still follow
|
|
201
|
+
// pre-existing child symlinks in the live destination tree.
|
|
202
|
+
// Extract into a private staging dir first, then merge through
|
|
203
|
+
// the same safe-open boundary checks used by direct file writes.
|
|
204
|
+
await tar.x({
|
|
205
|
+
file: params.archivePath,
|
|
206
|
+
cwd: stagingDir,
|
|
207
|
+
strip: Math.max(0, Math.floor(params.stripComponents ?? 0)),
|
|
208
|
+
gzip: params.tarGzip,
|
|
209
|
+
preservePaths: false,
|
|
210
|
+
strict: true,
|
|
211
|
+
onReadEntry(entry) {
|
|
212
|
+
try {
|
|
213
|
+
checkTarEntrySafety(readTarEntryInfo(entry));
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
217
|
+
// Node's EventEmitter calls listeners with `this` bound to the
|
|
218
|
+
// emitter (tar.Unpack), which exposes Parser.abort().
|
|
219
|
+
const emitter = this;
|
|
220
|
+
emitter.abort?.(error);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
await mergeExtractedTreeIntoDestination({
|
|
225
|
+
sourceDir: stagingDir,
|
|
226
|
+
destinationDir: destinationRealDir,
|
|
227
|
+
destinationRealDir,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
})(), params.timeoutMs, label);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await withTimeout(extractZip({
|
|
235
|
+
archivePath: params.archivePath,
|
|
236
|
+
destDir: params.destDir,
|
|
237
|
+
stripComponents: params.stripComponents,
|
|
238
|
+
limits: params.limits,
|
|
239
|
+
}), params.timeoutMs, label);
|
|
240
|
+
}
|
|
241
|
+
async function importOptionalTar() {
|
|
242
|
+
try {
|
|
243
|
+
return await import("tar");
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
throw missingOptionalArchiveDependencyError("tar", err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function missingOptionalArchiveDependencyError(packageName, cause) {
|
|
250
|
+
return new Error(`Optional archive dependency "${packageName}" is not installed. Install it to use TAR archive helpers from @openclaw/fs-safe/archive.`, { cause });
|
|
251
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-lock.d.ts","sourceRoot":"","sources":["../src/async-lock.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,KAEE,CAAC,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAapE"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function createAsyncLock() {
|
|
2
|
+
let lock = Promise.resolve();
|
|
3
|
+
return async function withLock(fn) {
|
|
4
|
+
const prev = lock;
|
|
5
|
+
let release;
|
|
6
|
+
lock = new Promise((resolve) => {
|
|
7
|
+
release = resolve;
|
|
8
|
+
});
|
|
9
|
+
await prev;
|
|
10
|
+
try {
|
|
11
|
+
return await fn();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
release?.();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/atomic.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { replaceFileAtomic, replaceFileAtomicSync, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicResult, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
|
|
2
|
+
export { writeTextAtomic, type WriteTextAtomicOptions } from "./text-atomic.js";
|
|
3
|
+
export { replaceDirectoryAtomic, type ReplaceDirectoryAtomicOptions } from "./replace-directory.js";
|
|
4
|
+
export { movePathWithCopyFallback, type MovePathWithCopyFallbackOptions } from "./move-path.js";
|
|
5
|
+
//# sourceMappingURL=atomic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,GAClC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/atomic.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import type { PathAliasPolicy } from "./path-alias-guards.js";
|
|
3
|
+
import { type SafeOpenSyncAllowedType, type SafeOpenSyncFailureReason } from "./safe-open-sync.js";
|
|
4
|
+
type BoundaryReadFs = Pick<typeof fs, "closeSync" | "constants" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "realpathSync">;
|
|
5
|
+
export type BoundaryFileOpenFailureReason = SafeOpenSyncFailureReason | "validation";
|
|
6
|
+
export type BoundaryFileOpenResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
path: string;
|
|
9
|
+
fd: number;
|
|
10
|
+
stat: fs.Stats;
|
|
11
|
+
rootRealPath: string;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: BoundaryFileOpenFailureReason;
|
|
15
|
+
error?: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type BoundaryFileOpenFailure = Extract<BoundaryFileOpenResult, {
|
|
18
|
+
ok: false;
|
|
19
|
+
}>;
|
|
20
|
+
export type OpenBoundaryFileSyncParams = {
|
|
21
|
+
absolutePath: string;
|
|
22
|
+
rootPath: string;
|
|
23
|
+
boundaryLabel: string;
|
|
24
|
+
rootRealPath?: string;
|
|
25
|
+
maxBytes?: number;
|
|
26
|
+
rejectHardlinks?: boolean;
|
|
27
|
+
allowedType?: SafeOpenSyncAllowedType;
|
|
28
|
+
skipLexicalRootCheck?: boolean;
|
|
29
|
+
ioFs?: BoundaryReadFs;
|
|
30
|
+
};
|
|
31
|
+
export type OpenBoundaryFileParams = OpenBoundaryFileSyncParams & {
|
|
32
|
+
aliasPolicy?: PathAliasPolicy;
|
|
33
|
+
};
|
|
34
|
+
export declare function canUseBoundaryFileOpen(ioFs: typeof fs): boolean;
|
|
35
|
+
export declare function openBoundaryFileSync(params: OpenBoundaryFileSyncParams): BoundaryFileOpenResult;
|
|
36
|
+
export declare function matchBoundaryFileOpenFailure<T>(failure: BoundaryFileOpenFailure, handlers: {
|
|
37
|
+
path?: (failure: BoundaryFileOpenFailure) => T;
|
|
38
|
+
validation?: (failure: BoundaryFileOpenFailure) => T;
|
|
39
|
+
io?: (failure: BoundaryFileOpenFailure) => T;
|
|
40
|
+
fallback: (failure: BoundaryFileOpenFailure) => T;
|
|
41
|
+
}): T;
|
|
42
|
+
export declare function openBoundaryFile(params: OpenBoundaryFileParams): Promise<BoundaryFileOpenResult>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=boundary-file-read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary-file-read.d.ts","sourceRoot":"","sources":["../src/boundary-file-read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC/B,MAAM,qBAAqB,CAAC;AAE7B,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,EAAE,EACP,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,cAAc,GACd,cAAc,CACjB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,yBAAyB,GAAG,YAAY,CAAC;AAErF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,6BAA6B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE1E,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAErF,MAAM,MAAM,0BAA0B,GAAG;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,0BAA0B,GAAG;IAChE,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAQF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAW/D;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,0BAA0B,GAAG,sBAAsB,CAuB/F;AAED,wBAAgB,4BAA4B,CAAC,CAAC,EAC5C,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE;IACR,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IAC/C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IACrD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IAC7C,QAAQ,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;CACnD,GACA,CAAC,CAUH;AAoDD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,sBAAsB,CAAC,CAsBjC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveBoundaryPath, resolveBoundaryPathSync, } from "./boundary-path.js";
|
|
4
|
+
import { openVerifiedFileSync, } from "./safe-open-sync.js";
|
|
5
|
+
export function canUseBoundaryFileOpen(ioFs) {
|
|
6
|
+
return (typeof ioFs.openSync === "function" &&
|
|
7
|
+
typeof ioFs.closeSync === "function" &&
|
|
8
|
+
typeof ioFs.fstatSync === "function" &&
|
|
9
|
+
typeof ioFs.lstatSync === "function" &&
|
|
10
|
+
typeof ioFs.realpathSync === "function" &&
|
|
11
|
+
typeof ioFs.readFileSync === "function" &&
|
|
12
|
+
typeof ioFs.constants === "object" &&
|
|
13
|
+
ioFs.constants !== null);
|
|
14
|
+
}
|
|
15
|
+
export function openBoundaryFileSync(params) {
|
|
16
|
+
const ioFs = params.ioFs ?? fs;
|
|
17
|
+
const resolved = resolveBoundaryFilePathGeneric({
|
|
18
|
+
absolutePath: params.absolutePath,
|
|
19
|
+
resolve: (absolutePath) => resolveBoundaryPathSync({
|
|
20
|
+
absolutePath,
|
|
21
|
+
rootPath: params.rootPath,
|
|
22
|
+
rootCanonicalPath: params.rootRealPath,
|
|
23
|
+
boundaryLabel: params.boundaryLabel,
|
|
24
|
+
skipLexicalRootCheck: params.skipLexicalRootCheck,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (resolved instanceof Promise) {
|
|
28
|
+
return toBoundaryValidationError(new Error("Unexpected async boundary resolution"));
|
|
29
|
+
}
|
|
30
|
+
return finalizeBoundaryFileOpen({
|
|
31
|
+
resolved,
|
|
32
|
+
maxBytes: params.maxBytes,
|
|
33
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
34
|
+
allowedType: params.allowedType,
|
|
35
|
+
ioFs,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function matchBoundaryFileOpenFailure(failure, handlers) {
|
|
39
|
+
switch (failure.reason) {
|
|
40
|
+
case "path":
|
|
41
|
+
return handlers.path ? handlers.path(failure) : handlers.fallback(failure);
|
|
42
|
+
case "validation":
|
|
43
|
+
return handlers.validation ? handlers.validation(failure) : handlers.fallback(failure);
|
|
44
|
+
case "io":
|
|
45
|
+
return handlers.io ? handlers.io(failure) : handlers.fallback(failure);
|
|
46
|
+
}
|
|
47
|
+
return handlers.fallback(failure);
|
|
48
|
+
}
|
|
49
|
+
function openBoundaryFileResolved(params) {
|
|
50
|
+
const opened = openVerifiedFileSync({
|
|
51
|
+
filePath: params.absolutePath,
|
|
52
|
+
resolvedPath: params.resolvedPath,
|
|
53
|
+
rejectHardlinks: params.rejectHardlinks ?? true,
|
|
54
|
+
maxBytes: params.maxBytes,
|
|
55
|
+
allowedType: params.allowedType,
|
|
56
|
+
ioFs: params.ioFs,
|
|
57
|
+
});
|
|
58
|
+
if (!opened.ok) {
|
|
59
|
+
return opened;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
path: opened.path,
|
|
64
|
+
fd: opened.fd,
|
|
65
|
+
stat: opened.stat,
|
|
66
|
+
rootRealPath: params.rootRealPath,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function finalizeBoundaryFileOpen(params) {
|
|
70
|
+
if ("ok" in params.resolved) {
|
|
71
|
+
return params.resolved;
|
|
72
|
+
}
|
|
73
|
+
return openBoundaryFileResolved({
|
|
74
|
+
absolutePath: params.resolved.absolutePath,
|
|
75
|
+
resolvedPath: params.resolved.resolvedPath,
|
|
76
|
+
rootRealPath: params.resolved.rootRealPath,
|
|
77
|
+
maxBytes: params.maxBytes,
|
|
78
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
79
|
+
allowedType: params.allowedType,
|
|
80
|
+
ioFs: params.ioFs,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export async function openBoundaryFile(params) {
|
|
84
|
+
const ioFs = params.ioFs ?? fs;
|
|
85
|
+
const maybeResolved = resolveBoundaryFilePathGeneric({
|
|
86
|
+
absolutePath: params.absolutePath,
|
|
87
|
+
resolve: (absolutePath) => resolveBoundaryPath({
|
|
88
|
+
absolutePath,
|
|
89
|
+
rootPath: params.rootPath,
|
|
90
|
+
rootCanonicalPath: params.rootRealPath,
|
|
91
|
+
boundaryLabel: params.boundaryLabel,
|
|
92
|
+
policy: params.aliasPolicy,
|
|
93
|
+
skipLexicalRootCheck: params.skipLexicalRootCheck,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
const resolved = maybeResolved instanceof Promise ? await maybeResolved : maybeResolved;
|
|
97
|
+
return finalizeBoundaryFileOpen({
|
|
98
|
+
resolved,
|
|
99
|
+
maxBytes: params.maxBytes,
|
|
100
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
101
|
+
allowedType: params.allowedType,
|
|
102
|
+
ioFs,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function toBoundaryValidationError(error) {
|
|
106
|
+
return { ok: false, reason: "validation", error };
|
|
107
|
+
}
|
|
108
|
+
function mapResolvedBoundaryPath(absolutePath, resolved) {
|
|
109
|
+
return {
|
|
110
|
+
absolutePath,
|
|
111
|
+
resolvedPath: resolved.canonicalPath,
|
|
112
|
+
rootRealPath: resolved.rootCanonicalPath,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function resolveBoundaryFilePathGeneric(params) {
|
|
116
|
+
const absolutePath = path.resolve(params.absolutePath);
|
|
117
|
+
try {
|
|
118
|
+
const resolved = params.resolve(absolutePath);
|
|
119
|
+
if (resolved instanceof Promise) {
|
|
120
|
+
return resolved
|
|
121
|
+
.then((value) => mapResolvedBoundaryPath(absolutePath, value))
|
|
122
|
+
.catch((error) => toBoundaryValidationError(error));
|
|
123
|
+
}
|
|
124
|
+
return mapResolvedBoundaryPath(absolutePath, resolved);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return toBoundaryValidationError(error);
|
|
128
|
+
}
|
|
129
|
+
}
|