mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:53:31 +00:00
fix: validate local web manifest icon refs (#3596)
Make derived manifest.json rewriting fail fast for missing local icon refs instead of falling back to unhashed root paths. Keep external and data URLs unchanged, and add regression coverage for root-relative local icons, missing local icons, and passthrough external/data refs. If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) ## Description: Describe the PR. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
This commit is contained in:
@@ -4,7 +4,6 @@ import { globSync } from "glob";
|
||||
import path from "path";
|
||||
import {
|
||||
type AssetManifest,
|
||||
buildAssetUrl,
|
||||
encodeAssetPath,
|
||||
normalizeAssetPath,
|
||||
} from "../core/AssetUrls";
|
||||
@@ -97,6 +96,12 @@ function getEmittedAssetRelativePath(
|
||||
return path.posix.relative(emittedFromDir, emittedTargetPath);
|
||||
}
|
||||
|
||||
function isExternalAssetReference(referencePath: string): boolean {
|
||||
return (
|
||||
/^[a-z][a-z0-9+.-]*:/i.test(referencePath) || referencePath.startsWith("//")
|
||||
);
|
||||
}
|
||||
|
||||
function renderWebManifestAsset({
|
||||
resourcesDir,
|
||||
assetManifest,
|
||||
@@ -105,10 +110,38 @@ function renderWebManifestAsset({
|
||||
const manifest = JSON.parse(fs.readFileSync(sourcePath, "utf8")) as {
|
||||
icons?: Array<{ src?: string }>;
|
||||
};
|
||||
manifest.icons = manifest.icons?.map((icon) => ({
|
||||
...icon,
|
||||
src: buildAssetUrl(icon.src ?? "", assetManifest),
|
||||
}));
|
||||
manifest.icons = manifest.icons?.map((icon) => {
|
||||
const src = icon.src;
|
||||
if (src === undefined) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
if (src.trim().length === 0) {
|
||||
throw new Error(
|
||||
"Derived asset manifest.json contains an icon with a blank src",
|
||||
);
|
||||
}
|
||||
|
||||
if (isExternalAssetReference(src)) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
const referencedAssetPath = resolveDerivedAssetReference(
|
||||
"manifest.json",
|
||||
src,
|
||||
);
|
||||
const referencedHashedUrl = assetManifest[referencedAssetPath];
|
||||
if (!referencedHashedUrl) {
|
||||
throw new Error(
|
||||
`Derived asset manifest.json references ${referencedAssetPath}, but it is missing from the asset manifest`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...icon,
|
||||
src: referencedHashedUrl,
|
||||
};
|
||||
});
|
||||
return `${JSON.stringify(manifest, null, 2)}\n`;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,23 @@ describe("PublicAssetManifest", () => {
|
||||
);
|
||||
}
|
||||
|
||||
async function writeWebManifestFixture(
|
||||
resourcesDir: string,
|
||||
icons: Array<{ src?: string }>,
|
||||
): Promise<void> {
|
||||
await fs.writeFile(
|
||||
path.join(resourcesDir, "manifest.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "OpenFront",
|
||||
icons,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
clearPublicAssetManifestCache();
|
||||
if (tempDir) {
|
||||
@@ -81,17 +98,9 @@ describe("PublicAssetManifest", () => {
|
||||
const { resourcesDir, outDir } = await createTempResources();
|
||||
|
||||
await fs.mkdir(path.join(resourcesDir, "icons"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(resourcesDir, "manifest.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "OpenFront",
|
||||
icons: [{ src: "icons/app-icon.png" }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
await writeWebManifestFixture(resourcesDir, [
|
||||
{ src: "icons/app-icon.png" },
|
||||
]);
|
||||
await fs.writeFile(
|
||||
path.join(resourcesDir, "icons", "app-icon.png"),
|
||||
"icon-v1",
|
||||
@@ -125,6 +134,61 @@ describe("PublicAssetManifest", () => {
|
||||
expect(firstOutput).not.toContain(secondIconHref);
|
||||
});
|
||||
|
||||
test("rewrites root-relative web manifest icon paths to hashed URLs", async () => {
|
||||
const { resourcesDir, outDir } = await createTempResources();
|
||||
|
||||
await fs.mkdir(path.join(resourcesDir, "icons"), { recursive: true });
|
||||
await writeWebManifestFixture(resourcesDir, [
|
||||
{ src: "/icons/app-icon.png" },
|
||||
]);
|
||||
await fs.writeFile(
|
||||
path.join(resourcesDir, "icons", "app-icon.png"),
|
||||
"icon-v1",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const assetManifest = buildPublicAssetManifest(resourcesDir);
|
||||
createHashedPublicAssetFiles(resourcesDir, outDir, assetManifest);
|
||||
|
||||
const emittedManifest = await emitHashedAsset(
|
||||
outDir,
|
||||
assetManifest["manifest.json"],
|
||||
);
|
||||
|
||||
expect(emittedManifest).toContain(assetManifest["icons/app-icon.png"]);
|
||||
expect(emittedManifest).not.toContain('"/icons/app-icon.png"');
|
||||
});
|
||||
|
||||
test("fails when web manifest references a missing local icon", async () => {
|
||||
const { resourcesDir } = await createTempResources();
|
||||
|
||||
await writeWebManifestFixture(resourcesDir, [{ src: "icons/missing.png" }]);
|
||||
|
||||
expect(() => buildPublicAssetManifest(resourcesDir)).toThrow(
|
||||
/manifest\.json references icons\/missing\.png/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("leaves external and data web manifest icon refs unchanged", async () => {
|
||||
const { resourcesDir, outDir } = await createTempResources();
|
||||
|
||||
await writeWebManifestFixture(resourcesDir, [
|
||||
{ src: "https://cdn.example.com/app-icon.png" },
|
||||
{ src: "data:image/png;base64,AAA" },
|
||||
]);
|
||||
|
||||
const assetManifest = buildPublicAssetManifest(resourcesDir);
|
||||
createHashedPublicAssetFiles(resourcesDir, outDir, assetManifest);
|
||||
|
||||
const emittedManifest = await emitHashedAsset(
|
||||
outDir,
|
||||
assetManifest["manifest.json"],
|
||||
);
|
||||
|
||||
expect(emittedManifest).toContain("https://cdn.example.com/app-icon.png");
|
||||
expect(emittedManifest).toContain("data:image/png;base64,AAA");
|
||||
});
|
||||
|
||||
test("rewrites BMFont XML page filenames to hashed relative paths", async () => {
|
||||
const { resourcesDir, outDir } = await createTempResources();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user