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::{Cell, OnceCell, RefCell};
27use std::collections::HashMap;
28use std::fmt;
29use std::io;
30
31use serde::Deserialize;
32use srcmap_codec::{DecodeError, vlq_encode_unsigned};
33use srcmap_scopes::ScopeInfo;
34
35pub mod js_identifiers;
36pub mod source_view;
37pub mod utils;
38
39pub use source_view::SourceView;
40
41// ── Constants ──────────────────────────────────────────────────────
42
43const NO_SOURCE: u32 = u32::MAX;
44const NO_NAME: u32 = u32::MAX;
45
46// ── Public types ───────────────────────────────────────────────────
47
48/// A single decoded mapping entry. Compact at 28 bytes (6 × u32 + bool with padding).
49///
50/// Maps a position in the generated output to an optional position in an
51/// original source file. Stored contiguously in a `Vec<Mapping>` sorted by
52/// `(generated_line, generated_column)` for cache-friendly binary search.
53#[derive(Debug, Clone, Copy)]
54pub struct Mapping {
55    /// 0-based line in the generated output.
56    pub generated_line: u32,
57    /// 0-based column in the generated output.
58    pub generated_column: u32,
59    /// Index into `SourceMap::sources`. `u32::MAX` if this mapping has no source.
60    pub source: u32,
61    /// 0-based line in the original source (only meaningful when `source != u32::MAX`).
62    pub original_line: u32,
63    /// 0-based column in the original source (only meaningful when `source != u32::MAX`).
64    pub original_column: u32,
65    /// Index into `SourceMap::names`. `u32::MAX` if this mapping has no name.
66    pub name: u32,
67    /// Whether this mapping is a range mapping (ECMA-426).
68    pub is_range_mapping: bool,
69}
70
71/// Result of an [`SourceMap::original_position_for`] lookup.
72///
73/// All indices are 0-based. Use [`SourceMap::source`] and [`SourceMap::name`]
74/// to resolve the `source` and `name` indices to strings.
75#[derive(Debug, Clone)]
76pub struct OriginalLocation {
77    /// Index into `SourceMap::sources`.
78    pub source: u32,
79    /// 0-based line in the original source.
80    pub line: u32,
81    /// 0-based column in the original source.
82    pub column: u32,
83    /// Index into `SourceMap::names`, if the mapping has a name.
84    pub name: Option<u32>,
85}
86
87/// Result of a [`SourceMap::generated_position_for`] lookup.
88///
89/// All values are 0-based.
90#[derive(Debug, Clone)]
91pub struct GeneratedLocation {
92    /// 0-based line in the generated output.
93    pub line: u32,
94    /// 0-based column in the generated output.
95    pub column: u32,
96}
97
98/// Search bias for position lookups.
99///
100/// Controls how non-exact matches are resolved during binary search:
101/// - `GreatestLowerBound` (default): find the closest mapping at or before the position
102/// - `LeastUpperBound`: find the closest mapping at or after the position
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
104pub enum Bias {
105    /// Return the closest position at or before the requested position (default).
106    #[default]
107    GreatestLowerBound,
108    /// Return the closest position at or after the requested position.
109    LeastUpperBound,
110}
111
112/// A mapped range: original start/end positions for a generated range.
113///
114/// Returned by [`SourceMap::map_range`]. Both endpoints must resolve to the
115/// same source file.
116#[derive(Debug, Clone)]
117pub struct MappedRange {
118    /// Index into `SourceMap::sources`.
119    pub source: u32,
120    /// 0-based start line in the original source.
121    pub original_start_line: u32,
122    /// 0-based start column in the original source.
123    pub original_start_column: u32,
124    /// 0-based end line in the original source.
125    pub original_end_line: u32,
126    /// 0-based end column in the original source.
127    pub original_end_column: u32,
128}
129
130/// Errors that can occur during source map parsing.
131#[derive(Debug)]
132pub enum ParseError {
133    /// The JSON could not be deserialized.
134    Json(serde_json::Error),
135    /// The VLQ mappings string is malformed.
136    Vlq(DecodeError),
137    /// The `version` field is not `3`.
138    InvalidVersion(u32),
139    /// The ECMA-426 scopes data could not be decoded.
140    Scopes(srcmap_scopes::ScopesError),
141    /// A section map in an indexed source map is itself an indexed map (not allowed per ECMA-426).
142    NestedIndexMap,
143    /// Sections in an indexed source map are not in ascending (line, column) order.
144    SectionsNotOrdered,
145    /// The data URL is malformed (not a valid `data:application/json` URL).
146    InvalidDataUrl,
147}
148
149impl fmt::Display for ParseError {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        match self {
152            Self::Json(e) => write!(f, "JSON parse error: {e}"),
153            Self::Vlq(e) => write!(f, "VLQ decode error: {e}"),
154            Self::InvalidVersion(v) => write!(f, "unsupported source map version: {v}"),
155            Self::Scopes(e) => write!(f, "scopes decode error: {e}"),
156            Self::NestedIndexMap => write!(f, "section map must not be an indexed source map"),
157            Self::SectionsNotOrdered => {
158                write!(f, "sections must be in ascending (line, column) order")
159            }
160            Self::InvalidDataUrl => write!(f, "malformed data URL"),
161        }
162    }
163}
164
165impl std::error::Error for ParseError {
166    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
167        match self {
168            Self::Json(e) => Some(e),
169            Self::Vlq(e) => Some(e),
170            Self::Scopes(e) => Some(e),
171            Self::InvalidVersion(_)
172            | Self::NestedIndexMap
173            | Self::SectionsNotOrdered
174            | Self::InvalidDataUrl => None,
175        }
176    }
177}
178
179impl From<serde_json::Error> for ParseError {
180    fn from(e: serde_json::Error) -> Self {
181        Self::Json(e)
182    }
183}
184
185impl From<DecodeError> for ParseError {
186    fn from(e: DecodeError) -> Self {
187        Self::Vlq(e)
188    }
189}
190
191impl From<srcmap_scopes::ScopesError> for ParseError {
192    fn from(e: srcmap_scopes::ScopesError) -> Self {
193        Self::Scopes(e)
194    }
195}
196
197// ── Helpers ────────────────────────────────────────────────────────
198
199/// Resolve source filenames by applying `source_root` prefix and replacing `None` with empty string.
200pub fn resolve_sources(raw_sources: &[Option<String>], source_root: &str) -> Vec<String> {
201    raw_sources
202        .iter()
203        .map(|s| match s {
204            Some(s) if !source_root.is_empty() => format!("{source_root}{s}"),
205            Some(s) => s.clone(),
206            None => String::new(),
207        })
208        .collect()
209}
210
211/// Build a source filename -> index lookup map.
212fn build_source_map(sources: &[String]) -> HashMap<String, u32> {
213    sources.iter().enumerate().map(|(i, s)| (s.clone(), i as u32)).collect()
214}
215
216// ── Raw JSON structure ─────────────────────────────────────────────
217
218#[derive(Deserialize)]
219struct RawSourceMap<'a> {
220    version: u32,
221    #[serde(default)]
222    file: Option<String>,
223    #[serde(default, rename = "sourceRoot")]
224    source_root: Option<String>,
225    #[serde(default)]
226    sources: Vec<Option<String>>,
227    #[serde(default, rename = "sourcesContent")]
228    sources_content: Option<Vec<Option<String>>>,
229    #[serde(default)]
230    names: Vec<String>,
231    #[serde(default, borrow)]
232    mappings: &'a str,
233    #[serde(default, rename = "ignoreList")]
234    ignore_list: Option<Vec<u32>>,
235    /// Deprecated Chrome DevTools field, fallback for `ignoreList`.
236    #[serde(default, rename = "x_google_ignoreList")]
237    x_google_ignore_list: Option<Vec<u32>>,
238    /// Debug ID for associating generated files with source maps (ECMA-426).
239    /// Accepts both `debugId` (spec) and `debug_id` (Sentry compat).
240    #[serde(default, rename = "debugId", alias = "debug_id")]
241    debug_id: Option<String>,
242    /// Scopes and variables (ECMA-426 scopes proposal).
243    #[serde(default, borrow)]
244    scopes: Option<&'a str>,
245    /// Range mappings (ECMA-426).
246    #[serde(default, borrow, rename = "rangeMappings")]
247    range_mappings: Option<&'a str>,
248    /// Indexed source maps use `sections` instead of `mappings`.
249    #[serde(default)]
250    sections: Option<Vec<RawSection>>,
251    /// Catch-all for unknown extension fields (x_*).
252    #[serde(flatten)]
253    extensions: HashMap<String, serde_json::Value>,
254}
255
256/// A section in an indexed source map.
257#[derive(Deserialize)]
258struct RawSection {
259    offset: RawOffset,
260    map: Box<serde_json::value::RawValue>,
261}
262
263#[derive(Deserialize)]
264struct RawOffset {
265    line: u32,
266    column: u32,
267}
268
269/// Lightweight version that skips sourcesContent allocation.
270/// Used by WASM bindings where sourcesContent is kept JS-side.
271///
272/// Note: Indexed/sectioned source maps are detected via the `sections` field
273/// and must be rejected by callers (LazySourceMap does not support them).
274#[derive(Deserialize)]
275pub struct RawSourceMapLite<'a> {
276    pub version: u32,
277    #[serde(default)]
278    pub file: Option<String>,
279    #[serde(default, rename = "sourceRoot")]
280    pub source_root: Option<String>,
281    #[serde(default)]
282    pub sources: Vec<Option<String>>,
283    #[serde(default)]
284    pub names: Vec<String>,
285    #[serde(default, borrow)]
286    pub mappings: &'a str,
287    #[serde(default, rename = "ignoreList")]
288    pub ignore_list: Option<Vec<u32>>,
289    #[serde(default, rename = "x_google_ignoreList")]
290    pub x_google_ignore_list: Option<Vec<u32>>,
291    #[serde(default, rename = "debugId", alias = "debug_id")]
292    pub debug_id: Option<String>,
293    #[serde(default, borrow)]
294    pub scopes: Option<&'a str>,
295    #[serde(default, borrow, rename = "rangeMappings")]
296    pub range_mappings: Option<&'a str>,
297    /// Indexed source maps use `sections` instead of `mappings`.
298    /// Presence is checked to reject indexed maps in lazy parse paths.
299    #[serde(default)]
300    pub sections: Option<Vec<serde_json::Value>>,
301}
302
303// ── SourceMap ──────────────────────────────────────────────────────
304
305/// A fully-parsed source map with O(log n) position lookups.
306///
307/// Supports both regular and indexed (sectioned) source maps, `ignoreList`,
308/// `debugId`, scopes (ECMA-426), and extension fields. All positions are
309/// 0-based lines and columns.
310///
311/// # Construction
312///
313/// - [`SourceMap::from_json`] — parse from a JSON string (most common)
314/// - [`SourceMap::from_parts`] — build from pre-decoded components
315/// - [`SourceMap::from_vlq`] — parse from pre-extracted parts + raw VLQ string
316/// - [`SourceMap::from_json_lines`] — partial parse for a line range
317///
318/// # Lookups
319///
320/// - [`SourceMap::original_position_for`] — forward: generated → original
321/// - [`SourceMap::generated_position_for`] — reverse: original → generated (lazy index)
322/// - [`SourceMap::all_generated_positions_for`] — all reverse matches
323/// - [`SourceMap::map_range`] — map a generated range to its original range
324///
325/// For cases where you only need a few lookups and want to avoid decoding
326/// all mappings upfront, see [`LazySourceMap`].
327#[derive(Debug, Clone)]
328pub struct SourceMap {
329    pub file: Option<String>,
330    pub source_root: Option<String>,
331    pub sources: Vec<String>,
332    pub sources_content: Vec<Option<String>>,
333    pub names: Vec<String>,
334    pub ignore_list: Vec<u32>,
335    /// Extension fields (x_* keys) preserved for passthrough.
336    pub extensions: HashMap<String, serde_json::Value>,
337    /// Debug ID (UUID) for associating generated files with source maps (ECMA-426).
338    pub debug_id: Option<String>,
339    /// Decoded scope and variable information (ECMA-426 scopes proposal).
340    pub scopes: Option<ScopeInfo>,
341
342    /// Flat decoded mappings, ordered by (generated_line, generated_column).
343    mappings: Vec<Mapping>,
344
345    /// `line_offsets[i]` = index of first mapping on generated line `i`.
346    /// `line_offsets[line_count]` = mappings.len() (sentinel).
347    line_offsets: Vec<u32>,
348
349    /// Indices into `mappings`, sorted by (source, original_line, original_column).
350    /// Built lazily on first `generated_position_for` call.
351    reverse_index: OnceCell<Vec<u32>>,
352
353    /// Source filename → index for O(1) lookup by name.
354    source_map: HashMap<String, u32>,
355
356    /// Cached flag: true if any mapping has `is_range_mapping == true`.
357    has_range_mappings: bool,
358}
359
360impl SourceMap {
361    /// Parse a source map from a JSON string.
362    /// Supports both regular and indexed (sectioned) source maps.
363    pub fn from_json(json: &str) -> Result<Self, ParseError> {
364        Self::from_json_inner(json, true)
365    }
366
367    /// Parse a source map from JSON, skipping sourcesContent allocation.
368    /// Useful for WASM bindings where sourcesContent is kept on the JS side.
369    /// The resulting SourceMap has an empty `sources_content` vec.
370    pub fn from_json_no_content(json: &str) -> Result<Self, ParseError> {
371        let raw: RawSourceMapLite<'_> = serde_json::from_str(json)?;
372
373        if raw.version != 3 {
374            return Err(ParseError::InvalidVersion(raw.version));
375        }
376
377        let source_root = raw.source_root.as_deref().unwrap_or("");
378        let sources = resolve_sources(&raw.sources, source_root);
379        let source_map = build_source_map(&sources);
380        let (mut mappings, line_offsets) = decode_mappings(raw.mappings)?;
381
382        if let Some(range_mappings_str) = raw.range_mappings
383            && !range_mappings_str.is_empty()
384        {
385            decode_range_mappings(range_mappings_str, &mut mappings, &line_offsets)?;
386        }
387
388        let num_sources = sources.len();
389        let scopes = match raw.scopes {
390            Some(scopes_str) if !scopes_str.is_empty() => {
391                Some(srcmap_scopes::decode_scopes(scopes_str, &raw.names, num_sources)?)
392            }
393            _ => None,
394        };
395
396        let ignore_list = match raw.ignore_list {
397            Some(list) => list,
398            None => raw.x_google_ignore_list.unwrap_or_default(),
399        };
400
401        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
402
403        Ok(Self {
404            file: raw.file,
405            source_root: raw.source_root,
406            sources,
407            sources_content: Vec::new(),
408            names: raw.names,
409            ignore_list,
410            extensions: HashMap::new(),
411            debug_id: raw.debug_id,
412            scopes,
413            mappings,
414            line_offsets,
415            reverse_index: OnceCell::new(),
416            source_map,
417            has_range_mappings,
418        })
419    }
420
421    /// Internal parser with control over whether indexed maps (sections) are allowed.
422    fn from_json_inner(json: &str, allow_sections: bool) -> Result<Self, ParseError> {
423        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
424
425        if raw.version != 3 {
426            return Err(ParseError::InvalidVersion(raw.version));
427        }
428
429        // Handle indexed source maps (sections)
430        if let Some(sections) = raw.sections {
431            if !allow_sections {
432                return Err(ParseError::NestedIndexMap);
433            }
434            return Self::from_sections(raw.file, sections);
435        }
436
437        Self::from_regular(raw)
438    }
439
440    /// Parse a regular (non-indexed) source map.
441    fn from_regular(raw: RawSourceMap<'_>) -> Result<Self, ParseError> {
442        let source_root = raw.source_root.as_deref().unwrap_or("");
443        let sources = resolve_sources(&raw.sources, source_root);
444        let sources_content = raw.sources_content.unwrap_or_default();
445        let source_map = build_source_map(&sources);
446
447        // Decode mappings directly into flat Mapping vec
448        let (mut mappings, line_offsets) = decode_mappings(raw.mappings)?;
449
450        // Decode range mappings if present
451        if let Some(range_mappings_str) = raw.range_mappings
452            && !range_mappings_str.is_empty()
453        {
454            decode_range_mappings(range_mappings_str, &mut mappings, &line_offsets)?;
455        }
456
457        // Decode scopes if present
458        let num_sources = sources.len();
459        let scopes = match raw.scopes {
460            Some(scopes_str) if !scopes_str.is_empty() => {
461                Some(srcmap_scopes::decode_scopes(scopes_str, &raw.names, num_sources)?)
462            }
463            _ => None,
464        };
465
466        // Use x_google_ignoreList as fallback only when ignoreList is absent
467        let ignore_list = match raw.ignore_list {
468            Some(list) => list,
469            None => raw.x_google_ignore_list.unwrap_or_default(),
470        };
471
472        // Filter extensions to only keep x_* and x-* fields
473        let extensions: HashMap<String, serde_json::Value> = raw
474            .extensions
475            .into_iter()
476            .filter(|(k, _)| k.starts_with("x_") || k.starts_with("x-"))
477            .collect();
478
479        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
480
481        Ok(Self {
482            file: raw.file,
483            source_root: raw.source_root,
484            sources,
485            sources_content,
486            names: raw.names,
487            ignore_list,
488            extensions,
489            debug_id: raw.debug_id,
490            scopes,
491            mappings,
492            line_offsets,
493            reverse_index: OnceCell::new(),
494            source_map,
495            has_range_mappings,
496        })
497    }
498
499    /// Flatten an indexed source map (with sections) into a regular one.
500    fn from_sections(file: Option<String>, sections: Vec<RawSection>) -> Result<Self, ParseError> {
501        let mut all_sources: Vec<String> = Vec::new();
502        let mut all_sources_content: Vec<Option<String>> = Vec::new();
503        let mut all_names: Vec<String> = Vec::new();
504        let mut all_mappings: Vec<Mapping> = Vec::new();
505        let mut all_ignore_list: Vec<u32> = Vec::new();
506        let mut max_line: u32 = 0;
507
508        // Source/name dedup maps to merge across sections
509        let mut source_index_map: HashMap<String, u32> = HashMap::new();
510        let mut name_index_map: HashMap<String, u32> = HashMap::new();
511
512        // Validate section ordering (must be in ascending line, column order)
513        for i in 1..sections.len() {
514            let prev = &sections[i - 1].offset;
515            let curr = &sections[i].offset;
516            if (curr.line, curr.column) <= (prev.line, prev.column) {
517                return Err(ParseError::SectionsNotOrdered);
518            }
519        }
520
521        for section in &sections {
522            // Section maps must not be indexed maps themselves (ECMA-426)
523            let sub = Self::from_json_inner(section.map.get(), false)?;
524
525            let line_offset = section.offset.line;
526            let col_offset = section.offset.column;
527
528            // Map section source indices to global indices
529            let source_remap: Vec<u32> = sub
530                .sources
531                .iter()
532                .enumerate()
533                .map(|(i, s)| {
534                    if let Some(&existing) = source_index_map.get(s) {
535                        existing
536                    } else {
537                        let idx = all_sources.len() as u32;
538                        all_sources.push(s.clone());
539                        // Add sourcesContent if available
540                        let content = sub.sources_content.get(i).cloned().unwrap_or(None);
541                        all_sources_content.push(content);
542                        source_index_map.insert(s.clone(), idx);
543                        idx
544                    }
545                })
546                .collect();
547
548            // Map section name indices to global indices
549            let name_remap: Vec<u32> = sub
550                .names
551                .iter()
552                .map(|n| {
553                    if let Some(&existing) = name_index_map.get(n) {
554                        existing
555                    } else {
556                        let idx = all_names.len() as u32;
557                        all_names.push(n.clone());
558                        name_index_map.insert(n.clone(), idx);
559                        idx
560                    }
561                })
562                .collect();
563
564            // Add ignore_list entries (remapped to global source indices)
565            for &idx in &sub.ignore_list {
566                let global_idx = source_remap[idx as usize];
567                if !all_ignore_list.contains(&global_idx) {
568                    all_ignore_list.push(global_idx);
569                }
570            }
571
572            // Remap and offset all mappings from this section
573            for m in &sub.mappings {
574                let gen_line = m.generated_line + line_offset;
575                let gen_col = if m.generated_line == 0 {
576                    m.generated_column + col_offset
577                } else {
578                    m.generated_column
579                };
580
581                all_mappings.push(Mapping {
582                    generated_line: gen_line,
583                    generated_column: gen_col,
584                    source: if m.source == NO_SOURCE {
585                        NO_SOURCE
586                    } else {
587                        source_remap[m.source as usize]
588                    },
589                    original_line: m.original_line,
590                    original_column: m.original_column,
591                    name: if m.name == NO_NAME { NO_NAME } else { name_remap[m.name as usize] },
592                    is_range_mapping: m.is_range_mapping,
593                });
594
595                if gen_line > max_line {
596                    max_line = gen_line;
597                }
598            }
599        }
600
601        // Sort mappings by (generated_line, generated_column)
602        all_mappings.sort_unstable_by(|a, b| {
603            a.generated_line
604                .cmp(&b.generated_line)
605                .then(a.generated_column.cmp(&b.generated_column))
606        });
607
608        // Build line_offsets
609        let line_count = if all_mappings.is_empty() { 0 } else { max_line as usize + 1 };
610        let mut line_offsets: Vec<u32> = vec![0; line_count + 1];
611        let mut current_line: usize = 0;
612        for (i, m) in all_mappings.iter().enumerate() {
613            while current_line < m.generated_line as usize {
614                current_line += 1;
615                if current_line < line_offsets.len() {
616                    line_offsets[current_line] = i as u32;
617                }
618            }
619        }
620        // Fill sentinel
621        if !line_offsets.is_empty() {
622            let last = all_mappings.len() as u32;
623            for offset in line_offsets.iter_mut().skip(current_line + 1) {
624                *offset = last;
625            }
626        }
627
628        let source_map = build_source_map(&all_sources);
629        let has_range_mappings = all_mappings.iter().any(|m| m.is_range_mapping);
630
631        Ok(Self {
632            file,
633            source_root: None,
634            sources: all_sources,
635            sources_content: all_sources_content,
636            names: all_names,
637            ignore_list: all_ignore_list,
638            extensions: HashMap::new(),
639            debug_id: None,
640            scopes: None, // TODO: merge scopes from sections
641            mappings: all_mappings,
642            line_offsets,
643            reverse_index: OnceCell::new(),
644            source_map,
645            has_range_mappings,
646        })
647    }
648
649    /// Look up the original source position for a generated position.
650    ///
651    /// Both `line` and `column` are 0-based.
652    /// Returns `None` if no mapping exists or the mapping has no source.
653    pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
654        self.original_position_for_with_bias(line, column, Bias::GreatestLowerBound)
655    }
656
657    /// Look up the original source position with a search bias.
658    ///
659    /// Both `line` and `column` are 0-based.
660    /// - `GreatestLowerBound`: find the closest mapping at or before the column (default)
661    /// - `LeastUpperBound`: find the closest mapping at or after the column
662    pub fn original_position_for_with_bias(
663        &self,
664        line: u32,
665        column: u32,
666        bias: Bias,
667    ) -> Option<OriginalLocation> {
668        let line_idx = line as usize;
669        if line_idx + 1 >= self.line_offsets.len() {
670            return self.range_mapping_fallback(line, column);
671        }
672
673        let start = self.line_offsets[line_idx] as usize;
674        let end = self.line_offsets[line_idx + 1] as usize;
675
676        if start == end {
677            return self.range_mapping_fallback(line, column);
678        }
679
680        let line_mappings = &self.mappings[start..end];
681
682        let idx = match bias {
683            Bias::GreatestLowerBound => {
684                match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
685                    Ok(i) => i,
686                    Err(0) => return self.range_mapping_fallback(line, column),
687                    Err(i) => i - 1,
688                }
689            }
690            Bias::LeastUpperBound => {
691                match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
692                    Ok(i) => i,
693                    Err(i) => {
694                        if i >= line_mappings.len() {
695                            return None;
696                        }
697                        i
698                    }
699                }
700            }
701        };
702
703        let mapping = &line_mappings[idx];
704
705        if mapping.source == NO_SOURCE {
706            return None;
707        }
708
709        if mapping.is_range_mapping && column >= mapping.generated_column {
710            let column_delta = column - mapping.generated_column;
711            return Some(OriginalLocation {
712                source: mapping.source,
713                line: mapping.original_line,
714                column: mapping.original_column + column_delta,
715                name: if mapping.name == NO_NAME { None } else { Some(mapping.name) },
716            });
717        }
718
719        Some(OriginalLocation {
720            source: mapping.source,
721            line: mapping.original_line,
722            column: mapping.original_column,
723            name: if mapping.name == NO_NAME { None } else { Some(mapping.name) },
724        })
725    }
726
727    /// Fall back to range mappings when no exact mapping is found for the position.
728    ///
729    /// Uses `saturating_sub` for column delta to prevent underflow when the
730    /// query column is before the range mapping's generated column.
731    fn range_mapping_fallback(&self, line: u32, column: u32) -> Option<OriginalLocation> {
732        let line_idx = line as usize;
733        let search_end = if line_idx + 1 < self.line_offsets.len() {
734            self.line_offsets[line_idx] as usize
735        } else {
736            self.mappings.len()
737        };
738        if search_end == 0 {
739            return None;
740        }
741        let last_mapping = &self.mappings[search_end - 1];
742        if !last_mapping.is_range_mapping || last_mapping.source == NO_SOURCE {
743            return None;
744        }
745        let line_delta = line - last_mapping.generated_line;
746        let column_delta =
747            if line_delta == 0 { column.saturating_sub(last_mapping.generated_column) } else { 0 };
748        Some(OriginalLocation {
749            source: last_mapping.source,
750            line: last_mapping.original_line + line_delta,
751            column: last_mapping.original_column + column_delta,
752            name: if last_mapping.name == NO_NAME { None } else { Some(last_mapping.name) },
753        })
754    }
755
756    /// Look up the generated position for an original source position.
757    ///
758    /// `source` is the source filename. `line` and `column` are 0-based.
759    /// Uses `GreatestLowerBound` by default (finds closest mapping at or before the position),
760    /// matching `@jridgewell/trace-mapping`'s `generatedPositionFor` semantics.
761    pub fn generated_position_for(
762        &self,
763        source: &str,
764        line: u32,
765        column: u32,
766    ) -> Option<GeneratedLocation> {
767        self.generated_position_for_with_bias(source, line, column, Bias::GreatestLowerBound)
768    }
769
770    /// Look up the generated position with a search bias.
771    ///
772    /// `source` is the source filename. `line` and `column` are 0-based.
773    /// - `GreatestLowerBound`: find the closest mapping at or before the position (default)
774    /// - `LeastUpperBound`: find the closest mapping at or after the position
775    pub fn generated_position_for_with_bias(
776        &self,
777        source: &str,
778        line: u32,
779        column: u32,
780        bias: Bias,
781    ) -> Option<GeneratedLocation> {
782        let &source_idx = self.source_map.get(source)?;
783
784        let reverse_index = self.reverse_index.get_or_init(|| build_reverse_index(&self.mappings));
785
786        // Binary search in reverse_index for (source, line, column)
787        let idx = reverse_index.partition_point(|&i| {
788            let m = &self.mappings[i as usize];
789            (m.source, m.original_line, m.original_column) < (source_idx, line, column)
790        });
791
792        // jridgewell's generatedPositionFor searches within a single original
793        // line only, so both GLB and LUB must be constrained to the same line.
794        match bias {
795            Bias::GreatestLowerBound => {
796                // partition_point gives us the first element >= target.
797                // For GLB, we want the element at or before on the SAME line.
798                if idx < reverse_index.len() {
799                    let mapping = &self.mappings[reverse_index[idx] as usize];
800                    if mapping.source == source_idx
801                        && mapping.original_line == line
802                        && mapping.original_column == column
803                    {
804                        return Some(GeneratedLocation {
805                            line: mapping.generated_line,
806                            column: mapping.generated_column,
807                        });
808                    }
809                }
810                // No exact match: use the element before (greatest lower bound)
811                if idx == 0 {
812                    return None;
813                }
814                let mapping = &self.mappings[reverse_index[idx - 1] as usize];
815                if mapping.source != source_idx || mapping.original_line != line {
816                    return None;
817                }
818                Some(GeneratedLocation {
819                    line: mapping.generated_line,
820                    column: mapping.generated_column,
821                })
822            }
823            Bias::LeastUpperBound => {
824                if idx >= reverse_index.len() {
825                    return None;
826                }
827                let mapping = &self.mappings[reverse_index[idx] as usize];
828                if mapping.source != source_idx || mapping.original_line != line {
829                    return None;
830                }
831                // On exact match, scan forward to find the last mapping with the
832                // same (source, origLine, origCol). This matches jridgewell's
833                // upperBound behavior: when multiple generated positions map to
834                // the same original position, return the last one.
835                // On non-exact match, return the first element > target as-is.
836                if mapping.original_column == column {
837                    let mut last_idx = idx;
838                    while last_idx + 1 < reverse_index.len() {
839                        let next = &self.mappings[reverse_index[last_idx + 1] as usize];
840                        if next.source != source_idx
841                            || next.original_line != line
842                            || next.original_column != column
843                        {
844                            break;
845                        }
846                        last_idx += 1;
847                    }
848                    let last_mapping = &self.mappings[reverse_index[last_idx] as usize];
849                    return Some(GeneratedLocation {
850                        line: last_mapping.generated_line,
851                        column: last_mapping.generated_column,
852                    });
853                }
854                Some(GeneratedLocation {
855                    line: mapping.generated_line,
856                    column: mapping.generated_column,
857                })
858            }
859        }
860    }
861
862    /// Find all generated positions for an original source position.
863    ///
864    /// `source` is the source filename. `line` and `column` are 0-based.
865    /// Returns all generated positions that map back to this original location.
866    pub fn all_generated_positions_for(
867        &self,
868        source: &str,
869        line: u32,
870        column: u32,
871    ) -> Vec<GeneratedLocation> {
872        let Some(&source_idx) = self.source_map.get(source) else {
873            return Vec::new();
874        };
875
876        let reverse_index = self.reverse_index.get_or_init(|| build_reverse_index(&self.mappings));
877
878        // Find the first entry matching (source, line, column)
879        let start = reverse_index.partition_point(|&i| {
880            let m = &self.mappings[i as usize];
881            (m.source, m.original_line, m.original_column) < (source_idx, line, column)
882        });
883
884        let mut results = Vec::new();
885
886        for &ri in &reverse_index[start..] {
887            let m = &self.mappings[ri as usize];
888            if m.source != source_idx || m.original_line != line || m.original_column != column {
889                break;
890            }
891            results.push(GeneratedLocation { line: m.generated_line, column: m.generated_column });
892        }
893
894        results
895    }
896
897    /// Map a generated range to its original range.
898    ///
899    /// Given a generated range `(start_line:start_column → end_line:end_column)`,
900    /// maps both endpoints through the source map and returns the original range.
901    /// Both endpoints must resolve to the same source file.
902    pub fn map_range(
903        &self,
904        start_line: u32,
905        start_column: u32,
906        end_line: u32,
907        end_column: u32,
908    ) -> Option<MappedRange> {
909        let start = self.original_position_for(start_line, start_column)?;
910        let end = self.original_position_for(end_line, end_column)?;
911
912        // Both endpoints must map to the same source
913        if start.source != end.source {
914            return None;
915        }
916
917        Some(MappedRange {
918            source: start.source,
919            original_start_line: start.line,
920            original_start_column: start.column,
921            original_end_line: end.line,
922            original_end_column: end.column,
923        })
924    }
925
926    /// Resolve a source index to its filename.
927    ///
928    /// # Panics
929    ///
930    /// Panics if `index` is out of bounds. Use [`get_source`](Self::get_source)
931    /// for a non-panicking alternative.
932    #[inline]
933    pub fn source(&self, index: u32) -> &str {
934        &self.sources[index as usize]
935    }
936
937    /// Resolve a source index to its filename, returning `None` if out of bounds.
938    #[inline]
939    pub fn get_source(&self, index: u32) -> Option<&str> {
940        self.sources.get(index as usize).map(|s| s.as_str())
941    }
942
943    /// Resolve a name index to its string.
944    ///
945    /// # Panics
946    ///
947    /// Panics if `index` is out of bounds. Use [`get_name`](Self::get_name)
948    /// for a non-panicking alternative.
949    #[inline]
950    pub fn name(&self, index: u32) -> &str {
951        &self.names[index as usize]
952    }
953
954    /// Resolve a name index to its string, returning `None` if out of bounds.
955    #[inline]
956    pub fn get_name(&self, index: u32) -> Option<&str> {
957        self.names.get(index as usize).map(|s| s.as_str())
958    }
959
960    /// Find the source index for a filename.
961    #[inline]
962    pub fn source_index(&self, name: &str) -> Option<u32> {
963        self.source_map.get(name).copied()
964    }
965
966    /// Total number of decoded mappings.
967    #[inline]
968    pub fn mapping_count(&self) -> usize {
969        self.mappings.len()
970    }
971
972    /// Number of generated lines.
973    #[inline]
974    pub fn line_count(&self) -> usize {
975        self.line_offsets.len().saturating_sub(1)
976    }
977
978    /// Get all mappings for a generated line (0-based).
979    #[inline]
980    pub fn mappings_for_line(&self, line: u32) -> &[Mapping] {
981        let line_idx = line as usize;
982        if line_idx + 1 >= self.line_offsets.len() {
983            return &[];
984        }
985        let start = self.line_offsets[line_idx] as usize;
986        let end = self.line_offsets[line_idx + 1] as usize;
987        &self.mappings[start..end]
988    }
989
990    /// Iterate all mappings.
991    #[inline]
992    pub fn all_mappings(&self) -> &[Mapping] {
993        &self.mappings
994    }
995
996    /// Serialize the source map back to JSON.
997    ///
998    /// Produces a valid source map v3 JSON string that can be written to a file
999    /// or embedded in a data URL.
1000    pub fn to_json(&self) -> String {
1001        self.to_json_with_options(false)
1002    }
1003
1004    /// Serialize the source map back to JSON with options.
1005    ///
1006    /// If `exclude_content` is true, `sourcesContent` is omitted from the output.
1007    pub fn to_json_with_options(&self, exclude_content: bool) -> String {
1008        let mappings = self.encode_mappings();
1009
1010        // Encode scopes first — this may add new names that need to be in the names array
1011        let scopes_encoded = if let Some(ref scopes_info) = self.scopes {
1012            let mut names_clone = self.names.clone();
1013            let s = srcmap_scopes::encode_scopes(scopes_info, &mut names_clone);
1014            Some((s, names_clone))
1015        } else {
1016            None
1017        };
1018        let names_for_json = match &scopes_encoded {
1019            Some((_, expanded_names)) => expanded_names,
1020            None => &self.names,
1021        };
1022
1023        let source_root_prefix = self.source_root.as_deref().unwrap_or("");
1024
1025        let mut json = String::with_capacity(256 + mappings.len());
1026        json.push_str(r#"{"version":3"#);
1027
1028        if let Some(ref file) = self.file {
1029            json.push_str(r#","file":"#);
1030            json_quote_into(&mut json, file);
1031        }
1032
1033        if let Some(ref root) = self.source_root {
1034            json.push_str(r#","sourceRoot":"#);
1035            json_quote_into(&mut json, root);
1036        }
1037
1038        // Strip sourceRoot prefix from sources to avoid double-application on roundtrip
1039        json.push_str(r#","sources":["#);
1040        for (i, s) in self.sources.iter().enumerate() {
1041            if i > 0 {
1042                json.push(',');
1043            }
1044            let source_name = if !source_root_prefix.is_empty() {
1045                s.strip_prefix(source_root_prefix).unwrap_or(s)
1046            } else {
1047                s
1048            };
1049            json_quote_into(&mut json, source_name);
1050        }
1051        json.push(']');
1052
1053        if !exclude_content
1054            && !self.sources_content.is_empty()
1055            && self.sources_content.iter().any(|c| c.is_some())
1056        {
1057            json.push_str(r#","sourcesContent":["#);
1058            for (i, c) in self.sources_content.iter().enumerate() {
1059                if i > 0 {
1060                    json.push(',');
1061                }
1062                match c {
1063                    Some(content) => json_quote_into(&mut json, content),
1064                    None => json.push_str("null"),
1065                }
1066            }
1067            json.push(']');
1068        }
1069
1070        json.push_str(r#","names":["#);
1071        for (i, n) in names_for_json.iter().enumerate() {
1072            if i > 0 {
1073                json.push(',');
1074            }
1075            json_quote_into(&mut json, n);
1076        }
1077        json.push(']');
1078
1079        // VLQ mappings are pure base64/,/; — no escaping needed
1080        json.push_str(r#","mappings":""#);
1081        json.push_str(&mappings);
1082        json.push('"');
1083
1084        if let Some(range_mappings) = self.encode_range_mappings() {
1085            // Range mappings are also pure VLQ — no escaping needed
1086            json.push_str(r#","rangeMappings":""#);
1087            json.push_str(&range_mappings);
1088            json.push('"');
1089        }
1090
1091        if !self.ignore_list.is_empty() {
1092            use std::fmt::Write;
1093            json.push_str(r#","ignoreList":["#);
1094            for (i, &idx) in self.ignore_list.iter().enumerate() {
1095                if i > 0 {
1096                    json.push(',');
1097                }
1098                let _ = write!(json, "{idx}");
1099            }
1100            json.push(']');
1101        }
1102
1103        if let Some(ref id) = self.debug_id {
1104            json.push_str(r#","debugId":"#);
1105            json_quote_into(&mut json, id);
1106        }
1107
1108        // scopes (ECMA-426 scopes proposal)
1109        if let Some((ref s, _)) = scopes_encoded {
1110            json.push_str(r#","scopes":"#);
1111            json_quote_into(&mut json, s);
1112        }
1113
1114        // Emit extension fields (x_* and x-* keys)
1115        let mut ext_keys: Vec<&String> = self.extensions.keys().collect();
1116        ext_keys.sort();
1117        for key in ext_keys {
1118            if let Some(val) = self.extensions.get(key) {
1119                json.push(',');
1120                json_quote_into(&mut json, key);
1121                json.push(':');
1122                json.push_str(&serde_json::to_string(val).unwrap_or_default());
1123            }
1124        }
1125
1126        json.push('}');
1127        json
1128    }
1129
1130    /// Construct a `SourceMap` from pre-built parts.
1131    ///
1132    /// This avoids the encode-then-decode round-trip used in composition pipelines.
1133    /// Mappings must be sorted by (generated_line, generated_column).
1134    /// Use `u32::MAX` for `source`/`name` fields to indicate absence.
1135    #[allow(
1136        clippy::too_many_arguments,
1137        reason = "constructor-style API keeps the hot path allocation-free"
1138    )]
1139    pub fn from_parts(
1140        file: Option<String>,
1141        source_root: Option<String>,
1142        sources: Vec<String>,
1143        sources_content: Vec<Option<String>>,
1144        names: Vec<String>,
1145        mappings: Vec<Mapping>,
1146        ignore_list: Vec<u32>,
1147        debug_id: Option<String>,
1148        scopes: Option<ScopeInfo>,
1149    ) -> Self {
1150        // Build line_offsets from sorted mappings
1151        let line_count = mappings.last().map_or(0, |m| m.generated_line as usize + 1);
1152        let mut line_offsets: Vec<u32> = vec![0; line_count + 1];
1153        let mut current_line: usize = 0;
1154        for (i, m) in mappings.iter().enumerate() {
1155            while current_line < m.generated_line as usize {
1156                current_line += 1;
1157                if current_line < line_offsets.len() {
1158                    line_offsets[current_line] = i as u32;
1159                }
1160            }
1161        }
1162        // Fill remaining with sentinel
1163        if !line_offsets.is_empty() {
1164            let last = mappings.len() as u32;
1165            for offset in line_offsets.iter_mut().skip(current_line + 1) {
1166                *offset = last;
1167            }
1168        }
1169
1170        let source_map = build_source_map(&sources);
1171        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
1172
1173        Self {
1174            file,
1175            source_root,
1176            sources,
1177            sources_content,
1178            names,
1179            ignore_list,
1180            extensions: HashMap::new(),
1181            debug_id,
1182            scopes,
1183            mappings,
1184            line_offsets,
1185            reverse_index: OnceCell::new(),
1186            source_map,
1187            has_range_mappings,
1188        }
1189    }
1190
1191    /// Build a source map from pre-parsed components and a VLQ mappings string.
1192    ///
1193    /// This is the fast path for WASM: JS does `JSON.parse()` (V8-native speed),
1194    /// then only the VLQ mappings string crosses into WASM for decoding.
1195    /// Avoids copying large `sourcesContent` into WASM linear memory.
1196    #[allow(clippy::too_many_arguments, reason = "WASM bindings pass parsed map parts directly")]
1197    pub fn from_vlq(
1198        mappings_str: &str,
1199        sources: Vec<String>,
1200        names: Vec<String>,
1201        file: Option<String>,
1202        source_root: Option<String>,
1203        sources_content: Vec<Option<String>>,
1204        ignore_list: Vec<u32>,
1205        debug_id: Option<String>,
1206    ) -> Result<Self, ParseError> {
1207        Self::from_vlq_with_range_mappings(
1208            mappings_str,
1209            sources,
1210            names,
1211            file,
1212            source_root,
1213            sources_content,
1214            ignore_list,
1215            debug_id,
1216            None,
1217        )
1218    }
1219
1220    /// Build a source map from pre-parsed components, a VLQ mappings string,
1221    /// and an optional range mappings string.
1222    #[allow(
1223        clippy::too_many_arguments,
1224        reason = "range mappings are optional but share the same low-level constructor shape"
1225    )]
1226    pub fn from_vlq_with_range_mappings(
1227        mappings_str: &str,
1228        sources: Vec<String>,
1229        names: Vec<String>,
1230        file: Option<String>,
1231        source_root: Option<String>,
1232        sources_content: Vec<Option<String>>,
1233        ignore_list: Vec<u32>,
1234        debug_id: Option<String>,
1235        range_mappings_str: Option<&str>,
1236    ) -> Result<Self, ParseError> {
1237        let (mut mappings, line_offsets) = decode_mappings(mappings_str)?;
1238        if let Some(rm_str) = range_mappings_str
1239            && !rm_str.is_empty()
1240        {
1241            decode_range_mappings(rm_str, &mut mappings, &line_offsets)?;
1242        }
1243        let source_map = build_source_map(&sources);
1244        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
1245        Ok(Self {
1246            file,
1247            source_root,
1248            sources,
1249            sources_content,
1250            names,
1251            ignore_list,
1252            extensions: HashMap::new(),
1253            debug_id,
1254            scopes: None,
1255            mappings,
1256            line_offsets,
1257            reverse_index: OnceCell::new(),
1258            source_map,
1259            has_range_mappings,
1260        })
1261    }
1262
1263    /// Create a builder for incrementally constructing a `SourceMap`.
1264    ///
1265    /// The builder accepts iterators for sources, names, and mappings,
1266    /// avoiding the need to pre-collect into `Vec`s.
1267    ///
1268    /// ```
1269    /// use srcmap_sourcemap::{SourceMap, Mapping};
1270    ///
1271    /// let sm = SourceMap::builder()
1272    ///     .file("output.js")
1273    ///     .sources(["input.ts"])
1274    ///     .sources_content([Some("let x = 1;")])
1275    ///     .names(["x"])
1276    ///     .mappings([Mapping {
1277    ///         generated_line: 0,
1278    ///         generated_column: 0,
1279    ///         source: 0,
1280    ///         original_line: 0,
1281    ///         original_column: 0,
1282    ///         name: 0,
1283    ///         is_range_mapping: false,
1284    ///     }])
1285    ///     .build();
1286    ///
1287    /// assert_eq!(sm.mapping_count(), 1);
1288    /// ```
1289    pub fn builder() -> SourceMapBuilder {
1290        SourceMapBuilder::new()
1291    }
1292
1293    /// Parse a source map from JSON, decoding only mappings for lines in `[start_line, end_line)`.
1294    ///
1295    /// This is useful for large source maps where only a subset of lines is needed.
1296    /// VLQ state is maintained through skipped lines (required for correct delta decoding),
1297    /// but `Mapping` structs are only allocated for lines in the requested range.
1298    pub fn from_json_lines(json: &str, start_line: u32, end_line: u32) -> Result<Self, ParseError> {
1299        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
1300
1301        if raw.version != 3 {
1302            return Err(ParseError::InvalidVersion(raw.version));
1303        }
1304
1305        let source_root = raw.source_root.as_deref().unwrap_or("");
1306        let sources = resolve_sources(&raw.sources, source_root);
1307        let sources_content = raw.sources_content.unwrap_or_default();
1308        let source_map = build_source_map(&sources);
1309
1310        // Decode only the requested line range
1311        let (mappings, line_offsets) = decode_mappings_range(raw.mappings, start_line, end_line)?;
1312
1313        // Decode scopes if present
1314        let num_sources = sources.len();
1315        let scopes = match raw.scopes {
1316            Some(scopes_str) if !scopes_str.is_empty() => {
1317                Some(srcmap_scopes::decode_scopes(scopes_str, &raw.names, num_sources)?)
1318            }
1319            _ => None,
1320        };
1321
1322        let ignore_list = match raw.ignore_list {
1323            Some(list) => list,
1324            None => raw.x_google_ignore_list.unwrap_or_default(),
1325        };
1326
1327        // Filter extensions to only keep x_* and x-* fields
1328        let extensions: HashMap<String, serde_json::Value> = raw
1329            .extensions
1330            .into_iter()
1331            .filter(|(k, _)| k.starts_with("x_") || k.starts_with("x-"))
1332            .collect();
1333
1334        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
1335
1336        Ok(Self {
1337            file: raw.file,
1338            source_root: raw.source_root,
1339            sources,
1340            sources_content,
1341            names: raw.names,
1342            ignore_list,
1343            extensions,
1344            debug_id: raw.debug_id,
1345            scopes,
1346            mappings,
1347            line_offsets,
1348            reverse_index: OnceCell::new(),
1349            source_map,
1350            has_range_mappings,
1351        })
1352    }
1353
1354    /// Encode all mappings back to a VLQ mappings string.
1355    pub fn encode_mappings(&self) -> String {
1356        if self.mappings.is_empty() {
1357            return String::new();
1358        }
1359
1360        let mut out: Vec<u8> = Vec::with_capacity(self.mappings.len() * 6);
1361
1362        let mut prev_gen_col: i64 = 0;
1363        let mut prev_source: i64 = 0;
1364        let mut prev_orig_line: i64 = 0;
1365        let mut prev_orig_col: i64 = 0;
1366        let mut prev_name: i64 = 0;
1367        let mut prev_gen_line: u32 = 0;
1368        let mut first_in_line = true;
1369
1370        for m in &self.mappings {
1371            while prev_gen_line < m.generated_line {
1372                out.push(b';');
1373                prev_gen_line += 1;
1374                prev_gen_col = 0;
1375                first_in_line = true;
1376            }
1377
1378            if !first_in_line {
1379                out.push(b',');
1380            }
1381            first_in_line = false;
1382
1383            srcmap_codec::vlq_encode(&mut out, m.generated_column as i64 - prev_gen_col);
1384            prev_gen_col = m.generated_column as i64;
1385
1386            if m.source != NO_SOURCE {
1387                srcmap_codec::vlq_encode(&mut out, m.source as i64 - prev_source);
1388                prev_source = m.source as i64;
1389
1390                srcmap_codec::vlq_encode(&mut out, m.original_line as i64 - prev_orig_line);
1391                prev_orig_line = m.original_line as i64;
1392
1393                srcmap_codec::vlq_encode(&mut out, m.original_column as i64 - prev_orig_col);
1394                prev_orig_col = m.original_column as i64;
1395
1396                if m.name != NO_NAME {
1397                    srcmap_codec::vlq_encode(&mut out, m.name as i64 - prev_name);
1398                    prev_name = m.name as i64;
1399                }
1400            }
1401        }
1402
1403        debug_assert!(out.is_ascii());
1404        // SAFETY: vlq_encode only pushes bytes from BASE64_ENCODE (all ASCII),
1405        // and we only add b';' and b',' — all valid UTF-8.
1406        unsafe { String::from_utf8_unchecked(out) }
1407    }
1408
1409    pub fn encode_range_mappings(&self) -> Option<String> {
1410        if !self.has_range_mappings {
1411            return None;
1412        }
1413        let line_count = self.line_offsets.len().saturating_sub(1);
1414        let mut out: Vec<u8> = Vec::new();
1415        for line_idx in 0..line_count {
1416            if line_idx > 0 {
1417                out.push(b';');
1418            }
1419            let start = self.line_offsets[line_idx] as usize;
1420            let end = self.line_offsets[line_idx + 1] as usize;
1421            let mut prev_offset: u64 = 0;
1422            let mut first_on_line = true;
1423            for (i, mapping) in self.mappings[start..end].iter().enumerate() {
1424                if mapping.is_range_mapping {
1425                    if !first_on_line {
1426                        out.push(b',');
1427                    }
1428                    first_on_line = false;
1429                    vlq_encode_unsigned(&mut out, i as u64 - prev_offset);
1430                    prev_offset = i as u64;
1431                }
1432            }
1433        }
1434        while out.last() == Some(&b';') {
1435            out.pop();
1436        }
1437        if out.is_empty() {
1438            return None;
1439        }
1440        debug_assert!(out.is_ascii());
1441        // SAFETY: vlq_encode_unsigned only pushes ASCII base64 chars,
1442        // and we only add b';' and b',' — all valid UTF-8.
1443        Some(unsafe { String::from_utf8_unchecked(out) })
1444    }
1445
1446    #[inline]
1447    pub fn has_range_mappings(&self) -> bool {
1448        self.has_range_mappings
1449    }
1450
1451    #[inline]
1452    pub fn range_mapping_count(&self) -> usize {
1453        self.mappings.iter().filter(|m| m.is_range_mapping).count()
1454    }
1455
1456    /// Parse a source map from a `data:` URL.
1457    ///
1458    /// Supports both base64-encoded and plain JSON data URLs:
1459    /// - `data:application/json;base64,<base64-encoded-json>`
1460    /// - `data:application/json;charset=utf-8;base64,<base64-encoded-json>`
1461    /// - `data:application/json,<json>`
1462    ///
1463    /// Returns [`ParseError::InvalidDataUrl`] if the URL format is not recognized
1464    /// or base64 decoding fails.
1465    pub fn from_data_url(url: &str) -> Result<Self, ParseError> {
1466        let rest = url.strip_prefix("data:application/json").ok_or(ParseError::InvalidDataUrl)?;
1467
1468        // Try base64-encoded variants first
1469        let json = if let Some(data) = rest
1470            .strip_prefix(";base64,")
1471            .or_else(|| rest.strip_prefix(";charset=utf-8;base64,"))
1472            .or_else(|| rest.strip_prefix(";charset=UTF-8;base64,"))
1473        {
1474            base64_decode(data).ok_or(ParseError::InvalidDataUrl)?
1475        } else if let Some(data) = rest.strip_prefix(',') {
1476            // Plain JSON — percent-decode if needed
1477            if data.contains('%') { percent_decode(data) } else { data.to_string() }
1478        } else {
1479            return Err(ParseError::InvalidDataUrl);
1480        };
1481
1482        Self::from_json(&json)
1483    }
1484
1485    /// Serialize the source map JSON to a writer.
1486    ///
1487    /// Equivalent to calling [`to_json`](Self::to_json) and writing the result.
1488    /// The full JSON string is built in memory before writing.
1489    pub fn to_writer(&self, mut writer: impl io::Write) -> io::Result<()> {
1490        let json = self.to_json();
1491        writer.write_all(json.as_bytes())
1492    }
1493
1494    /// Serialize the source map JSON to a writer with options.
1495    ///
1496    /// If `exclude_content` is true, `sourcesContent` is omitted from the output.
1497    pub fn to_writer_with_options(
1498        &self,
1499        mut writer: impl io::Write,
1500        exclude_content: bool,
1501    ) -> io::Result<()> {
1502        let json = self.to_json_with_options(exclude_content);
1503        writer.write_all(json.as_bytes())
1504    }
1505
1506    /// Serialize the source map to a `data:` URL.
1507    ///
1508    /// Format: `data:application/json;base64,<base64-encoded-json>`
1509    pub fn to_data_url(&self) -> String {
1510        utils::to_data_url(&self.to_json())
1511    }
1512
1513    // ── Mutable setters ─────────────────────────────────────────
1514
1515    /// Set or clear the `file` property.
1516    pub fn set_file(&mut self, file: Option<String>) {
1517        self.file = file;
1518    }
1519
1520    /// Set or clear the `sourceRoot` property.
1521    pub fn set_source_root(&mut self, source_root: Option<String>) {
1522        self.source_root = source_root;
1523    }
1524
1525    /// Set or clear the `debugId` property.
1526    pub fn set_debug_id(&mut self, debug_id: Option<String>) {
1527        self.debug_id = debug_id;
1528    }
1529
1530    /// Set the `ignoreList` property.
1531    pub fn set_ignore_list(&mut self, ignore_list: Vec<u32>) {
1532        self.ignore_list = ignore_list;
1533    }
1534
1535    /// Replace the sources array and rebuild the source index lookup map.
1536    pub fn set_sources(&mut self, sources: Vec<Option<String>>) {
1537        let source_root = self.source_root.as_deref().unwrap_or("");
1538        self.sources = resolve_sources(&sources, source_root);
1539        self.source_map = build_source_map(&self.sources);
1540        // Invalidate the reverse index since source indices may have changed
1541        self.reverse_index = OnceCell::new();
1542    }
1543}
1544
1545// ── LazySourceMap ──────────────────────────────────────────────────
1546
1547/// Cumulative VLQ state at a line boundary.
1548#[derive(Debug, Clone, Copy, Default)]
1549struct VlqState {
1550    source_index: i64,
1551    original_line: i64,
1552    original_column: i64,
1553    name_index: i64,
1554}
1555
1556/// Pre-scanned line info for O(1) random access into the raw mappings string.
1557#[derive(Debug, Clone)]
1558struct LineInfo {
1559    /// Byte offset into the raw mappings string where this line starts.
1560    byte_offset: usize,
1561    /// Byte offset where this line ends (exclusive, at `;` or end of string).
1562    byte_end: usize,
1563    /// Cumulative VLQ state at the start of this line.
1564    state: VlqState,
1565}
1566
1567/// A lazily-decoded source map that defers VLQ mappings decoding until needed.
1568///
1569/// For large source maps (100MB+), this avoids decoding all mappings upfront.
1570/// JSON metadata (sources, names, etc.) is parsed eagerly, but VLQ mappings
1571/// are decoded on a per-line basis on demand.
1572///
1573/// Not thread-safe (`!Sync`). Uses `RefCell`/`Cell` for internal caching.
1574/// Intended for single-threaded use (WASM) or with external synchronization.
1575///
1576/// # Examples
1577///
1578/// ```
1579/// use srcmap_sourcemap::LazySourceMap;
1580///
1581/// let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;AACA"}"#;
1582/// let sm = LazySourceMap::from_json(json).unwrap();
1583///
1584/// // Mappings are only decoded when accessed
1585/// let loc = sm.original_position_for(0, 0).unwrap();
1586/// assert_eq!(sm.source(loc.source), "input.js");
1587/// ```
1588#[derive(Debug)]
1589pub struct LazySourceMap {
1590    pub file: Option<String>,
1591    pub source_root: Option<String>,
1592    pub sources: Vec<String>,
1593    pub sources_content: Vec<Option<String>>,
1594    pub names: Vec<String>,
1595    pub ignore_list: Vec<u32>,
1596    pub extensions: HashMap<String, serde_json::Value>,
1597    pub debug_id: Option<String>,
1598    pub scopes: Option<ScopeInfo>,
1599
1600    /// Raw VLQ mappings string (owned).
1601    raw_mappings: String,
1602
1603    /// Pre-scanned line info for O(1) line access.
1604    /// In fast-scan mode, VlqState is zeroed and decoded progressively.
1605    line_info: Vec<LineInfo>,
1606
1607    /// Cache of decoded lines: line index -> `Vec<Mapping>`.
1608    decoded_lines: RefCell<HashMap<u32, Vec<Mapping>>>,
1609
1610    /// Source filename -> index for O(1) lookup by name.
1611    source_map: HashMap<String, u32>,
1612
1613    /// Whether line_info was built with fast-scan (no VLQ state tracking).
1614    /// If true, decode_line must decode sequentially from the start.
1615    fast_scan: bool,
1616
1617    /// Highest line fully decoded so far (for progressive decode in fast-scan mode).
1618    /// VLQ state at the end of this line is stored in `decode_state`.
1619    decode_watermark: Cell<u32>,
1620    decode_state: Cell<VlqState>,
1621}
1622
1623impl LazySourceMap {
1624    #[allow(
1625        clippy::too_many_arguments,
1626        reason = "private constructor centralizes shared LazySourceMap setup"
1627    )]
1628    fn new_inner(
1629        file: Option<String>,
1630        source_root: Option<String>,
1631        sources: Vec<String>,
1632        sources_content: Vec<Option<String>>,
1633        names: Vec<String>,
1634        ignore_list: Vec<u32>,
1635        extensions: HashMap<String, serde_json::Value>,
1636        debug_id: Option<String>,
1637        scopes: Option<ScopeInfo>,
1638        raw_mappings: String,
1639        line_info: Vec<LineInfo>,
1640        source_map: HashMap<String, u32>,
1641        fast_scan: bool,
1642    ) -> Self {
1643        Self {
1644            file,
1645            source_root,
1646            sources,
1647            sources_content,
1648            names,
1649            ignore_list,
1650            extensions,
1651            debug_id,
1652            scopes,
1653            raw_mappings,
1654            line_info,
1655            decoded_lines: RefCell::new(HashMap::new()),
1656            source_map,
1657            fast_scan,
1658            decode_watermark: Cell::new(0),
1659            decode_state: Cell::new(VlqState::default()),
1660        }
1661    }
1662
1663    /// Parse a source map from JSON, deferring VLQ mappings decoding.
1664    ///
1665    /// Parses all JSON metadata eagerly but stores the raw mappings string.
1666    /// VLQ mappings are decoded per-line on demand.
1667    pub fn from_json(json: &str) -> Result<Self, ParseError> {
1668        let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
1669
1670        if raw.version != 3 {
1671            return Err(ParseError::InvalidVersion(raw.version));
1672        }
1673
1674        let source_root = raw.source_root.as_deref().unwrap_or("");
1675        let sources = resolve_sources(&raw.sources, source_root);
1676        let sources_content = raw.sources_content.unwrap_or_default();
1677        let source_map = build_source_map(&sources);
1678
1679        // Pre-scan the raw mappings string to find semicolon positions
1680        // and compute cumulative VLQ state at each line boundary.
1681        let raw_mappings = raw.mappings.to_string();
1682        let line_info = prescan_mappings(&raw_mappings)?;
1683
1684        // Decode scopes if present
1685        let num_sources = sources.len();
1686        let scopes = match raw.scopes {
1687            Some(scopes_str) if !scopes_str.is_empty() => {
1688                Some(srcmap_scopes::decode_scopes(scopes_str, &raw.names, num_sources)?)
1689            }
1690            _ => None,
1691        };
1692
1693        let ignore_list = match raw.ignore_list {
1694            Some(list) => list,
1695            None => raw.x_google_ignore_list.unwrap_or_default(),
1696        };
1697
1698        // Filter extensions to only keep x_* and x-* fields
1699        let extensions: HashMap<String, serde_json::Value> = raw
1700            .extensions
1701            .into_iter()
1702            .filter(|(k, _)| k.starts_with("x_") || k.starts_with("x-"))
1703            .collect();
1704
1705        Ok(Self::new_inner(
1706            raw.file,
1707            raw.source_root,
1708            sources,
1709            sources_content,
1710            raw.names,
1711            ignore_list,
1712            extensions,
1713            raw.debug_id,
1714            scopes,
1715            raw_mappings,
1716            line_info,
1717            source_map,
1718            false,
1719        ))
1720    }
1721
1722    /// Parse a source map from JSON, skipping sourcesContent allocation
1723    /// and deferring VLQ mappings decoding.
1724    ///
1725    /// Useful for WASM bindings where sourcesContent is kept on the JS side.
1726    ///
1727    /// Returns `ParseError::NestedIndexMap` if the JSON contains `sections`
1728    /// (indexed source maps are not supported by `LazySourceMap`).
1729    pub fn from_json_no_content(json: &str) -> Result<Self, ParseError> {
1730        let raw: RawSourceMapLite<'_> = serde_json::from_str(json)?;
1731
1732        if raw.version != 3 {
1733            return Err(ParseError::InvalidVersion(raw.version));
1734        }
1735
1736        // LazySourceMap does not support indexed/sectioned source maps.
1737        // Use SourceMap::from_json() for indexed maps.
1738        if raw.sections.is_some() {
1739            return Err(ParseError::NestedIndexMap);
1740        }
1741
1742        let source_root = raw.source_root.as_deref().unwrap_or("");
1743        let sources = resolve_sources(&raw.sources, source_root);
1744        let source_map = build_source_map(&sources);
1745
1746        let raw_mappings = raw.mappings.to_string();
1747        let line_info = prescan_mappings(&raw_mappings)?;
1748
1749        let num_sources = sources.len();
1750        let scopes = match raw.scopes {
1751            Some(scopes_str) if !scopes_str.is_empty() => {
1752                Some(srcmap_scopes::decode_scopes(scopes_str, &raw.names, num_sources)?)
1753            }
1754            _ => None,
1755        };
1756
1757        let ignore_list = match raw.ignore_list {
1758            Some(list) => list,
1759            None => raw.x_google_ignore_list.unwrap_or_default(),
1760        };
1761
1762        Ok(Self::new_inner(
1763            raw.file,
1764            raw.source_root,
1765            sources,
1766            Vec::new(),
1767            raw.names,
1768            ignore_list,
1769            HashMap::new(),
1770            raw.debug_id,
1771            scopes,
1772            raw_mappings,
1773            line_info,
1774            source_map,
1775            false,
1776        ))
1777    }
1778
1779    /// Build a lazy source map from pre-parsed components.
1780    ///
1781    /// The raw VLQ mappings string is prescanned but not decoded.
1782    /// sourcesContent is NOT included. Does not support indexed source maps.
1783    pub fn from_vlq(
1784        mappings: &str,
1785        sources: Vec<String>,
1786        names: Vec<String>,
1787        file: Option<String>,
1788        source_root: Option<String>,
1789        ignore_list: Vec<u32>,
1790        debug_id: Option<String>,
1791    ) -> Result<Self, ParseError> {
1792        let source_map = build_source_map(&sources);
1793        let raw_mappings = mappings.to_string();
1794        let line_info = prescan_mappings(&raw_mappings)?;
1795
1796        Ok(Self::new_inner(
1797            file,
1798            source_root,
1799            sources,
1800            Vec::new(),
1801            names,
1802            ignore_list,
1803            HashMap::new(),
1804            debug_id,
1805            None,
1806            raw_mappings,
1807            line_info,
1808            source_map,
1809            false,
1810        ))
1811    }
1812
1813    /// Parse a source map from JSON using fast-scan mode.
1814    ///
1815    /// Only scans for semicolons at construction (no VLQ decode at all).
1816    /// VLQ state is computed progressively on demand. This gives the fastest
1817    /// possible parse time at the cost of first-lookup needing sequential decode.
1818    /// sourcesContent is skipped.
1819    ///
1820    /// Returns `ParseError::NestedIndexMap` if the JSON contains `sections`
1821    /// (indexed source maps are not supported by `LazySourceMap`).
1822    pub fn from_json_fast(json: &str) -> Result<Self, ParseError> {
1823        let raw: RawSourceMapLite<'_> = serde_json::from_str(json)?;
1824
1825        if raw.version != 3 {
1826            return Err(ParseError::InvalidVersion(raw.version));
1827        }
1828
1829        // LazySourceMap does not support indexed/sectioned source maps.
1830        // Use SourceMap::from_json() for indexed maps.
1831        if raw.sections.is_some() {
1832            return Err(ParseError::NestedIndexMap);
1833        }
1834
1835        let source_root = raw.source_root.as_deref().unwrap_or("");
1836        let sources = resolve_sources(&raw.sources, source_root);
1837        let source_map = build_source_map(&sources);
1838        let raw_mappings = raw.mappings.to_string();
1839
1840        // Fast scan: just find semicolons, no VLQ decode
1841        let line_info = fast_scan_lines(&raw_mappings);
1842
1843        let ignore_list = match raw.ignore_list {
1844            Some(list) => list,
1845            None => raw.x_google_ignore_list.unwrap_or_default(),
1846        };
1847
1848        Ok(Self::new_inner(
1849            raw.file,
1850            raw.source_root,
1851            sources,
1852            Vec::new(),
1853            raw.names,
1854            ignore_list,
1855            HashMap::new(),
1856            raw.debug_id,
1857            None,
1858            raw_mappings,
1859            line_info,
1860            source_map,
1861            true,
1862        ))
1863    }
1864
1865    /// Decode a single line's VLQ segment into mappings, given the initial VLQ state.
1866    /// Returns the decoded mappings and the final VLQ state after this line.
1867    ///
1868    /// Uses absolute byte positions into `raw_mappings` (matching `walk_vlq_state`
1869    /// and `prescan_mappings` patterns).
1870    fn decode_line_with_state(
1871        &self,
1872        line: u32,
1873        mut state: VlqState,
1874    ) -> Result<(Vec<Mapping>, VlqState), DecodeError> {
1875        let line_idx = line as usize;
1876        if line_idx >= self.line_info.len() {
1877            return Ok((Vec::new(), state));
1878        }
1879
1880        let info = &self.line_info[line_idx];
1881        let bytes = self.raw_mappings.as_bytes();
1882        let end = info.byte_end;
1883
1884        let mut mappings = Vec::new();
1885        let mut source_index = state.source_index;
1886        let mut original_line = state.original_line;
1887        let mut original_column = state.original_column;
1888        let mut name_index = state.name_index;
1889        let mut generated_column: i64 = 0;
1890        let mut pos = info.byte_offset;
1891
1892        while pos < end {
1893            let byte = bytes[pos];
1894            if byte == b',' {
1895                pos += 1;
1896                continue;
1897            }
1898
1899            generated_column += vlq_fast(bytes, &mut pos)?;
1900
1901            if pos < end && bytes[pos] != b',' && bytes[pos] != b';' {
1902                source_index += vlq_fast(bytes, &mut pos)?;
1903                if pos >= end || bytes[pos] == b',' || bytes[pos] == b';' {
1904                    return Err(DecodeError::InvalidSegmentLength { fields: 2, offset: pos });
1905                }
1906                original_line += vlq_fast(bytes, &mut pos)?;
1907                if pos >= end || bytes[pos] == b',' || bytes[pos] == b';' {
1908                    return Err(DecodeError::InvalidSegmentLength { fields: 3, offset: pos });
1909                }
1910                original_column += vlq_fast(bytes, &mut pos)?;
1911
1912                let name = if pos < end && bytes[pos] != b',' && bytes[pos] != b';' {
1913                    name_index += vlq_fast(bytes, &mut pos)?;
1914                    name_index as u32
1915                } else {
1916                    NO_NAME
1917                };
1918
1919                mappings.push(Mapping {
1920                    generated_line: line,
1921                    generated_column: generated_column as u32,
1922                    source: source_index as u32,
1923                    original_line: original_line as u32,
1924                    original_column: original_column as u32,
1925                    name,
1926                    is_range_mapping: false,
1927                });
1928            } else {
1929                mappings.push(Mapping {
1930                    generated_line: line,
1931                    generated_column: generated_column as u32,
1932                    source: NO_SOURCE,
1933                    original_line: 0,
1934                    original_column: 0,
1935                    name: NO_NAME,
1936                    is_range_mapping: false,
1937                });
1938            }
1939        }
1940
1941        state.source_index = source_index;
1942        state.original_line = original_line;
1943        state.original_column = original_column;
1944        state.name_index = name_index;
1945        Ok((mappings, state))
1946    }
1947
1948    /// Decode a single line's mappings on demand.
1949    ///
1950    /// Returns the cached result if the line has already been decoded.
1951    /// The line index is 0-based.
1952    pub fn decode_line(&self, line: u32) -> Result<Vec<Mapping>, DecodeError> {
1953        // Check cache first
1954        if let Some(cached) = self.decoded_lines.borrow().get(&line) {
1955            return Ok(cached.clone());
1956        }
1957
1958        let line_idx = line as usize;
1959        if line_idx >= self.line_info.len() {
1960            return Ok(Vec::new());
1961        }
1962
1963        if self.fast_scan {
1964            // In fast-scan mode, VLQ state is not pre-computed.
1965            // Decode sequentially from the watermark (or line 0 for backward seeks).
1966            // For both forward and backward walks, use cached lines where available
1967            // and only walk VLQ bytes to compute state for already-decoded lines.
1968            let watermark = self.decode_watermark.get();
1969            let start = if line >= watermark { watermark } else { 0 };
1970            let mut state = if line >= watermark {
1971                self.decode_state.get()
1972            } else {
1973                VlqState { source_index: 0, original_line: 0, original_column: 0, name_index: 0 }
1974            };
1975
1976            for l in start..=line {
1977                let info = &self.line_info[l as usize];
1978                if self.decoded_lines.borrow().contains_key(&l) {
1979                    // Already cached — just walk VLQ bytes to compute end-state
1980                    let bytes = self.raw_mappings.as_bytes();
1981                    state = walk_vlq_state(bytes, info.byte_offset, info.byte_end, state)?;
1982                } else {
1983                    let (mappings, new_state) = self.decode_line_with_state(l, state)?;
1984                    state = new_state;
1985                    self.decoded_lines.borrow_mut().insert(l, mappings);
1986                }
1987            }
1988
1989            // Update watermark (only advance, never regress)
1990            if line + 1 > self.decode_watermark.get() {
1991                self.decode_watermark.set(line + 1);
1992                self.decode_state.set(state);
1993            }
1994
1995            let cached = self.decoded_lines.borrow().get(&line).cloned();
1996            return Ok(cached.unwrap_or_default());
1997        }
1998
1999        // Normal mode: line_info has pre-computed VLQ state
2000        let state = self.line_info[line_idx].state;
2001        let (mappings, _) = self.decode_line_with_state(line, state)?;
2002        self.decoded_lines.borrow_mut().insert(line, mappings.clone());
2003        Ok(mappings)
2004    }
2005
2006    /// Look up the original source position for a generated position.
2007    ///
2008    /// Both `line` and `column` are 0-based.
2009    /// Returns `None` if no mapping exists or the mapping has no source.
2010    pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
2011        let line_mappings = self.decode_line(line).ok()?;
2012
2013        if line_mappings.is_empty() {
2014            return None;
2015        }
2016
2017        // Binary search for greatest lower bound
2018        let idx = match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
2019            Ok(i) => i,
2020            Err(0) => return None,
2021            Err(i) => i - 1,
2022        };
2023
2024        let mapping = &line_mappings[idx];
2025
2026        if mapping.source == NO_SOURCE {
2027            return None;
2028        }
2029
2030        Some(OriginalLocation {
2031            source: mapping.source,
2032            line: mapping.original_line,
2033            column: mapping.original_column,
2034            name: if mapping.name == NO_NAME { None } else { Some(mapping.name) },
2035        })
2036    }
2037
2038    /// Number of generated lines in the source map.
2039    #[inline]
2040    pub fn line_count(&self) -> usize {
2041        self.line_info.len()
2042    }
2043
2044    /// Resolve a source index to its filename.
2045    ///
2046    /// # Panics
2047    ///
2048    /// Panics if `index` is out of bounds. Use [`get_source`](Self::get_source)
2049    /// for a non-panicking alternative.
2050    #[inline]
2051    pub fn source(&self, index: u32) -> &str {
2052        &self.sources[index as usize]
2053    }
2054
2055    /// Resolve a source index to its filename, returning `None` if out of bounds.
2056    #[inline]
2057    pub fn get_source(&self, index: u32) -> Option<&str> {
2058        self.sources.get(index as usize).map(|s| s.as_str())
2059    }
2060
2061    /// Resolve a name index to its string.
2062    ///
2063    /// # Panics
2064    ///
2065    /// Panics if `index` is out of bounds. Use [`get_name`](Self::get_name)
2066    /// for a non-panicking alternative.
2067    #[inline]
2068    pub fn name(&self, index: u32) -> &str {
2069        &self.names[index as usize]
2070    }
2071
2072    /// Resolve a name index to its string, returning `None` if out of bounds.
2073    #[inline]
2074    pub fn get_name(&self, index: u32) -> Option<&str> {
2075        self.names.get(index as usize).map(|s| s.as_str())
2076    }
2077
2078    /// Find the source index for a filename.
2079    #[inline]
2080    pub fn source_index(&self, name: &str) -> Option<u32> {
2081        self.source_map.get(name).copied()
2082    }
2083
2084    /// Get all mappings for a line (decoding on demand).
2085    pub fn mappings_for_line(&self, line: u32) -> Vec<Mapping> {
2086        self.decode_line(line).unwrap_or_default()
2087    }
2088
2089    /// Fully decode all mappings into a regular `SourceMap`.
2090    ///
2091    /// Useful when you need the full map after lazy exploration.
2092    pub fn into_sourcemap(self) -> Result<SourceMap, ParseError> {
2093        let (mappings, line_offsets) = decode_mappings(&self.raw_mappings)?;
2094        let has_range_mappings = mappings.iter().any(|m| m.is_range_mapping);
2095
2096        Ok(SourceMap {
2097            file: self.file,
2098            source_root: self.source_root,
2099            sources: self.sources.clone(),
2100            sources_content: self.sources_content,
2101            names: self.names,
2102            ignore_list: self.ignore_list,
2103            extensions: self.extensions,
2104            debug_id: self.debug_id,
2105            scopes: self.scopes,
2106            mappings,
2107            line_offsets,
2108            reverse_index: OnceCell::new(),
2109            source_map: self.source_map,
2110            has_range_mappings,
2111        })
2112    }
2113}
2114
2115/// Pre-scan the raw mappings string to find semicolon positions and compute
2116/// cumulative VLQ state at each line boundary.
2117fn prescan_mappings(input: &str) -> Result<Vec<LineInfo>, DecodeError> {
2118    if input.is_empty() {
2119        return Ok(Vec::new());
2120    }
2121
2122    let bytes = input.as_bytes();
2123    let len = bytes.len();
2124
2125    // Count lines for pre-allocation
2126    let line_count = bytes.iter().filter(|&&b| b == b';').count() + 1;
2127    let mut line_info: Vec<LineInfo> = Vec::with_capacity(line_count);
2128
2129    let mut state = VlqState::default();
2130    let mut pos: usize = 0;
2131
2132    loop {
2133        let line_start = pos;
2134        let line_state = state;
2135        while pos < len && bytes[pos] != b';' {
2136            pos += 1;
2137        }
2138        let byte_end = pos;
2139        state = walk_vlq_state(bytes, line_start, byte_end, state)?;
2140
2141        line_info.push(LineInfo { byte_offset: line_start, byte_end, state: line_state });
2142
2143        if pos >= len {
2144            break;
2145        }
2146        pos += 1;
2147    }
2148
2149    Ok(line_info)
2150}
2151
2152/// Walk VLQ bytes for a line to compute end state, without producing Mapping structs.
2153fn walk_vlq_state(
2154    bytes: &[u8],
2155    start: usize,
2156    end: usize,
2157    mut state: VlqState,
2158) -> Result<VlqState, DecodeError> {
2159    let mut pos = start;
2160    while pos < end {
2161        let byte = bytes[pos];
2162        if byte == b',' {
2163            pos += 1;
2164            continue;
2165        }
2166
2167        // Field 1: generated column (skip, resets per line)
2168        vlq_fast(bytes, &mut pos)?;
2169
2170        if pos < end && bytes[pos] != b',' && bytes[pos] != b';' {
2171            state.source_index += vlq_fast(bytes, &mut pos)?;
2172            if pos >= end || bytes[pos] == b',' || bytes[pos] == b';' {
2173                return Err(DecodeError::InvalidSegmentLength { fields: 2, offset: pos });
2174            }
2175            state.original_line += vlq_fast(bytes, &mut pos)?;
2176            if pos >= end || bytes[pos] == b',' || bytes[pos] == b';' {
2177                return Err(DecodeError::InvalidSegmentLength { fields: 3, offset: pos });
2178            }
2179            state.original_column += vlq_fast(bytes, &mut pos)?;
2180            if pos < end && bytes[pos] != b',' && bytes[pos] != b';' {
2181                state.name_index += vlq_fast(bytes, &mut pos)?;
2182            }
2183        }
2184    }
2185    Ok(state)
2186}
2187
2188/// Fast scan: single-pass scan to find semicolons and record line byte offsets.
2189/// No VLQ decoding at all. VlqState is zeroed — must be computed progressively.
2190fn fast_scan_lines(input: &str) -> Vec<LineInfo> {
2191    if input.is_empty() {
2192        return Vec::new();
2193    }
2194
2195    let bytes = input.as_bytes();
2196    let len = bytes.len();
2197    let zero_state =
2198        VlqState { source_index: 0, original_line: 0, original_column: 0, name_index: 0 };
2199
2200    // Single pass: grow dynamically instead of double-scanning for semicolon count
2201    let mut line_info = Vec::new();
2202    let mut pos = 0;
2203    loop {
2204        let line_start = pos;
2205
2206        // Scan to next semicolon or end of string
2207        while pos < len && bytes[pos] != b';' {
2208            pos += 1;
2209        }
2210
2211        line_info.push(LineInfo {
2212            byte_offset: line_start,
2213            byte_end: pos,
2214            state: zero_state, // Will be computed progressively on demand
2215        });
2216
2217        if pos >= len {
2218            break;
2219        }
2220        pos += 1; // skip ';'
2221    }
2222
2223    line_info
2224}
2225
2226/// Result of parsing a sourceMappingURL reference.
2227#[derive(Debug, Clone, PartialEq, Eq)]
2228pub enum SourceMappingUrl {
2229    /// An inline base64 data URI containing the source map JSON.
2230    Inline(String),
2231    /// An external URL or relative path to the source map file.
2232    External(String),
2233}
2234
2235/// Extract the sourceMappingURL from generated source code.
2236///
2237/// Looks for `//# sourceMappingURL=<url>` or `//@ sourceMappingURL=<url>` comments.
2238/// For inline data URIs (`data:application/json;base64,...`), decodes the base64 content.
2239/// Returns `None` if no sourceMappingURL is found.
2240pub fn parse_source_mapping_url(source: &str) -> Option<SourceMappingUrl> {
2241    // Search backwards from the end (sourceMappingURL is typically the last line)
2242    for line in source.lines().rev() {
2243        let trimmed = line.trim();
2244        let url = if let Some(rest) = trimmed.strip_prefix("//# sourceMappingURL=") {
2245            rest.trim()
2246        } else if let Some(rest) = trimmed.strip_prefix("//@ sourceMappingURL=") {
2247            rest.trim()
2248        } else if let Some(rest) = trimmed.strip_prefix("/*# sourceMappingURL=") {
2249            rest.trim_end_matches("*/").trim()
2250        } else if let Some(rest) = trimmed.strip_prefix("/*@ sourceMappingURL=") {
2251            rest.trim_end_matches("*/").trim()
2252        } else {
2253            continue;
2254        };
2255
2256        if url.is_empty() {
2257            continue;
2258        }
2259
2260        // Check for inline data URI
2261        if let Some(base64_data) = url
2262            .strip_prefix("data:application/json;base64,")
2263            .or_else(|| url.strip_prefix("data:application/json;charset=utf-8;base64,"))
2264            .or_else(|| url.strip_prefix("data:application/json;charset=UTF-8;base64,"))
2265        {
2266            // Decode base64
2267            let decoded = base64_decode(base64_data);
2268            if let Some(json) = decoded {
2269                return Some(SourceMappingUrl::Inline(json));
2270            }
2271        }
2272
2273        return Some(SourceMappingUrl::External(url.to_string()));
2274    }
2275
2276    None
2277}
2278
2279/// Simple base64 decoder (no dependencies).
2280/// Decode percent-encoded strings (e.g. `%7B` → `{`).
2281fn percent_decode(input: &str) -> String {
2282    let mut output = Vec::with_capacity(input.len());
2283    let bytes = input.as_bytes();
2284    let mut i = 0;
2285    while i < bytes.len() {
2286        if bytes[i] == b'%'
2287            && i + 2 < bytes.len()
2288            && let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2]))
2289        {
2290            output.push((hi << 4) | lo);
2291            i += 3;
2292            continue;
2293        }
2294        output.push(bytes[i]);
2295        i += 1;
2296    }
2297    String::from_utf8(output).unwrap_or_else(|_| input.to_string())
2298}
2299
2300fn hex_val(b: u8) -> Option<u8> {
2301    match b {
2302        b'0'..=b'9' => Some(b - b'0'),
2303        b'a'..=b'f' => Some(b - b'a' + 10),
2304        b'A'..=b'F' => Some(b - b'A' + 10),
2305        _ => None,
2306    }
2307}
2308
2309fn base64_decode(input: &str) -> Option<String> {
2310    let input = input.trim();
2311    let bytes: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
2312
2313    let mut output = Vec::with_capacity(bytes.len() * 3 / 4);
2314
2315    for chunk in bytes.chunks(4) {
2316        let mut buf = [0u8; 4];
2317        let mut len = 0;
2318
2319        for &b in chunk {
2320            if b == b'=' {
2321                break;
2322            }
2323            let val = match b {
2324                b'A'..=b'Z' => b - b'A',
2325                b'a'..=b'z' => b - b'a' + 26,
2326                b'0'..=b'9' => b - b'0' + 52,
2327                b'+' => 62,
2328                b'/' => 63,
2329                _ => return None,
2330            };
2331            buf[len] = val;
2332            len += 1;
2333        }
2334
2335        if len >= 2 {
2336            output.push((buf[0] << 2) | (buf[1] >> 4));
2337        }
2338        if len >= 3 {
2339            output.push((buf[1] << 4) | (buf[2] >> 2));
2340        }
2341        if len >= 4 {
2342            output.push((buf[2] << 6) | buf[3]);
2343        }
2344    }
2345
2346    String::from_utf8(output).ok()
2347}
2348
2349/// Validate a source map with deep structural checks.
2350///
2351/// Performs bounds checking, segment ordering verification, source resolution,
2352/// and unreferenced sources detection beyond basic JSON parsing.
2353pub fn validate_deep(sm: &SourceMap) -> Vec<String> {
2354    let mut warnings = Vec::new();
2355
2356    // Check segment ordering (must be sorted by generated position)
2357    let mut prev_line: u32 = 0;
2358    let mut prev_col: u32 = 0;
2359    let mappings = sm.all_mappings();
2360    for m in mappings {
2361        if m.generated_line < prev_line
2362            || (m.generated_line == prev_line && m.generated_column < prev_col)
2363        {
2364            warnings.push(format!(
2365                "mappings out of order at {}:{}",
2366                m.generated_line, m.generated_column
2367            ));
2368        }
2369        prev_line = m.generated_line;
2370        prev_col = m.generated_column;
2371    }
2372
2373    // Check source indices in bounds
2374    for m in mappings {
2375        if m.source != NO_SOURCE && m.source as usize >= sm.sources.len() {
2376            warnings.push(format!(
2377                "source index {} out of bounds (max {})",
2378                m.source,
2379                sm.sources.len()
2380            ));
2381        }
2382        if m.name != NO_NAME && m.name as usize >= sm.names.len() {
2383            warnings.push(format!("name index {} out of bounds (max {})", m.name, sm.names.len()));
2384        }
2385    }
2386
2387    // Check ignoreList indices in bounds
2388    for &idx in &sm.ignore_list {
2389        if idx as usize >= sm.sources.len() {
2390            warnings.push(format!(
2391                "ignoreList index {} out of bounds (max {})",
2392                idx,
2393                sm.sources.len()
2394            ));
2395        }
2396    }
2397
2398    // Detect unreferenced sources
2399    let mut referenced_sources = std::collections::HashSet::new();
2400    for m in mappings {
2401        if m.source != NO_SOURCE {
2402            referenced_sources.insert(m.source);
2403        }
2404    }
2405    for (i, source) in sm.sources.iter().enumerate() {
2406        if !referenced_sources.contains(&(i as u32)) {
2407            warnings.push(format!("source \"{source}\" (index {i}) is unreferenced"));
2408        }
2409    }
2410
2411    warnings
2412}
2413
2414/// Append a JSON-quoted string to the output buffer.
2415fn json_quote_into(out: &mut String, s: &str) {
2416    let bytes = s.as_bytes();
2417    out.push('"');
2418
2419    let mut start = 0;
2420    for (i, &b) in bytes.iter().enumerate() {
2421        let escape = match b {
2422            b'"' => "\\\"",
2423            b'\\' => "\\\\",
2424            b'\n' => "\\n",
2425            b'\r' => "\\r",
2426            b'\t' => "\\t",
2427            // Remaining control chars (excluding \n, \r, \t handled above)
2428            0x00..=0x08 | 0x0b | 0x0c | 0x0e..=0x1f => {
2429                if start < i {
2430                    out.push_str(&s[start..i]);
2431                }
2432                use std::fmt::Write;
2433                let _ = write!(out, "\\u{:04x}", b);
2434                start = i + 1;
2435                continue;
2436            }
2437            _ => continue,
2438        };
2439        if start < i {
2440            out.push_str(&s[start..i]);
2441        }
2442        out.push_str(escape);
2443        start = i + 1;
2444    }
2445
2446    if start < bytes.len() {
2447        out.push_str(&s[start..]);
2448    }
2449
2450    out.push('"');
2451}
2452
2453// ── Internal: decode VLQ mappings directly into flat Mapping vec ───
2454
2455/// Base64 decode lookup table (byte → 6-bit value, 0xFF = invalid).
2456const B64: [u8; 128] = {
2457    let mut table = [0xFFu8; 128];
2458    let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2459    let mut i = 0u8;
2460    while i < 64 {
2461        table[chars[i as usize] as usize] = i;
2462        i += 1;
2463    }
2464    table
2465};
2466
2467/// Inline VLQ decode optimized for the hot path (no function call overhead).
2468/// Most source map VLQ values fit in 1-2 base64 characters.
2469#[inline(always)]
2470fn vlq_fast(bytes: &[u8], pos: &mut usize) -> Result<i64, DecodeError> {
2471    let p = *pos;
2472    if p >= bytes.len() {
2473        return Err(DecodeError::UnexpectedEof { offset: p });
2474    }
2475
2476    let b0 = bytes[p];
2477    if b0 >= 128 {
2478        return Err(DecodeError::InvalidBase64 { byte: b0, offset: p });
2479    }
2480    let d0 = B64[b0 as usize];
2481    if d0 == 0xFF {
2482        return Err(DecodeError::InvalidBase64 { byte: b0, offset: p });
2483    }
2484
2485    // Fast path: single character VLQ (values -15..15)
2486    if (d0 & 0x20) == 0 {
2487        *pos = p + 1;
2488        let val = (d0 >> 1) as i64;
2489        return Ok(if (d0 & 1) != 0 { -val } else { val });
2490    }
2491
2492    // Multi-character VLQ
2493    let mut result: u64 = (d0 & 0x1F) as u64;
2494    let mut shift: u32 = 5;
2495    let mut i = p + 1;
2496
2497    loop {
2498        if i >= bytes.len() {
2499            return Err(DecodeError::UnexpectedEof { offset: i });
2500        }
2501        let b = bytes[i];
2502        if b >= 128 {
2503            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2504        }
2505        let d = B64[b as usize];
2506        if d == 0xFF {
2507            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2508        }
2509        i += 1;
2510
2511        if shift >= 60 {
2512            return Err(DecodeError::VlqOverflow { offset: p });
2513        }
2514
2515        result += ((d & 0x1F) as u64) << shift;
2516        shift += 5;
2517
2518        if (d & 0x20) == 0 {
2519            break;
2520        }
2521    }
2522
2523    *pos = i;
2524    let value = if (result & 1) == 1 { -((result >> 1) as i64) } else { (result >> 1) as i64 };
2525    Ok(value)
2526}
2527
2528#[inline(always)]
2529fn vlq_unsigned_fast(bytes: &[u8], pos: &mut usize) -> Result<u64, DecodeError> {
2530    let p = *pos;
2531    if p >= bytes.len() {
2532        return Err(DecodeError::UnexpectedEof { offset: p });
2533    }
2534    let b0 = bytes[p];
2535    if b0 >= 128 {
2536        return Err(DecodeError::InvalidBase64 { byte: b0, offset: p });
2537    }
2538    let d0 = B64[b0 as usize];
2539    if d0 == 0xFF {
2540        return Err(DecodeError::InvalidBase64 { byte: b0, offset: p });
2541    }
2542    if (d0 & 0x20) == 0 {
2543        *pos = p + 1;
2544        return Ok(d0 as u64);
2545    }
2546    let mut result: u64 = (d0 & 0x1F) as u64;
2547    let mut shift: u32 = 5;
2548    let mut i = p + 1;
2549    loop {
2550        if i >= bytes.len() {
2551            return Err(DecodeError::UnexpectedEof { offset: i });
2552        }
2553        let b = bytes[i];
2554        if b >= 128 {
2555            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2556        }
2557        let d = B64[b as usize];
2558        if d == 0xFF {
2559            return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
2560        }
2561        i += 1;
2562        if shift >= 60 {
2563            return Err(DecodeError::VlqOverflow { offset: p });
2564        }
2565        result |= ((d & 0x1F) as u64) << shift;
2566        shift += 5;
2567        if (d & 0x20) == 0 {
2568            break;
2569        }
2570    }
2571    *pos = i;
2572    Ok(result)
2573}
2574
2575fn decode_range_mappings(
2576    input: &str,
2577    mappings: &mut [Mapping],
2578    line_offsets: &[u32],
2579) -> Result<(), DecodeError> {
2580    let bytes = input.as_bytes();
2581    let len = bytes.len();
2582    let mut pos: usize = 0;
2583    let mut generated_line: usize = 0;
2584    while pos < len {
2585        let line_start = if generated_line + 1 < line_offsets.len() {
2586            line_offsets[generated_line] as usize
2587        } else {
2588            break;
2589        };
2590        // Bound range marking to this line's mappings only
2591        let line_end = if generated_line + 2 < line_offsets.len() {
2592            line_offsets[generated_line + 1] as usize
2593        } else {
2594            mappings.len()
2595        };
2596        let mut mapping_index: u64 = 0;
2597        while pos < len {
2598            let byte = bytes[pos];
2599            if byte == b';' {
2600                pos += 1;
2601                break;
2602            }
2603            if byte == b',' {
2604                pos += 1;
2605                continue;
2606            }
2607            let offset = vlq_unsigned_fast(bytes, &mut pos)?;
2608            mapping_index += offset;
2609            let abs_idx = line_start + mapping_index as usize;
2610            if abs_idx < line_end {
2611                mappings[abs_idx].is_range_mapping = true;
2612            }
2613        }
2614        generated_line += 1;
2615    }
2616    Ok(())
2617}
2618
2619#[derive(Default)]
2620struct MappingsDecodeState {
2621    source_index: i64,
2622    original_line: i64,
2623    original_column: i64,
2624    name_index: i64,
2625}
2626
2627fn decode_mapping_segment(
2628    bytes: &[u8],
2629    pos: &mut usize,
2630    generated_line: u32,
2631    generated_column: &mut i64,
2632    state: &mut MappingsDecodeState,
2633) -> Result<Mapping, DecodeError> {
2634    *generated_column += vlq_fast(bytes, pos)?;
2635
2636    if *pos < bytes.len() && bytes[*pos] != b',' && bytes[*pos] != b';' {
2637        state.source_index += vlq_fast(bytes, pos)?;
2638
2639        // Reject 2-field segments (only 1, 4, or 5 are valid per ECMA-426)
2640        if *pos >= bytes.len() || bytes[*pos] == b',' || bytes[*pos] == b';' {
2641            return Err(DecodeError::InvalidSegmentLength { fields: 2, offset: *pos });
2642        }
2643
2644        state.original_line += vlq_fast(bytes, pos)?;
2645
2646        // Reject 3-field segments (only 1, 4, or 5 are valid per ECMA-426)
2647        if *pos >= bytes.len() || bytes[*pos] == b',' || bytes[*pos] == b';' {
2648            return Err(DecodeError::InvalidSegmentLength { fields: 3, offset: *pos });
2649        }
2650
2651        state.original_column += vlq_fast(bytes, pos)?;
2652
2653        let name = if *pos < bytes.len() && bytes[*pos] != b',' && bytes[*pos] != b';' {
2654            state.name_index += vlq_fast(bytes, pos)?;
2655            state.name_index as u32
2656        } else {
2657            NO_NAME
2658        };
2659
2660        Ok(Mapping {
2661            generated_line,
2662            generated_column: *generated_column as u32,
2663            source: state.source_index as u32,
2664            original_line: state.original_line as u32,
2665            original_column: state.original_column as u32,
2666            name,
2667            is_range_mapping: false,
2668        })
2669    } else {
2670        Ok(Mapping {
2671            generated_line,
2672            generated_column: *generated_column as u32,
2673            source: NO_SOURCE,
2674            original_line: 0,
2675            original_column: 0,
2676            name: NO_NAME,
2677            is_range_mapping: false,
2678        })
2679    }
2680}
2681
2682fn build_range_line_offsets(
2683    start_line: u32,
2684    end_line: u32,
2685    line_starts: &[(u32, u32)],
2686    total: u32,
2687) -> Vec<u32> {
2688    let mut line_offsets: Vec<u32> = vec![total; end_line as usize + 1];
2689
2690    for offset in line_offsets.iter_mut().take(start_line as usize + 1) {
2691        *offset = 0;
2692    }
2693
2694    for &(line, offset) in line_starts {
2695        line_offsets[line as usize] = offset;
2696    }
2697
2698    let mut next_offset = total;
2699    for i in (start_line as usize..end_line as usize).rev() {
2700        if line_offsets[i] == total {
2701            line_offsets[i] = next_offset;
2702        } else {
2703            next_offset = line_offsets[i];
2704        }
2705    }
2706
2707    line_offsets
2708}
2709
2710fn decode_mappings(input: &str) -> Result<(Vec<Mapping>, Vec<u32>), DecodeError> {
2711    if input.is_empty() {
2712        return Ok((Vec::new(), vec![0]));
2713    }
2714
2715    let bytes = input.as_bytes();
2716    let len = bytes.len();
2717
2718    // Pre-count lines and segments in a single pass for capacity hints
2719    let mut semicolons = 0usize;
2720    let mut commas = 0usize;
2721    for &b in bytes {
2722        semicolons += (b == b';') as usize;
2723        commas += (b == b',') as usize;
2724    }
2725    let line_count = semicolons + 1;
2726    let approx_segments = commas + line_count;
2727
2728    let mut mappings: Vec<Mapping> = Vec::with_capacity(approx_segments);
2729    let mut line_offsets: Vec<u32> = Vec::with_capacity(line_count + 1);
2730
2731    let mut state = MappingsDecodeState::default();
2732    let mut generated_line: u32 = 0;
2733    let mut pos: usize = 0;
2734
2735    loop {
2736        line_offsets.push(mappings.len() as u32);
2737        let mut generated_column: i64 = 0;
2738        let mut saw_semicolon = false;
2739
2740        while pos < len {
2741            let byte = bytes[pos];
2742
2743            if byte == b';' {
2744                pos += 1;
2745                saw_semicolon = true;
2746                break;
2747            }
2748
2749            if byte == b',' {
2750                pos += 1;
2751                continue;
2752            }
2753
2754            mappings.push(decode_mapping_segment(
2755                bytes,
2756                &mut pos,
2757                generated_line,
2758                &mut generated_column,
2759                &mut state,
2760            )?);
2761        }
2762
2763        if !saw_semicolon {
2764            break;
2765        }
2766        generated_line += 1;
2767    }
2768
2769    // Sentinel for line range computation
2770    line_offsets.push(mappings.len() as u32);
2771
2772    Ok((mappings, line_offsets))
2773}
2774
2775/// Decode VLQ mappings for a subset of lines `[start_line, end_line)`.
2776///
2777/// Walks VLQ state for all lines up to `end_line`, but only allocates Mapping
2778/// structs for lines in the requested range. The returned `line_offsets` is
2779/// indexed by the actual generated line number (not relative to start_line),
2780/// so that `mappings_for_line(line)` works correctly with the real line values.
2781fn decode_mappings_range(
2782    input: &str,
2783    start_line: u32,
2784    end_line: u32,
2785) -> Result<(Vec<Mapping>, Vec<u32>), DecodeError> {
2786    // Cap end_line against actual line count to prevent OOM on pathological input.
2787    // Count semicolons to determine actual line count.
2788    let actual_lines = if input.is_empty() {
2789        0u32
2790    } else {
2791        input.as_bytes().iter().filter(|&&b| b == b';').count() as u32 + 1
2792    };
2793    let end_line = end_line.min(actual_lines);
2794
2795    if input.is_empty() || start_line >= end_line {
2796        return Ok((Vec::new(), vec![0; end_line as usize + 1]));
2797    }
2798
2799    let bytes = input.as_bytes();
2800    let len = bytes.len();
2801
2802    let mut mappings: Vec<Mapping> = Vec::new();
2803
2804    let mut state = MappingsDecodeState::default();
2805    let mut generated_line: u32 = 0;
2806    let mut pos: usize = 0;
2807
2808    let mut line_starts: Vec<(u32, u32)> =
2809        Vec::with_capacity((end_line - start_line).min(actual_lines) as usize);
2810
2811    loop {
2812        let in_range = generated_line >= start_line && generated_line < end_line;
2813        if in_range {
2814            line_starts.push((generated_line, mappings.len() as u32));
2815        }
2816
2817        let mut generated_column: i64 = 0;
2818        let mut saw_semicolon = false;
2819
2820        while pos < len {
2821            let byte = bytes[pos];
2822
2823            if byte == b';' {
2824                pos += 1;
2825                saw_semicolon = true;
2826                break;
2827            }
2828
2829            if byte == b',' {
2830                pos += 1;
2831                continue;
2832            }
2833
2834            let mapping = decode_mapping_segment(
2835                bytes,
2836                &mut pos,
2837                generated_line,
2838                &mut generated_column,
2839                &mut state,
2840            )?;
2841            if in_range {
2842                mappings.push(mapping);
2843            }
2844        }
2845
2846        if !saw_semicolon {
2847            break;
2848        }
2849        generated_line += 1;
2850
2851        // Stop early once we've passed end_line
2852        if generated_line >= end_line {
2853            break;
2854        }
2855    }
2856
2857    let total = mappings.len() as u32;
2858    Ok((mappings, build_range_line_offsets(start_line, end_line, &line_starts, total)))
2859}
2860
2861/// Build reverse index: mapping indices sorted by (source, original_line, original_column).
2862fn build_reverse_index(mappings: &[Mapping]) -> Vec<u32> {
2863    let mut indices: Vec<u32> =
2864        (0..mappings.len() as u32).filter(|&i| mappings[i as usize].source != NO_SOURCE).collect();
2865
2866    indices.sort_unstable_by(|&a, &b| {
2867        let ma = &mappings[a as usize];
2868        let mb = &mappings[b as usize];
2869        ma.source
2870            .cmp(&mb.source)
2871            .then(ma.original_line.cmp(&mb.original_line))
2872            .then(ma.original_column.cmp(&mb.original_column))
2873            .then(ma.generated_line.cmp(&mb.generated_line))
2874            .then(ma.generated_column.cmp(&mb.generated_column))
2875    });
2876
2877    indices
2878}
2879
2880// ── Streaming iterator ────────────────────────────────────────────
2881
2882/// Lazy iterator over VLQ-encoded source map mappings.
2883///
2884/// Decodes one mapping at a time without allocating a full `Vec<Mapping>`.
2885/// Useful for streaming composition pipelines where intermediate allocation
2886/// is undesirable.
2887///
2888/// # Examples
2889///
2890/// ```
2891/// use srcmap_sourcemap::MappingsIter;
2892///
2893/// let vlq = "AAAA;AACA,EAAA;AACA";
2894/// let mappings: Vec<_> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
2895/// assert_eq!(mappings.len(), 4);
2896/// assert_eq!(mappings[0].generated_line, 0);
2897/// assert_eq!(mappings[1].generated_line, 1);
2898/// ```
2899pub struct MappingsIter<'a> {
2900    bytes: &'a [u8],
2901    len: usize,
2902    pos: usize,
2903    source_index: i64,
2904    original_line: i64,
2905    original_column: i64,
2906    name_index: i64,
2907    generated_line: u32,
2908    generated_column: i64,
2909    done: bool,
2910}
2911
2912impl<'a> MappingsIter<'a> {
2913    /// Create a new iterator over VLQ-encoded mappings.
2914    pub fn new(vlq: &'a str) -> Self {
2915        let bytes = vlq.as_bytes();
2916        Self {
2917            bytes,
2918            len: bytes.len(),
2919            pos: 0,
2920            source_index: 0,
2921            original_line: 0,
2922            original_column: 0,
2923            name_index: 0,
2924            generated_line: 0,
2925            generated_column: 0,
2926            done: false,
2927        }
2928    }
2929}
2930
2931impl Iterator for MappingsIter<'_> {
2932    type Item = Result<Mapping, DecodeError>;
2933
2934    fn next(&mut self) -> Option<Self::Item> {
2935        if self.done {
2936            return None;
2937        }
2938
2939        loop {
2940            if self.pos >= self.len {
2941                self.done = true;
2942                return None;
2943            }
2944
2945            let byte = self.bytes[self.pos];
2946
2947            if byte == b';' {
2948                self.pos += 1;
2949                self.generated_line += 1;
2950                self.generated_column = 0;
2951                continue;
2952            }
2953
2954            if byte == b',' {
2955                self.pos += 1;
2956                continue;
2957            }
2958
2959            // Field 1: generated column
2960            match vlq_fast(self.bytes, &mut self.pos) {
2961                Ok(delta) => self.generated_column += delta,
2962                Err(e) => {
2963                    self.done = true;
2964                    return Some(Err(e));
2965                }
2966            }
2967
2968            if self.pos < self.len && self.bytes[self.pos] != b',' && self.bytes[self.pos] != b';' {
2969                // Field 2: source index
2970                match vlq_fast(self.bytes, &mut self.pos) {
2971                    Ok(delta) => self.source_index += delta,
2972                    Err(e) => {
2973                        self.done = true;
2974                        return Some(Err(e));
2975                    }
2976                }
2977                // Reject 2-field segments (only 1, 4, or 5 are valid per ECMA-426)
2978                if self.pos >= self.len
2979                    || self.bytes[self.pos] == b','
2980                    || self.bytes[self.pos] == b';'
2981                {
2982                    self.done = true;
2983                    return Some(Err(DecodeError::InvalidSegmentLength {
2984                        fields: 2,
2985                        offset: self.pos,
2986                    }));
2987                }
2988                // Field 3: original line
2989                match vlq_fast(self.bytes, &mut self.pos) {
2990                    Ok(delta) => self.original_line += delta,
2991                    Err(e) => {
2992                        self.done = true;
2993                        return Some(Err(e));
2994                    }
2995                }
2996                // Reject 3-field segments (only 1, 4, or 5 are valid per ECMA-426)
2997                if self.pos >= self.len
2998                    || self.bytes[self.pos] == b','
2999                    || self.bytes[self.pos] == b';'
3000                {
3001                    self.done = true;
3002                    return Some(Err(DecodeError::InvalidSegmentLength {
3003                        fields: 3,
3004                        offset: self.pos,
3005                    }));
3006                }
3007                // Field 4: original column
3008                match vlq_fast(self.bytes, &mut self.pos) {
3009                    Ok(delta) => self.original_column += delta,
3010                    Err(e) => {
3011                        self.done = true;
3012                        return Some(Err(e));
3013                    }
3014                }
3015
3016                // Field 5: name (optional)
3017                let name = if self.pos < self.len
3018                    && self.bytes[self.pos] != b','
3019                    && self.bytes[self.pos] != b';'
3020                {
3021                    match vlq_fast(self.bytes, &mut self.pos) {
3022                        Ok(delta) => {
3023                            self.name_index += delta;
3024                            self.name_index as u32
3025                        }
3026                        Err(e) => {
3027                            self.done = true;
3028                            return Some(Err(e));
3029                        }
3030                    }
3031                } else {
3032                    NO_NAME
3033                };
3034
3035                return Some(Ok(Mapping {
3036                    generated_line: self.generated_line,
3037                    generated_column: self.generated_column as u32,
3038                    source: self.source_index as u32,
3039                    original_line: self.original_line as u32,
3040                    original_column: self.original_column as u32,
3041                    name,
3042                    is_range_mapping: false,
3043                }));
3044            } else {
3045                // 1-field segment: no source info
3046                return Some(Ok(Mapping {
3047                    generated_line: self.generated_line,
3048                    generated_column: self.generated_column as u32,
3049                    source: NO_SOURCE,
3050                    original_line: 0,
3051                    original_column: 0,
3052                    name: NO_NAME,
3053                    is_range_mapping: false,
3054                }));
3055            }
3056        }
3057    }
3058}
3059
3060// ── Builder ────────────────────────────────────────────────────────
3061
3062/// Builder for incrementally constructing a [`SourceMap`] from iterators.
3063///
3064/// Avoids the need to pre-collect sources, names, and mappings into `Vec`s.
3065/// Delegates to [`SourceMap::from_parts`] internally.
3066#[must_use]
3067pub struct SourceMapBuilder {
3068    file: Option<String>,
3069    source_root: Option<String>,
3070    sources: Vec<String>,
3071    sources_content: Vec<Option<String>>,
3072    names: Vec<String>,
3073    mappings: Vec<Mapping>,
3074    ignore_list: Vec<u32>,
3075    debug_id: Option<String>,
3076    scopes: Option<ScopeInfo>,
3077}
3078
3079impl SourceMapBuilder {
3080    pub fn new() -> Self {
3081        Self {
3082            file: None,
3083            source_root: None,
3084            sources: Vec::new(),
3085            sources_content: Vec::new(),
3086            names: Vec::new(),
3087            mappings: Vec::new(),
3088            ignore_list: Vec::new(),
3089            debug_id: None,
3090            scopes: None,
3091        }
3092    }
3093
3094    pub fn file(mut self, file: impl Into<String>) -> Self {
3095        self.file = Some(file.into());
3096        self
3097    }
3098
3099    pub fn source_root(mut self, root: impl Into<String>) -> Self {
3100        self.source_root = Some(root.into());
3101        self
3102    }
3103
3104    pub fn sources(mut self, sources: impl IntoIterator<Item = impl Into<String>>) -> Self {
3105        self.sources = sources.into_iter().map(Into::into).collect();
3106        self
3107    }
3108
3109    pub fn sources_content(
3110        mut self,
3111        content: impl IntoIterator<Item = Option<impl Into<String>>>,
3112    ) -> Self {
3113        self.sources_content = content.into_iter().map(|c| c.map(Into::into)).collect();
3114        self
3115    }
3116
3117    pub fn names(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
3118        self.names = names.into_iter().map(Into::into).collect();
3119        self
3120    }
3121
3122    pub fn mappings(mut self, mappings: impl IntoIterator<Item = Mapping>) -> Self {
3123        self.mappings = mappings.into_iter().collect();
3124        self
3125    }
3126
3127    pub fn ignore_list(mut self, list: impl IntoIterator<Item = u32>) -> Self {
3128        self.ignore_list = list.into_iter().collect();
3129        self
3130    }
3131
3132    pub fn debug_id(mut self, id: impl Into<String>) -> Self {
3133        self.debug_id = Some(id.into());
3134        self
3135    }
3136
3137    pub fn scopes(mut self, scopes: ScopeInfo) -> Self {
3138        self.scopes = Some(scopes);
3139        self
3140    }
3141
3142    /// Consume the builder and produce a [`SourceMap`].
3143    ///
3144    /// Mappings must be sorted by (generated_line, generated_column).
3145    pub fn build(self) -> SourceMap {
3146        SourceMap::from_parts(
3147            self.file,
3148            self.source_root,
3149            self.sources,
3150            self.sources_content,
3151            self.names,
3152            self.mappings,
3153            self.ignore_list,
3154            self.debug_id,
3155            self.scopes,
3156        )
3157    }
3158}
3159
3160impl Default for SourceMapBuilder {
3161    fn default() -> Self {
3162        Self::new()
3163    }
3164}
3165
3166// ── Tests ──────────────────────────────────────────────────────────
3167
3168#[cfg(test)]
3169mod tests {
3170    use super::*;
3171
3172    fn simple_map() -> &'static str {
3173        r#"{"version":3,"sources":["input.js"],"names":["hello"],"mappings":"AAAA;AACA,EAAA;AACA"}"#
3174    }
3175
3176    #[test]
3177    fn parse_basic() {
3178        let sm = SourceMap::from_json(simple_map()).unwrap();
3179        assert_eq!(sm.sources, vec!["input.js"]);
3180        assert_eq!(sm.names, vec!["hello"]);
3181        assert_eq!(sm.line_count(), 3);
3182        assert!(sm.mapping_count() > 0);
3183    }
3184
3185    #[test]
3186    fn to_json_roundtrip() {
3187        let json = simple_map();
3188        let sm = SourceMap::from_json(json).unwrap();
3189        let output = sm.to_json();
3190
3191        // Parse the output back and verify it produces identical lookups
3192        let sm2 = SourceMap::from_json(&output).unwrap();
3193        assert_eq!(sm2.sources, sm.sources);
3194        assert_eq!(sm2.names, sm.names);
3195        assert_eq!(sm2.mapping_count(), sm.mapping_count());
3196        assert_eq!(sm2.line_count(), sm.line_count());
3197
3198        // Verify all lookups match
3199        for m in sm.all_mappings() {
3200            let loc1 = sm.original_position_for(m.generated_line, m.generated_column);
3201            let loc2 = sm2.original_position_for(m.generated_line, m.generated_column);
3202            match (loc1, loc2) {
3203                (Some(a), Some(b)) => {
3204                    assert_eq!(a.source, b.source);
3205                    assert_eq!(a.line, b.line);
3206                    assert_eq!(a.column, b.column);
3207                    assert_eq!(a.name, b.name);
3208                }
3209                (None, None) => {}
3210                _ => panic!("lookup mismatch at ({}, {})", m.generated_line, m.generated_column),
3211            }
3212        }
3213    }
3214
3215    #[test]
3216    fn to_json_roundtrip_large() {
3217        let json = generate_test_sourcemap(50, 10, 3);
3218        let sm = SourceMap::from_json(&json).unwrap();
3219        let output = sm.to_json();
3220        let sm2 = SourceMap::from_json(&output).unwrap();
3221
3222        assert_eq!(sm2.mapping_count(), sm.mapping_count());
3223
3224        // Spot-check lookups
3225        for line in (0..sm.line_count() as u32).step_by(5) {
3226            for col in [0u32, 10, 20, 50] {
3227                let a = sm.original_position_for(line, col);
3228                let b = sm2.original_position_for(line, col);
3229                match (a, b) {
3230                    (Some(a), Some(b)) => {
3231                        assert_eq!(a.source, b.source);
3232                        assert_eq!(a.line, b.line);
3233                        assert_eq!(a.column, b.column);
3234                    }
3235                    (None, None) => {}
3236                    _ => panic!("mismatch at ({line}, {col})"),
3237                }
3238            }
3239        }
3240    }
3241
3242    #[test]
3243    fn to_json_preserves_fields() {
3244        let json = r#"{"version":3,"file":"out.js","sourceRoot":"src/","sources":["app.ts"],"sourcesContent":["const x = 1;"],"names":["x"],"mappings":"AAAAA","ignoreList":[0]}"#;
3245        let sm = SourceMap::from_json(json).unwrap();
3246        let output = sm.to_json();
3247
3248        assert!(output.contains(r#""file":"out.js""#));
3249        assert!(output.contains(r#""sourceRoot":"src/""#));
3250        assert!(output.contains(r#""sourcesContent":["const x = 1;"]"#));
3251        assert!(output.contains(r#""ignoreList":[0]"#));
3252
3253        // Note: sources will have sourceRoot prepended
3254        let sm2 = SourceMap::from_json(&output).unwrap();
3255        assert_eq!(sm2.file.as_deref(), Some("out.js"));
3256        assert_eq!(sm2.ignore_list, vec![0]);
3257    }
3258
3259    #[test]
3260    fn original_position_for_exact_match() {
3261        let sm = SourceMap::from_json(simple_map()).unwrap();
3262        let loc = sm.original_position_for(0, 0).unwrap();
3263        assert_eq!(loc.source, 0);
3264        assert_eq!(loc.line, 0);
3265        assert_eq!(loc.column, 0);
3266    }
3267
3268    #[test]
3269    fn original_position_for_column_within_segment() {
3270        let sm = SourceMap::from_json(simple_map()).unwrap();
3271        // Column 5 on line 1: should snap to the mapping at column 2
3272        let loc = sm.original_position_for(1, 5);
3273        assert!(loc.is_some());
3274    }
3275
3276    #[test]
3277    fn original_position_for_nonexistent_line() {
3278        let sm = SourceMap::from_json(simple_map()).unwrap();
3279        assert!(sm.original_position_for(999, 0).is_none());
3280    }
3281
3282    #[test]
3283    fn original_position_for_before_first_mapping() {
3284        // Line 1 first mapping is at column 2. Column 0 should return None.
3285        let sm = SourceMap::from_json(simple_map()).unwrap();
3286        let loc = sm.original_position_for(1, 0);
3287        // Column 0 on line 1: the first mapping at col 0 (AACA decodes to col=0, src delta=1...)
3288        // Actually depends on exact VLQ values. Let's just verify it doesn't crash.
3289        let _ = loc;
3290    }
3291
3292    #[test]
3293    fn generated_position_for_basic() {
3294        let sm = SourceMap::from_json(simple_map()).unwrap();
3295        let loc = sm.generated_position_for("input.js", 0, 0).unwrap();
3296        assert_eq!(loc.line, 0);
3297        assert_eq!(loc.column, 0);
3298    }
3299
3300    #[test]
3301    fn generated_position_for_unknown_source() {
3302        let sm = SourceMap::from_json(simple_map()).unwrap();
3303        assert!(sm.generated_position_for("nonexistent.js", 0, 0).is_none());
3304    }
3305
3306    #[test]
3307    fn parse_invalid_version() {
3308        let json = r#"{"version":2,"sources":[],"names":[],"mappings":""}"#;
3309        let err = SourceMap::from_json(json).unwrap_err();
3310        assert!(matches!(err, ParseError::InvalidVersion(2)));
3311    }
3312
3313    #[test]
3314    fn parse_empty_mappings() {
3315        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
3316        let sm = SourceMap::from_json(json).unwrap();
3317        assert_eq!(sm.mapping_count(), 0);
3318        assert!(sm.original_position_for(0, 0).is_none());
3319    }
3320
3321    #[test]
3322    fn parse_with_source_root() {
3323        let json = r#"{"version":3,"sourceRoot":"src/","sources":["foo.js"],"names":[],"mappings":"AAAA"}"#;
3324        let sm = SourceMap::from_json(json).unwrap();
3325        assert_eq!(sm.sources, vec!["src/foo.js"]);
3326    }
3327
3328    #[test]
3329    fn parse_with_sources_content() {
3330        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var x = 1;"],"names":[],"mappings":"AAAA"}"#;
3331        let sm = SourceMap::from_json(json).unwrap();
3332        assert_eq!(sm.sources_content, vec![Some("var x = 1;".to_string())]);
3333    }
3334
3335    #[test]
3336    fn mappings_for_line() {
3337        let sm = SourceMap::from_json(simple_map()).unwrap();
3338        let line0 = sm.mappings_for_line(0);
3339        assert!(!line0.is_empty());
3340        let empty = sm.mappings_for_line(999);
3341        assert!(empty.is_empty());
3342    }
3343
3344    #[test]
3345    fn large_sourcemap_lookup() {
3346        // Generate a realistic source map
3347        let json = generate_test_sourcemap(500, 20, 5);
3348        let sm = SourceMap::from_json(&json).unwrap();
3349
3350        // Verify lookups work across the whole map
3351        for line in [0, 10, 100, 250, 499] {
3352            let mappings = sm.mappings_for_line(line);
3353            if let Some(m) = mappings.first() {
3354                let loc = sm.original_position_for(line, m.generated_column);
3355                assert!(loc.is_some(), "lookup failed for line {line}");
3356            }
3357        }
3358    }
3359
3360    #[test]
3361    fn reverse_lookup_roundtrip() {
3362        let json = generate_test_sourcemap(100, 10, 3);
3363        let sm = SourceMap::from_json(&json).unwrap();
3364
3365        // Pick a mapping and verify forward + reverse roundtrip
3366        let mapping = &sm.mappings[50];
3367        if mapping.source != NO_SOURCE {
3368            let source_name = sm.source(mapping.source);
3369            let result = sm.generated_position_for(
3370                source_name,
3371                mapping.original_line,
3372                mapping.original_column,
3373            );
3374            assert!(result.is_some(), "reverse lookup failed");
3375        }
3376    }
3377
3378    #[test]
3379    fn all_generated_positions_for_basic() {
3380        let sm = SourceMap::from_json(simple_map()).unwrap();
3381        let results = sm.all_generated_positions_for("input.js", 0, 0);
3382        assert!(!results.is_empty(), "should find at least one position");
3383        assert_eq!(results[0].line, 0);
3384        assert_eq!(results[0].column, 0);
3385    }
3386
3387    #[test]
3388    fn all_generated_positions_for_unknown_source() {
3389        let sm = SourceMap::from_json(simple_map()).unwrap();
3390        let results = sm.all_generated_positions_for("nonexistent.js", 0, 0);
3391        assert!(results.is_empty());
3392    }
3393
3394    #[test]
3395    fn all_generated_positions_for_no_match() {
3396        let sm = SourceMap::from_json(simple_map()).unwrap();
3397        let results = sm.all_generated_positions_for("input.js", 999, 999);
3398        assert!(results.is_empty());
3399    }
3400
3401    #[test]
3402    fn encode_mappings_roundtrip() {
3403        let json = generate_test_sourcemap(50, 10, 3);
3404        let sm = SourceMap::from_json(&json).unwrap();
3405        let encoded = sm.encode_mappings();
3406        // Re-parse with encoded mappings
3407        let json2 = format!(
3408            r#"{{"version":3,"sources":{sources},"names":{names},"mappings":"{mappings}"}}"#,
3409            sources = serde_json::to_string(&sm.sources).unwrap(),
3410            names = serde_json::to_string(&sm.names).unwrap(),
3411            mappings = encoded,
3412        );
3413        let sm2 = SourceMap::from_json(&json2).unwrap();
3414        assert_eq!(sm2.mapping_count(), sm.mapping_count());
3415    }
3416
3417    #[test]
3418    fn indexed_source_map() {
3419        let json = r#"{
3420            "version": 3,
3421            "file": "bundle.js",
3422            "sections": [
3423                {
3424                    "offset": {"line": 0, "column": 0},
3425                    "map": {
3426                        "version": 3,
3427                        "sources": ["a.js"],
3428                        "names": ["foo"],
3429                        "mappings": "AAAAA"
3430                    }
3431                },
3432                {
3433                    "offset": {"line": 10, "column": 0},
3434                    "map": {
3435                        "version": 3,
3436                        "sources": ["b.js"],
3437                        "names": ["bar"],
3438                        "mappings": "AAAAA"
3439                    }
3440                }
3441            ]
3442        }"#;
3443
3444        let sm = SourceMap::from_json(json).unwrap();
3445
3446        // Should have both sources
3447        assert_eq!(sm.sources.len(), 2);
3448        assert!(sm.sources.contains(&"a.js".to_string()));
3449        assert!(sm.sources.contains(&"b.js".to_string()));
3450
3451        // Should have both names
3452        assert_eq!(sm.names.len(), 2);
3453        assert!(sm.names.contains(&"foo".to_string()));
3454        assert!(sm.names.contains(&"bar".to_string()));
3455
3456        // First section: line 0, col 0 should map to a.js
3457        let loc = sm.original_position_for(0, 0).unwrap();
3458        assert_eq!(sm.source(loc.source), "a.js");
3459        assert_eq!(loc.line, 0);
3460        assert_eq!(loc.column, 0);
3461
3462        // Second section: line 10, col 0 should map to b.js
3463        let loc = sm.original_position_for(10, 0).unwrap();
3464        assert_eq!(sm.source(loc.source), "b.js");
3465        assert_eq!(loc.line, 0);
3466        assert_eq!(loc.column, 0);
3467    }
3468
3469    #[test]
3470    fn indexed_source_map_shared_sources() {
3471        // Two sections referencing the same source
3472        let json = r#"{
3473            "version": 3,
3474            "sections": [
3475                {
3476                    "offset": {"line": 0, "column": 0},
3477                    "map": {
3478                        "version": 3,
3479                        "sources": ["shared.js"],
3480                        "names": [],
3481                        "mappings": "AAAA"
3482                    }
3483                },
3484                {
3485                    "offset": {"line": 5, "column": 0},
3486                    "map": {
3487                        "version": 3,
3488                        "sources": ["shared.js"],
3489                        "names": [],
3490                        "mappings": "AACA"
3491                    }
3492                }
3493            ]
3494        }"#;
3495
3496        let sm = SourceMap::from_json(json).unwrap();
3497
3498        // Should deduplicate sources
3499        assert_eq!(sm.sources.len(), 1);
3500        assert_eq!(sm.sources[0], "shared.js");
3501
3502        // Both sections should resolve to the same source
3503        let loc0 = sm.original_position_for(0, 0).unwrap();
3504        let loc5 = sm.original_position_for(5, 0).unwrap();
3505        assert_eq!(loc0.source, loc5.source);
3506    }
3507
3508    #[test]
3509    fn parse_ignore_list() {
3510        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js"],"names":[],"mappings":"AAAA;ACAA","ignoreList":[1]}"#;
3511        let sm = SourceMap::from_json(json).unwrap();
3512        assert_eq!(sm.ignore_list, vec![1]);
3513    }
3514
3515    /// Helper: build a source map JSON from absolute mappings data.
3516    fn build_sourcemap_json(
3517        sources: &[&str],
3518        names: &[&str],
3519        mappings_data: &[Vec<Vec<i64>>],
3520    ) -> String {
3521        let converted: Vec<Vec<srcmap_codec::Segment>> = mappings_data
3522            .iter()
3523            .map(|line| {
3524                line.iter().map(|seg| srcmap_codec::Segment::from(seg.as_slice())).collect()
3525            })
3526            .collect();
3527        let encoded = srcmap_codec::encode(&converted);
3528        format!(
3529            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
3530            sources.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(","),
3531            names.iter().map(|n| format!("\"{n}\"")).collect::<Vec<_>>().join(","),
3532            encoded,
3533        )
3534    }
3535
3536    // ── 1. Edge cases in decode_mappings ────────────────────────────
3537
3538    #[test]
3539    fn decode_multiple_consecutive_semicolons() {
3540        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;;AACA"}"#;
3541        let sm = SourceMap::from_json(json).unwrap();
3542        assert_eq!(sm.line_count(), 4);
3543        assert!(sm.mappings_for_line(1).is_empty());
3544        assert!(sm.mappings_for_line(2).is_empty());
3545        assert!(!sm.mappings_for_line(0).is_empty());
3546        assert!(!sm.mappings_for_line(3).is_empty());
3547    }
3548
3549    #[test]
3550    fn decode_trailing_semicolons() {
3551        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;"}"#;
3552        let sm = SourceMap::from_json(json).unwrap();
3553        assert_eq!(sm.line_count(), 3);
3554        assert!(!sm.mappings_for_line(0).is_empty());
3555        assert!(sm.mappings_for_line(1).is_empty());
3556        assert!(sm.mappings_for_line(2).is_empty());
3557    }
3558
3559    #[test]
3560    fn decode_leading_comma() {
3561        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":",AAAA"}"#;
3562        let sm = SourceMap::from_json(json).unwrap();
3563        assert_eq!(sm.mapping_count(), 1);
3564        let m = &sm.all_mappings()[0];
3565        assert_eq!(m.generated_line, 0);
3566        assert_eq!(m.generated_column, 0);
3567    }
3568
3569    #[test]
3570    fn decode_single_field_segments() {
3571        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,C"}"#;
3572        let sm = SourceMap::from_json(json).unwrap();
3573        assert_eq!(sm.mapping_count(), 2);
3574        for m in sm.all_mappings() {
3575            assert_eq!(m.source, NO_SOURCE);
3576        }
3577        assert_eq!(sm.all_mappings()[0].generated_column, 0);
3578        assert_eq!(sm.all_mappings()[1].generated_column, 1);
3579        assert!(sm.original_position_for(0, 0).is_none());
3580        assert!(sm.original_position_for(0, 1).is_none());
3581    }
3582
3583    #[test]
3584    fn decode_five_field_segments_with_names() {
3585        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0, 0], vec![10, 0, 0, 5, 1]]];
3586        let json = build_sourcemap_json(&["app.js"], &["foo", "bar"], &mappings_data);
3587        let sm = SourceMap::from_json(&json).unwrap();
3588        assert_eq!(sm.mapping_count(), 2);
3589        assert_eq!(sm.all_mappings()[0].name, 0);
3590        assert_eq!(sm.all_mappings()[1].name, 1);
3591
3592        let loc = sm.original_position_for(0, 0).unwrap();
3593        assert_eq!(loc.name, Some(0));
3594        assert_eq!(sm.name(0), "foo");
3595
3596        let loc = sm.original_position_for(0, 10).unwrap();
3597        assert_eq!(loc.name, Some(1));
3598        assert_eq!(sm.name(1), "bar");
3599    }
3600
3601    #[test]
3602    fn decode_large_vlq_values() {
3603        let mappings_data = vec![vec![vec![500_i64, 0, 1000, 2000]]];
3604        let json = build_sourcemap_json(&["big.js"], &[], &mappings_data);
3605        let sm = SourceMap::from_json(&json).unwrap();
3606        assert_eq!(sm.mapping_count(), 1);
3607        let m = &sm.all_mappings()[0];
3608        assert_eq!(m.generated_column, 500);
3609        assert_eq!(m.original_line, 1000);
3610        assert_eq!(m.original_column, 2000);
3611
3612        let loc = sm.original_position_for(0, 500).unwrap();
3613        assert_eq!(loc.line, 1000);
3614        assert_eq!(loc.column, 2000);
3615    }
3616
3617    #[test]
3618    fn decode_only_semicolons() {
3619        let json = r#"{"version":3,"sources":[],"names":[],"mappings":";;;"}"#;
3620        let sm = SourceMap::from_json(json).unwrap();
3621        assert_eq!(sm.line_count(), 4);
3622        assert_eq!(sm.mapping_count(), 0);
3623        for line in 0..4 {
3624            assert!(sm.mappings_for_line(line).is_empty());
3625        }
3626    }
3627
3628    #[test]
3629    fn decode_mixed_single_and_four_field_segments() {
3630        let mappings_data = vec![vec![srcmap_codec::Segment::four(5, 0, 0, 0)]];
3631        let four_field_encoded = srcmap_codec::encode(&mappings_data);
3632        let combined_mappings = format!("A,{four_field_encoded}");
3633        let json = format!(
3634            r#"{{"version":3,"sources":["x.js"],"names":[],"mappings":"{combined_mappings}"}}"#,
3635        );
3636        let sm = SourceMap::from_json(&json).unwrap();
3637        assert_eq!(sm.mapping_count(), 2);
3638        assert_eq!(sm.all_mappings()[0].source, NO_SOURCE);
3639        assert_eq!(sm.all_mappings()[1].source, 0);
3640    }
3641
3642    // ── 2. Source map parsing ───────────────────────────────────────
3643
3644    #[test]
3645    fn parse_missing_optional_fields() {
3646        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3647        let sm = SourceMap::from_json(json).unwrap();
3648        assert!(sm.file.is_none());
3649        assert!(sm.source_root.is_none());
3650        assert!(sm.sources_content.is_empty());
3651        assert!(sm.ignore_list.is_empty());
3652    }
3653
3654    #[test]
3655    fn parse_with_file_field() {
3656        let json =
3657            r#"{"version":3,"file":"output.js","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3658        let sm = SourceMap::from_json(json).unwrap();
3659        assert_eq!(sm.file.as_deref(), Some("output.js"));
3660    }
3661
3662    #[test]
3663    fn parse_null_entries_in_sources() {
3664        let json = r#"{"version":3,"sources":["a.js",null,"c.js"],"names":[],"mappings":"AAAA"}"#;
3665        let sm = SourceMap::from_json(json).unwrap();
3666        assert_eq!(sm.sources.len(), 3);
3667        assert_eq!(sm.sources[0], "a.js");
3668        assert_eq!(sm.sources[1], "");
3669        assert_eq!(sm.sources[2], "c.js");
3670    }
3671
3672    #[test]
3673    fn parse_null_entries_in_sources_with_source_root() {
3674        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js",null],"names":[],"mappings":"AAAA"}"#;
3675        let sm = SourceMap::from_json(json).unwrap();
3676        assert_eq!(sm.sources[0], "lib/a.js");
3677        assert_eq!(sm.sources[1], "");
3678    }
3679
3680    #[test]
3681    fn parse_empty_names_array() {
3682        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
3683        let sm = SourceMap::from_json(json).unwrap();
3684        assert!(sm.names.is_empty());
3685    }
3686
3687    #[test]
3688    fn parse_invalid_json() {
3689        let result = SourceMap::from_json("not valid json");
3690        assert!(result.is_err());
3691        assert!(matches!(result.unwrap_err(), ParseError::Json(_)));
3692    }
3693
3694    #[test]
3695    fn parse_json_missing_version() {
3696        let result = SourceMap::from_json(r#"{"sources":[],"names":[],"mappings":""}"#);
3697        assert!(result.is_err());
3698    }
3699
3700    #[test]
3701    fn parse_multiple_sources_overlapping_original_positions() {
3702        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10], vec![10, 1, 5, 10]]];
3703        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
3704        let sm = SourceMap::from_json(&json).unwrap();
3705
3706        let loc0 = sm.original_position_for(0, 0).unwrap();
3707        assert_eq!(loc0.source, 0);
3708        assert_eq!(sm.source(loc0.source), "a.js");
3709
3710        let loc1 = sm.original_position_for(0, 10).unwrap();
3711        assert_eq!(loc1.source, 1);
3712        assert_eq!(sm.source(loc1.source), "b.js");
3713
3714        assert_eq!(loc0.line, loc1.line);
3715        assert_eq!(loc0.column, loc1.column);
3716    }
3717
3718    #[test]
3719    fn parse_sources_content_with_null_entries() {
3720        let json = r#"{"version":3,"sources":["a.js","b.js"],"sourcesContent":["content a",null],"names":[],"mappings":"AAAA"}"#;
3721        let sm = SourceMap::from_json(json).unwrap();
3722        assert_eq!(sm.sources_content.len(), 2);
3723        assert_eq!(sm.sources_content[0], Some("content a".to_string()));
3724        assert_eq!(sm.sources_content[1], None);
3725    }
3726
3727    #[test]
3728    fn parse_empty_sources_and_names() {
3729        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
3730        let sm = SourceMap::from_json(json).unwrap();
3731        assert!(sm.sources.is_empty());
3732        assert!(sm.names.is_empty());
3733        assert_eq!(sm.mapping_count(), 0);
3734    }
3735
3736    // ── 3. Position lookups ─────────────────────────────────────────
3737
3738    #[test]
3739    fn lookup_exact_match() {
3740        let mappings_data =
3741            vec![vec![vec![0_i64, 0, 10, 20], vec![5, 0, 10, 25], vec![15, 0, 11, 0]]];
3742        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3743        let sm = SourceMap::from_json(&json).unwrap();
3744
3745        let loc = sm.original_position_for(0, 5).unwrap();
3746        assert_eq!(loc.line, 10);
3747        assert_eq!(loc.column, 25);
3748    }
3749
3750    #[test]
3751    fn lookup_before_first_segment() {
3752        let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
3753        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3754        let sm = SourceMap::from_json(&json).unwrap();
3755
3756        assert!(sm.original_position_for(0, 0).is_none());
3757        assert!(sm.original_position_for(0, 4).is_none());
3758    }
3759
3760    #[test]
3761    fn lookup_between_segments() {
3762        let mappings_data = vec![vec![vec![0_i64, 0, 1, 0], vec![10, 0, 2, 0], vec![20, 0, 3, 0]]];
3763        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3764        let sm = SourceMap::from_json(&json).unwrap();
3765
3766        let loc = sm.original_position_for(0, 7).unwrap();
3767        assert_eq!(loc.line, 1);
3768        assert_eq!(loc.column, 0);
3769
3770        let loc = sm.original_position_for(0, 15).unwrap();
3771        assert_eq!(loc.line, 2);
3772        assert_eq!(loc.column, 0);
3773    }
3774
3775    #[test]
3776    fn lookup_after_last_segment() {
3777        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 1, 5]]];
3778        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3779        let sm = SourceMap::from_json(&json).unwrap();
3780
3781        let loc = sm.original_position_for(0, 100).unwrap();
3782        assert_eq!(loc.line, 1);
3783        assert_eq!(loc.column, 5);
3784    }
3785
3786    #[test]
3787    fn lookup_empty_lines_no_mappings() {
3788        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]], vec![], vec![vec![0_i64, 0, 2, 0]]];
3789        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3790        let sm = SourceMap::from_json(&json).unwrap();
3791
3792        assert!(sm.original_position_for(1, 0).is_none());
3793        assert!(sm.original_position_for(1, 10).is_none());
3794        assert!(sm.original_position_for(0, 0).is_some());
3795        assert!(sm.original_position_for(2, 0).is_some());
3796    }
3797
3798    #[test]
3799    fn lookup_line_with_single_mapping() {
3800        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
3801        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3802        let sm = SourceMap::from_json(&json).unwrap();
3803
3804        let loc = sm.original_position_for(0, 0).unwrap();
3805        assert_eq!(loc.line, 0);
3806        assert_eq!(loc.column, 0);
3807
3808        let loc = sm.original_position_for(0, 50).unwrap();
3809        assert_eq!(loc.line, 0);
3810        assert_eq!(loc.column, 0);
3811    }
3812
3813    #[test]
3814    fn lookup_column_0_vs_column_nonzero() {
3815        let mappings_data = vec![vec![vec![0_i64, 0, 10, 0], vec![8, 0, 20, 5]]];
3816        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3817        let sm = SourceMap::from_json(&json).unwrap();
3818
3819        let loc0 = sm.original_position_for(0, 0).unwrap();
3820        assert_eq!(loc0.line, 10);
3821        assert_eq!(loc0.column, 0);
3822
3823        let loc8 = sm.original_position_for(0, 8).unwrap();
3824        assert_eq!(loc8.line, 20);
3825        assert_eq!(loc8.column, 5);
3826
3827        let loc4 = sm.original_position_for(0, 4).unwrap();
3828        assert_eq!(loc4.line, 10);
3829    }
3830
3831    #[test]
3832    fn lookup_beyond_last_line() {
3833        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
3834        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3835        let sm = SourceMap::from_json(&json).unwrap();
3836
3837        assert!(sm.original_position_for(1, 0).is_none());
3838        assert!(sm.original_position_for(100, 0).is_none());
3839    }
3840
3841    #[test]
3842    fn lookup_single_field_returns_none() {
3843        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A"}"#;
3844        let sm = SourceMap::from_json(json).unwrap();
3845        assert_eq!(sm.mapping_count(), 1);
3846        assert!(sm.original_position_for(0, 0).is_none());
3847    }
3848
3849    // ── 4. Reverse lookups (generated_position_for) ─────────────────
3850
3851    #[test]
3852    fn reverse_lookup_exact_match() {
3853        let mappings_data = vec![
3854            vec![vec![0_i64, 0, 0, 0]],
3855            vec![vec![4, 0, 1, 0], vec![10, 0, 1, 8]],
3856            vec![vec![0, 0, 2, 0]],
3857        ];
3858        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
3859        let sm = SourceMap::from_json(&json).unwrap();
3860
3861        let loc = sm.generated_position_for("main.js", 1, 8).unwrap();
3862        assert_eq!(loc.line, 1);
3863        assert_eq!(loc.column, 10);
3864    }
3865
3866    #[test]
3867    fn reverse_lookup_no_match() {
3868        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10]]];
3869        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
3870        let sm = SourceMap::from_json(&json).unwrap();
3871
3872        assert!(sm.generated_position_for("main.js", 99, 0).is_none());
3873    }
3874
3875    #[test]
3876    fn reverse_lookup_unknown_source() {
3877        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
3878        let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
3879        let sm = SourceMap::from_json(&json).unwrap();
3880
3881        assert!(sm.generated_position_for("unknown.js", 0, 0).is_none());
3882    }
3883
3884    #[test]
3885    fn reverse_lookup_multiple_mappings_same_original() {
3886        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]], vec![vec![20, 0, 5, 10]]];
3887        let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
3888        let sm = SourceMap::from_json(&json).unwrap();
3889
3890        let loc = sm.generated_position_for("src.js", 5, 10);
3891        assert!(loc.is_some());
3892        let loc = loc.unwrap();
3893        assert!(
3894            (loc.line == 0 && loc.column == 0) || (loc.line == 1 && loc.column == 20),
3895            "Expected (0,0) or (1,20), got ({},{})",
3896            loc.line,
3897            loc.column
3898        );
3899    }
3900
3901    #[test]
3902    fn reverse_lookup_with_multiple_sources() {
3903        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 1, 0, 0]]];
3904        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
3905        let sm = SourceMap::from_json(&json).unwrap();
3906
3907        let loc_a = sm.generated_position_for("a.js", 0, 0).unwrap();
3908        assert_eq!(loc_a.line, 0);
3909        assert_eq!(loc_a.column, 0);
3910
3911        let loc_b = sm.generated_position_for("b.js", 0, 0).unwrap();
3912        assert_eq!(loc_b.line, 0);
3913        assert_eq!(loc_b.column, 10);
3914    }
3915
3916    #[test]
3917    fn reverse_lookup_skips_single_field_segments() {
3918        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,KAAAA"}"#;
3919        let sm = SourceMap::from_json(json).unwrap();
3920
3921        let loc = sm.generated_position_for("a.js", 0, 0).unwrap();
3922        assert_eq!(loc.line, 0);
3923        assert_eq!(loc.column, 5);
3924    }
3925
3926    #[test]
3927    fn reverse_lookup_finds_each_original_line() {
3928        let mappings_data = vec![
3929            vec![vec![0_i64, 0, 0, 0]],
3930            vec![vec![0, 0, 1, 0]],
3931            vec![vec![0, 0, 2, 0]],
3932            vec![vec![0, 0, 3, 0]],
3933        ];
3934        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
3935        let sm = SourceMap::from_json(&json).unwrap();
3936
3937        for orig_line in 0..4 {
3938            let loc = sm.generated_position_for("x.js", orig_line, 0).unwrap();
3939            assert_eq!(loc.line, orig_line, "reverse lookup for orig line {orig_line}");
3940            assert_eq!(loc.column, 0);
3941        }
3942    }
3943
3944    // ── 5. ignoreList ───────────────────────────────────────────────
3945
3946    #[test]
3947    fn parse_with_ignore_list_multiple() {
3948        let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js","vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[1,2]}"#;
3949        let sm = SourceMap::from_json(json).unwrap();
3950        assert_eq!(sm.ignore_list, vec![1, 2]);
3951    }
3952
3953    #[test]
3954    fn parse_with_empty_ignore_list() {
3955        let json =
3956            r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA","ignoreList":[]}"#;
3957        let sm = SourceMap::from_json(json).unwrap();
3958        assert!(sm.ignore_list.is_empty());
3959    }
3960
3961    #[test]
3962    fn parse_without_ignore_list_field() {
3963        let json = r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA"}"#;
3964        let sm = SourceMap::from_json(json).unwrap();
3965        assert!(sm.ignore_list.is_empty());
3966    }
3967
3968    // ── Additional edge case tests ──────────────────────────────────
3969
3970    #[test]
3971    fn source_index_lookup() {
3972        let json = r#"{"version":3,"sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA"}"#;
3973        let sm = SourceMap::from_json(json).unwrap();
3974        assert_eq!(sm.source_index("a.js"), Some(0));
3975        assert_eq!(sm.source_index("b.js"), Some(1));
3976        assert_eq!(sm.source_index("c.js"), Some(2));
3977        assert_eq!(sm.source_index("d.js"), None);
3978    }
3979
3980    #[test]
3981    fn all_mappings_returns_complete_list() {
3982        let mappings_data =
3983            vec![vec![vec![0_i64, 0, 0, 0], vec![5, 0, 0, 5]], vec![vec![0, 0, 1, 0]]];
3984        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
3985        let sm = SourceMap::from_json(&json).unwrap();
3986        assert_eq!(sm.all_mappings().len(), 3);
3987        assert_eq!(sm.mapping_count(), 3);
3988    }
3989
3990    #[test]
3991    fn line_count_matches_decoded_lines() {
3992        let mappings_data =
3993            vec![vec![vec![0_i64, 0, 0, 0]], vec![], vec![vec![0_i64, 0, 2, 0]], vec![], vec![]];
3994        let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
3995        let sm = SourceMap::from_json(&json).unwrap();
3996        assert_eq!(sm.line_count(), 5);
3997    }
3998
3999    #[test]
4000    fn parse_error_display() {
4001        let err = ParseError::InvalidVersion(5);
4002        assert_eq!(format!("{err}"), "unsupported source map version: 5");
4003
4004        let json_err = SourceMap::from_json("{}").unwrap_err();
4005        let display = format!("{json_err}");
4006        assert!(display.contains("JSON parse error") || display.contains("missing field"));
4007    }
4008
4009    #[test]
4010    fn original_position_name_none_for_four_field() {
4011        let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]]];
4012        let json = build_sourcemap_json(&["a.js"], &["unused_name"], &mappings_data);
4013        let sm = SourceMap::from_json(&json).unwrap();
4014
4015        let loc = sm.original_position_for(0, 0).unwrap();
4016        assert!(loc.name.is_none());
4017    }
4018
4019    #[test]
4020    fn forward_and_reverse_roundtrip_comprehensive() {
4021        let mappings_data = vec![
4022            vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10], vec![20, 1, 5, 0]],
4023            vec![vec![0, 0, 1, 0], vec![5, 1, 6, 3]],
4024            vec![vec![0, 0, 2, 0]],
4025        ];
4026        let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
4027        let sm = SourceMap::from_json(&json).unwrap();
4028
4029        for m in sm.all_mappings() {
4030            if m.source == NO_SOURCE {
4031                continue;
4032            }
4033            let source_name = sm.source(m.source);
4034
4035            let orig = sm.original_position_for(m.generated_line, m.generated_column).unwrap();
4036            assert_eq!(orig.source, m.source);
4037            assert_eq!(orig.line, m.original_line);
4038            assert_eq!(orig.column, m.original_column);
4039
4040            let gen_loc =
4041                sm.generated_position_for(source_name, m.original_line, m.original_column).unwrap();
4042            assert_eq!(gen_loc.line, m.generated_line);
4043            assert_eq!(gen_loc.column, m.generated_column);
4044        }
4045    }
4046
4047    // ── 6. Comprehensive edge case tests ────────────────────────────
4048
4049    // -- sourceRoot edge cases --
4050
4051    #[test]
4052    fn source_root_with_multiple_sources() {
4053        let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA,KACA,KACA"}"#;
4054        let sm = SourceMap::from_json(json).unwrap();
4055        assert_eq!(sm.sources, vec!["lib/a.js", "lib/b.js", "lib/c.js"]);
4056    }
4057
4058    #[test]
4059    fn source_root_empty_string() {
4060        let json =
4061            r#"{"version":3,"sourceRoot":"","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4062        let sm = SourceMap::from_json(json).unwrap();
4063        assert_eq!(sm.sources, vec!["a.js"]);
4064    }
4065
4066    #[test]
4067    fn source_root_preserved_in_to_json() {
4068        let json =
4069            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4070        let sm = SourceMap::from_json(json).unwrap();
4071        let output = sm.to_json();
4072        assert!(output.contains(r#""sourceRoot":"src/""#));
4073    }
4074
4075    #[test]
4076    fn source_root_reverse_lookup_uses_prefixed_name() {
4077        let json =
4078            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4079        let sm = SourceMap::from_json(json).unwrap();
4080        // Must use the prefixed name for reverse lookups
4081        assert!(sm.generated_position_for("src/a.js", 0, 0).is_some());
4082        assert!(sm.generated_position_for("a.js", 0, 0).is_none());
4083    }
4084
4085    #[test]
4086    fn source_root_with_trailing_slash() {
4087        let json =
4088            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4089        let sm = SourceMap::from_json(json).unwrap();
4090        assert_eq!(sm.sources[0], "src/a.js");
4091    }
4092
4093    #[test]
4094    fn source_root_without_trailing_slash() {
4095        let json =
4096            r#"{"version":3,"sourceRoot":"src","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4097        let sm = SourceMap::from_json(json).unwrap();
4098        // sourceRoot is applied as raw prefix during parsing
4099        assert_eq!(sm.sources[0], "srca.js");
4100        // Roundtrip should strip the prefix back correctly
4101        let output = sm.to_json();
4102        let sm2 = SourceMap::from_json(&output).unwrap();
4103        assert_eq!(sm2.sources[0], "srca.js");
4104    }
4105
4106    // -- JSON/parsing error cases --
4107
4108    #[test]
4109    fn parse_empty_json_object() {
4110        // {} has no version field
4111        let result = SourceMap::from_json("{}");
4112        assert!(result.is_err());
4113    }
4114
4115    #[test]
4116    fn parse_version_0() {
4117        let json = r#"{"version":0,"sources":[],"names":[],"mappings":""}"#;
4118        assert!(matches!(SourceMap::from_json(json).unwrap_err(), ParseError::InvalidVersion(0)));
4119    }
4120
4121    #[test]
4122    fn parse_version_4() {
4123        let json = r#"{"version":4,"sources":[],"names":[],"mappings":""}"#;
4124        assert!(matches!(SourceMap::from_json(json).unwrap_err(), ParseError::InvalidVersion(4)));
4125    }
4126
4127    #[test]
4128    fn parse_extra_unknown_fields_ignored() {
4129        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom_field":true,"x_debug":{"foo":"bar"}}"#;
4130        let sm = SourceMap::from_json(json).unwrap();
4131        assert_eq!(sm.mapping_count(), 1);
4132    }
4133
4134    #[test]
4135    fn parse_vlq_error_propagated() {
4136        // '!' is not valid base64 — should surface as VLQ error
4137        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AA!A"}"#;
4138        let result = SourceMap::from_json(json);
4139        assert!(result.is_err());
4140        assert!(matches!(result.unwrap_err(), ParseError::Vlq(_)));
4141    }
4142
4143    #[test]
4144    fn parse_truncated_vlq_error() {
4145        // 'g' has continuation bit set — truncated VLQ
4146        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"g"}"#;
4147        let result = SourceMap::from_json(json);
4148        assert!(result.is_err());
4149    }
4150
4151    // -- to_json edge cases --
4152
4153    #[test]
4154    fn to_json_produces_valid_json() {
4155        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]}"#;
4156        let sm = SourceMap::from_json(json).unwrap();
4157        let output = sm.to_json();
4158        // Must be valid JSON that serde can parse
4159        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
4160    }
4161
4162    #[test]
4163    fn to_json_escapes_special_chars() {
4164        let json = r#"{"version":3,"sources":["path/with\"quotes.js"],"sourcesContent":["line1\nline2\ttab\\backslash"],"names":[],"mappings":"AAAA"}"#;
4165        let sm = SourceMap::from_json(json).unwrap();
4166        let output = sm.to_json();
4167        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
4168        let sm2 = SourceMap::from_json(&output).unwrap();
4169        assert_eq!(sm2.sources_content[0].as_deref(), Some("line1\nline2\ttab\\backslash"));
4170    }
4171
4172    #[test]
4173    fn to_json_empty_map() {
4174        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
4175        let sm = SourceMap::from_json(json).unwrap();
4176        let output = sm.to_json();
4177        let sm2 = SourceMap::from_json(&output).unwrap();
4178        assert_eq!(sm2.mapping_count(), 0);
4179        assert!(sm2.sources.is_empty());
4180    }
4181
4182    #[test]
4183    fn to_json_roundtrip_with_names() {
4184        let mappings_data =
4185            vec![vec![vec![0_i64, 0, 0, 0, 0], vec![10, 0, 0, 10, 1], vec![20, 0, 1, 0, 2]]];
4186        let json = build_sourcemap_json(&["src.js"], &["foo", "bar", "baz"], &mappings_data);
4187        let sm = SourceMap::from_json(&json).unwrap();
4188        let output = sm.to_json();
4189        let sm2 = SourceMap::from_json(&output).unwrap();
4190
4191        for m in sm2.all_mappings() {
4192            if m.source != NO_SOURCE && m.name != NO_NAME {
4193                let loc = sm2.original_position_for(m.generated_line, m.generated_column).unwrap();
4194                assert!(loc.name.is_some());
4195            }
4196        }
4197    }
4198
4199    // -- Indexed source map edge cases --
4200
4201    #[test]
4202    fn indexed_source_map_column_offset() {
4203        let json = r#"{
4204            "version": 3,
4205            "sections": [
4206                {
4207                    "offset": {"line": 0, "column": 10},
4208                    "map": {
4209                        "version": 3,
4210                        "sources": ["a.js"],
4211                        "names": [],
4212                        "mappings": "AAAA"
4213                    }
4214                }
4215            ]
4216        }"#;
4217        let sm = SourceMap::from_json(json).unwrap();
4218        // Mapping at col 0 in section should be offset to col 10 (first line only)
4219        let loc = sm.original_position_for(0, 10).unwrap();
4220        assert_eq!(loc.line, 0);
4221        assert_eq!(loc.column, 0);
4222        // Before the offset should have no mapping
4223        assert!(sm.original_position_for(0, 0).is_none());
4224    }
4225
4226    #[test]
4227    fn indexed_source_map_column_offset_only_first_line() {
4228        // Column offset only applies to the first line of a section
4229        let json = r#"{
4230            "version": 3,
4231            "sections": [
4232                {
4233                    "offset": {"line": 0, "column": 20},
4234                    "map": {
4235                        "version": 3,
4236                        "sources": ["a.js"],
4237                        "names": [],
4238                        "mappings": "AAAA;AAAA"
4239                    }
4240                }
4241            ]
4242        }"#;
4243        let sm = SourceMap::from_json(json).unwrap();
4244        // Line 0: column offset applies
4245        let loc = sm.original_position_for(0, 20).unwrap();
4246        assert_eq!(loc.column, 0);
4247        // Line 1: column offset does NOT apply
4248        let loc = sm.original_position_for(1, 0).unwrap();
4249        assert_eq!(loc.column, 0);
4250    }
4251
4252    #[test]
4253    fn indexed_source_map_empty_section() {
4254        let json = r#"{
4255            "version": 3,
4256            "sections": [
4257                {
4258                    "offset": {"line": 0, "column": 0},
4259                    "map": {
4260                        "version": 3,
4261                        "sources": [],
4262                        "names": [],
4263                        "mappings": ""
4264                    }
4265                },
4266                {
4267                    "offset": {"line": 5, "column": 0},
4268                    "map": {
4269                        "version": 3,
4270                        "sources": ["b.js"],
4271                        "names": [],
4272                        "mappings": "AAAA"
4273                    }
4274                }
4275            ]
4276        }"#;
4277        let sm = SourceMap::from_json(json).unwrap();
4278        assert_eq!(sm.sources.len(), 1);
4279        let loc = sm.original_position_for(5, 0).unwrap();
4280        assert_eq!(sm.source(loc.source), "b.js");
4281    }
4282
4283    #[test]
4284    fn indexed_source_map_with_sources_content() {
4285        let json = r#"{
4286            "version": 3,
4287            "sections": [
4288                {
4289                    "offset": {"line": 0, "column": 0},
4290                    "map": {
4291                        "version": 3,
4292                        "sources": ["a.js"],
4293                        "sourcesContent": ["var a = 1;"],
4294                        "names": [],
4295                        "mappings": "AAAA"
4296                    }
4297                },
4298                {
4299                    "offset": {"line": 5, "column": 0},
4300                    "map": {
4301                        "version": 3,
4302                        "sources": ["b.js"],
4303                        "sourcesContent": ["var b = 2;"],
4304                        "names": [],
4305                        "mappings": "AAAA"
4306                    }
4307                }
4308            ]
4309        }"#;
4310        let sm = SourceMap::from_json(json).unwrap();
4311        assert_eq!(sm.sources_content.len(), 2);
4312        assert_eq!(sm.sources_content[0], Some("var a = 1;".to_string()));
4313        assert_eq!(sm.sources_content[1], Some("var b = 2;".to_string()));
4314    }
4315
4316    #[test]
4317    fn indexed_source_map_with_ignore_list() {
4318        let json = r#"{
4319            "version": 3,
4320            "sections": [
4321                {
4322                    "offset": {"line": 0, "column": 0},
4323                    "map": {
4324                        "version": 3,
4325                        "sources": ["app.js", "vendor.js"],
4326                        "names": [],
4327                        "mappings": "AAAA",
4328                        "ignoreList": [1]
4329                    }
4330                }
4331            ]
4332        }"#;
4333        let sm = SourceMap::from_json(json).unwrap();
4334        assert!(!sm.ignore_list.is_empty());
4335    }
4336
4337    // -- Boundary conditions --
4338
4339    #[test]
4340    fn lookup_max_column_on_line() {
4341        let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
4342        let json = build_sourcemap_json(&["a.js"], &[], &mappings_data);
4343        let sm = SourceMap::from_json(&json).unwrap();
4344        // Very large column — should snap to the last mapping on line
4345        let loc = sm.original_position_for(0, u32::MAX - 1).unwrap();
4346        assert_eq!(loc.line, 0);
4347        assert_eq!(loc.column, 0);
4348    }
4349
4350    #[test]
4351    fn mappings_for_line_beyond_end() {
4352        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4353        let sm = SourceMap::from_json(json).unwrap();
4354        assert!(sm.mappings_for_line(u32::MAX).is_empty());
4355    }
4356
4357    #[test]
4358    fn source_with_unicode_path() {
4359        let json =
4360            r#"{"version":3,"sources":["src/日本語.ts"],"names":["変数"],"mappings":"AAAAA"}"#;
4361        let sm = SourceMap::from_json(json).unwrap();
4362        assert_eq!(sm.sources[0], "src/日本語.ts");
4363        assert_eq!(sm.names[0], "変数");
4364        let loc = sm.original_position_for(0, 0).unwrap();
4365        assert_eq!(sm.source(loc.source), "src/日本語.ts");
4366        assert_eq!(sm.name(loc.name.unwrap()), "変数");
4367    }
4368
4369    #[test]
4370    fn to_json_roundtrip_unicode_sources() {
4371        let json = r#"{"version":3,"sources":["src/日本語.ts"],"sourcesContent":["const 変数 = 1;"],"names":["変数"],"mappings":"AAAAA"}"#;
4372        let sm = SourceMap::from_json(json).unwrap();
4373        let output = sm.to_json();
4374        let _: serde_json::Value = serde_json::from_str(&output).unwrap();
4375        let sm2 = SourceMap::from_json(&output).unwrap();
4376        assert_eq!(sm2.sources[0], "src/日本語.ts");
4377        assert_eq!(sm2.sources_content[0], Some("const 変数 = 1;".to_string()));
4378    }
4379
4380    #[test]
4381    fn many_sources_lookup() {
4382        // 100 sources, verify source_index works for all
4383        let sources: Vec<String> = (0..100).map(|i| format!("src/file{i}.js")).collect();
4384        let source_strs: Vec<&str> = sources.iter().map(|s| s.as_str()).collect();
4385        let mappings_data = vec![
4386            sources
4387                .iter()
4388                .enumerate()
4389                .map(|(i, _)| vec![(i * 10) as i64, i as i64, 0, 0])
4390                .collect::<Vec<_>>(),
4391        ];
4392        let json = build_sourcemap_json(&source_strs, &[], &mappings_data);
4393        let sm = SourceMap::from_json(&json).unwrap();
4394
4395        for (i, src) in sources.iter().enumerate() {
4396            assert_eq!(sm.source_index(src), Some(i as u32));
4397        }
4398    }
4399
4400    #[test]
4401    fn clone_sourcemap() {
4402        let json = r#"{"version":3,"sources":["a.js"],"names":["x"],"mappings":"AAAAA"}"#;
4403        let sm = SourceMap::from_json(json).unwrap();
4404        let sm2 = sm.clone();
4405        assert_eq!(sm2.sources, sm.sources);
4406        assert_eq!(sm2.mapping_count(), sm.mapping_count());
4407        let loc = sm2.original_position_for(0, 0).unwrap();
4408        assert_eq!(sm2.source(loc.source), "a.js");
4409    }
4410
4411    #[test]
4412    fn parse_debug_id() {
4413        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
4414        let sm = SourceMap::from_json(json).unwrap();
4415        assert_eq!(sm.debug_id.as_deref(), Some("85314830-023f-4cf1-a267-535f4e37bb17"));
4416    }
4417
4418    #[test]
4419    fn parse_debug_id_snake_case() {
4420        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debug_id":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
4421        let sm = SourceMap::from_json(json).unwrap();
4422        assert_eq!(sm.debug_id.as_deref(), Some("85314830-023f-4cf1-a267-535f4e37bb17"));
4423    }
4424
4425    #[test]
4426    fn parse_no_debug_id() {
4427        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4428        let sm = SourceMap::from_json(json).unwrap();
4429        assert_eq!(sm.debug_id, None);
4430    }
4431
4432    #[test]
4433    fn debug_id_roundtrip() {
4434        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","debugId":"85314830-023f-4cf1-a267-535f4e37bb17"}"#;
4435        let sm = SourceMap::from_json(json).unwrap();
4436        let output = sm.to_json();
4437        assert!(output.contains(r#""debugId":"85314830-023f-4cf1-a267-535f4e37bb17""#));
4438        let sm2 = SourceMap::from_json(&output).unwrap();
4439        assert_eq!(sm.debug_id, sm2.debug_id);
4440    }
4441
4442    #[test]
4443    fn debug_id_not_in_json_when_absent() {
4444        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
4445        let sm = SourceMap::from_json(json).unwrap();
4446        let output = sm.to_json();
4447        assert!(!output.contains("debugId"));
4448    }
4449
4450    /// Generate a test source map JSON with realistic structure.
4451    fn generate_test_sourcemap(lines: usize, segs_per_line: usize, num_sources: usize) -> String {
4452        let sources: Vec<String> = (0..num_sources).map(|i| format!("src/file{i}.js")).collect();
4453        let names: Vec<String> = (0..20).map(|i| format!("var{i}")).collect();
4454
4455        let mut mappings_parts = Vec::with_capacity(lines);
4456        let mut gen_col;
4457        let mut src: i64 = 0;
4458        let mut src_line: i64 = 0;
4459        let mut src_col: i64;
4460        let mut name: i64 = 0;
4461
4462        for _ in 0..lines {
4463            gen_col = 0i64;
4464            let mut line_parts = Vec::with_capacity(segs_per_line);
4465
4466            for s in 0..segs_per_line {
4467                let gc_delta = 2 + (s as i64 * 3) % 20;
4468                gen_col += gc_delta;
4469
4470                let src_delta = i64::from(s % 7 == 0);
4471                src = (src + src_delta) % num_sources as i64;
4472
4473                src_line += 1;
4474                src_col = (s as i64 * 5 + 1) % 30;
4475
4476                let has_name = s % 4 == 0;
4477                if has_name {
4478                    name = (name + 1) % names.len() as i64;
4479                }
4480
4481                // Build segment using codec encode
4482                let segment = if has_name {
4483                    srcmap_codec::Segment::five(gen_col, src, src_line, src_col, name)
4484                } else {
4485                    srcmap_codec::Segment::four(gen_col, src, src_line, src_col)
4486                };
4487
4488                line_parts.push(segment);
4489            }
4490
4491            mappings_parts.push(line_parts);
4492        }
4493
4494        let encoded = srcmap_codec::encode(&mappings_parts);
4495
4496        format!(
4497            r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
4498            sources.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(","),
4499            names.iter().map(|n| format!("\"{n}\"")).collect::<Vec<_>>().join(","),
4500            encoded,
4501        )
4502    }
4503
4504    // ── Bias tests ───────────────────────────────────────────────
4505
4506    /// Map with multiple mappings per line for bias testing:
4507    /// Line 0: col 0 → src:0:0, col 5 → src:0:5, col 10 → src:0:10
4508    fn bias_map() -> &'static str {
4509        // AAAA = 0,0,0,0  KAAK = 5,0,0,5  KAAK = 5,0,0,5 (delta)
4510        r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,KAAK,KAAK"}"#
4511    }
4512
4513    #[test]
4514    fn original_position_glb_exact_match() {
4515        let sm = SourceMap::from_json(bias_map()).unwrap();
4516        let loc = sm.original_position_for_with_bias(0, 5, Bias::GreatestLowerBound).unwrap();
4517        assert_eq!(loc.column, 5);
4518    }
4519
4520    #[test]
4521    fn original_position_glb_snaps_left() {
4522        let sm = SourceMap::from_json(bias_map()).unwrap();
4523        // Column 7 should snap to the mapping at column 5
4524        let loc = sm.original_position_for_with_bias(0, 7, Bias::GreatestLowerBound).unwrap();
4525        assert_eq!(loc.column, 5);
4526    }
4527
4528    #[test]
4529    fn original_position_lub_exact_match() {
4530        let sm = SourceMap::from_json(bias_map()).unwrap();
4531        let loc = sm.original_position_for_with_bias(0, 5, Bias::LeastUpperBound).unwrap();
4532        assert_eq!(loc.column, 5);
4533    }
4534
4535    #[test]
4536    fn original_position_lub_snaps_right() {
4537        let sm = SourceMap::from_json(bias_map()).unwrap();
4538        // Column 3 with LUB should snap to the mapping at column 5
4539        let loc = sm.original_position_for_with_bias(0, 3, Bias::LeastUpperBound).unwrap();
4540        assert_eq!(loc.column, 5);
4541    }
4542
4543    #[test]
4544    fn original_position_lub_before_first() {
4545        let sm = SourceMap::from_json(bias_map()).unwrap();
4546        // Column 0 with LUB should find mapping at column 0
4547        let loc = sm.original_position_for_with_bias(0, 0, Bias::LeastUpperBound).unwrap();
4548        assert_eq!(loc.column, 0);
4549    }
4550
4551    #[test]
4552    fn original_position_lub_after_last() {
4553        let sm = SourceMap::from_json(bias_map()).unwrap();
4554        // Column 15 with LUB should return None (no mapping at or after 15)
4555        let loc = sm.original_position_for_with_bias(0, 15, Bias::LeastUpperBound);
4556        assert!(loc.is_none());
4557    }
4558
4559    #[test]
4560    fn original_position_glb_before_first() {
4561        let sm = SourceMap::from_json(bias_map()).unwrap();
4562        // Column 0 with GLB should find mapping at column 0
4563        let loc = sm.original_position_for_with_bias(0, 0, Bias::GreatestLowerBound).unwrap();
4564        assert_eq!(loc.column, 0);
4565    }
4566
4567    #[test]
4568    fn generated_position_lub() {
4569        let sm = SourceMap::from_json(bias_map()).unwrap();
4570        // LUB: find first generated position at or after original col 3
4571        let loc =
4572            sm.generated_position_for_with_bias("input.js", 0, 3, Bias::LeastUpperBound).unwrap();
4573        assert_eq!(loc.column, 5);
4574    }
4575
4576    #[test]
4577    fn generated_position_glb() {
4578        let sm = SourceMap::from_json(bias_map()).unwrap();
4579        // GLB: find last generated position at or before original col 7
4580        let loc = sm
4581            .generated_position_for_with_bias("input.js", 0, 7, Bias::GreatestLowerBound)
4582            .unwrap();
4583        assert_eq!(loc.column, 5);
4584    }
4585
4586    #[test]
4587    fn generated_position_for_default_bias_is_glb() {
4588        // The default bias must be GreatestLowerBound to match jridgewell's
4589        // generatedPositionFor semantics.
4590        let sm = SourceMap::from_json(bias_map()).unwrap();
4591        // With GLB: looking for original col 7, GLB finds the mapping at col 5
4592        let glb = sm.generated_position_for("input.js", 0, 7).unwrap();
4593        let glb_explicit = sm
4594            .generated_position_for_with_bias("input.js", 0, 7, Bias::GreatestLowerBound)
4595            .unwrap();
4596        assert_eq!(glb.line, glb_explicit.line);
4597        assert_eq!(glb.column, glb_explicit.column);
4598    }
4599
4600    // ── Range mapping tests ──────────────────────────────────────
4601
4602    #[test]
4603    fn map_range_basic() {
4604        let sm = SourceMap::from_json(bias_map()).unwrap();
4605        let range = sm.map_range(0, 0, 0, 10).unwrap();
4606        assert_eq!(range.source, 0);
4607        assert_eq!(range.original_start_line, 0);
4608        assert_eq!(range.original_start_column, 0);
4609        assert_eq!(range.original_end_line, 0);
4610        assert_eq!(range.original_end_column, 10);
4611    }
4612
4613    #[test]
4614    fn map_range_no_mapping() {
4615        let sm = SourceMap::from_json(bias_map()).unwrap();
4616        // Line 5 doesn't exist
4617        let range = sm.map_range(0, 0, 5, 0);
4618        assert!(range.is_none());
4619    }
4620
4621    #[test]
4622    fn map_range_different_sources() {
4623        // Map with two sources: line 0 → src0, line 1 → src1
4624        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA;ACAA"}"#;
4625        let sm = SourceMap::from_json(json).unwrap();
4626        // Start maps to a.js, end maps to b.js → should return None
4627        let range = sm.map_range(0, 0, 1, 0);
4628        assert!(range.is_none());
4629    }
4630
4631    // ── Phase 10 tests ───────────────────────────────────────────
4632
4633    #[test]
4634    fn extension_fields_preserved() {
4635        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_facebook_sources":[[{"names":["<global>"]}]],"x_google_linecount":42}"#;
4636        let sm = SourceMap::from_json(json).unwrap();
4637
4638        assert!(sm.extensions.contains_key("x_facebook_sources"));
4639        assert!(sm.extensions.contains_key("x_google_linecount"));
4640        assert_eq!(sm.extensions.get("x_google_linecount"), Some(&serde_json::json!(42)));
4641
4642        // Round-trip preserves extension fields
4643        let output = sm.to_json();
4644        assert!(output.contains("x_facebook_sources"));
4645        assert!(output.contains("x_google_linecount"));
4646    }
4647
4648    #[test]
4649    fn x_google_ignorelist_fallback() {
4650        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA","x_google_ignoreList":[1]}"#;
4651        let sm = SourceMap::from_json(json).unwrap();
4652        assert_eq!(sm.ignore_list, vec![1]);
4653    }
4654
4655    #[test]
4656    fn ignorelist_takes_precedence_over_x_google() {
4657        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA","ignoreList":[0],"x_google_ignoreList":[1]}"#;
4658        let sm = SourceMap::from_json(json).unwrap();
4659        assert_eq!(sm.ignore_list, vec![0]);
4660    }
4661
4662    #[test]
4663    fn source_mapping_url_external() {
4664        let source = "var a = 1;\n//# sourceMappingURL=app.js.map\n";
4665        let result = parse_source_mapping_url(source).unwrap();
4666        assert_eq!(result, SourceMappingUrl::External("app.js.map".to_string()));
4667    }
4668
4669    #[test]
4670    fn source_mapping_url_inline() {
4671        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
4672        let b64 = base64_encode_simple(json);
4673        let source =
4674            format!("var a = 1;\n//# sourceMappingURL=data:application/json;base64,{b64}\n");
4675        match parse_source_mapping_url(&source).unwrap() {
4676            SourceMappingUrl::Inline(decoded) => {
4677                assert_eq!(decoded, json);
4678            }
4679            SourceMappingUrl::External(_) => panic!("expected inline"),
4680        }
4681    }
4682
4683    #[test]
4684    fn source_mapping_url_at_sign() {
4685        let source = "var a = 1;\n//@ sourceMappingURL=old-style.map";
4686        let result = parse_source_mapping_url(source).unwrap();
4687        assert_eq!(result, SourceMappingUrl::External("old-style.map".to_string()));
4688    }
4689
4690    #[test]
4691    fn source_mapping_url_css_comment() {
4692        let source = "body { }\n/*# sourceMappingURL=styles.css.map */";
4693        let result = parse_source_mapping_url(source).unwrap();
4694        assert_eq!(result, SourceMappingUrl::External("styles.css.map".to_string()));
4695    }
4696
4697    #[test]
4698    fn source_mapping_url_none() {
4699        let source = "var a = 1;";
4700        assert!(parse_source_mapping_url(source).is_none());
4701    }
4702
4703    #[test]
4704    fn exclude_content_option() {
4705        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var a;"],"names":[],"mappings":"AAAA"}"#;
4706        let sm = SourceMap::from_json(json).unwrap();
4707
4708        let with_content = sm.to_json();
4709        assert!(with_content.contains("sourcesContent"));
4710
4711        let without_content = sm.to_json_with_options(true);
4712        assert!(!without_content.contains("sourcesContent"));
4713    }
4714
4715    #[test]
4716    fn validate_deep_clean_map() {
4717        let sm = SourceMap::from_json(simple_map()).unwrap();
4718        let warnings = validate_deep(&sm);
4719        assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
4720    }
4721
4722    #[test]
4723    fn validate_deep_unreferenced_source() {
4724        // Source "unused.js" has no mappings pointing to it
4725        let json =
4726            r#"{"version":3,"sources":["used.js","unused.js"],"names":[],"mappings":"AAAA"}"#;
4727        let sm = SourceMap::from_json(json).unwrap();
4728        let warnings = validate_deep(&sm);
4729        assert!(warnings.iter().any(|w| w.contains("unused.js")));
4730    }
4731
4732    // ── from_parts tests ──────────────────────────────────────────
4733
4734    #[test]
4735    fn from_parts_basic() {
4736        let mappings = vec![
4737            Mapping {
4738                generated_line: 0,
4739                generated_column: 0,
4740                source: 0,
4741                original_line: 0,
4742                original_column: 0,
4743                name: NO_NAME,
4744                is_range_mapping: false,
4745            },
4746            Mapping {
4747                generated_line: 1,
4748                generated_column: 4,
4749                source: 0,
4750                original_line: 1,
4751                original_column: 2,
4752                name: NO_NAME,
4753                is_range_mapping: false,
4754            },
4755        ];
4756
4757        let sm = SourceMap::from_parts(
4758            Some("out.js".to_string()),
4759            None,
4760            vec!["input.js".to_string()],
4761            vec![Some("var x = 1;".to_string())],
4762            vec![],
4763            mappings,
4764            vec![],
4765            None,
4766            None,
4767        );
4768
4769        assert_eq!(sm.line_count(), 2);
4770        assert_eq!(sm.mapping_count(), 2);
4771
4772        let loc = sm.original_position_for(0, 0).unwrap();
4773        assert_eq!(loc.source, 0);
4774        assert_eq!(loc.line, 0);
4775        assert_eq!(loc.column, 0);
4776
4777        let loc = sm.original_position_for(1, 4).unwrap();
4778        assert_eq!(loc.line, 1);
4779        assert_eq!(loc.column, 2);
4780    }
4781
4782    #[test]
4783    fn from_parts_empty() {
4784        let sm =
4785            SourceMap::from_parts(None, None, vec![], vec![], vec![], vec![], vec![], None, None);
4786        assert_eq!(sm.line_count(), 0);
4787        assert_eq!(sm.mapping_count(), 0);
4788        assert!(sm.original_position_for(0, 0).is_none());
4789    }
4790
4791    #[test]
4792    fn from_parts_with_names() {
4793        let mappings = vec![Mapping {
4794            generated_line: 0,
4795            generated_column: 0,
4796            source: 0,
4797            original_line: 0,
4798            original_column: 0,
4799            name: 0,
4800            is_range_mapping: false,
4801        }];
4802
4803        let sm = SourceMap::from_parts(
4804            None,
4805            None,
4806            vec!["input.js".to_string()],
4807            vec![],
4808            vec!["myVar".to_string()],
4809            mappings,
4810            vec![],
4811            None,
4812            None,
4813        );
4814
4815        let loc = sm.original_position_for(0, 0).unwrap();
4816        assert_eq!(loc.name, Some(0));
4817        assert_eq!(sm.name(0), "myVar");
4818    }
4819
4820    #[test]
4821    fn from_parts_roundtrip_via_json() {
4822        let json = generate_test_sourcemap(50, 10, 3);
4823        let sm = SourceMap::from_json(&json).unwrap();
4824
4825        let sm2 = SourceMap::from_parts(
4826            sm.file.clone(),
4827            sm.source_root.clone(),
4828            sm.sources.clone(),
4829            sm.sources_content.clone(),
4830            sm.names.clone(),
4831            sm.all_mappings().to_vec(),
4832            sm.ignore_list.clone(),
4833            sm.debug_id.clone(),
4834            None,
4835        );
4836
4837        assert_eq!(sm2.mapping_count(), sm.mapping_count());
4838        assert_eq!(sm2.line_count(), sm.line_count());
4839
4840        // Spot-check lookups
4841        for m in sm.all_mappings() {
4842            if m.source != NO_SOURCE {
4843                let a = sm.original_position_for(m.generated_line, m.generated_column);
4844                let b = sm2.original_position_for(m.generated_line, m.generated_column);
4845                match (a, b) {
4846                    (Some(a), Some(b)) => {
4847                        assert_eq!(a.source, b.source);
4848                        assert_eq!(a.line, b.line);
4849                        assert_eq!(a.column, b.column);
4850                    }
4851                    (None, None) => {}
4852                    _ => panic!("mismatch at ({}, {})", m.generated_line, m.generated_column),
4853                }
4854            }
4855        }
4856    }
4857
4858    #[test]
4859    fn from_parts_reverse_lookup() {
4860        let mappings = vec![
4861            Mapping {
4862                generated_line: 0,
4863                generated_column: 0,
4864                source: 0,
4865                original_line: 10,
4866                original_column: 5,
4867                name: NO_NAME,
4868                is_range_mapping: false,
4869            },
4870            Mapping {
4871                generated_line: 1,
4872                generated_column: 8,
4873                source: 0,
4874                original_line: 20,
4875                original_column: 0,
4876                name: NO_NAME,
4877                is_range_mapping: false,
4878            },
4879        ];
4880
4881        let sm = SourceMap::from_parts(
4882            None,
4883            None,
4884            vec!["src.js".to_string()],
4885            vec![],
4886            vec![],
4887            mappings,
4888            vec![],
4889            None,
4890            None,
4891        );
4892
4893        let loc = sm.generated_position_for("src.js", 10, 5).unwrap();
4894        assert_eq!(loc.line, 0);
4895        assert_eq!(loc.column, 0);
4896
4897        let loc = sm.generated_position_for("src.js", 20, 0).unwrap();
4898        assert_eq!(loc.line, 1);
4899        assert_eq!(loc.column, 8);
4900    }
4901
4902    #[test]
4903    fn from_parts_sparse_lines() {
4904        let mappings = vec![
4905            Mapping {
4906                generated_line: 0,
4907                generated_column: 0,
4908                source: 0,
4909                original_line: 0,
4910                original_column: 0,
4911                name: NO_NAME,
4912                is_range_mapping: false,
4913            },
4914            Mapping {
4915                generated_line: 5,
4916                generated_column: 0,
4917                source: 0,
4918                original_line: 5,
4919                original_column: 0,
4920                name: NO_NAME,
4921                is_range_mapping: false,
4922            },
4923        ];
4924
4925        let sm = SourceMap::from_parts(
4926            None,
4927            None,
4928            vec!["src.js".to_string()],
4929            vec![],
4930            vec![],
4931            mappings,
4932            vec![],
4933            None,
4934            None,
4935        );
4936
4937        assert_eq!(sm.line_count(), 6);
4938        assert!(sm.original_position_for(0, 0).is_some());
4939        assert!(sm.original_position_for(2, 0).is_none());
4940        assert!(sm.original_position_for(5, 0).is_some());
4941    }
4942
4943    // ── from_json_lines tests ────────────────────────────────────
4944
4945    #[test]
4946    fn from_json_lines_basic() {
4947        let json = generate_test_sourcemap(10, 5, 2);
4948        let sm_full = SourceMap::from_json(&json).unwrap();
4949
4950        // Decode only lines 3..7
4951        let sm_partial = SourceMap::from_json_lines(&json, 3, 7).unwrap();
4952
4953        // Verify mappings for lines in range match
4954        for line in 3..7u32 {
4955            let full_mappings = sm_full.mappings_for_line(line);
4956            let partial_mappings = sm_partial.mappings_for_line(line);
4957            assert_eq!(
4958                full_mappings.len(),
4959                partial_mappings.len(),
4960                "line {line} mapping count mismatch"
4961            );
4962            for (a, b) in full_mappings.iter().zip(partial_mappings.iter()) {
4963                assert_eq!(a.generated_column, b.generated_column);
4964                assert_eq!(a.source, b.source);
4965                assert_eq!(a.original_line, b.original_line);
4966                assert_eq!(a.original_column, b.original_column);
4967                assert_eq!(a.name, b.name);
4968            }
4969        }
4970    }
4971
4972    #[test]
4973    fn from_json_lines_first_lines() {
4974        let json = generate_test_sourcemap(10, 5, 2);
4975        let sm_full = SourceMap::from_json(&json).unwrap();
4976        let sm_partial = SourceMap::from_json_lines(&json, 0, 3).unwrap();
4977
4978        for line in 0..3u32 {
4979            let full_mappings = sm_full.mappings_for_line(line);
4980            let partial_mappings = sm_partial.mappings_for_line(line);
4981            assert_eq!(full_mappings.len(), partial_mappings.len());
4982        }
4983    }
4984
4985    #[test]
4986    fn from_json_lines_last_lines() {
4987        let json = generate_test_sourcemap(10, 5, 2);
4988        let sm_full = SourceMap::from_json(&json).unwrap();
4989        let sm_partial = SourceMap::from_json_lines(&json, 7, 10).unwrap();
4990
4991        for line in 7..10u32 {
4992            let full_mappings = sm_full.mappings_for_line(line);
4993            let partial_mappings = sm_partial.mappings_for_line(line);
4994            assert_eq!(full_mappings.len(), partial_mappings.len(), "line {line}");
4995        }
4996    }
4997
4998    #[test]
4999    fn from_json_lines_empty_range() {
5000        let json = generate_test_sourcemap(10, 5, 2);
5001        let sm = SourceMap::from_json_lines(&json, 5, 5).unwrap();
5002        assert_eq!(sm.mapping_count(), 0);
5003    }
5004
5005    #[test]
5006    fn from_json_lines_beyond_end() {
5007        let json = generate_test_sourcemap(5, 3, 1);
5008        // Request lines beyond what exists
5009        let sm = SourceMap::from_json_lines(&json, 3, 100).unwrap();
5010        // Should have mappings for lines 3 and 4 (the ones that exist in the range)
5011        assert!(sm.mapping_count() > 0);
5012    }
5013
5014    #[test]
5015    fn from_json_lines_single_line() {
5016        let json = generate_test_sourcemap(10, 5, 2);
5017        let sm_full = SourceMap::from_json(&json).unwrap();
5018        let sm_partial = SourceMap::from_json_lines(&json, 5, 6).unwrap();
5019
5020        let full_mappings = sm_full.mappings_for_line(5);
5021        let partial_mappings = sm_partial.mappings_for_line(5);
5022        assert_eq!(full_mappings.len(), partial_mappings.len());
5023    }
5024
5025    // ── LazySourceMap tests ──────────────────────────────────────
5026
5027    #[test]
5028    fn lazy_basic_lookup() {
5029        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;AACA"}"#;
5030        let sm = LazySourceMap::from_json(json).unwrap();
5031
5032        assert_eq!(sm.line_count(), 2);
5033        assert_eq!(sm.sources, vec!["input.js"]);
5034
5035        let loc = sm.original_position_for(0, 0).unwrap();
5036        assert_eq!(sm.source(loc.source), "input.js");
5037        assert_eq!(loc.line, 0);
5038        assert_eq!(loc.column, 0);
5039    }
5040
5041    #[test]
5042    fn lazy_multiple_lines() {
5043        let json = generate_test_sourcemap(20, 5, 3);
5044        let sm_eager = SourceMap::from_json(&json).unwrap();
5045        let sm_lazy = LazySourceMap::from_json(&json).unwrap();
5046
5047        assert_eq!(sm_lazy.line_count(), sm_eager.line_count());
5048
5049        // Verify lookups match for every mapping
5050        for m in sm_eager.all_mappings() {
5051            if m.source == NO_SOURCE {
5052                continue;
5053            }
5054            let eager_loc =
5055                sm_eager.original_position_for(m.generated_line, m.generated_column).unwrap();
5056            let lazy_loc =
5057                sm_lazy.original_position_for(m.generated_line, m.generated_column).unwrap();
5058            assert_eq!(eager_loc.source, lazy_loc.source);
5059            assert_eq!(eager_loc.line, lazy_loc.line);
5060            assert_eq!(eager_loc.column, lazy_loc.column);
5061            assert_eq!(eager_loc.name, lazy_loc.name);
5062        }
5063    }
5064
5065    #[test]
5066    fn lazy_empty_mappings() {
5067        let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
5068        let sm = LazySourceMap::from_json(json).unwrap();
5069        assert_eq!(sm.line_count(), 0);
5070        assert!(sm.original_position_for(0, 0).is_none());
5071    }
5072
5073    #[test]
5074    fn lazy_empty_lines() {
5075        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;;AACA"}"#;
5076        let sm = LazySourceMap::from_json(json).unwrap();
5077        assert_eq!(sm.line_count(), 4);
5078
5079        assert!(sm.original_position_for(0, 0).is_some());
5080        assert!(sm.original_position_for(1, 0).is_none());
5081        assert!(sm.original_position_for(2, 0).is_none());
5082        assert!(sm.original_position_for(3, 0).is_some());
5083    }
5084
5085    #[test]
5086    fn lazy_decode_line_caching() {
5087        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,KACA;AACA"}"#;
5088        let sm = LazySourceMap::from_json(json).unwrap();
5089
5090        // First call decodes
5091        let line0_a = sm.decode_line(0).unwrap();
5092        // Second call should return cached
5093        let line0_b = sm.decode_line(0).unwrap();
5094        assert_eq!(line0_a.len(), line0_b.len());
5095        assert_eq!(line0_a[0].generated_column, line0_b[0].generated_column);
5096    }
5097
5098    #[test]
5099    fn lazy_with_names() {
5100        let json = r#"{"version":3,"sources":["input.js"],"names":["foo","bar"],"mappings":"AAAAA,KACAC"}"#;
5101        let sm = LazySourceMap::from_json(json).unwrap();
5102
5103        let loc = sm.original_position_for(0, 0).unwrap();
5104        assert_eq!(loc.name, Some(0));
5105        assert_eq!(sm.name(0), "foo");
5106
5107        let loc = sm.original_position_for(0, 5).unwrap();
5108        assert_eq!(loc.name, Some(1));
5109        assert_eq!(sm.name(1), "bar");
5110    }
5111
5112    #[test]
5113    fn lazy_nonexistent_line() {
5114        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5115        let sm = LazySourceMap::from_json(json).unwrap();
5116        assert!(sm.original_position_for(99, 0).is_none());
5117        let line = sm.decode_line(99).unwrap();
5118        assert!(line.is_empty());
5119    }
5120
5121    #[test]
5122    fn lazy_into_sourcemap() {
5123        let json = generate_test_sourcemap(20, 5, 3);
5124        let sm_eager = SourceMap::from_json(&json).unwrap();
5125        let sm_lazy = LazySourceMap::from_json(&json).unwrap();
5126        let sm_converted = sm_lazy.into_sourcemap().unwrap();
5127
5128        assert_eq!(sm_converted.mapping_count(), sm_eager.mapping_count());
5129        assert_eq!(sm_converted.line_count(), sm_eager.line_count());
5130
5131        // Verify all lookups match
5132        for m in sm_eager.all_mappings() {
5133            let a = sm_eager.original_position_for(m.generated_line, m.generated_column);
5134            let b = sm_converted.original_position_for(m.generated_line, m.generated_column);
5135            match (a, b) {
5136                (Some(a), Some(b)) => {
5137                    assert_eq!(a.source, b.source);
5138                    assert_eq!(a.line, b.line);
5139                    assert_eq!(a.column, b.column);
5140                }
5141                (None, None) => {}
5142                _ => panic!("mismatch at ({}, {})", m.generated_line, m.generated_column),
5143            }
5144        }
5145    }
5146
5147    #[test]
5148    fn lazy_source_index_lookup() {
5149        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA;ACAA"}"#;
5150        let sm = LazySourceMap::from_json(json).unwrap();
5151        assert_eq!(sm.source_index("a.js"), Some(0));
5152        assert_eq!(sm.source_index("b.js"), Some(1));
5153        assert_eq!(sm.source_index("c.js"), None);
5154    }
5155
5156    #[test]
5157    fn lazy_mappings_for_line() {
5158        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,KACA;AACA"}"#;
5159        let sm = LazySourceMap::from_json(json).unwrap();
5160
5161        let line0 = sm.mappings_for_line(0);
5162        assert_eq!(line0.len(), 2);
5163
5164        let line1 = sm.mappings_for_line(1);
5165        assert_eq!(line1.len(), 1);
5166
5167        let line99 = sm.mappings_for_line(99);
5168        assert!(line99.is_empty());
5169    }
5170
5171    #[test]
5172    fn lazy_large_map_selective_decode() {
5173        // Generate a large map but only decode a few lines
5174        let json = generate_test_sourcemap(100, 10, 5);
5175        let sm_eager = SourceMap::from_json(&json).unwrap();
5176        let sm_lazy = LazySourceMap::from_json(&json).unwrap();
5177
5178        // Only decode lines 50 and 75
5179        for line in [50, 75] {
5180            let eager_mappings = sm_eager.mappings_for_line(line);
5181            let lazy_mappings = sm_lazy.mappings_for_line(line);
5182            assert_eq!(eager_mappings.len(), lazy_mappings.len(), "line {line} count mismatch");
5183            for (a, b) in eager_mappings.iter().zip(lazy_mappings.iter()) {
5184                assert_eq!(a.generated_column, b.generated_column);
5185                assert_eq!(a.source, b.source);
5186                assert_eq!(a.original_line, b.original_line);
5187                assert_eq!(a.original_column, b.original_column);
5188                assert_eq!(a.name, b.name);
5189            }
5190        }
5191    }
5192
5193    #[test]
5194    fn lazy_single_field_segments() {
5195        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,KAAAA"}"#;
5196        let sm = LazySourceMap::from_json(json).unwrap();
5197
5198        // First segment is single-field (no source info)
5199        assert!(sm.original_position_for(0, 0).is_none());
5200        // Second segment has source info
5201        let loc = sm.original_position_for(0, 5).unwrap();
5202        assert_eq!(loc.source, 0);
5203    }
5204
5205    // ── Coverage gap tests ──────────────────────────────────────────
5206
5207    #[test]
5208    fn parse_error_display_vlq() {
5209        let err = ParseError::Vlq(srcmap_codec::DecodeError::UnexpectedEof { offset: 3 });
5210        assert!(err.to_string().contains("VLQ decode error"));
5211    }
5212
5213    #[test]
5214    fn parse_error_display_scopes() {
5215        let err = ParseError::Scopes(srcmap_scopes::ScopesError::UnclosedScope);
5216        assert!(err.to_string().contains("scopes decode error"));
5217    }
5218
5219    #[test]
5220    fn indexed_map_with_names_in_sections() {
5221        let json = r#"{
5222            "version": 3,
5223            "sections": [
5224                {
5225                    "offset": {"line": 0, "column": 0},
5226                    "map": {
5227                        "version": 3,
5228                        "sources": ["a.js"],
5229                        "names": ["foo"],
5230                        "mappings": "AAAAA"
5231                    }
5232                },
5233                {
5234                    "offset": {"line": 1, "column": 0},
5235                    "map": {
5236                        "version": 3,
5237                        "sources": ["a.js"],
5238                        "names": ["foo"],
5239                        "mappings": "AAAAA"
5240                    }
5241                }
5242            ]
5243        }"#;
5244        let sm = SourceMap::from_json(json).unwrap();
5245        // Sources and names should be deduplicated
5246        assert_eq!(sm.sources.len(), 1);
5247        assert_eq!(sm.names.len(), 1);
5248    }
5249
5250    #[test]
5251    fn indexed_map_with_ignore_list() {
5252        let json = r#"{
5253            "version": 3,
5254            "sections": [
5255                {
5256                    "offset": {"line": 0, "column": 0},
5257                    "map": {
5258                        "version": 3,
5259                        "sources": ["vendor.js"],
5260                        "names": [],
5261                        "mappings": "AAAA",
5262                        "ignoreList": [0]
5263                    }
5264                }
5265            ]
5266        }"#;
5267        let sm = SourceMap::from_json(json).unwrap();
5268        assert_eq!(sm.ignore_list, vec![0]);
5269    }
5270
5271    #[test]
5272    fn indexed_map_with_generated_only_segment() {
5273        // Section with a generated-only (1-field) segment
5274        let json = r#"{
5275            "version": 3,
5276            "sections": [
5277                {
5278                    "offset": {"line": 0, "column": 0},
5279                    "map": {
5280                        "version": 3,
5281                        "sources": ["a.js"],
5282                        "names": [],
5283                        "mappings": "A,AAAA"
5284                    }
5285                }
5286            ]
5287        }"#;
5288        let sm = SourceMap::from_json(json).unwrap();
5289        assert!(sm.mapping_count() >= 1);
5290    }
5291
5292    #[test]
5293    fn indexed_map_empty_mappings() {
5294        let json = r#"{
5295            "version": 3,
5296            "sections": [
5297                {
5298                    "offset": {"line": 0, "column": 0},
5299                    "map": {
5300                        "version": 3,
5301                        "sources": [],
5302                        "names": [],
5303                        "mappings": ""
5304                    }
5305                }
5306            ]
5307        }"#;
5308        let sm = SourceMap::from_json(json).unwrap();
5309        assert_eq!(sm.mapping_count(), 0);
5310    }
5311
5312    #[test]
5313    fn generated_position_glb_exact_match() {
5314        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,EAAE,OAAO"}"#;
5315        let sm = SourceMap::from_json(json).unwrap();
5316
5317        let loc = sm.generated_position_for_with_bias("a.js", 0, 0, Bias::GreatestLowerBound);
5318        assert!(loc.is_some());
5319        assert_eq!(loc.unwrap().column, 0);
5320    }
5321
5322    #[test]
5323    fn generated_position_glb_no_exact_match() {
5324        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,EAAE"}"#;
5325        let sm = SourceMap::from_json(json).unwrap();
5326
5327        // Look for position between two mappings
5328        let loc = sm.generated_position_for_with_bias("a.js", 0, 0, Bias::GreatestLowerBound);
5329        assert!(loc.is_some());
5330    }
5331
5332    #[test]
5333    fn generated_position_glb_wrong_source() {
5334        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5335        let sm = SourceMap::from_json(json).unwrap();
5336
5337        // GLB for position in b.js that doesn't exist at that location
5338        let loc = sm.generated_position_for_with_bias("b.js", 5, 0, Bias::GreatestLowerBound);
5339        // Should find something or nothing depending on whether there's a mapping before
5340        // The key is that source filtering works
5341        if let Some(l) = loc {
5342            // Verify returned position is valid (line 0 is the only generated line)
5343            assert_eq!(l.line, 0);
5344        }
5345    }
5346
5347    #[test]
5348    fn generated_position_lub_wrong_source() {
5349        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5350        let sm = SourceMap::from_json(json).unwrap();
5351
5352        // LUB for non-existent source
5353        let loc =
5354            sm.generated_position_for_with_bias("nonexistent.js", 0, 0, Bias::LeastUpperBound);
5355        assert!(loc.is_none());
5356    }
5357
5358    #[test]
5359    fn to_json_with_ignore_list() {
5360        let json = r#"{"version":3,"sources":["vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[0]}"#;
5361        let sm = SourceMap::from_json(json).unwrap();
5362        let output = sm.to_json();
5363        assert!(output.contains("\"ignoreList\":[0]"));
5364    }
5365
5366    #[test]
5367    fn to_json_with_extensions() {
5368        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom":"test_value"}"#;
5369        let sm = SourceMap::from_json(json).unwrap();
5370        let output = sm.to_json();
5371        assert!(output.contains("x_custom"));
5372        assert!(output.contains("test_value"));
5373    }
5374
5375    #[test]
5376    fn from_parts_empty_mappings() {
5377        let sm = SourceMap::from_parts(
5378            None,
5379            None,
5380            vec!["a.js".to_string()],
5381            vec![Some("content".to_string())],
5382            vec![],
5383            vec![],
5384            vec![],
5385            None,
5386            None,
5387        );
5388        assert_eq!(sm.mapping_count(), 0);
5389        assert_eq!(sm.sources, vec!["a.js"]);
5390    }
5391
5392    #[test]
5393    fn from_vlq_basic() {
5394        let sm = SourceMap::from_vlq(
5395            "AAAA;AACA",
5396            vec!["a.js".to_string()],
5397            vec![],
5398            Some("out.js".to_string()),
5399            None,
5400            vec![Some("content".to_string())],
5401            vec![],
5402            None,
5403        )
5404        .unwrap();
5405
5406        assert_eq!(sm.file.as_deref(), Some("out.js"));
5407        assert_eq!(sm.sources, vec!["a.js"]);
5408        let loc = sm.original_position_for(0, 0).unwrap();
5409        assert_eq!(sm.source(loc.source), "a.js");
5410        assert_eq!(loc.line, 0);
5411    }
5412
5413    #[test]
5414    fn from_json_lines_basic_coverage() {
5415        let json =
5416            r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA"}"#;
5417        let sm = SourceMap::from_json_lines(json, 1, 3).unwrap();
5418        // Should have mappings for lines 1 and 2
5419        assert!(sm.original_position_for(1, 0).is_some());
5420        assert!(sm.original_position_for(2, 0).is_some());
5421    }
5422
5423    #[test]
5424    fn from_json_lines_with_source_root() {
5425        let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA;AACA"}"#;
5426        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5427        assert_eq!(sm.sources[0], "src/a.js");
5428    }
5429
5430    #[test]
5431    fn from_json_lines_with_null_source() {
5432        let json = r#"{"version":3,"sources":[null,"a.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5433        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5434        assert_eq!(sm.sources.len(), 2);
5435    }
5436
5437    #[test]
5438    fn json_escaping_special_chars_sourcemap() {
5439        // Build a source map with special chars in source name and content via JSON
5440        // The source name has a newline, the content has \r\n, tab, quotes, backslash, and control char
5441        let json = r#"{"version":3,"sources":["path/with\nnewline.js"],"sourcesContent":["line1\r\nline2\t\"quoted\"\\\u0001"],"names":[],"mappings":"AAAA"}"#;
5442        let sm = SourceMap::from_json(json).unwrap();
5443        // Roundtrip through to_json and re-parse
5444        let output = sm.to_json();
5445        let sm2 = SourceMap::from_json(&output).unwrap();
5446        assert_eq!(sm.sources[0], sm2.sources[0]);
5447        assert_eq!(sm.sources_content[0], sm2.sources_content[0]);
5448    }
5449
5450    #[test]
5451    fn to_json_exclude_content() {
5452        let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var a;"],"names":[],"mappings":"AAAA"}"#;
5453        let sm = SourceMap::from_json(json).unwrap();
5454        let output = sm.to_json_with_options(true);
5455        assert!(!output.contains("sourcesContent"));
5456        let output_with = sm.to_json_with_options(false);
5457        assert!(output_with.contains("sourcesContent"));
5458    }
5459
5460    #[test]
5461    fn encode_mappings_with_name() {
5462        // Ensure encode_mappings handles the name field (5th VLQ)
5463        let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AAAAA"}"#;
5464        let sm = SourceMap::from_json(json).unwrap();
5465        let encoded = sm.encode_mappings();
5466        assert_eq!(encoded, "AAAAA");
5467    }
5468
5469    #[test]
5470    fn encode_mappings_generated_only() {
5471        // Generated-only segments (NO_SOURCE) in encode
5472        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,AAAA"}"#;
5473        let sm = SourceMap::from_json(json).unwrap();
5474        let encoded = sm.encode_mappings();
5475        let roundtrip = SourceMap::from_json(&format!(
5476            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"{}"}}"#,
5477            encoded
5478        ))
5479        .unwrap();
5480        assert_eq!(roundtrip.mapping_count(), sm.mapping_count());
5481    }
5482
5483    #[test]
5484    fn map_range_single_result() {
5485        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA,EAAC,OAAO"}"#;
5486        let sm = SourceMap::from_json(json).unwrap();
5487        // map_range from col 0 to a mapped column
5488        let result = sm.map_range(0, 0, 0, 1);
5489        assert!(result.is_some());
5490        let range = result.unwrap();
5491        assert_eq!(range.source, 0);
5492    }
5493
5494    #[test]
5495    fn scopes_in_from_json() {
5496        // Source map with scopes field - build scopes string, then embed in JSON
5497        let info = srcmap_scopes::ScopeInfo {
5498            scopes: vec![Some(srcmap_scopes::OriginalScope {
5499                start: srcmap_scopes::Position { line: 0, column: 0 },
5500                end: srcmap_scopes::Position { line: 5, column: 0 },
5501                name: None,
5502                kind: None,
5503                is_stack_frame: false,
5504                variables: vec![],
5505                children: vec![],
5506            })],
5507            ranges: vec![],
5508        };
5509        let mut names = vec![];
5510        let scopes_str = srcmap_scopes::encode_scopes(&info, &mut names);
5511
5512        let json = format!(
5513            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"{scopes_str}"}}"#
5514        );
5515
5516        let sm = SourceMap::from_json(&json).unwrap();
5517        assert!(sm.scopes.is_some());
5518    }
5519
5520    #[test]
5521    fn from_json_lines_with_scopes() {
5522        let info = srcmap_scopes::ScopeInfo {
5523            scopes: vec![Some(srcmap_scopes::OriginalScope {
5524                start: srcmap_scopes::Position { line: 0, column: 0 },
5525                end: srcmap_scopes::Position { line: 5, column: 0 },
5526                name: None,
5527                kind: None,
5528                is_stack_frame: false,
5529                variables: vec![],
5530                children: vec![],
5531            })],
5532            ranges: vec![],
5533        };
5534        let mut names = vec![];
5535        let scopes_str = srcmap_scopes::encode_scopes(&info, &mut names);
5536        let json = format!(
5537            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AACA","scopes":"{scopes_str}"}}"#
5538        );
5539        let sm = SourceMap::from_json_lines(&json, 0, 2).unwrap();
5540        assert!(sm.scopes.is_some());
5541    }
5542
5543    #[test]
5544    fn from_json_lines_with_extensions() {
5545        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom":"val","not_x":"skip"}"#;
5546        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5547        assert!(sm.extensions.contains_key("x_custom"));
5548        assert!(!sm.extensions.contains_key("not_x"));
5549    }
5550
5551    #[test]
5552    fn lazy_sourcemap_version_error() {
5553        let json = r#"{"version":2,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5554        let err = LazySourceMap::from_json(json).unwrap_err();
5555        assert!(matches!(err, ParseError::InvalidVersion(2)));
5556    }
5557
5558    #[test]
5559    fn lazy_sourcemap_with_source_root() {
5560        let json =
5561            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5562        let sm = LazySourceMap::from_json(json).unwrap();
5563        assert_eq!(sm.sources[0], "src/a.js");
5564    }
5565
5566    #[test]
5567    fn lazy_sourcemap_with_ignore_list_and_extensions() {
5568        let json = r#"{"version":3,"sources":["v.js"],"names":[],"mappings":"AAAA","ignoreList":[0],"x_custom":"val","not_x":"skip"}"#;
5569        let sm = LazySourceMap::from_json(json).unwrap();
5570        assert_eq!(sm.ignore_list, vec![0]);
5571        assert!(sm.extensions.contains_key("x_custom"));
5572        assert!(!sm.extensions.contains_key("not_x"));
5573    }
5574
5575    #[test]
5576    fn lazy_sourcemap_with_scopes() {
5577        let info = srcmap_scopes::ScopeInfo {
5578            scopes: vec![Some(srcmap_scopes::OriginalScope {
5579                start: srcmap_scopes::Position { line: 0, column: 0 },
5580                end: srcmap_scopes::Position { line: 5, column: 0 },
5581                name: None,
5582                kind: None,
5583                is_stack_frame: false,
5584                variables: vec![],
5585                children: vec![],
5586            })],
5587            ranges: vec![],
5588        };
5589        let mut names = vec![];
5590        let scopes_str = srcmap_scopes::encode_scopes(&info, &mut names);
5591        let json = format!(
5592            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"{scopes_str}"}}"#
5593        );
5594        let sm = LazySourceMap::from_json(&json).unwrap();
5595        assert!(sm.scopes.is_some());
5596    }
5597
5598    #[test]
5599    fn lazy_sourcemap_null_source() {
5600        let json = r#"{"version":3,"sources":[null,"a.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5601        let sm = LazySourceMap::from_json(json).unwrap();
5602        assert_eq!(sm.sources.len(), 2);
5603    }
5604
5605    #[test]
5606    fn indexed_map_multi_line_section() {
5607        // Multi-line section to exercise line_offsets building in from_sections
5608        let json = r#"{
5609            "version": 3,
5610            "sections": [
5611                {
5612                    "offset": {"line": 0, "column": 0},
5613                    "map": {
5614                        "version": 3,
5615                        "sources": ["a.js"],
5616                        "names": [],
5617                        "mappings": "AAAA;AACA;AACA"
5618                    }
5619                },
5620                {
5621                    "offset": {"line": 5, "column": 0},
5622                    "map": {
5623                        "version": 3,
5624                        "sources": ["b.js"],
5625                        "names": [],
5626                        "mappings": "AAAA;AACA"
5627                    }
5628                }
5629            ]
5630        }"#;
5631        let sm = SourceMap::from_json(json).unwrap();
5632        assert!(sm.original_position_for(0, 0).is_some());
5633        assert!(sm.original_position_for(5, 0).is_some());
5634    }
5635
5636    #[test]
5637    fn source_mapping_url_extraction() {
5638        // External URL
5639        let input = "var x = 1;\n//# sourceMappingURL=bundle.js.map";
5640        let url = parse_source_mapping_url(input);
5641        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "bundle.js.map"));
5642
5643        // CSS comment style
5644        let input = "body { }\n/*# sourceMappingURL=style.css.map */";
5645        let url = parse_source_mapping_url(input);
5646        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "style.css.map"));
5647
5648        // @ sign variant
5649        let input = "var x;\n//@ sourceMappingURL=old-style.map";
5650        let url = parse_source_mapping_url(input);
5651        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "old-style.map"));
5652
5653        // CSS @ variant
5654        let input = "body{}\n/*@ sourceMappingURL=old-css.map */";
5655        let url = parse_source_mapping_url(input);
5656        assert!(matches!(url, Some(SourceMappingUrl::External(ref s)) if s == "old-css.map"));
5657
5658        // No URL
5659        let input = "var x = 1;";
5660        let url = parse_source_mapping_url(input);
5661        assert!(url.is_none());
5662
5663        // Empty URL
5664        let input = "//# sourceMappingURL=";
5665        let url = parse_source_mapping_url(input);
5666        assert!(url.is_none());
5667
5668        // Inline data URI
5669        let map_json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5670        let encoded = base64_encode_simple(map_json);
5671        let input = format!("var x;\n//# sourceMappingURL=data:application/json;base64,{encoded}");
5672        let url = parse_source_mapping_url(&input);
5673        assert!(matches!(url, Some(SourceMappingUrl::Inline(_))));
5674    }
5675
5676    #[test]
5677    fn validate_deep_unreferenced_coverage() {
5678        // Map with an unreferenced source
5679        let sm = SourceMap::from_parts(
5680            None,
5681            None,
5682            vec!["used.js".to_string(), "unused.js".to_string()],
5683            vec![None, None],
5684            vec![],
5685            vec![Mapping {
5686                generated_line: 0,
5687                generated_column: 0,
5688                source: 0,
5689                original_line: 0,
5690                original_column: 0,
5691                name: NO_NAME,
5692                is_range_mapping: false,
5693            }],
5694            vec![],
5695            None,
5696            None,
5697        );
5698        let warnings = validate_deep(&sm);
5699        assert!(warnings.iter().any(|w| w.contains("unreferenced")));
5700    }
5701
5702    #[test]
5703    fn from_json_lines_generated_only_segment() {
5704        // from_json_lines with 1-field segments to exercise the generated-only branch
5705        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,AAAA;AACA"}"#;
5706        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5707        assert!(sm.mapping_count() >= 2);
5708    }
5709
5710    #[test]
5711    fn from_json_lines_with_names() {
5712        let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AAAAA;AACAA"}"#;
5713        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5714        let loc = sm.original_position_for(0, 0).unwrap();
5715        assert_eq!(loc.name, Some(0));
5716    }
5717
5718    #[test]
5719    fn from_parts_with_line_gap() {
5720        // Mappings with a gap between lines to exercise line_offsets forward fill
5721        let sm = SourceMap::from_parts(
5722            None,
5723            None,
5724            vec!["a.js".to_string()],
5725            vec![None],
5726            vec![],
5727            vec![
5728                Mapping {
5729                    generated_line: 0,
5730                    generated_column: 0,
5731                    source: 0,
5732                    original_line: 0,
5733                    original_column: 0,
5734                    name: NO_NAME,
5735                    is_range_mapping: false,
5736                },
5737                Mapping {
5738                    generated_line: 5,
5739                    generated_column: 0,
5740                    source: 0,
5741                    original_line: 5,
5742                    original_column: 0,
5743                    name: NO_NAME,
5744                    is_range_mapping: false,
5745                },
5746            ],
5747            vec![],
5748            None,
5749            None,
5750        );
5751        assert!(sm.original_position_for(0, 0).is_some());
5752        assert!(sm.original_position_for(5, 0).is_some());
5753        // Lines 1-4 have no mappings
5754        assert!(sm.original_position_for(1, 0).is_none());
5755    }
5756
5757    #[test]
5758    fn lazy_decode_line_with_names_and_generated_only() {
5759        // LazySourceMap with both named and generated-only segments
5760        let json = r#"{"version":3,"sources":["a.js"],"names":["fn"],"mappings":"A,AAAAC"}"#;
5761        let sm = LazySourceMap::from_json(json).unwrap();
5762        let line = sm.decode_line(0).unwrap();
5763        assert!(line.len() >= 2);
5764        // First is generated-only
5765        assert_eq!(line[0].source, NO_SOURCE);
5766        // Second has name
5767        assert_ne!(line[1].name, NO_NAME);
5768    }
5769
5770    #[test]
5771    fn generated_position_glb_source_mismatch() {
5772        // a.js maps at (0,0)->(0,0), b.js maps at (0,5)->(1,0)
5773        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA,KCCA"}"#;
5774        let sm = SourceMap::from_json(json).unwrap();
5775
5776        // LUB for source that exists but position is way beyond all mappings
5777        let loc = sm.generated_position_for_with_bias("a.js", 100, 0, Bias::LeastUpperBound);
5778        assert!(loc.is_none());
5779
5780        // GLB for position before the only mapping in b.js (b.js has mapping at original 1,0)
5781        // Searching for (0,0) in b.js: partition_point finds first >= target,
5782        // then idx-1 if not exact, but that idx-1 maps to a.js (source mismatch), so None
5783        let loc = sm.generated_position_for_with_bias("b.js", 0, 0, Bias::GreatestLowerBound);
5784        assert!(loc.is_none());
5785
5786        // GLB for exact position in b.js
5787        let loc = sm.generated_position_for_with_bias("b.js", 1, 0, Bias::GreatestLowerBound);
5788        assert!(loc.is_some());
5789
5790        // LUB source mismatch: search for position in b.js that lands on a.js mapping
5791        let loc = sm.generated_position_for_with_bias("b.js", 99, 0, Bias::LeastUpperBound);
5792        assert!(loc.is_none());
5793    }
5794
5795    // ── Coverage gap tests ───────────────────────────────────────────
5796
5797    #[test]
5798    fn from_json_invalid_scopes_error() {
5799        // Invalid scopes string to trigger ParseError::Scopes
5800        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"!!invalid!!"}"#;
5801        let err = SourceMap::from_json(json).unwrap_err();
5802        assert!(matches!(err, ParseError::Scopes(_)));
5803    }
5804
5805    #[test]
5806    fn lazy_from_json_invalid_scopes_error() {
5807        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"!!invalid!!"}"#;
5808        let err = LazySourceMap::from_json(json).unwrap_err();
5809        assert!(matches!(err, ParseError::Scopes(_)));
5810    }
5811
5812    #[test]
5813    fn from_json_lines_invalid_scopes_error() {
5814        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","scopes":"!!invalid!!"}"#;
5815        let err = SourceMap::from_json_lines(json, 0, 1).unwrap_err();
5816        assert!(matches!(err, ParseError::Scopes(_)));
5817    }
5818
5819    #[test]
5820    fn from_json_lines_invalid_version() {
5821        let json = r#"{"version":2,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
5822        let err = SourceMap::from_json_lines(json, 0, 1).unwrap_err();
5823        assert!(matches!(err, ParseError::InvalidVersion(2)));
5824    }
5825
5826    #[test]
5827    fn indexed_map_with_ignore_list_remapped() {
5828        // Indexed map with 2 sections that have overlapping ignore_list
5829        let json = r#"{
5830            "version": 3,
5831            "sections": [{
5832                "offset": {"line": 0, "column": 0},
5833                "map": {
5834                    "version": 3,
5835                    "sources": ["a.js", "b.js"],
5836                    "names": [],
5837                    "mappings": "AAAA;ACAA",
5838                    "ignoreList": [1]
5839                }
5840            }, {
5841                "offset": {"line": 5, "column": 0},
5842                "map": {
5843                    "version": 3,
5844                    "sources": ["b.js", "c.js"],
5845                    "names": [],
5846                    "mappings": "AAAA;ACAA",
5847                    "ignoreList": [0]
5848                }
5849            }]
5850        }"#;
5851        let sm = SourceMap::from_json(json).unwrap();
5852        // b.js should be deduped across sections, ignore_list should have b.js global index
5853        assert!(!sm.ignore_list.is_empty());
5854    }
5855
5856    #[test]
5857    fn to_json_with_debug_id() {
5858        let sm = SourceMap::from_parts(
5859            Some("out.js".to_string()),
5860            None,
5861            vec!["a.js".to_string()],
5862            vec![None],
5863            vec![],
5864            vec![Mapping {
5865                generated_line: 0,
5866                generated_column: 0,
5867                source: 0,
5868                original_line: 0,
5869                original_column: 0,
5870                name: NO_NAME,
5871                is_range_mapping: false,
5872            }],
5873            vec![],
5874            Some("abc-123".to_string()),
5875            None,
5876        );
5877        let json = sm.to_json();
5878        assert!(json.contains(r#""debugId":"abc-123""#));
5879    }
5880
5881    #[test]
5882    fn to_json_with_ignore_list_and_extensions() {
5883        let mut sm = SourceMap::from_parts(
5884            None,
5885            None,
5886            vec!["a.js".to_string(), "b.js".to_string()],
5887            vec![None, None],
5888            vec![],
5889            vec![Mapping {
5890                generated_line: 0,
5891                generated_column: 0,
5892                source: 0,
5893                original_line: 0,
5894                original_column: 0,
5895                name: NO_NAME,
5896                is_range_mapping: false,
5897            }],
5898            vec![1],
5899            None,
5900            None,
5901        );
5902        sm.extensions.insert("x_test".to_string(), serde_json::json!(42));
5903        let json = sm.to_json();
5904        assert!(json.contains("\"ignoreList\":[1]"));
5905        assert!(json.contains("\"x_test\":42"));
5906    }
5907
5908    #[test]
5909    fn from_vlq_with_all_options() {
5910        let sm = SourceMap::from_vlq(
5911            "AAAA;AACA",
5912            vec!["a.js".to_string()],
5913            vec![],
5914            Some("out.js".to_string()),
5915            Some("src/".to_string()),
5916            vec![Some("content".to_string())],
5917            vec![0],
5918            Some("debug-123".to_string()),
5919        )
5920        .unwrap();
5921        assert_eq!(sm.source(0), "a.js");
5922        assert!(sm.original_position_for(0, 0).is_some());
5923        assert!(sm.original_position_for(1, 0).is_some());
5924    }
5925
5926    #[test]
5927    fn lazy_into_sourcemap_roundtrip() {
5928        let json = r#"{"version":3,"sources":["a.js"],"names":["x"],"mappings":"AAAAA;AACAA"}"#;
5929        let lazy = LazySourceMap::from_json(json).unwrap();
5930        let sm = lazy.into_sourcemap().unwrap();
5931        assert!(sm.original_position_for(0, 0).is_some());
5932        assert!(sm.original_position_for(1, 0).is_some());
5933        assert_eq!(sm.name(0), "x");
5934    }
5935
5936    #[test]
5937    fn lazy_original_position_for_no_match() {
5938        // LazySourceMap: column before any mapping should return None (Err(0) branch)
5939        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"KAAA"}"#;
5940        let sm = LazySourceMap::from_json(json).unwrap();
5941        // Column 0 is before column 5 (K = 5), should return None
5942        assert!(sm.original_position_for(0, 0).is_none());
5943    }
5944
5945    #[test]
5946    fn lazy_original_position_for_empty_line() {
5947        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":";AAAA"}"#;
5948        let sm = LazySourceMap::from_json(json).unwrap();
5949        // Line 0 is empty
5950        assert!(sm.original_position_for(0, 0).is_none());
5951        // Line 1 has mapping
5952        assert!(sm.original_position_for(1, 0).is_some());
5953    }
5954
5955    #[test]
5956    fn lazy_original_position_generated_only() {
5957        // Only a 1-field (generated-only) segment on line 0
5958        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A;AAAA"}"#;
5959        let sm = LazySourceMap::from_json(json).unwrap();
5960        // Line 0 has only generated-only segment → returns None
5961        assert!(sm.original_position_for(0, 0).is_none());
5962        // Line 1 has a 4-field segment → returns Some
5963        assert!(sm.original_position_for(1, 0).is_some());
5964    }
5965
5966    #[test]
5967    fn from_json_lines_null_source() {
5968        let json = r#"{"version":3,"sources":[null,"a.js"],"names":[],"mappings":"ACAA"}"#;
5969        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5970        assert!(sm.mapping_count() >= 1);
5971    }
5972
5973    #[test]
5974    fn from_json_lines_with_source_root_prefix() {
5975        let json =
5976            r#"{"version":3,"sourceRoot":"lib/","sources":["b.js"],"names":[],"mappings":"AAAA"}"#;
5977        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
5978        assert_eq!(sm.source(0), "lib/b.js");
5979    }
5980
5981    #[test]
5982    fn generated_position_for_glb_idx_zero() {
5983        // When the reverse index partition_point returns 0, GLB should return None
5984        // Create a map where source "a.js" only has mapping at original (5,0)
5985        // Searching for (0,0) in GLB mode: partition_point returns 0 (nothing <= (0,0)), so None
5986        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAKA"}"#;
5987        let sm = SourceMap::from_json(json).unwrap();
5988        let loc = sm.generated_position_for_with_bias("a.js", 0, 0, Bias::GreatestLowerBound);
5989        assert!(loc.is_none());
5990    }
5991
5992    #[test]
5993    fn from_json_lines_with_ignore_list() {
5994        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA;ACAA","ignoreList":[1]}"#;
5995        let sm = SourceMap::from_json_lines(json, 0, 2).unwrap();
5996        assert_eq!(sm.ignore_list, vec![1]);
5997    }
5998
5999    #[test]
6000    fn validate_deep_out_of_order_mappings() {
6001        // Manually construct a map with out-of-order segments
6002        let sm = SourceMap::from_parts(
6003            None,
6004            None,
6005            vec!["a.js".to_string()],
6006            vec![None],
6007            vec![],
6008            vec![
6009                Mapping {
6010                    generated_line: 1,
6011                    generated_column: 0,
6012                    source: 0,
6013                    original_line: 0,
6014                    original_column: 0,
6015                    name: NO_NAME,
6016                    is_range_mapping: false,
6017                },
6018                Mapping {
6019                    generated_line: 0,
6020                    generated_column: 0,
6021                    source: 0,
6022                    original_line: 0,
6023                    original_column: 0,
6024                    name: NO_NAME,
6025                    is_range_mapping: false,
6026                },
6027            ],
6028            vec![],
6029            None,
6030            None,
6031        );
6032        let warnings = validate_deep(&sm);
6033        assert!(warnings.iter().any(|w| w.contains("out of order")));
6034    }
6035
6036    #[test]
6037    fn validate_deep_out_of_bounds_source() {
6038        let sm = SourceMap::from_parts(
6039            None,
6040            None,
6041            vec!["a.js".to_string()],
6042            vec![None],
6043            vec![],
6044            vec![Mapping {
6045                generated_line: 0,
6046                generated_column: 0,
6047                source: 5,
6048                original_line: 0,
6049                original_column: 0,
6050                name: NO_NAME,
6051                is_range_mapping: false,
6052            }],
6053            vec![],
6054            None,
6055            None,
6056        );
6057        let warnings = validate_deep(&sm);
6058        assert!(warnings.iter().any(|w| w.contains("source index") && w.contains("out of bounds")));
6059    }
6060
6061    #[test]
6062    fn validate_deep_out_of_bounds_name() {
6063        let sm = SourceMap::from_parts(
6064            None,
6065            None,
6066            vec!["a.js".to_string()],
6067            vec![None],
6068            vec!["foo".to_string()],
6069            vec![Mapping {
6070                generated_line: 0,
6071                generated_column: 0,
6072                source: 0,
6073                original_line: 0,
6074                original_column: 0,
6075                name: 5,
6076                is_range_mapping: false,
6077            }],
6078            vec![],
6079            None,
6080            None,
6081        );
6082        let warnings = validate_deep(&sm);
6083        assert!(warnings.iter().any(|w| w.contains("name index") && w.contains("out of bounds")));
6084    }
6085
6086    #[test]
6087    fn validate_deep_out_of_bounds_ignore_list() {
6088        let sm = SourceMap::from_parts(
6089            None,
6090            None,
6091            vec!["a.js".to_string()],
6092            vec![None],
6093            vec![],
6094            vec![Mapping {
6095                generated_line: 0,
6096                generated_column: 0,
6097                source: 0,
6098                original_line: 0,
6099                original_column: 0,
6100                name: NO_NAME,
6101                is_range_mapping: false,
6102            }],
6103            vec![10],
6104            None,
6105            None,
6106        );
6107        let warnings = validate_deep(&sm);
6108        assert!(warnings.iter().any(|w| w.contains("ignoreList") && w.contains("out of bounds")));
6109    }
6110
6111    #[test]
6112    fn source_mapping_url_inline_decoded() {
6113        // Test that inline data URIs actually decode base64 and return the parsed map
6114        let map_json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6115        let encoded = base64_encode_simple(map_json);
6116        let input = format!("var x;\n//# sourceMappingURL=data:application/json;base64,{encoded}");
6117        let url = parse_source_mapping_url(&input);
6118        match url {
6119            Some(SourceMappingUrl::Inline(json)) => {
6120                assert!(json.contains("version"));
6121                assert!(json.contains("AAAA"));
6122            }
6123            _ => panic!("expected inline source map"),
6124        }
6125    }
6126
6127    #[test]
6128    fn source_mapping_url_charset_variant() {
6129        let map_json = r#"{"version":3}"#;
6130        let encoded = base64_encode_simple(map_json);
6131        let input =
6132            format!("x\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,{encoded}");
6133        let url = parse_source_mapping_url(&input);
6134        assert!(matches!(url, Some(SourceMappingUrl::Inline(_))));
6135    }
6136
6137    #[test]
6138    fn source_mapping_url_invalid_base64_falls_through_to_external() {
6139        // Data URI with invalid base64 that fails to decode should still return External
6140        let input = "x\n//# sourceMappingURL=data:application/json;base64,!!!invalid!!!";
6141        let url = parse_source_mapping_url(input);
6142        // Invalid base64 → base64_decode returns None → falls through to External
6143        assert!(matches!(url, Some(SourceMappingUrl::External(_))));
6144    }
6145
6146    #[test]
6147    fn from_json_lines_with_extensions_preserved() {
6148        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom":99}"#;
6149        let sm = SourceMap::from_json_lines(json, 0, 1).unwrap();
6150        assert!(sm.extensions.contains_key("x_custom"));
6151    }
6152
6153    // Helper for base64 encoding in tests
6154    fn base64_encode_simple(input: &str) -> String {
6155        const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
6156        let bytes = input.as_bytes();
6157        let mut result = String::new();
6158        for chunk in bytes.chunks(3) {
6159            let b0 = chunk[0] as u32;
6160            let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
6161            let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
6162            let n = (b0 << 16) | (b1 << 8) | b2;
6163            result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
6164            result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
6165            if chunk.len() > 1 {
6166                result.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
6167            } else {
6168                result.push('=');
6169            }
6170            if chunk.len() > 2 {
6171                result.push(CHARS[(n & 0x3F) as usize] as char);
6172            } else {
6173                result.push('=');
6174            }
6175        }
6176        result
6177    }
6178
6179    // ── MappingsIter tests ──────────────────────────────────────
6180
6181    #[test]
6182    fn mappings_iter_matches_decode() {
6183        let vlq = "AAAA;AACA,EAAA;AACA";
6184        let iter_mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
6185        let (decoded, _) = decode_mappings(vlq).unwrap();
6186        assert_eq!(iter_mappings.len(), decoded.len());
6187        for (a, b) in iter_mappings.iter().zip(decoded.iter()) {
6188            assert_eq!(a.generated_line, b.generated_line);
6189            assert_eq!(a.generated_column, b.generated_column);
6190            assert_eq!(a.source, b.source);
6191            assert_eq!(a.original_line, b.original_line);
6192            assert_eq!(a.original_column, b.original_column);
6193            assert_eq!(a.name, b.name);
6194        }
6195    }
6196
6197    #[test]
6198    fn mappings_iter_empty() {
6199        let mappings: Vec<Mapping> = MappingsIter::new("").collect::<Result<_, _>>().unwrap();
6200        assert!(mappings.is_empty());
6201    }
6202
6203    #[test]
6204    fn mappings_iter_generated_only() {
6205        let vlq = "A,AAAA";
6206        let mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
6207        assert_eq!(mappings.len(), 2);
6208        assert_eq!(mappings[0].source, u32::MAX);
6209        assert_eq!(mappings[1].source, 0);
6210    }
6211
6212    #[test]
6213    fn mappings_iter_with_names() {
6214        let vlq = "AAAAA";
6215        let mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
6216        assert_eq!(mappings.len(), 1);
6217        assert_eq!(mappings[0].name, 0);
6218    }
6219
6220    #[test]
6221    fn mappings_iter_multiple_lines() {
6222        let vlq = "AAAA;AACA;AACA";
6223        let mappings: Vec<Mapping> = MappingsIter::new(vlq).collect::<Result<_, _>>().unwrap();
6224        assert_eq!(mappings.len(), 3);
6225        assert_eq!(mappings[0].generated_line, 0);
6226        assert_eq!(mappings[1].generated_line, 1);
6227        assert_eq!(mappings[2].generated_line, 2);
6228    }
6229    // ── Range mappings tests ──────────────────────────────────────
6230
6231    #[test]
6232    fn range_mappings_basic_decode() {
6233        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC,GAAG","rangeMappings":"A,C"}"#;
6234        let sm = SourceMap::from_json(json).unwrap();
6235        assert!(sm.all_mappings()[0].is_range_mapping);
6236        assert!(!sm.all_mappings()[1].is_range_mapping);
6237        assert!(sm.all_mappings()[2].is_range_mapping);
6238    }
6239
6240    #[test]
6241    fn range_mapping_lookup_with_delta() {
6242        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,GAAG","rangeMappings":"A"}"#;
6243        let sm = SourceMap::from_json(json).unwrap();
6244        assert_eq!(sm.original_position_for(0, 0).unwrap().column, 0);
6245        assert_eq!(sm.original_position_for(0, 1).unwrap().column, 1);
6246        assert_eq!(sm.original_position_for(0, 2).unwrap().column, 2);
6247        assert_eq!(sm.original_position_for(0, 3).unwrap().column, 3);
6248    }
6249
6250    #[test]
6251    fn range_mapping_cross_line() {
6252        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA","rangeMappings":"A"}"#;
6253        let sm = SourceMap::from_json(json).unwrap();
6254        assert_eq!(sm.original_position_for(1, 5).unwrap().line, 1);
6255        assert_eq!(sm.original_position_for(1, 5).unwrap().column, 0);
6256        assert_eq!(sm.original_position_for(2, 10).unwrap().line, 2);
6257    }
6258
6259    #[test]
6260    fn range_mapping_encode_roundtrip() {
6261        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC,GAAG","rangeMappings":"A,C"}"#;
6262        assert_eq!(SourceMap::from_json(json).unwrap().encode_range_mappings().unwrap(), "A,C");
6263    }
6264
6265    #[test]
6266    fn no_range_mappings_test() {
6267        let sm = SourceMap::from_json(
6268            r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA"}"#,
6269        )
6270        .unwrap();
6271        assert!(!sm.has_range_mappings());
6272        assert!(sm.encode_range_mappings().is_none());
6273    }
6274
6275    #[test]
6276    fn range_mappings_multi_line_test() {
6277        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC;AAAA","rangeMappings":"A;A"}"#).unwrap();
6278        assert!(sm.all_mappings()[0].is_range_mapping);
6279        assert!(!sm.all_mappings()[1].is_range_mapping);
6280        assert!(sm.all_mappings()[2].is_range_mapping);
6281    }
6282
6283    #[test]
6284    fn range_mappings_json_roundtrip() {
6285        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC,GAAG","rangeMappings":"A,C"}"#).unwrap();
6286        let output = sm.to_json();
6287        assert!(output.contains("rangeMappings"));
6288        assert_eq!(SourceMap::from_json(&output).unwrap().range_mapping_count(), 2);
6289    }
6290
6291    #[test]
6292    fn range_mappings_absent_from_json_test() {
6293        assert!(
6294            !SourceMap::from_json(
6295                r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA"}"#
6296            )
6297            .unwrap()
6298            .to_json()
6299            .contains("rangeMappings")
6300        );
6301    }
6302
6303    #[test]
6304    fn range_mapping_fallback_test() {
6305        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA;KACK","rangeMappings":"A"}"#).unwrap();
6306        let loc = sm.original_position_for(1, 2).unwrap();
6307        assert_eq!(loc.line, 1);
6308        assert_eq!(loc.column, 0);
6309    }
6310
6311    #[test]
6312    fn range_mapping_no_fallback_non_range() {
6313        assert!(
6314            SourceMap::from_json(
6315                r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA"}"#
6316            )
6317            .unwrap()
6318            .original_position_for(1, 5)
6319            .is_none()
6320        );
6321    }
6322
6323    #[test]
6324    fn range_mapping_from_vlq_test() {
6325        let sm = SourceMap::from_vlq_with_range_mappings(
6326            "AAAA,CAAC",
6327            vec!["input.js".into()],
6328            vec![],
6329            None,
6330            None,
6331            vec![],
6332            vec![],
6333            None,
6334            Some("A"),
6335        )
6336        .unwrap();
6337        assert!(sm.all_mappings()[0].is_range_mapping);
6338        assert!(!sm.all_mappings()[1].is_range_mapping);
6339    }
6340
6341    #[test]
6342    fn range_mapping_encode_multi_line_test() {
6343        let sm = SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA,CAAC;AAAA,CAAC","rangeMappings":"A;B"}"#).unwrap();
6344        assert!(sm.all_mappings()[0].is_range_mapping);
6345        assert!(!sm.all_mappings()[1].is_range_mapping);
6346        assert!(!sm.all_mappings()[2].is_range_mapping);
6347        assert!(sm.all_mappings()[3].is_range_mapping);
6348        assert_eq!(sm.encode_range_mappings().unwrap(), "A;B");
6349    }
6350
6351    #[test]
6352    fn range_mapping_from_parts_test() {
6353        let sm = SourceMap::from_parts(
6354            None,
6355            None,
6356            vec!["input.js".into()],
6357            vec![],
6358            vec![],
6359            vec![
6360                Mapping {
6361                    generated_line: 0,
6362                    generated_column: 0,
6363                    source: 0,
6364                    original_line: 0,
6365                    original_column: 0,
6366                    name: NO_NAME,
6367                    is_range_mapping: true,
6368                },
6369                Mapping {
6370                    generated_line: 0,
6371                    generated_column: 5,
6372                    source: 0,
6373                    original_line: 0,
6374                    original_column: 5,
6375                    name: NO_NAME,
6376                    is_range_mapping: false,
6377                },
6378            ],
6379            vec![],
6380            None,
6381            None,
6382        );
6383        assert_eq!(sm.original_position_for(0, 2).unwrap().column, 2);
6384        assert_eq!(sm.original_position_for(0, 6).unwrap().column, 5);
6385    }
6386
6387    #[test]
6388    fn range_mapping_indexed_test() {
6389        let sm = SourceMap::from_json(r#"{"version":3,"sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","rangeMappings":"A"}}]}"#).unwrap();
6390        assert!(sm.has_range_mappings());
6391        assert_eq!(sm.original_position_for(1, 3).unwrap().line, 1);
6392    }
6393
6394    #[test]
6395    fn range_mapping_empty_string_test() {
6396        assert!(!SourceMap::from_json(r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"AAAA","rangeMappings":""}"#).unwrap().has_range_mappings());
6397    }
6398
6399    #[test]
6400    fn range_mapping_lub_no_underflow() {
6401        // Range mapping at col 5, query col 2 with LUB bias
6402        // LUB should find the mapping at col 5, but NOT apply range delta
6403        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"KAAK","rangeMappings":"A"}"#;
6404        let sm = SourceMap::from_json(json).unwrap();
6405
6406        let loc = sm.original_position_for_with_bias(0, 2, Bias::LeastUpperBound);
6407        assert!(loc.is_some());
6408        let loc = loc.unwrap();
6409        // Should return the mapping's own position, not apply a delta
6410        assert_eq!(loc.line, 0);
6411        assert_eq!(loc.column, 5);
6412    }
6413
6414    // ── Builder tests ──────────────────────────────────────────────
6415
6416    #[test]
6417    fn builder_basic() {
6418        let sm = SourceMap::builder()
6419            .file("output.js")
6420            .sources(["input.ts"])
6421            .sources_content([Some("let x = 1;")])
6422            .names(["x"])
6423            .mappings([Mapping {
6424                generated_line: 0,
6425                generated_column: 0,
6426                source: 0,
6427                original_line: 0,
6428                original_column: 4,
6429                name: 0,
6430                is_range_mapping: false,
6431            }])
6432            .build();
6433
6434        assert_eq!(sm.file.as_deref(), Some("output.js"));
6435        assert_eq!(sm.sources, vec!["input.ts"]);
6436        assert_eq!(sm.sources_content, vec![Some("let x = 1;".to_string())]);
6437        assert_eq!(sm.names, vec!["x"]);
6438        assert_eq!(sm.mapping_count(), 1);
6439
6440        let loc = sm.original_position_for(0, 0).unwrap();
6441        assert_eq!(sm.source(loc.source), "input.ts");
6442        assert_eq!(loc.column, 4);
6443        assert_eq!(sm.name(loc.name.unwrap()), "x");
6444    }
6445
6446    #[test]
6447    fn builder_empty() {
6448        let sm = SourceMap::builder().build();
6449        assert_eq!(sm.mapping_count(), 0);
6450        assert_eq!(sm.sources.len(), 0);
6451        assert_eq!(sm.names.len(), 0);
6452        assert!(sm.file.is_none());
6453    }
6454
6455    #[test]
6456    fn builder_multiple_sources() {
6457        let sm = SourceMap::builder()
6458            .sources(["a.ts", "b.ts", "c.ts"])
6459            .sources_content([Some("// a"), Some("// b"), None])
6460            .mappings([
6461                Mapping {
6462                    generated_line: 0,
6463                    generated_column: 0,
6464                    source: 0,
6465                    original_line: 0,
6466                    original_column: 0,
6467                    name: u32::MAX,
6468                    is_range_mapping: false,
6469                },
6470                Mapping {
6471                    generated_line: 1,
6472                    generated_column: 0,
6473                    source: 1,
6474                    original_line: 0,
6475                    original_column: 0,
6476                    name: u32::MAX,
6477                    is_range_mapping: false,
6478                },
6479                Mapping {
6480                    generated_line: 2,
6481                    generated_column: 0,
6482                    source: 2,
6483                    original_line: 0,
6484                    original_column: 0,
6485                    name: u32::MAX,
6486                    is_range_mapping: false,
6487                },
6488            ])
6489            .build();
6490
6491        assert_eq!(sm.sources.len(), 3);
6492        assert_eq!(sm.mapping_count(), 3);
6493        assert_eq!(sm.line_count(), 3);
6494
6495        let loc0 = sm.original_position_for(0, 0).unwrap();
6496        assert_eq!(sm.source(loc0.source), "a.ts");
6497
6498        let loc1 = sm.original_position_for(1, 0).unwrap();
6499        assert_eq!(sm.source(loc1.source), "b.ts");
6500
6501        let loc2 = sm.original_position_for(2, 0).unwrap();
6502        assert_eq!(sm.source(loc2.source), "c.ts");
6503    }
6504
6505    #[test]
6506    fn builder_with_iterators() {
6507        let source_names: Vec<String> = (0..5).map(|i| format!("mod_{i}.ts")).collect();
6508        let mappings = (0..5u32).map(|i| Mapping {
6509            generated_line: i,
6510            generated_column: 0,
6511            source: i,
6512            original_line: i,
6513            original_column: 0,
6514            name: u32::MAX,
6515            is_range_mapping: false,
6516        });
6517
6518        let sm = SourceMap::builder()
6519            .sources(source_names.iter().map(|s| s.as_str()))
6520            .mappings(mappings)
6521            .build();
6522
6523        assert_eq!(sm.sources.len(), 5);
6524        assert_eq!(sm.mapping_count(), 5);
6525        for i in 0..5u32 {
6526            let loc = sm.original_position_for(i, 0).unwrap();
6527            assert_eq!(sm.source(loc.source), format!("mod_{i}.ts"));
6528        }
6529    }
6530
6531    #[test]
6532    fn builder_ignore_list_and_debug_id() {
6533        let sm = SourceMap::builder()
6534            .sources(["app.ts", "node_modules/lib.js"])
6535            .ignore_list([1])
6536            .debug_id("85314830-023f-4cf1-a267-535f4e37bb17")
6537            .build();
6538
6539        assert_eq!(sm.ignore_list, vec![1]);
6540        assert_eq!(sm.debug_id.as_deref(), Some("85314830-023f-4cf1-a267-535f4e37bb17"));
6541    }
6542
6543    #[test]
6544    fn builder_range_mappings() {
6545        let sm = SourceMap::builder()
6546            .sources(["input.ts"])
6547            .mappings([
6548                Mapping {
6549                    generated_line: 0,
6550                    generated_column: 0,
6551                    source: 0,
6552                    original_line: 0,
6553                    original_column: 0,
6554                    name: u32::MAX,
6555                    is_range_mapping: true,
6556                },
6557                Mapping {
6558                    generated_line: 0,
6559                    generated_column: 10,
6560                    source: 0,
6561                    original_line: 5,
6562                    original_column: 0,
6563                    name: u32::MAX,
6564                    is_range_mapping: false,
6565                },
6566            ])
6567            .build();
6568
6569        assert!(sm.has_range_mappings());
6570        assert_eq!(sm.mapping_count(), 2);
6571    }
6572
6573    #[test]
6574    fn builder_json_roundtrip() {
6575        let sm = SourceMap::builder()
6576            .file("out.js")
6577            .source_root("/src/")
6578            .sources(["a.ts", "b.ts"])
6579            .sources_content([Some("// a"), Some("// b")])
6580            .names(["foo", "bar"])
6581            .mappings([
6582                Mapping {
6583                    generated_line: 0,
6584                    generated_column: 0,
6585                    source: 0,
6586                    original_line: 0,
6587                    original_column: 0,
6588                    name: 0,
6589                    is_range_mapping: false,
6590                },
6591                Mapping {
6592                    generated_line: 1,
6593                    generated_column: 5,
6594                    source: 1,
6595                    original_line: 3,
6596                    original_column: 2,
6597                    name: 1,
6598                    is_range_mapping: false,
6599                },
6600            ])
6601            .build();
6602
6603        let json = sm.to_json();
6604        let sm2 = SourceMap::from_json(&json).unwrap();
6605
6606        assert_eq!(sm2.file, sm.file);
6607        // source_root is prepended to sources on parse
6608        assert_eq!(sm2.sources, vec!["/src/a.ts", "/src/b.ts"]);
6609        assert_eq!(sm2.names, sm.names);
6610        assert_eq!(sm2.mapping_count(), sm.mapping_count());
6611
6612        for m in sm.all_mappings() {
6613            let a = sm.original_position_for(m.generated_line, m.generated_column);
6614            let b = sm2.original_position_for(m.generated_line, m.generated_column);
6615            match (a, b) {
6616                (Some(a), Some(b)) => {
6617                    assert_eq!(a.source, b.source);
6618                    assert_eq!(a.line, b.line);
6619                    assert_eq!(a.column, b.column);
6620                    assert_eq!(a.name, b.name);
6621                }
6622                (None, None) => {}
6623                _ => panic!("lookup mismatch"),
6624            }
6625        }
6626    }
6627
6628    // ── Tests for review fixes ────────────────────────────────────
6629
6630    #[test]
6631    fn range_mapping_fallback_column_underflow() {
6632        // Range mapping at col 5, query line 0 col 2 — column < generated_column
6633        // This should NOT panic (saturating_sub prevents u32 underflow)
6634        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"KAAK","rangeMappings":"A"}"#;
6635        let sm = SourceMap::from_json(json).unwrap();
6636        // Query col 2, but the range mapping starts at col 5
6637        // GLB should snap to col 5 mapping, and the range delta should saturate to 0
6638        let loc = sm.original_position_for(0, 2);
6639        // No mapping at col < 5 on this line, so None is expected
6640        assert!(loc.is_none());
6641    }
6642
6643    #[test]
6644    fn range_mapping_fallback_cross_line_column_zero() {
6645        // Range mapping on line 0, col 10, orig(0,10). Query line 1, col 0.
6646        // line_delta = 1, column_delta = 0 (else branch).
6647        // Result: orig_line = 0 + 1 = 1, orig_column = 10 + 0 = 10.
6648        let json = r#"{"version":3,"sources":["input.js"],"names":[],"mappings":"UAAU","rangeMappings":"A"}"#;
6649        let sm = SourceMap::from_json(json).unwrap();
6650        let loc = sm.original_position_for(1, 0).unwrap();
6651        assert_eq!(loc.line, 1);
6652        assert_eq!(loc.column, 10);
6653    }
6654
6655    #[test]
6656    fn vlq_overflow_at_shift_60() {
6657        // Build a VLQ that uses exactly shift=60 (13 continuation chars + 1 terminator)
6658        // This should be rejected by vlq_fast (shift >= 60)
6659        // 13 continuation chars: each is base64 with continuation bit set (e.g. 'g' = 0x20 | 0x00)
6660        // followed by a terminator (e.g. 'A' = 0x00)
6661        let overflow_vlq = "ggggggggggggggA"; // 14 continuation + terminator
6662        let json = format!(
6663            r#"{{"version":3,"sources":["a.js"],"names":[],"mappings":"{}"}}"#,
6664            overflow_vlq
6665        );
6666        let result = SourceMap::from_json(&json);
6667        assert!(result.is_err());
6668        assert!(matches!(result.unwrap_err(), ParseError::Vlq(_)));
6669    }
6670
6671    #[test]
6672    fn lazy_sourcemap_rejects_indexed_maps() {
6673        let json = r#"{"version":3,"sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}}]}"#;
6674        let result = LazySourceMap::from_json_fast(json);
6675        assert!(result.is_err());
6676        assert!(matches!(result.unwrap_err(), ParseError::NestedIndexMap));
6677
6678        let result = LazySourceMap::from_json_no_content(json);
6679        assert!(result.is_err());
6680        assert!(matches!(result.unwrap_err(), ParseError::NestedIndexMap));
6681    }
6682
6683    #[test]
6684    fn lazy_sourcemap_regular_map_still_works() {
6685        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AACA"}"#;
6686        let sm = LazySourceMap::from_json_fast(json).unwrap();
6687        let loc = sm.original_position_for(0, 0).unwrap();
6688        assert_eq!(sm.source(loc.source), "a.js");
6689        assert_eq!(loc.line, 0);
6690    }
6691
6692    #[test]
6693    fn lazy_sourcemap_get_source_name_bounds() {
6694        let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AAAAA"}"#;
6695        let sm = LazySourceMap::from_json_fast(json).unwrap();
6696        assert_eq!(sm.get_source(0), Some("a.js"));
6697        assert_eq!(sm.get_source(1), None);
6698        assert_eq!(sm.get_source(u32::MAX), None);
6699        assert_eq!(sm.get_name(0), Some("foo"));
6700        assert_eq!(sm.get_name(1), None);
6701        assert_eq!(sm.get_name(u32::MAX), None);
6702    }
6703
6704    #[test]
6705    fn lazy_sourcemap_backward_seek() {
6706        // Test that backward seek works correctly in fast-scan mode
6707        let json =
6708            r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA"}"#;
6709        let sm = LazySourceMap::from_json_fast(json).unwrap();
6710
6711        // Forward: decode lines 0, 1, 2, 3
6712        let loc3 = sm.original_position_for(3, 0).unwrap();
6713        assert_eq!(loc3.line, 3);
6714
6715        // Backward: seek line 1 (below watermark of 4)
6716        let loc1 = sm.original_position_for(1, 0).unwrap();
6717        assert_eq!(loc1.line, 1);
6718
6719        // Forward again: line 4
6720        let loc4 = sm.original_position_for(4, 0).unwrap();
6721        assert_eq!(loc4.line, 4);
6722
6723        // Backward again to line 0
6724        let loc0 = sm.original_position_for(0, 0).unwrap();
6725        assert_eq!(loc0.line, 0);
6726    }
6727
6728    #[test]
6729    fn lazy_sourcemap_fast_scan_vs_prescan_consistency() {
6730        // Verify fast_scan and prescan produce identical lookup results
6731        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":["x","y"],"mappings":"AAAAA,KACAC;ACAAD,KACAC"}"#;
6732        let fast = LazySourceMap::from_json_fast(json).unwrap();
6733        let prescan = LazySourceMap::from_json_no_content(json).unwrap();
6734
6735        for line in 0..2 {
6736            for col in [0, 5, 10] {
6737                let a = fast.original_position_for(line, col);
6738                let b = prescan.original_position_for(line, col);
6739                match (&a, &b) {
6740                    (Some(a), Some(b)) => {
6741                        assert_eq!(a.source, b.source, "line={line}, col={col}");
6742                        assert_eq!(a.line, b.line, "line={line}, col={col}");
6743                        assert_eq!(a.column, b.column, "line={line}, col={col}");
6744                        assert_eq!(a.name, b.name, "line={line}, col={col}");
6745                    }
6746                    (None, None) => {}
6747                    _ => panic!("mismatch at line={line}, col={col}: {a:?} vs {b:?}"),
6748                }
6749            }
6750        }
6751    }
6752
6753    #[test]
6754    fn mappings_iter_rejects_two_field_segment() {
6755        // "AA" is 2 fields (generated column + source index, missing original line/column)
6756        let result: Result<Vec<_>, _> = MappingsIter::new("AA").collect();
6757        assert!(result.is_err());
6758        assert!(matches!(result.unwrap_err(), DecodeError::InvalidSegmentLength { fields: 2, .. }));
6759    }
6760
6761    #[test]
6762    fn mappings_iter_rejects_three_field_segment() {
6763        // "AAA" is 3 fields (generated column + source index + original line, missing original column)
6764        let result: Result<Vec<_>, _> = MappingsIter::new("AAA").collect();
6765        assert!(result.is_err());
6766        assert!(matches!(result.unwrap_err(), DecodeError::InvalidSegmentLength { fields: 3, .. }));
6767    }
6768
6769    #[test]
6770    fn decode_mappings_range_caps_end_line() {
6771        // Pathological end_line should not OOM — capped against actual line count
6772        let mappings = "AAAA;AACA";
6773        let (result, offsets) = decode_mappings_range(mappings, 0, 1_000_000).unwrap();
6774        // Should produce mappings for the 2 actual lines, not allocate 1M entries
6775        assert_eq!(result.len(), 2);
6776        assert!(offsets.len() <= 3); // 2 lines + sentinel
6777    }
6778
6779    #[test]
6780    fn decode_range_mappings_cross_line_bound_check() {
6781        // Range mapping index that exceeds the current line's mappings
6782        // should NOT mark a mapping on the next line
6783        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AAAA","rangeMappings":"E"}"#;
6784        let sm = SourceMap::from_json(json).unwrap();
6785        // Line 0 has 1 mapping (idx 0). rangeMappings="E" encodes index 2, which is out of bounds
6786        // for line 0. Line 1's mapping (idx 1) should NOT be marked as range mapping.
6787        assert!(!sm.all_mappings()[1].is_range_mapping);
6788    }
6789
6790    #[test]
6791    fn fast_scan_lines_empty() {
6792        let result = fast_scan_lines("");
6793        assert!(result.is_empty());
6794    }
6795
6796    #[test]
6797    fn fast_scan_lines_no_semicolons() {
6798        let result = fast_scan_lines("AAAA,CAAC");
6799        assert_eq!(result.len(), 1);
6800        assert_eq!(result[0].byte_offset, 0);
6801        assert_eq!(result[0].byte_end, 9);
6802    }
6803
6804    #[test]
6805    fn fast_scan_lines_only_semicolons() {
6806        let result = fast_scan_lines(";;;");
6807        assert_eq!(result.len(), 4);
6808        for info in &result {
6809            assert_eq!(info.byte_offset, info.byte_end); // empty lines
6810        }
6811    }
6812
6813    // ── from_data_url ────────────────────────────────────────────
6814
6815    #[test]
6816    fn from_data_url_base64() {
6817        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6818        let encoded = base64_encode_simple(json);
6819        let url = format!("data:application/json;base64,{encoded}");
6820        let sm = SourceMap::from_data_url(&url).unwrap();
6821        assert_eq!(sm.sources, vec!["a.js"]);
6822        let loc = sm.original_position_for(0, 0).unwrap();
6823        assert_eq!(loc.line, 0);
6824        assert_eq!(loc.column, 0);
6825    }
6826
6827    #[test]
6828    fn from_data_url_base64_charset_utf8() {
6829        let json = r#"{"version":3,"sources":["b.js"],"names":[],"mappings":"AAAA"}"#;
6830        let encoded = base64_encode_simple(json);
6831        let url = format!("data:application/json;charset=utf-8;base64,{encoded}");
6832        let sm = SourceMap::from_data_url(&url).unwrap();
6833        assert_eq!(sm.sources, vec!["b.js"]);
6834    }
6835
6836    #[test]
6837    fn from_data_url_plain_json() {
6838        let json = r#"{"version":3,"sources":["c.js"],"names":[],"mappings":"AAAA"}"#;
6839        let url = format!("data:application/json,{json}");
6840        let sm = SourceMap::from_data_url(&url).unwrap();
6841        assert_eq!(sm.sources, vec!["c.js"]);
6842    }
6843
6844    #[test]
6845    fn from_data_url_percent_encoded() {
6846        let url = "data:application/json,%7B%22version%22%3A3%2C%22sources%22%3A%5B%22d.js%22%5D%2C%22names%22%3A%5B%5D%2C%22mappings%22%3A%22AAAA%22%7D";
6847        let sm = SourceMap::from_data_url(url).unwrap();
6848        assert_eq!(sm.sources, vec!["d.js"]);
6849    }
6850
6851    #[test]
6852    fn from_data_url_invalid_prefix() {
6853        let result = SourceMap::from_data_url("data:text/plain;base64,abc");
6854        assert!(result.is_err());
6855    }
6856
6857    #[test]
6858    fn from_data_url_not_a_data_url() {
6859        let result = SourceMap::from_data_url("https://example.com/foo.map");
6860        assert!(result.is_err());
6861    }
6862
6863    #[test]
6864    fn from_data_url_invalid_base64() {
6865        let result = SourceMap::from_data_url("data:application/json;base64,!!!invalid!!!");
6866        assert!(result.is_err());
6867    }
6868
6869    #[test]
6870    fn from_data_url_roundtrip_with_to_data_url() {
6871        use crate::utils::to_data_url;
6872        let json = r#"{"version":3,"sources":["round.js"],"names":["x"],"mappings":"AACAA"}"#;
6873        let url = to_data_url(json);
6874        let sm = SourceMap::from_data_url(&url).unwrap();
6875        assert_eq!(sm.sources, vec!["round.js"]);
6876        assert_eq!(sm.names, vec!["x"]);
6877    }
6878
6879    // ── to_writer ────────────────────────────────────────────────
6880
6881    #[test]
6882    fn to_writer_basic() {
6883        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6884        let sm = SourceMap::from_json(json).unwrap();
6885        let mut buf = Vec::new();
6886        sm.to_writer(&mut buf).unwrap();
6887        let output = String::from_utf8(buf).unwrap();
6888        assert!(output.contains("\"version\":3"));
6889        assert!(output.contains("\"sources\":[\"a.js\"]"));
6890        // Verify it parses back correctly
6891        let sm2 = SourceMap::from_json(&output).unwrap();
6892        assert_eq!(sm2.sources, sm.sources);
6893    }
6894
6895    #[test]
6896    fn to_writer_matches_to_json() {
6897        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":["foo"],"mappings":"AACAA,GCAA","sourcesContent":["var foo;","var bar;"]}"#;
6898        let sm = SourceMap::from_json(json).unwrap();
6899        let expected = sm.to_json();
6900        let mut buf = Vec::new();
6901        sm.to_writer(&mut buf).unwrap();
6902        let output = String::from_utf8(buf).unwrap();
6903        assert_eq!(output, expected);
6904    }
6905
6906    #[test]
6907    fn to_writer_with_options_excludes_content() {
6908        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","sourcesContent":["var x;"]}"#;
6909        let sm = SourceMap::from_json(json).unwrap();
6910        let mut buf = Vec::new();
6911        sm.to_writer_with_options(&mut buf, true).unwrap();
6912        let output = String::from_utf8(buf).unwrap();
6913        assert!(!output.contains("sourcesContent"));
6914    }
6915
6916    // ── Setter tests ─────────────────────────────────────────────
6917
6918    #[test]
6919    fn set_file() {
6920        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6921        let mut sm = SourceMap::from_json(json).unwrap();
6922        assert_eq!(sm.file, None);
6923
6924        sm.set_file(Some("output.js".to_string()));
6925        assert_eq!(sm.file, Some("output.js".to_string()));
6926        assert!(sm.to_json().contains(r#""file":"output.js""#));
6927
6928        sm.set_file(None);
6929        assert_eq!(sm.file, None);
6930        assert!(!sm.to_json().contains("file"));
6931    }
6932
6933    #[test]
6934    fn set_source_root() {
6935        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6936        let mut sm = SourceMap::from_json(json).unwrap();
6937        assert_eq!(sm.source_root, None);
6938
6939        sm.set_source_root(Some("src/".to_string()));
6940        assert_eq!(sm.source_root, Some("src/".to_string()));
6941        assert!(sm.to_json().contains(r#""sourceRoot":"src/""#));
6942
6943        sm.set_source_root(None);
6944        assert_eq!(sm.source_root, None);
6945    }
6946
6947    #[test]
6948    fn set_debug_id() {
6949        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6950        let mut sm = SourceMap::from_json(json).unwrap();
6951        assert_eq!(sm.debug_id, None);
6952
6953        sm.set_debug_id(Some("abc-123".to_string()));
6954        assert_eq!(sm.debug_id, Some("abc-123".to_string()));
6955        assert!(sm.to_json().contains(r#""debugId":"abc-123""#));
6956
6957        sm.set_debug_id(None);
6958        assert_eq!(sm.debug_id, None);
6959        assert!(!sm.to_json().contains("debugId"));
6960    }
6961
6962    #[test]
6963    fn set_ignore_list() {
6964        let json = r#"{"version":3,"sources":["a.js","b.js"],"names":[],"mappings":"AAAA"}"#;
6965        let mut sm = SourceMap::from_json(json).unwrap();
6966        assert!(sm.ignore_list.is_empty());
6967
6968        sm.set_ignore_list(vec![0, 1]);
6969        assert_eq!(sm.ignore_list, vec![0, 1]);
6970        assert!(sm.to_json().contains("\"ignoreList\":[0,1]"));
6971
6972        sm.set_ignore_list(vec![]);
6973        assert!(sm.ignore_list.is_empty());
6974        assert!(!sm.to_json().contains("ignoreList"));
6975    }
6976
6977    #[test]
6978    fn set_sources() {
6979        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6980        let mut sm = SourceMap::from_json(json).unwrap();
6981        assert_eq!(sm.sources, vec!["a.js"]);
6982
6983        sm.set_sources(vec![Some("x.js".to_string()), Some("y.js".to_string())]);
6984        assert_eq!(sm.sources, vec!["x.js", "y.js"]);
6985        assert_eq!(sm.source_index("x.js"), Some(0));
6986        assert_eq!(sm.source_index("y.js"), Some(1));
6987        assert_eq!(sm.source_index("a.js"), None);
6988    }
6989
6990    #[test]
6991    fn set_sources_with_source_root() {
6992        let json =
6993            r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
6994        let mut sm = SourceMap::from_json(json).unwrap();
6995        assert_eq!(sm.sources, vec!["src/a.js"]);
6996
6997        sm.set_sources(vec![Some("b.js".to_string())]);
6998        assert_eq!(sm.sources, vec!["src/b.js"]);
6999    }
7000
7001    #[test]
7002    fn to_data_url_roundtrip() {
7003        let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
7004        let sm = SourceMap::from_json(json).unwrap();
7005        let url = sm.to_data_url();
7006        assert!(url.starts_with("data:application/json;base64,"));
7007        let sm2 = SourceMap::from_data_url(&url).unwrap();
7008        assert_eq!(sm.sources, sm2.sources);
7009        assert_eq!(sm.to_json(), sm2.to_json());
7010    }
7011}