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