Skip to content

Commit 00c53ed

Browse files
committed
Speed up regex against ddos attacks.
1 parent 4c96ac2 commit 00c53ed

4 files changed

Lines changed: 130 additions & 17 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
buildMdxFenceDelimiterEdits,
4+
type MdxFence,
5+
maskMdxFencedRegions,
6+
parseInlineMdxFenceCloser,
7+
parseInlineMdxFenceOpener,
8+
restoreMdxFencedRegions,
9+
} from './fences'
10+
import { applySourceEdits, SourceText } from './source'
11+
12+
describe('inline fence delimiters', () => {
13+
it.each(['```', '````', '~~~', '~~~~~~'])(
14+
'preserves %s marker length, fence info, content, and repeated-format stability',
15+
(fence) => {
16+
const source = `before \t${fence}mdx title="Example"\n <Panel />\nafter \t${fence}\t `
17+
const expected = `before\n${fence}mdx title="Example"\n <Panel />\nafter\n${fence}`
18+
const edits = buildMdxFenceDelimiterEdits(new SourceText(source), [])
19+
expect(applySourceEdits(source, edits)).toBe(expected)
20+
expect(buildMdxFenceDelimiterEdits(new SourceText(expected), [])).toEqual(
21+
[],
22+
)
23+
const { fences, placeholderContent } = maskMdxFencedRegions(source)
24+
expect(restoreMdxFencedRegions(placeholderContent, fences)).toBe(expected)
25+
},
26+
)
27+
28+
it('uses the first inline opener and preserves its complete suffix', () => {
29+
expect(parseInlineMdxFenceOpener('before ~~ ````mdx ~~~')).toEqual({
30+
marker: '`',
31+
length: 4,
32+
suffix: 'mdx ~~~',
33+
})
34+
expect(parseInlineMdxFenceOpener(' \t```mdx')).toBeUndefined()
35+
expect(parseInlineMdxFenceOpener('before `` ~ around')).toBeUndefined()
36+
})
37+
38+
it('accepts longer closing fences on either side and rejects shorter or different markers', () => {
39+
const active: MdxFence = { marker: '`', length: 4 }
40+
expect(parseInlineMdxFenceCloser('before \t`````\t ', active)).toEqual({
41+
marker: '`',
42+
length: 5,
43+
suffix: '',
44+
})
45+
expect(parseInlineMdxFenceCloser('\t`````<Panel/>', active)).toEqual({
46+
marker: '`',
47+
length: 5,
48+
suffix: '',
49+
})
50+
for (const line of [
51+
'before ```',
52+
'before ~~~~',
53+
'before ```` x',
54+
'before ````\u00a0',
55+
]) {
56+
expect(parseInlineMdxFenceCloser(line, active)).toBeUndefined()
57+
}
58+
})
59+
60+
it('preserves CRLF and honors excluded ranges', () => {
61+
const source = 'before ```mdx\r\n<Panel /> ```\r\n'
62+
const document = new SourceText(source)
63+
const edits = buildMdxFenceDelimiterEdits(document, [])
64+
expect(applySourceEdits(source, edits)).toBe(
65+
'before\r\n```mdx\r\n<Panel />\r\n```\r\n',
66+
)
67+
expect(
68+
buildMdxFenceDelimiterEdits(document, [{ start: 0, end: source.length }]),
69+
).toEqual([])
70+
})
71+
72+
it('handles long nonmatching and matching whitespace runs before openers and closers', () => {
73+
const whitespace = '\t'.repeat(100_000)
74+
const source = `a${whitespace}\nbefore${whitespace}~~~mdx\na${whitespace}x\nafter${whitespace}~~~\t `
75+
const expected = `a${whitespace}\nbefore\n~~~mdx\na${whitespace}x\nafter\n~~~`
76+
expect(
77+
applySourceEdits(
78+
source,
79+
buildMdxFenceDelimiterEdits(new SourceText(source), []),
80+
),
81+
).toBe(expected)
82+
})
83+
})

