@hoardodile/host 0.0.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 (144) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +73 -0
  3. package/dist/chunks/worker-entry.mjs +422 -0
  4. package/dist/container-fKxefO7p.d.ts +58 -0
  5. package/dist/contract/index.d.ts +49 -0
  6. package/dist/contract/index.js +187 -0
  7. package/dist/contract/index.js.map +1 -0
  8. package/dist/hoard/index.d.ts +754 -0
  9. package/dist/hoard/index.js +1939 -0
  10. package/dist/hoard/index.js.map +1 -0
  11. package/dist/image-Dx2Be3Bj.d.ts +168 -0
  12. package/dist/index.d.ts +788 -0
  13. package/dist/index.js +3311 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/media/index.d.ts +96 -0
  16. package/dist/media/index.js +152 -0
  17. package/dist/media/index.js.map +1 -0
  18. package/dist/probe/index.d.ts +113 -0
  19. package/dist/probe/index.js +454 -0
  20. package/dist/probe/index.js.map +1 -0
  21. package/dist/probe-cache-BkRI7HEK.d.ts +25 -0
  22. package/dist/render/index.d.ts +125 -0
  23. package/dist/render/index.js +430 -0
  24. package/dist/render/index.js.map +1 -0
  25. package/package.json +103 -0
  26. package/src/__testutils__/zip-fixtures.ts +139 -0
  27. package/src/activation.ts +116 -0
  28. package/src/api-types.ts +60 -0
  29. package/src/api.test.ts +564 -0
  30. package/src/api.ts +501 -0
  31. package/src/archive/7z.test.ts +222 -0
  32. package/src/archive/7z.ts +261 -0
  33. package/src/archive/archive-ops.test.ts +654 -0
  34. package/src/archive/extract-archive.test.ts +237 -0
  35. package/src/archive/extract-archive.ts +534 -0
  36. package/src/archive/extract.ts +693 -0
  37. package/src/archive/format.ts +49 -0
  38. package/src/archive/index.ts +116 -0
  39. package/src/archive/listing.ts +121 -0
  40. package/src/archive/materialize.ts +88 -0
  41. package/src/archive/name-decode.test.ts +34 -0
  42. package/src/archive/name-decode.ts +287 -0
  43. package/src/archive/nested-cd-cache.ts +55 -0
  44. package/src/archive/nested-entry.test.ts +194 -0
  45. package/src/archive/nested-entry.ts +254 -0
  46. package/src/archive/pack.ts +40 -0
  47. package/src/archive/zip-entries.test.ts +56 -0
  48. package/src/archive/zip-entries.ts +336 -0
  49. package/src/capability-guard.ts +73 -0
  50. package/src/container.ts +62 -0
  51. package/src/contract/container-fixture.ts +74 -0
  52. package/src/contract/index.ts +10 -0
  53. package/src/contract/suite.test.ts +101 -0
  54. package/src/contract/suite.ts +205 -0
  55. package/src/define-plugin.test.ts +53 -0
  56. package/src/define-plugin.ts +12 -0
  57. package/src/dev-runner.test.ts +98 -0
  58. package/src/dev-runner.ts +73 -0
  59. package/src/directory-api.test.ts +229 -0
  60. package/src/directory-api.ts +42 -0
  61. package/src/directory-container.ts +141 -0
  62. package/src/discovery.ts +295 -0
  63. package/src/errors.ts +74 -0
  64. package/src/fixtures.ts +8 -0
  65. package/src/hash.test.ts +301 -0
  66. package/src/hash.ts +167 -0
  67. package/src/hoard/dir-size.ts +58 -0
  68. package/src/hoard/index.ts +84 -0
  69. package/src/hoard/migrate-script.test.ts +539 -0
  70. package/src/hoard/order-manifest.test.ts +115 -0
  71. package/src/hoard/order-manifest.ts +131 -0
  72. package/src/hoard/paths.test.ts +226 -0
  73. package/src/hoard/paths.ts +569 -0
  74. package/src/hoard/plugin-vault.test.ts +160 -0
  75. package/src/hoard/plugin-vault.ts +233 -0
  76. package/src/hoard/sanitize.test.ts +149 -0
  77. package/src/hoard/sanitize.ts +230 -0
  78. package/src/hoard/staging-dir.ts +133 -0
  79. package/src/hoard/version.test.ts +285 -0
  80. package/src/hoard/version.ts +216 -0
  81. package/src/hoard/versioned-folder-ops.ts +152 -0
  82. package/src/hoard/write-versioned.test.ts +67 -0
  83. package/src/hoard/write-versioned.ts +39 -0
  84. package/src/hooks.test.ts +528 -0
  85. package/src/hooks.ts +428 -0
  86. package/src/index.ts +135 -0
  87. package/src/loader.test.ts +442 -0
  88. package/src/loader.ts +213 -0
  89. package/src/media/index.ts +16 -0
  90. package/src/media/render-cache.ts +27 -0
  91. package/src/media/seekable.ts +35 -0
  92. package/src/media/thumb-input.test.ts +109 -0
  93. package/src/media/thumb-input.ts +194 -0
  94. package/src/nested-view.test.ts +223 -0
  95. package/src/nested-view.ts +239 -0
  96. package/src/probe/audio.test.ts +135 -0
  97. package/src/probe/audio.ts +140 -0
  98. package/src/probe/av.ts +84 -0
  99. package/src/probe/ffprobe.ts +197 -0
  100. package/src/probe/image.test.ts +251 -0
  101. package/src/probe/image.ts +336 -0
  102. package/src/probe/index.ts +54 -0
  103. package/src/probe/sniff.test.ts +72 -0
  104. package/src/probe/sniff.ts +67 -0
  105. package/src/probe/video.test.ts +41 -0
  106. package/src/probe/video.ts +116 -0
  107. package/src/probe-cache.test.ts +89 -0
  108. package/src/probe-cache.ts +65 -0
  109. package/src/render/ffmpeg.ts +78 -0
  110. package/src/render/index.ts +34 -0
  111. package/src/render/pipeline.test.ts +187 -0
  112. package/src/render/pipeline.ts +479 -0
  113. package/src/sandbox/call-timers.ts +68 -0
  114. package/src/sandbox/fixtures/api-error-plugin.mjs +11 -0
  115. package/src/sandbox/fixtures/asset-plugin.mjs +13 -0
  116. package/src/sandbox/fixtures/chatty-plugin.mjs +14 -0
  117. package/src/sandbox/fixtures/container-plugin.mjs +8 -0
  118. package/src/sandbox/fixtures/context-plugin.mjs +19 -0
  119. package/src/sandbox/fixtures/crash-plugin.mjs +6 -0
  120. package/src/sandbox/fixtures/echo-plugin.mjs +17 -0
  121. package/src/sandbox/fixtures/exit-plugin.mjs +6 -0
  122. package/src/sandbox/fixtures/fetch-plugin.mjs +8 -0
  123. package/src/sandbox/fixtures/flood-api-plugin.mjs +8 -0
  124. package/src/sandbox/fixtures/flood-log-plugin.mjs +8 -0
  125. package/src/sandbox/fixtures/hostile-dynamic-plugin.mjs +9 -0
  126. package/src/sandbox/fixtures/hostile-plugin.mjs +13 -0
  127. package/src/sandbox/fixtures/huge-result-plugin.mjs +8 -0
  128. package/src/sandbox/fixtures/logging-plugin.mjs +9 -0
  129. package/src/sandbox/fixtures/range-plugin.mjs +9 -0
  130. package/src/sandbox/fixtures/slow-api-plugin.mjs +11 -0
  131. package/src/sandbox/fixtures/spin-plugin.mjs +9 -0
  132. package/src/sandbox/fixtures/stuck-plugin.mjs +12 -0
  133. package/src/sandbox/fixtures/thrower-plugin.mjs +7 -0
  134. package/src/sandbox/host.test.ts +619 -0
  135. package/src/sandbox/host.ts +933 -0
  136. package/src/sandbox/host.unit.test.ts +330 -0
  137. package/src/sandbox/protocol.test.ts +85 -0
  138. package/src/sandbox/protocol.ts +147 -0
  139. package/src/sandbox/sandboxed-plugin.ts +30 -0
  140. package/src/sandbox/worker-entry.mjs +422 -0
  141. package/src/seed.test.ts +102 -0
  142. package/src/seed.ts +160 -0
  143. package/src/settings-store.ts +26 -0
  144. package/src/types.ts +21 -0
