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