Skip to main content

srcmap_codec/
encode.rs

1use crate::SourceMapMappings;
2use crate::vlq::vlq_encode;
3
4/// Encode decoded source map mappings back into a VLQ-encoded string.
5///
6/// This is the inverse of [`decode`](crate::decode). Values are delta-encoded:
7/// generated column resets per line, all other fields are cumulative.
8///
9/// Empty segments are silently skipped.
10pub fn encode(mappings: &SourceMapMappings) -> String {
11    if mappings.is_empty() {
12        return String::new();
13    }
14
15    // Estimate capacity: ~4 bytes per segment on average
16    let segment_count: usize = mappings.iter().map(|line| line.len()).sum();
17    let mut buf: Vec<u8> = Vec::with_capacity(segment_count * 4 + mappings.len());
18
19    // Cumulative state
20    let mut prev_source: i64 = 0;
21    let mut prev_original_line: i64 = 0;
22    let mut prev_original_column: i64 = 0;
23    let mut prev_name: i64 = 0;
24
25    for (line_idx, line) in mappings.iter().enumerate() {
26        if line_idx > 0 {
27            buf.push(b';');
28        }
29
30        // Generated column resets per line
31        let mut prev_generated_column: i64 = 0;
32        let mut wrote_segment = false;
33
34        for segment in line.iter() {
35            if segment.is_empty() {
36                continue;
37            }
38
39            if wrote_segment {
40                buf.push(b',');
41            }
42            wrote_segment = true;
43
44            // Field 1: generated column (delta from previous in this line)
45            vlq_encode(&mut buf, segment[0] - prev_generated_column);
46            prev_generated_column = segment[0];
47
48            if segment.len() >= 4 {
49                // Field 2: source index (cumulative delta)
50                vlq_encode(&mut buf, segment[1] - prev_source);
51                prev_source = segment[1];
52
53                // Field 3: original line (cumulative delta)
54                vlq_encode(&mut buf, segment[2] - prev_original_line);
55                prev_original_line = segment[2];
56
57                // Field 4: original column (cumulative delta)
58                vlq_encode(&mut buf, segment[3] - prev_original_column);
59                prev_original_column = segment[3];
60
61                if segment.len() >= 5 {
62                    // Field 5: name index (cumulative delta)
63                    vlq_encode(&mut buf, segment[4] - prev_name);
64                    prev_name = segment[4];
65                }
66            }
67        }
68    }
69
70    // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
71    // and we only add b';' and b',' — all valid UTF-8.
72    debug_assert!(buf.is_ascii());
73    unsafe { String::from_utf8_unchecked(buf) }
74}
75
76/// Encode a single line's segments to VLQ bytes.
77///
78/// Generated column resets per line (starts at 0).
79/// Cumulative state (source, original line/column, name) is passed in.
80#[cfg(feature = "parallel")]
81fn encode_line_to_bytes(
82    segments: &[crate::Segment],
83    init_source: i64,
84    init_original_line: i64,
85    init_original_column: i64,
86    init_name: i64,
87) -> Vec<u8> {
88    let mut buf = Vec::with_capacity(segments.len() * 6);
89    let mut prev_generated_column: i64 = 0;
90    let mut prev_source = init_source;
91    let mut prev_original_line = init_original_line;
92    let mut prev_original_column = init_original_column;
93    let mut prev_name = init_name;
94    let mut wrote_segment = false;
95
96    for segment in segments {
97        if segment.is_empty() {
98            continue;
99        }
100
101        if wrote_segment {
102            buf.push(b',');
103        }
104        wrote_segment = true;
105
106        vlq_encode(&mut buf, segment[0] - prev_generated_column);
107        prev_generated_column = segment[0];
108
109        if segment.len() >= 4 {
110            vlq_encode(&mut buf, segment[1] - prev_source);
111            prev_source = segment[1];
112
113            vlq_encode(&mut buf, segment[2] - prev_original_line);
114            prev_original_line = segment[2];
115
116            vlq_encode(&mut buf, segment[3] - prev_original_column);
117            prev_original_column = segment[3];
118
119            if segment.len() >= 5 {
120                vlq_encode(&mut buf, segment[4] - prev_name);
121                prev_name = segment[4];
122            }
123        }
124    }
125
126    buf
127}
128
129/// Encode source map mappings using parallel encoding with rayon.
130///
131/// Uses the same delta-encoding as [`encode`], but distributes line encoding
132/// across threads. Falls back to sequential [`encode`] for small maps.
133///
134/// Two-phase approach:
135/// 1. **Sequential scan** — compute cumulative state at each line boundary
136/// 2. **Parallel encode** — encode each line independently via rayon
137#[cfg(feature = "parallel")]
138pub fn encode_parallel(mappings: &SourceMapMappings) -> String {
139    use rayon::prelude::*;
140
141    if mappings.is_empty() {
142        return String::new();
143    }
144
145    let total_segments: usize = mappings.iter().map(|l| l.len()).sum();
146    if mappings.len() < 1024 || total_segments < 4096 {
147        return encode(mappings);
148    }
149
150    // Pass 1 (sequential): compute cumulative state at each line boundary
151    let mut states: Vec<(i64, i64, i64, i64)> = Vec::with_capacity(mappings.len());
152    let mut prev_source: i64 = 0;
153    let mut prev_original_line: i64 = 0;
154    let mut prev_original_column: i64 = 0;
155    let mut prev_name: i64 = 0;
156
157    for line in mappings.iter() {
158        states.push((
159            prev_source,
160            prev_original_line,
161            prev_original_column,
162            prev_name,
163        ));
164        for segment in line.iter() {
165            if segment.len() >= 4 {
166                prev_source = segment[1];
167                prev_original_line = segment[2];
168                prev_original_column = segment[3];
169                if segment.len() >= 5 {
170                    prev_name = segment[4];
171                }
172            }
173        }
174    }
175
176    // Pass 2 (parallel): encode each line independently
177    let encoded_lines: Vec<Vec<u8>> = mappings
178        .par_iter()
179        .zip(states.par_iter())
180        .map(|(line, &(src, ol, oc, name))| encode_line_to_bytes(line, src, ol, oc, name))
181        .collect();
182
183    // Join with semicolons
184    let total_len: usize =
185        encoded_lines.iter().map(|l| l.len()).sum::<usize>() + encoded_lines.len() - 1;
186    let mut buf: Vec<u8> = Vec::with_capacity(total_len);
187    for (i, line_bytes) in encoded_lines.iter().enumerate() {
188        if i > 0 {
189            buf.push(b';');
190        }
191        buf.extend_from_slice(line_bytes);
192    }
193
194    // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
195    // and we only add b';' — all valid UTF-8.
196    debug_assert!(buf.is_ascii());
197    unsafe { String::from_utf8_unchecked(buf) }
198}