@poe-platform/safe-fs 0.1.553 → 0.1.554

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.
@@ -0,0 +1,16 @@
1
+ import type { FileSystem } from "../contracts/filesystem.js";
2
+ export interface AtomicFileSystemConformanceFixture {
3
+ readonly fs: FileSystem;
4
+ readonly peer?: FileSystem;
5
+ readonly root: string;
6
+ dispose(): void | Promise<void>;
7
+ }
8
+ export interface AtomicFileSystemConformanceOptions {
9
+ readonly createFixture: () => AtomicFileSystemConformanceFixture | Promise<AtomicFileSystemConformanceFixture>;
10
+ readonly includeSymlinks?: boolean;
11
+ }
12
+ export interface AtomicFileSystemConformanceCase {
13
+ readonly name: string;
14
+ run(): Promise<void>;
15
+ }
16
+ export declare function createAtomicFileSystemConformance(options: AtomicFileSystemConformanceOptions): readonly AtomicFileSystemConformanceCase[];
@@ -0,0 +1,496 @@
1
+ function check(condition, message) {
2
+ if (!condition)
3
+ throw new Error(message);
4
+ }
5
+ function identity(stat, label) {
6
+ check((typeof stat.identityScope === "object" && stat.identityScope !== null) || typeof stat.identityScope === "symbol", `${label}: unavailable identityScope`);
7
+ check(Number.isSafeInteger(stat.ino) && stat.ino >= 0 && Number.isSafeInteger(stat.dev) && stat.dev >= 0, `${label}: unavailable scoped identity`);
8
+ }
9
+ function sameIdentity(actual, expected, label) {
10
+ identity(actual, label);
11
+ identity(expected, label);
12
+ for (const field of ["identityScope", "ino", "dev", "type"]) {
13
+ check(actual[field] === expected[field], `${label}: changed ${field}`);
14
+ }
15
+ }
16
+ function revision(stat, label) {
17
+ identity(stat, label);
18
+ check(Number.isSafeInteger(stat.revision) && stat.revision >= 0, `${label}: unavailable revision`);
19
+ }
20
+ function sameSnapshot(actual, expected, label) {
21
+ sameIdentity(actual, expected, label);
22
+ if (actual.type !== "directory" || actual.revision !== undefined)
23
+ revision(actual, label);
24
+ if (expected.type !== "directory" || expected.revision !== undefined)
25
+ revision(expected, label);
26
+ for (const field of ["revision", "size", "mode", "nlink", "mtimeMs", "ctimeMs"]) {
27
+ check(actual[field] === expected[field], `${label}: changed ${field}`);
28
+ }
29
+ }
30
+ async function bytes(fs, path, expected) {
31
+ const actual = await fs.readFile(path);
32
+ check(actual.length === expected.length && actual.every((value, index) => value === expected[index]), `${path}: incorrect bytes`);
33
+ }
34
+ async function entries(fs, path, expected) {
35
+ const actual = (await fs.readdir(path)).map(entry => entry.name).sort();
36
+ const names = [...expected].sort();
37
+ check(actual.length === names.length && actual.every((name, index) => name === names[index]), `${path}: unexpected directory entries (${actual.join(", ")})`);
38
+ }
39
+ async function refuses(operation, codes) {
40
+ try {
41
+ await operation();
42
+ }
43
+ catch (error) {
44
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
45
+ check(typeof code === "string" && codes.includes(code), `expected ${codes.join(" or ")}, received ${String(code)}`);
46
+ return;
47
+ }
48
+ throw new Error(`expected rejection (${codes.join(" or ")})`);
49
+ }
50
+ async function validate(fs, root, symlinks) {
51
+ const profiles = {
52
+ atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
53
+ atomicFileMutation: ["writeFileConditional", "removeFileConditional"],
54
+ atomicDirectoryMetadata: ["prepareDirectory"],
55
+ };
56
+ const capabilities = fs.capabilitiesFor ? await fs.capabilitiesFor(root) : fs.capabilities;
57
+ for (const [capability, methods] of Object.entries(profiles)) {
58
+ check(fs.capabilities[capability] === true && capabilities[capability] === true, `required capability ${capability} is unavailable`);
59
+ for (const method of methods)
60
+ check(typeof fs[method] === "function", `required method ${method} is unavailable`);
61
+ }
62
+ if (symlinks) {
63
+ for (const capability of ["symlinks", "readlink"]) {
64
+ check(fs.capabilities[capability] === true && capabilities[capability] === true, `selected capability ${capability} is unavailable`);
65
+ }
66
+ for (const method of ["symlink", "readlink"])
67
+ check(typeof fs[method] === "function", `selected method ${method} is unavailable`);
68
+ }
69
+ const stat = await fs.lstat(root);
70
+ check(stat.type === "directory", "fixture root must be an owned directory, not a symlink");
71
+ identity(stat, "fixture root");
72
+ check((await fs.readdir(root)).length === 0, "fixture root must be isolated and empty");
73
+ return fs;
74
+ }
75
+ async function stage(context, name = ".stage", content = { type: "file", data: Uint8Array.of(0, 255, 1, 128) }) {
76
+ const { fs, root, path } = context;
77
+ const parent = await fs.lstat(root);
78
+ const receipt = await fs.createStagedFile(path(name), "file", content, { parent });
79
+ check(receipt.parent.path === root && receipt.directory.path === path(name) && receipt.file.path === path(`${name}/file`), "staging receipt paths must remain in the owned root");
80
+ sameIdentity(receipt.parent.stat, parent, "staging parent receipt");
81
+ check(receipt.directory.stat.type === "directory", "staging directory receipt type");
82
+ check(receipt.file.stat.type === content.type, "staging file receipt type");
83
+ sameSnapshot(receipt.directory.stat, await fs.lstat(receipt.directory.path), "staging directory receipt");
84
+ sameSnapshot(receipt.file.stat, await fs.lstat(receipt.file.path), "staging file receipt");
85
+ return receipt;
86
+ }
87
+ async function winner(operations, clients, path, candidates) {
88
+ const results = await Promise.allSettled(operations);
89
+ check(results.filter(result => result.status === "fulfilled").length === 1, "conditional mutation must have exactly one winner");
90
+ for (const [index, result] of results.entries()) {
91
+ if (result.status === "fulfilled") {
92
+ const fs = clients[index];
93
+ sameSnapshot(result.value, await fs.lstat(path), "winner receipt");
94
+ await bytes(fs, path, candidates[index]);
95
+ }
96
+ else {
97
+ check(result.reason?.code === "EAGAIN", "conditional mutation loser must reject EAGAIN");
98
+ }
99
+ }
100
+ }
101
+ export function createAtomicFileSystemConformance(options) {
102
+ const cases = [];
103
+ const add = (name, exercise) => {
104
+ cases.push(Object.freeze({ name, async run() {
105
+ const fixture = await options.createFixture();
106
+ try {
107
+ const { root } = fixture;
108
+ check(root.startsWith("/") && !root.includes("\0") && (root === "/" || root.slice(1).split("/").every(part => part !== "" && part !== "." && part !== "..")), "fixture root must be a normalized absolute path");
109
+ const fs = await validate(fixture.fs, root, options.includeSymlinks === true);
110
+ const peer = fixture.peer ? await validate(fixture.peer, root, options.includeSymlinks === true) : fs;
111
+ await exercise({ fs, peer, root, path: name => `${root === "/" ? "" : root}/${name}` });
112
+ }
113
+ finally {
114
+ await fixture.dispose();
115
+ }
116
+ } }));
117
+ };
118
+ add("conditional receipts preserve original identity and revision", async ({ fs, root, path }) => {
119
+ const parent = await fs.lstat(root);
120
+ const receipt = await fs.writeFileConditional(path("file"), Uint8Array.of(1, 2), { parent, expected: null, mode: 0o600 });
121
+ check(receipt.type === "file", "conditional receipt must be a regular file");
122
+ sameSnapshot(receipt, await fs.lstat(path("file")), "creation receipt");
123
+ const original = { ...receipt };
124
+ const updated = await fs.writeFileConditional(path("file"), Uint8Array.of(3, 4), { parent, expected: receipt });
125
+ sameIdentity(updated, receipt, "updated file");
126
+ check(updated.revision !== original.revision, "write must advance revision");
127
+ sameSnapshot(receipt, original, "original receipt must not mutate");
128
+ sameSnapshot(updated, await fs.lstat(path("file")), "update receipt");
129
+ await bytes(fs, path("file"), Uint8Array.of(3, 4));
130
+ await fs.removeFileConditional(path("file"), { parent, expected: updated });
131
+ await entries(fs, root, []);
132
+ });
133
+ for (const existing of [false, true])
134
+ add(`conditional ${existing ? "update" : "create"} has one winner`, async ({ fs, peer, root, path }) => {
135
+ const parent = await fs.lstat(root);
136
+ if (existing)
137
+ await fs.writeFileConditional(path("file"), Uint8Array.of(0), { parent, expected: null });
138
+ const expected = existing ? await fs.lstat(path("file")) : null;
139
+ const peerExpected = existing ? await peer.lstat(path("file")) : null;
140
+ const peerParent = await peer.lstat(root);
141
+ const candidates = [Uint8Array.of(1, 2), Uint8Array.of(3, 4, 5)];
142
+ await winner([
143
+ fs.writeFileConditional(path("file"), candidates[0], { parent, expected }),
144
+ peer.writeFileConditional(path("file"), candidates[1], { parent: peerParent, expected: peerExpected }),
145
+ ], [fs, peer], path("file"), candidates);
146
+ await bytes(peer, path("file"), await fs.readFile(path("file")));
147
+ });
148
+ add("conditional append creates and preserves exact bytes", async ({ fs, root, path }) => {
149
+ const parent = await fs.lstat(root);
150
+ const created = await fs.writeFileConditional(path("file"), Uint8Array.of(0, 255), { parent, expected: null, append: true });
151
+ const appended = await fs.writeFileConditional(path("file"), Uint8Array.of(128, 1), { parent, expected: created, append: true });
152
+ await bytes(fs, path("file"), Uint8Array.of(0, 255, 128, 1));
153
+ check(appended.revision !== created.revision, "append must advance revision");
154
+ sameSnapshot(appended, await fs.lstat(path("file")), "append receipt");
155
+ });
156
+ add("same-length writes invalidate file revisions", async ({ fs, peer, root, path }) => {
157
+ const parent = await fs.lstat(root);
158
+ const expected = await fs.writeFileConditional(path("file"), Uint8Array.of(1, 2), { parent, expected: null });
159
+ await peer.writeFile(path("file"), Uint8Array.of(3, 4));
160
+ const current = await fs.lstat(path("file"));
161
+ check(current.revision !== expected.revision, "same-length write must advance revision");
162
+ const stale = { ...current, revision: expected.revision };
163
+ for (const append of [false, true])
164
+ await refuses(() => fs.writeFileConditional(path("file"), Uint8Array.of(5, 6), { parent, expected: stale, append }), ["EAGAIN"]);
165
+ await refuses(() => fs.removeFileConditional(path("file"), { parent, expected: stale }), ["EAGAIN"]);
166
+ sameSnapshot(await fs.lstat(path("file")), current, "stale mutation refusal");
167
+ await bytes(fs, path("file"), Uint8Array.of(3, 4));
168
+ });
169
+ add("missing expected files are not recreated", async ({ fs, peer, root, path }) => {
170
+ const parent = await fs.lstat(root);
171
+ await fs.writeFile(path("file"), Uint8Array.of(1));
172
+ const expected = await fs.lstat(path("file"));
173
+ await peer.rm(path("file"));
174
+ for (const append of [false, true])
175
+ await refuses(() => fs.writeFileConditional(path("file"), Uint8Array.of(2), { parent, expected, append }), ["EAGAIN"]);
176
+ await refuses(() => fs.removeFileConditional(path("file"), { parent, expected }), ["EAGAIN"]);
177
+ await entries(fs, root, []);
178
+ });
179
+ add("file snapshot metadata is checked", async (context) => {
180
+ const { fs, root, path } = context;
181
+ const parent = await fs.lstat(root);
182
+ const expected = await fs.writeFileConditional(path("file"), Uint8Array.of(1), { parent, expected: null });
183
+ const staged = await stage(context);
184
+ for (const field of ["size", "mode", "nlink", "mtimeMs", "ctimeMs"]) {
185
+ const changed = { ...expected, [field]: (expected[field] ?? 0) + 1 };
186
+ const changedStage = { ...staged, file: { ...staged.file, stat: {
187
+ ...staged.file.stat, [field]: (staged.file.stat[field] ?? 0) + 1,
188
+ } } };
189
+ await refuses(() => fs.writeFileConditional(path("file"), Uint8Array.of(2), { parent, expected: changed }), ["EAGAIN"]);
190
+ await refuses(() => fs.removeFileConditional(path("file"), { parent, expected: changed }), ["EAGAIN"]);
191
+ await refuses(() => fs.publishStagedFile(staged, path("file"), { parent, destination: changed }), ["EAGAIN"]);
192
+ await refuses(() => fs.publishStagedFile(changedStage, path("output"), { parent, destination: null }), ["EAGAIN"]);
193
+ await refuses(() => fs.removeStagedFile(changedStage), ["EAGAIN"]);
194
+ }
195
+ sameSnapshot(await fs.lstat(path("file")), expected, "file metadata refusal");
196
+ sameSnapshot(await fs.lstat(staged.file.path), staged.file.stat, "staged metadata refusal");
197
+ await bytes(fs, path("file"), Uint8Array.of(1));
198
+ await bytes(fs, staged.file.path, Uint8Array.of(0, 255, 1, 128));
199
+ await entries(fs, root, ["file", ".stage"]);
200
+ });
201
+ add("unknown identities and revisions refuse without mutation", async (context) => {
202
+ const { fs, root, path } = context;
203
+ const parent = await fs.lstat(root);
204
+ const expected = await fs.writeFileConditional(path("file"), Uint8Array.of(1), { parent, expected: null });
205
+ const unknownParent = { ...parent };
206
+ delete unknownParent.identityScope;
207
+ const unknownFile = { ...expected };
208
+ delete unknownFile.revision;
209
+ await refuses(() => fs.writeFileConditional(path("file"), Uint8Array.of(2), { parent, expected: unknownFile }), ["ENOTSUP"]);
210
+ await refuses(() => fs.removeFileConditional(path("file"), { parent, expected: unknownFile }), ["ENOTSUP"]);
211
+ await refuses(() => fs.writeFileConditional(path("new"), Uint8Array.of(2), { parent: unknownParent, expected: null }), ["ENOTSUP"]);
212
+ await refuses(() => fs.removeFileConditional(path("file"), { parent: unknownParent, expected }), ["ENOTSUP"]);
213
+ await refuses(() => fs.createStagedFile(path(".denied"), "file", { type: "file", data: Uint8Array.of(2) }, { parent: unknownParent }), ["ENOTSUP"]);
214
+ await refuses(() => fs.prepareDirectory(path("denied"), { parent: unknownParent, expected: null }), ["ENOTSUP"]);
215
+ const staged = await stage(context);
216
+ const unknownStage = { ...staged, file: { ...staged.file, stat: { ...staged.file.stat } } };
217
+ delete unknownStage.file.stat.revision;
218
+ await refuses(() => fs.publishStagedFile(unknownStage, path("output"), { parent, destination: null }), ["ENOTSUP"]);
219
+ await refuses(() => fs.removeStagedFile(unknownStage), ["ENOTSUP"]);
220
+ await fs.removeStagedFile(staged);
221
+ sameSnapshot(await fs.lstat(path("file")), expected, "unsupported conditions");
222
+ await bytes(fs, path("file"), Uint8Array.of(1));
223
+ await entries(fs, root, ["file"]);
224
+ });
225
+ add("parent replacement refuses mutations and staging", async (context) => {
226
+ const { fs, peer, path } = context;
227
+ await fs.mkdir(path("parent"));
228
+ const parent = await fs.lstat(path("parent"));
229
+ const expected = await fs.writeFileConditional(path("parent/file"), Uint8Array.of(1), { parent, expected: null });
230
+ const staged = await stage({ ...context, root: path("parent"), path: name => path(`parent/${name}`) });
231
+ await peer.rename(path("parent"), path("original"));
232
+ await peer.mkdir(path("parent"));
233
+ await peer.writeFile(path("parent/file"), Uint8Array.of(9));
234
+ await refuses(() => fs.writeFileConditional(path("parent/file"), Uint8Array.of(2), { parent, expected }), ["EAGAIN"]);
235
+ await refuses(() => fs.writeFileConditional(path("parent/new"), Uint8Array.of(2), { parent, expected: null }), ["EAGAIN"]);
236
+ await refuses(() => fs.removeFileConditional(path("parent/file"), { parent, expected }), ["EAGAIN"]);
237
+ await refuses(() => fs.createStagedFile(path("parent/.new"), "file", { type: "file", data: Uint8Array.of(2) }, { parent }), ["EAGAIN"]);
238
+ await refuses(() => fs.prepareDirectory(path("parent/new"), { parent, expected: null }), ["EAGAIN"]);
239
+ await refuses(() => fs.publishStagedFile(staged, path("parent/output"), { parent, destination: null }), ["EAGAIN"]);
240
+ await refuses(() => fs.removeStagedFile(staged), ["EAGAIN"]);
241
+ await bytes(fs, path("parent/file"), Uint8Array.of(9));
242
+ await bytes(fs, path("original/file"), Uint8Array.of(1));
243
+ await bytes(fs, path("original/.stage/file"), Uint8Array.of(0, 255, 1, 128));
244
+ await entries(fs, path("parent"), ["file"]);
245
+ });
246
+ add("staging receipts preserve original identity and revision", async (context) => {
247
+ const { fs, path } = context;
248
+ const staged = await stage(context);
249
+ const original = { ...staged.file.stat };
250
+ await fs.writeFile(staged.file.path, Uint8Array.of(4, 3, 2, 1));
251
+ sameSnapshot(staged.file.stat, original, "original staging receipt must not mutate");
252
+ const current = await fs.lstat(staged.file.path);
253
+ check(current.revision !== original.revision, "staged same-length write must advance revision");
254
+ await refuses(() => fs.publishStagedFile(staged, path("output"), { parent: staged.parent.stat, destination: null }), ["EAGAIN"]);
255
+ await refuses(() => fs.removeStagedFile(staged), ["EAGAIN"]);
256
+ const stale = { ...staged, file: { ...staged.file, stat: { ...current, revision: original.revision } } };
257
+ await refuses(() => fs.publishStagedFile(stale, path("output"), { parent: staged.parent.stat, destination: null }), ["EAGAIN"]);
258
+ await refuses(() => fs.removeStagedFile(stale), ["EAGAIN"]);
259
+ await bytes(fs, staged.file.path, Uint8Array.of(4, 3, 2, 1));
260
+ await refuses(() => fs.lstat(path("output")), ["ENOENT"]);
261
+ });
262
+ add("destination-parent replacement refuses publication", async (context) => {
263
+ const { fs, peer, path } = context;
264
+ const staged = await stage(context);
265
+ await fs.mkdir(path("destination"));
266
+ const parent = await fs.lstat(path("destination"));
267
+ await peer.rename(path("destination"), path("original"));
268
+ await peer.mkdir(path("destination"));
269
+ await peer.writeFile(path("destination/unrelated"), Uint8Array.of(9));
270
+ await refuses(() => fs.publishStagedFile(staged, path("destination/output"), { parent, destination: null }), ["EAGAIN"]);
271
+ await bytes(fs, staged.file.path, Uint8Array.of(0, 255, 1, 128));
272
+ await bytes(fs, path("destination/unrelated"), Uint8Array.of(9));
273
+ await entries(fs, path("destination"), ["unrelated"]);
274
+ await entries(fs, path("original"), []);
275
+ await fs.removeStagedFile(staged);
276
+ });
277
+ add("staging is exclusive and private", async (context) => {
278
+ const { fs } = context;
279
+ const staged = await stage(context);
280
+ check((staged.directory.stat.mode & 0o777) === 0o700, "staging receipt must declare private mode 0700");
281
+ check(((await fs.lstat(staged.directory.path)).mode & 0o777) === 0o700, "staging must be private mode 0700");
282
+ await refuses(() => fs.createStagedFile(staged.directory.path, "other", { type: "file", data: Uint8Array.of(9) }, { parent: staged.parent.stat }), ["EEXIST"]);
283
+ sameSnapshot(await fs.lstat(staged.file.path), staged.file.stat, "exclusive staging refusal");
284
+ await entries(fs, staged.directory.path, ["file"]);
285
+ await bytes(fs, staged.file.path, Uint8Array.of(0, 255, 1, 128));
286
+ await fs.removeStagedFile(staged);
287
+ await entries(fs, context.root, []);
288
+ });
289
+ for (const directory of [false, true])
290
+ add(`stage-${directory ? "directory" : "file"} replacement survives rejection`, async (context) => {
291
+ const { fs, peer, path } = context;
292
+ const staged = await stage(context);
293
+ const replaced = directory ? staged.directory.path : staged.file.path;
294
+ const original = path("original");
295
+ await peer.rename(replaced, original);
296
+ if (directory)
297
+ await peer.mkdir(replaced, { mode: 0o700 });
298
+ await peer.writeFile(staged.file.path, Uint8Array.of(9));
299
+ const replacement = await fs.lstat(staged.file.path);
300
+ await refuses(() => fs.publishStagedFile(staged, path("output"), { parent: staged.parent.stat, destination: null }), ["EAGAIN"]);
301
+ await refuses(() => fs.removeStagedFile(staged), ["EAGAIN"]);
302
+ sameSnapshot(await fs.lstat(staged.file.path), replacement, "replacement must survive");
303
+ await bytes(fs, staged.file.path, Uint8Array.of(9));
304
+ await bytes(fs, directory ? `${original}/file` : original, Uint8Array.of(0, 255, 1, 128));
305
+ await refuses(() => fs.lstat(path("output")), ["ENOENT"]);
306
+ });
307
+ for (const replaced of [false, true])
308
+ add(replaced ? "destination replacement survives publication rejection" : "destination revision changes refuse publication", async (context) => {
309
+ const { fs, peer, root, path } = context;
310
+ const parent = await fs.lstat(root);
311
+ await fs.writeFile(path("output"), Uint8Array.of(1, 2));
312
+ const destination = await fs.lstat(path("output"));
313
+ const staged = await stage(context);
314
+ if (replaced)
315
+ await peer.rename(path("output"), path("original"));
316
+ await peer.writeFile(path("output"), Uint8Array.of(3, 4));
317
+ const replacement = await fs.lstat(path("output"));
318
+ await refuses(() => fs.publishStagedFile(staged, path("output"), { parent, destination }), ["EAGAIN"]);
319
+ await fs.removeStagedFile(staged);
320
+ sameSnapshot(await fs.lstat(path("output")), replacement, "destination must survive");
321
+ await bytes(fs, path("output"), Uint8Array.of(3, 4));
322
+ if (replaced)
323
+ await bytes(fs, path("original"), Uint8Array.of(1, 2));
324
+ });
325
+ add("missing and unexpectedly present destinations refuse publication", async (context) => {
326
+ const { fs, peer, path } = context;
327
+ await fs.writeFile(path("output"), Uint8Array.of(1));
328
+ const destination = await fs.lstat(path("output"));
329
+ const staged = await stage(context);
330
+ await peer.rm(path("output"));
331
+ await refuses(() => fs.publishStagedFile(staged, path("output"), { parent: staged.parent.stat, destination }), ["EAGAIN"]);
332
+ await refuses(() => fs.lstat(path("output")), ["ENOENT"]);
333
+ await peer.writeFile(path("output"), Uint8Array.of(9));
334
+ await refuses(() => fs.publishStagedFile(staged, path("output"), { parent: staged.parent.stat, destination: null }), ["EAGAIN"]);
335
+ await fs.removeStagedFile(staged);
336
+ await bytes(fs, path("output"), Uint8Array.of(9));
337
+ });
338
+ add("unexpected staging children survive cleanup refusal", async (context) => {
339
+ const { fs, peer } = context;
340
+ const staged = await stage(context);
341
+ await peer.writeFile(`${staged.directory.path}/unexpected`, Uint8Array.of(9));
342
+ await refuses(() => fs.removeStagedFile(staged), ["EAGAIN", "ENOTEMPTY"]);
343
+ await bytes(fs, staged.file.path, Uint8Array.of(0, 255, 1, 128));
344
+ await bytes(fs, `${staged.directory.path}/unexpected`, Uint8Array.of(9));
345
+ await entries(fs, staged.directory.path, ["file", "unexpected"]);
346
+ });
347
+ add("unexpected children survive post-publication cleanup refusal", async (context) => {
348
+ const { fs, peer, path } = context;
349
+ const staged = await stage(context);
350
+ await fs.publishStagedFile(staged, path("output"), { parent: staged.parent.stat, destination: null });
351
+ await peer.writeFile(`${staged.directory.path}/unexpected`, Uint8Array.of(9));
352
+ await refuses(() => fs.removeStagedFile(staged), ["EAGAIN", "ENOTEMPTY"]);
353
+ await bytes(fs, path("output"), Uint8Array.of(0, 255, 1, 128));
354
+ await bytes(fs, `${staged.directory.path}/unexpected`, Uint8Array.of(9));
355
+ await entries(fs, staged.directory.path, ["unexpected"]);
356
+ });
357
+ for (const existing of [false, true])
358
+ add(`publish ${existing ? "updates" : "creates"} exact bytes and cleans only the private directory`, async (context) => {
359
+ const { fs, root, path } = context;
360
+ const parent = await fs.lstat(root);
361
+ await fs.writeFile(path("unrelated"), Uint8Array.of(9));
362
+ if (existing)
363
+ await fs.writeFile(path("output"), Uint8Array.of(8));
364
+ const destination = existing ? await fs.lstat(path("output")) : null;
365
+ const staged = await stage(context);
366
+ await fs.publishStagedFile(staged, path("output"), { parent, destination });
367
+ sameIdentity(await fs.lstat(path("output")), staged.file.stat, "published identity");
368
+ await entries(fs, staged.directory.path, []);
369
+ await bytes(fs, path("output"), Uint8Array.of(0, 255, 1, 128));
370
+ await fs.removeStagedFile(staged);
371
+ await bytes(fs, path("output"), Uint8Array.of(0, 255, 1, 128));
372
+ await bytes(fs, path("unrelated"), Uint8Array.of(9));
373
+ await entries(fs, root, ["output", "unrelated"]);
374
+ });
375
+ add("directories create exclusively and update despite child changes", async ({ fs, peer, root, path }) => {
376
+ const parent = await fs.lstat(root);
377
+ const receipt = await fs.prepareDirectory(path("directory"), { parent, expected: null, mode: 0o755 });
378
+ check(receipt.type === "directory", "prepareDirectory must return a directory");
379
+ sameSnapshot(receipt, await fs.lstat(path("directory")), "directory creation receipt");
380
+ const original = { ...receipt };
381
+ await refuses(() => fs.prepareDirectory(path("directory"), { parent, expected: null, mode: 0o700 }), ["EAGAIN"]);
382
+ await peer.writeFile(path("directory/child"), Uint8Array.of(9));
383
+ const updated = await fs.prepareDirectory(path("directory"), { parent, expected: receipt, mode: 0o700 });
384
+ sameIdentity(updated, original, "directory update identity");
385
+ sameSnapshot(updated, await fs.lstat(path("directory")), "directory update receipt");
386
+ sameSnapshot(receipt, original, "original directory receipt must not mutate");
387
+ check((updated.mode & 0o777) === 0o700, "directory metadata update must apply mode");
388
+ await bytes(fs, path("directory/child"), Uint8Array.of(9));
389
+ });
390
+ add("directory creation has one winner", async ({ fs, peer, root, path }) => {
391
+ const parent = await fs.lstat(root);
392
+ const peerParent = await peer.lstat(root);
393
+ const results = await Promise.allSettled([
394
+ fs.prepareDirectory(path("directory"), { parent, expected: null, mode: 0o755 }),
395
+ peer.prepareDirectory(path("directory"), { parent: peerParent, expected: null, mode: 0o700 }),
396
+ ]);
397
+ check(results.filter(result => result.status === "fulfilled").length === 1, "directory creation must have exactly one winner");
398
+ for (const [index, result] of results.entries()) {
399
+ if (result.status === "fulfilled") {
400
+ check(result.value.type === "directory", "directory winner receipt type");
401
+ check((result.value.mode & 0o777) === (index === 0 ? 0o755 : 0o700), "directory winner metadata");
402
+ sameSnapshot(result.value, await [fs, peer][index].lstat(path("directory")), "directory winner receipt");
403
+ }
404
+ else
405
+ check(result.reason?.code === "EAGAIN", "directory creation loser must reject EAGAIN");
406
+ }
407
+ check((await peer.lstat(path("directory"))).type === "directory", "peer directory visibility");
408
+ await entries(peer, path("directory"), []);
409
+ await entries(fs, path("directory"), []);
410
+ await entries(fs, root, ["directory"]);
411
+ });
412
+ add("stale directories refuse metadata changes", async ({ fs, peer, root, path }) => {
413
+ const parent = await fs.lstat(root);
414
+ const expected = await fs.prepareDirectory(path("directory"), { parent, expected: null, mode: 0o755 });
415
+ await peer.rename(path("directory"), path("original"));
416
+ await peer.mkdir(path("directory"), { mode: 0o755 });
417
+ await peer.writeFile(path("directory/child"), Uint8Array.of(9));
418
+ const replacement = await fs.lstat(path("directory"));
419
+ await refuses(() => fs.prepareDirectory(path("directory"), { parent, expected, mode: 0o700 }), ["EAGAIN"]);
420
+ sameSnapshot(await fs.lstat(path("directory")), replacement, "replacement directory must survive");
421
+ await bytes(fs, path("directory/child"), Uint8Array.of(9));
422
+ await entries(fs, path("original"), []);
423
+ });
424
+ add("pre-aborted operations leave entries unchanged", async (context) => {
425
+ const { fs, root, path } = context;
426
+ const parent = await fs.lstat(root);
427
+ const file = await fs.writeFileConditional(path("file"), Uint8Array.of(9), { parent, expected: null });
428
+ const directory = await fs.prepareDirectory(path("directory"), { parent, expected: null, mode: 0o755 });
429
+ const staged = await stage(context);
430
+ const controller = new AbortController();
431
+ const reason = new Error("conformance pre-abort");
432
+ controller.abort(reason);
433
+ const signal = controller.signal;
434
+ const operations = [
435
+ () => fs.writeFileConditional(path("new"), Uint8Array.of(1), { parent, expected: null, signal }),
436
+ () => fs.writeFileConditional(path("file"), Uint8Array.of(1), { parent, expected: file, signal }),
437
+ () => fs.removeFileConditional(path("file"), { parent, expected: file, signal }),
438
+ () => fs.createStagedFile(path(".new"), "file", { type: "file", data: Uint8Array.of(1) }, { parent, signal }),
439
+ () => fs.publishStagedFile(staged, path("output"), { parent, destination: null, signal }),
440
+ () => fs.removeStagedFile(staged, { signal }),
441
+ () => fs.prepareDirectory(path("new-directory"), { parent, expected: null, signal }),
442
+ () => fs.prepareDirectory(path("directory"), { parent, expected: directory, mode: 0o700, signal }),
443
+ ];
444
+ for (const operation of operations) {
445
+ let rejected = false;
446
+ try {
447
+ await operation();
448
+ }
449
+ catch (error) {
450
+ rejected = true;
451
+ check(error === reason, "pre-aborted operation must preserve the signal reason");
452
+ }
453
+ check(rejected, "pre-aborted operation must reject");
454
+ }
455
+ sameSnapshot(await fs.lstat(path("file")), file, "aborted file mutation");
456
+ sameSnapshot(await fs.lstat(path("directory")), directory, "aborted directory mutation");
457
+ sameSnapshot(await fs.lstat(staged.file.path), staged.file.stat, "aborted staging mutation");
458
+ await bytes(fs, path("file"), Uint8Array.of(9));
459
+ await bytes(fs, staged.file.path, Uint8Array.of(0, 255, 1, 128));
460
+ await entries(fs, root, ["file", "directory", ".stage"]);
461
+ await entries(fs, staged.directory.path, ["file"]);
462
+ });
463
+ if (options.includeSymlinks) {
464
+ add("symlink staging publishes the exact target without following it", async (context) => {
465
+ const { fs, root, path } = context;
466
+ await fs.writeFile(path("target"), Uint8Array.of(9));
467
+ const staged = await stage(context, ".stage", { type: "symlink", target: path("target") });
468
+ check(await fs.readlink(staged.file.path) === path("target"), "staged symlink target must be exact");
469
+ await fs.publishStagedFile(staged, path("output"), { parent: staged.parent.stat, destination: null });
470
+ check((await fs.lstat(path("output"))).type === "symlink", "publish must preserve symlink type");
471
+ check(await fs.readlink(path("output")) === path("target"), "published symlink target must be exact");
472
+ await fs.removeStagedFile(staged);
473
+ await bytes(fs, path("target"), Uint8Array.of(9));
474
+ await entries(fs, root, ["target", "output"]);
475
+ });
476
+ for (const directory of [false, true])
477
+ add(`symlink replacement of staging ${directory ? "directory" : "file"} survives rejection`, async (context) => {
478
+ const { fs, peer, path } = context;
479
+ const staged = await stage(context);
480
+ await fs.mkdir(path("target"));
481
+ await fs.writeFile(path("target/file"), Uint8Array.of(9));
482
+ const replaced = directory ? staged.directory.path : staged.file.path;
483
+ const target = path(directory ? "target" : "target/file");
484
+ await peer.rename(replaced, path("original"));
485
+ await peer.symlink(target, replaced);
486
+ await refuses(() => fs.publishStagedFile(staged, path("output"), { parent: staged.parent.stat, destination: null }), ["EAGAIN"]);
487
+ await refuses(() => fs.removeStagedFile(staged), ["EAGAIN"]);
488
+ check((await fs.lstat(replaced)).type === "symlink", "replacement symlink must survive");
489
+ check(await fs.readlink(replaced) === target, "replacement symlink target must survive");
490
+ await bytes(fs, path("target/file"), Uint8Array.of(9));
491
+ await bytes(fs, path(directory ? "original/file" : "original"), Uint8Array.of(0, 255, 1, 128));
492
+ await refuses(() => fs.lstat(path("output")), ["ENOENT"]);
493
+ });
494
+ }
495
+ return Object.freeze(cases);
496
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.553",
3
+ "version": "0.1.554",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,6 +31,12 @@
31
31
  "types": "./dist/safe-fs/core.d.ts",
32
32
  "import": "./dist/safe-fs/core.js"
33
33
  },
34
+ "./testing/atomic": {
35
+ "types": "./dist/safe-fs/testing/atomic-filesystem.d.ts",
36
+ "workerd": "./dist/safe-fs/testing/atomic-filesystem.js",
37
+ "browser": "./dist/safe-fs/testing/atomic-filesystem.js",
38
+ "import": "./dist/safe-fs/testing/atomic-filesystem.js"
39
+ },
34
40
  "./node": {
35
41
  "types": {
36
42
  "browser": "./dist/safe-fs/node-unavailable.d.ts",