‎packages/ts-format/src/fences.ts‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -324,15 +324,15 @@ function splitInlineMdxFenceDelimiters(source: string) {
324324
function splitLineBeforeInlineMdxFenceOpener(line: string) {
325325
if (parseMdxFenceDelimiter(line.trim())) return undefined
326326

327-
const match = /^(.+?)[\t ]*((?:`{3,}|~{3,}).*)$/.exec(line)
327+
const match = /`{3}|~{3}/.exec(line)
328328
if (!match) return undefined
329329

330-
const before = match[1].trimEnd()
330+
const before = line.slice(0, match.index).trimEnd()
331331
if (before.trim().length === 0) return undefined
332332

333333
return {
334334
before,
335-
fenceLine: match[2],
335+
fenceLine: line.slice(match.index),
336336
}
337337
}
338338

@@ -361,21 +361,18 @@ function splitLineBeforeInlineMdxFenceCloser(
361361
line: string,
362362
activeFence: MdxFence,
363363
) {
364-
const pattern = new RegExp(
365-
`^(.*?)[\\t ]*(${escapeRegExp(activeFence.marker.repeat(activeFence.length))}${escapeRegExp(activeFence.marker)}*)[\\t ]*$`,
366-
)
367-
const match = pattern.exec(line)
368-
if (!match || match[1].trim().length === 0) return undefined
364+
let end = line.length
365+
while (end > 0 && (line[end - 1] === '\t' || line[end - 1] === ' ')) end--
366+
let start = end
367+
while (start > 0 && line[start - 1] === activeFence.marker) start--
368+
if (end - start < activeFence.length) return undefined
369369

370-
const closingDelimiter = parseMdxFenceDelimiter(match[2].trim())
371-
if (!closingDelimiter) return undefined
372-
if (!isMdxFenceClosingDelimiter(closingDelimiter, activeFence)) {
373-
return undefined
374-
}
370+
const before = line.slice(0, start).trimEnd()
371+
if (before.trim().length === 0) return undefined
375372

376373
return {
377-
before: match[1].trimEnd(),
378-
fenceLine: match[2],
374+
before,
375+
fenceLine: line.slice(start, end),
379376
}
380377
}
381378

‎packages/ts-format/src/html.test.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { describe, expect, it } from 'vitest'
2-
import { normalizeClosingTagSpacing } from './html'
2+
import {
3+
normalizeClosingTagSpacing,
4+
normalizeSelfClosingTagSpacing,
5+
} from './html'
36
import { formatSource } from './index'
47

58
describe('closing tag formatting', () => {
@@ -46,3 +49,30 @@ describe('closing tag formatting', () => {
4649
)
4750
})
4851
})
52+
53+
describe('self-closing tag spacing', () => {
54+
it.each([' ', '\t', '\n', '\r\n', '\u00a0', '\u2028', '\u2029', '\ufeff'])(
55+
'removes whitespace before /> and preserves other whitespace (%j)',
56+
(whitespace) => {
57+
const source = `before${whitespace}<Panel${whitespace}/>${whitespace}after`
58+
expect(normalizeSelfClosingTagSpacing(source)).toBe(
59+
`before${whitespace}<Panel/>${whitespace}after`,
60+
)
61+
},
62+
)
63+
64+
it('normalizes multiple tags and mixed whitespace without consuming a bare slash', () => {
65+
const source = '<Panel \t\r\n/><img/><Icon /> / > <Other \t/>'
66+
const formatted = '<Panel/><img/><Icon/> / > <Other/>'
67+
expect(normalizeSelfClosingTagSpacing(source)).toBe(formatted)
68+
expect(normalizeSelfClosingTagSpacing(formatted)).toBe(formatted)
69+
})
70+
71+
it('handles long whitespace runs with and without a following self-closing delimiter', () => {
72+
const whitespace = ' \t'.repeat(50_000)
73+
const source = `${whitespace}text${whitespace}/x${whitespace}/>`
74+
expect(normalizeSelfClosingTagSpacing(source)).toBe(
75+
`${whitespace}text${whitespace}/x/>`,
76+
)
77+
})
78+
})

‎packages/ts-format/src/html.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,8 @@ export function getLineIndent(line: string) {
5656
}
5757

5858
export function normalizeSelfClosingTagSpacing(formatted: string) {
59-
return formatted.replace(/\s+\/>/g, '/>')
59+
// Match each whitespace run once, even when no /> follows it.
60+
return formatted.replace(/\s+/g, (whitespace, offset: number) =>
61+
formatted.startsWith('/>', offset + whitespace.length) ? '' : whitespace,
62+
)
6063
}

0 commit comments

Comments
 (0)