@@ -154,8 +154,9 @@ function indexesOf(array, item) {
154154 return indexes ;
155155}
156156function uniqs ( ) {
157- var list = Array . prototype . concat . apply ( [ ] , arguments ) ;
158- return list . filter ( function ( item , i ) { return i === list . indexOf ( item ) ; } ) ;
157+ // A Set keeps the first-occurrence order the previous filter/indexOf pass
158+ // produced, without its quadratic cost on long index lists.
159+ return Array . from ( new Set ( Array . prototype . concat . apply ( [ ] , arguments ) ) ) ;
159160}
160161var Parser = /** @class */ ( function ( ) {
161162 function Parser ( rule , options ) {
@@ -200,6 +201,11 @@ var Parser = /** @class */ (function () {
200201 attr . push ( this . currToken ) ;
201202 this . position ++ ;
202203 }
204+ if ( ! this . currToken ) {
205+ // Ran off the end of the token stream: the attribute was never closed.
206+ // Point at the opening bracket, which is where the author needs to look.
207+ return this . expected ( "closing square bracket" , startingToken [ tokenize_1 . FIELDS . START_POS ] ) ;
208+ }
203209 if ( this . currToken [ tokenize_1 . FIELDS . TYPE ] !== tokens . closeSquare ) {
204210 return this . expected ( "closing square bracket" , this . currToken [ tokenize_1 . FIELDS . START_POS ] ) ;
205211 }
@@ -260,7 +266,7 @@ var Parser = /** @class */ (function () {
260266 }
261267 if ( commentBefore ) {
262268 ( 0 , util_1 . ensureObject ) ( node , "raws" , "spaces" , "attribute" ) ;
263- node . raws . spaces . attribute . before = spaceBefore ;
269+ node . raws . spaces . attribute . before = commentBefore ;
264270 commentBefore = "" ;
265271 }
266272 node . namespace = ( node . namespace || "" ) + content ;
@@ -686,7 +692,21 @@ var Parser = /** @class */ (function () {
686692 return this . error ( "Unexpected '|'." , this . currToken [ tokenize_1 . FIELDS . START_POS ] ) ;
687693 } ;
688694 Parser . prototype . namespace = function ( ) {
689- var before = ( this . prevToken && this . content ( this . prevToken ) ) || true ;
695+ var prev = this . prevToken ;
696+ // Only treat the previous token as a namespace prefix when it can actually
697+ // be one (a type/word or the universal `*`). A comment, comma, combinator
698+ // or whitespace before `|` means an empty namespace, not a prefix.
699+ var before = prev &&
700+ ( prev [ tokenize_1 . FIELDS . TYPE ] === tokens . word ||
701+ prev [ tokenize_1 . FIELDS . TYPE ] === tokens . asterisk ||
702+ prev [ tokenize_1 . FIELDS . TYPE ] === tokens . ampersand )
703+ ? this . content ( prev )
704+ : true ;
705+ if ( ! this . nextToken ) {
706+ // A trailing `|` with nothing after it. `unexpectedPipe` reports against
707+ // `currToken`, which is the pipe itself and always present here.
708+ return this . unexpectedPipe ( ) ;
709+ }
690710 if ( this . nextToken [ tokenize_1 . FIELDS . TYPE ] === tokens . word ) {
691711 this . position ++ ;
692712 return this . word ( before ) ;
@@ -716,6 +736,7 @@ var Parser = /** @class */ (function () {
716736 Parser . prototype . parentheses = function ( ) {
717737 var last = this . current . last ;
718738 var unbalanced = 1 ;
739+ var openingToken = this . currToken ;
719740 this . position ++ ;
720741 if ( last && last . type === types . PSEUDO ) {
721742 var selector = new selector_1 . default ( {
@@ -785,7 +806,9 @@ var Parser = /** @class */ (function () {
785806 }
786807 }
787808 if ( unbalanced ) {
788- return this . expected ( "closing parenthesis" , this . currToken [ tokenize_1 . FIELDS . START_POS ] ) ;
809+ // `currToken` is undefined when the token stream ran out before the
810+ // parenthesis was closed; fall back to the opening one.
811+ return this . expected ( "closing parenthesis" , ( this . currToken || openingToken ) [ tokenize_1 . FIELDS . START_POS ] ) ;
789812 }
790813 } ;
791814 Parser . prototype . pseudo = function ( ) {
@@ -890,8 +913,14 @@ var Parser = /** @class */ (function () {
890913 // Eliminate Sass interpolations from the list of id indexes
891914 var interpolations = indexesOf ( word , "#{" ) ;
892915 if ( interpolations . length ) {
893- hasId = hasId . filter ( function ( hashIndex ) { return ! ~ interpolations . indexOf ( hashIndex ) ; } ) ;
916+ var interpolationIndexes_1 = new Set ( interpolations ) ;
917+ hasId = hasId . filter ( function ( hashIndex ) { return ! interpolationIndexes_1 . has ( hashIndex ) ; } ) ;
894918 }
919+ // Membership tests are built once. Scanning the arrays per index would make
920+ // a flat selector such as `.a.a.a...` quadratic in its number of class/id
921+ // indexes, which is enough to pin a CPU core on attacker-supplied input.
922+ var classIndexes = new Set ( hasClass ) ;
923+ var idIndexes = new Set ( hasId ) ;
895924 var indices = ( 0 , sortAscending_1 . default ) ( uniqs ( __spreadArray ( __spreadArray ( [ 0 ] , __read ( hasClass ) , false ) , __read ( hasId ) , false ) ) ) ;
896925 indices . forEach ( function ( ind , i ) {
897926 var index = indices [ i + 1 ] || word . length ;
@@ -903,15 +932,15 @@ var Parser = /** @class */ (function () {
903932 var current = _this . currToken ;
904933 var sourceIndex = current [ tokenize_1 . FIELDS . START_POS ] + indices [ i ] ;
905934 var source = getSource ( current [ 1 ] , current [ 2 ] + ind , current [ 3 ] , current [ 2 ] + ( index - 1 ) ) ;
906- if ( ~ hasClass . indexOf ( ind ) ) {
935+ if ( classIndexes . has ( ind ) ) {
907936 var classNameOpts = {
908937 value : value . slice ( 1 ) ,
909938 source : source ,
910939 sourceIndex : sourceIndex ,
911940 } ;
912941 node = new className_1 . default ( unescapeProp ( classNameOpts , "value" ) ) ;
913942 }
914- else if ( ~ hasId . indexOf ( ind ) ) {
943+ else if ( idIndexes . has ( ind ) ) {
915944 var idOpts = {
916945 value : value . slice ( 1 ) ,
917946 source : source ,
0 commit comments