1use std::cell::OnceCell;
27use std::collections::HashMap;
28use std::fmt;
29
30use serde::Deserialize;
31use srcmap_codec::DecodeError;
32
33const NO_SOURCE: u32 = u32::MAX;
36const NO_NAME: u32 = u32::MAX;
37
38#[derive(Debug, Clone, Copy)]
42pub struct Mapping {
43 pub generated_line: u32,
44 pub generated_column: u32,
45 pub source: u32,
47 pub original_line: u32,
48 pub original_column: u32,
49 pub name: u32,
51}
52
53#[derive(Debug, Clone)]
55pub struct OriginalLocation {
56 pub source: u32,
57 pub line: u32,
58 pub column: u32,
59 pub name: Option<u32>,
60}
61
62#[derive(Debug, Clone)]
64pub struct GeneratedLocation {
65 pub line: u32,
66 pub column: u32,
67}
68
69#[derive(Debug)]
71pub enum ParseError {
72 Json(serde_json::Error),
73 Vlq(DecodeError),
74 InvalidVersion(u32),
75}
76
77impl fmt::Display for ParseError {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 Self::Json(e) => write!(f, "JSON parse error: {e}"),
81 Self::Vlq(e) => write!(f, "VLQ decode error: {e}"),
82 Self::InvalidVersion(v) => write!(f, "unsupported source map version: {v}"),
83 }
84 }
85}
86
87impl std::error::Error for ParseError {}
88
89impl From<serde_json::Error> for ParseError {
90 fn from(e: serde_json::Error) -> Self {
91 Self::Json(e)
92 }
93}
94
95impl From<DecodeError> for ParseError {
96 fn from(e: DecodeError) -> Self {
97 Self::Vlq(e)
98 }
99}
100
101#[derive(Deserialize)]
104struct RawSourceMap<'a> {
105 version: u32,
106 #[serde(default)]
107 file: Option<String>,
108 #[serde(default, rename = "sourceRoot")]
109 source_root: Option<String>,
110 #[serde(default)]
111 sources: Vec<Option<String>>,
112 #[serde(default, rename = "sourcesContent")]
113 sources_content: Option<Vec<Option<String>>>,
114 #[serde(default)]
115 names: Vec<String>,
116 #[serde(default, borrow)]
117 mappings: &'a str,
118 #[serde(default, rename = "ignoreList")]
119 ignore_list: Vec<u32>,
120 #[serde(default)]
122 sections: Option<Vec<RawSection>>,
123}
124
125#[derive(Deserialize)]
127struct RawSection {
128 offset: RawOffset,
129 map: serde_json::Value,
130}
131
132#[derive(Deserialize)]
133struct RawOffset {
134 line: u32,
135 column: u32,
136}
137
138#[derive(Debug, Clone)]
142pub struct SourceMap {
143 pub file: Option<String>,
144 pub source_root: Option<String>,
145 pub sources: Vec<String>,
146 pub sources_content: Vec<Option<String>>,
147 pub names: Vec<String>,
148 pub ignore_list: Vec<u32>,
149
150 mappings: Vec<Mapping>,
152
153 line_offsets: Vec<u32>,
156
157 reverse_index: OnceCell<Vec<u32>>,
160
161 source_map: HashMap<String, u32>,
163}
164
165impl SourceMap {
166 pub fn from_json(json: &str) -> Result<Self, ParseError> {
169 let raw: RawSourceMap<'_> = serde_json::from_str(json)?;
170
171 if raw.version != 3 {
172 return Err(ParseError::InvalidVersion(raw.version));
173 }
174
175 if let Some(sections) = raw.sections {
177 return Self::from_sections(raw.file, sections);
178 }
179
180 Self::from_regular(raw)
181 }
182
183 fn from_regular(raw: RawSourceMap<'_>) -> Result<Self, ParseError> {
185 let source_root = raw.source_root.as_deref().unwrap_or("");
187 let sources: Vec<String> = raw
188 .sources
189 .iter()
190 .map(|s| match s {
191 Some(s) if !source_root.is_empty() => format!("{source_root}{s}"),
192 Some(s) => s.clone(),
193 None => String::new(),
194 })
195 .collect();
196
197 let sources_content = raw.sources_content.unwrap_or_default();
198
199 let source_map: HashMap<String, u32> = sources
201 .iter()
202 .enumerate()
203 .map(|(i, s)| (s.clone(), i as u32))
204 .collect();
205
206 let (mappings, line_offsets) = decode_mappings(raw.mappings)?;
208
209 Ok(Self {
210 file: raw.file,
211 source_root: raw.source_root,
212 sources,
213 sources_content,
214 names: raw.names,
215 ignore_list: raw.ignore_list,
216 mappings,
217 line_offsets,
218 reverse_index: OnceCell::new(),
219 source_map,
220 })
221 }
222
223 fn from_sections(file: Option<String>, sections: Vec<RawSection>) -> Result<Self, ParseError> {
225 let mut all_sources: Vec<String> = Vec::new();
226 let mut all_sources_content: Vec<Option<String>> = Vec::new();
227 let mut all_names: Vec<String> = Vec::new();
228 let mut all_mappings: Vec<Mapping> = Vec::new();
229 let mut all_ignore_list: Vec<u32> = Vec::new();
230 let mut max_line: u32 = 0;
231
232 let mut source_index_map: HashMap<String, u32> = HashMap::new();
234 let mut name_index_map: HashMap<String, u32> = HashMap::new();
235
236 for section in §ions {
237 let section_json = serde_json::to_string(§ion.map).map_err(ParseError::Json)?;
238 let sub = Self::from_json(§ion_json)?;
239
240 let line_offset = section.offset.line;
241 let col_offset = section.offset.column;
242
243 let source_remap: Vec<u32> = sub
245 .sources
246 .iter()
247 .enumerate()
248 .map(|(i, s)| {
249 if let Some(&existing) = source_index_map.get(s) {
250 existing
251 } else {
252 let idx = all_sources.len() as u32;
253 all_sources.push(s.clone());
254 let content = sub.sources_content.get(i).cloned().unwrap_or(None);
256 all_sources_content.push(content);
257 source_index_map.insert(s.clone(), idx);
258 idx
259 }
260 })
261 .collect();
262
263 let name_remap: Vec<u32> = sub
265 .names
266 .iter()
267 .map(|n| {
268 if let Some(&existing) = name_index_map.get(n) {
269 existing
270 } else {
271 let idx = all_names.len() as u32;
272 all_names.push(n.clone());
273 name_index_map.insert(n.clone(), idx);
274 idx
275 }
276 })
277 .collect();
278
279 for &idx in &sub.ignore_list {
281 let global_idx = source_remap[idx as usize];
282 if !all_ignore_list.contains(&global_idx) {
283 all_ignore_list.push(global_idx);
284 }
285 }
286
287 for m in &sub.mappings {
289 let gen_line = m.generated_line + line_offset;
290 let gen_col = if m.generated_line == 0 {
291 m.generated_column + col_offset
292 } else {
293 m.generated_column
294 };
295
296 all_mappings.push(Mapping {
297 generated_line: gen_line,
298 generated_column: gen_col,
299 source: if m.source == NO_SOURCE {
300 NO_SOURCE
301 } else {
302 source_remap[m.source as usize]
303 },
304 original_line: m.original_line,
305 original_column: m.original_column,
306 name: if m.name == NO_NAME {
307 NO_NAME
308 } else {
309 name_remap[m.name as usize]
310 },
311 });
312
313 if gen_line > max_line {
314 max_line = gen_line;
315 }
316 }
317 }
318
319 all_mappings.sort_unstable_by(|a, b| {
321 a.generated_line
322 .cmp(&b.generated_line)
323 .then(a.generated_column.cmp(&b.generated_column))
324 });
325
326 let line_count = if all_mappings.is_empty() {
328 0
329 } else {
330 max_line as usize + 1
331 };
332 let mut line_offsets: Vec<u32> = vec![0; line_count + 1];
333 let mut current_line: usize = 0;
334 for (i, m) in all_mappings.iter().enumerate() {
335 while current_line < m.generated_line as usize {
336 current_line += 1;
337 if current_line < line_offsets.len() {
338 line_offsets[current_line] = i as u32;
339 }
340 }
341 }
342 if !line_offsets.is_empty() {
344 let last = all_mappings.len() as u32;
345 for offset in line_offsets.iter_mut().skip(current_line + 1) {
346 *offset = last;
347 }
348 }
349
350 Ok(Self {
351 file,
352 source_root: None,
353 sources: all_sources.clone(),
354 sources_content: all_sources_content,
355 names: all_names,
356 ignore_list: all_ignore_list,
357 mappings: all_mappings,
358 line_offsets,
359 reverse_index: OnceCell::new(),
360 source_map: all_sources
361 .into_iter()
362 .enumerate()
363 .map(|(i, s)| (s, i as u32))
364 .collect(),
365 })
366 }
367
368 pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
373 let line_idx = line as usize;
374 if line_idx + 1 >= self.line_offsets.len() {
375 return None;
376 }
377
378 let start = self.line_offsets[line_idx] as usize;
379 let end = self.line_offsets[line_idx + 1] as usize;
380
381 if start == end {
382 return None;
383 }
384
385 let line_mappings = &self.mappings[start..end];
386
387 let idx = match line_mappings.binary_search_by_key(&column, |m| m.generated_column) {
389 Ok(i) => i,
390 Err(0) => return None,
391 Err(i) => i - 1,
392 };
393
394 let mapping = &line_mappings[idx];
395
396 if mapping.source == NO_SOURCE {
397 return None;
398 }
399
400 Some(OriginalLocation {
401 source: mapping.source,
402 line: mapping.original_line,
403 column: mapping.original_column,
404 name: if mapping.name == NO_NAME {
405 None
406 } else {
407 Some(mapping.name)
408 },
409 })
410 }
411
412 pub fn generated_position_for(
416 &self,
417 source: &str,
418 line: u32,
419 column: u32,
420 ) -> Option<GeneratedLocation> {
421 let &source_idx = self.source_map.get(source)?;
422
423 let reverse_index = self
424 .reverse_index
425 .get_or_init(|| build_reverse_index(&self.mappings));
426
427 let idx = reverse_index.partition_point(|&i| {
429 let m = &self.mappings[i as usize];
430 (m.source, m.original_line, m.original_column) < (source_idx, line, column)
431 });
432
433 if idx >= reverse_index.len() {
434 return None;
435 }
436
437 let mapping = &self.mappings[reverse_index[idx] as usize];
438
439 if mapping.source != source_idx {
440 return None;
441 }
442
443 Some(GeneratedLocation {
444 line: mapping.generated_line,
445 column: mapping.generated_column,
446 })
447 }
448
449 pub fn source(&self, index: u32) -> &str {
451 &self.sources[index as usize]
452 }
453
454 pub fn name(&self, index: u32) -> &str {
456 &self.names[index as usize]
457 }
458
459 pub fn source_index(&self, name: &str) -> Option<u32> {
461 self.source_map.get(name).copied()
462 }
463
464 pub fn mapping_count(&self) -> usize {
466 self.mappings.len()
467 }
468
469 pub fn line_count(&self) -> usize {
471 self.line_offsets.len().saturating_sub(1)
472 }
473
474 pub fn mappings_for_line(&self, line: u32) -> &[Mapping] {
476 let line_idx = line as usize;
477 if line_idx + 1 >= self.line_offsets.len() {
478 return &[];
479 }
480 let start = self.line_offsets[line_idx] as usize;
481 let end = self.line_offsets[line_idx + 1] as usize;
482 &self.mappings[start..end]
483 }
484
485 pub fn all_mappings(&self) -> &[Mapping] {
487 &self.mappings
488 }
489
490 pub fn to_json(&self) -> String {
495 let mappings = self.encode_mappings();
496
497 let mut json = String::with_capacity(256 + mappings.len());
498 json.push_str(r#"{"version":3"#);
499
500 if let Some(ref file) = self.file {
501 json.push_str(r#","file":"#);
502 json_quote_into(&mut json, file);
503 }
504
505 if let Some(ref root) = self.source_root {
506 json.push_str(r#","sourceRoot":"#);
507 json_quote_into(&mut json, root);
508 }
509
510 json.push_str(r#","sources":["#);
511 for (i, s) in self.sources.iter().enumerate() {
512 if i > 0 {
513 json.push(',');
514 }
515 json_quote_into(&mut json, s);
516 }
517 json.push(']');
518
519 if !self.sources_content.is_empty() && self.sources_content.iter().any(|c| c.is_some()) {
520 json.push_str(r#","sourcesContent":["#);
521 for (i, c) in self.sources_content.iter().enumerate() {
522 if i > 0 {
523 json.push(',');
524 }
525 match c {
526 Some(content) => json_quote_into(&mut json, content),
527 None => json.push_str("null"),
528 }
529 }
530 json.push(']');
531 }
532
533 json.push_str(r#","names":["#);
534 for (i, n) in self.names.iter().enumerate() {
535 if i > 0 {
536 json.push(',');
537 }
538 json_quote_into(&mut json, n);
539 }
540 json.push(']');
541
542 json.push_str(r#","mappings":"#);
543 json_quote_into(&mut json, &mappings);
544
545 if !self.ignore_list.is_empty() {
546 json.push_str(r#","ignoreList":["#);
547 for (i, &idx) in self.ignore_list.iter().enumerate() {
548 if i > 0 {
549 json.push(',');
550 }
551 json.push_str(&idx.to_string());
552 }
553 json.push(']');
554 }
555
556 json.push('}');
557 json
558 }
559
560 fn encode_mappings(&self) -> String {
562 if self.mappings.is_empty() {
563 return String::new();
564 }
565
566 let mut out: Vec<u8> = Vec::with_capacity(self.mappings.len() * 6);
567
568 let mut prev_gen_col: i64 = 0;
569 let mut prev_source: i64 = 0;
570 let mut prev_orig_line: i64 = 0;
571 let mut prev_orig_col: i64 = 0;
572 let mut prev_name: i64 = 0;
573 let mut prev_gen_line: u32 = 0;
574 let mut first_in_line = true;
575
576 for m in &self.mappings {
577 while prev_gen_line < m.generated_line {
578 out.push(b';');
579 prev_gen_line += 1;
580 prev_gen_col = 0;
581 first_in_line = true;
582 }
583
584 if !first_in_line {
585 out.push(b',');
586 }
587 first_in_line = false;
588
589 srcmap_codec::vlq_encode(&mut out, m.generated_column as i64 - prev_gen_col);
590 prev_gen_col = m.generated_column as i64;
591
592 if m.source != NO_SOURCE {
593 srcmap_codec::vlq_encode(&mut out, m.source as i64 - prev_source);
594 prev_source = m.source as i64;
595
596 srcmap_codec::vlq_encode(&mut out, m.original_line as i64 - prev_orig_line);
597 prev_orig_line = m.original_line as i64;
598
599 srcmap_codec::vlq_encode(&mut out, m.original_column as i64 - prev_orig_col);
600 prev_orig_col = m.original_column as i64;
601
602 if m.name != NO_NAME {
603 srcmap_codec::vlq_encode(&mut out, m.name as i64 - prev_name);
604 prev_name = m.name as i64;
605 }
606 }
607 }
608
609 unsafe { String::from_utf8_unchecked(out) }
611 }
612}
613
614fn json_quote_into(out: &mut String, s: &str) {
616 out.push('"');
617 for c in s.chars() {
618 match c {
619 '"' => out.push_str("\\\""),
620 '\\' => out.push_str("\\\\"),
621 '\n' => out.push_str("\\n"),
622 '\r' => out.push_str("\\r"),
623 '\t' => out.push_str("\\t"),
624 c if c < '\x20' => {
625 out.push_str(&format!("\\u{:04x}", c as u32));
626 }
627 c => out.push(c),
628 }
629 }
630 out.push('"');
631}
632
633const B64: [u8; 128] = {
637 let mut table = [0xFFu8; 128];
638 let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
639 let mut i = 0u8;
640 while i < 64 {
641 table[chars[i as usize] as usize] = i;
642 i += 1;
643 }
644 table
645};
646
647#[inline(always)]
650fn vlq_fast(bytes: &[u8], pos: &mut usize) -> Result<i64, DecodeError> {
651 let p = *pos;
652 if p >= bytes.len() {
653 return Err(DecodeError::UnexpectedEof { offset: p });
654 }
655
656 let b0 = bytes[p];
657 if b0 >= 128 {
658 return Err(DecodeError::InvalidBase64 {
659 byte: b0,
660 offset: p,
661 });
662 }
663 let d0 = B64[b0 as usize];
664 if d0 == 0xFF {
665 return Err(DecodeError::InvalidBase64 {
666 byte: b0,
667 offset: p,
668 });
669 }
670
671 if (d0 & 0x20) == 0 {
673 *pos = p + 1;
674 let val = (d0 >> 1) as i64;
675 return Ok(if (d0 & 1) != 0 { -val } else { val });
676 }
677
678 let mut result: u64 = (d0 & 0x1F) as u64;
680 let mut shift: u32 = 5;
681 let mut i = p + 1;
682
683 loop {
684 if i >= bytes.len() {
685 return Err(DecodeError::UnexpectedEof { offset: i });
686 }
687 let b = bytes[i];
688 if b >= 128 {
689 return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
690 }
691 let d = B64[b as usize];
692 if d == 0xFF {
693 return Err(DecodeError::InvalidBase64 { byte: b, offset: i });
694 }
695 i += 1;
696
697 if shift >= 64 {
698 return Err(DecodeError::VlqOverflow { offset: p });
699 }
700
701 result += ((d & 0x1F) as u64) << shift;
702 shift += 5;
703
704 if (d & 0x20) == 0 {
705 break;
706 }
707 }
708
709 *pos = i;
710 let value = if (result & 1) == 1 {
711 -((result >> 1) as i64)
712 } else {
713 (result >> 1) as i64
714 };
715 Ok(value)
716}
717
718fn decode_mappings(input: &str) -> Result<(Vec<Mapping>, Vec<u32>), DecodeError> {
719 if input.is_empty() {
720 return Ok((Vec::new(), vec![0]));
721 }
722
723 let bytes = input.as_bytes();
724 let len = bytes.len();
725
726 let line_count = bytes.iter().filter(|&&b| b == b';').count() + 1;
728 let approx_segments = bytes.iter().filter(|&&b| b == b',').count() + line_count;
729
730 let mut mappings: Vec<Mapping> = Vec::with_capacity(approx_segments);
731 let mut line_offsets: Vec<u32> = Vec::with_capacity(line_count + 1);
732
733 let mut source_index: i64 = 0;
734 let mut original_line: i64 = 0;
735 let mut original_column: i64 = 0;
736 let mut name_index: i64 = 0;
737 let mut generated_line: u32 = 0;
738 let mut pos: usize = 0;
739
740 loop {
741 line_offsets.push(mappings.len() as u32);
742 let mut generated_column: i64 = 0;
743 let mut saw_semicolon = false;
744
745 while pos < len {
746 let byte = bytes[pos];
747
748 if byte == b';' {
749 pos += 1;
750 saw_semicolon = true;
751 break;
752 }
753
754 if byte == b',' {
755 pos += 1;
756 continue;
757 }
758
759 generated_column += vlq_fast(bytes, &mut pos)?;
761
762 if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
763 source_index += vlq_fast(bytes, &mut pos)?;
765 original_line += vlq_fast(bytes, &mut pos)?;
766 original_column += vlq_fast(bytes, &mut pos)?;
767
768 let name = if pos < len && bytes[pos] != b',' && bytes[pos] != b';' {
770 name_index += vlq_fast(bytes, &mut pos)?;
771 name_index as u32
772 } else {
773 NO_NAME
774 };
775
776 mappings.push(Mapping {
777 generated_line,
778 generated_column: generated_column as u32,
779 source: source_index as u32,
780 original_line: original_line as u32,
781 original_column: original_column as u32,
782 name,
783 });
784 } else {
785 mappings.push(Mapping {
787 generated_line,
788 generated_column: generated_column as u32,
789 source: NO_SOURCE,
790 original_line: 0,
791 original_column: 0,
792 name: NO_NAME,
793 });
794 }
795 }
796
797 if !saw_semicolon {
798 break;
799 }
800 generated_line += 1;
801 }
802
803 line_offsets.push(mappings.len() as u32);
805
806 Ok((mappings, line_offsets))
807}
808
809fn build_reverse_index(mappings: &[Mapping]) -> Vec<u32> {
811 let mut indices: Vec<u32> = (0..mappings.len() as u32)
812 .filter(|&i| mappings[i as usize].source != NO_SOURCE)
813 .collect();
814
815 indices.sort_unstable_by(|&a, &b| {
816 let ma = &mappings[a as usize];
817 let mb = &mappings[b as usize];
818 ma.source
819 .cmp(&mb.source)
820 .then(ma.original_line.cmp(&mb.original_line))
821 .then(ma.original_column.cmp(&mb.original_column))
822 });
823
824 indices
825}
826
827#[cfg(test)]
830mod tests {
831 use super::*;
832
833 fn simple_map() -> &'static str {
834 r#"{"version":3,"sources":["input.js"],"names":["hello"],"mappings":"AAAA;AACA,EAAA;AACA"}"#
835 }
836
837 #[test]
838 fn parse_basic() {
839 let sm = SourceMap::from_json(simple_map()).unwrap();
840 assert_eq!(sm.sources, vec!["input.js"]);
841 assert_eq!(sm.names, vec!["hello"]);
842 assert_eq!(sm.line_count(), 3);
843 assert!(sm.mapping_count() > 0);
844 }
845
846 #[test]
847 fn to_json_roundtrip() {
848 let json = simple_map();
849 let sm = SourceMap::from_json(json).unwrap();
850 let output = sm.to_json();
851
852 let sm2 = SourceMap::from_json(&output).unwrap();
854 assert_eq!(sm2.sources, sm.sources);
855 assert_eq!(sm2.names, sm.names);
856 assert_eq!(sm2.mapping_count(), sm.mapping_count());
857 assert_eq!(sm2.line_count(), sm.line_count());
858
859 for m in sm.all_mappings() {
861 let loc1 = sm.original_position_for(m.generated_line, m.generated_column);
862 let loc2 = sm2.original_position_for(m.generated_line, m.generated_column);
863 match (loc1, loc2) {
864 (Some(a), Some(b)) => {
865 assert_eq!(a.source, b.source);
866 assert_eq!(a.line, b.line);
867 assert_eq!(a.column, b.column);
868 assert_eq!(a.name, b.name);
869 }
870 (None, None) => {}
871 _ => panic!(
872 "lookup mismatch at ({}, {})",
873 m.generated_line, m.generated_column
874 ),
875 }
876 }
877 }
878
879 #[test]
880 fn to_json_roundtrip_large() {
881 let json = generate_test_sourcemap(50, 10, 3);
882 let sm = SourceMap::from_json(&json).unwrap();
883 let output = sm.to_json();
884 let sm2 = SourceMap::from_json(&output).unwrap();
885
886 assert_eq!(sm2.mapping_count(), sm.mapping_count());
887
888 for line in (0..sm.line_count() as u32).step_by(5) {
890 for col in [0u32, 10, 20, 50] {
891 let a = sm.original_position_for(line, col);
892 let b = sm2.original_position_for(line, col);
893 match (a, b) {
894 (Some(a), Some(b)) => {
895 assert_eq!(a.source, b.source);
896 assert_eq!(a.line, b.line);
897 assert_eq!(a.column, b.column);
898 }
899 (None, None) => {}
900 _ => panic!("mismatch at ({line}, {col})"),
901 }
902 }
903 }
904 }
905
906 #[test]
907 fn to_json_preserves_fields() {
908 let json = r#"{"version":3,"file":"out.js","sourceRoot":"src/","sources":["app.ts"],"sourcesContent":["const x = 1;"],"names":["x"],"mappings":"AAAAA","ignoreList":[0]}"#;
909 let sm = SourceMap::from_json(json).unwrap();
910 let output = sm.to_json();
911
912 assert!(output.contains(r#""file":"out.js""#));
913 assert!(output.contains(r#""sourceRoot":"src/""#));
914 assert!(output.contains(r#""sourcesContent":["const x = 1;"]"#));
915 assert!(output.contains(r#""ignoreList":[0]"#));
916
917 let sm2 = SourceMap::from_json(&output).unwrap();
919 assert_eq!(sm2.file.as_deref(), Some("out.js"));
920 assert_eq!(sm2.ignore_list, vec![0]);
921 }
922
923 #[test]
924 fn original_position_for_exact_match() {
925 let sm = SourceMap::from_json(simple_map()).unwrap();
926 let loc = sm.original_position_for(0, 0).unwrap();
927 assert_eq!(loc.source, 0);
928 assert_eq!(loc.line, 0);
929 assert_eq!(loc.column, 0);
930 }
931
932 #[test]
933 fn original_position_for_column_within_segment() {
934 let sm = SourceMap::from_json(simple_map()).unwrap();
935 let loc = sm.original_position_for(1, 5);
937 assert!(loc.is_some());
938 }
939
940 #[test]
941 fn original_position_for_nonexistent_line() {
942 let sm = SourceMap::from_json(simple_map()).unwrap();
943 assert!(sm.original_position_for(999, 0).is_none());
944 }
945
946 #[test]
947 fn original_position_for_before_first_mapping() {
948 let sm = SourceMap::from_json(simple_map()).unwrap();
950 let loc = sm.original_position_for(1, 0);
951 let _ = loc;
954 }
955
956 #[test]
957 fn generated_position_for_basic() {
958 let sm = SourceMap::from_json(simple_map()).unwrap();
959 let loc = sm.generated_position_for("input.js", 0, 0).unwrap();
960 assert_eq!(loc.line, 0);
961 assert_eq!(loc.column, 0);
962 }
963
964 #[test]
965 fn generated_position_for_unknown_source() {
966 let sm = SourceMap::from_json(simple_map()).unwrap();
967 assert!(sm.generated_position_for("nonexistent.js", 0, 0).is_none());
968 }
969
970 #[test]
971 fn parse_invalid_version() {
972 let json = r#"{"version":2,"sources":[],"names":[],"mappings":""}"#;
973 let err = SourceMap::from_json(json).unwrap_err();
974 assert!(matches!(err, ParseError::InvalidVersion(2)));
975 }
976
977 #[test]
978 fn parse_empty_mappings() {
979 let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
980 let sm = SourceMap::from_json(json).unwrap();
981 assert_eq!(sm.mapping_count(), 0);
982 assert!(sm.original_position_for(0, 0).is_none());
983 }
984
985 #[test]
986 fn parse_with_source_root() {
987 let json = r#"{"version":3,"sourceRoot":"src/","sources":["foo.js"],"names":[],"mappings":"AAAA"}"#;
988 let sm = SourceMap::from_json(json).unwrap();
989 assert_eq!(sm.sources, vec!["src/foo.js"]);
990 }
991
992 #[test]
993 fn parse_with_sources_content() {
994 let json = r#"{"version":3,"sources":["a.js"],"sourcesContent":["var x = 1;"],"names":[],"mappings":"AAAA"}"#;
995 let sm = SourceMap::from_json(json).unwrap();
996 assert_eq!(sm.sources_content, vec![Some("var x = 1;".to_string())]);
997 }
998
999 #[test]
1000 fn mappings_for_line() {
1001 let sm = SourceMap::from_json(simple_map()).unwrap();
1002 let line0 = sm.mappings_for_line(0);
1003 assert!(!line0.is_empty());
1004 let empty = sm.mappings_for_line(999);
1005 assert!(empty.is_empty());
1006 }
1007
1008 #[test]
1009 fn large_sourcemap_lookup() {
1010 let json = generate_test_sourcemap(500, 20, 5);
1012 let sm = SourceMap::from_json(&json).unwrap();
1013
1014 for line in [0, 10, 100, 250, 499] {
1016 let mappings = sm.mappings_for_line(line);
1017 if let Some(m) = mappings.first() {
1018 let loc = sm.original_position_for(line, m.generated_column);
1019 assert!(loc.is_some(), "lookup failed for line {line}");
1020 }
1021 }
1022 }
1023
1024 #[test]
1025 fn reverse_lookup_roundtrip() {
1026 let json = generate_test_sourcemap(100, 10, 3);
1027 let sm = SourceMap::from_json(&json).unwrap();
1028
1029 let mapping = &sm.mappings[50];
1031 if mapping.source != NO_SOURCE {
1032 let source_name = sm.source(mapping.source);
1033 let result = sm.generated_position_for(
1034 source_name,
1035 mapping.original_line,
1036 mapping.original_column,
1037 );
1038 assert!(result.is_some(), "reverse lookup failed");
1039 }
1040 }
1041
1042 #[test]
1043 fn indexed_source_map() {
1044 let json = r#"{
1045 "version": 3,
1046 "file": "bundle.js",
1047 "sections": [
1048 {
1049 "offset": {"line": 0, "column": 0},
1050 "map": {
1051 "version": 3,
1052 "sources": ["a.js"],
1053 "names": ["foo"],
1054 "mappings": "AAAAA"
1055 }
1056 },
1057 {
1058 "offset": {"line": 10, "column": 0},
1059 "map": {
1060 "version": 3,
1061 "sources": ["b.js"],
1062 "names": ["bar"],
1063 "mappings": "AAAAA"
1064 }
1065 }
1066 ]
1067 }"#;
1068
1069 let sm = SourceMap::from_json(json).unwrap();
1070
1071 assert_eq!(sm.sources.len(), 2);
1073 assert!(sm.sources.contains(&"a.js".to_string()));
1074 assert!(sm.sources.contains(&"b.js".to_string()));
1075
1076 assert_eq!(sm.names.len(), 2);
1078 assert!(sm.names.contains(&"foo".to_string()));
1079 assert!(sm.names.contains(&"bar".to_string()));
1080
1081 let loc = sm.original_position_for(0, 0).unwrap();
1083 assert_eq!(sm.source(loc.source), "a.js");
1084 assert_eq!(loc.line, 0);
1085 assert_eq!(loc.column, 0);
1086
1087 let loc = sm.original_position_for(10, 0).unwrap();
1089 assert_eq!(sm.source(loc.source), "b.js");
1090 assert_eq!(loc.line, 0);
1091 assert_eq!(loc.column, 0);
1092 }
1093
1094 #[test]
1095 fn indexed_source_map_shared_sources() {
1096 let json = r#"{
1098 "version": 3,
1099 "sections": [
1100 {
1101 "offset": {"line": 0, "column": 0},
1102 "map": {
1103 "version": 3,
1104 "sources": ["shared.js"],
1105 "names": [],
1106 "mappings": "AAAA"
1107 }
1108 },
1109 {
1110 "offset": {"line": 5, "column": 0},
1111 "map": {
1112 "version": 3,
1113 "sources": ["shared.js"],
1114 "names": [],
1115 "mappings": "AACA"
1116 }
1117 }
1118 ]
1119 }"#;
1120
1121 let sm = SourceMap::from_json(json).unwrap();
1122
1123 assert_eq!(sm.sources.len(), 1);
1125 assert_eq!(sm.sources[0], "shared.js");
1126
1127 let loc0 = sm.original_position_for(0, 0).unwrap();
1129 let loc5 = sm.original_position_for(5, 0).unwrap();
1130 assert_eq!(loc0.source, loc5.source);
1131 }
1132
1133 #[test]
1134 fn parse_ignore_list() {
1135 let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js"],"names":[],"mappings":"AAAA;ACAA","ignoreList":[1]}"#;
1136 let sm = SourceMap::from_json(json).unwrap();
1137 assert_eq!(sm.ignore_list, vec![1]);
1138 }
1139
1140 fn build_sourcemap_json(
1142 sources: &[&str],
1143 names: &[&str],
1144 mappings_data: &[Vec<Vec<i64>>],
1145 ) -> String {
1146 let mappings_vec: Vec<Vec<Vec<i64>>> = mappings_data.to_vec();
1147 let encoded = srcmap_codec::encode(&mappings_vec);
1148 format!(
1149 r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
1150 sources
1151 .iter()
1152 .map(|s| format!("\"{s}\""))
1153 .collect::<Vec<_>>()
1154 .join(","),
1155 names
1156 .iter()
1157 .map(|n| format!("\"{n}\""))
1158 .collect::<Vec<_>>()
1159 .join(","),
1160 encoded,
1161 )
1162 }
1163
1164 #[test]
1167 fn decode_multiple_consecutive_semicolons() {
1168 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;;AACA"}"#;
1169 let sm = SourceMap::from_json(json).unwrap();
1170 assert_eq!(sm.line_count(), 4);
1171 assert!(sm.mappings_for_line(1).is_empty());
1172 assert!(sm.mappings_for_line(2).is_empty());
1173 assert!(!sm.mappings_for_line(0).is_empty());
1174 assert!(!sm.mappings_for_line(3).is_empty());
1175 }
1176
1177 #[test]
1178 fn decode_trailing_semicolons() {
1179 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;;"}"#;
1180 let sm = SourceMap::from_json(json).unwrap();
1181 assert_eq!(sm.line_count(), 3);
1182 assert!(!sm.mappings_for_line(0).is_empty());
1183 assert!(sm.mappings_for_line(1).is_empty());
1184 assert!(sm.mappings_for_line(2).is_empty());
1185 }
1186
1187 #[test]
1188 fn decode_leading_comma() {
1189 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":",AAAA"}"#;
1190 let sm = SourceMap::from_json(json).unwrap();
1191 assert_eq!(sm.mapping_count(), 1);
1192 let m = &sm.all_mappings()[0];
1193 assert_eq!(m.generated_line, 0);
1194 assert_eq!(m.generated_column, 0);
1195 }
1196
1197 #[test]
1198 fn decode_single_field_segments() {
1199 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,C"}"#;
1200 let sm = SourceMap::from_json(json).unwrap();
1201 assert_eq!(sm.mapping_count(), 2);
1202 for m in sm.all_mappings() {
1203 assert_eq!(m.source, NO_SOURCE);
1204 }
1205 assert_eq!(sm.all_mappings()[0].generated_column, 0);
1206 assert_eq!(sm.all_mappings()[1].generated_column, 1);
1207 assert!(sm.original_position_for(0, 0).is_none());
1208 assert!(sm.original_position_for(0, 1).is_none());
1209 }
1210
1211 #[test]
1212 fn decode_five_field_segments_with_names() {
1213 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0, 0], vec![10, 0, 0, 5, 1]]];
1214 let json = build_sourcemap_json(&["app.js"], &["foo", "bar"], &mappings_data);
1215 let sm = SourceMap::from_json(&json).unwrap();
1216 assert_eq!(sm.mapping_count(), 2);
1217 assert_eq!(sm.all_mappings()[0].name, 0);
1218 assert_eq!(sm.all_mappings()[1].name, 1);
1219
1220 let loc = sm.original_position_for(0, 0).unwrap();
1221 assert_eq!(loc.name, Some(0));
1222 assert_eq!(sm.name(0), "foo");
1223
1224 let loc = sm.original_position_for(0, 10).unwrap();
1225 assert_eq!(loc.name, Some(1));
1226 assert_eq!(sm.name(1), "bar");
1227 }
1228
1229 #[test]
1230 fn decode_large_vlq_values() {
1231 let mappings_data = vec![vec![vec![500_i64, 0, 1000, 2000]]];
1232 let json = build_sourcemap_json(&["big.js"], &[], &mappings_data);
1233 let sm = SourceMap::from_json(&json).unwrap();
1234 assert_eq!(sm.mapping_count(), 1);
1235 let m = &sm.all_mappings()[0];
1236 assert_eq!(m.generated_column, 500);
1237 assert_eq!(m.original_line, 1000);
1238 assert_eq!(m.original_column, 2000);
1239
1240 let loc = sm.original_position_for(0, 500).unwrap();
1241 assert_eq!(loc.line, 1000);
1242 assert_eq!(loc.column, 2000);
1243 }
1244
1245 #[test]
1246 fn decode_only_semicolons() {
1247 let json = r#"{"version":3,"sources":[],"names":[],"mappings":";;;"}"#;
1248 let sm = SourceMap::from_json(json).unwrap();
1249 assert_eq!(sm.line_count(), 4);
1250 assert_eq!(sm.mapping_count(), 0);
1251 for line in 0..4 {
1252 assert!(sm.mappings_for_line(line).is_empty());
1253 }
1254 }
1255
1256 #[test]
1257 fn decode_mixed_single_and_four_field_segments() {
1258 let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
1259 let four_field_encoded = srcmap_codec::encode(&mappings_data);
1260 let combined_mappings = format!("A,{four_field_encoded}");
1261 let json = format!(
1262 r#"{{"version":3,"sources":["x.js"],"names":[],"mappings":"{combined_mappings}"}}"#,
1263 );
1264 let sm = SourceMap::from_json(&json).unwrap();
1265 assert_eq!(sm.mapping_count(), 2);
1266 assert_eq!(sm.all_mappings()[0].source, NO_SOURCE);
1267 assert_eq!(sm.all_mappings()[1].source, 0);
1268 }
1269
1270 #[test]
1273 fn parse_missing_optional_fields() {
1274 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1275 let sm = SourceMap::from_json(json).unwrap();
1276 assert!(sm.file.is_none());
1277 assert!(sm.source_root.is_none());
1278 assert!(sm.sources_content.is_empty());
1279 assert!(sm.ignore_list.is_empty());
1280 }
1281
1282 #[test]
1283 fn parse_with_file_field() {
1284 let json =
1285 r#"{"version":3,"file":"output.js","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1286 let sm = SourceMap::from_json(json).unwrap();
1287 assert_eq!(sm.file.as_deref(), Some("output.js"));
1288 }
1289
1290 #[test]
1291 fn parse_null_entries_in_sources() {
1292 let json = r#"{"version":3,"sources":["a.js",null,"c.js"],"names":[],"mappings":"AAAA"}"#;
1293 let sm = SourceMap::from_json(json).unwrap();
1294 assert_eq!(sm.sources.len(), 3);
1295 assert_eq!(sm.sources[0], "a.js");
1296 assert_eq!(sm.sources[1], "");
1297 assert_eq!(sm.sources[2], "c.js");
1298 }
1299
1300 #[test]
1301 fn parse_null_entries_in_sources_with_source_root() {
1302 let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js",null],"names":[],"mappings":"AAAA"}"#;
1303 let sm = SourceMap::from_json(json).unwrap();
1304 assert_eq!(sm.sources[0], "lib/a.js");
1305 assert_eq!(sm.sources[1], "");
1306 }
1307
1308 #[test]
1309 fn parse_empty_names_array() {
1310 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1311 let sm = SourceMap::from_json(json).unwrap();
1312 assert!(sm.names.is_empty());
1313 }
1314
1315 #[test]
1316 fn parse_invalid_json() {
1317 let result = SourceMap::from_json("not valid json");
1318 assert!(result.is_err());
1319 assert!(matches!(result.unwrap_err(), ParseError::Json(_)));
1320 }
1321
1322 #[test]
1323 fn parse_json_missing_version() {
1324 let result = SourceMap::from_json(r#"{"sources":[],"names":[],"mappings":""}"#);
1325 assert!(result.is_err());
1326 }
1327
1328 #[test]
1329 fn parse_multiple_sources_overlapping_original_positions() {
1330 let mappings_data = vec![vec![vec![0_i64, 0, 5, 10], vec![10, 1, 5, 10]]];
1331 let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1332 let sm = SourceMap::from_json(&json).unwrap();
1333
1334 let loc0 = sm.original_position_for(0, 0).unwrap();
1335 assert_eq!(loc0.source, 0);
1336 assert_eq!(sm.source(loc0.source), "a.js");
1337
1338 let loc1 = sm.original_position_for(0, 10).unwrap();
1339 assert_eq!(loc1.source, 1);
1340 assert_eq!(sm.source(loc1.source), "b.js");
1341
1342 assert_eq!(loc0.line, loc1.line);
1343 assert_eq!(loc0.column, loc1.column);
1344 }
1345
1346 #[test]
1347 fn parse_sources_content_with_null_entries() {
1348 let json = r#"{"version":3,"sources":["a.js","b.js"],"sourcesContent":["content a",null],"names":[],"mappings":"AAAA"}"#;
1349 let sm = SourceMap::from_json(json).unwrap();
1350 assert_eq!(sm.sources_content.len(), 2);
1351 assert_eq!(sm.sources_content[0], Some("content a".to_string()));
1352 assert_eq!(sm.sources_content[1], None);
1353 }
1354
1355 #[test]
1356 fn parse_empty_sources_and_names() {
1357 let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1358 let sm = SourceMap::from_json(json).unwrap();
1359 assert!(sm.sources.is_empty());
1360 assert!(sm.names.is_empty());
1361 assert_eq!(sm.mapping_count(), 0);
1362 }
1363
1364 #[test]
1367 fn lookup_exact_match() {
1368 let mappings_data = vec![vec![
1369 vec![0_i64, 0, 10, 20],
1370 vec![5, 0, 10, 25],
1371 vec![15, 0, 11, 0],
1372 ]];
1373 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1374 let sm = SourceMap::from_json(&json).unwrap();
1375
1376 let loc = sm.original_position_for(0, 5).unwrap();
1377 assert_eq!(loc.line, 10);
1378 assert_eq!(loc.column, 25);
1379 }
1380
1381 #[test]
1382 fn lookup_before_first_segment() {
1383 let mappings_data = vec![vec![vec![5_i64, 0, 0, 0]]];
1384 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1385 let sm = SourceMap::from_json(&json).unwrap();
1386
1387 assert!(sm.original_position_for(0, 0).is_none());
1388 assert!(sm.original_position_for(0, 4).is_none());
1389 }
1390
1391 #[test]
1392 fn lookup_between_segments() {
1393 let mappings_data = vec![vec![
1394 vec![0_i64, 0, 1, 0],
1395 vec![10, 0, 2, 0],
1396 vec![20, 0, 3, 0],
1397 ]];
1398 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1399 let sm = SourceMap::from_json(&json).unwrap();
1400
1401 let loc = sm.original_position_for(0, 7).unwrap();
1402 assert_eq!(loc.line, 1);
1403 assert_eq!(loc.column, 0);
1404
1405 let loc = sm.original_position_for(0, 15).unwrap();
1406 assert_eq!(loc.line, 2);
1407 assert_eq!(loc.column, 0);
1408 }
1409
1410 #[test]
1411 fn lookup_after_last_segment() {
1412 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 1, 5]]];
1413 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1414 let sm = SourceMap::from_json(&json).unwrap();
1415
1416 let loc = sm.original_position_for(0, 100).unwrap();
1417 assert_eq!(loc.line, 1);
1418 assert_eq!(loc.column, 5);
1419 }
1420
1421 #[test]
1422 fn lookup_empty_lines_no_mappings() {
1423 let mappings_data = vec![
1424 vec![vec![0_i64, 0, 0, 0]],
1425 vec![],
1426 vec![vec![0_i64, 0, 2, 0]],
1427 ];
1428 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1429 let sm = SourceMap::from_json(&json).unwrap();
1430
1431 assert!(sm.original_position_for(1, 0).is_none());
1432 assert!(sm.original_position_for(1, 10).is_none());
1433 assert!(sm.original_position_for(0, 0).is_some());
1434 assert!(sm.original_position_for(2, 0).is_some());
1435 }
1436
1437 #[test]
1438 fn lookup_line_with_single_mapping() {
1439 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1440 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1441 let sm = SourceMap::from_json(&json).unwrap();
1442
1443 let loc = sm.original_position_for(0, 0).unwrap();
1444 assert_eq!(loc.line, 0);
1445 assert_eq!(loc.column, 0);
1446
1447 let loc = sm.original_position_for(0, 50).unwrap();
1448 assert_eq!(loc.line, 0);
1449 assert_eq!(loc.column, 0);
1450 }
1451
1452 #[test]
1453 fn lookup_column_0_vs_column_nonzero() {
1454 let mappings_data = vec![vec![vec![0_i64, 0, 10, 0], vec![8, 0, 20, 5]]];
1455 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1456 let sm = SourceMap::from_json(&json).unwrap();
1457
1458 let loc0 = sm.original_position_for(0, 0).unwrap();
1459 assert_eq!(loc0.line, 10);
1460 assert_eq!(loc0.column, 0);
1461
1462 let loc8 = sm.original_position_for(0, 8).unwrap();
1463 assert_eq!(loc8.line, 20);
1464 assert_eq!(loc8.column, 5);
1465
1466 let loc4 = sm.original_position_for(0, 4).unwrap();
1467 assert_eq!(loc4.line, 10);
1468 }
1469
1470 #[test]
1471 fn lookup_beyond_last_line() {
1472 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1473 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1474 let sm = SourceMap::from_json(&json).unwrap();
1475
1476 assert!(sm.original_position_for(1, 0).is_none());
1477 assert!(sm.original_position_for(100, 0).is_none());
1478 }
1479
1480 #[test]
1481 fn lookup_single_field_returns_none() {
1482 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A"}"#;
1483 let sm = SourceMap::from_json(json).unwrap();
1484 assert_eq!(sm.mapping_count(), 1);
1485 assert!(sm.original_position_for(0, 0).is_none());
1486 }
1487
1488 #[test]
1491 fn reverse_lookup_exact_match() {
1492 let mappings_data = vec![
1493 vec![vec![0_i64, 0, 0, 0]],
1494 vec![vec![4, 0, 1, 0], vec![10, 0, 1, 8]],
1495 vec![vec![0, 0, 2, 0]],
1496 ];
1497 let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1498 let sm = SourceMap::from_json(&json).unwrap();
1499
1500 let loc = sm.generated_position_for("main.js", 1, 8).unwrap();
1501 assert_eq!(loc.line, 1);
1502 assert_eq!(loc.column, 10);
1503 }
1504
1505 #[test]
1506 fn reverse_lookup_no_match() {
1507 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10]]];
1508 let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1509 let sm = SourceMap::from_json(&json).unwrap();
1510
1511 assert!(sm.generated_position_for("main.js", 99, 0).is_none());
1512 }
1513
1514 #[test]
1515 fn reverse_lookup_unknown_source() {
1516 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1517 let json = build_sourcemap_json(&["main.js"], &[], &mappings_data);
1518 let sm = SourceMap::from_json(&json).unwrap();
1519
1520 assert!(sm.generated_position_for("unknown.js", 0, 0).is_none());
1521 }
1522
1523 #[test]
1524 fn reverse_lookup_multiple_mappings_same_original() {
1525 let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]], vec![vec![20, 0, 5, 10]]];
1526 let json = build_sourcemap_json(&["src.js"], &[], &mappings_data);
1527 let sm = SourceMap::from_json(&json).unwrap();
1528
1529 let loc = sm.generated_position_for("src.js", 5, 10);
1530 assert!(loc.is_some());
1531 let loc = loc.unwrap();
1532 assert!(
1533 (loc.line == 0 && loc.column == 0) || (loc.line == 1 && loc.column == 20),
1534 "Expected (0,0) or (1,20), got ({},{})",
1535 loc.line,
1536 loc.column
1537 );
1538 }
1539
1540 #[test]
1541 fn reverse_lookup_with_multiple_sources() {
1542 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0], vec![10, 1, 0, 0]]];
1543 let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1544 let sm = SourceMap::from_json(&json).unwrap();
1545
1546 let loc_a = sm.generated_position_for("a.js", 0, 0).unwrap();
1547 assert_eq!(loc_a.line, 0);
1548 assert_eq!(loc_a.column, 0);
1549
1550 let loc_b = sm.generated_position_for("b.js", 0, 0).unwrap();
1551 assert_eq!(loc_b.line, 0);
1552 assert_eq!(loc_b.column, 10);
1553 }
1554
1555 #[test]
1556 fn reverse_lookup_skips_single_field_segments() {
1557 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,KAAAA"}"#;
1558 let sm = SourceMap::from_json(&json).unwrap();
1559
1560 let loc = sm.generated_position_for("a.js", 0, 0).unwrap();
1561 assert_eq!(loc.line, 0);
1562 assert_eq!(loc.column, 5);
1563 }
1564
1565 #[test]
1566 fn reverse_lookup_finds_each_original_line() {
1567 let mappings_data = vec![
1568 vec![vec![0_i64, 0, 0, 0]],
1569 vec![vec![0, 0, 1, 0]],
1570 vec![vec![0, 0, 2, 0]],
1571 vec![vec![0, 0, 3, 0]],
1572 ];
1573 let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1574 let sm = SourceMap::from_json(&json).unwrap();
1575
1576 for orig_line in 0..4 {
1577 let loc = sm.generated_position_for("x.js", orig_line, 0).unwrap();
1578 assert_eq!(
1579 loc.line, orig_line,
1580 "reverse lookup for orig line {orig_line}"
1581 );
1582 assert_eq!(loc.column, 0);
1583 }
1584 }
1585
1586 #[test]
1589 fn parse_with_ignore_list_multiple() {
1590 let json = r#"{"version":3,"sources":["app.js","node_modules/lib.js","vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[1,2]}"#;
1591 let sm = SourceMap::from_json(json).unwrap();
1592 assert_eq!(sm.ignore_list, vec![1, 2]);
1593 }
1594
1595 #[test]
1596 fn parse_with_empty_ignore_list() {
1597 let json =
1598 r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA","ignoreList":[]}"#;
1599 let sm = SourceMap::from_json(json).unwrap();
1600 assert!(sm.ignore_list.is_empty());
1601 }
1602
1603 #[test]
1604 fn parse_without_ignore_list_field() {
1605 let json = r#"{"version":3,"sources":["app.js"],"names":[],"mappings":"AAAA"}"#;
1606 let sm = SourceMap::from_json(json).unwrap();
1607 assert!(sm.ignore_list.is_empty());
1608 }
1609
1610 #[test]
1613 fn source_index_lookup() {
1614 let json = r#"{"version":3,"sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA"}"#;
1615 let sm = SourceMap::from_json(json).unwrap();
1616 assert_eq!(sm.source_index("a.js"), Some(0));
1617 assert_eq!(sm.source_index("b.js"), Some(1));
1618 assert_eq!(sm.source_index("c.js"), Some(2));
1619 assert_eq!(sm.source_index("d.js"), None);
1620 }
1621
1622 #[test]
1623 fn all_mappings_returns_complete_list() {
1624 let mappings_data = vec![
1625 vec![vec![0_i64, 0, 0, 0], vec![5, 0, 0, 5]],
1626 vec![vec![0, 0, 1, 0]],
1627 ];
1628 let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1629 let sm = SourceMap::from_json(&json).unwrap();
1630 assert_eq!(sm.all_mappings().len(), 3);
1631 assert_eq!(sm.mapping_count(), 3);
1632 }
1633
1634 #[test]
1635 fn line_count_matches_decoded_lines() {
1636 let mappings_data = vec![
1637 vec![vec![0_i64, 0, 0, 0]],
1638 vec![],
1639 vec![vec![0_i64, 0, 2, 0]],
1640 vec![],
1641 vec![],
1642 ];
1643 let json = build_sourcemap_json(&["x.js"], &[], &mappings_data);
1644 let sm = SourceMap::from_json(&json).unwrap();
1645 assert_eq!(sm.line_count(), 5);
1646 }
1647
1648 #[test]
1649 fn parse_error_display() {
1650 let err = ParseError::InvalidVersion(5);
1651 assert_eq!(format!("{err}"), "unsupported source map version: 5");
1652
1653 let json_err = SourceMap::from_json("{}").unwrap_err();
1654 let display = format!("{json_err}");
1655 assert!(display.contains("JSON parse error") || display.contains("missing field"));
1656 }
1657
1658 #[test]
1659 fn original_position_name_none_for_four_field() {
1660 let mappings_data = vec![vec![vec![0_i64, 0, 5, 10]]];
1661 let json = build_sourcemap_json(&["a.js"], &["unused_name"], &mappings_data);
1662 let sm = SourceMap::from_json(&json).unwrap();
1663
1664 let loc = sm.original_position_for(0, 0).unwrap();
1665 assert!(loc.name.is_none());
1666 }
1667
1668 #[test]
1669 fn forward_and_reverse_roundtrip_comprehensive() {
1670 let mappings_data = vec![
1671 vec![vec![0_i64, 0, 0, 0], vec![10, 0, 0, 10], vec![20, 1, 5, 0]],
1672 vec![vec![0, 0, 1, 0], vec![5, 1, 6, 3]],
1673 vec![vec![0, 0, 2, 0]],
1674 ];
1675 let json = build_sourcemap_json(&["a.js", "b.js"], &[], &mappings_data);
1676 let sm = SourceMap::from_json(&json).unwrap();
1677
1678 for m in sm.all_mappings() {
1679 if m.source == NO_SOURCE {
1680 continue;
1681 }
1682 let source_name = sm.source(m.source);
1683
1684 let orig = sm
1685 .original_position_for(m.generated_line, m.generated_column)
1686 .unwrap();
1687 assert_eq!(orig.source, m.source);
1688 assert_eq!(orig.line, m.original_line);
1689 assert_eq!(orig.column, m.original_column);
1690
1691 let gen_loc = sm
1692 .generated_position_for(source_name, m.original_line, m.original_column)
1693 .unwrap();
1694 assert_eq!(gen_loc.line, m.generated_line);
1695 assert_eq!(gen_loc.column, m.generated_column);
1696 }
1697 }
1698
1699 #[test]
1704 fn source_root_with_multiple_sources() {
1705 let json = r#"{"version":3,"sourceRoot":"lib/","sources":["a.js","b.js","c.js"],"names":[],"mappings":"AAAA,KACA,KACA"}"#;
1706 let sm = SourceMap::from_json(json).unwrap();
1707 assert_eq!(sm.sources, vec!["lib/a.js", "lib/b.js", "lib/c.js"]);
1708 }
1709
1710 #[test]
1711 fn source_root_empty_string() {
1712 let json = r#"{"version":3,"sourceRoot":"","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1713 let sm = SourceMap::from_json(json).unwrap();
1714 assert_eq!(sm.sources, vec!["a.js"]);
1715 }
1716
1717 #[test]
1718 fn source_root_preserved_in_to_json() {
1719 let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1720 let sm = SourceMap::from_json(json).unwrap();
1721 let output = sm.to_json();
1722 assert!(output.contains(r#""sourceRoot":"src/""#));
1723 }
1724
1725 #[test]
1726 fn source_root_reverse_lookup_uses_prefixed_name() {
1727 let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1728 let sm = SourceMap::from_json(json).unwrap();
1729 assert!(sm.generated_position_for("src/a.js", 0, 0).is_some());
1731 assert!(sm.generated_position_for("a.js", 0, 0).is_none());
1732 }
1733
1734 #[test]
1735 fn source_root_with_trailing_slash() {
1736 let json = r#"{"version":3,"sourceRoot":"src/","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1737 let sm = SourceMap::from_json(json).unwrap();
1738 assert_eq!(sm.sources[0], "src/a.js");
1739 }
1740
1741 #[test]
1742 fn source_root_without_trailing_slash() {
1743 let json = r#"{"version":3,"sourceRoot":"src","sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1744 let sm = SourceMap::from_json(json).unwrap();
1745 assert_eq!(sm.sources[0], "srca.js"); }
1747
1748 #[test]
1751 fn parse_empty_json_object() {
1752 let result = SourceMap::from_json("{}");
1754 assert!(result.is_err());
1755 }
1756
1757 #[test]
1758 fn parse_version_0() {
1759 let json = r#"{"version":0,"sources":[],"names":[],"mappings":""}"#;
1760 assert!(matches!(
1761 SourceMap::from_json(json).unwrap_err(),
1762 ParseError::InvalidVersion(0)
1763 ));
1764 }
1765
1766 #[test]
1767 fn parse_version_4() {
1768 let json = r#"{"version":4,"sources":[],"names":[],"mappings":""}"#;
1769 assert!(matches!(
1770 SourceMap::from_json(json).unwrap_err(),
1771 ParseError::InvalidVersion(4)
1772 ));
1773 }
1774
1775 #[test]
1776 fn parse_extra_unknown_fields_ignored() {
1777 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA","x_custom_field":true,"x_debug":{"foo":"bar"}}"#;
1778 let sm = SourceMap::from_json(json).unwrap();
1779 assert_eq!(sm.mapping_count(), 1);
1780 }
1781
1782 #[test]
1783 fn parse_vlq_error_propagated() {
1784 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AA!A"}"#;
1786 let result = SourceMap::from_json(json);
1787 assert!(result.is_err());
1788 assert!(matches!(result.unwrap_err(), ParseError::Vlq(_)));
1789 }
1790
1791 #[test]
1792 fn parse_truncated_vlq_error() {
1793 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"g"}"#;
1795 let result = SourceMap::from_json(json);
1796 assert!(result.is_err());
1797 }
1798
1799 #[test]
1802 fn to_json_produces_valid_json() {
1803 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]}"#;
1804 let sm = SourceMap::from_json(json).unwrap();
1805 let output = sm.to_json();
1806 let _: serde_json::Value = serde_json::from_str(&output).unwrap();
1808 }
1809
1810 #[test]
1811 fn to_json_escapes_special_chars() {
1812 let json = r#"{"version":3,"sources":["path/with\"quotes.js"],"sourcesContent":["line1\nline2\ttab\\backslash"],"names":[],"mappings":"AAAA"}"#;
1813 let sm = SourceMap::from_json(json).unwrap();
1814 let output = sm.to_json();
1815 let _: serde_json::Value = serde_json::from_str(&output).unwrap();
1816 let sm2 = SourceMap::from_json(&output).unwrap();
1817 assert_eq!(sm2.sources_content[0].as_deref(), Some("line1\nline2\ttab\\backslash"));
1818 }
1819
1820 #[test]
1821 fn to_json_empty_map() {
1822 let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1823 let sm = SourceMap::from_json(json).unwrap();
1824 let output = sm.to_json();
1825 let sm2 = SourceMap::from_json(&output).unwrap();
1826 assert_eq!(sm2.mapping_count(), 0);
1827 assert!(sm2.sources.is_empty());
1828 }
1829
1830 #[test]
1831 fn to_json_roundtrip_with_names() {
1832 let mappings_data = vec![vec![
1833 vec![0_i64, 0, 0, 0, 0],
1834 vec![10, 0, 0, 10, 1],
1835 vec![20, 0, 1, 0, 2],
1836 ]];
1837 let json = build_sourcemap_json(&["src.js"], &["foo", "bar", "baz"], &mappings_data);
1838 let sm = SourceMap::from_json(&json).unwrap();
1839 let output = sm.to_json();
1840 let sm2 = SourceMap::from_json(&output).unwrap();
1841
1842 for m in sm2.all_mappings() {
1843 if m.source != NO_SOURCE && m.name != NO_NAME {
1844 let loc = sm2.original_position_for(m.generated_line, m.generated_column).unwrap();
1845 assert!(loc.name.is_some());
1846 }
1847 }
1848 }
1849
1850 #[test]
1853 fn indexed_source_map_column_offset() {
1854 let json = r#"{
1855 "version": 3,
1856 "sections": [
1857 {
1858 "offset": {"line": 0, "column": 10},
1859 "map": {
1860 "version": 3,
1861 "sources": ["a.js"],
1862 "names": [],
1863 "mappings": "AAAA"
1864 }
1865 }
1866 ]
1867 }"#;
1868 let sm = SourceMap::from_json(json).unwrap();
1869 let loc = sm.original_position_for(0, 10).unwrap();
1871 assert_eq!(loc.line, 0);
1872 assert_eq!(loc.column, 0);
1873 assert!(sm.original_position_for(0, 0).is_none());
1875 }
1876
1877 #[test]
1878 fn indexed_source_map_column_offset_only_first_line() {
1879 let json = r#"{
1881 "version": 3,
1882 "sections": [
1883 {
1884 "offset": {"line": 0, "column": 20},
1885 "map": {
1886 "version": 3,
1887 "sources": ["a.js"],
1888 "names": [],
1889 "mappings": "AAAA;AAAA"
1890 }
1891 }
1892 ]
1893 }"#;
1894 let sm = SourceMap::from_json(json).unwrap();
1895 let loc = sm.original_position_for(0, 20).unwrap();
1897 assert_eq!(loc.column, 0);
1898 let loc = sm.original_position_for(1, 0).unwrap();
1900 assert_eq!(loc.column, 0);
1901 }
1902
1903 #[test]
1904 fn indexed_source_map_empty_section() {
1905 let json = r#"{
1906 "version": 3,
1907 "sections": [
1908 {
1909 "offset": {"line": 0, "column": 0},
1910 "map": {
1911 "version": 3,
1912 "sources": [],
1913 "names": [],
1914 "mappings": ""
1915 }
1916 },
1917 {
1918 "offset": {"line": 5, "column": 0},
1919 "map": {
1920 "version": 3,
1921 "sources": ["b.js"],
1922 "names": [],
1923 "mappings": "AAAA"
1924 }
1925 }
1926 ]
1927 }"#;
1928 let sm = SourceMap::from_json(json).unwrap();
1929 assert_eq!(sm.sources.len(), 1);
1930 let loc = sm.original_position_for(5, 0).unwrap();
1931 assert_eq!(sm.source(loc.source), "b.js");
1932 }
1933
1934 #[test]
1935 fn indexed_source_map_with_sources_content() {
1936 let json = r#"{
1937 "version": 3,
1938 "sections": [
1939 {
1940 "offset": {"line": 0, "column": 0},
1941 "map": {
1942 "version": 3,
1943 "sources": ["a.js"],
1944 "sourcesContent": ["var a = 1;"],
1945 "names": [],
1946 "mappings": "AAAA"
1947 }
1948 },
1949 {
1950 "offset": {"line": 5, "column": 0},
1951 "map": {
1952 "version": 3,
1953 "sources": ["b.js"],
1954 "sourcesContent": ["var b = 2;"],
1955 "names": [],
1956 "mappings": "AAAA"
1957 }
1958 }
1959 ]
1960 }"#;
1961 let sm = SourceMap::from_json(json).unwrap();
1962 assert_eq!(sm.sources_content.len(), 2);
1963 assert_eq!(sm.sources_content[0], Some("var a = 1;".to_string()));
1964 assert_eq!(sm.sources_content[1], Some("var b = 2;".to_string()));
1965 }
1966
1967 #[test]
1968 fn indexed_source_map_with_ignore_list() {
1969 let json = r#"{
1970 "version": 3,
1971 "sections": [
1972 {
1973 "offset": {"line": 0, "column": 0},
1974 "map": {
1975 "version": 3,
1976 "sources": ["app.js", "vendor.js"],
1977 "names": [],
1978 "mappings": "AAAA",
1979 "ignoreList": [1]
1980 }
1981 }
1982 ]
1983 }"#;
1984 let sm = SourceMap::from_json(json).unwrap();
1985 assert!(!sm.ignore_list.is_empty());
1986 }
1987
1988 #[test]
1991 fn lookup_max_column_on_line() {
1992 let mappings_data = vec![vec![vec![0_i64, 0, 0, 0]]];
1993 let json = build_sourcemap_json(&["a.js"], &[], &mappings_data);
1994 let sm = SourceMap::from_json(&json).unwrap();
1995 let loc = sm.original_position_for(0, u32::MAX - 1).unwrap();
1997 assert_eq!(loc.line, 0);
1998 assert_eq!(loc.column, 0);
1999 }
2000
2001 #[test]
2002 fn mappings_for_line_beyond_end() {
2003 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
2004 let sm = SourceMap::from_json(json).unwrap();
2005 assert!(sm.mappings_for_line(u32::MAX).is_empty());
2006 }
2007
2008 #[test]
2009 fn source_with_unicode_path() {
2010 let json = r#"{"version":3,"sources":["src/日本語.ts"],"names":["変数"],"mappings":"AAAAA"}"#;
2011 let sm = SourceMap::from_json(json).unwrap();
2012 assert_eq!(sm.sources[0], "src/日本語.ts");
2013 assert_eq!(sm.names[0], "変数");
2014 let loc = sm.original_position_for(0, 0).unwrap();
2015 assert_eq!(sm.source(loc.source), "src/日本語.ts");
2016 assert_eq!(sm.name(loc.name.unwrap()), "変数");
2017 }
2018
2019 #[test]
2020 fn to_json_roundtrip_unicode_sources() {
2021 let json = r#"{"version":3,"sources":["src/日本語.ts"],"sourcesContent":["const 変数 = 1;"],"names":["変数"],"mappings":"AAAAA"}"#;
2022 let sm = SourceMap::from_json(json).unwrap();
2023 let output = sm.to_json();
2024 let _: serde_json::Value = serde_json::from_str(&output).unwrap();
2025 let sm2 = SourceMap::from_json(&output).unwrap();
2026 assert_eq!(sm2.sources[0], "src/日本語.ts");
2027 assert_eq!(sm2.sources_content[0], Some("const 変数 = 1;".to_string()));
2028 }
2029
2030 #[test]
2031 fn many_sources_lookup() {
2032 let sources: Vec<String> = (0..100).map(|i| format!("src/file{i}.js")).collect();
2034 let source_strs: Vec<&str> = sources.iter().map(|s| s.as_str()).collect();
2035 let mappings_data = vec![sources.iter().enumerate().map(|(i, _)| {
2036 vec![(i * 10) as i64, i as i64, 0, 0]
2037 }).collect::<Vec<_>>()];
2038 let json = build_sourcemap_json(&source_strs, &[], &mappings_data);
2039 let sm = SourceMap::from_json(&json).unwrap();
2040
2041 for (i, src) in sources.iter().enumerate() {
2042 assert_eq!(sm.source_index(src), Some(i as u32));
2043 }
2044 }
2045
2046 #[test]
2047 fn clone_sourcemap() {
2048 let json = r#"{"version":3,"sources":["a.js"],"names":["x"],"mappings":"AAAAA"}"#;
2049 let sm = SourceMap::from_json(json).unwrap();
2050 let sm2 = sm.clone();
2051 assert_eq!(sm2.sources, sm.sources);
2052 assert_eq!(sm2.mapping_count(), sm.mapping_count());
2053 let loc = sm2.original_position_for(0, 0).unwrap();
2054 assert_eq!(sm2.source(loc.source), "a.js");
2055 }
2056
2057 fn generate_test_sourcemap(lines: usize, segs_per_line: usize, num_sources: usize) -> String {
2059 let sources: Vec<String> = (0..num_sources)
2060 .map(|i| format!("src/file{i}.js"))
2061 .collect();
2062 let names: Vec<String> = (0..20).map(|i| format!("var{i}")).collect();
2063
2064 let mut mappings_parts = Vec::with_capacity(lines);
2065 let mut gen_col;
2066 let mut src: i64 = 0;
2067 let mut src_line: i64 = 0;
2068 let mut src_col: i64 = 0;
2069 let mut name: i64 = 0;
2070
2071 for _ in 0..lines {
2072 gen_col = 0i64;
2073 let mut line_parts = Vec::with_capacity(segs_per_line);
2074
2075 for s in 0..segs_per_line {
2076 let gc_delta = 2 + (s as i64 * 3) % 20;
2077 gen_col += gc_delta;
2078
2079 let src_delta = if s % 7 == 0 { 1 } else { 0 };
2080 src = (src + src_delta) % num_sources as i64;
2081
2082 src_line += 1;
2083 let sc_delta = (s as i64 * 5 + 1) % 30;
2084 src_col = sc_delta;
2085
2086 let has_name = s % 4 == 0;
2087 if has_name {
2088 name = (name + 1) % names.len() as i64;
2089 }
2090
2091 let segment = if has_name {
2093 vec![gen_col, src, src_line, src_col, name]
2094 } else {
2095 vec![gen_col, src, src_line, src_col]
2096 };
2097
2098 line_parts.push(segment);
2099 }
2100
2101 mappings_parts.push(line_parts);
2102 }
2103
2104 let encoded = srcmap_codec::encode(&mappings_parts);
2105
2106 format!(
2107 r#"{{"version":3,"sources":[{}],"names":[{}],"mappings":"{}"}}"#,
2108 sources
2109 .iter()
2110 .map(|s| format!("\"{s}\""))
2111 .collect::<Vec<_>>()
2112 .join(","),
2113 names
2114 .iter()
2115 .map(|n| format!("\"{n}\""))
2116 .collect::<Vec<_>>()
2117 .join(","),
2118 encoded,
2119 )
2120 }
2121}