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