@@ -231,8 +231,35 @@ fn decode_mapping(mapping: &str, names_len: usize, sources_len: usize) -> Result
231231#[ repr( align( 64 ) ) ]
232232struct Aligned64 ( [ i8 ; 256 ] ) ;
233233
234- #[ rustfmt:: skip]
235- static B64 : Aligned64 = Aligned64 ( [ -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , 62 , -1 , -1 , -1 , 63 , 52 , 53 , 54 , 55 , 56 , 57 , 58 , 59 , 60 , 61 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , -1 , -1 , -1 , -1 , -1 , -1 , 26 , 27 , 28 , 29 , 30 , 31 , 32 , 33 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 , 42 , 43 , 44 , 45 , 46 , 47 , 48 , 49 , 50 , 51 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 , -1 ] ) ;
234+ /// VLQ decode table. Entry semantics:
235+ /// * `-1` — segment (`,`) or line (`;`) delimiter, terminates a value scan.
236+ /// * `0..=63` — base64 payload; bit 5 (value >= 32) is the VLQ continuation flag.
237+ ///
238+ /// Bytes outside the base64 alphabet decode as `63`, i.e. exactly like `'/'`
239+ /// (payload 31 with the continuation bit set). This preserves the historical
240+ /// lenient behavior: invalid characters are consumed as continuation bytes and
241+ /// surface as `VlqLeftover`/`VlqOverflow` or garbage values, never a panic.
242+ static B64_DECODE : Aligned64 = build_b64_decode ( ) ;
243+
244+ const fn build_b64_decode ( ) -> Aligned64 {
245+ let mut table = [ 63i8 ; 256 ] ;
246+ let mut i = 0 ;
247+ while i < 26 {
248+ table[ ( b'A' + i) as usize ] = i as i8 ;
249+ table[ ( b'a' + i) as usize ] = 26 + i as i8 ;
250+ i += 1 ;
251+ }
252+ let mut i = 0 ;
253+ while i < 10 {
254+ table[ ( b'0' + i) as usize ] = 52 + i as i8 ;
255+ i += 1 ;
256+ }
257+ table[ b'+' as usize ] = 62 ;
258+ table[ b'/' as usize ] = 63 ;
259+ table[ b',' as usize ] = -1 ;
260+ table[ b';' as usize ] = -1 ;
261+ Aligned64 ( table)
262+ }
236263
237264/// Pick a `Vec<Token>` capacity for `decode_mapping`.
238265///
@@ -266,59 +293,75 @@ fn estimate_token_capacity(mapping: &[u8]) -> usize {
266293///
267294/// Returns the number of decoded values in the segment. Values are written into
268295/// `rv` for the first 5 fields (the maximum valid segment size). If the segment
269- /// contains more than 5 fields, we keep counting in `rv_len` so caller can reject
270- /// it with `BadSegmentSize`.
296+ /// contains more than 5 fields, we keep parsing and counting so the caller can
297+ /// reject it with `BadSegmentSize` carrying the true field count .
271298fn parse_vlq_segment_into ( mapping : & [ u8 ] , cursor : & mut usize , rv : & mut [ i64 ; 5 ] ) -> Result < usize > {
272- let mut cur = 0i64 ;
273- let mut shift = 0u32 ;
274299 let mut rv_len = 0usize ;
275-
276- while * cursor < mapping. len ( ) {
277- let c = mapping[ * cursor] ;
278- if c == b',' || c == b';' {
279- break ;
300+ while let Some ( value) = next_vlq ( mapping, cursor) ? {
301+ if rv_len < rv. len ( ) {
302+ rv[ rv_len] = value;
280303 }
304+ rv_len += 1 ;
305+ }
306+ if rv_len == 0 {
307+ return Err ( Error :: VlqNoValues ) ;
308+ }
309+ Ok ( rv_len)
310+ }
281311
282- // SAFETY: B64 is a 256-element lookup table, and c is a u8 (0-255)
283- let enc = unsafe { i64:: from ( * B64 . 0 . get_unchecked ( c as usize ) ) } ;
284- let val = enc & 0b11111 ;
285- let cont = enc >> 5 ;
312+ /// Decode one VLQ value starting at `*cursor`.
313+ ///
314+ /// Returns `Ok(None)` without consuming anything when positioned at a
315+ /// delimiter (`,` / `;`) or end of input; otherwise consumes the value's bytes.
316+ #[ inline( always) ]
317+ fn next_vlq ( mapping : & [ u8 ] , cursor : & mut usize ) -> Result < Option < i64 > > {
318+ let Some ( & byte) = mapping. get ( * cursor) else { return Ok ( None ) } ;
319+ let first = i64:: from ( B64_DECODE . 0 [ byte as usize ] ) ;
320+ if first < 0 {
321+ return Ok ( None ) ;
322+ }
323+ * cursor += 1 ;
324+ if first < 32 {
325+ // No continuation bit: a single-byte value, the dominant case in real
326+ // mappings (small line/column deltas).
327+ return Ok ( Some ( decode_sign ( first) ) ) ;
328+ }
286329
330+ let mut cur = first & 0b11111 ;
331+ let mut shift = 5u32 ;
332+ loop {
333+ let Some ( & byte) = mapping. get ( * cursor) else {
334+ // Input ended while a continuation was pending.
335+ return Err ( Error :: VlqLeftover ) ;
336+ } ;
337+ let enc = i64:: from ( B64_DECODE . 0 [ byte as usize ] ) ;
338+ if enc < 0 {
339+ // Delimiter while a continuation was pending.
340+ return Err ( Error :: VlqLeftover ) ;
341+ }
342+ * cursor += 1 ;
287343 // VLQ shift grows by 5 bits per continuation byte. Bail out before
288- // `val << shift` could overflow i64: the largest safe shift for a 5-bit
289- // value is 62, and `shift` only ever takes multiples of 5, so the first
290- // offending shift is 65.
344+ // `payload << shift` could overflow i64: the largest safe shift for a
345+ // 5-bit value is 62, and `shift` only ever takes multiples of 5, so
346+ // the first offending shift is 65.
291347 if shift > 62 {
292348 return Err ( Error :: VlqOverflow ) ;
293349 }
294- cur = cur. wrapping_add ( val << shift) ;
295-
296- * cursor += 1 ;
350+ cur |= ( enc & 0b11111 ) << shift;
297351 shift += 5 ;
298-
299- if cont == 0 {
300- // VLQ stores sign in low bit; remaining bits are the magnitude.
301- let sign = cur & 1 ;
302- cur >>= 1 ;
303- if sign != 0 {
304- cur = -cur;
305- }
306- if rv_len < rv. len ( ) {
307- rv[ rv_len] = cur;
308- }
309- rv_len += 1 ;
310- cur = 0 ;
311- shift = 0 ;
352+ if enc < 32 {
353+ return Ok ( Some ( decode_sign ( cur) ) ) ;
312354 }
313355 }
356+ }
314357
315- if cur != 0 || shift != 0 {
316- Err ( Error :: VlqLeftover )
317- } else if rv_len == 0 {
318- Err ( Error :: VlqNoValues )
319- } else {
320- Ok ( rv_len )
321- }
358+ /// VLQ stores the sign in the low bit; remaining bits are the magnitude.
359+ /// Branchless sign-magnitude decode: `-0` collapses to `0`.
360+ # [ inline ( always ) ]
361+ fn decode_sign ( cur : i64 ) -> i64 {
362+ let mag = cur >> 1 ;
363+ let sign = cur & 1 ;
364+ ( mag ^ -sign ) + sign
322365}
323366
324367#[ cfg( test) ]
@@ -473,6 +516,67 @@ mod tests {
473516 assert ! ( matches!( SourceMap :: from_json( bad_mapping) , Err ( Error :: BadSourceReference ( _) ) ) ) ;
474517 }
475518
519+ #[ test]
520+ fn decode_invalid_char_behaves_like_slash ( ) {
521+ // Bytes outside the base64 alphabet decode as payload 31 with the
522+ // continuation bit set — exactly like '/'. A map using '!' must
523+ // therefore produce the same tokens as the same map using '/'.
524+ let make = |mappings : & str | {
525+ format ! ( r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"# )
526+ } ;
527+ let bang_json = make ( "gD,!C" ) ;
528+ let slash_json = make ( "gD,/C" ) ;
529+ let bang = SourceMap :: from_json_string ( & bang_json) . unwrap ( ) ;
530+ let slash = SourceMap :: from_json_string ( & slash_json) . unwrap ( ) ;
531+ let tokens: Vec < Token > = bang. get_tokens ( ) . collect ( ) ;
532+ assert_eq ! ( tokens, slash. get_tokens( ) . collect:: <Vec <Token >>( ) ) ;
533+ assert_eq ! ( tokens[ 0 ] . get_dst_col( ) , 48 ) ;
534+ assert_eq ! ( tokens[ 1 ] . get_dst_col( ) , 1 ) ;
535+ }
536+
537+ #[ test]
538+ fn decode_dangling_continuation_before_delimiter_is_leftover ( ) {
539+ // 13 continuation bytes stay under the shift-overflow bound, so ending
540+ // the segment there must surface VlqLeftover, not VlqOverflow.
541+ let mappings = format ! ( "{},A" , "g" . repeat( 13 ) ) ;
542+ let input = format ! ( r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"# ) ;
543+ let err = SourceMap :: from_json_string ( & input) . unwrap_err ( ) ;
544+ assert ! ( matches!( err, Error :: VlqLeftover ) ) ;
545+ }
546+
547+ #[ test]
548+ fn decode_oversized_segments_report_exact_field_count ( ) {
549+ for ( mappings, expected) in [ ( "AAAAAA" , 6u32 ) , ( "AAAAAAA" , 7u32 ) ] {
550+ let input =
551+ format ! ( r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"# ) ;
552+ let err = SourceMap :: from_json_string ( & input) . unwrap_err ( ) ;
553+ assert ! ( matches!( err, Error :: BadSegmentSize ( n) if n == expected) ) ;
554+ }
555+ }
556+
557+ #[ test]
558+ fn decode_parse_error_beats_segment_validation ( ) {
559+ // The whole segment is VLQ-parsed before any field validation, so a
560+ // dangling continuation reports VlqLeftover even when the decoded
561+ // fields would also fail the column check ("Dg": delta -1) or the
562+ // segment-size check ("AAAAAg": 6 fields).
563+ for mappings in [ "Dg" , "AAAAAg" ] {
564+ let input =
565+ format ! ( r#"{{"version":3,"names":[],"sources":[],"mappings":"{mappings}"}}"# ) ;
566+ let err = SourceMap :: from_json_string ( & input) . unwrap_err ( ) ;
567+ assert ! ( matches!( err, Error :: VlqLeftover ) , "mappings {mappings:?}: {err:?}" ) ;
568+ }
569+ }
570+
571+ #[ test]
572+ fn decode_negative_column_beats_size_check ( ) {
573+ // "DA" decodes to two fields with a negative generated column; the
574+ // column check runs before the field-count check.
575+ let input = r#"{"version":3,"names":[],"sources":[],"mappings":"DA"}"# ;
576+ let err = SourceMap :: from_json_string ( input) . unwrap_err ( ) ;
577+ assert ! ( matches!( err, Error :: BadSegmentSize ( 0 ) ) ) ;
578+ }
579+
476580 #[ test]
477581 fn parse_vlq_segment_empty_is_no_values ( ) {
478582 // Directly exercise the defensive `VlqNoValues` branch: with the cursor
0 commit comments