@poe-platform/safe-fs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/dist/safe-fs/bridge/confinement.d.ts +3 -0
  4. package/dist/safe-fs/bridge/confinement.js +171 -0
  5. package/dist/safe-fs/bridge/filesystem.d.ts +88 -0
  6. package/dist/safe-fs/bridge/filesystem.js +411 -0
  7. package/dist/safe-fs/bridge/index.d.ts +6 -0
  8. package/dist/safe-fs/bridge/index.js +18 -0
  9. package/dist/safe-fs/bridge/stats.d.ts +4 -0
  10. package/dist/safe-fs/bridge/stats.js +37 -0
  11. package/dist/safe-fs/bridge/types.d.ts +75 -0
  12. package/dist/safe-fs/bridge/types.js +1 -0
  13. package/dist/safe-fs/bridge/values.d.ts +6 -0
  14. package/dist/safe-fs/bridge/values.js +65 -0
  15. package/dist/safe-fs/config/memory.d.ts +3 -0
  16. package/dist/safe-fs/config/memory.js +9 -0
  17. package/dist/safe-fs/config/real.d.ts +5 -0
  18. package/dist/safe-fs/config/real.js +16 -0
  19. package/dist/safe-fs/config.d.ts +15 -0
  20. package/dist/safe-fs/config.js +46 -0
  21. package/dist/safe-fs/config.node.d.ts +2 -0
  22. package/dist/safe-fs/config.node.js +22 -0
  23. package/dist/safe-fs/contracts/abort.d.ts +5 -0
  24. package/dist/safe-fs/contracts/abort.js +34 -0
  25. package/dist/safe-fs/contracts/cleanup.d.ts +1 -0
  26. package/dist/safe-fs/contracts/cleanup.js +9 -0
  27. package/dist/safe-fs/contracts/errors.d.ts +50 -0
  28. package/dist/safe-fs/contracts/errors.js +77 -0
  29. package/dist/safe-fs/contracts/filesystem.d.ts +96 -0
  30. package/dist/safe-fs/contracts/filesystem.js +1 -0
  31. package/dist/safe-fs/contracts/index.d.ts +4 -0
  32. package/dist/safe-fs/contracts/index.js +4 -0
  33. package/dist/safe-fs/contracts/io.d.ts +8 -0
  34. package/dist/safe-fs/contracts/io.js +92 -0
  35. package/dist/safe-fs/contracts/path.d.ts +8 -0
  36. package/dist/safe-fs/contracts/path.js +8 -0
  37. package/dist/safe-fs/contracts/portable-path.d.ts +13 -0
  38. package/dist/safe-fs/contracts/portable-path.js +57 -0
  39. package/dist/safe-fs/contracts/virtual-path.d.ts +7 -0
  40. package/dist/safe-fs/contracts/virtual-path.js +66 -0
  41. package/dist/safe-fs/core.d.ts +12 -0
  42. package/dist/safe-fs/core.js +12 -0
  43. package/dist/safe-fs/fs/memory/index.d.ts +58 -0
  44. package/dist/safe-fs/fs/memory/index.js +532 -0
  45. package/dist/safe-fs/fs/mount/comparison.d.ts +19 -0
  46. package/dist/safe-fs/fs/mount/comparison.js +118 -0
  47. package/dist/safe-fs/fs/mount/identity.d.ts +2 -0
  48. package/dist/safe-fs/fs/mount/identity.js +12 -0
  49. package/dist/safe-fs/fs/mount/index.d.ts +44 -0
  50. package/dist/safe-fs/fs/mount/index.js +546 -0
  51. package/dist/safe-fs/fs/overlay/index.d.ts +73 -0
  52. package/dist/safe-fs/fs/overlay/index.js +927 -0
  53. package/dist/safe-fs/fs/readonly/index.d.ts +31 -0
  54. package/dist/safe-fs/fs/readonly/index.js +128 -0
  55. package/dist/safe-fs/fs/real/allocation.d.ts +1 -0
  56. package/dist/safe-fs/fs/real/allocation.js +8 -0
  57. package/dist/safe-fs/fs/real/index.d.ts +76 -0
  58. package/dist/safe-fs/fs/real/index.js +543 -0
  59. package/dist/safe-fs/fs/s3/authority.d.ts +7 -0
  60. package/dist/safe-fs/fs/s3/authority.js +31 -0
  61. package/dist/safe-fs/fs/s3/filesystem.d.ts +93 -0
  62. package/dist/safe-fs/fs/s3/filesystem.js +1024 -0
  63. package/dist/safe-fs/fs/s3/http/index.d.ts +2 -0
  64. package/dist/safe-fs/fs/s3/http/index.js +1 -0
  65. package/dist/safe-fs/fs/s3/http/request.d.ts +16 -0
  66. package/dist/safe-fs/fs/s3/http/request.js +196 -0
  67. package/dist/safe-fs/fs/s3/http/signature.d.ts +20 -0
  68. package/dist/safe-fs/fs/s3/http/signature.js +74 -0
  69. package/dist/safe-fs/fs/s3/http/transport.d.ts +3 -0
  70. package/dist/safe-fs/fs/s3/http/transport.js +455 -0
  71. package/dist/safe-fs/fs/s3/http/types.d.ts +30 -0
  72. package/dist/safe-fs/fs/s3/http/types.js +1 -0
  73. package/dist/safe-fs/fs/s3/http/xml.d.ts +11 -0
  74. package/dist/safe-fs/fs/s3/http/xml.js +188 -0
  75. package/dist/safe-fs/fs/s3/index.d.ts +6 -0
  76. package/dist/safe-fs/fs/s3/index.js +3 -0
  77. package/dist/safe-fs/fs/s3/mock.d.ts +48 -0
  78. package/dist/safe-fs/fs/s3/mock.js +233 -0
  79. package/dist/safe-fs/fs/s3/registry.d.ts +10 -0
  80. package/dist/safe-fs/fs/s3/registry.js +71 -0
  81. package/dist/safe-fs/fs/s3/transport.d.ts +103 -0
  82. package/dist/safe-fs/fs/s3/transport.js +28 -0
  83. package/dist/safe-fs/fs/webdav/index.d.ts +2 -0
  84. package/dist/safe-fs/fs/webdav/index.js +1 -0
  85. package/dist/safe-fs/fs/webdav/resource-id.d.ts +18 -0
  86. package/dist/safe-fs/fs/webdav/resource-id.js +109 -0
  87. package/dist/safe-fs/fs/webdav/webdav.d.ts +101 -0
  88. package/dist/safe-fs/fs/webdav/webdav.js +1226 -0
  89. package/dist/safe-fs/fs/webdav/xml.d.ts +15 -0
  90. package/dist/safe-fs/fs/webdav/xml.js +250 -0
  91. package/dist/safe-fs/index.d.ts +14 -0
  92. package/dist/safe-fs/index.js +12 -0
  93. package/dist/safe-fs/node/filesystem.d.ts +11 -0
  94. package/dist/safe-fs/node/filesystem.js +28 -0
  95. package/dist/safe-fs/node/index.d.ts +2 -0
  96. package/dist/safe-fs/node/index.js +1 -0
  97. package/dist/safe-fs/node/stats.d.ts +1 -0
  98. package/dist/safe-fs/node/stats.js +1 -0
  99. package/dist/safe-fs/node/values.d.ts +1 -0
  100. package/dist/safe-fs/node/values.js +1 -0
  101. package/dist/safe-fs/node-host.d.ts +1 -0
  102. package/dist/safe-fs/node-host.js +1 -0
  103. package/dist/safe-fs/node-unavailable.d.ts +1 -0
  104. package/dist/safe-fs/node-unavailable.js +1 -0
  105. package/dist/safe-fs/platform/browser.d.ts +12 -0
  106. package/dist/safe-fs/platform/browser.js +39 -0
  107. package/dist/safe-fs/platform/node.d.ts +13 -0
  108. package/dist/safe-fs/platform/node.js +26 -0
  109. package/package.json +108 -0
