Skip to main content

srcmap_codec/
decode.rs

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