Skip to main content

srcmap_sourcemap/
lib.rs

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