Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,18 @@ const config = {
*
* @param {string} data
* @param {string=} from
* @param {number=} maxEntityCount Maximum number of XML entities the parser is
* allowed to expand before throwing. When omitted, the underlying parser
* falls back to its own default (512 in recent versions).
* @returns {import('./types.js').XastRoot}
*/
export const parseSvg = (data, from) => {
const sax = SAX.parser(config.strict, config);
export const parseSvg = (data, from, maxEntityCount) => {
// Build a fresh options object per call: sax mutates the options it receives
// (e.g. it stamps a default `maxEntityCount`), so the shared `config` must
// not be passed directly.
const parserConfig =
maxEntityCount == null ? { ...config } : { ...config, maxEntityCount };
const sax = SAX.parser(parserConfig.strict, parserConfig);
/** @type {import('./types.js').XastRoot} */
const root = { type: 'root', children: [] };
/** @type {import('./types.js').XastParent} */
Expand Down
41 changes: 40 additions & 1 deletion lib/parser.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from 'vitest';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import SAX from 'sax';
import { parseSvg } from './parser.js';
import { stringifySvg } from './stringifier.js';

Expand Down Expand Up @@ -31,3 +32,41 @@ test('a text preserved', () => {

expect(actual).toBe(expected);
});

describe('maxEntityCount', () => {
// sax mutates the options object it receives (it stamps a default
// `maxEntityCount`), so snapshot the options at call time, before sax runs.
/** @type {any} */
let receivedOpt;

beforeEach(() => {
vi.spyOn(SAX, 'parser').mockImplementation((_strict, opt) => {
receivedOpt = { ...opt };
// Minimal stub: parseSvg only assigns handlers then calls write().close().
return /** @type {any} */ ({
ENTITIES: {},
write() {
return this;
},
close() {
return this;
},
});
});
});

afterEach(() => {
vi.restoreAllMocks();
receivedOpt = undefined;
});

test('forwards a provided maxEntityCount to the parser options', () => {
parseSvg('<svg xmlns="http://www.w3.org/2000/svg"/>', undefined, 1234);
expect(receivedOpt.maxEntityCount).toBe(1234);
});

test('does not set maxEntityCount when it is omitted', () => {
parseSvg('<svg xmlns="http://www.w3.org/2000/svg"/>');
expect(receivedOpt.maxEntityCount).toBeUndefined();
});
});
2 changes: 1 addition & 1 deletion lib/svgo.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const optimize = (input, config) => {
}
for (let i = 0; i < maxPassCount; i += 1) {
info.multipassCount = i;
const ast = parseSvg(input, config.path);
const ast = parseSvg(input, config.path, config.maxEntityCount);
const plugins = config.plugins || ['preset-default'];
if (!Array.isArray(plugins)) {
throw Error(
Expand Down
17 changes: 17 additions & 0 deletions lib/svgo/coa.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export default function makeProgram(program) {
'-p, --precision <INTEGER>',
'Set number of digits in the fractional part, overrides plugins params',
)
.option(
'--max-entity-count <INTEGER>',
'Maximum number of XML entities the parser may expand before throwing',
)
.option(
'--config <CONFIG>',
'Custom config file, only .js, .mjs, and .cjs is supported',
Expand Down Expand Up @@ -189,6 +193,19 @@ async function action(args, opts, command) {
}
}

// --max-entity-count
if (opts.maxEntityCount != null) {
const number = Number.parseInt(opts.maxEntityCount, 10);
if (Number.isNaN(number)) {
console.error(
"error: option '--max-entity-count' argument must be an integer number",
);
process.exit(1);
} else {
config.maxEntityCount = number;
}
}

// --multipass
if (opts.multipass) {
config.multipass = true;
Expand Down
6 changes: 6 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ export type Config = {
* supports this param.
*/
floatPrecision?: number;
/**
* Maximum number of XML entities the parser is allowed to expand before
* throwing. When omitted, the underlying parser falls back to its own
* default (512 in recent versions).
*/
maxEntityCount?: number;
/**
* Plugins configuration. By default SVGO uses `preset-default`, but may
* contain builtin or custom plugins.
Expand Down