@openclaw/fs-safe 0.2.5 → 0.2.7
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 +13 -0
- package/README.md +1 -1
- package/dist/archive-deadline.d.ts +9 -0
- package/dist/archive-deadline.d.ts.map +1 -0
- package/dist/archive-deadline.js +67 -0
- package/dist/archive-file-io.d.ts +9 -0
- package/dist/archive-file-io.d.ts.map +1 -0
- package/dist/archive-file-io.js +11 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -3
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +213 -105
- package/dist/file-store-boundary.js +2 -2
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +6 -6
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +5 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +1 -0
- package/dist/path.d.ts +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -0
- package/dist/pinned-python.d.ts.map +1 -1
- package/dist/pinned-python.js +45 -38
- package/dist/pinned-write.d.ts +3 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +45 -42
- package/dist/root-path.js +2 -2
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +16 -5
- package/dist/secret-file.d.ts +1 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +99 -53
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +9 -3
- package/dist/symlink-parents.js +2 -2
- package/dist/temp-target.js +1 -1
- package/docs/secret-file.md +3 -2
- package/docs/timing.md +1 -1
- package/package.json +1 -1
package/dist/guarded-mkdir.js
CHANGED
|
@@ -2,17 +2,17 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { isPathRelativeEscape } from "./path.js";
|
|
5
6
|
function isSameOrChildPath(candidate, parent) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function isPathEscape(relativePath) {
|
|
9
|
-
return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
7
|
+
const parentPrefix = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`;
|
|
8
|
+
return candidate === parent || candidate.startsWith(parentPrefix);
|
|
10
9
|
}
|
|
11
10
|
export async function mkdirPathComponentsWithGuards(params) {
|
|
12
11
|
const root = path.resolve(params.rootReal);
|
|
12
|
+
const rootCanonical = path.resolve(await fs.realpath(root));
|
|
13
13
|
const target = path.resolve(params.targetPath);
|
|
14
14
|
const relative = path.relative(root, target);
|
|
15
|
-
if (
|
|
15
|
+
if (isPathRelativeEscape(relative)) {
|
|
16
16
|
throw new FsSafeError("outside-workspace", "directory is outside workspace root");
|
|
17
17
|
}
|
|
18
18
|
let current = root;
|
|
@@ -35,7 +35,7 @@ export async function mkdirPathComponentsWithGuards(params) {
|
|
|
35
35
|
}
|
|
36
36
|
// Node's recursive mkdir follows symlinks in missing components. Build one
|
|
37
37
|
// segment at a time and realpath-check each segment before descending.
|
|
38
|
-
if (!isSameOrChildPath(path.resolve(await fs.realpath(next)),
|
|
38
|
+
if (!isSameOrChildPath(path.resolve(await fs.realpath(next)), rootCanonical)) {
|
|
39
39
|
throw new FsSafeError("outside-workspace", "directory escaped workspace root");
|
|
40
40
|
}
|
|
41
41
|
await createAsyncDirectoryGuard(next);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-durable-queue.d.ts","sourceRoot":"","sources":["../src/json-durable-queue.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAOF,eAAO,MAAM,0CAA0C,QAAmB,CAAC;AAY3E,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE;AAED,wBAAsB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUpF;AAmBD,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,GACT,0BAA0B,CAM5B;AAED,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+KD,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;
|
|
1
|
+
{"version":3,"file":"json-durable-queue.d.ts","sourceRoot":"","sources":["../src/json-durable-queue.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAOF,eAAO,MAAM,0CAA0C,QAAmB,CAAC;AAY3E,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE;AAED,wBAAsB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUpF;AAmBD,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,GACT,0BAA0B,CAM5B;AAED,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+KD,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAsDD,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAED,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE;IACzD,KAAK,EAAE,0BAA0B,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAwBpB;AAED,wBAAsB,kCAAkC,CAAC,CAAC,EACxD,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,CAAC,EAAE,CAAC,CAkDd;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
|
|
@@ -215,6 +215,9 @@ async function readBoundedUtf8File(params) {
|
|
|
215
215
|
if (initialStat.isSymbolicLink() || !initialStat.isFile()) {
|
|
216
216
|
throw new Error("queue entry is not a regular file");
|
|
217
217
|
}
|
|
218
|
+
if (initialStat.nlink > 1) {
|
|
219
|
+
throw new Error("queue entry hardlinks are not allowed");
|
|
220
|
+
}
|
|
218
221
|
if (initialStat.size > params.maxBytes) {
|
|
219
222
|
throw new Error(`queue entry exceeds ${params.maxBytes} bytes`);
|
|
220
223
|
}
|
|
@@ -228,6 +231,8 @@ async function readBoundedUtf8File(params) {
|
|
|
228
231
|
if (!openedStat.isFile() ||
|
|
229
232
|
pathStat.isSymbolicLink() ||
|
|
230
233
|
!pathStat.isFile() ||
|
|
234
|
+
openedStat.nlink > 1 ||
|
|
235
|
+
pathStat.nlink > 1 ||
|
|
231
236
|
!sameFileIdentity(initialStat, openedStat) ||
|
|
232
237
|
!sameFileIdentity(pathStat, openedStat)) {
|
|
233
238
|
throw new Error("queue entry changed during read");
|
package/dist/output.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA6CF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA6CF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA+BrC"}
|
package/dist/output.js
CHANGED
|
@@ -42,6 +42,7 @@ export async function writeExternalFileWithinRoot(options) {
|
|
|
42
42
|
if (requestedTargetPath.length === 0) {
|
|
43
43
|
throw new FsSafeError("invalid-path", "target path is required");
|
|
44
44
|
}
|
|
45
|
+
assertFileTargetPath(requestedTargetPath);
|
|
45
46
|
const targetPath = toRootPathInput({
|
|
46
47
|
rootDir: targetRoot.rootDir,
|
|
47
48
|
rootReal: targetRoot.rootReal,
|
package/dist/path.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare function assertNoNulPathInput(filePath: string, message?: string)
|
|
|
6
6
|
export declare function isNotFoundPathError(value: unknown): boolean;
|
|
7
7
|
export declare function isSymlinkOpenError(value: unknown): boolean;
|
|
8
8
|
export declare function isPathInside(root: string, target: string): boolean;
|
|
9
|
+
export declare function isPathRelativeEscape(relativePath: string): boolean;
|
|
9
10
|
export declare function resolveSafeBaseDir(rootDir: string): string;
|
|
10
11
|
export declare function isWithinDir(rootDir: string, targetPath: string): boolean;
|
|
11
12
|
export declare function safeRealpathSync(targetPath: string, cache?: Map<string, string>): string | null;
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAUzB,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA4BlE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAuBpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAUzB,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA4BlE;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAuBpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
|
package/dist/path.js
CHANGED
|
@@ -55,6 +55,9 @@ export function isPathInside(root, target) {
|
|
|
55
55
|
const firstSegment = relative.split(path.posix.sep)[0];
|
|
56
56
|
return relative === "" || (firstSegment !== ".." && !path.isAbsolute(relative));
|
|
57
57
|
}
|
|
58
|
+
export function isPathRelativeEscape(relativePath) {
|
|
59
|
+
return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
60
|
+
}
|
|
58
61
|
export function resolveSafeBaseDir(rootDir) {
|
|
59
62
|
const resolved = path.resolve(rootDir);
|
|
60
63
|
return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-python.d.ts","sourceRoot":"","sources":["../src/pinned-python.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pinned-python.d.ts","sourceRoot":"","sources":["../src/pinned-python.ts"],"names":[],"mappings":"AA2ZA,KAAK,qBAAqB,GACtB,MAAM,GACN,MAAM,GACN,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,OAAO,CAAC;AAiBZ,wBAAgB,gCAAgC,IAAI,IAAI,CAQvD;AAiOD,wBAAsB,wBAAwB,CAAC,CAAC,EAAE,MAAM,EAAE;IACxD,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,GAAG,OAAO,CAAC,CAAC,CAAC,CA4Bb;AAED,wBAAgB,oCAAoC,IAAI,IAAI,CAK3D;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAarF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEjE"}
|
package/dist/pinned-python.js
CHANGED
|
@@ -3,26 +3,20 @@ import fsSync from "node:fs";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
5
5
|
const PINNED_PYTHON_WORKER_SOURCE = String.raw `
|
|
6
|
-
import base64
|
|
7
|
-
import errno
|
|
8
|
-
import json
|
|
9
|
-
import os
|
|
10
|
-
import secrets
|
|
11
|
-
import stat
|
|
12
|
-
import sys
|
|
13
|
-
|
|
6
|
+
import base64, errno, json, os, secrets, stat, sys
|
|
14
7
|
DIR_FLAGS = os.O_RDONLY
|
|
15
8
|
if hasattr(os, "O_DIRECTORY"):
|
|
16
9
|
DIR_FLAGS |= os.O_DIRECTORY
|
|
17
10
|
if hasattr(os, "O_NOFOLLOW"):
|
|
18
11
|
DIR_FLAGS |= os.O_NOFOLLOW
|
|
19
12
|
READ_FLAGS = os.O_RDONLY
|
|
13
|
+
if hasattr(os, "O_NONBLOCK"):
|
|
14
|
+
READ_FLAGS |= os.O_NONBLOCK
|
|
20
15
|
if hasattr(os, "O_NOFOLLOW"):
|
|
21
16
|
READ_FLAGS |= os.O_NOFOLLOW
|
|
22
17
|
WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
23
18
|
if hasattr(os, "O_NOFOLLOW"):
|
|
24
19
|
WRITE_FLAGS |= os.O_NOFOLLOW
|
|
25
|
-
|
|
26
20
|
def split_relative(value):
|
|
27
21
|
if value in ("", "."):
|
|
28
22
|
return []
|
|
@@ -35,10 +29,8 @@ def split_relative(value):
|
|
|
35
29
|
if part == "..":
|
|
36
30
|
raise OSError(errno.EPERM, "path traversal is not allowed")
|
|
37
31
|
return parts
|
|
38
|
-
|
|
39
32
|
def open_dir(path_value, dir_fd=None):
|
|
40
33
|
return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
|
|
41
|
-
|
|
42
34
|
def walk_dir(root_fd, segments, mkdir_enabled=False):
|
|
43
35
|
current_fd = os.dup(root_fd)
|
|
44
36
|
try:
|
|
@@ -56,14 +48,12 @@ def walk_dir(root_fd, segments, mkdir_enabled=False):
|
|
|
56
48
|
except Exception:
|
|
57
49
|
os.close(current_fd)
|
|
58
50
|
raise
|
|
59
|
-
|
|
60
51
|
def parent_and_basename(root_fd, relative):
|
|
61
52
|
segments = split_relative(relative)
|
|
62
53
|
if not segments:
|
|
63
54
|
raise OSError(errno.EPERM, "operation requires a non-root path")
|
|
64
55
|
parent_fd = walk_dir(root_fd, segments[:-1])
|
|
65
56
|
return parent_fd, segments[-1]
|
|
66
|
-
|
|
67
57
|
def encode_stat(st):
|
|
68
58
|
mode = st.st_mode
|
|
69
59
|
return {
|
|
@@ -79,14 +69,12 @@ def encode_stat(st):
|
|
|
79
69
|
"size": st.st_size,
|
|
80
70
|
"uid": st.st_uid,
|
|
81
71
|
}
|
|
82
|
-
|
|
83
72
|
def reject_unsafe_endpoint(st):
|
|
84
73
|
mode = st.st_mode
|
|
85
74
|
if stat.S_ISLNK(mode):
|
|
86
75
|
raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
|
|
87
76
|
if stat.S_ISREG(mode) and st.st_nlink > 1:
|
|
88
77
|
raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
|
|
89
|
-
|
|
90
78
|
def copy_bytes(source_fd, dest_fd):
|
|
91
79
|
while True:
|
|
92
80
|
chunk = os.read(source_fd, 65536)
|
|
@@ -98,7 +86,6 @@ def copy_bytes(source_fd, dest_fd):
|
|
|
98
86
|
if written <= 0:
|
|
99
87
|
raise OSError(errno.EIO, "short write")
|
|
100
88
|
view = view[written:]
|
|
101
|
-
|
|
102
89
|
def write_all(fd, data):
|
|
103
90
|
view = memoryview(data)
|
|
104
91
|
while view:
|
|
@@ -106,11 +93,9 @@ def write_all(fd, data):
|
|
|
106
93
|
if written <= 0:
|
|
107
94
|
raise OSError(errno.EIO, "short write")
|
|
108
95
|
view = view[written:]
|
|
109
|
-
|
|
110
96
|
def link_unsupported(exc):
|
|
111
97
|
unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
|
|
112
98
|
return getattr(exc, "errno", None) in unsupported
|
|
113
|
-
|
|
114
99
|
def link_no_replace(name, new_name, source_fd, target_fd):
|
|
115
100
|
linked = False
|
|
116
101
|
try:
|
|
@@ -125,11 +110,9 @@ def link_no_replace(name, new_name, source_fd, target_fd):
|
|
|
125
110
|
os.fsync(source_fd)
|
|
126
111
|
if source_fd != target_fd:
|
|
127
112
|
os.fsync(target_fd)
|
|
128
|
-
|
|
129
113
|
def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
|
|
130
114
|
source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
|
|
131
|
-
dest_fd = None
|
|
132
|
-
success = False
|
|
115
|
+
dest_fd = None; success = False; dest_stat = None
|
|
133
116
|
try:
|
|
134
117
|
if expected is not None:
|
|
135
118
|
source_stat = os.fstat(source_fd)
|
|
@@ -138,6 +121,7 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
|
|
|
138
121
|
dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
|
|
139
122
|
copy_bytes(source_fd, dest_fd)
|
|
140
123
|
os.fsync(dest_fd)
|
|
124
|
+
dest_stat = os.fstat(dest_fd)
|
|
141
125
|
success = True
|
|
142
126
|
finally:
|
|
143
127
|
os.close(source_fd)
|
|
@@ -153,19 +137,40 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
|
|
|
153
137
|
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
154
138
|
except FileNotFoundError: pass
|
|
155
139
|
raise
|
|
156
|
-
|
|
157
|
-
def
|
|
140
|
+
return dest_stat
|
|
141
|
+
def same_identity(left, right):
|
|
142
|
+
return left.st_dev == right.st_dev and left.st_ino == right.st_ino
|
|
143
|
+
def verify_temp_name(parent_fd, temp_name, expected_stat):
|
|
144
|
+
current_stat = os.lstat(temp_name, dir_fd=parent_fd)
|
|
145
|
+
if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
|
|
146
|
+
raise RuntimeError("fs-safe-temp-mismatch")
|
|
147
|
+
def verify_committed_temp(parent_fd, basename, expected_stat):
|
|
148
|
+
final_stat = os.lstat(basename, dir_fd=parent_fd)
|
|
149
|
+
if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
|
|
150
|
+
return final_stat
|
|
151
|
+
try: os.unlink(basename, dir_fd=parent_fd)
|
|
152
|
+
except FileNotFoundError: pass
|
|
153
|
+
raise RuntimeError("fs-safe-temp-mismatch")
|
|
154
|
+
def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
|
|
155
|
+
verify_temp_name(parent_fd, temp_name, expected_stat)
|
|
158
156
|
if overwrite:
|
|
159
157
|
os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
158
|
+
return verify_committed_temp(parent_fd, basename, expected_stat)
|
|
160
159
|
else:
|
|
161
160
|
try:
|
|
162
161
|
os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
|
|
162
|
+
final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
|
|
163
163
|
os.unlink(temp_name, dir_fd=parent_fd)
|
|
164
|
+
return final_stat
|
|
164
165
|
except OSError as exc:
|
|
165
166
|
if not link_unsupported(exc):
|
|
166
167
|
raise
|
|
167
|
-
copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode,
|
|
168
|
-
|
|
168
|
+
return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
|
|
169
|
+
def assert_expected_root(root_fd, payload):
|
|
170
|
+
if "rootDev" in payload or "rootIno" in payload:
|
|
171
|
+
root_stat = os.fstat(root_fd)
|
|
172
|
+
if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
|
|
173
|
+
raise RuntimeError("fs-safe-root-mismatch")
|
|
169
174
|
def stat_path(root_fd, payload):
|
|
170
175
|
relative = payload.get("relativePath", "")
|
|
171
176
|
segments = split_relative(relative)
|
|
@@ -179,7 +184,6 @@ def stat_path(root_fd, payload):
|
|
|
179
184
|
return encode_stat(st)
|
|
180
185
|
finally:
|
|
181
186
|
os.close(parent_fd)
|
|
182
|
-
|
|
183
187
|
def readdir_path(root_fd, payload):
|
|
184
188
|
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
|
|
185
189
|
try:
|
|
@@ -195,12 +199,9 @@ def readdir_path(root_fd, payload):
|
|
|
195
199
|
return entries
|
|
196
200
|
finally:
|
|
197
201
|
os.close(dir_fd)
|
|
198
|
-
|
|
199
202
|
def mkdirp_path(root_fd, payload):
|
|
200
203
|
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
|
|
201
|
-
os.close(dir_fd)
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
+
os.close(dir_fd); return None
|
|
204
205
|
def remove_tree(parent_fd, basename):
|
|
205
206
|
st = os.lstat(basename, dir_fd=parent_fd)
|
|
206
207
|
if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
|
|
@@ -213,7 +214,6 @@ def remove_tree(parent_fd, basename):
|
|
|
213
214
|
os.rmdir(basename, dir_fd=parent_fd)
|
|
214
215
|
else:
|
|
215
216
|
os.unlink(basename, dir_fd=parent_fd)
|
|
216
|
-
|
|
217
217
|
def remove_path(root_fd, payload):
|
|
218
218
|
parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
|
|
219
219
|
try:
|
|
@@ -296,14 +296,15 @@ def write_path(root_fd, payload):
|
|
|
296
296
|
except FileNotFoundError:
|
|
297
297
|
pass
|
|
298
298
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
299
|
+
os.fchmod(temp_fd, mode)
|
|
299
300
|
write_all(temp_fd, data)
|
|
300
301
|
os.fsync(temp_fd)
|
|
302
|
+
temp_stat = os.fstat(temp_fd)
|
|
301
303
|
os.close(temp_fd)
|
|
302
304
|
temp_fd = None
|
|
303
|
-
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
305
|
+
result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
|
|
304
306
|
temp_name = None
|
|
305
307
|
os.fsync(parent_fd)
|
|
306
|
-
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
307
308
|
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
308
309
|
finally:
|
|
309
310
|
if temp_fd is not None:
|
|
@@ -334,6 +335,7 @@ def copy_path(root_fd, payload):
|
|
|
334
335
|
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
|
|
335
336
|
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
336
337
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
338
|
+
os.fchmod(temp_fd, mode)
|
|
337
339
|
written_bytes = 0
|
|
338
340
|
while True:
|
|
339
341
|
chunk = os.read(source_fd, 65536)
|
|
@@ -349,12 +351,12 @@ def copy_path(root_fd, payload):
|
|
|
349
351
|
raise OSError(errno.EIO, "short write")
|
|
350
352
|
view = view[written:]
|
|
351
353
|
os.fsync(temp_fd)
|
|
354
|
+
temp_stat = os.fstat(temp_fd)
|
|
352
355
|
os.close(temp_fd)
|
|
353
356
|
temp_fd = None
|
|
354
|
-
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
357
|
+
result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
|
|
355
358
|
temp_name = None
|
|
356
359
|
os.fsync(parent_fd)
|
|
357
|
-
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
358
360
|
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
359
361
|
finally:
|
|
360
362
|
os.close(source_fd)
|
|
@@ -371,6 +373,7 @@ def copy_path(root_fd, payload):
|
|
|
371
373
|
def run_operation(operation, root_path, payload):
|
|
372
374
|
root_fd = open_dir(root_path)
|
|
373
375
|
try:
|
|
376
|
+
assert_expected_root(root_fd, payload)
|
|
374
377
|
if operation == "stat":
|
|
375
378
|
return stat_path(root_fd, payload)
|
|
376
379
|
if operation == "readdir":
|
|
@@ -475,6 +478,12 @@ function mapWorkerError(response) {
|
|
|
475
478
|
if (message.includes("fs-safe-source-mismatch")) {
|
|
476
479
|
return new FsSafeError("path-mismatch", "source path changed during copy");
|
|
477
480
|
}
|
|
481
|
+
if (message.includes("fs-safe-temp-mismatch")) {
|
|
482
|
+
return new FsSafeError("path-mismatch", "temp path changed during write");
|
|
483
|
+
}
|
|
484
|
+
if (message.includes("fs-safe-root-mismatch")) {
|
|
485
|
+
return new FsSafeError("path-mismatch", "root path changed during operation");
|
|
486
|
+
}
|
|
478
487
|
if (message.includes("fs-safe-directory-noreplace-unsupported")) {
|
|
479
488
|
return new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
480
489
|
}
|
|
@@ -655,9 +664,7 @@ export function validatePinnedOperationPayload(payload) {
|
|
|
655
664
|
}
|
|
656
665
|
}
|
|
657
666
|
export function isPinnedHelperUnavailable(error) {
|
|
658
|
-
return error instanceof Error &&
|
|
659
|
-
"code" in error &&
|
|
660
|
-
error.code === "helper-unavailable";
|
|
667
|
+
return error instanceof Error && "code" in error && error.code === "helper-unavailable";
|
|
661
668
|
}
|
|
662
669
|
function validatePinnedRelativePath(relativePath) {
|
|
663
670
|
if (relativePath.length === 0 || relativePath === ".") {
|
package/dist/pinned-write.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
2
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
3
|
type PinnedWriteInput = {
|
|
4
4
|
kind: "buffer";
|
|
@@ -17,6 +17,7 @@ export declare function runPinnedWriteHelper(params: {
|
|
|
17
17
|
overwrite?: boolean;
|
|
18
18
|
maxBytes?: number;
|
|
19
19
|
input: PinnedWriteInput;
|
|
20
|
+
rootIdentity?: FileIdentityStat;
|
|
20
21
|
}): Promise<FileIdentityStat>;
|
|
21
22
|
export declare function runPinnedCopyHelper(params: {
|
|
22
23
|
rootPath: string;
|
|
@@ -28,6 +29,7 @@ export declare function runPinnedCopyHelper(params: {
|
|
|
28
29
|
maxBytes?: number;
|
|
29
30
|
sourcePath: string;
|
|
30
31
|
sourceIdentity: FileIdentityStat;
|
|
32
|
+
rootIdentity?: FileIdentityStat;
|
|
31
33
|
}): Promise<FileIdentityStat>;
|
|
32
34
|
export {};
|
|
33
35
|
//# sourceMappingURL=pinned-write.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAW3D,KAAK,gBAAgB,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AA6EzC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;CACjC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwC5B;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,gBAAgB,CAAC;IACjC,YAAY,CAAC,EAAE,gBAAgB,CAAC;CACjC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqB5B"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -2,11 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { pipeline } from "node:stream/promises";
|
|
7
|
-
import { createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
5
|
+
import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
8
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
9
8
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
9
|
+
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
10
10
|
import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
11
11
|
import { assertPinnedPythonOperationAvailable, runPinnedPythonOperation, validatePinnedOperationPayload, } from "./pinned-python.js";
|
|
12
12
|
function byteLength(input, encoding) {
|
|
@@ -28,29 +28,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
|
|
|
28
28
|
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
function
|
|
32
|
-
if (maxBytes === undefined) {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
31
|
+
async function writeStreamToHandle(stream, handle, maxBytes) {
|
|
35
32
|
let bytes = 0;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
for await (const chunk of stream) {
|
|
34
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
35
|
+
bytes += buffer.byteLength;
|
|
36
|
+
assertWithinMaxBytes(bytes, maxBytes);
|
|
37
|
+
let offset = 0;
|
|
38
|
+
while (offset < buffer.byteLength) {
|
|
39
|
+
const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
|
|
40
|
+
if (bytesWritten <= 0) {
|
|
41
|
+
throw new FsSafeError("helper-failed", "fallback stream write made no progress");
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
async function pipelineWithMaxBytes(stream, destination, maxBytes) {
|
|
48
|
-
const limiter = createMaxBytesTransform(maxBytes);
|
|
49
|
-
if (limiter) {
|
|
50
|
-
await pipeline(stream, limiter, destination);
|
|
51
|
-
return;
|
|
43
|
+
offset += bytesWritten;
|
|
44
|
+
}
|
|
52
45
|
}
|
|
53
|
-
await pipeline(stream, destination);
|
|
54
46
|
}
|
|
55
47
|
async function inputToBase64(input, maxBytes) {
|
|
56
48
|
if (input.kind === "buffer") {
|
|
@@ -96,6 +88,7 @@ export async function runPinnedWriteHelper(params) {
|
|
|
96
88
|
mode: params.mode || 0o600,
|
|
97
89
|
overwrite: params.overwrite !== false,
|
|
98
90
|
relativeParentPath: params.relativeParentPath,
|
|
91
|
+
...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
|
|
99
92
|
};
|
|
100
93
|
try {
|
|
101
94
|
return await runPinnedPythonOperation({
|
|
@@ -126,6 +119,7 @@ export async function runPinnedCopyHelper(params) {
|
|
|
126
119
|
mode: params.mode || 0o600,
|
|
127
120
|
overwrite: params.overwrite !== false,
|
|
128
121
|
relativeParentPath: params.relativeParentPath,
|
|
122
|
+
...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
|
|
129
123
|
sourceDev: params.sourceIdentity.dev,
|
|
130
124
|
sourceIno: params.sourceIdentity.ino,
|
|
131
125
|
sourcePath: params.sourcePath,
|
|
@@ -136,12 +130,12 @@ async function runPinnedWriteFallback(params) {
|
|
|
136
130
|
const parentPath = params.relativeParentPath
|
|
137
131
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
138
132
|
: params.rootPath;
|
|
139
|
-
const parentGuard = await createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
140
133
|
if (params.mkdir) {
|
|
141
|
-
await
|
|
142
|
-
await fs.mkdir(parentPath, { recursive: true });
|
|
143
|
-
});
|
|
134
|
+
await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
|
|
144
135
|
}
|
|
136
|
+
const parentGuard = params.mkdir
|
|
137
|
+
? await createAsyncDirectoryGuard(parentPath)
|
|
138
|
+
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
145
139
|
const targetPath = path.join(parentPath, params.basename);
|
|
146
140
|
if (params.overwrite === false) {
|
|
147
141
|
let handle = await withAsyncDirectoryGuards([parentGuard], async () => await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode), {
|
|
@@ -163,7 +157,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
163
157
|
}
|
|
164
158
|
}
|
|
165
159
|
else {
|
|
166
|
-
await
|
|
160
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
167
161
|
}
|
|
168
162
|
const stat = await handle.stat();
|
|
169
163
|
created = false;
|
|
@@ -184,7 +178,9 @@ async function runPinnedWriteFallback(params) {
|
|
|
184
178
|
? fsSync.constants.O_NOFOLLOW
|
|
185
179
|
: 0);
|
|
186
180
|
let handle;
|
|
187
|
-
let
|
|
181
|
+
let tempStat;
|
|
182
|
+
let targetStat;
|
|
183
|
+
let renamed = false;
|
|
188
184
|
try {
|
|
189
185
|
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
190
186
|
if (params.input.kind === "buffer") {
|
|
@@ -197,27 +193,34 @@ async function runPinnedWriteFallback(params) {
|
|
|
197
193
|
}
|
|
198
194
|
}
|
|
199
195
|
else {
|
|
200
|
-
|
|
201
|
-
writable.once("close", () => {
|
|
202
|
-
handleClosedByStream = true;
|
|
203
|
-
});
|
|
204
|
-
await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
|
|
196
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
205
197
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
198
|
+
tempStat = await handle.stat();
|
|
199
|
+
const tempPathStat = await fs.lstat(tempPath);
|
|
200
|
+
if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
|
|
201
|
+
throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
|
|
209
202
|
}
|
|
203
|
+
const expectedTempStat = tempStat;
|
|
204
|
+
await handle.close().catch(() => undefined);
|
|
205
|
+
handle = undefined;
|
|
210
206
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
211
207
|
await fs.rename(tempPath, targetPath);
|
|
208
|
+
renamed = true;
|
|
209
|
+
targetStat = await fs.lstat(targetPath);
|
|
210
|
+
if (targetStat.isSymbolicLink() || !sameFileIdentity(targetStat, expectedTempStat)) {
|
|
211
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
212
|
+
}
|
|
212
213
|
});
|
|
213
214
|
}
|
|
214
215
|
catch (error) {
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
await handle?.close().catch(() => undefined);
|
|
217
|
+
if (!renamed) {
|
|
218
|
+
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
217
219
|
}
|
|
218
|
-
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
219
220
|
throw error;
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
if (!targetStat) {
|
|
223
|
+
throw new FsSafeError("path-mismatch", "fallback target was not verified");
|
|
224
|
+
}
|
|
225
|
+
return { dev: targetStat.dev, ino: targetStat.ino };
|
|
223
226
|
}
|
package/dist/root-path.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
5
|
+
import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
|
|
6
6
|
export const ROOT_PATH_ALIAS_POLICIES = {
|
|
7
7
|
strict: Object.freeze({
|
|
8
8
|
allowFinalSymlinkForUnlink: false,
|
|
@@ -531,7 +531,7 @@ function relativeInsideRoot(rootPath, targetPath) {
|
|
|
531
531
|
if (!relative || relative === ".") {
|
|
532
532
|
return "";
|
|
533
533
|
}
|
|
534
|
-
if (
|
|
534
|
+
if (isPathRelativeEscape(relative)) {
|
|
535
535
|
return "";
|
|
536
536
|
}
|
|
537
537
|
return relative;
|
package/dist/root-paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"AAMA,KAAK,iBAAiB,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACtD,KAAK,4BAA4B,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AACF,KAAK,4BAA4B,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAAC;AACtF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AACF,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CACL,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAChC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,4BAA4B,CAAC;IACnE,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IAC1E,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACvE,QAAQ,CACN,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,SAAS,CACP,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GACpD,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvE,CAAC;
|
|
1
|
+
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
|
package/dist/root-paths.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
4
|
+
import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
|
|
5
5
|
import { root as openRoot } from "./root.js";
|
|
6
6
|
function invalidPath(scopeLabel) {
|
|
7
7
|
return {
|
|
@@ -9,6 +9,10 @@ function invalidPath(scopeLabel) {
|
|
|
9
9
|
error: `Invalid path: must stay within ${scopeLabel}`,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
function pathStaysWithinRoot(rootDir, candidatePath) {
|
|
13
|
+
const relative = path.relative(rootDir, candidatePath);
|
|
14
|
+
return Boolean(relative) && !isPathRelativeEscape(relative);
|
|
15
|
+
}
|
|
12
16
|
async function resolveRealPathIfExists(targetPath) {
|
|
13
17
|
try {
|
|
14
18
|
return await fs.realpath(targetPath);
|
|
@@ -58,11 +62,14 @@ export function resolvePathWithinRoot(params) {
|
|
|
58
62
|
if (!params.defaultFileName) {
|
|
59
63
|
return { ok: false, error: "path is required" };
|
|
60
64
|
}
|
|
61
|
-
|
|
65
|
+
const defaultPath = path.resolve(root, params.defaultFileName);
|
|
66
|
+
if (!pathStaysWithinRoot(root, defaultPath)) {
|
|
67
|
+
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, path: defaultPath };
|
|
62
70
|
}
|
|
63
71
|
const resolved = path.resolve(root, raw);
|
|
64
|
-
|
|
65
|
-
if (!rel || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
72
|
+
if (!pathStaysWithinRoot(root, resolved)) {
|
|
66
73
|
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
67
74
|
}
|
|
68
75
|
return { ok: true, path: resolved };
|
|
@@ -118,7 +125,7 @@ async function resolveNearestExistingPath(targetPath) {
|
|
|
118
125
|
}
|
|
119
126
|
async function assertNoSymlinkSegments(params) {
|
|
120
127
|
const relative = path.relative(params.rootDir, params.targetPath);
|
|
121
|
-
if (
|
|
128
|
+
if (isPathRelativeEscape(relative)) {
|
|
122
129
|
throw new Error(`Invalid path: must stay within ${params.scopeLabel}`);
|
|
123
130
|
}
|
|
124
131
|
let current = params.rootDir;
|
|
@@ -195,6 +202,10 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
204
|
}
|
|
205
|
+
const currentReal = await fs.realpath(current);
|
|
206
|
+
if (!isPathInside(rootReal, currentReal)) {
|
|
207
|
+
return invalidPath(params.scopeLabel);
|
|
208
|
+
}
|
|
198
209
|
}
|
|
199
210
|
const targetReal = await fs.realpath(targetPath);
|
|
200
211
|
if (!isPathInside(rootReal, targetReal)) {
|