@openclaw/fs-safe 0.2.4 → 0.2.6
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 +21 -1
- package/README.md +8 -9
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +12 -1
- package/dist/file-store-boundary.js +2 -2
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +6 -5
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +6 -6
- package/dist/json-document-store.d.ts +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +5 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +157 -4
- package/dist/json-stringify.d.ts +2 -0
- package/dist/json-stringify.d.ts.map +1 -0
- package/dist/json-stringify.js +7 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +3 -2
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -1
- package/dist/path.d.ts +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +7 -10
- package/dist/pinned-python.d.ts.map +1 -1
- package/dist/pinned-python.js +158 -69
- package/dist/pinned-write.d.ts +3 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +54 -45
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +6 -2
- package/dist/root-path.js +2 -2
- package/dist/root-paths.js +25 -3
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +76 -50
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +3 -1
- package/dist/temp-target.js +1 -1
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +8 -1
- package/docs/index.md +1 -1
- package/docs/json-store.md +3 -1
- package/docs/python-helper.md +3 -3
- package/docs/root.md +5 -6
- package/package.json +4 -4
- package/dist/archive-utils.d.ts +0 -3
- package/dist/archive-utils.d.ts.map +0 -1
- package/dist/archive-utils.js +0 -26
- package/dist/boundary-file-read.d.ts +0 -44
- package/dist/boundary-file-read.d.ts.map +0 -1
- package/dist/boundary-file-read.js +0 -129
- package/dist/boundary-path.d.ts +0 -39
- package/dist/boundary-path.d.ts.map +0 -1
- package/dist/boundary-path.js +0 -598
- package/dist/fs-pinned-path-helper.d.ts +0 -7
- package/dist/fs-pinned-path-helper.d.ts.map +0 -1
- package/dist/fs-pinned-path-helper.js +0 -182
- package/dist/fs-pinned-write-helper.d.ts +0 -21
- package/dist/fs-pinned-write-helper.d.ts.map +0 -1
- package/dist/fs-pinned-write-helper.js +0 -263
- package/dist/hardlink-guards.d.ts +0 -7
- package/dist/hardlink-guards.d.ts.map +0 -1
- package/dist/hardlink-guards.js +0 -30
- package/dist/install-safe-path.d.ts +0 -20
- package/dist/install-safe-path.d.ts.map +0 -1
- package/dist/install-safe-path.js +0 -94
- package/dist/json-file.d.ts +0 -3
- package/dist/json-file.d.ts.map +0 -1
- package/dist/json-file.js +0 -123
- package/dist/json-files.d.ts +0 -20
- package/dist/json-files.d.ts.map +0 -1
- package/dist/json-files.js +0 -153
- package/dist/path-alias-guards.d.ts +0 -19
- package/dist/path-alias-guards.d.ts.map +0 -1
- package/dist/path-alias-guards.js +0 -21
- package/dist/path-guards.d.ts +0 -7
- package/dist/path-guards.d.ts.map +0 -1
- package/dist/path-guards.js +0 -49
- package/dist/path-safety.d.ts +0 -12
- package/dist/path-safety.d.ts.map +0 -1
- package/dist/path-safety.js +0 -50
- package/dist/path-scope.d.ts +0 -2
- package/dist/path-scope.d.ts.map +0 -1
- package/dist/path-scope.js +0 -1
- package/dist/private-file-store.d.ts +0 -60
- package/dist/private-file-store.d.ts.map +0 -1
- package/dist/private-file-store.js +0 -233
- package/dist/safe-open-sync.d.ts +0 -24
- package/dist/safe-open-sync.d.ts.map +0 -1
- package/dist/safe-open-sync.js +0 -71
- package/dist/safe-root.d.ts +0 -123
- package/dist/safe-root.d.ts.map +0 -1
- package/dist/safe-root.js +0 -1060
- package/dist/secure-temp-workspace.d.ts +0 -25
- package/dist/secure-temp-workspace.d.ts.map +0 -1
- package/dist/secure-temp-workspace.js +0 -136
- package/dist/sibling-temp-file.d.ts +0 -16
- package/dist/sibling-temp-file.d.ts.map +0 -1
- package/dist/sibling-temp-file.js +0 -73
- package/dist/sibling-temp-write.d.ts +0 -8
- package/dist/sibling-temp-write.d.ts.map +0 -1
- package/dist/sibling-temp-write.js +0 -40
package/dist/permissions.js
CHANGED
|
@@ -18,7 +18,6 @@ const TRUSTED_BASE = new Set([
|
|
|
18
18
|
"autoridade nt\\system",
|
|
19
19
|
]);
|
|
20
20
|
const WORLD_SUFFIXES = ["\\users", "\\authenticated users"];
|
|
21
|
-
const TRUSTED_SUFFIXES = ["\\administrators", "\\system", "\\système"];
|
|
22
21
|
const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
|
|
23
22
|
const TRUSTED_SIDS = new Set([
|
|
24
23
|
"s-1-5-18",
|
|
@@ -32,6 +31,10 @@ const STATUS_PREFIXES = [
|
|
|
32
31
|
"failed processing",
|
|
33
32
|
"no mapping between account names",
|
|
34
33
|
];
|
|
34
|
+
function stripDiacritics(value) {
|
|
35
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
36
|
+
}
|
|
37
|
+
const TRUSTED_BASE_ASCII = new Set([...TRUSTED_BASE].map(stripDiacritics));
|
|
35
38
|
const normalize = (value) => normalizeLowercaseStringOrEmpty(value);
|
|
36
39
|
const defaultWindowsUserInfo = () => os.userInfo();
|
|
37
40
|
function defaultPermissionExec(command, args) {
|
|
@@ -241,21 +244,15 @@ function classifyPrincipal(principal, trustedPrincipals) {
|
|
|
241
244
|
return "trusted";
|
|
242
245
|
return "group";
|
|
243
246
|
}
|
|
244
|
-
if (trustedPrincipals.has(normalized) ||
|
|
245
|
-
TRUSTED_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
|
|
247
|
+
if (trustedPrincipals.has(normalized) || TRUSTED_BASE.has(normalized)) {
|
|
246
248
|
return "trusted";
|
|
247
249
|
}
|
|
248
250
|
if (WORLD_PRINCIPALS.has(normalized) ||
|
|
249
251
|
WORLD_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
|
|
250
252
|
return "world";
|
|
251
253
|
}
|
|
252
|
-
const stripped = normalized
|
|
253
|
-
if (stripped !== normalized &&
|
|
254
|
-
(TRUSTED_BASE.has(stripped) ||
|
|
255
|
-
TRUSTED_SUFFIXES.some((suffix) => {
|
|
256
|
-
const strippedSuffix = suffix.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
257
|
-
return stripped.endsWith(strippedSuffix);
|
|
258
|
-
}))) {
|
|
254
|
+
const stripped = stripDiacritics(normalized);
|
|
255
|
+
if (stripped !== normalized && TRUSTED_BASE_ASCII.has(stripped)) {
|
|
259
256
|
return "trusted";
|
|
260
257
|
}
|
|
261
258
|
return "group";
|
|
@@ -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,108 @@ 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
|
-
|
|
78
|
+
def copy_bytes(source_fd, dest_fd):
|
|
79
|
+
while True:
|
|
80
|
+
chunk = os.read(source_fd, 65536)
|
|
81
|
+
if not chunk:
|
|
82
|
+
break
|
|
83
|
+
view = memoryview(chunk)
|
|
84
|
+
while view:
|
|
85
|
+
written = os.write(dest_fd, view)
|
|
86
|
+
if written <= 0:
|
|
87
|
+
raise OSError(errno.EIO, "short write")
|
|
88
|
+
view = view[written:]
|
|
89
|
+
def write_all(fd, data):
|
|
90
|
+
view = memoryview(data)
|
|
91
|
+
while view:
|
|
92
|
+
written = os.write(fd, view)
|
|
93
|
+
if written <= 0:
|
|
94
|
+
raise OSError(errno.EIO, "short write")
|
|
95
|
+
view = view[written:]
|
|
96
|
+
def link_unsupported(exc):
|
|
97
|
+
unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
|
|
98
|
+
return getattr(exc, "errno", None) in unsupported
|
|
99
|
+
def link_no_replace(name, new_name, source_fd, target_fd):
|
|
100
|
+
linked = False
|
|
101
|
+
try:
|
|
102
|
+
os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
|
|
103
|
+
linked = True
|
|
104
|
+
os.unlink(name, dir_fd=source_fd)
|
|
105
|
+
except Exception:
|
|
106
|
+
if linked:
|
|
107
|
+
try: os.unlink(new_name, dir_fd=target_fd)
|
|
108
|
+
except FileNotFoundError: pass
|
|
109
|
+
raise
|
|
110
|
+
os.fsync(source_fd)
|
|
111
|
+
if source_fd != target_fd:
|
|
112
|
+
os.fsync(target_fd)
|
|
113
|
+
def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
|
|
114
|
+
source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
|
|
115
|
+
dest_fd = None; success = False; dest_stat = None
|
|
116
|
+
try:
|
|
117
|
+
if expected is not None:
|
|
118
|
+
source_stat = os.fstat(source_fd)
|
|
119
|
+
if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
|
|
120
|
+
raise RuntimeError("fs-safe-source-mismatch")
|
|
121
|
+
dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
|
|
122
|
+
copy_bytes(source_fd, dest_fd)
|
|
123
|
+
os.fsync(dest_fd)
|
|
124
|
+
dest_stat = os.fstat(dest_fd)
|
|
125
|
+
success = True
|
|
126
|
+
finally:
|
|
127
|
+
os.close(source_fd)
|
|
128
|
+
if dest_fd is not None:
|
|
129
|
+
os.close(dest_fd)
|
|
130
|
+
if dest_fd is not None and not success:
|
|
131
|
+
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
132
|
+
except FileNotFoundError: pass
|
|
133
|
+
if unlink_source:
|
|
134
|
+
try:
|
|
135
|
+
os.unlink(source_name, dir_fd=source_parent_fd)
|
|
136
|
+
except Exception:
|
|
137
|
+
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
138
|
+
except FileNotFoundError: pass
|
|
139
|
+
raise
|
|
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)
|
|
156
|
+
if overwrite:
|
|
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)
|
|
159
|
+
else:
|
|
160
|
+
try:
|
|
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
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
164
|
+
return final_stat
|
|
165
|
+
except OSError as exc:
|
|
166
|
+
if not link_unsupported(exc):
|
|
167
|
+
raise
|
|
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")
|
|
90
174
|
def stat_path(root_fd, payload):
|
|
91
175
|
relative = payload.get("relativePath", "")
|
|
92
176
|
segments = split_relative(relative)
|
|
@@ -100,7 +184,6 @@ def stat_path(root_fd, payload):
|
|
|
100
184
|
return encode_stat(st)
|
|
101
185
|
finally:
|
|
102
186
|
os.close(parent_fd)
|
|
103
|
-
|
|
104
187
|
def readdir_path(root_fd, payload):
|
|
105
188
|
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
|
|
106
189
|
try:
|
|
@@ -116,12 +199,9 @@ def readdir_path(root_fd, payload):
|
|
|
116
199
|
return entries
|
|
117
200
|
finally:
|
|
118
201
|
os.close(dir_fd)
|
|
119
|
-
|
|
120
202
|
def mkdirp_path(root_fd, payload):
|
|
121
203
|
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
|
|
122
|
-
os.close(dir_fd)
|
|
123
|
-
return None
|
|
124
|
-
|
|
204
|
+
os.close(dir_fd); return None
|
|
125
205
|
def remove_tree(parent_fd, basename):
|
|
126
206
|
st = os.lstat(basename, dir_fd=parent_fd)
|
|
127
207
|
if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
|
|
@@ -134,7 +214,6 @@ def remove_tree(parent_fd, basename):
|
|
|
134
214
|
os.rmdir(basename, dir_fd=parent_fd)
|
|
135
215
|
else:
|
|
136
216
|
os.unlink(basename, dir_fd=parent_fd)
|
|
137
|
-
|
|
138
217
|
def remove_path(root_fd, payload):
|
|
139
218
|
parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
|
|
140
219
|
try:
|
|
@@ -161,7 +240,18 @@ def rename_path(root_fd, payload):
|
|
|
161
240
|
try:
|
|
162
241
|
from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
|
|
163
242
|
reject_unsafe_endpoint(from_stat)
|
|
164
|
-
|
|
243
|
+
overwrite = payload.get("overwrite", True)
|
|
244
|
+
if not overwrite and stat.S_ISREG(from_stat.st_mode):
|
|
245
|
+
try:
|
|
246
|
+
link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
|
|
247
|
+
except OSError as exc:
|
|
248
|
+
if not link_unsupported(exc):
|
|
249
|
+
raise
|
|
250
|
+
copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
|
|
251
|
+
return None
|
|
252
|
+
if not overwrite and stat.S_ISDIR(from_stat.st_mode):
|
|
253
|
+
raise RuntimeError("fs-safe-directory-noreplace-unsupported")
|
|
254
|
+
if not overwrite:
|
|
165
255
|
try:
|
|
166
256
|
os.lstat(to_base, dir_fd=to_parent_fd)
|
|
167
257
|
raise FileExistsError(errno.EEXIST, "destination exists", to_base)
|
|
@@ -206,19 +296,15 @@ def write_path(root_fd, payload):
|
|
|
206
296
|
except FileNotFoundError:
|
|
207
297
|
pass
|
|
208
298
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
written = os.write(temp_fd, view)
|
|
212
|
-
if written <= 0:
|
|
213
|
-
raise OSError(errno.EIO, "short write")
|
|
214
|
-
view = view[written:]
|
|
299
|
+
os.fchmod(temp_fd, mode)
|
|
300
|
+
write_all(temp_fd, data)
|
|
215
301
|
os.fsync(temp_fd)
|
|
302
|
+
temp_stat = os.fstat(temp_fd)
|
|
216
303
|
os.close(temp_fd)
|
|
217
304
|
temp_fd = None
|
|
218
|
-
|
|
305
|
+
result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
|
|
219
306
|
temp_name = None
|
|
220
307
|
os.fsync(parent_fd)
|
|
221
|
-
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
222
308
|
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
223
309
|
finally:
|
|
224
310
|
if temp_fd is not None:
|
|
@@ -248,13 +334,8 @@ def copy_path(root_fd, payload):
|
|
|
248
334
|
if max_bytes >= 0 and source_stat.st_size > max_bytes:
|
|
249
335
|
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
|
|
250
336
|
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
251
|
-
if not overwrite:
|
|
252
|
-
try:
|
|
253
|
-
os.lstat(basename, dir_fd=parent_fd)
|
|
254
|
-
raise FileExistsError(errno.EEXIST, "destination exists", basename)
|
|
255
|
-
except FileNotFoundError:
|
|
256
|
-
pass
|
|
257
337
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
338
|
+
os.fchmod(temp_fd, mode)
|
|
258
339
|
written_bytes = 0
|
|
259
340
|
while True:
|
|
260
341
|
chunk = os.read(source_fd, 65536)
|
|
@@ -270,12 +351,12 @@ def copy_path(root_fd, payload):
|
|
|
270
351
|
raise OSError(errno.EIO, "short write")
|
|
271
352
|
view = view[written:]
|
|
272
353
|
os.fsync(temp_fd)
|
|
354
|
+
temp_stat = os.fstat(temp_fd)
|
|
273
355
|
os.close(temp_fd)
|
|
274
356
|
temp_fd = None
|
|
275
|
-
|
|
357
|
+
result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
|
|
276
358
|
temp_name = None
|
|
277
359
|
os.fsync(parent_fd)
|
|
278
|
-
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
279
360
|
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
280
361
|
finally:
|
|
281
362
|
os.close(source_fd)
|
|
@@ -292,6 +373,7 @@ def copy_path(root_fd, payload):
|
|
|
292
373
|
def run_operation(operation, root_path, payload):
|
|
293
374
|
root_fd = open_dir(root_path)
|
|
294
375
|
try:
|
|
376
|
+
assert_expected_root(root_fd, payload)
|
|
295
377
|
if operation == "stat":
|
|
296
378
|
return stat_path(root_fd, payload)
|
|
297
379
|
if operation == "readdir":
|
|
@@ -396,6 +478,15 @@ function mapWorkerError(response) {
|
|
|
396
478
|
if (message.includes("fs-safe-source-mismatch")) {
|
|
397
479
|
return new FsSafeError("path-mismatch", "source path changed during copy");
|
|
398
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
|
+
}
|
|
487
|
+
if (message.includes("fs-safe-directory-noreplace-unsupported")) {
|
|
488
|
+
return new FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
489
|
+
}
|
|
399
490
|
if (code === "FileNotFoundError" || errno === 2) {
|
|
400
491
|
return new FsSafeError("not-found", "file not found");
|
|
401
492
|
}
|
|
@@ -413,19 +504,19 @@ function mapWorkerError(response) {
|
|
|
413
504
|
}
|
|
414
505
|
return new FsSafeError("helper-failed", message);
|
|
415
506
|
}
|
|
416
|
-
function rejectPending(error) {
|
|
417
|
-
if (!worker) {
|
|
507
|
+
function rejectPending(error, targetWorker = worker) {
|
|
508
|
+
if (!targetWorker || worker !== targetWorker) {
|
|
418
509
|
return;
|
|
419
510
|
}
|
|
420
|
-
setWorkerRef(
|
|
421
|
-
for (const pending of
|
|
511
|
+
setWorkerRef(targetWorker, false);
|
|
512
|
+
for (const pending of targetWorker.pending.values()) {
|
|
422
513
|
pending.reject(error);
|
|
423
514
|
}
|
|
424
|
-
|
|
515
|
+
targetWorker.pending.clear();
|
|
425
516
|
worker = null;
|
|
426
517
|
}
|
|
427
|
-
function handleWorkerLine(line) {
|
|
428
|
-
if (
|
|
518
|
+
function handleWorkerLine(currentWorker, line) {
|
|
519
|
+
if (worker !== currentWorker || !line.trim()) {
|
|
429
520
|
return;
|
|
430
521
|
}
|
|
431
522
|
let decoded;
|
|
@@ -433,11 +524,11 @@ function handleWorkerLine(line) {
|
|
|
433
524
|
decoded = JSON.parse(line);
|
|
434
525
|
}
|
|
435
526
|
catch {
|
|
436
|
-
rejectPending(new FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
|
|
527
|
+
rejectPending(new FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
|
|
437
528
|
return;
|
|
438
529
|
}
|
|
439
530
|
if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
|
|
440
|
-
rejectPending(new FsSafeError("helper-failed", "pinned helper returned invalid response"));
|
|
531
|
+
rejectPending(new FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
|
|
441
532
|
return;
|
|
442
533
|
}
|
|
443
534
|
const response = decoded;
|
|
@@ -445,13 +536,13 @@ function handleWorkerLine(line) {
|
|
|
445
536
|
if (id === undefined) {
|
|
446
537
|
return;
|
|
447
538
|
}
|
|
448
|
-
const pending =
|
|
539
|
+
const pending = currentWorker.pending.get(id);
|
|
449
540
|
if (!pending) {
|
|
450
541
|
return;
|
|
451
542
|
}
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
setWorkerRef(
|
|
543
|
+
currentWorker.pending.delete(id);
|
|
544
|
+
if (currentWorker.pending.size === 0) {
|
|
545
|
+
setWorkerRef(currentWorker, false);
|
|
455
546
|
}
|
|
456
547
|
if (response.ok === true) {
|
|
457
548
|
pending.resolve(response.result);
|
|
@@ -467,28 +558,28 @@ function getWorker() {
|
|
|
467
558
|
const child = spawn(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
|
|
468
559
|
stdio: ["pipe", "pipe", "pipe"],
|
|
469
560
|
});
|
|
470
|
-
|
|
561
|
+
const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
|
|
562
|
+
worker = currentWorker;
|
|
471
563
|
child.stdout.setEncoding("utf8");
|
|
472
564
|
child.stderr.setEncoding("utf8");
|
|
473
565
|
child.stdout.on("data", (chunk) => {
|
|
474
|
-
|
|
475
|
-
if (!current) {
|
|
566
|
+
if (worker !== currentWorker) {
|
|
476
567
|
return;
|
|
477
568
|
}
|
|
478
|
-
|
|
569
|
+
currentWorker.stdoutBuffer += chunk;
|
|
479
570
|
for (;;) {
|
|
480
|
-
const newline =
|
|
571
|
+
const newline = currentWorker.stdoutBuffer.indexOf("\n");
|
|
481
572
|
if (newline < 0) {
|
|
482
573
|
break;
|
|
483
574
|
}
|
|
484
|
-
const line =
|
|
485
|
-
|
|
486
|
-
handleWorkerLine(line);
|
|
575
|
+
const line = currentWorker.stdoutBuffer.slice(0, newline);
|
|
576
|
+
currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
|
|
577
|
+
handleWorkerLine(currentWorker, line);
|
|
487
578
|
}
|
|
488
579
|
});
|
|
489
580
|
child.stderr.on("data", (chunk) => {
|
|
490
|
-
if (worker) {
|
|
491
|
-
|
|
581
|
+
if (worker === currentWorker) {
|
|
582
|
+
currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
|
|
492
583
|
}
|
|
493
584
|
});
|
|
494
585
|
child.once("error", (error) => {
|
|
@@ -497,17 +588,17 @@ function getWorker() {
|
|
|
497
588
|
: error instanceof Error
|
|
498
589
|
? error
|
|
499
590
|
: new Error(String(error));
|
|
500
|
-
rejectPending(mapped);
|
|
591
|
+
rejectPending(mapped, currentWorker);
|
|
501
592
|
});
|
|
502
593
|
child.once("close", (code, signal) => {
|
|
503
|
-
const stderr =
|
|
504
|
-
rejectPending(new FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
|
|
594
|
+
const stderr = currentWorker.stderr.trim();
|
|
595
|
+
rejectPending(new FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
|
|
505
596
|
});
|
|
506
597
|
process.once("exit", () => {
|
|
507
598
|
child.kill("SIGTERM");
|
|
508
599
|
});
|
|
509
|
-
setWorkerRef(
|
|
510
|
-
return
|
|
600
|
+
setWorkerRef(currentWorker, false);
|
|
601
|
+
return currentWorker;
|
|
511
602
|
}
|
|
512
603
|
function setRefable(value, ref) {
|
|
513
604
|
if (!value) {
|
|
@@ -573,9 +664,7 @@ export function validatePinnedOperationPayload(payload) {
|
|
|
573
664
|
}
|
|
574
665
|
}
|
|
575
666
|
export function isPinnedHelperUnavailable(error) {
|
|
576
|
-
return error instanceof Error &&
|
|
577
|
-
"code" in error &&
|
|
578
|
-
error.code === "helper-unavailable";
|
|
667
|
+
return error instanceof Error && "code" in error && error.code === "helper-unavailable";
|
|
579
668
|
}
|
|
580
669
|
function validatePinnedRelativePath(relativePath) {
|
|
581
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"}
|