@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/regular-file.js
CHANGED
|
@@ -2,12 +2,14 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { readFileDescriptorBoundedSync, readFileHandleBounded } from "./bounded-read.js";
|
|
5
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
5
6
|
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
6
7
|
import { FsSafeError } from "./errors.js";
|
|
7
|
-
import {
|
|
8
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
8
9
|
import { isNotFoundPathError } from "./path.js";
|
|
9
10
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
10
11
|
import { assertNoSymlinkParents, assertNoSymlinkParentsSync } from "./symlink-parents.js";
|
|
12
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
11
13
|
export function resolveRegularFileAppendFlags(constants = fsSync.constants) {
|
|
12
14
|
const noFollow = constants.O_NOFOLLOW;
|
|
13
15
|
return (constants.O_CREAT |
|
|
@@ -57,13 +59,15 @@ export function statRegularFileSync(filePath) {
|
|
|
57
59
|
return { missing: false, stat };
|
|
58
60
|
}
|
|
59
61
|
export async function readRegularFile(params) {
|
|
62
|
+
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
60
63
|
assertNoUnsafeDeviceReadPath(params.filePath);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const before = await inspectFileIdentity(async () => {
|
|
65
|
+
const stat = await fs.lstat(params.filePath, { bigint: true });
|
|
66
|
+
assertRegularReadStat(stat, params.filePath, true);
|
|
67
|
+
return stat;
|
|
68
|
+
}).catch((error) => throwReadPreviewError(error, params.filePath));
|
|
69
|
+
if (maxBytes !== undefined && before.size > maxBytes) {
|
|
70
|
+
throw regularFileTooLargeError(params.filePath, maxBytes);
|
|
67
71
|
}
|
|
68
72
|
let handle;
|
|
69
73
|
try {
|
|
@@ -77,9 +81,17 @@ export async function readRegularFile(params) {
|
|
|
77
81
|
}
|
|
78
82
|
try {
|
|
79
83
|
const stat = await handle.stat();
|
|
80
|
-
|
|
84
|
+
const identity = await inspectFileIdentity(async () => {
|
|
85
|
+
const exact = await handle.stat({ bigint: true });
|
|
86
|
+
assertRegularReadStat(exact, params.filePath);
|
|
87
|
+
return exact;
|
|
88
|
+
}, before);
|
|
81
89
|
try {
|
|
82
|
-
|
|
90
|
+
await inspectFileIdentity(async () => {
|
|
91
|
+
const current = await fs.lstat(params.filePath, { bigint: true });
|
|
92
|
+
assertRegularReadStat(current, params.filePath);
|
|
93
|
+
return current;
|
|
94
|
+
}, identity);
|
|
83
95
|
}
|
|
84
96
|
catch (err) {
|
|
85
97
|
if (isNotFoundPathError(err)) {
|
|
@@ -87,27 +99,21 @@ export async function readRegularFile(params) {
|
|
|
87
99
|
}
|
|
88
100
|
throw err;
|
|
89
101
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
pathStat,
|
|
93
|
-
postOpenStat: stat,
|
|
94
|
-
preOpenStat: result.stat,
|
|
95
|
-
});
|
|
96
|
-
if (params.maxBytes !== undefined && stat.size > params.maxBytes) {
|
|
97
|
-
throw regularFileTooLargeError(params.filePath, params.maxBytes);
|
|
102
|
+
if (maxBytes !== undefined && stat.size > maxBytes) {
|
|
103
|
+
throw regularFileTooLargeError(params.filePath, maxBytes);
|
|
98
104
|
}
|
|
99
105
|
// With a byte cap, avoid readFile(): a raced file growth would allocate
|
|
100
106
|
// the oversized content before the post-read check could reject it.
|
|
101
107
|
let buffer;
|
|
102
108
|
try {
|
|
103
109
|
buffer =
|
|
104
|
-
|
|
110
|
+
maxBytes === undefined
|
|
105
111
|
? await handle.readFile()
|
|
106
|
-
: await readFileHandleBounded(handle,
|
|
112
|
+
: await readFileHandleBounded(handle, maxBytes);
|
|
107
113
|
}
|
|
108
114
|
catch (error) {
|
|
109
|
-
if (
|
|
110
|
-
translateBoundedReadOverflow(error, params.filePath,
|
|
115
|
+
if (maxBytes !== undefined) {
|
|
116
|
+
translateBoundedReadOverflow(error, params.filePath, maxBytes);
|
|
111
117
|
}
|
|
112
118
|
throw error;
|
|
113
119
|
}
|
|
@@ -117,20 +123,30 @@ export async function readRegularFile(params) {
|
|
|
117
123
|
await handle.close();
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
|
-
function
|
|
121
|
-
if (
|
|
122
|
-
throw new Error(`File
|
|
126
|
+
function throwReadPreviewError(error, filePath) {
|
|
127
|
+
if (isNotFoundPathError(error)) {
|
|
128
|
+
throw Object.assign(new Error(`File not found: ${filePath}`), { code: "ENOENT" });
|
|
123
129
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
function assertRegularReadStat(stat, filePath, preview = false) {
|
|
133
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
134
|
+
throw new Error(preview ? "path must be a regular file" : `File is not a regular file: ${filePath}`);
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
function readOpenedRegularFileSync(params) {
|
|
130
138
|
const stat = fsSync.fstatSync(params.fd);
|
|
131
|
-
|
|
139
|
+
const identity = inspectFileIdentitySync(() => {
|
|
140
|
+
const exact = fsSync.fstatSync(params.fd, { bigint: true });
|
|
141
|
+
assertRegularReadStat(exact, params.filePath);
|
|
142
|
+
return exact;
|
|
143
|
+
}, params.preOpenStat);
|
|
132
144
|
try {
|
|
133
|
-
|
|
145
|
+
inspectFileIdentitySync(() => {
|
|
146
|
+
const current = fsSync.lstatSync(params.filePath, { bigint: true });
|
|
147
|
+
assertRegularReadStat(current, params.filePath);
|
|
148
|
+
return current;
|
|
149
|
+
}, identity);
|
|
134
150
|
}
|
|
135
151
|
catch (error) {
|
|
136
152
|
if (isNotFoundPathError(error)) {
|
|
@@ -138,12 +154,6 @@ function readOpenedRegularFileSync(params) {
|
|
|
138
154
|
}
|
|
139
155
|
throw error;
|
|
140
156
|
}
|
|
141
|
-
verifyStableReadTarget({
|
|
142
|
-
filePath: params.filePath,
|
|
143
|
-
pathStat,
|
|
144
|
-
postOpenStat: stat,
|
|
145
|
-
preOpenStat: params.preOpenStat,
|
|
146
|
-
});
|
|
147
157
|
if (params.maxBytes !== undefined && stat.size > params.maxBytes) {
|
|
148
158
|
throw regularFileTooLargeError(params.filePath, params.maxBytes);
|
|
149
159
|
}
|
|
@@ -165,13 +175,21 @@ function readOpenedRegularFileSync(params) {
|
|
|
165
175
|
return { buffer, stat };
|
|
166
176
|
}
|
|
167
177
|
export function readRegularFileSync(params) {
|
|
178
|
+
const maxBytes = normalizeMaxBytes(params.maxBytes);
|
|
168
179
|
assertNoUnsafeDeviceReadPath(params.filePath);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
let before;
|
|
181
|
+
try {
|
|
182
|
+
before = inspectFileIdentitySync(() => {
|
|
183
|
+
const stat = fsSync.lstatSync(params.filePath, { bigint: true });
|
|
184
|
+
assertRegularReadStat(stat, params.filePath, true);
|
|
185
|
+
return stat;
|
|
186
|
+
});
|
|
172
187
|
}
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
catch (error) {
|
|
189
|
+
throwReadPreviewError(error, params.filePath);
|
|
190
|
+
}
|
|
191
|
+
if (maxBytes !== undefined && before.size > maxBytes) {
|
|
192
|
+
throw regularFileTooLargeError(params.filePath, maxBytes);
|
|
175
193
|
}
|
|
176
194
|
let fd;
|
|
177
195
|
try {
|
|
@@ -187,25 +205,28 @@ export function readRegularFileSync(params) {
|
|
|
187
205
|
return readOpenedRegularFileSync({
|
|
188
206
|
fd,
|
|
189
207
|
filePath: params.filePath,
|
|
190
|
-
preOpenStat:
|
|
191
|
-
maxBytes
|
|
208
|
+
preOpenStat: before,
|
|
209
|
+
maxBytes,
|
|
192
210
|
});
|
|
193
211
|
}
|
|
194
212
|
finally {
|
|
195
213
|
fsSync.closeSync(fd);
|
|
196
214
|
}
|
|
197
215
|
}
|
|
198
|
-
function
|
|
199
|
-
if (!
|
|
200
|
-
throw new Error(`Refusing to append to non-file: ${
|
|
216
|
+
function assertRegularAppendStat(stat, filePath) {
|
|
217
|
+
if (!stat.isFile()) {
|
|
218
|
+
throw new Error(`Refusing to append to non-file: ${filePath}`);
|
|
201
219
|
}
|
|
202
|
-
if (
|
|
203
|
-
throw new Error(`Refusing to append to hardlinked file: ${
|
|
220
|
+
if (stat.nlink > 1n) {
|
|
221
|
+
throw new Error(`Refusing to append to hardlinked file: ${filePath}`);
|
|
204
222
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
223
|
+
}
|
|
224
|
+
function throwAppendIdentityError(error, filePath) {
|
|
225
|
+
if (isNotFoundPathError(error) ||
|
|
226
|
+
(error instanceof FsSafeError && error.code === "path-mismatch")) {
|
|
227
|
+
throw new Error(`Refusing to append after file changed: ${filePath}`, { cause: error });
|
|
208
228
|
}
|
|
229
|
+
throw error;
|
|
209
230
|
}
|
|
210
231
|
export async function appendRegularFile(options) {
|
|
211
232
|
if (options.rejectSymlinkParents === true) {
|
|
@@ -221,32 +242,49 @@ export async function appendRegularFile(options) {
|
|
|
221
242
|
}
|
|
222
243
|
let preOpenStat;
|
|
223
244
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
245
|
+
preOpenStat = await inspectFileIdentity(async () => {
|
|
246
|
+
const stat = await fs.lstat(options.filePath, { bigint: true });
|
|
247
|
+
if (stat.isSymbolicLink()) {
|
|
248
|
+
throw new Error(`Refusing to append through symlink: ${options.filePath}`);
|
|
249
|
+
}
|
|
250
|
+
if (!stat.isFile()) {
|
|
251
|
+
throw new Error(`Refusing to append to non-file: ${options.filePath}`);
|
|
252
|
+
}
|
|
253
|
+
return stat;
|
|
254
|
+
});
|
|
232
255
|
}
|
|
233
|
-
catch (
|
|
234
|
-
if (!isNotFoundPathError(
|
|
235
|
-
throw
|
|
236
|
-
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (!isNotFoundPathError(error))
|
|
258
|
+
throw error;
|
|
237
259
|
}
|
|
238
260
|
const contentBytes = Buffer.isBuffer(options.content)
|
|
239
261
|
? options.content.byteLength
|
|
240
262
|
: Buffer.byteLength(options.content, options.encoding ?? "utf8");
|
|
241
263
|
if (options.maxFileBytes !== undefined &&
|
|
242
|
-
(preOpenStat?.size ??
|
|
264
|
+
Number(preOpenStat?.size ?? 0n) + contentBytes > options.maxFileBytes) {
|
|
243
265
|
return;
|
|
244
266
|
}
|
|
267
|
+
await getFsSafeTestHooks()?.beforeRegularFileAppendOpen?.(options.filePath);
|
|
245
268
|
const handle = await fs.open(options.filePath, resolveRegularFileAppendFlags(), options.mode ?? 0o600);
|
|
246
269
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
let identity;
|
|
271
|
+
try {
|
|
272
|
+
identity = await inspectFileIdentity(async () => {
|
|
273
|
+
const stat = await handle.stat({ bigint: true });
|
|
274
|
+
assertRegularAppendStat(stat, options.filePath);
|
|
275
|
+
return stat;
|
|
276
|
+
}, preOpenStat);
|
|
277
|
+
await inspectFileIdentity(async () => {
|
|
278
|
+
const current = await fs.lstat(options.filePath, { bigint: true });
|
|
279
|
+
assertRegularAppendStat(current, options.filePath);
|
|
280
|
+
return current;
|
|
281
|
+
}, identity);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
throwAppendIdentityError(error, options.filePath);
|
|
285
|
+
}
|
|
286
|
+
if (options.maxFileBytes !== undefined &&
|
|
287
|
+
Number(identity.size) + contentBytes > options.maxFileBytes) {
|
|
250
288
|
return;
|
|
251
289
|
}
|
|
252
290
|
await handle.chmod(options.mode ?? 0o600);
|
|
@@ -270,33 +308,49 @@ export function appendRegularFileSync(options) {
|
|
|
270
308
|
}
|
|
271
309
|
let preOpenStat;
|
|
272
310
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
preOpenStat = inspectFileIdentitySync(() => {
|
|
312
|
+
const stat = fsSync.lstatSync(options.filePath, { bigint: true });
|
|
313
|
+
if (stat.isSymbolicLink()) {
|
|
314
|
+
throw new Error(`Refusing to append through symlink: ${options.filePath}`);
|
|
315
|
+
}
|
|
316
|
+
if (!stat.isFile()) {
|
|
317
|
+
throw new Error(`Refusing to append to non-file: ${options.filePath}`);
|
|
318
|
+
}
|
|
319
|
+
return stat;
|
|
320
|
+
});
|
|
281
321
|
}
|
|
282
|
-
catch (
|
|
283
|
-
if (!isNotFoundPathError(
|
|
284
|
-
throw
|
|
285
|
-
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (!isNotFoundPathError(error))
|
|
324
|
+
throw error;
|
|
286
325
|
}
|
|
287
326
|
const contentBuffer = typeof options.content === "string"
|
|
288
327
|
? Buffer.from(options.content, options.encoding ?? "utf8")
|
|
289
328
|
: Buffer.from(options.content);
|
|
290
329
|
if (options.maxFileBytes !== undefined &&
|
|
291
|
-
(preOpenStat?.size ??
|
|
330
|
+
Number(preOpenStat?.size ?? 0n) + contentBuffer.byteLength > options.maxFileBytes) {
|
|
292
331
|
return;
|
|
293
332
|
}
|
|
333
|
+
getFsSafeTestHooks()?.beforeRegularFileAppendOpenSync?.(options.filePath);
|
|
294
334
|
const fd = fsSync.openSync(options.filePath, resolveRegularFileAppendFlags(), options.mode ?? 0o600);
|
|
295
335
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
336
|
+
let identity;
|
|
337
|
+
try {
|
|
338
|
+
identity = inspectFileIdentitySync(() => {
|
|
339
|
+
const stat = fsSync.fstatSync(fd, { bigint: true });
|
|
340
|
+
assertRegularAppendStat(stat, options.filePath);
|
|
341
|
+
return stat;
|
|
342
|
+
}, preOpenStat);
|
|
343
|
+
inspectFileIdentitySync(() => {
|
|
344
|
+
const current = fsSync.lstatSync(options.filePath, { bigint: true });
|
|
345
|
+
assertRegularAppendStat(current, options.filePath);
|
|
346
|
+
return current;
|
|
347
|
+
}, identity);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
throwAppendIdentityError(error, options.filePath);
|
|
351
|
+
}
|
|
298
352
|
if (options.maxFileBytes !== undefined &&
|
|
299
|
-
|
|
353
|
+
Number(identity.size) + contentBuffer.byteLength > options.maxFileBytes) {
|
|
300
354
|
return;
|
|
301
355
|
}
|
|
302
356
|
fsSync.fchmodSync(fd, options.mode ?? 0o600);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import syncFs from "node:fs";
|
|
1
|
+
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
2
|
export type ReplaceFileDestinationHardlinkPolicy = "reject";
|
|
3
3
|
export type ReplaceFileCopyFallbackRestorePolicy = "restore-original" | "none";
|
|
4
4
|
export type ReplaceFileAtomicRestoreCleanup = "restored" | "restore-failed";
|
|
@@ -21,6 +21,7 @@ export declare function copyFallbackReplace(params: {
|
|
|
21
21
|
destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
|
|
22
22
|
restore: ReplaceFileCopyFallbackRestorePolicy;
|
|
23
23
|
maxRestoreBytes?: number;
|
|
24
|
+
expectedSourceIdentity?: BigIntStats;
|
|
24
25
|
sync: boolean;
|
|
25
26
|
}): Promise<void>;
|
|
26
27
|
export declare function copyFallbackReplaceSync(params: {
|
|
@@ -30,6 +31,7 @@ export declare function copyFallbackReplaceSync(params: {
|
|
|
30
31
|
destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
|
|
31
32
|
restore: ReplaceFileCopyFallbackRestorePolicy;
|
|
32
33
|
maxRestoreBytes?: number;
|
|
34
|
+
expectedSourceIdentity?: BigIntStats;
|
|
33
35
|
fchmodSync?: (fd: number, mode: number) => void;
|
|
34
36
|
sync: boolean;
|
|
35
37
|
}): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAO/D,MAAM,MAAM,oCAAoC,GAAG,QAAQ,CAAC;AAC5D,MAAM,MAAM,oCAAoC,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAC/E,MAAM,MAAM,+BAA+B,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAC5E,MAAM,MAAM,sCAAsC,GAAG;IACnD,OAAO,EAAE,+BAA+B,CAAC;CAC1C,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,kBAAkB,EAAE,KAAK,CAAC;IAC/C,IAAI,EAAE,cAAc,kBAAkB,EAAE,IAAI,CAAC;IAC7C,EAAE,EAAE,cAAc,kBAAkB,EAAE,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,kBAAkB,EAAE,MAAM,CAAC;CAClD,CAAC;AAEF,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,WAAW,GACX,UAAU,GACV,UAAU,GACV,QAAQ,GACR,YAAY,GACZ,WAAW,CACd,CAAC;AAuFF,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,IAAI,CAwBN;AAmID,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDhB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAkEP"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import syncFs, {} from "node:fs";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
3
|
import { sameFileIdentity } from "./file-identity.js";
|
|
4
|
+
import { readOwnedCopySource, readOwnedCopySourceSync } from "./replace-file-copy-source.js";
|
|
5
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
4
6
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants;
|
|
5
7
|
const NOFOLLOW = SUPPORTS_NOFOLLOW ? syncFs.constants.O_NOFOLLOW : 0;
|
|
6
|
-
const OPEN_READ_FLAGS =
|
|
8
|
+
const OPEN_READ_FLAGS = resolveReadOpenFlags();
|
|
7
9
|
const OPEN_READ_WRITE_FLAGS = syncFs.constants.O_RDWR | NOFOLLOW;
|
|
8
10
|
const OPEN_WRITE_EXCLUSIVE_FLAGS = syncFs.constants.O_WRONLY | syncFs.constants.O_CREAT | syncFs.constants.O_EXCL | NOFOLLOW;
|
|
9
11
|
const READ_CHUNK_BYTES = 64 * 1024;
|
|
@@ -236,24 +238,19 @@ function replacePinnedWithRestoreSync(fsModule, fd, replacement, maxRestoreBytes
|
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
export async function copyFallbackReplace(params) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
const source = await readOwnedCopySource({
|
|
242
|
+
fsModule: params.fsModule,
|
|
243
|
+
src: params.src,
|
|
244
|
+
expectedIdentity: params.expectedSourceIdentity,
|
|
245
|
+
});
|
|
246
|
+
const { replacement } = source;
|
|
244
247
|
let destHandle = null;
|
|
245
248
|
try {
|
|
246
|
-
const sourceStat = await sourceHandle.stat();
|
|
247
|
-
const sourceCurrent = await params.fsModule.lstat(params.src);
|
|
248
|
-
if (!sourceStat.isFile() || sourceCurrent.isSymbolicLink() || !sameFileIdentity(sourceStat, sourceCurrent)) {
|
|
249
|
-
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
250
|
-
}
|
|
251
|
-
const replacement = await sourceHandle.readFile();
|
|
252
249
|
if (params.restore === "restore-original") {
|
|
253
250
|
const pinned = await openPinnedDestination(params.fsModule, params.dest, params.destinationHardlinks);
|
|
254
251
|
if (pinned) {
|
|
255
252
|
destHandle = pinned.handle;
|
|
256
|
-
await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes,
|
|
253
|
+
await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes, source.mode);
|
|
257
254
|
}
|
|
258
255
|
}
|
|
259
256
|
if (!destHandle) {
|
|
@@ -269,9 +266,9 @@ export async function copyFallbackReplace(params) {
|
|
|
269
266
|
await assertDestinationHardlinkPolicy(params.fsModule, params.dest, params.destinationHardlinks);
|
|
270
267
|
await params.fsModule.rm(params.dest, { force: true });
|
|
271
268
|
}
|
|
272
|
-
destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS,
|
|
269
|
+
destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, source.mode & 0o777);
|
|
273
270
|
await destHandle.writeFile(replacement);
|
|
274
|
-
await destHandle.chmod(
|
|
271
|
+
await destHandle.chmod(source.mode);
|
|
275
272
|
if (params.sync) {
|
|
276
273
|
await destHandle.sync();
|
|
277
274
|
}
|
|
@@ -279,28 +276,22 @@ export async function copyFallbackReplace(params) {
|
|
|
279
276
|
}
|
|
280
277
|
finally {
|
|
281
278
|
await destHandle?.close().catch(() => undefined);
|
|
282
|
-
await sourceHandle.close().catch(() => undefined);
|
|
283
279
|
}
|
|
284
280
|
}
|
|
285
281
|
export function copyFallbackReplaceSync(params) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
282
|
+
const source = readOwnedCopySourceSync({
|
|
283
|
+
fsModule: params.fsModule,
|
|
284
|
+
src: params.src,
|
|
285
|
+
expectedIdentity: params.expectedSourceIdentity,
|
|
286
|
+
});
|
|
287
|
+
const { replacement } = source;
|
|
291
288
|
let destFd;
|
|
292
289
|
try {
|
|
293
|
-
const sourceStat = params.fsModule.fstatSync(sourceFd);
|
|
294
|
-
const sourceCurrent = params.fsModule.lstatSync(params.src);
|
|
295
|
-
if (!sourceStat.isFile() || sourceCurrent.isSymbolicLink() || !sameFileIdentity(sourceStat, sourceCurrent)) {
|
|
296
|
-
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
297
|
-
}
|
|
298
|
-
const replacement = readBoundedSync(params.fsModule, sourceFd, Number.MAX_SAFE_INTEGER);
|
|
299
290
|
if (params.restore === "restore-original") {
|
|
300
291
|
const pinned = openPinnedDestinationSync(params.fsModule, params.dest, params.destinationHardlinks);
|
|
301
292
|
if (pinned) {
|
|
302
293
|
destFd = pinned.fd;
|
|
303
|
-
replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes,
|
|
294
|
+
replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes, source.mode, params.fchmodSync);
|
|
304
295
|
}
|
|
305
296
|
}
|
|
306
297
|
if (destFd === undefined) {
|
|
@@ -319,9 +310,9 @@ export function copyFallbackReplaceSync(params) {
|
|
|
319
310
|
assertDestinationHardlinkPolicySync(params.fsModule, params.dest, params.destinationHardlinks);
|
|
320
311
|
params.fsModule.rmSync(params.dest, { force: true });
|
|
321
312
|
}
|
|
322
|
-
destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS,
|
|
313
|
+
destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, source.mode & 0o777);
|
|
323
314
|
writeAllSync(params.fsModule, destFd, replacement);
|
|
324
|
-
params.fchmodSync?.(destFd,
|
|
315
|
+
params.fchmodSync?.(destFd, source.mode);
|
|
325
316
|
if (params.sync) {
|
|
326
317
|
params.fsModule.fsyncSync(destFd);
|
|
327
318
|
}
|
|
@@ -336,11 +327,5 @@ export function copyFallbackReplaceSync(params) {
|
|
|
336
327
|
// Best-effort close after fallback replacement.
|
|
337
328
|
}
|
|
338
329
|
}
|
|
339
|
-
try {
|
|
340
|
-
params.fsModule.closeSync(sourceFd);
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
// Best-effort close after fallback replacement.
|
|
344
|
-
}
|
|
345
330
|
}
|
|
346
331
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
|
+
type AsyncSourceFileSystem = Pick<typeof import("node:fs/promises"), "lstat" | "open">;
|
|
3
|
+
type SyncSourceFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readSync">;
|
|
4
|
+
export declare function readOwnedCopySource(params: {
|
|
5
|
+
fsModule: AsyncSourceFileSystem;
|
|
6
|
+
src: string;
|
|
7
|
+
expectedIdentity?: BigIntStats;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
replacement: Buffer;
|
|
10
|
+
mode: number;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function readOwnedCopySourceSync(params: {
|
|
13
|
+
fsModule: SyncSourceFileSystem;
|
|
14
|
+
src: string;
|
|
15
|
+
expectedIdentity?: BigIntStats;
|
|
16
|
+
}): {
|
|
17
|
+
replacement: Buffer;
|
|
18
|
+
mode: number;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=replace-file-copy-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replace-file-copy-source.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-source.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAMnD,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AACvF,KAAK,oBAAoB,GAAG,IAAI,CAC9B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAClE,CAAC;AAgEF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BjD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA8BxC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import syncFs, {} from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
4
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
|
+
const NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants
|
|
6
|
+
? syncFs.constants.O_NOFOLLOW
|
|
7
|
+
: 0;
|
|
8
|
+
const NONBLOCK = process.platform !== "win32" && "O_NONBLOCK" in syncFs.constants
|
|
9
|
+
? syncFs.constants.O_NONBLOCK
|
|
10
|
+
: 0;
|
|
11
|
+
const OPEN_READ_FLAGS = syncFs.constants.O_RDONLY | NOFOLLOW | NONBLOCK;
|
|
12
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
13
|
+
function assertSourcePreview(source, src) {
|
|
14
|
+
if (source.isSymbolicLink()) {
|
|
15
|
+
throw new FsSafeError("symlink", `Refusing copy fallback from non-file source: ${src}`);
|
|
16
|
+
}
|
|
17
|
+
if (!source.isFile()) {
|
|
18
|
+
throw new FsSafeError("not-file", `Refusing copy fallback from non-file source: ${src}`);
|
|
19
|
+
}
|
|
20
|
+
if (source.nlink !== 1) {
|
|
21
|
+
throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${src}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function sourceSymlinkError(src, cause) {
|
|
25
|
+
return new FsSafeError("symlink", `Refusing copy fallback from non-file source: ${src}`, {
|
|
26
|
+
cause,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function openSource(fsModule, src) {
|
|
30
|
+
try {
|
|
31
|
+
return await fsModule.open(src, OPEN_READ_FLAGS);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "ELOOP") {
|
|
35
|
+
throw sourceSymlinkError(src, error);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function openSourceSync(fsModule, src) {
|
|
41
|
+
try {
|
|
42
|
+
return fsModule.openSync(src, OPEN_READ_FLAGS);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === "ELOOP") {
|
|
46
|
+
throw sourceSymlinkError(src, error);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function readAllSync(fsModule, fd) {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let position = 0;
|
|
54
|
+
while (true) {
|
|
55
|
+
const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
56
|
+
const bytesRead = fsModule.readSync(fd, buffer, 0, buffer.length, position);
|
|
57
|
+
if (bytesRead === 0)
|
|
58
|
+
break;
|
|
59
|
+
position += bytesRead;
|
|
60
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
61
|
+
}
|
|
62
|
+
return Buffer.concat(chunks, position);
|
|
63
|
+
}
|
|
64
|
+
export async function readOwnedCopySource(params) {
|
|
65
|
+
assertSourcePreview(await params.fsModule.lstat(params.src), params.src);
|
|
66
|
+
const handle = await openSource(params.fsModule, params.src);
|
|
67
|
+
try {
|
|
68
|
+
if (params.expectedIdentity) {
|
|
69
|
+
const exact = await inspectFileIdentity(() => handle.stat({ bigint: true }), params.expectedIdentity);
|
|
70
|
+
await inspectFileIdentity(() => params.fsModule.lstat(params.src, { bigint: true }), exact);
|
|
71
|
+
}
|
|
72
|
+
const opened = await handle.stat();
|
|
73
|
+
const current = await params.fsModule.lstat(params.src);
|
|
74
|
+
if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
|
|
75
|
+
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
76
|
+
}
|
|
77
|
+
if (opened.nlink !== 1) {
|
|
78
|
+
throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
|
|
79
|
+
}
|
|
80
|
+
return { replacement: await handle.readFile(), mode: opened.mode };
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await handle.close().catch(() => undefined);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function readOwnedCopySourceSync(params) {
|
|
87
|
+
assertSourcePreview(params.fsModule.lstatSync(params.src), params.src);
|
|
88
|
+
const fd = openSourceSync(params.fsModule, params.src);
|
|
89
|
+
try {
|
|
90
|
+
if (params.expectedIdentity) {
|
|
91
|
+
const exact = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }), params.expectedIdentity);
|
|
92
|
+
inspectFileIdentitySync(() => params.fsModule.lstatSync(params.src, { bigint: true }), exact);
|
|
93
|
+
}
|
|
94
|
+
const opened = params.fsModule.fstatSync(fd);
|
|
95
|
+
const current = params.fsModule.lstatSync(params.src);
|
|
96
|
+
if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
|
|
97
|
+
throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
|
|
98
|
+
}
|
|
99
|
+
if (opened.nlink !== 1) {
|
|
100
|
+
throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
|
|
101
|
+
}
|
|
102
|
+
return { replacement: readAllSync(params.fsModule, fd), mode: opened.mode };
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
try {
|
|
106
|
+
params.fsModule.closeSync(fd);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Best-effort close after reading a copy-fallback source.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|