mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 21:23:04 +00:00
* fix(app): stop a string SVG transform from crashing the app
A root <Svg> is a real React Native view, so react-native-svg rewrites a
*string* `transform` into RN style syntax in `extractTransformSvgView`,
using a PEG parser whose grammar only accepts SVG transform functions.
Upstream lets that parser's SyntaxError escape, so any string it does not
understand takes the whole app down during render:
SyntaxError: Expected transform functions but "n" found.
"found": "n", "location": { "start": { "offset": 0 } }
`none` reproduces that message character for character; so does any other
CSS value (`rotate(-90deg)`, `translate(10px, 0)`). Every sibling
extractor in that file already catches the same error and degrades, so
the asymmetry is the bug: a string that is merely ignored on a <Path>
crashes on an <Svg>.
We feed that path untrusted input. The daemon scans a project for
favicon.svg / icon.svg / logo.svg (packages/server/src/utils/project-icon.ts)
and ships it to the client, where ProjectIconImage hands it to SvgCss.
Running the library's own XML parser over `<svg style="transform:none">`
yields exactly `transform: "none"` on the root, which is why the crash
starts once a host connects and the project icons render, with no
terminal involved.
Note this is *not* reachable from an RN-style array transform: the
extractor returns arrays untouched and only parses strings, so
`style={{ transform: [{ rotate: "-90deg" }] }}` (the context window meter
and the checks ring) was never a factor.
Patch the three builds a consumer can resolve - Metro takes the package's
`react-native` field (src/*.ts), Node and Vitest take main/module - and
register react-native-svg with the postinstall patch runner so the patch
actually applies. The new test pins all three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDwfsMWoSFfPm2cFXe5noQ
* test(app): cover parsed root SVG transforms
Exercise the dependency patch with root props produced by the library's XML and CSS parsers, including embedded style rules that can arrive through project icons. Keep dependency-patch documentation focused on the reusable install and resolution constraints.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
const BUILDS = {
|
|
// Metro resolves src; Node consumers resolve one of the compiled builds.
|
|
src: "react-native-svg/src/lib/extract/extractTransform.ts",
|
|
esm: "react-native-svg/lib/module/lib/extract/extractTransform",
|
|
cjs: "react-native-svg/lib/commonjs/lib/extract/extractTransform.js",
|
|
} as const;
|
|
|
|
const CSS_MODULE = "react-native-svg/src/css/index.tsx";
|
|
const XML_MODULE = "react-native-svg/src/xml.tsx";
|
|
|
|
type Extractor = (props: { transform?: unknown }) => unknown;
|
|
type XmlMiddleware = (root: unknown) => unknown;
|
|
interface ParsedRoot {
|
|
props: Record<string, unknown>;
|
|
}
|
|
type XmlParser = (source: string, middleware?: XmlMiddleware) => ParsedRoot | null;
|
|
|
|
async function loadBuild(extractSpecifier: string) {
|
|
const [cssModule, extractModule, xmlModule] = (await Promise.all([
|
|
import(/* @vite-ignore */ CSS_MODULE),
|
|
import(/* @vite-ignore */ extractSpecifier),
|
|
import(/* @vite-ignore */ XML_MODULE),
|
|
])) as [
|
|
{ inlineStyles?: XmlMiddleware; default?: { inlineStyles?: XmlMiddleware } },
|
|
{
|
|
extractTransformSvgView?: Extractor;
|
|
default?: { extractTransformSvgView?: Extractor };
|
|
},
|
|
{ parse?: XmlParser; default?: { parse?: XmlParser } },
|
|
];
|
|
const inlineStyles = cssModule.inlineStyles ?? cssModule.default?.inlineStyles;
|
|
const extract =
|
|
extractModule.extractTransformSvgView ?? extractModule.default?.extractTransformSvgView;
|
|
const parseXml = xmlModule.parse ?? xmlModule.default?.parse;
|
|
if (!inlineStyles || !extract || !parseXml)
|
|
throw new Error(`could not load react-native-svg build`);
|
|
|
|
return (xml: string) => {
|
|
const root = parseXml(xml, inlineStyles);
|
|
if (!root) throw new Error(`could not parse root SVG`);
|
|
const props = { ...root.props };
|
|
const style = props.style;
|
|
if (style && typeof style === "object" && "transform" in style) {
|
|
props.transform = style.transform;
|
|
}
|
|
return extract(props);
|
|
};
|
|
}
|
|
|
|
describe.each(Object.entries(BUILDS))("root SVG transforms (%s build)", (_name, specifier) => {
|
|
it("still converts a real SVG transform string", async () => {
|
|
const extractFromXml = await loadBuild(specifier);
|
|
|
|
expect(extractFromXml(`<svg transform="rotate(-90)"></svg>`)).toEqual([{ rotate: "-90deg" }]);
|
|
});
|
|
|
|
it.each([
|
|
["an inline CSS keyword", `<svg style="transform:none"></svg>`],
|
|
["an embedded CSS rule", `<svg><style>svg { transform: none; }</style></svg>`],
|
|
["a CSS angle unit", `<svg transform="rotate(-90deg)"></svg>`],
|
|
["a CSS length unit", `<svg transform="translate(10px, 0)"></svg>`],
|
|
])("drops %s from parsed root SVG props", async (_label, xml) => {
|
|
const extractFromXml = await loadBuild(specifier);
|
|
|
|
expect(() => extractFromXml(xml)).not.toThrow();
|
|
expect(extractFromXml(xml)).toBeUndefined();
|
|
});
|
|
});
|