@@ -0,0 +1,411 @@
1
+ import { composeAbortSignals } from "../contracts/abort.js";
2
+ import { bridgeDirent, bridgeStats } from "./stats.js";
3
+ import { booleanValue, checkSignal, onlyKeys, record, withSignal } from "./values.js";
4
+ import { assertBridgePath, checkedBridgePath } from "./confinement.js";
5
+ function fsError(code, syscall, path) {
6
+ return Object.assign(new Error(`${code}: ${syscall}${path === undefined ? "" : ` '${path}'`}`), {
7
+ code,
8
+ syscall,
9
+ ...(path === undefined ? {} : { path }),
10
+ });
11
+ }
12
+ function unsupported(operation) {
13
+ throw fsError("ENOTSUP", operation);
14
+ }
15
+ function hasCode(error, code) {
16
+ return error instanceof Error && "code" in error && error.code === code;
17
+ }
18
+ function childPath(parent, name) {
19
+ return `${parent.replace(/\/$/u, "")}/${name}`;
20
+ }
21
+ function optionsRecord(value, allowed) {
22
+ const options = value == null ? {} : typeof value === "string" ? { encoding: value } : record(value, "options");
23
+ onlyKeys(options, allowed);
24
+ return options;
25
+ }
26
+ function modeValue(value, fallback) {
27
+ if (value === undefined)
28
+ return fallback;
29
+ const parsed = typeof value === "string" && /^[0-7]+$/u.test(value) ? Number.parseInt(value, 8) : value;
30
+ if (typeof parsed !== "number" || !Number.isInteger(parsed) || parsed < 0 || parsed > 0o7777) {
31
+ throw new TypeError("Invalid file mode");
32
+ }
33
+ return parsed;
34
+ }
35
+ function timeValue(value) {
36
+ const number = value instanceof Date ? value.getTime() : Number(value) * 1000;
37
+ if ((typeof value !== "string" && typeof value !== "number" && !(value instanceof Date)) || !Number.isFinite(number)) {
38
+ throw new TypeError("Invalid timestamp");
39
+ }
40
+ return number;
41
+ }
42
+ export class FileSystemBridge {
43
+ #fs;
44
+ #cwd;
45
+ #signal;
46
+ #primitives;
47
+ #codec;
48
+ constructor(fs, options, primitives) {
49
+ if (fs === undefined)
50
+ throw new TypeError("An explicit filesystem is required");
51
+ const cwd = options.cwd ?? "/";
52
+ if (!primitives.paths.isAbsolute(cwd) || cwd.includes("\0"))
53
+ throw new TypeError("cwd must be an absolute virtual path");
54
+ const codec = primitives.codec;
55
+ if (codec === undefined || codec === null || typeof codec.isEncoding !== "function" || typeof codec.encode !== "function" || typeof codec.decode !== "function") {
56
+ throw new TypeError("An explicit filesystem codec is required");
57
+ }
58
+ this.#primitives = primitives;
59
+ this.#codec = Object.freeze({
60
+ isEncoding: codec.isEncoding.bind(codec),
61
+ encode: codec.encode.bind(codec),
62
+ decode: codec.decode.bind(codec)
63
+ });
64
+ this.#fs = fs;
65
+ this.#cwd = primitives.paths.resolve("/", cwd);
66
+ this.#signal = options.signal;
67
+ }
68
+ #encoding(value, fallback, names = false) {
69
+ const selected = value == null ? fallback : value;
70
+ if (typeof selected !== "string" || (selected !== "buffer" && !this.#codec.isEncoding(selected)) || (names && !this.#codec.isEncoding("utf8"))) {
71
+ throw new TypeError("Invalid encoding");
72
+ }
73
+ return selected;
74
+ }
75
+ #textBytes(value) {
76
+ return this.#primitives.copyBytes(this.#codec.encode(value, "utf8"));
77
+ }
78
+ #path(value) {
79
+ const path = this.#primitives.pathValue === undefined ? value : this.#primitives.pathValue(value);
80
+ if (typeof path !== "string" || path.includes("\0"))
81
+ throw new TypeError("Expected a path without NUL bytes; file handles are unsupported");
82
+ if (path.length === 0)
83
+ throw fsError("ENOENT", "path", path);
84
+ checkSignal(this.#signal);
85
+ const absolute = this.#primitives.paths.isAbsolute(path) ? path : childPath(this.#cwd, path);
86
+ assertBridgePath(this.#cwd, absolute);
87
+ return absolute;
88
+ }
89
+ async #call(paths, operation, signal, noFollow = []) {
90
+ if (signal !== undefined && !(signal instanceof AbortSignal))
91
+ throw new TypeError("Invalid AbortSignal");
92
+ const signalScope = signal !== undefined && this.#signal !== undefined && signal !== this.#signal
93
+ ? composeAbortSignals([signal, this.#signal]) : undefined;
94
+ const combined = signalScope?.signal ?? signal ?? this.#signal;
95
+ try {
96
+ checkSignal(combined);
97
+ const options = combined === undefined ? {} : { signal: combined };
98
+ return await withSignal(combined, async () => {
99
+ const absolute = paths.map((path) => this.#path(path));
100
+ const resolved = [...absolute];
101
+ if (this.#cwd !== "/") {
102
+ for (let index = 0; index < absolute.length; index++) {
103
+ resolved[index] = await checkedBridgePath(this.#fs, this.#cwd, absolute[index], options, !noFollow.includes(index));
104
+ }
105
+ }
106
+ checkSignal(combined);
107
+ return operation(options, resolved);
108
+ });
109
+ }
110
+ finally {
111
+ signalScope?.dispose();
112
+ }
113
+ }
114
+ async readFile(path, value) {
115
+ const options = optionsRecord(value, ["encoding", "flag", "signal"]);
116
+ if (options.flag !== undefined && options.flag !== "r")
117
+ unsupported("readFile flag");
118
+ if (options.encoding === "buffer")
119
+ throw new TypeError("Invalid read encoding");
120
+ const codec = this.#encoding(options.encoding, "buffer");
121
+ const bytes = await this.#call([path], (signal, resolved) => this.#fs.readFile(resolved[0], signal), options.signal);
122
+ const buffer = this.#primitives.copyBytes(bytes);
123
+ return codec === "buffer" ? buffer : this.#codec.decode(buffer, codec);
124
+ }
125
+ async #write(path, data, value, fallback) {
126
+ const options = optionsRecord(value, ["encoding", "flag", "mode", "flush", "signal"]);
127
+ if (booleanValue(options.flush))
128
+ unsupported("writeFile flush");
129
+ const codec = this.#encoding(options.encoding, "utf8");
130
+ if (codec === "buffer")
131
+ throw new TypeError("Invalid write encoding");
132
+ const flag = options.flag ?? fallback;
133
+ if (flag !== "w" && flag !== "wx" && flag !== "a" && flag !== "ax")
134
+ unsupported("writeFile flag");
135
+ const mode = modeValue(options.mode, 0o666);
136
+ let bytes;
137
+ if (typeof data === "string")
138
+ bytes = this.#primitives.copyBytes(this.#codec.encode(data, codec));
139
+ else if (ArrayBuffer.isView(data))
140
+ bytes = this.#primitives.copyBytes(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
141
+ else
142
+ throw new TypeError("writeFile accepts a string or ArrayBuffer view; streams and file handles are unsupported");
143
+ await this.#call([path], (signal, resolved) => this.#fs.writeFile(resolved[0], bytes, { ...signal, flag, mode }), options.signal);
144
+ }
145
+ async writeFile(path, data, options) {
146
+ await this.#write(path, data, options, "w");
147
+ }
148
+ async appendFile(path, data, options) {
149
+ await this.#write(path, data, options, "a");
150
+ }
151
+ async stat(path, value) {
152
+ const options = optionsRecord(value, ["bigint"]);
153
+ if (booleanValue(options.bigint))
154
+ unsupported("stat bigint");
155
+ return bridgeStats(await this.#call([path], (signal, resolved) => this.#fs.stat(resolved[0], signal)));
156
+ }
157
+ async lstat(path, value) {
158
+ const options = optionsRecord(value, ["bigint"]);
159
+ if (booleanValue(options.bigint))
160
+ unsupported("lstat bigint");
161
+ return bridgeStats(await this.#call([path], (signal, resolved) => this.#fs.lstat(resolved[0], signal), undefined, [0]));
162
+ }
163
+ async readdir(path, value) {
164
+ const options = optionsRecord(value, ["encoding", "withFileTypes", "recursive"]);
165
+ const codec = this.#encoding(options.encoding, "utf8", true);
166
+ const withFileTypes = booleanValue(options.withFileTypes);
167
+ const recursive = booleanValue(options.recursive);
168
+ const root = this.#path(path);
169
+ const entries = [];
170
+ const pending = [root];
171
+ while (pending.length > 0) {
172
+ const directory = pending.shift();
173
+ if (directory === undefined)
174
+ break;
175
+ for (const entry of await this.#call([directory], (signal, resolved) => this.#fs.readdir(resolved[0], signal))) {
176
+ if (entry.name === "" || entry.name === "." || entry.name === ".." || entry.name.includes("/") || entry.name.includes("\0")) {
177
+ throw new TypeError("Invalid directory entry from filesystem");
178
+ }
179
+ const fullPath = childPath(directory, entry.name);
180
+ entries.push({ ...entry, parentPath: directory, relative: this.#primitives.paths.relative(root, fullPath) });
181
+ if (recursive && entry.type === "directory")
182
+ pending.push(fullPath);
183
+ }
184
+ }
185
+ if (withFileTypes) {
186
+ return codec === "buffer"
187
+ ? entries.map((entry) => bridgeDirent(this.#textBytes(entry.name), entry.parentPath, entry.type))
188
+ : entries.map((entry) => bridgeDirent(this.#codec.decode(this.#textBytes(entry.name), codec), entry.parentPath, entry.type));
189
+ }
190
+ return codec === "buffer"
191
+ ? entries.map((entry) => this.#textBytes(entry.relative))
192
+ : entries.map((entry) => this.#codec.decode(this.#textBytes(entry.relative), codec));
193
+ }
194
+ async mkdir(path, value) {
195
+ const options = typeof value === "number" || typeof value === "string" ? { mode: value } : optionsRecord(value, ["mode", "recursive"]);
196
+ const mode = modeValue(options.mode, 0o777);
197
+ const recursive = booleanValue(options.recursive);
198
+ const target = this.#path(path);
199
+ let firstCreated;
200
+ if (recursive) {
201
+ let candidate = target;
202
+ while (true) {
203
+ try {
204
+ await this.#call([candidate], (signal, resolved) => this.#fs.stat(resolved[0], signal));
205
+ break;
206
+ }
207
+ catch (error) {
208
+ if (!hasCode(error, "ENOENT"))
209
+ throw error;
210
+ firstCreated = candidate;
211
+ if (candidate === this.#cwd)
212
+ break;
213
+ const parent = this.#primitives.paths.dirname(candidate);
214
+ if (parent === candidate)
215
+ break;
216
+ candidate = parent;
217
+ }
218
+ }
219
+ }
220
+ await this.#call([target], (signal, resolved) => this.#fs.mkdir(resolved[0], { ...signal, mode, recursive }));
221
+ return firstCreated;
222
+ }
223
+ async access(path, mode = 0) {
224
+ if (!Number.isInteger(mode) || mode < 0 || mode > 7)
225
+ throw new TypeError("Invalid access mode");
226
+ await this.#call([path], (signal, resolved) => this.#fs.access(resolved[0], mode, signal));
227
+ }
228
+ async rm(path, value) {
229
+ const options = optionsRecord(value, ["force", "recursive", "maxRetries", "retryDelay"]);
230
+ if (options.maxRetries !== undefined && options.maxRetries !== 0)
231
+ unsupported("rm retries");
232
+ if (options.retryDelay !== undefined)
233
+ unsupported("rm retryDelay");
234
+ const recursive = booleanValue(options.recursive);
235
+ const force = booleanValue(options.force);
236
+ await this.#call([path], (signal, resolved) => this.#fs.rm(resolved[0], { ...signal, recursive, force }), undefined, [0]);
237
+ }
238
+ async rmdir(path, value) {
239
+ const options = optionsRecord(value, ["recursive", "maxRetries", "retryDelay"]);
240
+ const stat = await this.lstat(path);
241
+ if (!stat.isDirectory())
242
+ throw fsError("ENOTDIR", "rmdir", this.#path(path));
243
+ if (!booleanValue(options.recursive)) {
244
+ if ((await this.readdir(path)).length > 0)
245
+ throw fsError("ENOTEMPTY", "rmdir", this.#path(path));
246
+ if (options.maxRetries !== undefined && options.maxRetries !== 0)
247
+ unsupported("rmdir retries");
248
+ if (options.retryDelay !== undefined)
249
+ unsupported("rmdir retryDelay");
250
+ const method = this.#fs.rmdir;
251
+ if (method === undefined)
252
+ unsupported("atomic rmdir");
253
+ await this.#call([path], (signal, resolved) => method.call(this.#fs, resolved[0], signal), undefined, [0]);
254
+ }
255
+ else
256
+ await this.rm(path, options);
257
+ }
258
+ async rename(source, destination) {
259
+ await this.#call([source, destination], (signal, resolved) => this.#fs.rename(resolved[0], resolved[1], signal), undefined, [0, 1]);
260
+ }
261
+ async copyFile(source, destination, mode = 0) {
262
+ if (mode !== 0 && mode !== 1)
263
+ unsupported("copyFile mode");
264
+ await this.#call([source, destination], (signal, resolved) => this.#fs.copyFile(resolved[0], resolved[1], { ...signal, exclusive: mode === 1 }));
265
+ }
266
+ async cp(source, destination, value) {
267
+ const options = optionsRecord(value, ["recursive", "force", "errorOnExist", "mode", "dereference", "preserveTimestamps", "verbatimSymlinks"]);
268
+ if (booleanValue(options.dereference) || booleanValue(options.preserveTimestamps) || booleanValue(options.verbatimSymlinks))
269
+ unsupported("cp options");
270
+ const recursive = booleanValue(options.recursive);
271
+ const force = booleanValue(options.force, true);
272
+ const errorOnExist = booleanValue(options.errorOnExist);
273
+ if (options.mode !== undefined && options.mode !== 0 && options.mode !== 1)
274
+ unsupported("cp mode");
275
+ const from = this.#path(source);
276
+ const to = this.#path(destination);
277
+ const canonicalFrom = await this.realpath(from);
278
+ let ancestor = to === this.#cwd ? to : this.#primitives.paths.dirname(to);
279
+ let canonicalAncestor;
280
+ while (true) {
281
+ try {
282
+ canonicalAncestor = await this.realpath(ancestor);
283
+ break;
284
+ }
285
+ catch (error) {
286
+ if (!hasCode(error, "ENOENT"))
287
+ throw error;
288
+ const parent = this.#primitives.paths.dirname(ancestor);
289
+ if (parent === ancestor)
290
+ throw error;
291
+ ancestor = parent;
292
+ }
293
+ }
294
+ const canonicalTo = this.#primitives.paths.resolve(canonicalAncestor, this.#primitives.paths.relative(ancestor, to));
295
+ if (canonicalFrom === "/" || canonicalTo === canonicalFrom || canonicalTo.startsWith(`${canonicalFrom}/`))
296
+ throw fsError("EINVAL", "cp", to);
297
+ const copy = async (current, target) => {
298
+ const sourceStat = await this.lstat(current);
299
+ if (sourceStat.isSymbolicLink())
300
+ unsupported("cp symlink");
301
+ let destinationStat;
302
+ try {
303
+ destinationStat = await this.lstat(target);
304
+ }
305
+ catch (error) {
306
+ if (!hasCode(error, "ENOENT"))
307
+ throw error;
308
+ }
309
+ if (destinationStat?.isSymbolicLink())
310
+ unsupported("cp destination symlink");
311
+ if (sourceStat.isDirectory()) {
312
+ if (!recursive)
313
+ throw fsError("EISDIR", "cp", current);
314
+ if (destinationStat !== undefined && !destinationStat.isDirectory())
315
+ throw fsError("ENOTDIR", "cp", target);
316
+ await this.mkdir(target, { recursive: true, mode: sourceStat.mode & 0o7777 });
317
+ for (const entry of await this.readdir(current))
318
+ await copy(childPath(current, entry), childPath(target, entry));
319
+ }
320
+ else {
321
+ if (destinationStat?.isDirectory())
322
+ throw fsError("EISDIR", "cp", target);
323
+ if (destinationStat !== undefined && !force) {
324
+ if (errorOnExist)
325
+ throw fsError("EEXIST", "cp", target);
326
+ return;
327
+ }
328
+ await this.mkdir(this.#primitives.paths.dirname(target), { recursive: true });
329
+ await this.copyFile(current, target, options.mode === 1 || !force ? 1 : 0);
330
+ }
331
+ };
332
+ await copy(from, to);
333
+ }
334
+ async readlink(path, value) {
335
+ const codec = this.#encoding(optionsRecord(value, ["encoding"]).encoding, "utf8", true);
336
+ const method = this.#fs.readlink;
337
+ if (method === undefined)
338
+ unsupported("readlink");
339
+ const target = await this.#call([path], (signal, resolved) => method.call(this.#fs, resolved[0], signal), undefined, [0]);
340
+ return codec === "buffer" ? this.#textBytes(target) : this.#codec.decode(this.#textBytes(target), codec);
341
+ }
342
+ async realpath(path, value) {
343
+ const codec = this.#encoding(optionsRecord(value, ["encoding"]).encoding, "utf8", true);
344
+ const target = await this.#call([path], (signal, resolved) => this.#fs.realpath(resolved[0], signal));
345
+ assertBridgePath(this.#cwd, target);
346
+ return codec === "buffer" ? this.#textBytes(target) : this.#codec.decode(this.#textBytes(target), codec);
347
+ }
348
+ async mkdtemp(prefix, value) {
349
+ if (typeof prefix !== "string" || prefix.includes("\0"))
350
+ throw new TypeError("Invalid mkdtemp prefix");
351
+ const codec = this.#encoding(optionsRecord(value, ["encoding"]).encoding, "utf8", true);
352
+ for (let attempt = 0; attempt < 10; attempt += 1) {
353
+ const path = prefix + this.#primitives.randomSuffix();
354
+ try {
355
+ await this.mkdir(path, { mode: 0o700 });
356
+ return codec === "buffer" ? this.#textBytes(path) : this.#codec.decode(this.#textBytes(path), codec);
357
+ }
358
+ catch (error) {
359
+ if (!hasCode(error, "EEXIST"))
360
+ throw error;
361
+ }
362
+ }
363
+ throw fsError("EEXIST", "mkdtemp", prefix);
364
+ }
365
+ async symlink(target, path, type) {
366
+ if (type !== undefined && type !== null && type !== "file" && type !== "dir")
367
+ unsupported("symlink type");
368
+ const linkTarget = this.#primitives.pathValue === undefined ? target : this.#primitives.pathValue(target);
369
+ if (typeof linkTarget !== "string" || linkTarget.includes("\0"))
370
+ throw new TypeError("Invalid symlink target");
371
+ const destination = this.#path(path);
372
+ const absoluteTarget = this.#path(this.#primitives.paths.isAbsolute(linkTarget)
373
+ ? linkTarget : childPath(this.#primitives.paths.dirname(destination), linkTarget));
374
+ if (this.#cwd !== "/" && this.#primitives.paths.isAbsolute(linkTarget)) {
375
+ throw fsError("ENOTSUP", "symlink", destination);
376
+ }
377
+ const method = this.#fs.symlink;
378
+ if (method === undefined)
379
+ unsupported("symlink");
380
+ await this.#call([destination, absoluteTarget], (signal, resolved) => method.call(this.#fs, linkTarget, resolved[0], signal), undefined, [0]);
381
+ }
382
+ async link(existing, path) {
383
+ const method = this.#fs.link;
384
+ if (method === undefined)
385
+ unsupported("link");
386
+ await this.#call([existing, path], (signal, resolved) => method.call(this.#fs, resolved[0], resolved[1], signal), undefined, [1]);
387
+ }
388
+ async chmod(path, value) {
389
+ const mode = modeValue(value, 0);
390
+ const method = this.#fs.chmod;
391
+ if (method === undefined)
392
+ unsupported("chmod");
393
+ await this.#call([path], (signal, resolved) => method.call(this.#fs, resolved[0], mode, signal));
394
+ }
395
+ async utimes(path, atime, mtime) {
396
+ const accessTime = timeValue(atime);
397
+ const modificationTime = timeValue(mtime);
398
+ const method = this.#fs.utimes;
399
+ if (method === undefined)
400
+ unsupported("utimes");
401
+ await this.#call([path], (signal, resolved) => method.call(this.#fs, resolved[0], accessTime, modificationTime, signal));
402
+ }
403
+ async truncate(path, length = 0) {
404
+ if (!Number.isInteger(length))
405
+ throw new TypeError("Invalid truncate length");
406
+ const method = this.#fs.truncate;
407
+ if (method === undefined)
408
+ unsupported("truncate");
409
+ await this.#call([path], (signal, resolved) => method.call(this.#fs, resolved[0], Math.max(0, length), signal));
410
+ }
411
+ }
@@ -0,0 +1,6 @@
1
+ import type { FileSystem } from "../contracts/filesystem.js";
2
+ import { FileSystemBridge } from "./filesystem.js";
3
+ import type { FsBridgeOptions } from "./types.js";
4
+ export type { FsBridgeCodec, FsBridgeDirent, FsBridgeEncoding, FsBridgeFileSystem, FsBridgeOptions, FsBridgeStats } from "./types.js";
5
+ export type FsBridge = FileSystemBridge<Uint8Array<ArrayBuffer>>;
6
+ export declare function createFsBridge(fs: FileSystem, options: FsBridgeOptions): FsBridge;
@@ -0,0 +1,18 @@
1
+ import { platform } from "#safe-fs-platform";
2
+ import { dirname, relativePath, resolvePath } from "../contracts/virtual-path.js";
3
+ import { FileSystemBridge } from "./filesystem.js";
4
+ export function createFsBridge(fs, options) {
5
+ if (options === undefined || options === null)
6
+ throw new TypeError("An explicit filesystem codec is required");
7
+ return new FileSystemBridge(fs, options, {
8
+ codec: options.codec,
9
+ copyBytes(bytes) { return new Uint8Array(bytes); },
10
+ randomSuffix() { return platform.randomUUID().slice(0, 6); },
11
+ paths: {
12
+ isAbsolute(path) { return typeof path === "string" && path.startsWith("/"); },
13
+ dirname,
14
+ relative: relativePath,
15
+ resolve: resolvePath
16
+ }
17
+ });
18
+ }
@@ -0,0 +1,4 @@
1
+ import type { FsBridgeDirent, FsBridgeStats } from "./types.js";
2
+ import type { FileStat, FileType } from "../contracts/filesystem.js";
3
+ export declare function bridgeStats(stat: FileStat): FsBridgeStats;
4
+ export declare function bridgeDirent<Name extends string | Uint8Array>(name: Name, parentPath: string, type: FileType): FsBridgeDirent<Name>;
@@ -0,0 +1,37 @@
1
+ function predicates(type) {
2
+ return {
3
+ isFile: () => type === "file",
4
+ isDirectory: () => type === "directory",
5
+ isSymbolicLink: () => type === "symlink",
6
+ isBlockDevice: () => false,
7
+ isCharacterDevice: () => false,
8
+ isFIFO: () => false,
9
+ isSocket: () => false,
10
+ };
11
+ }
12
+ export function bridgeStats(stat) {
13
+ return {
14
+ dev: stat.dev ?? 0,
15
+ ino: stat.ino ?? 0,
16
+ mode: (stat.mode & 0o7777) | (stat.type === "directory" ? 0o040000 : stat.type === "symlink" ? 0o120000 : 0o100000),
17
+ nlink: stat.nlink ?? 1,
18
+ uid: stat.uid ?? 0,
19
+ gid: stat.gid ?? 0,
20
+ rdev: 0,
21
+ size: stat.size,
22
+ blksize: 4096,
23
+ blocks: Math.ceil(stat.size / 512),
24
+ atimeMs: stat.atimeMs,
25
+ mtimeMs: stat.mtimeMs,
26
+ ctimeMs: stat.ctimeMs,
27
+ birthtimeMs: stat.birthtimeMs ?? stat.ctimeMs,
28
+ atime: new Date(stat.atimeMs),
29
+ mtime: new Date(stat.mtimeMs),
30
+ ctime: new Date(stat.ctimeMs),
31
+ birthtime: new Date(stat.birthtimeMs ?? stat.ctimeMs),
32
+ ...predicates(stat.type),
33
+ };
34
+ }
35
+ export function bridgeDirent(name, parentPath, type) {
36
+ return { name, parentPath, path: parentPath, ...predicates(type) };
37
+ }
@@ -0,0 +1,75 @@
1
+ import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
2
+ export interface FsBridgeCodec {
3
+ isEncoding(encoding: string): boolean;
4
+ encode(text: string, encoding: string): Uint8Array;
5
+ decode(bytes: Uint8Array, encoding: string): string;
6
+ }
7
+ export interface FsBridgeOptions {
8
+ readonly codec: FsBridgeCodec;
9
+ readonly cwd?: string;
10
+ readonly signal?: AbortSignal;
11
+ }
12
+ export interface FsBridgeFileSystem extends FileSystem {
13
+ rmdir?(path: string, options?: FsOptions): Promise<void>;
14
+ }
15
+ export type FsBridgeEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "utf-16le" | "ucs2" | "ucs-2" | "base64" | "base64url" | "latin1" | "binary" | "hex";
16
+ export interface ObjectEncodingOptions {
17
+ encoding?: FsBridgeEncoding | null | undefined;
18
+ }
19
+ export type BufferEncodingOption = "buffer" | {
20
+ encoding: "buffer";
21
+ };
22
+ export type Mode = number | string;
23
+ export interface MakeDirectoryOptions {
24
+ recursive?: boolean | undefined;
25
+ mode?: Mode | undefined;
26
+ }
27
+ export interface StatOptions {
28
+ bigint?: boolean | undefined;
29
+ }
30
+ export interface FsBridgePredicates {
31
+ isFile(): boolean;
32
+ isDirectory(): boolean;
33
+ isSymbolicLink(): boolean;
34
+ isBlockDevice(): boolean;
35
+ isCharacterDevice(): boolean;
36
+ isFIFO(): boolean;
37
+ isSocket(): boolean;
38
+ }
39
+ export interface FsBridgeStats extends FsBridgePredicates {
40
+ dev: number;
41
+ ino: number;
42
+ mode: number;
43
+ nlink: number;
44
+ uid: number;
45
+ gid: number;
46
+ rdev: number;
47
+ size: number;
48
+ blksize: number;
49
+ blocks: number;
50
+ atimeMs: number;
51
+ mtimeMs: number;
52
+ ctimeMs: number;
53
+ birthtimeMs: number;
54
+ atime: Date;
55
+ mtime: Date;
56
+ ctime: Date;
57
+ birthtime: Date;
58
+ }
59
+ export interface FsBridgeDirent<Name extends string | Uint8Array = string> extends FsBridgePredicates {
60
+ name: Name;
61
+ parentPath: string;
62
+ path: string;
63
+ }
64
+ export interface BridgePrimitives<Binary extends Uint8Array> {
65
+ readonly codec: FsBridgeCodec;
66
+ copyBytes(bytes: Uint8Array): Binary;
67
+ pathValue?(value: unknown): unknown;
68
+ randomSuffix(): string;
69
+ readonly paths: {
70
+ isAbsolute(path: string): boolean;
71
+ dirname(path: string): string;
72
+ relative(from: string, to: string): string;
73
+ resolve(cwd: string, ...paths: string[]): string;
74
+ };
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export declare function record(value: unknown, name: string): Record<string, unknown>;
2
+ export declare function onlyKeys(value: Record<string, unknown>, allowed: readonly string[]): void;
3
+ export declare function booleanValue(value: unknown, fallback?: boolean): boolean;
4
+ export declare function abortError(): Error;
5
+ export declare function checkSignal(signal: AbortSignal | undefined): void;
6
+ export declare function withSignal<Value>(signal: AbortSignal | undefined, operation: () => Promise<Value>): Promise<Value>;
@@ -0,0 +1,65 @@
1
+ export function record(value, name) {
2
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3
+ throw new TypeError(`${name} must be a plain object`);
4
+ }
5
+ const prototype = Object.getPrototypeOf(value);
6
+ if (prototype !== Object.prototype && prototype !== null) {
7
+ throw new TypeError(`${name} must be a plain object`);
8
+ }
9
+ const result = {};
10
+ for (const key of Reflect.ownKeys(value)) {
11
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
12
+ if (typeof key !== "string" || descriptor === undefined || !("value" in descriptor)) {
13
+ throw new TypeError(`${name} must contain only string-keyed data properties`);
14
+ }
15
+ Object.defineProperty(result, key, {
16
+ value: descriptor.value,
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ });
21
+ }
22
+ return result;
23
+ }
24
+ export function onlyKeys(value, allowed) {
25
+ for (const key of Object.keys(value)) {
26
+ if (!allowed.includes(key))
27
+ throw new TypeError(`Unsupported option: ${key}`);
28
+ }
29
+ }
30
+ export function booleanValue(value, fallback = false) {
31
+ if (value === undefined)
32
+ return fallback;
33
+ if (typeof value !== "boolean")
34
+ throw new TypeError("Expected a boolean option");
35
+ return value;
36
+ }
37
+ export function abortError() {
38
+ return Object.assign(new Error("The operation was aborted"), {
39
+ name: "AbortError",
40
+ code: "ABORT_ERR",
41
+ });
42
+ }
43
+ export function checkSignal(signal) {
44
+ if (signal?.aborted)
45
+ throw abortError();
46
+ }
47
+ export async function withSignal(signal, operation) {
48
+ checkSignal(signal);
49
+ if (signal === undefined)
50
+ return operation();
51
+ let onAbort = () => undefined;
52
+ const cancelled = new Promise((_resolve, reject) => {
53
+ onAbort = () => reject(abortError());
54
+ signal.addEventListener("abort", onAbort, { once: true });
55
+ });
56
+ try {
57
+ return await Promise.race([Promise.resolve().then(() => {
58
+ checkSignal(signal);
59
+ return operation();
60
+ }), cancelled]);
61
+ }
62
+ finally {
63
+ signal.removeEventListener("abort", onAbort);
64
+ }
65
+ }
@@ -0,0 +1,3 @@
1
+ import type { FileSystemFactory } from "../contracts/filesystem.js";
2
+ import { type FileSystemAdapterDescriptor } from "../config.js";
3
+ export declare function createMemoryFileSystemAdapter(create: FileSystemFactory): FileSystemAdapterDescriptor;
@@ -0,0 +1,9 @@
1
+ import { readConfigRecord } from "../config.js";
2
+ export function createMemoryFileSystemAdapter(create) {
3
+ return {
4
+ validateOptions(options) {
5
+ readConfigRecord(options, "memory option", []);
6
+ },
7
+ create
8
+ };
9
+ }
@@ -0,0 +1,5 @@
1
+ import type { FileSystemFactory } from "../contracts/filesystem.js";
2
+ import { type FileSystemAdapterDescriptor } from "../config.js";
3
+ export declare function createRealFileSystemAdapter(create: (options: {
4
+ readonly root: string;
5
+ }) => ReturnType<FileSystemFactory>): FileSystemAdapterDescriptor;
@@ -0,0 +1,16 @@
1
+ import { readConfigRecord } from "../config.js";
2
+ export function createRealFileSystemAdapter(create) {
3
+ return {
4
+ validateOptions(options) {
5
+ const record = readConfigRecord(options, "real option", ["root"]);
6
+ if (typeof record.root !== "string" ||
7
+ !record.root.startsWith("/") ||
8
+ record.root.includes("\0")) {
9
+ throw new TypeError("adapter.options.root must be an absolute host directory");
10
+ }
11
+ },
12
+ create(options) {
13
+ return create({ root: options.root });
14
+ }
15
+ };
16
+ }