Skip to main content

srcmap_codec/
lib.rs

1//! High-performance VLQ source map codec.
2//!
3//! Encodes and decodes source map mappings using the Base64 VLQ format
4//! as specified in the Source Map v3 specification (ECMA-426).
5//!
6//! # Features
7//!
8//! - **`parallel`** — enables [`encode_parallel`] for multi-threaded encoding via rayon.
9//!   ~1.5x faster for large maps (5K+ lines).
10//!
11//! # Examples
12//!
13//! Decode and re-encode a mappings string:
14//!
15//! ```
16//! use srcmap_codec::{decode, encode};
17//!
18//! let mappings = decode("AAAA;AACA,EAAE").unwrap();
19//! assert_eq!(mappings.len(), 2); // 2 lines
20//! assert_eq!(mappings[0][0], vec![0, 0, 0, 0]); // first segment
21//!
22//! let encoded = encode(&mappings);
23//! assert_eq!(encoded, "AAAA;AACA,EAAE");
24//! ```
25//!
26//! Low-level VLQ primitives:
27//!
28//! ```
29//! use srcmap_codec::{vlq_decode, vlq_encode};
30//!
31//! let mut buf = Vec::new();
32//! vlq_encode(&mut buf, 42);
33//!
34//! let (value, bytes_read) = vlq_decode(&buf, 0).unwrap();
35//! assert_eq!(value, 42);
36//! ```
37
38mod decode;
39mod encode;
40mod vlq;
41
42pub use decode::decode;
43pub use encode::encode;
44#[cfg(feature = "parallel")]
45pub use encode::encode_parallel;
46pub use vlq::{vlq_decode, vlq_decode_unsigned, vlq_encode, vlq_encode_unsigned};
47
48use std::fmt;
49
50/// A single source map segment.
51///
52/// Segments have 1, 4, or 5 fields:
53/// - 1 field:  `[generated_column]`
54/// - 4 fields: `[generated_column, source_index, original_line, original_column]`
55/// - 5 fields: `[generated_column, source_index, original_line, original_column, name_index]`
56pub type Segment = Vec<i64>;
57
58/// A source map line is a list of segments.
59pub type Line = Vec<Segment>;
60
61/// Decoded source map mappings: a list of lines, each containing segments.
62pub type SourceMapMappings = Vec<Line>;
63
64/// Errors that can occur when decoding a VLQ-encoded mappings string.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum DecodeError {
67    /// A byte that is not a valid base64 character was encountered.
68    InvalidBase64 { byte: u8, offset: usize },
69    /// Input ended in the middle of a VLQ sequence (continuation bit was set).
70    UnexpectedEof { offset: usize },
71    /// A VLQ value exceeded the maximum representable range.
72    VlqOverflow { offset: usize },
73}
74
75impl fmt::Display for DecodeError {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            Self::InvalidBase64 { byte, offset } => {
79                write!(
80                    f,
81                    "invalid base64 character 0x{byte:02x} at offset {offset}"
82                )
83            }
84            Self::UnexpectedEof { offset } => {
85                write!(f, "unexpected end of input at offset {offset}")
86            }
87            Self::VlqOverflow { offset } => {
88                write!(f, "VLQ value overflow at offset {offset}")
89            }
90        }
91    }
92}
93
94impl std::error::Error for DecodeError {}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    // --- Roundtrip tests ---
101
102    #[test]
103    fn roundtrip_empty() {
104        let decoded = decode("").unwrap();
105        assert!(decoded.is_empty());
106        assert_eq!(encode(&decoded), "");
107    }
108
109    #[test]
110    fn roundtrip_simple() {
111        let input = "AAAA;AACA";
112        let decoded = decode(input).unwrap();
113        let encoded = encode(&decoded);
114        assert_eq!(encoded, input);
115    }
116
117    #[test]
118    fn roundtrip_multiple_segments() {
119        let input = "AAAA,GAAG,EAAE;AACA";
120        let decoded = decode(input).unwrap();
121        let encoded = encode(&decoded);
122        assert_eq!(encoded, input);
123    }
124
125    #[test]
126    fn roundtrip_large_values() {
127        let mappings = vec![vec![vec![1000_i64, 50, 999, 500, 100]]];
128        let encoded = encode(&mappings);
129        let decoded = decode(&encoded).unwrap();
130        assert_eq!(decoded, mappings);
131    }
132
133    #[test]
134    fn roundtrip_negative_deltas() {
135        let mappings = vec![vec![vec![10_i64, 0, 10, 10], vec![20, 0, 5, 5]]];
136        let encoded = encode(&mappings);
137        let decoded = decode(&encoded).unwrap();
138        assert_eq!(decoded, mappings);
139    }
140
141    // --- Decode structure tests ---
142
143    #[test]
144    fn decode_single_field_segment() {
145        let decoded = decode("A").unwrap();
146        assert_eq!(decoded.len(), 1);
147        assert_eq!(decoded[0].len(), 1);
148        assert_eq!(decoded[0][0], vec![0]);
149    }
150
151    #[test]
152    fn decode_four_field_segment() {
153        let decoded = decode("AAAA").unwrap();
154        assert_eq!(decoded.len(), 1);
155        assert_eq!(decoded[0].len(), 1);
156        assert_eq!(decoded[0][0], vec![0, 0, 0, 0]);
157    }
158
159    #[test]
160    fn decode_five_field_segment() {
161        let decoded = decode("AAAAA").unwrap();
162        assert_eq!(decoded.len(), 1);
163        assert_eq!(decoded[0].len(), 1);
164        assert_eq!(decoded[0][0], vec![0, 0, 0, 0, 0]);
165    }
166
167    #[test]
168    fn decode_negative_values() {
169        let decoded = decode("DADD").unwrap();
170        assert_eq!(decoded[0][0], vec![-1, 0, -1, -1]);
171    }
172
173    #[test]
174    fn decode_multiple_lines() {
175        let decoded = decode("AAAA;AACA;AACA").unwrap();
176        assert_eq!(decoded.len(), 3);
177    }
178
179    #[test]
180    fn decode_empty_lines() {
181        let decoded = decode("AAAA;;;AACA").unwrap();
182        assert_eq!(decoded.len(), 4);
183        assert!(decoded[1].is_empty());
184        assert!(decoded[2].is_empty());
185    }
186
187    #[test]
188    fn decode_trailing_semicolon() {
189        // Trailing `;` means an empty line follows
190        let decoded = decode("AAAA;").unwrap();
191        assert_eq!(decoded.len(), 2);
192        assert_eq!(decoded[0].len(), 1);
193        assert!(decoded[1].is_empty());
194    }
195
196    #[test]
197    fn decode_only_semicolons() {
198        let decoded = decode(";;;").unwrap();
199        assert_eq!(decoded.len(), 4);
200        for line in &decoded {
201            assert!(line.is_empty());
202        }
203    }
204
205    // --- Malformed input tests ---
206
207    #[test]
208    fn decode_invalid_ascii_char() {
209        let err = decode("AA!A").unwrap_err();
210        assert_eq!(
211            err,
212            DecodeError::InvalidBase64 {
213                byte: b'!',
214                offset: 2
215            }
216        );
217    }
218
219    #[test]
220    fn decode_non_ascii_byte() {
221        // 'À' is UTF-8 bytes [0xC3, 0x80] — both >= 128, caught by non-ASCII guard
222        let err = decode("AAÀ").unwrap_err();
223        assert_eq!(
224            err,
225            DecodeError::InvalidBase64 {
226                byte: 0xC3,
227                offset: 2
228            }
229        );
230    }
231
232    #[test]
233    fn decode_truncated_vlq() {
234        // 'g' has value 32, which has the continuation bit set — needs more chars
235        let err = decode("g").unwrap_err();
236        assert_eq!(err, DecodeError::UnexpectedEof { offset: 1 });
237    }
238
239    #[test]
240    fn decode_vlq_overflow() {
241        // 14 continuation characters: each 'g' = value 32 (continuation bit set)
242        // After 13 digits, shift reaches 65 which exceeds i64 range
243        let err = decode("gggggggggggggg").unwrap_err();
244        matches!(err, DecodeError::VlqOverflow { .. });
245    }
246
247    #[test]
248    fn decode_truncated_segment() {
249        // "AC" = two VLQ values (0, 1) — starts a 4-field segment but only has 2 values
250        let err = decode("AC").unwrap_err();
251        assert!(matches!(
252            err,
253            DecodeError::UnexpectedEof { .. } | DecodeError::InvalidBase64 { .. }
254        ));
255    }
256
257    // --- Encode edge cases ---
258
259    #[test]
260    fn encode_empty_segments_no_dangling_comma() {
261        // Empty segments should be skipped without producing dangling commas
262        let mappings = vec![vec![vec![], vec![0, 0, 0, 0], vec![], vec![2, 0, 0, 1]]];
263        let encoded = encode(&mappings);
264        assert!(
265            !encoded.contains(",,"),
266            "should not contain dangling commas"
267        );
268        // Should encode as if empty segments don't exist
269        let expected = encode(&vec![vec![vec![0, 0, 0, 0], vec![2, 0, 0, 1]]]);
270        assert_eq!(encoded, expected);
271    }
272
273    #[test]
274    fn encode_all_empty_segments() {
275        let mappings = vec![vec![vec![], vec![], vec![]]];
276        let encoded = encode(&mappings);
277        assert_eq!(encoded, "");
278    }
279
280    // --- Parallel encoding tests ---
281
282    #[cfg(feature = "parallel")]
283    mod parallel_tests {
284        use super::*;
285
286        fn build_large_mappings(lines: usize, segments_per_line: usize) -> SourceMapMappings {
287            let mut mappings = Vec::with_capacity(lines);
288            for line in 0..lines {
289                let mut line_segments = Vec::with_capacity(segments_per_line);
290                for seg in 0..segments_per_line {
291                    line_segments.push(vec![
292                        (seg * 10) as i64, // generated column
293                        (seg % 5) as i64,  // source index
294                        line as i64,       // original line
295                        (seg * 5) as i64,  // original column
296                        (seg % 3) as i64,  // name index
297                    ]);
298                }
299                mappings.push(line_segments);
300            }
301            mappings
302        }
303
304        #[test]
305        fn parallel_matches_sequential_large() {
306            let mappings = build_large_mappings(2000, 10);
307            let sequential = encode(&mappings);
308            let parallel = encode_parallel(&mappings);
309            assert_eq!(sequential, parallel);
310        }
311
312        #[test]
313        fn parallel_matches_sequential_with_empty_lines() {
314            let mut mappings = build_large_mappings(1500, 8);
315            // Insert empty lines
316            for i in (0..mappings.len()).step_by(3) {
317                mappings[i] = Vec::new();
318            }
319            let sequential = encode(&mappings);
320            let parallel = encode_parallel(&mappings);
321            assert_eq!(sequential, parallel);
322        }
323
324        #[test]
325        fn parallel_matches_sequential_mixed_segments() {
326            let mut mappings: SourceMapMappings = Vec::with_capacity(2000);
327            for line in 0..2000 {
328                let mut line_segments = Vec::new();
329                for seg in 0..8 {
330                    if seg % 4 == 0 {
331                        // 1-field segment (generated-only)
332                        line_segments.push(vec![(seg * 10) as i64]);
333                    } else if seg % 4 == 3 {
334                        // 5-field segment (with name)
335                        line_segments.push(vec![
336                            (seg * 10) as i64,
337                            (seg % 3) as i64,
338                            line as i64,
339                            (seg * 5) as i64,
340                            (seg % 2) as i64,
341                        ]);
342                    } else {
343                        // 4-field segment
344                        line_segments.push(vec![
345                            (seg * 10) as i64,
346                            (seg % 3) as i64,
347                            line as i64,
348                            (seg * 5) as i64,
349                        ]);
350                    }
351                }
352                mappings.push(line_segments);
353            }
354            let sequential = encode(&mappings);
355            let parallel = encode_parallel(&mappings);
356            assert_eq!(sequential, parallel);
357        }
358
359        #[test]
360        fn parallel_roundtrip() {
361            let mappings = build_large_mappings(2000, 10);
362            let encoded = encode_parallel(&mappings);
363            let decoded = decode(&encoded).unwrap();
364            assert_eq!(decoded, mappings);
365        }
366
367        #[test]
368        fn parallel_fallback_for_small_maps() {
369            // Below threshold — should still produce correct output
370            let mappings = build_large_mappings(10, 5);
371            let sequential = encode(&mappings);
372            let parallel = encode_parallel(&mappings);
373            assert_eq!(sequential, parallel);
374        }
375    }
376}