@openclaw/fs-safe 0.5.6 → 0.7.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 +89 -0
- package/README.md +23 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts +2 -0
- package/dist/archive-crc32.d.ts.map +1 -0
- package/dist/archive-crc32.js +14 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-kind.js +2 -2
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +31 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +88 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +135 -19
- package/dist/archive-tar-pax.d.ts +8 -0
- package/dist/archive-tar-pax.d.ts.map +1 -0
- package/dist/archive-tar-pax.js +100 -0
- package/dist/archive-tar-runtime.d.ts +4 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-admission.d.ts +7 -0
- package/dist/archive-zip-admission.d.ts.map +1 -0
- package/dist/archive-zip-admission.js +60 -0
- package/dist/archive-zip-count.d.ts +2 -0
- package/dist/archive-zip-count.d.ts.map +1 -0
- package/dist/archive-zip-count.js +140 -0
- package/dist/archive-zip-directory.d.ts +8 -0
- package/dist/archive-zip-directory.d.ts.map +1 -0
- package/dist/archive-zip-directory.js +224 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +1 -14
- package/dist/archive-zip-names.d.ts +12 -0
- package/dist/archive-zip-names.d.ts.map +1 -0
- package/dist/archive-zip-names.js +99 -0
- package/dist/archive-zip-preflight.d.ts +1 -1
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -145
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +63 -42
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +26 -9
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +15 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +4 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +22 -6
- package/dist/native-pinned-write-windows.d.ts +8 -0
- package/dist/native-pinned-write-windows.d.ts.map +1 -0
- package/dist/native-pinned-write-windows.js +92 -0
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +136 -127
- package/dist/native-staged-file.d.ts +24 -0
- package/dist/native-staged-file.d.ts.map +1 -0
- package/dist/native-staged-file.js +337 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -4
- package/dist/opened-realpath.d.ts +2 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +12 -7
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -1
- package/dist/permission-exec.d.ts +19 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +57 -11
- package/dist/permissions-public.d.ts +1 -1
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-windows.d.ts +3 -0
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +16 -5
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +26 -37
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +118 -98
- package/dist/root-paths.d.ts +11 -14
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +36 -27
- package/dist/root-write-verification.d.ts +11 -0
- package/dist/root-write-verification.d.ts.map +1 -0
- package/dist/root-write-verification.js +91 -0
- package/dist/secret-file.d.ts +1 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +49 -120
- package/dist/secret-read-async.d.ts +1 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +51 -72
- package/dist/secret-read-policy.d.ts +13 -0
- package/dist/secret-read-policy.d.ts.map +1 -0
- package/dist/secret-read-policy.js +28 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +42 -29
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/staged-directory.d.ts +16 -0
- package/dist/staged-directory.d.ts.map +1 -0
- package/dist/staged-directory.js +60 -0
- package/dist/staged-file-types.d.ts +56 -0
- package/dist/staged-file-types.d.ts.map +1 -0
- package/dist/staged-file-types.js +1 -0
- package/dist/staged-file.d.ts +10 -0
- package/dist/staged-file.d.ts.map +1 -0
- package/dist/staged-file.js +15 -0
- package/dist/strict-file-identity.d.ts +6 -0
- package/dist/strict-file-identity.d.ts.map +1 -0
- package/dist/strict-file-identity.js +48 -0
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-owner.d.ts +3 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +10 -2
- package/docs/advanced.md +19 -2
- package/docs/archive.md +250 -35
- package/docs/atomic.md +11 -2
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +18 -5
- package/docs/errors.md +16 -1
- package/docs/file-store.md +2 -0
- package/docs/index.md +3 -1
- package/docs/install.md +20 -8
- package/docs/json.md +8 -4
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +30 -8
- package/docs/native.md +73 -17
- package/docs/output.md +10 -0
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +3 -1
- package/docs/secret-file.md +12 -0
- package/docs/secure-file.md +21 -3
- package/docs/security-model.md +33 -1
- package/docs/sidecar-lock.md +15 -1
- package/docs/staged-file.md +178 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +16 -9
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/archive.js
CHANGED
|
@@ -3,9 +3,9 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
|
-
import { createArchiveOutputPathTracker,
|
|
6
|
+
import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
7
7
|
import { createPipelineTimeoutError, waitForDeadline, withExtractionDeadline, } from "./archive-deadline.js";
|
|
8
|
-
import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, } from "./archive-limits.js";
|
|
8
|
+
import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, resolveTarMeterLimits, } from "./archive-limits.js";
|
|
9
9
|
import { resolveArchiveKind } from "./archive-kind.js";
|
|
10
10
|
import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
11
11
|
import { createTarEntryPreflightChecker, readTarEntryInfo, } from "./archive-tar.js";
|
|
@@ -17,9 +17,10 @@ import { ArchiveSecurityError } from "./archive-errors.js";
|
|
|
17
17
|
import { extractNativeArchive } from "./archive-native.js";
|
|
18
18
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
19
19
|
import { getNativeBinding } from "./native.js";
|
|
20
|
-
import { resolveArchiveEntryMode, shouldExtractArchiveEntry, } from "./archive-policy.js";
|
|
21
|
-
import { importOptionalTar, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
20
|
+
import { resolveArchiveEntryMode, resolveArchiveFilteredEntryPolicy, shouldExtractArchiveEntry, } from "./archive-policy.js";
|
|
21
|
+
import { importOptionalTar, NODE_TAR_RATIO_DISABLED, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
22
22
|
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
23
|
+
import { createTarAdmissionPlan } from "./archive-tar-admission.js";
|
|
23
24
|
import { writeSiblingTempFile } from "./sibling-temp.js";
|
|
24
25
|
export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
25
26
|
export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
|
|
@@ -45,12 +46,16 @@ async function readZipEntryStream(entry) {
|
|
|
45
46
|
}
|
|
46
47
|
function resolveZipOutputPath(params) {
|
|
47
48
|
validateArchiveEntryPath(params.entryPath);
|
|
48
|
-
const
|
|
49
|
+
const canonicalPath = stripArchivePath(params.entryPath, 0);
|
|
50
|
+
if (!canonicalPath)
|
|
51
|
+
return null;
|
|
52
|
+
const relPath = stripArchivePath(canonicalPath, params.strip);
|
|
49
53
|
if (!relPath) {
|
|
50
54
|
return null;
|
|
51
55
|
}
|
|
52
56
|
validateArchiveEntryPath(relPath);
|
|
53
57
|
return {
|
|
58
|
+
canonicalPath,
|
|
54
59
|
relPath,
|
|
55
60
|
outPath: resolveArchiveOutputPath({
|
|
56
61
|
rootDir: params.destinationDir,
|
|
@@ -151,10 +156,13 @@ async function extractZip(params) {
|
|
|
151
156
|
if (!shouldExtractArchiveEntry({
|
|
152
157
|
filter: params.entryFilter,
|
|
153
158
|
onFiltered: params.onFiltered,
|
|
154
|
-
entry: { path:
|
|
159
|
+
entry: { path: output.canonicalPath, kind: entryKind, size: entrySize },
|
|
155
160
|
})) {
|
|
156
161
|
continue;
|
|
157
162
|
}
|
|
163
|
+
if (isSymlink) {
|
|
164
|
+
throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${entry.name}`);
|
|
165
|
+
}
|
|
158
166
|
const mode = zipEntryMode(entry, params.entryModes);
|
|
159
167
|
await prepareZipOutputPath({
|
|
160
168
|
destinationDir: stagingRealDir,
|
|
@@ -163,14 +171,12 @@ async function extractZip(params) {
|
|
|
163
171
|
outPath: output.outPath,
|
|
164
172
|
originalPath: entry.name,
|
|
165
173
|
isDirectory: entry.dir,
|
|
174
|
+
deadline: params.deadline,
|
|
166
175
|
});
|
|
167
176
|
if (entry.dir) {
|
|
168
177
|
await fs.chmod(output.outPath, mode);
|
|
169
178
|
continue;
|
|
170
179
|
}
|
|
171
|
-
if (isSymlink) {
|
|
172
|
-
throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${entry.name}`);
|
|
173
|
-
}
|
|
174
180
|
await writeZipFileEntry({
|
|
175
181
|
entry,
|
|
176
182
|
outPath: output.outPath,
|
|
@@ -184,6 +190,7 @@ async function extractZip(params) {
|
|
|
184
190
|
sourceDir: stagingRealDir,
|
|
185
191
|
destinationDir: params.destDir,
|
|
186
192
|
destinationRealDir,
|
|
193
|
+
deadline: params.deadline,
|
|
187
194
|
});
|
|
188
195
|
params.deadline.check();
|
|
189
196
|
},
|
|
@@ -194,11 +201,14 @@ async function extractZip(params) {
|
|
|
194
201
|
}
|
|
195
202
|
}
|
|
196
203
|
export async function extractArchive(params) {
|
|
204
|
+
const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
|
|
197
205
|
const kind = params.kind ?? resolveArchiveKind(params.archivePath);
|
|
198
206
|
if (!kind) {
|
|
199
207
|
throw new Error(`unsupported archive: ${params.archivePath}`);
|
|
200
208
|
}
|
|
201
209
|
const label = kind === "zip" ? "extract zip" : "extract tar";
|
|
210
|
+
const limits = resolveExtractLimits(params.limits);
|
|
211
|
+
const tarLimits = resolveTarMeterLimits(limits);
|
|
202
212
|
const native = getNativeBinding();
|
|
203
213
|
if (native) {
|
|
204
214
|
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({
|
|
@@ -207,31 +217,34 @@ export async function extractArchive(params) {
|
|
|
207
217
|
destDir: params.destDir,
|
|
208
218
|
kind,
|
|
209
219
|
stripComponents: params.stripComponents,
|
|
210
|
-
limits
|
|
220
|
+
limits,
|
|
221
|
+
tarLimits,
|
|
211
222
|
deadline,
|
|
212
223
|
entryModes: params.entryModes,
|
|
213
224
|
entryFilter: params.entryFilter,
|
|
214
|
-
onFiltered
|
|
225
|
+
onFiltered,
|
|
215
226
|
}));
|
|
216
227
|
return;
|
|
217
228
|
}
|
|
218
229
|
if (kind === "tar-zstd" || kind === "tar-bzip2") {
|
|
219
|
-
throw new FsSafeError("helper-unavailable", `${kind} archives require
|
|
230
|
+
throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
|
|
231
|
+
"install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
|
|
220
232
|
}
|
|
221
233
|
if (kind === "tar") {
|
|
222
234
|
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
|
|
223
235
|
const tar = await importOptionalTar();
|
|
224
|
-
const limits = resolveExtractLimits(params.limits);
|
|
225
236
|
const stagedArchive = await stageArchiveFileForExtraction({
|
|
226
237
|
archivePath: params.archivePath,
|
|
227
238
|
limits,
|
|
228
239
|
deadline,
|
|
229
240
|
});
|
|
230
241
|
try {
|
|
242
|
+
const manifest = [];
|
|
231
243
|
await preflightTarMetadata({
|
|
232
244
|
archivePath: stagedArchive.path,
|
|
233
|
-
|
|
245
|
+
limits: tarLimits,
|
|
234
246
|
signal: deadline.signal,
|
|
247
|
+
onMember: (entry) => { manifest.push(entry); },
|
|
235
248
|
});
|
|
236
249
|
deadline.check();
|
|
237
250
|
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
|
|
@@ -239,49 +252,54 @@ export async function extractArchive(params) {
|
|
|
239
252
|
destinationRealDir,
|
|
240
253
|
run: async (stagingDir) => {
|
|
241
254
|
deadline.check();
|
|
255
|
+
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
|
|
242
256
|
const checkTarEntrySafety = createTarEntryPreflightChecker({
|
|
243
257
|
rootDir: destinationRealDir,
|
|
244
258
|
stripComponents: params.stripComponents,
|
|
245
259
|
limits,
|
|
246
260
|
entryFilter: params.entryFilter,
|
|
247
|
-
onFiltered
|
|
261
|
+
onFiltered,
|
|
248
262
|
});
|
|
263
|
+
const admission = createTarAdmissionPlan(manifest, (entry) => {
|
|
264
|
+
deadline.check();
|
|
265
|
+
return checkTarEntrySafety(entry);
|
|
266
|
+
}, strip);
|
|
249
267
|
const acceptedEntries = [];
|
|
250
|
-
//
|
|
251
|
-
// pre-existing child symlinks in the live destination tree.
|
|
252
|
-
// Extract into a private staging dir first, then merge through
|
|
253
|
-
// the same safe-open boundary checks used by direct file writes.
|
|
268
|
+
// Extract privately, then merge through the safe-open boundary.
|
|
254
269
|
const extractor = tar.x({
|
|
255
270
|
cwd: stagingDir,
|
|
256
|
-
|
|
271
|
+
// fs-safe owns stripping: node-tar counts the `.` and empty path
|
|
272
|
+
// components that stripArchivePath() drops, so the two disagree.
|
|
273
|
+
strip: 0,
|
|
257
274
|
gzip: params.tarGzip,
|
|
258
275
|
signal: deadline.signal,
|
|
259
276
|
preservePaths: false,
|
|
260
277
|
noChmod: true,
|
|
261
278
|
preserveOwner: false,
|
|
279
|
+
noMtime: true,
|
|
262
280
|
strict: true,
|
|
263
|
-
maxMetaEntrySize:
|
|
281
|
+
maxMetaEntrySize: tarLimits.maxMetaEntryBytes,
|
|
282
|
+
maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
|
|
264
283
|
filter(_entryPath, entry) {
|
|
265
284
|
try {
|
|
266
285
|
const info = readTarEntryInfo(entry);
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
const relPath = stripArchivePath(info.path, Math.max(0, Math.floor(params.stripComponents ?? 0)));
|
|
271
|
-
if (relPath) {
|
|
272
|
-
acceptedEntries.push({
|
|
273
|
-
path: relPath,
|
|
274
|
-
mode: resolveArchiveEntryMode({
|
|
275
|
-
kind: info.type === "Directory" || info.type === "GNUDumpDir"
|
|
276
|
-
? "directory"
|
|
277
|
-
: "file",
|
|
278
|
-
archivedMode: info.mode,
|
|
279
|
-
policy: params.entryModes,
|
|
280
|
-
}),
|
|
281
|
-
});
|
|
282
|
-
}
|
|
286
|
+
const relPath = admission.consume(info);
|
|
287
|
+
if (!relPath) {
|
|
288
|
+
return false;
|
|
283
289
|
}
|
|
284
|
-
|
|
290
|
+
// Writes and the mode pass use the admitted output identity.
|
|
291
|
+
entry.path = relPath;
|
|
292
|
+
acceptedEntries.push({
|
|
293
|
+
path: relPath,
|
|
294
|
+
mode: resolveArchiveEntryMode({
|
|
295
|
+
kind: info.type === "Directory" || info.type === "GNUDumpDir"
|
|
296
|
+
? "directory"
|
|
297
|
+
: "file",
|
|
298
|
+
archivedMode: info.mode,
|
|
299
|
+
policy: params.entryModes,
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
return true;
|
|
285
303
|
}
|
|
286
304
|
catch (error) {
|
|
287
305
|
// Abort through the parser so pipeline tears down both the
|
|
@@ -297,8 +315,7 @@ export async function extractArchive(params) {
|
|
|
297
315
|
}
|
|
298
316
|
catch (err) {
|
|
299
317
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
300
|
-
//
|
|
301
|
-
// emitter (tar.Unpack), which exposes Parser.abort().
|
|
318
|
+
// EventEmitter binds `this` to tar.Unpack, exposing abort().
|
|
302
319
|
const emitter = this;
|
|
303
320
|
emitter.abort?.(error);
|
|
304
321
|
}
|
|
@@ -312,19 +329,23 @@ export async function extractArchive(params) {
|
|
|
312
329
|
catch (error) {
|
|
313
330
|
throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
|
|
314
331
|
}
|
|
332
|
+
admission.finish();
|
|
315
333
|
for (const accepted of acceptedEntries) {
|
|
334
|
+
deadline.check();
|
|
316
335
|
const outputPath = resolveArchiveOutputPath({
|
|
317
336
|
rootDir: stagingDir,
|
|
318
337
|
relPath: accepted.path,
|
|
319
338
|
originalPath: accepted.path,
|
|
320
339
|
});
|
|
321
340
|
await fs.chmod(outputPath, accepted.mode);
|
|
341
|
+
deadline.check();
|
|
322
342
|
}
|
|
323
343
|
deadline.check();
|
|
324
344
|
await mergeExtractedTreeIntoDestination({
|
|
325
345
|
sourceDir: stagingDir,
|
|
326
346
|
destinationDir: params.destDir,
|
|
327
347
|
destinationRealDir,
|
|
348
|
+
deadline,
|
|
328
349
|
});
|
|
329
350
|
deadline.check();
|
|
330
351
|
},
|
|
@@ -340,10 +361,10 @@ export async function extractArchive(params) {
|
|
|
340
361
|
archivePath: params.archivePath,
|
|
341
362
|
destDir: params.destDir,
|
|
342
363
|
stripComponents: params.stripComponents,
|
|
343
|
-
limits
|
|
364
|
+
limits,
|
|
344
365
|
deadline,
|
|
345
366
|
entryModes: params.entryModes,
|
|
346
367
|
entryFilter: params.entryFilter,
|
|
347
|
-
onFiltered
|
|
368
|
+
onFiltered,
|
|
348
369
|
}));
|
|
349
370
|
}
|
package/dist/atomic.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { replaceFileAtomic, replaceFileAtomicSync, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
|
|
1
|
+
export { replaceFileAtomic, replaceFileAtomicSync, type RenameIdentityPolicy, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
|
|
2
2
|
export { writeTextAtomic, type WriteTextAtomicOptions } from "./text-atomic.js";
|
|
3
3
|
export { replaceDirectoryAtomic, type ReplaceDirectoryAtomicOptions } from "./replace-directory.js";
|
|
4
4
|
export { movePathWithCopyFallback, type MovePathWithCopyFallbackOptions } from "./move-path.js";
|
package/dist/atomic.d.ts.map
CHANGED
|
@@ -1 +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,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,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"}
|
|
1
|
+
{"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAIlD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAmBnE;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,MAAM,EAAE;QAAE,gBAAgB,IAAI,MAAM,CAAC,cAAc,CAAA;KAAE,CAAA;CAAE,EACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAAC,cAAc,CAWvB"}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { pipeline, Transform } from "node:stream";
|
|
2
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
3
|
import { FsSafeError } from "./errors.js";
|
|
3
4
|
export function createMaxBytesTransform(maxBytes) {
|
|
4
|
-
|
|
5
|
-
(!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
|
|
6
|
-
throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
|
|
7
|
-
}
|
|
5
|
+
normalizeMaxBytes(maxBytes);
|
|
8
6
|
let bytes = 0;
|
|
9
7
|
return new Transform({
|
|
10
8
|
transform(chunk, _encoding, callback) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAmDnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB;AAcD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}
|
package/dist/bounded-read.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
3
|
import { FsSafeError } from "./errors.js";
|
|
3
4
|
const READ_CHUNK_BYTES = 64 * 1024;
|
|
4
|
-
function assertMaxBytes(maxBytes) {
|
|
5
|
-
if (maxBytes === Number.POSITIVE_INFINITY) {
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
9
|
-
throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
5
|
function createScratchBuffer(maxBytes) {
|
|
13
6
|
const initialReadBytes = Number.isFinite(maxBytes)
|
|
14
7
|
? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
|
|
@@ -29,7 +22,7 @@ function appendChunk(params) {
|
|
|
29
22
|
return total;
|
|
30
23
|
}
|
|
31
24
|
async function readBoundedAsync(maxBytes, readChunk) {
|
|
32
|
-
|
|
25
|
+
normalizeMaxBytes(maxBytes);
|
|
33
26
|
const chunks = [];
|
|
34
27
|
const scratch = createScratchBuffer(maxBytes);
|
|
35
28
|
let total = 0;
|
|
@@ -71,7 +64,7 @@ export async function readFileDescriptorBounded(fd, maxBytes) {
|
|
|
71
64
|
}
|
|
72
65
|
/** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
|
|
73
66
|
export function readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
74
|
-
|
|
67
|
+
normalizeMaxBytes(maxBytes);
|
|
75
68
|
const chunks = [];
|
|
76
69
|
const scratch = createScratchBuffer(maxBytes);
|
|
77
70
|
let total = 0;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byte-budget.d.ts","sourceRoot":"","sources":["../src/byte-budget.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACtD,MAAM,GAAG,SAAS,CASpB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function normalizeMaxBytes(value, options = {}) {
|
|
2
|
+
const selected = value === undefined ? options.defaultValue : value;
|
|
3
|
+
if (selected === undefined || selected === Number.POSITIVE_INFINITY)
|
|
4
|
+
return selected;
|
|
5
|
+
if (!Number.isSafeInteger(selected) || selected < 0) {
|
|
6
|
+
throw new RangeError(`${options.label ?? "maxBytes"} must be a non-negative safe integer or Infinity`);
|
|
7
|
+
}
|
|
8
|
+
return selected;
|
|
9
|
+
}
|
package/dist/file-hash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAInE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,GACrD,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;AA+CD,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAElF"}
|
package/dist/file-hash.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
5
4
|
import { getNativeBinding } from "./native.js";
|
|
6
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
7
7
|
export async function hashFileHandle(handle, native = getNativeBinding()) {
|
|
8
8
|
const stat = await handle.stat();
|
|
9
9
|
if (!stat.isFile()) {
|
|
@@ -25,13 +25,16 @@ export async function hashFileHandle(handle, native = getNativeBinding()) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
async function hashPath(filePath) {
|
|
28
|
-
const before = await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
const before = await inspectFileIdentity(async () => {
|
|
29
|
+
const stat = await fs.lstat(filePath, { bigint: true });
|
|
30
|
+
if (stat.isSymbolicLink()) {
|
|
31
|
+
throw new FsSafeError("symlink", "SHA-256 path must not be a symbolic link");
|
|
32
|
+
}
|
|
33
|
+
if (!stat.isFile()) {
|
|
34
|
+
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
35
|
+
}
|
|
36
|
+
return stat;
|
|
37
|
+
});
|
|
35
38
|
let handle;
|
|
36
39
|
try {
|
|
37
40
|
handle = await fs.open(filePath, resolveReadOpenFlags());
|
|
@@ -45,17 +48,20 @@ async function hashPath(filePath) {
|
|
|
45
48
|
throw error;
|
|
46
49
|
}
|
|
47
50
|
try {
|
|
48
|
-
const opened = await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
const opened = await inspectFileIdentity(async () => {
|
|
52
|
+
const stat = await handle.stat({ bigint: true });
|
|
53
|
+
if (!stat.isFile()) {
|
|
54
|
+
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
55
|
+
}
|
|
56
|
+
return stat;
|
|
57
|
+
}, before);
|
|
58
|
+
await inspectFileIdentity(async () => {
|
|
59
|
+
const stat = await fs.lstat(filePath, { bigint: true });
|
|
60
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
61
|
+
throw new FsSafeError("path-mismatch", "SHA-256 path changed while opening");
|
|
62
|
+
}
|
|
63
|
+
return stat;
|
|
64
|
+
}, opened);
|
|
59
65
|
return await hashFileHandle(handle);
|
|
60
66
|
}
|
|
61
67
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AAOnC,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,MAAM,0BAA0B,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAChC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,QAAQ,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,KAAK,CAAC;KAC1B,KAAK,OAAO,CAAC;IACd,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,CAAC;IACxE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IAC3D,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,IAAI,OAAO,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA0IF,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,GAC5C,kBAAkB,CAwKpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,EAC7C,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAOH"}
|
package/dist/file-lock-sync.js
CHANGED
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { readSidecarLockSnapshotSync, relativeSidecarLockPath, removeSidecarLockIfUnchangedSync, serializeSidecarLockPayload, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
5
|
-
import { computeSidecarLockDelayMs, sidecarLockPayloadCreatedAtMs, } from "./sidecar-lock-policy.js";
|
|
5
|
+
import { computeSidecarLockDelayMs, sidecarLockPayloadCreatedAtMs, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
6
|
+
import { getFsSafeLockConfig } from "./lock-config.js";
|
|
6
7
|
import { sleepSync } from "./timing.js";
|
|
7
8
|
const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
8
9
|
const SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
|
|
@@ -126,6 +127,11 @@ function reclaimGuardExists(reclaimGuardPath) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
export function acquireFileLockSync(targetPath, options) {
|
|
130
|
+
const defaults = getFsSafeLockConfig();
|
|
131
|
+
const retry = options.retry ?? defaults.retry ?? {};
|
|
132
|
+
const timeoutMs = options.timeoutMs ?? defaults.timeoutMs;
|
|
133
|
+
validateSidecarLockRetryOptions(retry);
|
|
134
|
+
validateSidecarLockTimeoutMs(timeoutMs);
|
|
129
135
|
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
130
136
|
const lockPath = boundedLockPath(options.lockPath ?? `${normalizedTargetPath}.lock`, options.lockRoot);
|
|
131
137
|
const heldLocks = getSyncHeldLocks();
|
|
@@ -137,14 +143,16 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
137
143
|
held.refCount += 1;
|
|
138
144
|
return createSyncHeldLockHandle(held);
|
|
139
145
|
}
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
// Process defaults fill the same fields here as withLockDefaults() fills for
|
|
147
|
+
// the asynchronous manager, so both acquirers honor configureFsSafeLocks().
|
|
148
|
+
const staleMs = options.staleMs ?? defaults.staleMs ?? 30_000;
|
|
149
|
+
const staleRecovery = options.staleRecovery ?? defaults.staleRecovery;
|
|
142
150
|
const startedAt = Date.now();
|
|
143
151
|
let attempt = 0;
|
|
144
152
|
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
145
153
|
const waitForRetry = () => {
|
|
146
154
|
const elapsed = Date.now() - startedAt;
|
|
147
|
-
const timedOut =
|
|
155
|
+
const timedOut = timeoutMs !== undefined && elapsed >= timeoutMs;
|
|
148
156
|
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
149
157
|
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
150
158
|
code: "file_lock_timeout",
|
|
@@ -152,7 +160,10 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
152
160
|
normalizedTargetPath,
|
|
153
161
|
});
|
|
154
162
|
}
|
|
155
|
-
|
|
163
|
+
const remaining = timeoutMs === undefined || timeoutMs === Number.POSITIVE_INFINITY
|
|
164
|
+
? Number.POSITIVE_INFINITY
|
|
165
|
+
: Math.max(0, timeoutMs - elapsed);
|
|
166
|
+
sleepSync(Math.min(computeSidecarLockDelayMs(retry, attempt), remaining));
|
|
156
167
|
attempt += 1;
|
|
157
168
|
};
|
|
158
169
|
while (true) {
|
|
@@ -190,9 +201,15 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
190
201
|
const returnedHandle = createSyncHeldLockHandle(createdHeld);
|
|
191
202
|
if (options.onCompromised && (options.compromiseCheckIntervalMs ?? 0) > 0) {
|
|
192
203
|
createdHeld.timer = setInterval(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
let stillHeld;
|
|
205
|
+
try {
|
|
206
|
+
stillHeld = returnedHandle.verifyStillHeld();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
stillHeld = false;
|
|
210
|
+
}
|
|
211
|
+
if (!stillHeld && createdHeld.timer) {
|
|
212
|
+
clearInterval(createdHeld.timer);
|
|
196
213
|
createdHeld.timer = undefined;
|
|
197
214
|
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
198
215
|
}
|
|
@@ -232,7 +249,7 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
232
249
|
})
|
|
233
250
|
: defaultShouldReclaim(snapshot.payload, lockPath, staleMs, nowMs);
|
|
234
251
|
if (reclaim) {
|
|
235
|
-
if (
|
|
252
|
+
if (staleRecovery === "remove-if-unchanged" &&
|
|
236
253
|
snapshot.raw !== undefined &&
|
|
237
254
|
options.shouldRemoveStaleLock?.({
|
|
238
255
|
lockPath,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhB;AAgDD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAwC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAOlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC;CAC1C,GAAG,eAAe,CAyDlB"}
|
|
@@ -2,6 +2,7 @@ import syncFs from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Transform } from "node:stream";
|
|
5
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
5
6
|
import { pipeline } from "node:stream/promises";
|
|
6
7
|
import { assertSyncDirectoryGuard as assertDirectoryGuardSync, createSyncDirectoryGuard, } from "./directory-guard.js";
|
|
7
8
|
import { FsSafeError } from "./errors.js";
|
|
@@ -22,9 +23,10 @@ export async function ensureParentInRoot(scopedRoot, relativePath, mode) {
|
|
|
22
23
|
await chmodDirectoryInRootBestEffort(scopedRoot, parent, mode).catch(() => undefined);
|
|
23
24
|
}
|
|
24
25
|
export async function openWritableStoreRoot(params) {
|
|
26
|
+
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
25
27
|
await fs.mkdir(params.rootDir, { recursive: true, mode: params.dirMode });
|
|
26
28
|
await fs.chmod(params.rootDir, params.dirMode).catch(() => undefined);
|
|
27
|
-
return await root(params.rootDir, { hardlinks: "reject", maxBytes
|
|
29
|
+
return await root(params.rootDir, { hardlinks: "reject", maxBytes });
|
|
28
30
|
}
|
|
29
31
|
async function chmodDirectoryInRootBestEffort(scopedRoot, relativePath, mode) {
|
|
30
32
|
const dirPath = await scopedRoot.resolve(relativePath);
|
|
@@ -49,7 +51,8 @@ async function chmodDirectoryInRootBestEffort(scopedRoot, relativePath, mode) {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
function createMaxBytesTransform(maxBytes) {
|
|
52
|
-
|
|
54
|
+
const limit = normalizeMaxBytes(maxBytes);
|
|
55
|
+
if (limit === undefined) {
|
|
53
56
|
return undefined;
|
|
54
57
|
}
|
|
55
58
|
let total = 0;
|
|
@@ -57,8 +60,8 @@ function createMaxBytesTransform(maxBytes) {
|
|
|
57
60
|
transform(chunk, _encoding, callback) {
|
|
58
61
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
59
62
|
total += buffer.byteLength;
|
|
60
|
-
if (total >
|
|
61
|
-
callback(new FsSafeError("too-large", `file exceeds maximum size of ${
|
|
63
|
+
if (total > limit) {
|
|
64
|
+
callback(new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`));
|
|
62
65
|
return;
|
|
63
66
|
}
|
|
64
67
|
callback(null, buffer);
|
|
@@ -66,6 +69,7 @@ function createMaxBytesTransform(maxBytes) {
|
|
|
66
69
|
});
|
|
67
70
|
}
|
|
68
71
|
export async function writeStreamToTempSource(params) {
|
|
72
|
+
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
69
73
|
const tempRoot = resolveSecureTempRoot({
|
|
70
74
|
fallbackPrefix: "fs-safe-file-store",
|
|
71
75
|
unsafeFallbackLabel: "file store temp dir",
|
|
@@ -81,7 +85,7 @@ export async function writeStreamToTempSource(params) {
|
|
|
81
85
|
writable.once("close", () => {
|
|
82
86
|
handleClosedByStream = true;
|
|
83
87
|
});
|
|
84
|
-
const limiter = createMaxBytesTransform(
|
|
88
|
+
const limiter = createMaxBytesTransform(maxBytes);
|
|
85
89
|
if (limiter) {
|
|
86
90
|
await pipeline(params.stream, limiter, writable);
|
|
87
91
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store-limit.d.ts","sourceRoot":"","sources":["../src/file-store-limit.ts"],"names":[],"mappings":"AAGA,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAK7E"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
export function assertFileStoreMaxBytes(size, maxBytes) {
|
|
4
|
+
const limit = normalizeMaxBytes(maxBytes);
|
|
5
|
+
if (limit !== undefined && size > limit) {
|
|
6
|
+
throw new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAwBA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CA6GT"}
|