Skip to main content

srcmap_sourcemap/
lib.rs

1//! High-performance source map parser and consumer (ECMA-426).
2//!
3//! Parses source map JSON and provides O(log n) position lookups.
4//! Uses a flat, cache-friendly representation internally.
5//!
6//! # Examples
7//!
8//! ```
9//! use srcmap_sourcemap::SourceMap;
10//!
11//! let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;AACA"}"#;
12//! let sm = SourceMap::from_json(json).unwrap();
13//!
14//! // Look up original position for generated line 0, column 0
15//! let loc = sm.original_position_for(0, 0).unwrap();
16//! assert_eq!(sm.source(loc.source), "input.js");
17//! assert_eq!(loc.line, 0);
18//! assert_eq!(loc.column, 0);
19//!
20//! // Reverse lookup
21//! let pos = sm.generated_position_for("input.js", 0, 0).unwrap();
22//! assert_eq!(pos.line, 0);
23//! assert_eq!(pos.column, 0);
24//! ```
25
26use std::cell::OnceCell;
27use std::collections::HashMap;
28use std::fmt;
29
30use serde::Deserialize;
31use srcmap_codec::DecodeError;
32use srcmap_scopes::ScopeInfo;
33
34// ── Constants ──────────────────────────────────────────────────────
35
36const NO_SOURCE: u32 = u32::MAX;
37const NO_NAME: u32 = u32::MAX;
38
39// ── Public types ───────────────────────────────────────────────────
40
41/// A single decoded mapping entry. Compact at 24 bytes (6 × u32).
42#[derive(Debug, Clone, Copy)]
43pub struct Mapping {
44    pub generated_line: u32,
45    pub generated_column: u32,
46    /// Index into `SourceMap::sources`. `u32::MAX` if absent.
47    pub source: u32,
48    pub original_line: u32,
49    pub original_column: u32,
50    /// Index into `SourceMap::names`. `u32::MAX` if absent.
51    pub name: u32,
52}
53
54/// Result of an `original_position_for` lookup.
55#[derive(Debug, Clone)]
56pub struct OriginalLocation {
57    pub source: u32,
58    pub line: u32,
59    pub column: u32,
60    pub name: Option<u32>,
61}
62
63/// Result of a `generated_position_for` lookup.
64#[derive(Debug, Clone)]
65pub struct GeneratedLocation {
66    pub line: u32,
67    pub column: u32,
68}
69
70/// Errors during source map parsing.
71#[derive(Debug)]
72pub enum ParseError {
73    Json(serde_json::Error),
74    Vlq(DecodeError),
75    InvalidVersion(u32),
76    Scopes(String),
77}
78
79impl fmt::Display for ParseError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::Json(e) => write!(f, "JSON parse error: {e}"),
83            Self::Vlq(e) => write!(f, "VLQ decode error: {e}"),
84            Self::InvalidVersion(v) => write!(f, "unsupported source map version: {v}"),
85            Self::Scopes(e) => write!(f, "scopes decode error: {e}"),
86        }
87    }
88}
89
90impl std::error::Error for ParseError {}
91
92impl From<serde_json::Error> for ParseError {
93    fn from(e: serde_json::Error) -> Self {
94        Self::Json(e)
95    }
96}
97
98impl From<DecodeError> for ParseError {
99    fn from(e: DecodeError) -> Self {
100        Self::Vlq(e)
101    }
102}
103
104// ── Raw JSON structure ─────────────────────────────────────────────
105
106#[derive(Deserialize)]
107struct RawSourceMap<'a> {
108    version: u32,
109    #[serde(default)]
110    file: Option<String>,
111    #[serde(default, rename = "sourceRoot")]
112    source_root: Option<String>,
113    #[serde(default)]
114    sources: Vec<Option<String>>,
115    #[serde(default, rename = "sourcesContent")]
116    sources_content: Option<Vec<Option<String>>>,
117    #[serde(default)]
118    names: Vec<String>,
119    #[serde(default, borrow)]
120    mappings: &'a str,
121    #[serde(default, rename = "ignoreList")]
122    ignore_list: Vec<u32>,
123    /// Debug ID for associating generated files with source maps (ECMA-426).
124    /// Accepts both `debugId` (spec) and `debug_id` (Sentry compat).
125    #[serde(default, rename = "debugId", alias = "debug_id")]
126    debug_id: Option<String>,
127    /// Scopes and variables (ECMA-426 scopes proposal).
128    #[serde(default, borrow)]
129    scopes: Option<&'a str>,
130    /// Indexed source maps use `sections` instead of `mappings`.
131    #[serde(default)]
132    sections: Option<Vec<RawSection>>,
133}
134
135/// A section in an indexed source map.
136#[derive(Deserialize)]
137struct RawSection {
138    offset: RawOffset,
139    map: serde_json::Value,
140}
141
142#[derive(Deserialize)]
143struct RawOffset {
144    line: u32,
145    column: u32,
146}
147
148// ── SourceMap ──────────────────────────────────────────────────────
149
150/// A parsed source map with O(log n) position lookups.
151#[derive(Debug, Clone)]
152pub struct SourceMap {
153    pub file: Option<String>,
154    pub source_root: Option<String>,
155    pub sources: Vec<String>,
156    pub sources_content: Vec<Option<String>>,
157    pub names: Vec<String>,
158    pub ignore_list: Vec<u32>,
159    /// Debug ID (UUID) for associating generated files with source maps (ECMA-426).
160    pub debug_id: Option<String>,
161    /// Decoded scope and variable information (ECMA-426 scopes proposal).
162    pub scopes: Option<ScopeInfo>,
163
164    /// Flat decoded mappings, ordered by (generated_line, generated_column).
165    mappings: Vec<Mapping>,
166
167    /// `line_offsets[i]` = index of first mapping on generated line `i`.
168    /// `line_offsets[line_count]` = mappings.len() (sentinel).
169    line_offsets: Vec<u32>,
170
171    /// Indices into `mappings`, sorted by (source, original_line, original_column).
172    /// Built lazily on first `generated_position_for` call.
173    reverse_index: OnceCell<Vec<u32>>,
174
175    /// Source filename → index for O(1) lookup by name.
176    source_map: HashMap<String, u32>,
177}
178
179impl SourceMap {
180    /// Parse a source map from a JSON string.
181    /// Supports both regular and indexed (sectioned) source maps.
182    pub fn from_json(json: &str) -> Result<Self, ParseError> {
183        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
184
185        if raw.version != 3 {
186            return Err(ParseError::InvalidVersion(raw.version));
187        }
188
189        // Handle indexed source maps (sections)
190        if let Some(sections) = raw.sections {
191            return Self::from_sections(raw.file, sections);
192        }
193
194        Self::from_regular(raw)
195    }
196
197    /// Parse a regular (non-indexed) source map.
198    fn from_regular(raw: RawSourceMap<'_>) -> Result<Self, ParseError> {
199        // Resolve sources: apply sourceRoot, replace None with empty string
200        let source_root = raw.source_root.as_deref().unwrap_or("");
201        let sources: Vec<String> = raw
202            .sources
203            .iter()
204            .map(|s| match s {
205                Some(s) if !source_root.is_empty() => format!("{source_root}{s}"),
206                Some(s) => s.clone(),
207                None => String::new(),
208            })
209            .collect();
210
211        let sources_content = raw.sources_content.unwrap_or_default();
212
213        // Build source name → index map
214        let source_map: HashMap<String, u32> = sources
215            .iter()
216            .enumerate()
217            .map(|(i, s)| (s.clone(), i as u32))
218            .collect();
219
220        // Decode mappings directly into flat Mapping vec
221        let (mappings, line_offsets) = decode_mappings(raw.mappings)?;
222
223        // Decode scopes if present
224        let num_sources = sources.len();
225        let scopes = match raw.scopes {
226            Some(scopes_str) if !scopes_str.is_empty() => Some(
227                srcmap_scopes::decode_scopes(scopes_str, &raw.names, num_sources)
228                    .map_err(|e| ParseError::Scopes(e.to_string()))?,
229            ),
230            _ => None,
231        };
232
233        Ok(Self {
234            file: raw.file,
235            source_root: raw.source_root,
236            sources,
237            sources_content,
238            names: raw.names,
239            ignore_list: raw.ignore_list,
240            debug_id: raw.debug_id,
241            scopes,
242            mappings,
243            line_offsets,
244            reverse_index: OnceCell::new(),
245            source_map,
246        })
247    }
248
249    /// Flatten an indexed source map (with sections) into a regular one.
250    fn from_sections(file: Option<String>, sections: Vec<RawSection>) -> Result<Self, ParseError> {
251        let mut all_sources: Vec<String> = Vec::new();
252        let mut all_sources_content: Vec<Option<String>> = Vec::new();
253        let mut all_names: Vec<String> = Vec::new();
254        let mut all_mappings: Vec<Mapping> = Vec::new();
255        let mut all_ignore_list: Vec<u32> = Vec::new();
256        let mut max_line: u32 = 0;
257
258        // Source/name dedup maps to merge across sections
259        let mut source_index_map: HashMap<String, u32> = HashMap::new();
260        let mut name_index_map: HashMap<String, u32> = HashMap::new();
261
262        for section in &sections {
263            let section_json = serde_json::to_string(&section.map).map_err(ParseError::Json)?;
264            let sub = Self::from_json(&section_json)?;
265
266            let line_offset = section.offset.line;
267            let col_offset = section.offset.column;
268
269            // Map section source indices to global indices
270            let source_remap: Vec<u32> = sub
271                .sources
272                .iter()
273                .enumerate()
274                .map(|(i, s)| {
275                    if let Some(&existing) = source_index_map.get(s) {
276                        existing
277                    } else {
278                        let idx = all_sources.len() as u32;
279                        all_sources.push(s.clone());
280                        // Add sourcesContent if available
281                        let content = sub.sources_content.get(i).cloned().unwrap_or(None);
282                        all_sources_content.push(content);
283                        source_index_map.insert(s.clone(), idx);
284                        idx
285                    }
286                })
287                .collect();
288
289            // Map section name indices to global indices
290            let name_remap: Vec<u32> = sub
291                .names
292                .iter()
293                .map(|n| {
294                    if let Some(&existing) = name_index_map.get(n) {
295                        existing
296                    } else {
297                        let idx = all_names.len() as u32;
298                        all_names.push(n.clone());
299                        name_index_map.insert(n.clone(), idx);
300                        idx
301                    }
302                })
303                .collect();
304
305            // Add ignore_list entries (remapped to global source indices)
306            for &idx in &sub.ignore_list {
307                let global_idx = source_remap[idx as usize];
308                if !all_ignore_list.contains(&global_idx) {
309                    all_ignore_list.push(global_idx);
310                }
311            }
312
313            // Remap and offset all mappings from this section
314            for m in &sub.mappings {
315                let gen_line = m.generated_line + line_offset;
316                let gen_col = if m.generated_line == 0 {
317                    m.generated_column + col_offset
318                } else {
319                    m.generated_column
320                };
321
322                all_mappings.push(Mapping {
323                    generated_line: gen_line,
324                    generated_column: gen_col,
325                    source: if m.source == NO_SOURCE {
326                        NO_SOURCE
327                    } else {
328                        source_remap[m.source as usize]
329                    },
330                    original_line: m.original_line,
331                    original_column: m.original_column,
332                    name: if m.name == NO_NAME {
333                        NO_NAME
334                    } else {
335                        name_remap[m.name as usize]
336                    },
337                });
338
339                if gen_line > max_line {
340                    max_line = gen_line;
341                }
342            }
343        }
344
345        // Sort mappings by (generated_line, generated_column)
346        all_mappings.sort_unstable_by(|a, b| {
347            a.generated_line
348                .cmp(&b.generated_line)
349                .then(a.generated_column.cmp(&b.generated_column))
350        });
351
352        // Build line_offsets
353        let line_count = if all_mappings.is_empty() {
354            0
355        } else {
356            max_line as usize + 1
357        };
358        let mut line_offsets: Vec<u32> = vec![0; line_count + 1];
359        let mut current_line: usize = 0;
360        for (i, m) in all_mappings.iter().enumerate() {
361            while current_line < m.generated_line as usize {
362                current_line += 1;
363                if current_line < line_offsets.len() {
364                    line_offsets[current_line] = i as u32;
365                }
366            }
367        }
368        // Fill sentinel
369        if !line_offsets.is_empty() {
370            let last = all_mappings.len() as u32;
371            for offset in line_offsets.iter_mut().skip(current_line + 1) {
372                *offset = last;
373            }
374        }
375
376        Ok(Self {
377            file,
378            source_root: None,
379            sources: all_sources.clone(),
380            sources_content: all_sources_content,
381            names: all_names,
382            ignore_list: all_ignore_list,
383            debug_id: None,
384            scopes: None, // TODO: merge scopes from sections
385            mappings: all_mappings,
386            line_offsets,
387            reverse_index: OnceCell::new(),
388            source_map: all_sources
389                .into_iter()
390                .enumerate()
391                .map(|(i, s)| (s, i as u32))
392                .collect(),
393        })
394    }
395
396    /// Look up the original source position for a generated position.
397    ///
398    /// Both `line` and `column` are 0-based.
399    /// Returns `None` if no mapping exists or the mapping has no source.
400    pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
401        let line_idx = line as usize;
402        if line_idx + 1 >= self.line_offsets.len() {
403            return None;
404        }
405
406        let start = self.line_offsets[line_idx] as usize;
407        let end = self.line_offsets[line_idx + 1] as usize;
408
409        if start == end {
410            return None;
411        }
412
413        let line_mappings = &self.mappings[start..end];
414
415        // Binary search: find largest generated_column <= column
416        let idx = match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
417            Ok(i) => i,
418            Err(0) => return None,
419            Err(i) => i - 1,
420        };
421
422        let mapping = &line_mappings[idx];
423
424        if mapping.source == NO_SOURCE {
425            return None;
426        }
427
428        Some(OriginalLocation {
429            source: mapping.source,
430            line: mapping.original_line,
431            column: mapping.original_column,
432            name: if mapping.name == NO_NAME {
433                None
434            } else {
435                Some(mapping.name)
436            },
437        })
438    }
439
440    /// Look up the generated position for an original source position.
441    ///
442    /// `source` is the source filename. `line` and `column` are 0-based.
443    pub fn generated_position_for(
444        &self,
445        source: &str,
446        line: u32,
447        column: u32,
448    ) -> Option<GeneratedLocation> {
449        let &source_idx = self.source_map.get(source)?;
450
451        let reverse_index = self
452            .reverse_index
453            .get_or_init(|| build_reverse_index(&self.mappings));
454
455        // Binary search in reverse_index for (source, line, column)
456        let idx = reverse_index.partition_point(|&i| {
457            let m = &self.mappings[i as usize];
458            (m.source, m.original_line, m.original_column) < (source_idx, line, column)
459        });
460
461        if idx >= reverse_index.len() {
462            return None;
463        }
464
465        let mapping = &self.mappings[reverse_index[idx] as usize];
466
467        if mapping.source != source_idx {
468            return None;
469        }
470
471        Some(GeneratedLocation {
472            line: mapping.generated_line,
473            column: mapping.generated_column,
474        })
475    }
476
477    /// Resolve a source index to its filename.
478    pub fn source(&self, index: u32) -> &str {
479        &self.sources[index as usize]
480    }
481
482    /// Resolve a name index to its string.
483    pub fn name(&self, index: u32) -> &str {
484        &self.names[index as usize]
485    }
486
487    /// Find the source index for a filename.
488    pub fn source_index(&self, name: &str) -> Option<u32> {
489        self.source_map.get(name).copied()
490    }
491
492    /// Total number of decoded mappings.
493    pub fn mapping_count(&self) -> usize {
494        self.mappings.len()
495    }
496
497    /// Number of generated lines.
498    pub fn line_count(&self) -> usize {
499        self.line_offsets.len().saturating_sub(1)
500    }
501
502    /// Get all mappings for a generated line (0-based).
503    pub fn mappings_for_line(&self, line: u32) -> &[Mapping] {
504        let line_idx = line as usize;
505        if line_idx + 1 >= self.line_offsets.len() {
506            return &[];
507        }
508        let start = self.line_offsets[line_idx] as usize;
509        let end = self.line_offsets[line_idx + 1] as usize;
510        &self.mappings[start..end]
511    }
512
513    /// Iterate all mappings.
514    pub fn all_mappings(&self) -> &[Mapping] {
515        &self.mappings
516    }
517
518    /// Serialize the source map back to JSON.
519    ///
520    /// Produces a valid source map v3 JSON string that can be written to a file
521    /// or embedded in a data URL.
522    pub fn to_json(&self) -> String {
523        let mappings = self.encode_mappings();
524
525        let mut json = String::with_capacity(256 + mappings.len());
526        json.push_str(r#"{"version":3"#);
527
528        if let Some(ref file) = self.file {
529            json.push_str(r#","file":"#);
530            json_quote_into(&mut json, file);
531        }
532
533        if let Some(ref root) = self.source_root {
534            json.push_str(r#","sourceRoot":"#);
535            json_quote_into(&mut json, root);
536        }
537
538        json.push_str(r#","sources":["#);
539        for (i, s) in self.sources.iter().enumerate() {
540            if i > 0 {
541                json.push(',');
542            }
543            json_quote_into(&mut json, s);
544        }
545        json.push(']');
546
547        if !self.sources_content.is_empty() && self.sources_content.iter().any(|c| c.is_some()) {
548            json.push_str(r#","sourcesContent":["#);
549            for (i, c) in self.sources_content.iter().enumerate() {
550                if i > 0 {
551                    json.push(',');
552                }
553                match c {
554                    Some(content) => json_quote_into(&mut json, content),
555                    None => json.push_str("null"),
556                }
557            }
558            json.push(']');
559        }
560
561        json.push_str(r#","names":["#);
562        for (i, n) in self.names.iter().enumerate() {
563            if i > 0 {
564                json.push(',');
565            }
566            json_quote_into(&mut json, n);
567        }
568        json.push(']');
569
570        json.push_str(r#","mappings":"#);
571        json_quote_into(&mut json, &mappings);
572
573        if !self.ignore_list.is_empty() {
574            json.push_str(r#","ignoreList":["#);
575            for (i, &idx) in self.ignore_list.iter().enumerate() {
576                if i > 0 {
577                    json.push(',');
578                }
579                json.push_str(&idx.to_string());
580            }
581            json.push(']');
582        }
583
584        if let Some(ref id) = self.debug_id {
585            json.push_str(r#","debugId":"#);
586            json_quote_into(&mut json, id);
587        }
588
589        json.push('}');
590        json
591    }
592
593    /// Encode all mappings back to a VLQ mappings string.
594    fn encode_mappings(&self) -> String {
595        if self.mappings.is_empty() {
596            return String::new();
597        }
598
599        let mut out: Vec<u8> = Vec::with_capacity(self.mappings.len() * 6);
600
601        let mut prev_gen_col: i64 = 0;
602        let mut prev_source: i64 = 0;
603        let mut prev_orig_line: i64 = 0;
604        let mut prev_orig_col: i64 = 0;
605        let mut prev_name: i64 = 0;
606        let mut prev_gen_line: u32 = 0;
607        let mut first_in_line = true;
608
609        for m in &self.mappings {
610            while prev_gen_line < m.generated_line {
611                out.push(b';');
612                prev_gen_line += 1;
613                prev_gen_col = 0;
614                first_in_line = true;
615            }
616
617            if !first_in_line {
618                out.push(b',');
619            }
620            first_in_line = false;
621
622            srcmap_codec::vlq_encode(&mut out, m.generated_column as i64 - prev_gen_col);
623            prev_gen_col = m.generated_column as i64;
624
625            if m.source != NO_SOURCE {
626                srcmap_codec::vlq_encode(&mut out, m.source as i64 - prev_source);
627                prev_source = m.source as i64;
628
629                srcmap_codec::vlq_encode(&mut out, m.original_line as i64 - prev_orig_line);
630                prev_orig_line = m.original_line as i64;
631
632                srcmap_codec::vlq_encode(&mut out, m.original_column as i64 - prev_orig_col);
633                prev_orig_col = m.original_column as i64;
634
635                if m.name != NO_NAME {
636                    srcmap_codec::vlq_encode(&mut out, m.name as i64 - prev_name);
637                    prev_name = m.name as i64;
638                }
639            }
640        }
641
642        // SAFETY: VLQ output is always valid ASCII
643        unsafe { String::from_utf8_unchecked(out) }
644    }
645}
646
647/// Append a JSON-quoted string to the output buffer.
648fn json_quote_into(out: &mut String, s: &str) {
649    out.push('"');
650    for c in s.chars() {
651        match c {
652            '"' => out.push_str("\\\""),
653            '\\' => out.push_str("\\\\"),
654            '\n' => out.push_str("\\n"),
655            '\r' => out.push_str("\\r"),
656            '\t' => out.push_str("\\t"),
657            c if c < '\x20' => {
658                out.push_str(&format!("\\u{:04x}", c as u32));
659            }
660            c => out.push(c),
661        }
662    }
663    out.push('"');
664}
665
666// ── Internal: decode VLQ mappings directly into flat Mapping vec ───
667
668/// Base64 decode lookup table (byte → 6-bit value, 0xFF = invalid).
669const B64: [u8; 128] = {
670    let mut table = [0xFFu8; 128];
671    let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
672    let mut i = 0u8;
673    while i < 64 {
674        table[chars[i as usize] as usize] = i;
675        i += 1;
676    }
677    table
678};
679
680/// Inline VLQ decode optimized for the hot path (no function call overhead).
681/// Most source map VLQ values fit in 1-2 base64 characters.
682#[inline(always)]
683fn vlq_fast(bytes: &[u8], pos: &mut usize) -> Result<i64, DecodeError> {
684    let p = *pos;
685    if p >= bytes.len() {
686        return Err(DecodeError::UnexpectedEof { offset: p });
687    }
688
689    let b0 = bytes[p];
690    if b0 >= 128 {
691        return Err(DecodeError::InvalidBase64 {
692            byte: b0,
693            offset: p,
694        });
695    }
696    let d0 = B64[b0 as usize];
697    if d0 == 0xFF {
698        return Err(DecodeError::InvalidBase64 {
699            byte: b0,
700            offset: p,
701        });
702    }
703
704    // Fast path: single character VLQ (values -15..15)
705    if (d0 & 0x20) == 0 {
706        *pos = p + 1;
707        let val = (d0 >> 1) as i64;
708        return Ok(if (d0 & 1) != 0 { -val } else { val });
709    }
710
711    // Multi-character VLQ
712    let mut result: u64 = (d0 & 0x1F) as u64;
713    let mut shift: u32 = 5;
714    let mut i = p + 1;
715
716    loop {
717        if i >= bytes.len() {
718            return Err(DecodeError::UnexpectedEof { offset: i });
719        }
720        let b = bytes[i];
721        if b >= 128 {
722            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
723        }
724        let d = B64[b as usize];
725        if d == 0xFF {
726            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
727        }
728        i += 1;
729
730        if shift >= 64 {
731            return Err(DecodeError::VlqOverflow { offset: p });
732        }
733
734        result += ((d & 0x1F) as u64) << shift;
735        shift += 5;
736
737        if (d & 0x20) == 0 {
738            break;
739        }
740    }
741
742    *pos = i;
743    let value = if (result & 1) == 1 {
744        -((result >> 1) as i64)
745    } else {
746        (result >> 1) as i64
747    };
748    Ok(value)
749}
750
751fn decode_mappings(input: &str) -> Result<(Vec<Mapping>, Vec<u32>), DecodeError> {
752    if input.is_empty() {
753        return Ok((Vec::new(), vec![0]));
754    }
755
756    let bytes = input.as_bytes();
757    let len = bytes.len();
758
759    // Pre-count for capacity hints
760    let line_count = bytes.iter().filter(|&&b| b == b';').count() + 1;
761    let approx_segments = bytes.iter().filter(|&&b| b == b',').count() + line_count;
762
763    let mut mappings: Vec<Mapping> = Vec::with_capacity(approx_segments);
764    let mut line_offsets: Vec<u32> = Vec::with_capacity(line_count + 1);
765
766    let mut source_index: i64 = 0;
767    let mut original_line: i64 = 0;
768    let mut original_column: i64 = 0;
769    let mut name_index: i64 = 0;
770    let mut generated_line: u32 = 0;
771    let mut pos: usize = 0;
772
773    loop {
774        line_offsets.push(mappings.len() as u32);
775        let mut generated_column: i64 = 0;
776        let mut saw_semicolon = false;
777
778        while pos < len {
779            let byte = bytes[pos];
780
781            if byte == b';' {
782                pos += 1;
783                saw_semicolon = true;
784                break;
785            }
786
787            if byte == b',' {
788                pos += 1;
789                continue;
790            }
791
792            // Field 1: generated column
793            generated_column += vlq_fast(bytes, &mut pos)?;
794
795            if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
796                // Fields 2-4: source, original line, original column
797                source_index += vlq_fast(bytes, &mut pos)?;
798                original_line += vlq_fast(bytes, &mut pos)?;
799                original_column += vlq_fast(bytes, &mut pos)?;
800
801                // Field 5: name (optional)
802                let name = if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
803                    name_index += vlq_fast(bytes, &mut pos)?;
804                    name_index as u32
805                } else {
806                    NO_NAME
807                };
808
809                mappings.push(Mapping {
810                    generated_line,
811                    generated_column: generated_column as u32,
812                    source: source_index as u32,
813                    original_line: original_line as u32,
814                    original_column: original_column as u32,
815                    name,
816                });
817            } else {
818                // 1-field segment: no source info
819                mappings.push(Mapping {
820                    generated_line,
821                    generated_column: generated_column as u32,
822                    source: NO_SOURCE,
823                    original_line: 0,
824                    original_column: 0,
825                    name: NO_NAME,
826                });
827            }
828        }
829
830        if !saw_semicolon {
831            break;
832        }
833        generated_line += 1;
834    }
835
836    // Sentinel for line range computation
837    line_offsets.push(mappings.len() as u32);
838
839    Ok((mappings, line_offsets))
840}
841
842/// Build reverse index: mapping indices sorted by (source, original_line, original_column).
843fn build_reverse_index(mappings: &[Mapping]) -> Vec<u32> {
844    let mut indices: Vec<u32> = (0..mappings.len() as u32)
845        .filter(|&i| mappings[i as usize].source != NO_SOURCE)
846        .collect();
847
848    indices.sort_unstable_by(|&a, &b| {
849        let ma = &mappings[a as usize];
850        let mb = &mappings[b as usize];
851        ma.source
852            .cmp(&mb.source)
853            .then(ma.original_line.cmp(&mb.original_line))
854            .then(ma.original_column.cmp(&mb.original_column))
855    });
856
857    indices
858}
859
860// ── Tests ──────────────────────────────────────────────────────────
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    fn simple_map() -> &'static str {
867        r#"{"version":3,"sources":["input.js"],"names":["hello"],"mappings":"AAAA;AACA,EAAA;AACA"}"#
868    }
869
870    #[test]
871    fn parse_basic() {
872        let sm = SourceMap::from_json(simple_map()).unwrap();
873        assert_eq!(sm.sources, vec!["input.js"]);
874        assert_eq!(sm.names, vec!["hello"]);
875        assert_eq!(sm.line_count(), 3);
876        assert!(sm.mapping_count() > 0);
877    }
878
879    #[test]
880    fn to_json_roundtrip() {
881        let json = simple_map();
882        let sm = SourceMap::from_json(json).unwrap();
883        let output = sm.to_json();
884
885        // Parse the output back and verify it produces identical lookups
886        let sm2 = SourceMap::from_json(&output).unwrap();
887        assert_eq!(sm2.sources, sm.sources);
888        assert_eq!(sm2.names, sm.names);
889        assert_eq!(sm2.mapping_count(), sm.mapping_count());
890        assert_eq!(sm2.line_count(), sm.line_count());
891
892        // Verify all lookups match
893        for m in sm.all_mappings() {
894            let loc1 = sm.original_position_for(m.generated_line, m.generated_column);
895            let loc2 = sm2.original_position_for(m.generated_line, m.generated_column);
896            match (loc1, loc2) {
897                (Some(a), Some(b)) => {
898                    assert_eq!(a.source, b.source);
899                    assert_eq!(a.line, b.line);
900                    assert_eq!(a.column, b.column);
901                    assert_eq!(a.name, b.name);
902                }
903                (None, None) => {}
904                _ => panic!(
905                    "lookup mismatch at ({}, {})",
906                    m.generated_line, m.generated_column
907                ),
908            }
909        }
910    }
911
912    #[test]
913    fn to_json_roundtrip_large() {
914        let json = generate_test_sourcemap(50, 10, 3);
915        let sm = SourceMap::from_json(&json).unwrap();
916        let output = sm.to_json();
917        let sm2 = SourceMap::from_json(&output).unwrap();
918
919        assert_eq!(sm2.mapping_count(), sm.mapping_count());
920
921        // Spot-check lookups
922        for line in (0..sm.line_count() as u32).step_by(5) {
923            for col in [0u32, 10, 20, 50] {
924                let a = sm.original_position_for(line, col);
925                let b = sm2.original_position_for(line, col);
926                match (a, b) {
927                    (Some(a), Some(b)) => {
928                        assert_eq!(a.source, b.source);
929                        assert_eq!(a.line, b.line);
930                        assert_eq!(a.column, b.column);
931                    }
932                    (None, None) => {}
933                    _ => panic!("mismatch at ({line}, {col})"),
934                }
935            }
936        }
937    }
938
939    #[test]
940    fn to_json_preserves_fields() {
941        let json = r#"{"version":3,"file":"out.js","sourceRoot":"src/","sources":["app.ts"],"sourcesContent":["const x = 1;"],"names":["x"],"mappings":"AAAAA","ignoreList":[0]}"#;
942        let sm = SourceMap::from_json(json).unwrap();
943        let output = sm.to_json();
944
945        assert!(output.contains(r#""file":"out.js""#));
946        assert!(output.contains(r#""sourceRoot":"src/""#));
947        assert!(output.contains(r#""sourcesContent":["const x = 1;"]"#));
948        assert!(output.contains(r#""ignoreList":[0]"#));
949
950        // Note: sources will have sourceRoot prepended
951        let sm2 = SourceMap::from_json(&output).unwrap();
952        assert_eq!(sm2.file.as_deref(), Some("out.js"));
953        assert_eq!(sm2.ignore_list, vec![0]);
954    }
955
956    #[test]
957    fn original_position_for_exact_match() {
958        let sm = SourceMap::from_json(simple_map()).unwrap();
959        let loc = sm.original_position_for(0, 0).unwrap();
960        assert_eq!(loc.source, 0);
961        assert_eq!(loc.line, 0);
962        assert_eq!(loc.column, 0);
963    }
964
965    #[test]
966    fn original_position_for_column_within_segment() {
967        let sm = SourceMap::from_json(simple_map()).unwrap();
968        // Column 5 on line 1: should snap to the mapping at column 2
969        let loc = sm.original_position_for(1, 5);
970        assert!(loc.is_some());
971    }
972
973    #[test]
974    fn original_position_for_nonexistent_line() {
975        let sm = SourceMap::from_json(simple_map()).unwrap();
976        assert!(sm.original_position_for(999, 0).is_none());
977    }
978
979    #[test]
980    fn original_position_for_before_first_mapping() {
981        // Line 1 first mapping is at column 2. Column 0 should return None.
982        let sm = SourceMap::from_json(simple_map()).unwrap();
983        let loc = sm.original_position_for(1, 0);
984        // Column 0 on line 1: the first mapping at col 0 (AACA decodes to col=0, src delta=1...)
985        // Actually depends on exact VLQ values. Let's just verify it doesn't crash.
986        let _ = loc;
987    }
988
989    #[test]
990    fn generated_position_for_basic() {
991        let sm = SourceMap::from_json(simple_map()).unwrap();
992        let loc = sm.generated_position_for("input.js", 0, 0).unwrap();
993        assert_eq!(loc.line, 0);
994        assert_eq!(loc.column, 0);
995    }
996
997    #[test]
998    fn generated_position_for_unknown_source() {
999        let sm = SourceMap::from_json(simple_map()).unwrap();
1000        assert!(sm.generated_position_for("nonexistent.js", 0, 0).is_none());
1001    }
1002
1003    #[test]
1004    fn parse_invalid_version() {
1005        let json = r#"{"version":2,"sources":[],"names":[],"mappings":""}"#;
1006        let err = SourceMap::from_json(json).unwrap_err();
1007        assert!(matches!(err, ParseError::InvalidVersion(2)));
1008    }
1009
1010    #[test]
1011    fn parse_empty_mappings() {
1012        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1013        let sm = SourceMap::from_json(json).unwrap();
1014        assert_eq!(sm.mapping_count(), 0);
1015        assert!(sm.original_position_for(0, 0).is_none());
1016    }
1017
1018    #[test]
1019    fn parse_with_source_root() {
1020        let json = r#"{"version":3,"sourceRoot":"src/","sources":["foo.js"],"names":[],"mappings":"AAAA"}"#;
1021        let sm = SourceMap::from_json(json).unwrap();
1022        assert_eq!(sm.sources, vec!["src/foo.js"]);
1023    }
1024
1025    #[test]
1026    fn parse_with_sources_content() {
1027        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var x = 1;"],"names":[],"mappings":"AAAA"}"#;
1028        let sm = SourceMap::from_json(json).unwrap();
1029        assert_eq!(sm.sources_content, vec![Some("var x = 1;".to_string())]);
1030    }
1031
1032    #[test]
1033    fn mappings_for_line() {
1034        let sm = SourceMap::from_json(simple_map()).unwrap();
1035        let line0 = sm.mappings_for_line(0);
1036        assert!(!line0.is_empty());
1037        let empty = sm.mappings_for_line(999);
1038        assert!(empty.is_empty());
1039    }
1040
1041    #[test]
1042    fn large_sourcemap_lookup() {
1043        // Generate a realistic source map
1044        let json = generate_test_sourcemap(500, 20, 5);
1045        let sm = SourceMap::from_json(&json).unwrap();
1046
1047        // Verify lookups work across the whole map
1048        for line in [0, 10, 100, 250, 499] {
1049            let mappings = sm.mappings_for_line(line);
1050            if let Some(m) = mappings.first() {
1051                let loc = sm.original_position_for(line, m.generated_column);
1052                assert!(loc.is_some(), "lookup failed for line {line}");
1053            }
1054        }
1055    }
1056
1057    #[test]
1058    fn reverse_lookup_roundtrip() {
1059        let json = generate_test_sourcemap(100, 10, 3);
1060        let sm = SourceMap::from_json(&json).unwrap();
1061
1062        // Pick a mapping and verify forward + reverse roundtrip
1063        let mapping = &sm.mappings[50];
1064        if mapping.source != NO_SOURCE {
1065            let source_name = sm.source(mapping.source);
1066            let result = sm.generated_position_for(
1067                source_name,
1068                mapping.original_line,
1069                mapping.original_column,
1070            );
1071            assert!(result.is_some(), "reverse lookup failed");
1072        }
1073    }
1074
1075    #[test]
1076    fn indexed_source_map() {
1077        let json = r#"{
1078            "version": 3,
1079            "file": "bundle.js",
1080            "sections": [
1081                {
1082                    "offset": {"line": 0, "column": 0},
1083                    "map": {
1084                        "version": 3,
1085                        "sources": ["a.js"],
1086                        "names": ["foo"],
1087                        "mappings": "AAAAA"
1088                    }
1089                },
1090                {
1091                    "offset": {"line": 10, "column": 0},
1092                    "map": {
1093                        "version": 3,
1094                        "sources": ["b.js"],
1095                        "names": ["bar"],
1096                        "mappings": "AAAAA"
1097                    }
1098                }
1099            ]
1100        }"#;
1101
1102        let sm = SourceMap::from_json(json).unwrap();
1103
1104        // Should have both sources
1105        assert_eq!(sm.sources.len(), 2);
1106        assert!(sm.sources.contains(&"a.js".to_string()));
1107        assert!(sm.sources.contains(&"b.js".to_string()));
1108
1109        // Should have both names
1110        assert_eq!(sm.names.len(), 2);
1111        assert!(sm.names.contains(&"foo".to_string()));
1112        assert!(sm.names.contains(&"bar".to_string()));
1113
1114        // First section: line 0, col 0 should map to a.js
1115        let loc = sm.original_position_for(0, 0).unwrap();
1116        assert_eq!(sm.source(loc.source), "a.js");
1117        assert_eq!(loc.line, 0);
1118        assert_eq!(loc.column, 0);
1119
1120        // Second section: line 10, col 0 should map to b.js
1121        let loc = sm.original_position_for(10, 0).unwrap();
1122        assert_eq!(sm.source(loc.source), "b.js");
1123        assert_eq!(loc.line, 0);
1124        assert_eq!(loc.column, 0);
1125    }
1126
1127    #[test]
1128    fn indexed_source_map_shared_sources() {
1129        // Two sections referencing the same source
1130        let json = r#"{
1131            "version": 3,
1132            "sections": [
1133                {
1134                    "offset": {"line": 0, "column": 0},
1135                    "map": {
1136                        "version": 3,
1137                        "sources": ["shared.js"],
1138                        "names": [],
1139                        "mappings": "AAAA"
1140                    }
1141                },
1142                {
1143                    "offset": {"line": 5, "column": 0},
1144                    "map": {
1145                        "version": 3,
1146                        "sources": ["shared.js"],
1147                        "names": [],
1148                        "mappings": "AACA"
1149                    }
1150                }
1151            ]
1152        }"#;
1153
1154        let sm = SourceMap::from_json(json).unwrap();
1155
1156        // Should deduplicate sources
1157        assert_eq!(sm.sources.len(), 1);
1158        assert_eq!(sm.sources[0], "shared.js");
1159
1160        // Both sections should resolve to the same source
1161        let loc0 = sm.original_position_for(0, 0).unwrap();
1162        let loc5 = sm.original_position_for(5, 0).unwrap();
1163        assert_eq!(loc0.source, loc5.source);
1164    }
1165
1166    #[test]
1167    fn parse_ignore_list() {
1168        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js"],"names":[],"mappings":"AAAA;ACAA","ignoreList":[1]}"#;
1169        let sm = SourceMap::from_json(json).unwrap();
1170        assert_eq!(sm.ignore_list, vec![1]);
1171    }
1172
1173    /// Helper: build a source map JSON from absolute mappings data.
1174    fn build_sourcemap_json(
1175        sources: &[&str],
1176        names: &[&str],
1177        mappings_data: &[Vec<Vec<i64>>],
1178    ) -> String {
1179        let mappings_vec: Vec<Vec<Vec<i64>>> = mappings_data.to_vec();
1180        let encoded = srcmap_codec::encode(&mappings_vec);
1181        format!(
1182            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
1183            sources
1184                .iter()
1185                .map(|s| format!("\"{s}\""))
1186                .collect::<Vec<_>>()
1187                .join(","),
1188            names
1189                .iter()
1190                .map(|n| format!("\"{n}\""))
1191                .collect::<Vec<_>>()
1192                .join(","),
1193            encoded,
1194        )
1195    }
1196
1197    // ── 1. Edge cases in decode_mappings ────────────────────────────
1198
1199    #[test]
1200    fn decode_multiple_consecutive_semicolons() {
1201        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;;AACA"}"#;
1202        let sm = SourceMap::from_json(json).unwrap();
1203        assert_eq!(sm.line_count(), 4);
1204        assert!(sm.mappings_for_line(1).is_empty());
1205        assert!(sm.mappings_for_line(2).is_empty());
1206        assert!(!sm.mappings_for_line(0).is_empty());
1207        assert!(!sm.mappings_for_line(3).is_empty());
1208    }
1209
1210    #[test]
1211    fn decode_trailing_semicolons() {
1212        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;"}"#;
1213        let sm = SourceMap::from_json(json).unwrap();
1214        assert_eq!(sm.line_count(), 3);
1215        assert!(!sm.mappings_for_line(0).is_empty());
1216        assert!(sm.mappings_for_line(1).is_empty());
1217        assert!(sm.mappings_for_line(2).is_empty());
1218    }
1219
1220    #[test]
1221    fn decode_leading_comma() {
1222        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":",AAAA"}"#;
1223        let sm = SourceMap::from_json(json).unwrap();
1224        assert_eq!(sm.mapping_count(), 1);
1225        let m = &sm.all_mappings()[0];
1226        assert_eq!(m.generated_line, 0);
1227        assert_eq!(m.generated_column, 0);
1228    }
1229
1230    #[test]
1231    fn decode_single_field_segments() {
1232        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,C"}"#;
1233        let sm = SourceMap::from_json(json).unwrap();
1234        assert_eq!(sm.mapping_count(), 2);
1235        for m in sm.all_mappings() {
1236            assert_eq!(m.source, NO_SOURCE);
1237        }
1238        assert_eq!(sm.all_mappings()[0].generated_column, 0);
1239        assert_eq!(sm.all_mappings()[1].generated_column, 1);
1240        assert!(sm.original_position_for(0, 0).is_none());
1241        assert!(sm.original_position_for(0, 1).is_none());
1242    }
1243
1244    #[test]
1245    fn decode_five_field_segments_with_names() {
1246        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0, 0], vec![10, 0, 0, 5, 1]]];
1247        let json = build_sourcemap_json(&["app.js"], &["foo", "bar"], &mappings_data);
1248        let sm = SourceMap::from_json(&json).unwrap();
1249        assert_eq!(sm.mapping_count(), 2);
1250        assert_eq!(sm.all_mappings()[0].name, 0);
1251        assert_eq!(sm.all_mappings()[1].name, 1);
1252
1253        let loc = sm.original_position_for(0, 0).unwrap();
1254        assert_eq!(loc.name, Some(0));
1255        assert_eq!(sm.name(0), "foo");
1256
1257        let loc = sm.original_position_for(0, 10).unwrap();
1258        assert_eq!(loc.name, Some(1));
1259        assert_eq!(sm.name(1), "bar");
1260    }
1261
1262    #[test]
1263    fn decode_large_vlq_values() {
1264        let mappings_data = vec![vec![vec![500_i64, 0, 1000, 2000]]];
1265        let json = build_sourcemap_json(&["big.js"], &[], &mappings_data);
1266        let sm = SourceMap::from_json(&json).unwrap();
1267        assert_eq!(sm.mapping_count(), 1);
1268        let m = &sm.all_mappings()[0];
1269        assert_eq!(m.generated_column, 500);
1270        assert_eq!(m.original_line, 1000);
1271        assert_eq!(m.original_column, 2000);
1272
1273        let loc = sm.original_position_for(0, 500).unwrap();
1274        assert_eq!(loc.line, 1000);
1275        assert_eq!(loc.column, 2000);
1276    }
1277
1278    #[test]
1279    fn decode_only_semicolons() {
1280        let json = r#"{"version":3,"sources":[],"names":[],"mappings":";;;"}"#;
1281        let sm = SourceMap::from_json(json).unwrap();
1282        assert_eq!(sm.line_count(), 4);
1283        assert_eq!(sm.mapping_count(), 0);
1284        for line in 0..4 {
1285            assert!(sm.mappings_for_line(line).is_empty());
1286        }
1287    }
1288
1289    #[test]
1290    fn decode_mixed_single_and_four_field_segments() {
1291        let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
1292        let four_field_encoded = srcmap_codec::encode(&mappings_data);
1293        let combined_mappings = format!("A,{four_field_encoded}");
1294        let json = format!(
1295            r#"{{"version":3,"sources":["x.js"],"names":[],"mappings":"{combined_mappings}"}}"#,
1296        );
1297        let sm = SourceMap::from_json(&json).unwrap();
1298        assert_eq!(sm.mapping_count(), 2);
1299        assert_eq!(sm.all_mappings()[0].source, NO_SOURCE);
1300        assert_eq!(sm.all_mappings()[1].source, 0);
1301    }
1302
1303    // ── 2. Source map parsing ───────────────────────────────────────
1304
1305    #[test]
1306    fn parse_missing_optional_fields() {
1307        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1308        let sm = SourceMap::from_json(json).unwrap();
1309        assert!(sm.file.is_none());
1310        assert!(sm.source_root.is_none());
1311        assert!(sm.sources_content.is_empty());
1312        assert!(sm.ignore_list.is_empty());
1313    }
1314
1315    #[test]
1316    fn parse_with_file_field() {
1317        let json =
1318            r#"{"version":3,"file":"output.js","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1319        let sm = SourceMap::from_json(json).unwrap();
1320        assert_eq!(sm.file.as_deref(), Some("output.js"));
1321    }
1322
1323    #[test]
1324    fn parse_null_entries_in_sources() {
1325        let json = r#"{"version":3,"sources":["a.js",null,"c.js"],"names":[],"mappings":"AAAA"}"#;
1326        let sm = SourceMap::from_json(json).unwrap();
1327        assert_eq!(sm.sources.len(), 3);
1328        assert_eq!(sm.sources[0], "a.js");
1329        assert_eq!(sm.sources[1], "");
1330        assert_eq!(sm.sources[2], "c.js");
1331    }
1332
1333    #[test]
1334    fn parse_null_entries_in_sources_with_source_root() {
1335        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js",null],"names":[],"mappings":"AAAA"}"#;
1336        let sm = SourceMap::from_json(json).unwrap();
1337        assert_eq!(sm.sources[0], "lib/a.js");
1338        assert_eq!(sm.sources[1], "");
1339    }
1340
1341    #[test]
1342    fn parse_empty_names_array() {
1343        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1344        let sm = SourceMap::from_json(json).unwrap();
1345        assert!(sm.names.is_empty());
1346    }
1347
1348    #[test]
1349    fn parse_invalid_json() {
1350        let result = SourceMap::from_json("not valid json");
1351        assert!(result.is_err());
1352        assert!(matches!(result.unwrap_err(), ParseError::Json(_)));
1353    }
1354
1355    #[test]
1356    fn parse_json_missing_version() {
1357        let result = SourceMap::from_json(r#"{"sources":[],"names":[],"mappings":""}"#);
1358        assert!(result.is_err());
1359    }
1360
1361    #[test]
1362    fn parse_multiple_sources_overlapping_original_positions() {
1363        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10], vec![10, 1, 5, 10]]];
1364        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1365        let sm = SourceMap::from_json(&json).unwrap();
1366
1367        let loc0 = sm.original_position_for(0, 0).unwrap();
1368        assert_eq!(loc0.source, 0);
1369        assert_eq!(sm.source(loc0.source), "a.js");
1370
1371        let loc1 = sm.original_position_for(0, 10).unwrap();
1372        assert_eq!(loc1.source, 1);
1373        assert_eq!(sm.source(loc1.source), "b.js");
1374
1375        assert_eq!(loc0.line, loc1.line);
1376        assert_eq!(loc0.column, loc1.column);
1377    }
1378
1379    #[test]
1380    fn parse_sources_content_with_null_entries() {
1381        let json = r#"{"version":3,"sources":["a.js","b.js"],"sourcesContent":["content a",null],"names":[],"mappings":"AAAA"}"#;
1382        let sm = SourceMap::from_json(json).unwrap();
1383        assert_eq!(sm.sources_content.len(), 2);
1384        assert_eq!(sm.sources_content[0], Some("content a".to_string()));
1385        assert_eq!(sm.sources_content[1], None);
1386    }
1387
1388    #[test]
1389    fn parse_empty_sources_and_names() {
1390        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1391        let sm = SourceMap::from_json(json).unwrap();
1392        assert!(sm.sources.is_empty());
1393        assert!(sm.names.is_empty());
1394        assert_eq!(sm.mapping_count(), 0);
1395    }
1396
1397    // ── 3. Position lookups ─────────────────────────────────────────
1398
1399    #[test]
1400    fn lookup_exact_match() {
1401        let mappings_data = vec![vec![
1402            vec![0_i64, 0, 10, 20],
1403            vec![5, 0, 10, 25],
1404            vec![15, 0, 11, 0],
1405        ]];
1406        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1407        let sm = SourceMap::from_json(&json).unwrap();
1408
1409        let loc = sm.original_position_for(0, 5).unwrap();
1410        assert_eq!(loc.line, 10);
1411        assert_eq!(loc.column, 25);
1412    }
1413
1414    #[test]
1415    fn lookup_before_first_segment() {
1416        let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
1417        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1418        let sm = SourceMap::from_json(&json).unwrap();
1419
1420        assert!(sm.original_position_for(0, 0).is_none());
1421        assert!(sm.original_position_for(0, 4).is_none());
1422    }
1423
1424    #[test]
1425    fn lookup_between_segments() {
1426        let mappings_data = vec![vec![
1427            vec![0_i64, 0, 1, 0],
1428            vec![10, 0, 2, 0],
1429            vec![20, 0, 3, 0],
1430        ]];
1431        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1432        let sm = SourceMap::from_json(&json).unwrap();
1433
1434        let loc = sm.original_position_for(0, 7).unwrap();
1435        assert_eq!(loc.line, 1);
1436        assert_eq!(loc.column, 0);
1437
1438        let loc = sm.original_position_for(0, 15).unwrap();
1439        assert_eq!(loc.line, 2);
1440        assert_eq!(loc.column, 0);
1441    }
1442
1443    #[test]
1444    fn lookup_after_last_segment() {
1445        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 1, 5]]];
1446        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1447        let sm = SourceMap::from_json(&json).unwrap();
1448
1449        let loc = sm.original_position_for(0, 100).unwrap();
1450        assert_eq!(loc.line, 1);
1451        assert_eq!(loc.column, 5);
1452    }
1453
1454    #[test]
1455    fn lookup_empty_lines_no_mappings() {
1456        let mappings_data = vec![
1457            vec![vec![0_i64, 0, 0, 0]],
1458            vec![],
1459            vec![vec![0_i64, 0, 2, 0]],
1460        ];
1461        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1462        let sm = SourceMap::from_json(&json).unwrap();
1463
1464        assert!(sm.original_position_for(1, 0).is_none());
1465        assert!(sm.original_position_for(1, 10).is_none());
1466        assert!(sm.original_position_for(0, 0).is_some());
1467        assert!(sm.original_position_for(2, 0).is_some());
1468    }
1469
1470    #[test]
1471    fn lookup_line_with_single_mapping() {
1472        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1473        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1474        let sm = SourceMap::from_json(&json).unwrap();
1475
1476        let loc = sm.original_position_for(0, 0).unwrap();
1477        assert_eq!(loc.line, 0);
1478        assert_eq!(loc.column, 0);
1479
1480        let loc = sm.original_position_for(0, 50).unwrap();
1481        assert_eq!(loc.line, 0);
1482        assert_eq!(loc.column, 0);
1483    }
1484
1485    #[test]
1486    fn lookup_column_0_vs_column_nonzero() {
1487        let mappings_data = vec![vec![vec![0_i64, 0, 10, 0], vec![8, 0, 20, 5]]];
1488        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1489        let sm = SourceMap::from_json(&json).unwrap();
1490
1491        let loc0 = sm.original_position_for(0, 0).unwrap();
1492        assert_eq!(loc0.line, 10);
1493        assert_eq!(loc0.column, 0);
1494
1495        let loc8 = sm.original_position_for(0, 8).unwrap();
1496        assert_eq!(loc8.line, 20);
1497        assert_eq!(loc8.column, 5);
1498
1499        let loc4 = sm.original_position_for(0, 4).unwrap();
1500        assert_eq!(loc4.line, 10);
1501    }
1502
1503    #[test]
1504    fn lookup_beyond_last_line() {
1505        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1506        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1507        let sm = SourceMap::from_json(&json).unwrap();
1508
1509        assert!(sm.original_position_for(1, 0).is_none());
1510        assert!(sm.original_position_for(100, 0).is_none());
1511    }
1512
1513    #[test]
1514    fn lookup_single_field_returns_none() {
1515        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A"}"#;
1516        let sm = SourceMap::from_json(json).unwrap();
1517        assert_eq!(sm.mapping_count(), 1);
1518        assert!(sm.original_position_for(0, 0).is_none());
1519    }
1520
1521    // ── 4. Reverse lookups (generated_position_for) ─────────────────
1522
1523    #[test]
1524    fn reverse_lookup_exact_match() {
1525        let mappings_data = vec![
1526            vec![vec![0_i64, 0, 0, 0]],
1527            vec![vec![4, 0, 1, 0], vec![10, 0, 1, 8]],
1528            vec![vec![0, 0, 2, 0]],
1529        ];
1530        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1531        let sm = SourceMap::from_json(&json).unwrap();
1532
1533        let loc = sm.generated_position_for("main.js", 1, 8).unwrap();
1534        assert_eq!(loc.line, 1);
1535        assert_eq!(loc.column, 10);
1536    }
1537
1538    #[test]
1539    fn reverse_lookup_no_match() {
1540        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10]]];
1541        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1542        let sm = SourceMap::from_json(&json).unwrap();
1543
1544        assert!(sm.generated_position_for("main.js", 99, 0).is_none());
1545    }
1546
1547    #[test]
1548    fn reverse_lookup_unknown_source() {
1549        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1550        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1551        let sm = SourceMap::from_json(&json).unwrap();
1552
1553        assert!(sm.generated_position_for("unknown.js", 0, 0).is_none());
1554    }
1555
1556    #[test]
1557    fn reverse_lookup_multiple_mappings_same_original() {
1558        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]], vec![vec![20, 0, 5, 10]]];
1559        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1560        let sm = SourceMap::from_json(&json).unwrap();
1561
1562        let loc = sm.generated_position_for("src.js", 5, 10);
1563        assert!(loc.is_some());
1564        let loc = loc.unwrap();
1565        assert!(
1566            (loc.line == 0 && loc.column == 0) || (loc.line == 1 && loc.column == 20),
1567            "Expected (0,0) or (1,20), got ({},{})",
1568            loc.line,
1569            loc.column
1570        );
1571    }
1572
1573    #[test]
1574    fn reverse_lookup_with_multiple_sources() {
1575        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 1, 0, 0]]];
1576        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1577        let sm = SourceMap::from_json(&json).unwrap();
1578
1579        let loc_a = sm.generated_position_for("a.js", 0, 0).unwrap();
1580        assert_eq!(loc_a.line, 0);
1581        assert_eq!(loc_a.column, 0);
1582
1583        let loc_b = sm.generated_position_for("b.js", 0, 0).unwrap();
1584        assert_eq!(loc_b.line, 0);
1585        assert_eq!(loc_b.column, 10);
1586    }
1587
1588    #[test]
1589    fn reverse_lookup_skips_single_field_segments() {
1590        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,KAAAA"}"#;
1591        let sm = SourceMap::from_json(json).unwrap();
1592
1593        let loc = sm.generated_position_for("a.js", 0, 0).unwrap();
1594        assert_eq!(loc.line, 0);
1595        assert_eq!(loc.column, 5);
1596    }
1597
1598    #[test]
1599    fn reverse_lookup_finds_each_original_line() {
1600        let mappings_data = vec![
1601            vec![vec![0_i64, 0, 0, 0]],
1602            vec![vec![0, 0, 1, 0]],
1603            vec![vec![0, 0, 2, 0]],
1604            vec![vec![0, 0, 3, 0]],
1605        ];
1606        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1607        let sm = SourceMap::from_json(&json).unwrap();
1608
1609        for orig_line in 0..4 {
1610            let loc = sm.generated_position_for("x.js", orig_line, 0).unwrap();
1611            assert_eq!(
1612                loc.line, orig_line,
1613                "reverse lookup for orig line {orig_line}"
1614            );
1615            assert_eq!(loc.column, 0);
1616        }
1617    }
1618
1619    // ── 5. ignoreList ───────────────────────────────────────────────
1620
1621    #[test]
1622    fn parse_with_ignore_list_multiple() {
1623        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js","vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[1,2]}"#;
1624        let sm = SourceMap::from_json(json).unwrap();
1625        assert_eq!(sm.ignore_list, vec![1, 2]);
1626    }
1627
1628    #[test]
1629    fn parse_with_empty_ignore_list() {
1630        let json =
1631            r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA","ignoreList":[]}"#;
1632        let sm = SourceMap::from_json(json).unwrap();
1633        assert!(sm.ignore_list.is_empty());
1634    }
1635
1636    #[test]
1637    fn parse_without_ignore_list_field() {
1638        let json = r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA"}"#;
1639        let sm = SourceMap::from_json(json).unwrap();
1640        assert!(sm.ignore_list.is_empty());
1641    }
1642
1643    // ── Additional edge case tests ──────────────────────────────────
1644
1645    #[test]
1646    fn source_index_lookup() {
1647        let json = r#"{"version":3,"sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA"}"#;
1648        let sm = SourceMap::from_json(json).unwrap();
1649        assert_eq!(sm.source_index("a.js"), Some(0));
1650        assert_eq!(sm.source_index("b.js"), Some(1));
1651        assert_eq!(sm.source_index("c.js"), Some(2));
1652        assert_eq!(sm.source_index("d.js"), None);
1653    }
1654
1655    #[test]
1656    fn all_mappings_returns_complete_list() {
1657        let mappings_data = vec![
1658            vec![vec![0_i64, 0, 0, 0], vec![5, 0, 0, 5]],
1659            vec![vec![0, 0, 1, 0]],
1660        ];
1661        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1662        let sm = SourceMap::from_json(&json).unwrap();
1663        assert_eq!(sm.all_mappings().len(), 3);
1664        assert_eq!(sm.mapping_count(), 3);
1665    }
1666
1667    #[test]
1668    fn line_count_matches_decoded_lines() {
1669        let mappings_data = vec![
1670            vec![vec![0_i64, 0, 0, 0]],
1671            vec![],
1672            vec![vec![0_i64, 0, 2, 0]],
1673            vec![],
1674            vec![],
1675        ];
1676        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1677        let sm = SourceMap::from_json(&json).unwrap();
1678        assert_eq!(sm.line_count(), 5);
1679    }
1680
1681    #[test]
1682    fn parse_error_display() {
1683        let err = ParseError::InvalidVersion(5);
1684        assert_eq!(format!("{err}"), "unsupported source map version: 5");
1685
1686        let json_err = SourceMap::from_json("{}").unwrap_err();
1687        let display = format!("{json_err}");
1688        assert!(display.contains("JSON parse error") || display.contains("missing field"));
1689    }
1690
1691    #[test]
1692    fn original_position_name_none_for_four_field() {
1693        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]]];
1694        let json = build_sourcemap_json(&["a.js"], &["unused_name"], &mappings_data);
1695        let sm = SourceMap::from_json(&json).unwrap();
1696
1697        let loc = sm.original_position_for(0, 0).unwrap();
1698        assert!(loc.name.is_none());
1699    }
1700
1701    #[test]
1702    fn forward_and_reverse_roundtrip_comprehensive() {
1703        let mappings_data = vec![
1704            vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10], vec![20, 1, 5, 0]],
1705            vec![vec![0, 0, 1, 0], vec![5, 1, 6, 3]],
1706            vec![vec![0, 0, 2, 0]],
1707        ];
1708        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1709        let sm = SourceMap::from_json(&json).unwrap();
1710
1711        for m in sm.all_mappings() {
1712            if m.source == NO_SOURCE {
1713                continue;
1714            }
1715            let source_name = sm.source(m.source);
1716
1717            let orig = sm
1718                .original_position_for(m.generated_line, m.generated_column)
1719                .unwrap();
1720            assert_eq!(orig.source, m.source);
1721            assert_eq!(orig.line, m.original_line);
1722            assert_eq!(orig.column, m.original_column);
1723
1724            let gen_loc = sm
1725                .generated_position_for(source_name, m.original_line, m.original_column)
1726                .unwrap();
1727            assert_eq!(gen_loc.line, m.generated_line);
1728            assert_eq!(gen_loc.column, m.generated_column);
1729        }
1730    }
1731
1732    // ── 6. Comprehensive edge case tests ────────────────────────────
1733
1734    // -- sourceRoot edge cases --
1735
1736    #[test]
1737    fn source_root_with_multiple_sources() {
1738        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA,KACA,KACA"}"#;
1739        let sm = SourceMap::from_json(json).unwrap();
1740        assert_eq!(sm.sources, vec!["lib/a.js", "lib/b.js", "lib/c.js"]);
1741    }
1742
1743    #[test]
1744    fn source_root_empty_string() {
1745        let json =
1746            r#"{"version":3,"sourceRoot":"","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1747        let sm = SourceMap::from_json(json).unwrap();
1748        assert_eq!(sm.sources, vec!["a.js"]);
1749    }
1750
1751    #[test]
1752    fn source_root_preserved_in_to_json() {
1753        let json =
1754            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1755        let sm = SourceMap::from_json(json).unwrap();
1756        let output = sm.to_json();
1757        assert!(output.contains(r#""sourceRoot":"src/""#));
1758    }
1759
1760    #[test]
1761    fn source_root_reverse_lookup_uses_prefixed_name() {
1762        let json =
1763            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1764        let sm = SourceMap::from_json(json).unwrap();
1765        // Must use the prefixed name for reverse lookups
1766        assert!(sm.generated_position_for("src/a.js", 0, 0).is_some());
1767        assert!(sm.generated_position_for("a.js", 0, 0).is_none());
1768    }
1769
1770    #[test]
1771    fn source_root_with_trailing_slash() {
1772        let json =
1773            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1774        let sm = SourceMap::from_json(json).unwrap();
1775        assert_eq!(sm.sources[0], "src/a.js");
1776    }
1777
1778    #[test]
1779    fn source_root_without_trailing_slash() {
1780        let json =
1781            r#"{"version":3,"sourceRoot":"src","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1782        let sm = SourceMap::from_json(json).unwrap();
1783        assert_eq!(sm.sources[0], "srca.js"); // No auto-slash — sourceRoot is raw prefix
1784    }
1785
1786    // -- JSON/parsing error cases --
1787
1788    #[test]
1789    fn parse_empty_json_object() {
1790        // {} has no version field
1791        let result = SourceMap::from_json("{}");
1792        assert!(result.is_err());
1793    }
1794
1795    #[test]
1796    fn parse_version_0() {
1797        let json = r#"{"version":0,"sources":[],"names":[],"mappings":""}"#;
1798        assert!(matches!(
1799            SourceMap::from_json(json).unwrap_err(),
1800            ParseError::InvalidVersion(0)
1801        ));
1802    }
1803
1804    #[test]
1805    fn parse_version_4() {
1806        let json = r#"{"version":4,"sources":[],"names":[],"mappings":""}"#;
1807        assert!(matches!(
1808            SourceMap::from_json(json).unwrap_err(),
1809            ParseError::InvalidVersion(4)
1810        ));
1811    }
1812
1813    #[test]
1814    fn parse_extra_unknown_fields_ignored() {
1815        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom_field":true,"x_debug":{"foo":"bar"}}"#;
1816        let sm = SourceMap::from_json(json).unwrap();
1817        assert_eq!(sm.mapping_count(), 1);
1818    }
1819
1820    #[test]
1821    fn parse_vlq_error_propagated() {
1822        // '!' is not valid base64 — should surface as VLQ error
1823        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AA!A"}"#;
1824        let result = SourceMap::from_json(json);
1825        assert!(result.is_err());
1826        assert!(matches!(result.unwrap_err(), ParseError::Vlq(_)));
1827    }
1828
1829    #[test]
1830    fn parse_truncated_vlq_error() {
1831        // 'g' has continuation bit set — truncated VLQ
1832        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"g"}"#;
1833        let result = SourceMap::from_json(json);
1834        assert!(result.is_err());
1835    }
1836
1837    // -- to_json edge cases --
1838
1839    #[test]
1840    fn to_json_produces_valid_json() {
1841        let json = r#"{"version":3,"file":"out.js","sourceRoot":"src/","sources":["a.ts","b.ts"],"sourcesContent":["const x = 1;\nconst y = \"hello\";",null],"names":["x","y"],"mappings":"AAAAA,KACAC;AACA","ignoreList":[1]}"#;
1842        let sm = SourceMap::from_json(json).unwrap();
1843        let output = sm.to_json();
1844        // Must be valid JSON that serde can parse
1845        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
1846    }
1847
1848    #[test]
1849    fn to_json_escapes_special_chars() {
1850        let json = r#"{"version":3,"sources":["path/with\"quotes.js"],"sourcesContent":["line1\nline2\ttab\\backslash"],"names":[],"mappings":"AAAA"}"#;
1851        let sm = SourceMap::from_json(json).unwrap();
1852        let output = sm.to_json();
1853        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
1854        let sm2 = SourceMap::from_json(&output).unwrap();
1855        assert_eq!(
1856            sm2.sources_content[0].as_deref(),
1857            Some("line1\nline2\ttab\\backslash")
1858        );
1859    }
1860
1861    #[test]
1862    fn to_json_empty_map() {
1863        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1864        let sm = SourceMap::from_json(json).unwrap();
1865        let output = sm.to_json();
1866        let sm2 = SourceMap::from_json(&output).unwrap();
1867        assert_eq!(sm2.mapping_count(), 0);
1868        assert!(sm2.sources.is_empty());
1869    }
1870
1871    #[test]
1872    fn to_json_roundtrip_with_names() {
1873        let mappings_data = vec![vec![
1874            vec![0_i64, 0, 0, 0, 0],
1875            vec![10, 0, 0, 10, 1],
1876            vec![20, 0, 1, 0, 2],
1877        ]];
1878        let json = build_sourcemap_json(&["src.js"], &["foo", "bar", "baz"], &mappings_data);
1879        let sm = SourceMap::from_json(&json).unwrap();
1880        let output = sm.to_json();
1881        let sm2 = SourceMap::from_json(&output).unwrap();
1882
1883        for m in sm2.all_mappings() {
1884            if m.source != NO_SOURCE && m.name != NO_NAME {
1885                let loc = sm2
1886                    .original_position_for(m.generated_line, m.generated_column)
1887                    .unwrap();
1888                assert!(loc.name.is_some());
1889            }
1890        }
1891    }
1892
1893    // -- Indexed source map edge cases --
1894
1895    #[test]
1896    fn indexed_source_map_column_offset() {
1897        let json = r#"{
1898            "version": 3,
1899            "sections": [
1900                {
1901                    "offset": {"line": 0, "column": 10},
1902                    "map": {
1903                        "version": 3,
1904                        "sources": ["a.js"],
1905                        "names": [],
1906                        "mappings": "AAAA"
1907                    }
1908                }
1909            ]
1910        }"#;
1911        let sm = SourceMap::from_json(json).unwrap();
1912        // Mapping at col 0 in section should be offset to col 10 (first line only)
1913        let loc = sm.original_position_for(0, 10).unwrap();
1914        assert_eq!(loc.line, 0);
1915        assert_eq!(loc.column, 0);
1916        // Before the offset should have no mapping
1917        assert!(sm.original_position_for(0, 0).is_none());
1918    }
1919
1920    #[test]
1921    fn indexed_source_map_column_offset_only_first_line() {
1922        // Column offset only applies to the first line of a section
1923        let json = r#"{
1924            "version": 3,
1925            "sections": [
1926                {
1927                    "offset": {"line": 0, "column": 20},
1928                    "map": {
1929                        "version": 3,
1930                        "sources": ["a.js"],
1931                        "names": [],
1932                        "mappings": "AAAA;AAAA"
1933                    }
1934                }
1935            ]
1936        }"#;
1937        let sm = SourceMap::from_json(json).unwrap();
1938        // Line 0: column offset applies
1939        let loc = sm.original_position_for(0, 20).unwrap();
1940        assert_eq!(loc.column, 0);
1941        // Line 1: column offset does NOT apply
1942        let loc = sm.original_position_for(1, 0).unwrap();
1943        assert_eq!(loc.column, 0);
1944    }
1945
1946    #[test]
1947    fn indexed_source_map_empty_section() {
1948        let json = r#"{
1949            "version": 3,
1950            "sections": [
1951                {
1952                    "offset": {"line": 0, "column": 0},
1953                    "map": {
1954                        "version": 3,
1955                        "sources": [],
1956                        "names": [],
1957                        "mappings": ""
1958                    }
1959                },
1960                {
1961                    "offset": {"line": 5, "column": 0},
1962                    "map": {
1963                        "version": 3,
1964                        "sources": ["b.js"],
1965                        "names": [],
1966                        "mappings": "AAAA"
1967                    }
1968                }
1969            ]
1970        }"#;
1971        let sm = SourceMap::from_json(json).unwrap();
1972        assert_eq!(sm.sources.len(), 1);
1973        let loc = sm.original_position_for(5, 0).unwrap();
1974        assert_eq!(sm.source(loc.source), "b.js");
1975    }
1976
1977    #[test]
1978    fn indexed_source_map_with_sources_content() {
1979        let json = r#"{
1980            "version": 3,
1981            "sections": [
1982                {
1983                    "offset": {"line": 0, "column": 0},
1984                    "map": {
1985                        "version": 3,
1986                        "sources": ["a.js"],
1987                        "sourcesContent": ["var a = 1;"],
1988                        "names": [],
1989                        "mappings": "AAAA"
1990                    }
1991                },
1992                {
1993                    "offset": {"line": 5, "column": 0},
1994                    "map": {
1995                        "version": 3,
1996                        "sources": ["b.js"],
1997                        "sourcesContent": ["var b = 2;"],
1998                        "names": [],
1999                        "mappings": "AAAA"
2000                    }
2001                }
2002            ]
2003        }"#;
2004        let sm = SourceMap::from_json(json).unwrap();
2005        assert_eq!(sm.sources_content.len(), 2);
2006        assert_eq!(sm.sources_content[0], Some("var a = 1;".to_string()));
2007        assert_eq!(sm.sources_content[1], Some("var b = 2;".to_string()));
2008    }
2009
2010    #[test]
2011    fn indexed_source_map_with_ignore_list() {
2012        let json = r#"{
2013            "version": 3,
2014            "sections": [
2015                {
2016                    "offset": {"line": 0, "column": 0},
2017                    "map": {
2018                        "version": 3,
2019                        "sources": ["app.js", "vendor.js"],
2020                        "names": [],
2021                        "mappings": "AAAA",
2022                        "ignoreList": [1]
2023                    }
2024                }
2025            ]
2026        }"#;
2027        let sm = SourceMap::from_json(json).unwrap();
2028        assert!(!sm.ignore_list.is_empty());
2029    }
2030
2031    // -- Boundary conditions --
2032
2033    #[test]
2034    fn lookup_max_column_on_line() {
2035        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
2036        let json = build_sourcemap_json(&["a.js"], &[], &mappings_data);
2037        let sm = SourceMap::from_json(&json).unwrap();
2038        // Very large column — should snap to the last mapping on line
2039        let loc = sm.original_position_for(0, u32::MAX - 1).unwrap();
2040        assert_eq!(loc.line, 0);
2041        assert_eq!(loc.column, 0);
2042    }
2043
2044    #[test]
2045    fn mappings_for_line_beyond_end() {
2046        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
2047        let sm = SourceMap::from_json(json).unwrap();
2048        assert!(sm.mappings_for_line(u32::MAX).is_empty());
2049    }
2050
2051    #[test]
2052    fn source_with_unicode_path() {
2053        let json =
2054            r#"{"version":3,"sources":["src/日本語.ts"],"names":["変数"],"mappings":"AAAAA"}"#;
2055        let sm = SourceMap::from_json(json).unwrap();
2056        assert_eq!(sm.sources[0], "src/日本語.ts");
2057        assert_eq!(sm.names[0], "変数");
2058        let loc = sm.original_position_for(0, 0).unwrap();
2059        assert_eq!(sm.source(loc.source), "src/日本語.ts");
2060        assert_eq!(sm.name(loc.name.unwrap()), "変数");
2061    }
2062
2063    #[test]
2064    fn to_json_roundtrip_unicode_sources() {
2065        let json = r#"{"version":3,"sources":["src/日本語.ts"],"sourcesContent":["const 変数 = 1;"],"names":["変数"],"mappings":"AAAAA"}"#;
2066        let sm = SourceMap::from_json(json).unwrap();
2067        let output = sm.to_json();
2068        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
2069        let sm2 = SourceMap::from_json(&output).unwrap();
2070        assert_eq!(sm2.sources[0], "src/日本語.ts");
2071        assert_eq!(sm2.sources_content[0], Some("const 変数 = 1;".to_string()));
2072    }
2073
2074    #[test]
2075    fn many_sources_lookup() {
2076        // 100 sources, verify source_index works for all
2077        let sources: Vec<String> = (0..100).map(|i| format!("src/file{i}.js")).collect();
2078        let source_strs: Vec<&str> = sources.iter().map(|s| s.as_str()).collect();
2079        let mappings_data = vec![
2080            sources
2081                .iter()
2082                .enumerate()
2083                .map(|(i, _)| vec![(i * 10) as i64, i as i64, 0, 0])
2084                .collect::<Vec<_>>(),
2085        ];
2086        let json = build_sourcemap_json(&source_strs, &[], &mappings_data);
2087        let sm = SourceMap::from_json(&json).unwrap();
2088
2089        for (i, src) in sources.iter().enumerate() {
2090            assert_eq!(sm.source_index(src), Some(i as u32));
2091        }
2092    }
2093
2094    #[test]
2095    fn clone_sourcemap() {
2096        let json = r#"{"version":3,"sources":["a.js"],"names":["x"],"mappings":"AAAAA"}"#;
2097        let sm = SourceMap::from_json(json).unwrap();
2098        let sm2 = sm.clone();
2099        assert_eq!(sm2.sources, sm.sources);
2100        assert_eq!(sm2.mapping_count(), sm.mapping_count());
2101        let loc = sm2.original_position_for(0, 0).unwrap();
2102        assert_eq!(sm2.source(loc.source), "a.js");
2103    }
2104
2105    #[test]
2106    fn parse_debug_id() {
2107        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
2108        let sm = SourceMap::from_json(json).unwrap();
2109        assert_eq!(
2110            sm.debug_id.as_deref(),
2111            Some("85314830-023f-4cf1-a267-535f4e37bb17")
2112        );
2113    }
2114
2115    #[test]
2116    fn parse_debug_id_snake_case() {
2117        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debug_id":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
2118        let sm = SourceMap::from_json(json).unwrap();
2119        assert_eq!(
2120            sm.debug_id.as_deref(),
2121            Some("85314830-023f-4cf1-a267-535f4e37bb17")
2122        );
2123    }
2124
2125    #[test]
2126    fn parse_no_debug_id() {
2127        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
2128        let sm = SourceMap::from_json(json).unwrap();
2129        assert_eq!(sm.debug_id, None);
2130    }
2131
2132    #[test]
2133    fn debug_id_roundtrip() {
2134        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
2135        let sm = SourceMap::from_json(json).unwrap();
2136        let output = sm.to_json();
2137        assert!(output.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
2138        let sm2 = SourceMap::from_json(&output).unwrap();
2139        assert_eq!(sm.debug_id, sm2.debug_id);
2140    }
2141
2142    #[test]
2143    fn debug_id_not_in_json_when_absent() {
2144        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
2145        let sm = SourceMap::from_json(json).unwrap();
2146        let output = sm.to_json();
2147        assert!(!output.contains("debugId"));
2148    }
2149
2150    /// Generate a test source map JSON with realistic structure.
2151    fn generate_test_sourcemap(lines: usize, segs_per_line: usize, num_sources: usize) -> String {
2152        let sources: Vec<String> = (0..num_sources)
2153            .map(|i| format!("src/file{i}.js"))
2154            .collect();
2155        let names: Vec<String> = (0..20).map(|i| format!("var{i}")).collect();
2156
2157        let mut mappings_parts = Vec::with_capacity(lines);
2158        let mut gen_col;
2159        let mut src: i64 = 0;
2160        let mut src_line: i64 = 0;
2161        let mut src_col: i64;
2162        let mut name: i64 = 0;
2163
2164        for _ in 0..lines {
2165            gen_col = 0i64;
2166            let mut line_parts = Vec::with_capacity(segs_per_line);
2167
2168            for s in 0..segs_per_line {
2169                let gc_delta = 2 + (s as i64 * 3) % 20;
2170                gen_col += gc_delta;
2171
2172                let src_delta = if s % 7 == 0 { 1 } else { 0 };
2173                src = (src + src_delta) % num_sources as i64;
2174
2175                src_line += 1;
2176                src_col = (s as i64 * 5 + 1) % 30;
2177
2178                let has_name = s % 4 == 0;
2179                if has_name {
2180                    name = (name + 1) % names.len() as i64;
2181                }
2182
2183                // Build segment using codec encode
2184                let segment = if has_name {
2185                    vec![gen_col, src, src_line, src_col, name]
2186                } else {
2187                    vec![gen_col, src, src_line, src_col]
2188                };
2189
2190                line_parts.push(segment);
2191            }
2192
2193            mappings_parts.push(line_parts);
2194        }
2195
2196        let encoded = srcmap_codec::encode(&mappings_parts);
2197
2198        format!(
2199            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
2200            sources
2201                .iter()
2202                .map(|s| format!("\"{s}\""))
2203                .collect::<Vec<_>>()
2204                .join(","),
2205            names
2206                .iter()
2207                .map(|n| format!("\"{n}\""))
2208                .collect::<Vec<_>>()
2209                .join(","),
2210            encoded,
2211        )
2212    }
2213}