Skip to content

Commit eb5d54e

Browse files
OskarEichlermikehardy
authored andcommitted
fix(app): parse data URL payloads completely
1 parent adcefc5 commit eb5d54e

2 files changed

Lines changed: 65 additions & 9 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
3+
import { Base64, getDataUrlParts } from '../lib/common';
4+
5+
describe('common utilities', () => {
6+
describe('getDataUrlParts', () => {
7+
it('preserves commas after the data URL delimiter', () => {
8+
expect(getDataUrlParts('data:text/plain,one,two')).toEqual({
9+
base64String: Base64.btoa('one,two'),
10+
mediaType: 'text/plain',
11+
});
12+
});
13+
14+
it('allows an empty data URL payload', () => {
15+
expect(getDataUrlParts('data:text/plain,')).toEqual({
16+
base64String: '',
17+
mediaType: 'text/plain',
18+
});
19+
});
20+
21+
it('encodes decoded text as UTF-8 bytes', () => {
22+
expect(getDataUrlParts('data:text/plain,%E2%9C%93')).toEqual({
23+
base64String: '4pyT',
24+
mediaType: 'text/plain',
25+
});
26+
});
27+
28+
it('only recognizes a base64 marker in the metadata suffix', () => {
29+
expect(getDataUrlParts('data:text/plain,value;base64')).toEqual({
30+
base64String: Base64.btoa('value;base64'),
31+
mediaType: 'text/plain',
32+
});
33+
});
34+
35+
it.each(['text/plain,value', 'data:text/plain', 'data:text/plain,%E0%A4%A'])(
36+
'rejects malformed data URL %s',
37+
value => {
38+
expect(getDataUrlParts(value)).toEqual({
39+
base64String: undefined,
40+
mediaType: undefined,
41+
});
42+
},
43+
);
44+
});
45+
});

‎packages/app/lib/common/index.ts‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,29 @@ export { default as ReferenceBase } from './ReferenceBase';
3030
export type { DataUrlParts, Observer };
3131

3232
export function getDataUrlParts(dataUrlString: string): DataUrlParts {
33-
const isBase64 = dataUrlString.includes(';base64');
34-
let [mediaType, base64String] = dataUrlString.split(',');
35-
if (!mediaType || !base64String) {
33+
const match = /^data:([^,]*?)(;base64)?,([\s\S]*)$/.exec(dataUrlString);
34+
if (!match) {
3635
return { base64String: undefined, mediaType: undefined };
3736
}
38-
mediaType = mediaType.replace('data:', '').replace(';base64', '');
39-
if (base64String && base64String.includes('%')) {
40-
base64String = decodeURIComponent(base64String);
41-
}
42-
if (!isBase64) {
43-
base64String = Base64.btoa(base64String);
37+
38+
const mediaType = match[1] || undefined;
39+
let base64String = match[3] ?? '';
40+
41+
try {
42+
if (base64String.includes('%')) {
43+
base64String = decodeURIComponent(base64String);
44+
}
45+
if (!match[2]) {
46+
const binaryString = encodeURIComponent(base64String).replace(
47+
/%([0-9A-F]{2})/g,
48+
(_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)),
49+
);
50+
base64String = Base64.btoa(binaryString);
51+
}
52+
} catch (_) {
53+
return { base64String: undefined, mediaType: undefined };
4454
}
55+
4556
return { base64String, mediaType };
4657
}
4758

0 commit comments

Comments
 (0)