Skip to main content

srcmap_codec/
decode.rs

1use crate::vlq::vlq_decode;
2use crate::{DecodeError, Line, Segment, SourceMapMappings};
3
4#[derive(Default)]
5struct DecodeState {
6    source_index: i64,
7    original_line: i64,
8    original_column: i64,
9    name_index: i64,
10}
11
12/// Decode a VLQ-encoded source map mappings string into structured data.
13///
14/// The mappings string uses `;` to separate lines and `,` to separate
15/// segments within a line. Each segment is a sequence of VLQ-encoded
16/// values representing column offsets, source indices, and name indices.
17///
18/// Values are relative (delta-encoded) within the mappings string:
19/// - Generated column resets to 0 for each new line
20/// - Source index, original line, original column, and name index
21///   are cumulative across the entire mappings string
22///
23/// # Errors
24///
25/// Returns [`DecodeError`] if the input contains invalid base64 characters,
26/// truncated VLQ sequences, or values that overflow i64.
27pub fn decode(input: &str) -> Result<SourceMapMappings, DecodeError> {
28    if input.is_empty() {
29        return Ok(Vec::new());
30    }
31
32    let bytes = input.as_bytes();
33    let len = bytes.len();
34
35    // Pre-count lines and segments for capacity hints. memchr's count is
36    // SIMD-accelerated; the equivalent scalar byte loop is not auto-vectorized
37    // and measured ~7x slower.
38    let (line_count, avg_segments_per_line) = decode_capacity_hints(bytes);
39    let mut mappings: SourceMapMappings = Vec::with_capacity(line_count);
40
41    let mut state = DecodeState::default();
42    let mut pos: usize = 0;
43
44    loop {
45        // Generated column resets per line
46        let mut generated_column: i64 = 0;
47        let mut line: Line = Vec::with_capacity(avg_segments_per_line);
48        let mut saw_semicolon = false;
49
50        while pos < len {
51            let byte = bytes[pos];
52
53            if byte == b';' {
54                pos += 1;
55                saw_semicolon = true;
56                break;
57            }
58
59            if byte == b',' {
60                pos += 1;
61                continue;
62            }
63
64            // Field 1: generated column (always present)
65            let (delta, consumed) = vlq_decode(bytes, pos)?;
66            generated_column += delta;
67            pos += consumed;
68
69            // Build segment with exact allocation (1, 4, or 5 fields)
70            let segment: Segment = if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
71                decode_sourced_segment(bytes, &mut pos, generated_column, &mut state)?
72            } else {
73                Segment::one(generated_column)
74            };
75
76            line.push(segment);
77        }
78
79        mappings.push(line);
80
81        if !saw_semicolon {
82            break;
83        }
84    }
85
86    Ok(mappings)
87}
88
89fn decode_capacity_hints(bytes: &[u8]) -> (usize, usize) {
90    let semicolons = memchr::memchr_iter(b';', bytes).count();
91    let commas = memchr::memchr_iter(b',', bytes).count();
92    let line_count = semicolons + 1;
93    let approx_segments = commas + line_count;
94    (line_count, approx_segments / line_count)
95}
96
97fn decode_sourced_segment(
98    bytes: &[u8],
99    pos: &mut usize,
100    generated_column: i64,
101    state: &mut DecodeState,
102) -> Result<Segment, DecodeError> {
103    let (delta, consumed) = vlq_decode(bytes, *pos)?;
104    state.source_index += delta;
105    *pos += consumed;
106
107    if *pos >= bytes.len() || bytes[*pos] == b',' || bytes[*pos] == b';' {
108        return Err(DecodeError::InvalidSegmentLength { fields: 2, offset: *pos });
109    }
110
111    let (delta, consumed) = vlq_decode(bytes, *pos)?;
112    state.original_line += delta;
113    *pos += consumed;
114
115    if *pos >= bytes.len() || bytes[*pos] == b',' || bytes[*pos] == b';' {
116        return Err(DecodeError::InvalidSegmentLength { fields: 3, offset: *pos });
117    }
118
119    let (delta, consumed) = vlq_decode(bytes, *pos)?;
120    state.original_column += delta;
121    *pos += consumed;
122
123    if *pos < bytes.len() && bytes[*pos] != b',' && bytes[*pos] != b';' {
124        let (delta, consumed) = vlq_decode(bytes, *pos)?;
125        state.name_index += delta;
126        *pos += consumed;
127        return Ok(Segment::five(
128            generated_column,
129            state.source_index,
130            state.original_line,
131            state.original_column,
132            state.name_index,
133        ));
134    }
135
136    Ok(Segment::four(
137        generated_column,
138        state.source_index,
139        state.original_line,
140        state.original_column,
141    ))
142}