Skip to main content

srcmap_codec/
encode.rs

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