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