@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,131 @@
1
+ import { readFile, rename, rm, writeFile } from "node:fs/promises"
2
+ import { join } from "node:path"
3
+
4
+ /**
5
+ * Explicit file order for a resource's content root (`data/`). Uploads
6
+ * install entries in the user's chosen order and persist that sequence
7
+ * in this manifest so the display order matches the upload order even
8
+ * when filenames sort differently. Written at commit time; because it
9
+ * lives inside the replaced `data/` subtree it is always consistent
10
+ * with the entries around it.
11
+ *
12
+ * The manifest is a JSON array of relative entry names (`/`-separated),
13
+ * listing the final installed names (after collision suffixing). Reads
14
+ * are strict: a malformed manifest, or one whose entries do not all
15
+ * resolve against the actual listing, is ignored entirely and callers
16
+ * fall back to the natural name sort. Strictness matters because
17
+ * `createDirectoryContainer` also serves arbitrary user directories
18
+ * (folder import, plugin dev) that may coincidentally contain a file
19
+ * named `.order`.
20
+ */
21
+ export const ORDER_MANIFEST_NAME = ".order"
22
+
23
+ /** Path of the order manifest inside a resource content root. */
24
+ export function orderManifestPath(dataDir: string): string {
25
+ return join(dataDir, ORDER_MANIFEST_NAME)
26
+ }
27
+
28
+ /**
29
+ * Atomically persist `names` as the manifest of `dataDir` (tmp + rename,
30
+ * so a concurrent reader never observes a half-written file).
31
+ */
32
+ export async function writeOrderManifest(
33
+ dataDir: string,
34
+ names: readonly string[],
35
+ ): Promise<void> {
36
+ const dest = orderManifestPath(dataDir)
37
+ const tmp = `${dest}.writing-${process.pid}-${Date.now()}`
38
+ try {
39
+ await writeFile(tmp, JSON.stringify(names), "utf8")
40
+ await rename(tmp, dest)
41
+ } catch (err) {
42
+ await rm(tmp, { force: true }).catch(() => {})
43
+ throw err
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Read the manifest of `dataDir`. Resolves to `undefined` when the
49
+ * manifest is absent or fails structural validation
50
+ * ({@link parseOrderManifest}) — callers fall back to natural order.
51
+ */
52
+ export async function readOrderManifest(
53
+ dataDir: string,
54
+ ): Promise<readonly string[] | undefined> {
55
+ let raw: string
56
+ try {
57
+ raw = await readFile(orderManifestPath(dataDir), "utf8")
58
+ } catch {
59
+ return undefined
60
+ }
61
+ return parseOrderManifest(raw)
62
+ }
63
+
64
+ /**
65
+ * Structural validation of a raw manifest body: a JSON array of
66
+ * non-empty relative paths with `/` separators that cannot escape the
67
+ * content root. Returns `undefined` when the body is not a usable
68
+ * manifest. Semantic validation (entries exist in the listing) happens
69
+ * in {@link orderEntries}.
70
+ */
71
+ export function parseOrderManifest(raw: string): readonly string[] | undefined {
72
+ let parsed: unknown
73
+ try {
74
+ parsed = JSON.parse(raw)
75
+ } catch {
76
+ return undefined
77
+ }
78
+ if (!Array.isArray(parsed) || parsed.length === 0) return undefined
79
+ const names: string[] = []
80
+ for (const entry of parsed) {
81
+ if (typeof entry !== "string") return undefined
82
+ if (!isManifestPathSafe(entry)) return undefined
83
+ names.push(entry)
84
+ }
85
+ if (names.length === 0) return undefined
86
+ return names
87
+ }
88
+
89
+ /**
90
+ * Reorder `listed` by the manifest: entries named by the manifest come
91
+ * first, in manifest order; entries the manifest does not mention are
92
+ * appended in natural order. Returns `undefined` when the manifest
93
+ * cannot be applied against this listing (some manifest entry is
94
+ * missing — the strict trust rule), so the caller falls back to the
95
+ * natural sort.
96
+ */
97
+ export function orderEntries(
98
+ manifest: readonly string[],
99
+ listed: readonly string[],
100
+ ): readonly string[] | undefined {
101
+ const available = new Set(listed)
102
+ const uniqueManifest = new Set<string>()
103
+ for (const name of manifest) {
104
+ if (uniqueManifest.has(name)) return undefined
105
+ uniqueManifest.add(name)
106
+ if (!available.has(name)) return undefined
107
+ }
108
+ const ordered = manifest.filter((name) => available.has(name))
109
+ const rest = naturalSort(listed.filter((name) => !uniqueManifest.has(name)))
110
+ return [...ordered, ...rest]
111
+ }
112
+
113
+ /** Case-insensitive natural name sort (`1 < 2 < 10`, `a < b`). */
114
+ export function naturalSort(names: readonly string[]): readonly string[] {
115
+ return [...names].sort((a, b) =>
116
+ a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }),
117
+ )
118
+ }
119
+
120
+ function isManifestPathSafe(entry: string): boolean {
121
+ if (entry.length === 0 || entry.includes("\0") || entry.includes("\\")) {
122
+ return false
123
+ }
124
+ const segments = entry.split("/")
125
+ for (const segment of segments) {
126
+ if (segment.length === 0 || segment === "." || segment === "..") {
127
+ return false
128
+ }
129
+ }
130
+ return true
131
+ }
@@ -0,0 +1,226 @@
1
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { describe, expect, test } from "vitest"
5
+ import {
6
+ assertInside,
7
+ assertSafeSegment,
8
+ createStoragePaths,
9
+ imageVariantKey,
10
+ } from "./paths.ts"
11
+
12
+ // path-guard-exempt: platform branch, each literal matches its own OS.
13
+ const ROOT = process.platform === "win32" ? "C:\\data\\app" : "/data/app"
14
+
15
+ describe("createStoragePaths", () => {
16
+ test("defaults latestVersion from max versions/<n> on disk when omitted", () => {
17
+ const root = mkdtempSync(join(tmpdir(), "paths-ver-"))
18
+ try {
19
+ mkdirSync(join(root, "versions", "1"), { recursive: true })
20
+ mkdirSync(join(root, "versions", "2"), { recursive: true })
21
+ const paths = createStoragePaths({ root })
22
+ expect(paths.latestVersion).toBe(2)
23
+ expect(paths.activeVersion).toBe(2)
24
+ expect(paths.active.version).toBe(2)
25
+ expect(paths.latest.version).toBe(2)
26
+ expect(paths.active.root).toContain(`${sep()}versions${sep()}2`)
27
+ } finally {
28
+ rmSync(root, { recursive: true, force: true })
29
+ }
30
+ })
31
+
32
+ test("rejects relative roots", () => {
33
+ expect(() => createStoragePaths({ root: "relative/path" })).toThrow(
34
+ /absolute path/,
35
+ )
36
+ })
37
+
38
+ test("builds versions and local subtrees under the root", () => {
39
+ const paths = createStoragePaths({ root: ROOT })
40
+ expect(paths.active.root).toContain("versions")
41
+ expect(paths.local.root).toContain("local")
42
+ expect(paths.active.root).not.toBe(paths.local.root)
43
+ })
44
+
45
+ test("resource path nests under versions/resources/<id>", () => {
46
+ const paths = createStoragePaths({ root: ROOT })
47
+ const p = paths.active.resource("res_42")
48
+ expect(p.endsWith(`resources${sep()}res_42`)).toBe(true)
49
+ expect(p.startsWith(paths.active.root)).toBe(true)
50
+ })
51
+
52
+ test("version plugins nest under versions/<v>/plugins", () => {
53
+ const paths = createStoragePaths({ root: ROOT })
54
+ const p = paths.latest.plugins()
55
+ expect(p.endsWith(`versions${sep()}1${sep()}plugins`)).toBe(true)
56
+ expect(p.startsWith(paths.latest.root)).toBe(true)
57
+ expect(p.startsWith(paths.local.root)).toBe(false)
58
+ })
59
+
60
+ test("upload staging root and pool nest under local/.tmp", () => {
61
+ const paths = createStoragePaths({ root: ROOT })
62
+ const root = paths.local.uploadStagingRoot()
63
+ expect(root.endsWith(`${sep()}local${sep()}.tmp`)).toBe(true)
64
+ expect(root.startsWith(paths.local.root)).toBe(true)
65
+ const pool = paths.local.stagingPoolRoot()
66
+ expect(pool.startsWith(`${root}${sep()}`)).toBe(true)
67
+ const file = paths.local.stagingPoolFile(
68
+ "550e8400-e29b-41d4-a716-446655440000",
69
+ ".png",
70
+ )
71
+ expect(file.startsWith(`${pool}${sep()}`)).toBe(true)
72
+ expect(file.endsWith("550e8400-e29b-41d4-a716-446655440000.png")).toBe(true)
73
+ })
74
+
75
+ test("cache root nests under local/cache", () => {
76
+ const paths = createStoragePaths({ root: ROOT })
77
+ const cache = paths.local.cache()
78
+ expect(cache.endsWith(`${sep()}local${sep()}cache`)).toBe(true)
79
+ expect(cache.startsWith(paths.local.root)).toBe(true)
80
+ })
81
+
82
+ test("derived caches nest under local/cache, persistent dirs do not", () => {
83
+ const paths = createStoragePaths({ root: ROOT })
84
+ const cache = paths.local.cache()
85
+ const derived = [
86
+ paths.local.resource("res_1"),
87
+ paths.local.character("char_1"),
88
+ paths.local.resFilesCache("res_1"),
89
+ paths.local.resFilePreviewDir("res_1"),
90
+ paths.local.resVideoFrame("res_1", "clip.mp4", 1000),
91
+ paths.local.resExtractedDir("res_1", 1),
92
+ paths.local.resExtractedEntry("res_1", 1, "a/b.png"),
93
+ paths.local.tmp(),
94
+ paths.local.tmpFile("scratch.bin"),
95
+ ]
96
+ for (const p of derived) {
97
+ expect(p.startsWith(`${cache}${sep()}`)).toBe(true)
98
+ }
99
+ const persistent = [
100
+ paths.local.trash(),
101
+ paths.local.logs(),
102
+ paths.local.sessionKey(),
103
+ paths.local.uploadStagingRoot(),
104
+ paths.local.stagingPoolRoot(),
105
+ ]
106
+ for (const p of persistent) {
107
+ expect(p.startsWith(`${cache}${sep()}`)).toBe(false)
108
+ expect(p.startsWith(paths.local.root)).toBe(true)
109
+ }
110
+ })
111
+
112
+ test("thumb path nests under local/cache/<kind>/<id>/<variant>.webp", () => {
113
+ const paths = createStoragePaths({ root: ROOT })
114
+ const r = paths.local.localCover("resource", "res_1", "preview", "webp")
115
+ expect(r.endsWith(`resources${sep()}res_1${sep()}preview.webp`)).toBe(true)
116
+ expect(r.startsWith(paths.local.cache())).toBe(true)
117
+ const c = paths.local.localCover("character", "char_1", "avatar", "webp")
118
+ expect(c.endsWith(`characters${sep()}char_1${sep()}avatar.webp`)).toBe(true)
119
+ expect(c.startsWith(paths.local.cache())).toBe(true)
120
+ })
121
+
122
+ test("per-file variant cache basenames fold ext, key and flattened separators", () => {
123
+ const paths = createStoragePaths({ root: ROOT })
124
+ const variant = paths.local.resFileVariant(
125
+ "res_1",
126
+ "dir/page.jpg",
127
+ "ab12cd34",
128
+ "avif",
129
+ )
130
+ expect(variant).toMatch(/dir__page__jpg__[0-9a-f]{8}__ab12cd34\.avif$/)
131
+ // Backslash names from odd archives are flattened the same way.
132
+ const frame = paths.local.resVideoFrame("res_1", "sub\\clip.mp4", 1000)
133
+ expect(frame).toMatch(/sub__clip__mp4__[0-9a-f]{8}[\\/]1000\.avif$/)
134
+ })
135
+
136
+ test("same-named files in nested folders never share a cache identity", () => {
137
+ const paths = createStoragePaths({ root: ROOT })
138
+ const a = paths.local.resFileVariant("res_1", "a/tex.png", "k1", "avif")
139
+ const b = paths.local.resFileVariant("res_1", "b/tex.png", "k1", "avif")
140
+ expect(a).not.toBe(b)
141
+ // Same file, same spec: stable identity across calls.
142
+ const again = paths.local.resFileVariant("res_1", "a/tex.png", "k1", "avif")
143
+ expect(again).toBe(a)
144
+ })
145
+
146
+ test("flattening ambiguities are disambiguated by the path hash", () => {
147
+ const paths = createStoragePaths({ root: ROOT })
148
+ // Both flatten to `a__b__c__png`; only the hash tells them apart.
149
+ const folderUnderscore = paths.local.resFileVariant(
150
+ "res_1",
151
+ "a__b/c.png",
152
+ "k1",
153
+ "avif",
154
+ )
155
+ const fileUnderscore = paths.local.resFileVariant(
156
+ "res_1",
157
+ "a/b__c.png",
158
+ "k1",
159
+ "avif",
160
+ )
161
+ expect(folderUnderscore).not.toBe(fileUnderscore)
162
+ })
163
+
164
+ test("extracted entries carry the same collision-free identity", () => {
165
+ const paths = createStoragePaths({ root: ROOT })
166
+ const first = paths.local.resExtractedEntry("res_1", 1, "a__b/c.png")
167
+ const second = paths.local.resExtractedEntry("res_1", 1, "a/b__c.png")
168
+ expect(first).not.toBe(second)
169
+ expect(first).toMatch(/a__b__c__png__[0-9a-f]{8}$/)
170
+ })
171
+
172
+ test("imageVariantKey is a short stable hex digest", () => {
173
+ const key = imageVariantKey("avif:inside:4000000:70:90")
174
+ expect(key).toMatch(/^[0-9a-f]{8}$/)
175
+ expect(imageVariantKey("avif:inside:4000000:70:90")).toBe(key)
176
+ expect(imageVariantKey("webp:exact:4000000:70:90")).not.toBe(key)
177
+ })
178
+
179
+ test("rejects path-like ids to prevent directory traversal", () => {
180
+ const paths = createStoragePaths({ root: ROOT })
181
+ expect(() => paths.active.resource("../escape")).toThrow(/separators/)
182
+ expect(() => paths.active.resource("../../etc/passwd")).toThrow(
183
+ /separators/,
184
+ )
185
+ expect(() => paths.active.resource("a\\b")).toThrow(/separators/)
186
+ })
187
+
188
+ test("rejects Windows reserved basenames and trailing dot/space", () => {
189
+ const paths = createStoragePaths({ root: ROOT })
190
+ expect(() => paths.active.resource("CON")).toThrow(/reserved/)
191
+ expect(() => paths.active.resource("prn.txt")).toThrow(/reserved/)
192
+ expect(() => paths.active.resource("trailing.")).toThrow(/dot or space/)
193
+ expect(() => paths.active.resource("trailing ")).toThrow(/dot or space/)
194
+ })
195
+
196
+ test("rejects empty and dot segments", () => {
197
+ expect(() => assertSafeSegment("")).toThrow(/empty/)
198
+ expect(() => assertSafeSegment(".")).toThrow(/'\.'/)
199
+ expect(() => assertSafeSegment("..")).toThrow(/'\.\.'/)
200
+ })
201
+
202
+ test("rejects control characters", () => {
203
+ expect(() => assertSafeSegment("nul\u0000byte")).toThrow(/disallowed/)
204
+ })
205
+ })
206
+
207
+ describe("assertInside", () => {
208
+ test("accepts descendants and the ancestor itself", () => {
209
+ expect(assertInside(ROOT, ROOT)).toBe(ROOT)
210
+ const descendant =
211
+ process.platform === "win32"
212
+ ? `${ROOT}\\sub\\leaf.txt`
213
+ : `${ROOT}/sub/leaf.txt`
214
+ expect(assertInside(ROOT, descendant)).toBe(descendant)
215
+ })
216
+
217
+ test("rejects siblings that share a prefix", () => {
218
+ const sibling =
219
+ process.platform === "win32" ? `${ROOT}-other` : `${ROOT}-other`
220
+ expect(() => assertInside(ROOT, sibling)).toThrow(/escapes/)
221
+ })
222
+ })
223
+
224
+ function sep(): string {
225
+ return process.platform === "win32" ? "\\" : "/"
226
+ }