@@ -0,0 +1,187 @@
1
+ import { Readable } from 'stream';
2
+ import { afterAll, describe, test, expect } from 'vitest';
3
+
4
+ // src/contract/container-fixture.ts
5
+ function createContainerFixture(config = {}) {
6
+ const files = config.files ?? {};
7
+ function bytesOf(relPath) {
8
+ const content = files[relPath];
9
+ if (content === void 0) return void 0;
10
+ return typeof content === "string" ? new TextEncoder().encode(content) : content;
11
+ }
12
+ return {
13
+ async listEntries() {
14
+ return Object.keys(files);
15
+ },
16
+ async readEntry(relPath) {
17
+ const bytes = bytesOf(relPath);
18
+ if (bytes === void 0) {
19
+ throw new Error(`fixture container has no entry ${relPath}`);
20
+ }
21
+ return Buffer.from(bytes);
22
+ },
23
+ async readEntrySlice(relPath, start, end) {
24
+ const bytes = bytesOf(relPath);
25
+ if (bytes === void 0) {
26
+ throw new Error(`fixture container has no entry ${relPath}`);
27
+ }
28
+ const clampedStart = Math.min(Math.max(0, start), bytes.length);
29
+ const clampedEnd = Math.min(Math.max(clampedStart, end), bytes.length);
30
+ return Buffer.from(bytes.subarray(clampedStart, clampedEnd));
31
+ },
32
+ async openEntryStream(relPath) {
33
+ const bytes = bytesOf(relPath);
34
+ if (bytes === void 0) {
35
+ throw new Error(`fixture container has no entry ${relPath}`);
36
+ }
37
+ return { stream: Readable.from(bytes), size: bytes.length };
38
+ },
39
+ async resolveByteRange(relPath) {
40
+ const bytes = bytesOf(relPath);
41
+ return bytes === void 0 ? void 0 : { size: bytes.length };
42
+ }
43
+ };
44
+ }
45
+ var CONTAINER_CONTRACT_CASES = [
46
+ { name: "empty container", files: {} },
47
+ {
48
+ name: "flat files",
49
+ files: { "a.txt": "alpha", "b.bin": "beta", "c.jpg": "gamma" }
50
+ },
51
+ {
52
+ name: "nested entry names",
53
+ files: { "dir/a.txt": "nested a", "dir/sub/b.txt": "nested b" }
54
+ },
55
+ {
56
+ name: "binary content",
57
+ files: { "blob.bin": Uint8Array.from([0, 1, 2, 250, 255, 128]) }
58
+ },
59
+ { name: "empty entry", files: { "empty.txt": "" } },
60
+ {
61
+ name: "unicode entry names",
62
+ files: { "\u0424\u043E\u0442\u043E/\u044F.jpg": "unicode" }
63
+ }
64
+ ];
65
+ function toBytes(content) {
66
+ return typeof content === "string" ? new TextEncoder().encode(content) : content;
67
+ }
68
+ async function collectManifest(api) {
69
+ const files = [...await api.listFileNames()].sort();
70
+ const manifest = {};
71
+ manifest.files = files;
72
+ for (const name of files) {
73
+ manifest[`stat:${name}`] = await api.statFile(name);
74
+ manifest[`read:${name}`] = Array.from(await api.readFile(name));
75
+ manifest[`range:${name}`] = Array.from(
76
+ await api.readFile(name, { start: 1, end: 3 })
77
+ );
78
+ manifest[`type:${name}`] = await api.sniff(name);
79
+ manifest[`probe:${name}`] = await api.probe(name);
80
+ }
81
+ const batched = await api.statFiles(files);
82
+ manifest.statAll = files.map((name, i) => ({
83
+ name,
84
+ stat: batched[i]
85
+ }));
86
+ manifest.missingStat = await api.statFile("missing-entry.bin");
87
+ manifest.missingStatAll = await api.statFiles(["missing-entry.bin"]);
88
+ return manifest;
89
+ }
90
+ function containerContractSuite(backends) {
91
+ afterAll(async () => {
92
+ await Promise.all(backends.map((b) => b.cleanup?.()));
93
+ });
94
+ describe("container contract", () => {
95
+ for (const backend of backends) {
96
+ describe(backend.name, () => {
97
+ for (const c of CONTAINER_CONTRACT_CASES) {
98
+ test(`case "${c.name}": core semantics`, async () => {
99
+ const api = await backend.build(c.files);
100
+ const names = Object.keys(c.files);
101
+ expect([...await api.listFileNames()].sort()).toEqual(
102
+ [...names].sort()
103
+ );
104
+ for (const name of names) {
105
+ const content = c.files[name];
106
+ if (content === void 0) continue;
107
+ const expected = toBytes(content);
108
+ expect(Array.from(await api.readFile(name))).toEqual([
109
+ ...expected
110
+ ]);
111
+ const mid = await api.readFile(name, {
112
+ start: 1,
113
+ end: Math.min(3, expected.length)
114
+ });
115
+ expect(Array.from(mid)).toEqual([
116
+ ...expected.slice(1, Math.min(3, expected.length))
117
+ ]);
118
+ const past = await api.readFile(name, {
119
+ start: expected.length + 5,
120
+ end: expected.length + 9
121
+ });
122
+ expect(past.byteLength).toBe(0);
123
+ expect(await api.statFile(name)).toEqual({
124
+ sizeBytes: expected.length
125
+ });
126
+ }
127
+ await expect(api.readFile("missing-entry.bin")).rejects.toThrow();
128
+ await expect(
129
+ api.statFile("missing-entry.bin")
130
+ ).resolves.toBeUndefined();
131
+ const singleStats = names.map(
132
+ (n) => c.files[n] !== void 0 ? { sizeBytes: toBytes(c.files[n]).length } : void 0
133
+ );
134
+ const batchStats = await api.statFiles(names);
135
+ expect(batchStats.length).toBe(names.length);
136
+ for (let i = 0; i < names.length; i++) {
137
+ expect(batchStats[i]).toEqual(singleStats[i]);
138
+ }
139
+ await expect(api.statFiles(["missing-entry.bin"])).resolves.toEqual(
140
+ [void 0]
141
+ );
142
+ for (const name of names) {
143
+ const type = await api.sniff(name);
144
+ const probed = await api.probe(name);
145
+ if (type !== void 0 && type.kind !== "other") {
146
+ expect(probed).toEqual({
147
+ kind: "unknown",
148
+ reason: "unavailable"
149
+ });
150
+ } else if (type !== void 0) {
151
+ expect(probed).toEqual({ kind: "other", mime: type.mime });
152
+ } else {
153
+ expect(probed).toEqual({
154
+ kind: "unknown",
155
+ reason: "unsupported"
156
+ });
157
+ }
158
+ }
159
+ await expect(
160
+ api.sniff("missing-entry.bin")
161
+ ).resolves.toBeUndefined();
162
+ await expect(api.probe("missing-entry.bin")).resolves.toEqual({
163
+ kind: "unknown",
164
+ reason: "unsupported"
165
+ });
166
+ });
167
+ }
168
+ });
169
+ }
170
+ for (const c of CONTAINER_CONTRACT_CASES) {
171
+ test(`case "${c.name}": identical results across backends`, async () => {
172
+ const results = [];
173
+ for (const backend of backends) {
174
+ const api = await backend.build(c.files);
175
+ results.push(await collectManifest(api));
176
+ }
177
+ for (let i = 1; i < results.length; i++) {
178
+ expect(results[i]).toEqual(results[0]);
179
+ }
180
+ });
181
+ }
182
+ });
183
+ }
184
+
185
+ export { CONTAINER_CONTRACT_CASES, containerContractSuite, createContainerFixture };
186
+ //# sourceMappingURL=index.js.map
187
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/contract/container-fixture.ts","../../src/contract/suite.ts"],"names":[],"mappings":";;;;AAeO,SAAS,sBAAA,CACf,MAAA,GAAiC,EAAC,EACd;AACpB,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,IAAS,EAAC;AAE/B,EAAA,SAAS,QAAQ,OAAA,EAAyC;AACzD,IAAA,MAAM,OAAA,GAAU,MAAM,OAAO,CAAA;AAC7B,IAAA,IAAI,OAAA,KAAY,QAAW,OAAO,MAAA;AAClC,IAAA,OAAO,OAAO,YAAY,QAAA,GACvB,IAAI,aAAY,CAAE,MAAA,CAAO,OAAO,CAAA,GAChC,OAAA;AAAA,EACJ;AAEA,EAAA,OAAO;AAAA,IACN,MAAM,WAAA,GAA0C;AAC/C,MAAA,OAAO,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACzB,CAAA;AAAA,IAEA,MAAM,UAAU,OAAA,EAAkC;AACjD,MAAA,MAAM,KAAA,GAAQ,QAAQ,OAAO,CAAA;AAC7B,MAAA,IAAI,UAAU,MAAA,EAAW;AACxB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,+BAAA,EAAkC,OAAO,CAAA,CAAE,CAAA;AAAA,MAC5D;AACA,MAAA,OAAO,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,IACzB,CAAA;AAAA,IAEA,MAAM,cAAA,CACL,OAAA,EACA,KAAA,EACA,GAAA,EACkB;AAClB,MAAA,MAAM,KAAA,GAAQ,QAAQ,OAAO,CAAA;AAC7B,MAAA,IAAI,UAAU,MAAA,EAAW;AACxB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,+BAAA,EAAkC,OAAO,CAAA,CAAE,CAAA;AAAA,MAC5D;AACA,MAAA,MAAM,YAAA,GAAe,KAAK,GAAA,CAAI,IAAA,CAAK,IAAI,CAAA,EAAG,KAAK,CAAA,EAAG,KAAA,CAAM,MAAM,CAAA;AAC9D,MAAA,MAAM,UAAA,GAAa,KAAK,GAAA,CAAI,IAAA,CAAK,IAAI,YAAA,EAAc,GAAG,CAAA,EAAG,KAAA,CAAM,MAAM,CAAA;AACrE,MAAA,OAAO,OAAO,IAAA,CAAK,KAAA,CAAM,QAAA,CAAS,YAAA,EAAc,UAAU,CAAC,CAAA;AAAA,IAC5D,CAAA;AAAA,IAEA,MAAM,gBAAgB,OAAA,EAGnB;AACF,MAAA,MAAM,KAAA,GAAQ,QAAQ,OAAO,CAAA;AAC7B,MAAA,IAAI,UAAU,MAAA,EAAW;AACxB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,+BAAA,EAAkC,OAAO,CAAA,CAAE,CAAA;AAAA,MAC5D;AACA,MAAA,OAAO,EAAE,QAAQ,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA,EAAG,IAAA,EAAM,MAAM,MAAA,EAAO;AAAA,IAC3D,CAAA;AAAA,IAEA,MAAM,iBACL,OAAA,EACiD;AACjD,MAAA,MAAM,KAAA,GAAQ,QAAQ,OAAO,CAAA;AAC7B,MAAA,OAAO,UAAU,MAAA,GAAY,MAAA,GAAY,EAAE,IAAA,EAAM,MAAM,MAAA,EAAO;AAAA,IAC/D;AAAA,GACD;AACD;ACtDO,IAAM,wBAAA,GAA6D;AAAA,EACzE,EAAE,IAAA,EAAM,iBAAA,EAAmB,KAAA,EAAO,EAAC,EAAE;AAAA,EACrC;AAAA,IACC,IAAA,EAAM,YAAA;AAAA,IACN,OAAO,EAAE,OAAA,EAAS,SAAS,OAAA,EAAS,MAAA,EAAQ,SAAS,OAAA;AAAQ,GAC9D;AAAA,EACA;AAAA,IACC,IAAA,EAAM,oBAAA;AAAA,IACN,KAAA,EAAO,EAAE,WAAA,EAAa,UAAA,EAAY,iBAAiB,UAAA;AAAW,GAC/D;AAAA,EACA;AAAA,IACC,IAAA,EAAM,gBAAA;AAAA,IACN,KAAA,EAAO,EAAE,UAAA,EAAY,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC,CAAA;AAAE,GAChE;AAAA,EACA,EAAE,IAAA,EAAM,aAAA,EAAe,OAAO,EAAE,WAAA,EAAa,IAAG,EAAE;AAAA,EAClD;AAAA,IACC,IAAA,EAAM,qBAAA;AAAA,IACN,KAAA,EAAO,EAAE,qCAAA,EAAc,SAAA;AAAU;AAEnC;AAeA,SAAS,QAAQ,OAAA,EAA0C;AAC1D,EAAA,OAAO,OAAO,YAAY,QAAA,GACvB,IAAI,aAAY,CAAE,MAAA,CAAO,OAAO,CAAA,GAChC,OAAA;AACJ;AAEA,eAAe,gBAAgB,GAAA,EAAoC;AAClE,EAAA,MAAM,KAAA,GAAQ,CAAC,GAAI,MAAM,IAAI,aAAA,EAAgB,EAAE,IAAA,EAAK;AACpD,EAAA,MAAM,WAAoC,EAAC;AAC3C,EAAA,QAAA,CAAS,KAAA,GAAQ,KAAA;AACjB,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACzB,IAAA,QAAA,CAAS,QAAQ,IAAI,CAAA,CAAE,IAAI,MAAM,GAAA,CAAI,SAAS,IAAI,CAAA;AAClD,IAAA,QAAA,CAAS,CAAA,KAAA,EAAQ,IAAI,CAAA,CAAE,CAAA,GAAI,KAAA,CAAM,KAAK,MAAM,GAAA,CAAI,QAAA,CAAS,IAAI,CAAC,CAAA;AAC9D,IAAA,QAAA,CAAS,CAAA,MAAA,EAAS,IAAI,CAAA,CAAE,CAAA,GAAI,KAAA,CAAM,IAAA;AAAA,MACjC,MAAM,IAAI,QAAA,CAAS,IAAA,EAAM,EAAE,KAAA,EAAO,CAAA,EAAG,GAAA,EAAK,CAAA,EAAG;AAAA,KAC9C;AAGA,IAAA,QAAA,CAAS,QAAQ,IAAI,CAAA,CAAE,IAAI,MAAM,GAAA,CAAI,MAAM,IAAI,CAAA;AAC/C,IAAA,QAAA,CAAS,SAAS,IAAI,CAAA,CAAE,IAAI,MAAM,GAAA,CAAI,MAAM,IAAI,CAAA;AAAA,EACjD;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,GAAA,CAAI,SAAA,CAAU,KAAK,CAAA;AACzC,EAAA,QAAA,CAAS,OAAA,GAAU,KAAA,CAAM,GAAA,CAAI,CAAC,MAAM,CAAA,MAAO;AAAA,IAC1C,IAAA;AAAA,IACA,IAAA,EAAM,QAAQ,CAAC;AAAA,GAChB,CAAE,CAAA;AACF,EAAA,QAAA,CAAS,WAAA,GAAc,MAAM,GAAA,CAAI,QAAA,CAAS,mBAAmB,CAAA;AAC7D,EAAA,QAAA,CAAS,iBAAiB,MAAM,GAAA,CAAI,SAAA,CAAU,CAAC,mBAAmB,CAAC,CAAA;AACnE,EAAA,OAAO,QAAA;AACR;AAMO,SAAS,uBACf,QAAA,EACO;AACP,EAAA,QAAA,CAAS,YAAY;AACpB,IAAA,MAAM,OAAA,CAAQ,IAAI,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,OAAA,IAAW,CAAC,CAAA;AAAA,EACrD,CAAC,CAAA;AAED,EAAA,QAAA,CAAS,sBAAsB,MAAM;AACpC,IAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC/B,MAAA,QAAA,CAAS,OAAA,CAAQ,MAAM,MAAM;AAC5B,QAAA,KAAA,MAAW,KAAK,wBAAA,EAA0B;AACzC,UAAA,IAAA,CAAK,CAAA,MAAA,EAAS,CAAA,CAAE,IAAI,CAAA,iBAAA,CAAA,EAAqB,YAAY;AACpD,YAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,KAAA,CAAM,EAAE,KAAK,CAAA;AACvC,YAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,CAAA,CAAE,KAAK,CAAA;AAEjC,YAAA,MAAA,CAAO,CAAC,GAAI,MAAM,GAAA,CAAI,eAAgB,CAAA,CAAE,IAAA,EAAM,CAAA,CAAE,OAAA;AAAA,cAC/C,CAAC,GAAG,KAAK,CAAA,CAAE,IAAA;AAAK,aACjB;AAEA,YAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACzB,cAAA,MAAM,OAAA,GAAU,CAAA,CAAE,KAAA,CAAM,IAAI,CAAA;AAC5B,cAAA,IAAI,YAAY,MAAA,EAAW;AAC3B,cAAA,MAAM,QAAA,GAAW,QAAQ,OAAO,CAAA;AAChC,cAAA,MAAA,CAAO,KAAA,CAAM,KAAK,MAAM,GAAA,CAAI,SAAS,IAAI,CAAC,CAAC,CAAA,CAAE,OAAA,CAAQ;AAAA,gBACpD,GAAG;AAAA,eACH,CAAA;AAED,cAAA,MAAM,GAAA,GAAM,MAAM,GAAA,CAAI,QAAA,CAAS,IAAA,EAAM;AAAA,gBACpC,KAAA,EAAO,CAAA;AAAA,gBACP,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,SAAS,MAAM;AAAA,eAChC,CAAA;AACD,cAAA,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAC,EAAE,OAAA,CAAQ;AAAA,gBAC/B,GAAG,SAAS,KAAA,CAAM,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,QAAA,CAAS,MAAM,CAAC;AAAA,eACjD,CAAA;AAED,cAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,QAAA,CAAS,IAAA,EAAM;AAAA,gBACrC,KAAA,EAAO,SAAS,MAAA,GAAS,CAAA;AAAA,gBACzB,GAAA,EAAK,SAAS,MAAA,GAAS;AAAA,eACvB,CAAA;AACD,cAAA,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA;AAE9B,cAAA,MAAA,CAAO,MAAM,GAAA,CAAI,QAAA,CAAS,IAAI,CAAC,EAAE,OAAA,CAAQ;AAAA,gBACxC,WAAW,QAAA,CAAS;AAAA,eACpB,CAAA;AAAA,YACF;AAGA,YAAA,MAAM,OAAO,GAAA,CAAI,QAAA,CAAS,mBAAmB,CAAC,CAAA,CAAE,QAAQ,OAAA,EAAQ;AAChE,YAAA,MAAM,MAAA;AAAA,cACL,GAAA,CAAI,SAAS,mBAAmB;AAAA,aACjC,CAAE,SAAS,aAAA,EAAc;AAGzB,YAAA,MAAM,cAAc,KAAA,CAAM,GAAA;AAAA,cAAI,CAAC,CAAA,KAC9B,CAAA,CAAE,KAAA,CAAM,CAAC,MAAM,MAAA,GACZ,EAAE,SAAA,EAAW,OAAA,CAAQ,EAAE,KAAA,CAAM,CAAC,CAAE,CAAA,CAAE,QAAO,GACzC;AAAA,aACJ;AACA,YAAA,MAAM,UAAA,GAAa,MAAM,GAAA,CAAI,SAAA,CAAU,KAAK,CAAA;AAC5C,YAAA,MAAA,CAAO,UAAA,CAAW,MAAM,CAAA,CAAE,IAAA,CAAK,MAAM,MAAM,CAAA;AAC3C,YAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACtC,cAAA,MAAA,CAAO,WAAW,CAAC,CAAC,EAAE,OAAA,CAAQ,WAAA,CAAY,CAAC,CAAC,CAAA;AAAA,YAC7C;AACA,YAAA,MAAM,MAAA,CAAO,IAAI,SAAA,CAAU,CAAC,mBAAmB,CAAC,CAAC,EAAE,QAAA,CAAS,OAAA;AAAA,cAC3D,CAAC,MAAS;AAAA,aACX;AAMA,YAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACzB,cAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AACjC,cAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AACnC,cAAA,IAAI,IAAA,KAAS,MAAA,IAAa,IAAA,CAAK,IAAA,KAAS,OAAA,EAAS;AAChD,gBAAA,MAAA,CAAO,MAAM,EAAE,OAAA,CAAQ;AAAA,kBACtB,IAAA,EAAM,SAAA;AAAA,kBACN,MAAA,EAAQ;AAAA,iBACR,CAAA;AAAA,cACF,CAAA,MAAA,IAAW,SAAS,MAAA,EAAW;AAC9B,gBAAA,MAAA,CAAO,MAAM,EAAE,OAAA,CAAQ,EAAE,MAAM,OAAA,EAAS,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,cAC1D,CAAA,MAAO;AACN,gBAAA,MAAA,CAAO,MAAM,EAAE,OAAA,CAAQ;AAAA,kBACtB,IAAA,EAAM,SAAA;AAAA,kBACN,MAAA,EAAQ;AAAA,iBACR,CAAA;AAAA,cACF;AAAA,YACD;AAGA,YAAA,MAAM,MAAA;AAAA,cACL,GAAA,CAAI,MAAM,mBAAmB;AAAA,aAC9B,CAAE,SAAS,aAAA,EAAc;AACzB,YAAA,MAAM,OAAO,GAAA,CAAI,KAAA,CAAM,mBAAmB,CAAC,CAAA,CAAE,SAAS,OAAA,CAAQ;AAAA,cAC7D,IAAA,EAAM,SAAA;AAAA,cACN,MAAA,EAAQ;AAAA,aACR,CAAA;AAAA,UACF,CAAC,CAAA;AAAA,QACF;AAAA,MACD,CAAC,CAAA;AAAA,IACF;AAEA,IAAA,KAAA,MAAW,KAAK,wBAAA,EAA0B;AACzC,MAAA,IAAA,CAAK,CAAA,MAAA,EAAS,CAAA,CAAE,IAAI,CAAA,oCAAA,CAAA,EAAwC,YAAY;AACvE,QAAA,MAAM,UAAU,EAAC;AACjB,QAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC/B,UAAA,MAAM,GAAA,GAAM,MAAM,OAAA,CAAQ,KAAA,CAAM,EAAE,KAAK,CAAA;AACvC,UAAA,OAAA,CAAQ,IAAA,CAAK,MAAM,eAAA,CAAgB,GAAG,CAAC,CAAA;AAAA,QACxC;AACA,QAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,OAAA,CAAQ,QAAQ,CAAA,EAAA,EAAK;AACxC,UAAA,MAAA,CAAO,QAAQ,CAAC,CAAC,EAAE,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAC,CAAA;AAAA,QACtC;AAAA,MACD,CAAC,CAAA;AAAA,IACF;AAAA,EACD,CAAC,CAAA;AACF","file":"index.js","sourcesContent":["import { Readable } from \"node:stream\"\nimport type { ResourceContainer } from \"../container.ts\"\n\n/** Declarative in-memory content for a {@link ResourceContainer}. */\nexport type ContainerFixtureConfig = {\n\t/** Entry name → content. Names may contain `/` for nesting. */\n\treadonly files?: Readonly<Record<string, string | Uint8Array>>\n}\n\n/**\n * An in-memory {@link ResourceContainer} driven by a declarative config.\n * The test double in the container contract suite: every assertion runs\n * against it, the directory container and the zip container, and the\n * three must agree.\n */\nexport function createContainerFixture(\n\tconfig: ContainerFixtureConfig = {},\n): ResourceContainer {\n\tconst files = config.files ?? {}\n\n\tfunction bytesOf(relPath: string): Uint8Array | undefined {\n\t\tconst content = files[relPath]\n\t\tif (content === undefined) return undefined\n\t\treturn typeof content === \"string\"\n\t\t\t? new TextEncoder().encode(content)\n\t\t\t: content\n\t}\n\n\treturn {\n\t\tasync listEntries(): Promise<readonly string[]> {\n\t\t\treturn Object.keys(files)\n\t\t},\n\n\t\tasync readEntry(relPath: string): Promise<Buffer> {\n\t\t\tconst bytes = bytesOf(relPath)\n\t\t\tif (bytes === undefined) {\n\t\t\t\tthrow new Error(`fixture container has no entry ${relPath}`)\n\t\t\t}\n\t\t\treturn Buffer.from(bytes)\n\t\t},\n\n\t\tasync readEntrySlice(\n\t\t\trelPath: string,\n\t\t\tstart: number,\n\t\t\tend: number,\n\t\t): Promise<Buffer> {\n\t\t\tconst bytes = bytesOf(relPath)\n\t\t\tif (bytes === undefined) {\n\t\t\t\tthrow new Error(`fixture container has no entry ${relPath}`)\n\t\t\t}\n\t\t\tconst clampedStart = Math.min(Math.max(0, start), bytes.length)\n\t\t\tconst clampedEnd = Math.min(Math.max(clampedStart, end), bytes.length)\n\t\t\treturn Buffer.from(bytes.subarray(clampedStart, clampedEnd))\n\t\t},\n\n\t\tasync openEntryStream(relPath: string): Promise<{\n\t\t\treadonly stream: Readable\n\t\t\treadonly size: number\n\t\t}> {\n\t\t\tconst bytes = bytesOf(relPath)\n\t\t\tif (bytes === undefined) {\n\t\t\t\tthrow new Error(`fixture container has no entry ${relPath}`)\n\t\t\t}\n\t\t\treturn { stream: Readable.from(bytes), size: bytes.length }\n\t\t},\n\n\t\tasync resolveByteRange(\n\t\t\trelPath: string,\n\t\t): Promise<{ readonly size: number } | undefined> {\n\t\t\tconst bytes = bytesOf(relPath)\n\t\t\treturn bytes === undefined ? undefined : { size: bytes.length }\n\t\t},\n\t}\n}\n","import { afterAll, describe, expect, test } from \"vitest\"\nimport type { ResourceAPI } from \"../types.ts\"\n\n/**\n * Shared contract suite for the container-backed {@link ResourceAPI}\n * implementations. Every backend (in-memory fixture, directory, zip\n * archive, and the server's own artifact view) must behave identically:\n * same entries, same bytes, same clamping, same probe semantics.\n *\n * Consumers build each backend from a declarative file map, then run the\n * whole suite with `containerContractSuite([...])` inside a test file.\n */\n\n/** A declarative container case: entry name → content. */\nexport type ContainerContractCase = {\n\treadonly name: string\n\treadonly files: Readonly<Record<string, string | Uint8Array>>\n}\n\nexport const CONTAINER_CONTRACT_CASES: readonly ContainerContractCase[] = [\n\t{ name: \"empty container\", files: {} },\n\t{\n\t\tname: \"flat files\",\n\t\tfiles: { \"a.txt\": \"alpha\", \"b.bin\": \"beta\", \"c.jpg\": \"gamma\" },\n\t},\n\t{\n\t\tname: \"nested entry names\",\n\t\tfiles: { \"dir/a.txt\": \"nested a\", \"dir/sub/b.txt\": \"nested b\" },\n\t},\n\t{\n\t\tname: \"binary content\",\n\t\tfiles: { \"blob.bin\": Uint8Array.from([0, 1, 2, 250, 255, 128]) },\n\t},\n\t{ name: \"empty entry\", files: { \"empty.txt\": \"\" } },\n\t{\n\t\tname: \"unicode entry names\",\n\t\tfiles: { \"Фото/я.jpg\": \"unicode\" },\n\t},\n]\n\nexport type ContractBackend = {\n\treadonly name: string\n\t/**\n\t * Build a {@link ResourceAPI} from a declarative file map. Called\n\t * once per case; must produce an independent instance each time.\n\t */\n\treadonly build: (\n\t\tfiles: Readonly<Record<string, string | Uint8Array>>,\n\t) => Promise<ResourceAPI>\n\t/** Optional teardown for resources created by `build` calls. */\n\treadonly cleanup?: () => Promise<void> | void\n}\n\nfunction toBytes(content: string | Uint8Array): Uint8Array {\n\treturn typeof content === \"string\"\n\t\t? new TextEncoder().encode(content)\n\t\t: content\n}\n\nasync function collectManifest(api: ResourceAPI): Promise<unknown> {\n\tconst files = [...(await api.listFileNames())].sort()\n\tconst manifest: Record<string, unknown> = {}\n\tmanifest.files = files\n\tfor (const name of files) {\n\t\tmanifest[`stat:${name}`] = await api.statFile(name)\n\t\tmanifest[`read:${name}`] = Array.from(await api.readFile(name))\n\t\tmanifest[`range:${name}`] = Array.from(\n\t\t\tawait api.readFile(name, { start: 1, end: 3 }),\n\t\t)\n\t\t// Identification must agree across backends too — the sniffer\n\t\t// reads through the same container abstraction.\n\t\tmanifest[`type:${name}`] = await api.sniff(name)\n\t\tmanifest[`probe:${name}`] = await api.probe(name)\n\t}\n\t// Batch stat: positions preserved, missing entries undefined.\n\tconst batched = await api.statFiles(files)\n\tmanifest.statAll = files.map((name, i) => ({\n\t\tname,\n\t\tstat: batched[i],\n\t}))\n\tmanifest.missingStat = await api.statFile(\"missing-entry.bin\")\n\tmanifest.missingStatAll = await api.statFiles([\"missing-entry.bin\"])\n\treturn manifest\n}\n\n/**\n * Run the full container contract suite against the given backends.\n * Must be called from within a vitest test file.\n */\nexport function containerContractSuite(\n\tbackends: readonly ContractBackend[],\n): void {\n\tafterAll(async () => {\n\t\tawait Promise.all(backends.map((b) => b.cleanup?.()))\n\t})\n\n\tdescribe(\"container contract\", () => {\n\t\tfor (const backend of backends) {\n\t\t\tdescribe(backend.name, () => {\n\t\t\t\tfor (const c of CONTAINER_CONTRACT_CASES) {\n\t\t\t\t\ttest(`case \"${c.name}\": core semantics`, async () => {\n\t\t\t\t\t\tconst api = await backend.build(c.files)\n\t\t\t\t\t\tconst names = Object.keys(c.files)\n\n\t\t\t\t\t\texpect([...(await api.listFileNames())].sort()).toEqual(\n\t\t\t\t\t\t\t[...names].sort(),\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\tfor (const name of names) {\n\t\t\t\t\t\t\tconst content = c.files[name]\n\t\t\t\t\t\t\tif (content === undefined) continue\n\t\t\t\t\t\t\tconst expected = toBytes(content)\n\t\t\t\t\t\t\texpect(Array.from(await api.readFile(name))).toEqual([\n\t\t\t\t\t\t\t\t...expected,\n\t\t\t\t\t\t\t])\n\n\t\t\t\t\t\t\tconst mid = await api.readFile(name, {\n\t\t\t\t\t\t\t\tstart: 1,\n\t\t\t\t\t\t\t\tend: Math.min(3, expected.length),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\texpect(Array.from(mid)).toEqual([\n\t\t\t\t\t\t\t\t...expected.slice(1, Math.min(3, expected.length)),\n\t\t\t\t\t\t\t])\n\n\t\t\t\t\t\t\tconst past = await api.readFile(name, {\n\t\t\t\t\t\t\t\tstart: expected.length + 5,\n\t\t\t\t\t\t\t\tend: expected.length + 9,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\texpect(past.byteLength).toBe(0)\n\n\t\t\t\t\t\t\texpect(await api.statFile(name)).toEqual({\n\t\t\t\t\t\t\t\tsizeBytes: expected.length,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Missing entries: readFile rejects, statFile is undefined.\n\t\t\t\t\t\tawait expect(api.readFile(\"missing-entry.bin\")).rejects.toThrow()\n\t\t\t\t\t\tawait expect(\n\t\t\t\t\t\t\tapi.statFile(\"missing-entry.bin\"),\n\t\t\t\t\t\t).resolves.toBeUndefined()\n\n\t\t\t\t\t\t// Batch stat matches the single-stats, in order.\n\t\t\t\t\t\tconst singleStats = names.map((n) =>\n\t\t\t\t\t\t\tc.files[n] !== undefined\n\t\t\t\t\t\t\t\t? { sizeBytes: toBytes(c.files[n]!).length }\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tconst batchStats = await api.statFiles(names)\n\t\t\t\t\t\texpect(batchStats.length).toBe(names.length)\n\t\t\t\t\t\tfor (let i = 0; i < names.length; i++) {\n\t\t\t\t\t\t\texpect(batchStats[i]).toEqual(singleStats[i])\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait expect(api.statFiles([\"missing-entry.bin\"])).resolves.toEqual(\n\t\t\t\t\t\t\t[undefined],\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\t// Contract backends wire no probe implementations, so\n\t\t\t\t\t\t// identified media reports the dedicated \"no backend\"\n\t\t\t\t\t\t// reason instead of looking like a decode failure,\n\t\t\t\t\t\t// while non-media entries still identify normally.\n\t\t\t\t\t\tfor (const name of names) {\n\t\t\t\t\t\t\tconst type = await api.sniff(name)\n\t\t\t\t\t\t\tconst probed = await api.probe(name)\n\t\t\t\t\t\t\tif (type !== undefined && type.kind !== \"other\") {\n\t\t\t\t\t\t\t\texpect(probed).toEqual({\n\t\t\t\t\t\t\t\t\tkind: \"unknown\",\n\t\t\t\t\t\t\t\t\treason: \"unavailable\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t} else if (type !== undefined) {\n\t\t\t\t\t\t\t\texpect(probed).toEqual({ kind: \"other\", mime: type.mime })\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\texpect(probed).toEqual({\n\t\t\t\t\t\t\t\t\tkind: \"unknown\",\n\t\t\t\t\t\t\t\t\treason: \"unsupported\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Neither call rejects on a missing entry.\n\t\t\t\t\t\tawait expect(\n\t\t\t\t\t\t\tapi.sniff(\"missing-entry.bin\"),\n\t\t\t\t\t\t).resolves.toBeUndefined()\n\t\t\t\t\t\tawait expect(api.probe(\"missing-entry.bin\")).resolves.toEqual({\n\t\t\t\t\t\t\tkind: \"unknown\",\n\t\t\t\t\t\t\treason: \"unsupported\",\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\tfor (const c of CONTAINER_CONTRACT_CASES) {\n\t\t\ttest(`case \"${c.name}\": identical results across backends`, async () => {\n\t\t\t\tconst results = []\n\t\t\t\tfor (const backend of backends) {\n\t\t\t\t\tconst api = await backend.build(c.files)\n\t\t\t\t\tresults.push(await collectManifest(api))\n\t\t\t\t}\n\t\t\t\tfor (let i = 1; i < results.length; i++) {\n\t\t\t\t\texpect(results[i]).toEqual(results[0])\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"]}