@@ -6,6 +6,19 @@ import { isWhiteSpace, isPunctCharCode, isMdAsciiPunct } from '../common/utils.m
66const QUOTE_TEST_RE = / [ ' " ] /
77const QUOTE_RE = / [ ' " ] / g
88const APOSTROPHE = '\u2019' /* ’ */
9+ // Caps memory on malicious input
10+ const MAX_OPENERS = 1000
11+
12+ function truncateStack ( stack , heads , length ) {
13+ while ( stack . length > length ) {
14+ const item = stack . pop ( )
15+ if ( item . isSingleQuote ) {
16+ heads . single = item . prevSameQuoteIdx
17+ } else {
18+ heads . double = item . prevSameQuoteIdx
19+ }
20+ }
21+ }
922
1023function addReplacement ( replacements , tokenIdx , pos , ch ) {
1124 if ( ! replacements [ tokenIdx ] ) {
@@ -35,6 +48,8 @@ function process_inlines (tokens, state) {
3548 let j
3649
3750 const stack = [ ]
51+ // stack indexes of topmost openers of each type (-1 if none)
52+ const heads = { single : - 1 , double : - 1 }
3853 // token index -> list of replacements in the original token content
3954 const replacements = { }
4055
@@ -46,7 +61,7 @@ function process_inlines (tokens, state) {
4661 for ( j = stack . length - 1 ; j >= 0 ; j -- ) {
4762 if ( stack [ j ] . level <= thisLevel ) { break }
4863 }
49- stack . length = j + 1
64+ truncateStack ( stack , heads , j + 1 )
5065
5166 if ( token . type !== 'text' ) { continue }
5267
@@ -150,39 +165,45 @@ function process_inlines (tokens, state) {
150165 }
151166
152167 if ( canClose ) {
153- // this could be a closing quote, rewind the stack to get a match
154- for ( j = stack . length - 1 ; j >= 0 ; j -- ) {
155- let item = stack [ j ]
156- if ( stack [ j ] . level < thisLevel ) { break }
157- if ( item . single === isSingle && stack [ j ] . level === thisLevel ) {
158- item = stack [ j ]
159-
160- let openQuote
161- let closeQuote
162- if ( isSingle ) {
163- openQuote = state . md . options . quotes [ 2 ]
164- closeQuote = state . md . options . quotes [ 3 ]
165- } else {
166- openQuote = state . md . options . quotes [ 0 ]
167- closeQuote = state . md . options . quotes [ 1 ]
168- }
169-
170- addReplacement ( replacements , i , t . index , closeQuote )
171- addReplacement ( replacements , item . token , item . pos , openQuote )
172-
173- stack . length = j
174- continue OUTER
168+ // Stack levels never decrease, so an opener of this type below
169+ // the current level means there is no match
170+ j = isSingle ? heads . single : heads . double
171+ if ( j >= 0 && stack [ j ] . level === thisLevel ) {
172+ const item = stack [ j ]
173+
174+ let openQuote
175+ let closeQuote
176+ if ( isSingle ) {
177+ openQuote = state . md . options . quotes [ 2 ]
178+ closeQuote = state . md . options . quotes [ 3 ]
179+ } else {
180+ openQuote = state . md . options . quotes [ 0 ]
181+ closeQuote = state . md . options . quotes [ 1 ]
175182 }
183+
184+ addReplacement ( replacements , i , t . index , closeQuote )
185+ addReplacement ( replacements , item . tokenIdx , item . contentPos , openQuote )
186+
187+ truncateStack ( stack , heads , j )
188+ continue OUTER
176189 }
177190 }
178191
179192 if ( canOpen ) {
193+ if ( stack . length >= MAX_OPENERS ) { return }
180194 stack . push ( {
181- token : i ,
182- pos : t . index ,
183- single : isSingle ,
184- level : thisLevel
195+ tokenIdx : i ,
196+ contentPos : t . index ,
197+ isSingleQuote : isSingle ,
198+ level : thisLevel ,
199+ // stack index of the previous opener of the same quote type, -1 if none
200+ prevSameQuoteIdx : isSingle ? heads . single : heads . double
185201 } )
202+ if ( isSingle ) {
203+ heads . single = stack . length - 1
204+ } else {
205+ heads . double = stack . length - 1
206+ }
186207 } else if ( canClose && isSingle ) {
187208 addReplacement ( replacements , i , t . index , APOSTROPHE )
188209 }
0 commit comments