History

fix(server): canonicalise media paths the same way their callers do (#9575)

Julius Marminge committed 2 months ago

Commit 5f4c7161 parent 912276b3 files · +22 −11Browse files

Unified · Split

3 files
  1. apps/server/src/assets/AssetAccess.test.ts +2 −2
  2. apps/server/src/assets/MediaFile.ts +10 −1
  3. apps/server/src/project/ProjectFaviconResolver.test.ts +10 −8
apps/server/src/assets/AssetAccess.test.ts+2−2
@@ -754,7 +754,7 @@ describe("AssetAccess", () => { · 753 unmodified lines
754 projectFaviconPath: "brand/custom.svg",754 projectFaviconPath: "brand/custom.svg",
755 });755 });
756 756
757 expect(result.sourcePath).toBe("brand/custom.svg");757 expect(result.sourcePath).toBe(path.join("brand", "custom.svg"));
758 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-custom\.svg$/);758 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-custom\.svg$/);
759 }).pipe(Effect.provide(testLayer)),759 }).pipe(Effect.provide(testLayer)),
760 );760 );
@@ -813,7 +813,7 @@ describe("AssetAccess", () => { · 52 unmodified lines
813 projectFaviconPath: "brand/saved.svg",813 projectFaviconPath: "brand/saved.svg",
814 });814 });
815 815
816 expect(result.sourcePath).toBe("brand/saved.svg");816 expect(result.sourcePath).toBe(path.join("brand", "saved.svg"));
817 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-saved\.svg$/);817 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-saved\.svg$/);
818 }).pipe(Effect.provide(testLayer)),818 }).pipe(Effect.provide(testLayer)),
819 );819 );
apps/server/src/assets/MediaFile.ts+10−1
@@ -37,6 +37,11 @@ export interface OpenMediaFile { · 36 unmodified lines
37 readonly info: NodeFS.BigIntStats;37 readonly info: NodeFS.BigIntStats;
38}38}
39 39
40const realpathLikeFileSystem = (filePath: string) =>
41 new Promise<string>((resolve, reject) => {
42 NodeFS.realpath(filePath, (error, resolved) => (error ? reject(error) : resolve(resolved)));
43 });
44
40/** Opens a canonical media path once. Replacements cannot change the response's source. */45/** Opens a canonical media path once. Replacements cannot change the response's source. */
41export const openMediaFile = Effect.fn("openMediaFile")(function* (46export const openMediaFile = Effect.fn("openMediaFile")(function* (
42 filePath: string,47 filePath: string,
@@ -71,7 +76,11 @@ export const openMediaFile = Effect.fn("openMediaFile")(function* ( · 28 unmodified lines
71 ) {76 ) {
72 return null;77 return null;
73 }78 }
74 if ((await NodeFSP.realpath(filePath)) !== filePath) return null;79 // Callers canonicalise with Effect's FileSystem.realPath, which is
80 // Node's JS realpath. fs/promises.realpath is the native binding and
81 // on Windows also expands 8.3 short names, so a path that is already
82 // canonical by the caller's rules would still look swapped here.
83 if ((await realpathLikeFileSystem(filePath)) !== filePath) return null;
75 const after = await NodeFSP.lstat(filePath, { bigint: true });84 const after = await NodeFSP.lstat(filePath, { bigint: true });
76 if (!after.isFile() || info.dev !== after.dev || info.ino !== after.ino) return null;85 if (!after.isFile() || info.dev !== after.dev || info.ino !== after.ino) return null;
77 accepted = true;86 accepted = true;
apps/server/src/project/ProjectFaviconResolver.test.ts+10−8
@@ -126,6 +126,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 125 unmodified lines
126 it.effect("prefers a t3.json iconPath over well-known files", () =>126 it.effect("prefers a t3.json iconPath over well-known files", () =>
127 Effect.gen(function* () {127 Effect.gen(function* () {
128 const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;128 const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
129 const path = yield* Path.Path;
129 const cwd = yield* makeTempDir;130 const cwd = yield* makeTempDir;
130 yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "brand/mark.svg" }');131 yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "brand/mark.svg" }');
131 yield* writeTextFile(cwd, "brand/mark.svg", "<svg>mark</svg>");132 yield* writeTextFile(cwd, "brand/mark.svg", "<svg>mark</svg>");
@@ -134,13 +135,14 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 2 unmodified lines
134 const resolved = yield* resolver.resolvePath(cwd);135 const resolved = yield* resolver.resolvePath(cwd);
135 136
136 expect(resolved).not.toBeNull();137 expect(resolved).not.toBeNull();
137 expect(resolved).toContain("brand/mark.svg");138 expect(resolved).toBe(path.join(cwd, "brand", "mark.svg"));
138 }),139 }),
139 );140 );
140 141
141 it.effect("uses a saved project favicon override", () =>142 it.effect("uses a saved project favicon override", () =>
142 Effect.gen(function* () {143 Effect.gen(function* () {
143 const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;144 const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
145 const path = yield* Path.Path;
144 const cwd = yield* makeTempDir;146 const cwd = yield* makeTempDir;
145 yield* writeTextFile(cwd, "brand/custom.svg", "<svg>custom</svg>");147 yield* writeTextFile(cwd, "brand/custom.svg", "<svg>custom</svg>");
146 yield* writeTextFile(cwd, "favicon.svg", "<svg>automatic</svg>");148 yield* writeTextFile(cwd, "favicon.svg", "<svg>automatic</svg>");
@@ -148,7 +150,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 1 unmodified line
148 const resolved = yield* resolver.resolvePath(cwd, "brand/custom.svg");150 const resolved = yield* resolver.resolvePath(cwd, "brand/custom.svg");
149 151
150 expect(resolved).not.toBeNull();152 expect(resolved).not.toBeNull();
151 expect(resolved).toContain("brand/custom.svg");153 expect(resolved).toBe(path.join(cwd, "brand", "custom.svg"));
152 }),154 }),
153 );155 );
154 156
@@ -232,7 +234,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 77 unmodified lines
232 const resolved = yield* resolver.resolvePath(cwd);234 const resolved = yield* resolver.resolvePath(cwd);
233 235
234 expect(resolved).not.toBeNull();236 expect(resolved).not.toBeNull();
235 expect(resolved).toContain("public/brand/logo.svg");237 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
236 }),238 }),
237 );239 );
238 240
@@ -257,7 +259,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 18 unmodified lines
257 const resolved = yield* resolver.resolvePath(cwd);259 const resolved = yield* resolver.resolvePath(cwd);
258 260
259 expect(resolved).not.toBeNull();261 expect(resolved).not.toBeNull();
260 expect(resolved).toContain("public/brand/logo.svg");262 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
261 }),263 }),
262 );264 );
263 265
@@ -275,7 +277,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 11 unmodified lines
275 const resolved = yield* resolver.resolvePath(cwd);277 const resolved = yield* resolver.resolvePath(cwd);
276 278
277 expect(resolved).not.toBeNull();279 expect(resolved).not.toBeNull();
278 expect(resolved).toContain("public/brand/logo.svg");280 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
279 }),281 }),
280 );282 );
281 283
@@ -293,7 +295,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 11 unmodified lines
293 const resolved = yield* resolver.resolvePath(cwd);295 const resolved = yield* resolver.resolvePath(cwd);
294 296
295 expect(resolved).not.toBeNull();297 expect(resolved).not.toBeNull();
296 expect(resolved).toContain("public/brand/logo.svg");298 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
297 }),299 }),
298 );300 );
299 301
@@ -311,7 +313,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 11 unmodified lines
311 const resolved = yield* resolver.resolvePath(cwd);313 const resolved = yield* resolver.resolvePath(cwd);
312 314
313 expect(resolved).not.toBeNull();315 expect(resolved).not.toBeNull();
314 expect(resolved).toContain("public/brand/logo.svg");316 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
315 }),317 }),
316 );318 );
317 319
@@ -459,7 +461,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 141 unmodified lines
459 const resolved = yield* resolver.resolvePath(cwd);461 const resolved = yield* resolver.resolvePath(cwd);
460 462
461 expect(resolved).not.toBeNull();463 expect(resolved).not.toBeNull();
462 expect(resolved).toContain("public/brand/logo.svg");464 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
463 }),465 }),
464 );466 );
465 });467 });

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t