1use std::fmt;
33use std::ops::{Deref, DerefMut};
34
35use srcmap_codec::{DecodeError, vlq_decode};
36use srcmap_sourcemap::SourceMap;
37
38#[derive(Debug)]
42pub enum HermesError {
43 Parse(srcmap_sourcemap::ParseError),
45 Vlq(DecodeError),
47 InvalidFunctionMap(String),
49}
50
51impl fmt::Display for HermesError {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Parse(e) => write!(f, "source map parse error: {e}"),
55 Self::Vlq(e) => write!(f, "VLQ decode error in function map: {e}"),
56 Self::InvalidFunctionMap(msg) => write!(f, "invalid function map: {msg}"),
57 }
58 }
59}
60
61impl std::error::Error for HermesError {
62 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
63 match self {
64 Self::Parse(e) => Some(e),
65 Self::Vlq(e) => Some(e),
66 Self::InvalidFunctionMap(_) => None,
67 }
68 }
69}
70
71impl From<srcmap_sourcemap::ParseError> for HermesError {
72 fn from(e: srcmap_sourcemap::ParseError) -> Self {
73 Self::Parse(e)
74 }
75}
76
77impl From<DecodeError> for HermesError {
78 fn from(e: DecodeError) -> Self {
79 Self::Vlq(e)
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct HermesScopeOffset {
89 pub line: u32,
91 pub column: u32,
93 pub name_index: u32,
95}
96
97#[derive(Debug, Clone)]
100pub struct HermesFunctionMap {
101 pub names: Vec<String>,
103 pub mappings: Vec<HermesScopeOffset>,
105}
106
107pub struct SourceMapHermes {
110 sm: SourceMap,
111 function_maps: Vec<Option<HermesFunctionMap>>,
112 x_facebook_offsets: Option<Vec<Option<u32>>>,
114 x_metro_module_paths: Option<Vec<String>>,
116}
117
118impl Deref for SourceMapHermes {
119 type Target = SourceMap;
120
121 #[inline]
122 fn deref(&self) -> &SourceMap {
123 &self.sm
124 }
125}
126
127impl DerefMut for SourceMapHermes {
128 #[inline]
129 fn deref_mut(&mut self) -> &mut SourceMap {
130 &mut self.sm
131 }
132}
133
134fn decode_function_mappings(mappings_str: &str) -> Result<Vec<HermesScopeOffset>, HermesError> {
145 let input = mappings_str.as_bytes();
146 if input.is_empty() {
147 return Ok(Vec::new());
148 }
149
150 let mut result = Vec::new();
151 let mut pos = 0;
152
153 let mut prev_column: i64 = 0;
155 let mut prev_name_index: i64 = 0;
156 let mut prev_line: i64 = 0;
157
158 while pos < input.len() {
159 if input[pos] == b',' {
161 pos += 1;
162 continue;
163 }
164
165 let (col_delta, consumed) = vlq_decode(input, pos)?;
167 pos += consumed;
168 prev_column += col_delta;
169
170 if pos >= input.len() || input[pos] == b',' {
172 return Err(HermesError::InvalidFunctionMap(
173 "expected 3 values per segment, got 1".to_string(),
174 ));
175 }
176 let (name_delta, consumed) = vlq_decode(input, pos)?;
177 pos += consumed;
178 prev_name_index += name_delta;
179
180 if pos >= input.len() || input[pos] == b',' {
182 return Err(HermesError::InvalidFunctionMap(
183 "expected 3 values per segment, got 2".to_string(),
184 ));
185 }
186 let (line_delta, consumed) = vlq_decode(input, pos)?;
187 pos += consumed;
188 prev_line += line_delta;
189
190 if prev_line < 0 || prev_column < 0 || prev_name_index < 0 {
191 return Err(HermesError::InvalidFunctionMap(
192 "negative accumulated delta value".to_string(),
193 ));
194 }
195
196 result.push(HermesScopeOffset {
197 line: prev_line as u32,
198 column: prev_column as u32,
199 name_index: prev_name_index as u32,
200 });
201 }
202
203 Ok(result)
204}
205
206fn parse_function_map(entry: &serde_json::Value) -> Result<HermesFunctionMap, HermesError> {
208 let names = entry
209 .get("names")
210 .and_then(|n| n.as_array())
211 .ok_or_else(|| HermesError::InvalidFunctionMap("missing 'names' array".to_string()))?
212 .iter()
213 .map(|v| {
214 v.as_str()
215 .ok_or_else(|| HermesError::InvalidFunctionMap("name is not a string".to_string()))
216 .map(|s| s.to_string())
217 })
218 .collect::<Result<Vec<_>, _>>()?;
219
220 let mappings_str = entry
221 .get("mappings")
222 .and_then(|m| m.as_str())
223 .ok_or_else(|| HermesError::InvalidFunctionMap("missing 'mappings' string".to_string()))?;
224
225 let mappings = decode_function_mappings(mappings_str)?;
226
227 Ok(HermesFunctionMap { names, mappings })
228}
229
230fn parse_facebook_sources(
244 value: &serde_json::Value,
245) -> Result<Vec<Option<HermesFunctionMap>>, HermesError> {
246 let sources_array = value.as_array().ok_or_else(|| {
247 HermesError::InvalidFunctionMap("x_facebook_sources is not an array".to_string())
248 })?;
249
250 let mut result = Vec::with_capacity(sources_array.len());
251
252 for entry in sources_array {
253 if entry.is_null() {
254 result.push(None);
255 continue;
256 }
257
258 let entries = entry.as_array().ok_or_else(|| {
259 HermesError::InvalidFunctionMap(
260 "x_facebook_sources entry is not an array or null".to_string(),
261 )
262 })?;
263
264 if entries.is_empty() {
265 result.push(None);
266 continue;
267 }
268
269 let function_map = parse_function_map(&entries[0])?;
271 result.push(Some(function_map));
272 }
273
274 Ok(result)
275}
276
277fn parse_facebook_offsets(value: &serde_json::Value) -> Option<Vec<Option<u32>>> {
279 let arr = value.as_array()?;
280 Some(arr.iter().map(|v| v.as_u64().map(|n| n as u32)).collect())
281}
282
283fn parse_metro_module_paths(value: &serde_json::Value) -> Option<Vec<String>> {
285 let arr = value.as_array()?;
286 Some(
287 arr.iter()
288 .map(|v| v.as_str().unwrap_or("").to_string())
289 .collect(),
290 )
291}
292
293impl SourceMapHermes {
296 pub fn from_json(json: &str) -> Result<Self, HermesError> {
302 let sm = SourceMap::from_json(json)?;
303
304 let function_maps = match sm.extensions.get("x_facebook_sources") {
305 Some(value) => parse_facebook_sources(value)?,
306 None => Vec::new(),
307 };
308
309 let x_facebook_offsets = sm
310 .extensions
311 .get("x_facebook_offsets")
312 .and_then(parse_facebook_offsets);
313
314 let x_metro_module_paths = sm
315 .extensions
316 .get("x_metro_module_paths")
317 .and_then(parse_metro_module_paths);
318
319 Ok(Self {
320 sm,
321 function_maps,
322 x_facebook_offsets,
323 x_metro_module_paths,
324 })
325 }
326
327 #[inline]
329 pub fn inner(&self) -> &SourceMap {
330 &self.sm
331 }
332
333 #[inline]
335 pub fn into_inner(self) -> SourceMap {
336 self.sm
337 }
338
339 #[inline]
341 pub fn get_function_map(&self, source_idx: u32) -> Option<&HermesFunctionMap> {
342 self.function_maps
343 .get(source_idx as usize)
344 .and_then(|fm| fm.as_ref())
345 }
346
347 pub fn get_scope_for_token(&self, line: u32, column: u32) -> Option<&str> {
354 let loc = self.sm.original_position_for(line, column)?;
355 let function_map = self.get_function_map(loc.source)?;
356
357 if function_map.mappings.is_empty() {
358 return None;
359 }
360
361 let idx = match function_map.mappings.binary_search_by(|offset| {
363 offset
364 .line
365 .cmp(&loc.line)
366 .then(offset.column.cmp(&loc.column))
367 }) {
368 Ok(i) => i,
369 Err(0) => return None,
370 Err(i) => i - 1,
371 };
372
373 let scope = &function_map.mappings[idx];
374 function_map
375 .names
376 .get(scope.name_index as usize)
377 .map(|n| n.as_str())
378 }
379
380 pub fn get_original_function_name(&self, line: u32, column: u32) -> Option<&str> {
386 let loc = self.sm.original_position_for(line, column)?;
387 let function_map = self.get_function_map(loc.source)?;
388
389 if function_map.mappings.is_empty() {
390 return None;
391 }
392
393 let idx = match function_map.mappings.binary_search_by(|offset| {
395 offset
396 .line
397 .cmp(&loc.line)
398 .then(offset.column.cmp(&loc.column))
399 }) {
400 Ok(i) => i,
401 Err(0) => return None,
402 Err(i) => i - 1,
403 };
404
405 let scope = &function_map.mappings[idx];
406 function_map
407 .names
408 .get(scope.name_index as usize)
409 .map(|n| n.as_str())
410 }
411
412 #[inline]
416 pub fn is_for_ram_bundle(&self) -> bool {
417 self.x_facebook_offsets.is_some()
418 }
419
420 #[inline]
422 pub fn x_facebook_offsets(&self) -> Option<&[Option<u32>]> {
423 self.x_facebook_offsets.as_deref()
424 }
425
426 #[inline]
428 pub fn x_metro_module_paths(&self) -> Option<&[String]> {
429 self.x_metro_module_paths.as_deref()
430 }
431
432 pub fn to_json(&self) -> String {
438 self.sm.to_json()
439 }
440}
441
442impl fmt::Debug for SourceMapHermes {
443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444 f.debug_struct("SourceMapHermes")
445 .field("sources", &self.sm.sources)
446 .field("function_maps_count", &self.function_maps.len())
447 .field("has_facebook_offsets", &self.x_facebook_offsets.is_some())
448 .field(
449 "has_metro_module_paths",
450 &self.x_metro_module_paths.is_some(),
451 )
452 .finish()
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 fn sample_hermes_json() -> &'static str {
461 r#"{
462 "version": 3,
463 "sources": ["input.js"],
464 "names": ["myFunc"],
465 "mappings": "AAAA;AACA",
466 "x_facebook_sources": [
467 [{"names": ["<global>", "foo", "bar"], "mappings": "AAA,ECA,GGC"}]
468 ]
469 }"#
470 }
471
472 #[test]
473 fn parse_hermes_sourcemap() {
474 let sm = SourceMapHermes::from_json(sample_hermes_json()).unwrap();
475 assert_eq!(sm.sources.len(), 1);
476 assert_eq!(sm.sources[0], "input.js");
477 assert!(sm.get_function_map(0).is_some());
478 assert!(sm.get_function_map(1).is_none());
479 }
480
481 #[test]
482 fn function_map_names() {
483 let sm = SourceMapHermes::from_json(sample_hermes_json()).unwrap();
484 let fm = sm.get_function_map(0).unwrap();
485 assert_eq!(fm.names, vec!["<global>", "foo", "bar"]);
486 }
487
488 #[test]
489 fn function_map_mappings_decoded() {
490 let sm = SourceMapHermes::from_json(sample_hermes_json()).unwrap();
491 let fm = sm.get_function_map(0).unwrap();
492
493 assert_eq!(
495 fm.mappings[0],
496 HermesScopeOffset {
497 line: 0,
498 column: 0,
499 name_index: 0
500 }
501 );
502
503 assert_eq!(
506 fm.mappings[1],
507 HermesScopeOffset {
508 line: 0,
509 column: 2,
510 name_index: 1
511 }
512 );
513
514 assert_eq!(
527 fm.mappings[2],
528 HermesScopeOffset {
529 line: 1,
530 column: 5,
531 name_index: 4
532 }
533 );
534 }
535
536 #[test]
537 fn scope_resolution() {
538 let json = r#"{
540 "version": 3,
541 "sources": ["a.js"],
542 "names": [],
543 "mappings": "AAAA;AACA;AACA;AACA;AACA",
544 "x_facebook_sources": [
545 [{"names": ["<global>", "foo", "bar"], "mappings": "AAA,ECA,AGC"}]
546 ]
547 }"#;
548 let sm = SourceMapHermes::from_json(json).unwrap();
555 let fm = sm.get_function_map(0).unwrap();
556
557 assert_eq!(fm.mappings[0].name_index, 0);
559 }
573
574 #[test]
575 fn scope_for_token_basic() {
576 let json = r#"{
579 "version": 3,
580 "sources": ["a.js"],
581 "names": [],
582 "mappings": "AAAA,CAAC",
583 "x_facebook_sources": [
584 [{"names": ["<global>", "hello"], "mappings": "AAA,CCA"}]
585 ]
586 }"#;
587 let sm = SourceMapHermes::from_json(json).unwrap();
591
592 let scope = sm.get_scope_for_token(0, 0);
594 assert_eq!(scope, Some("<global>"));
595
596 let scope = sm.get_scope_for_token(0, 1);
598 assert_eq!(scope, Some("hello"));
599 }
600
601 #[test]
602 fn empty_function_map() {
603 let json = r#"{
604 "version": 3,
605 "sources": ["a.js", "b.js"],
606 "names": [],
607 "mappings": "AAAA",
608 "x_facebook_sources": [
609 [{"names": ["<global>"], "mappings": "AAA"}],
610 null
611 ]
612 }"#;
613
614 let sm = SourceMapHermes::from_json(json).unwrap();
615 assert!(sm.get_function_map(0).is_some());
616 assert!(sm.get_function_map(1).is_none());
617 }
618
619 #[test]
620 fn no_facebook_sources() {
621 let json = r#"{
622 "version": 3,
623 "sources": ["a.js"],
624 "names": [],
625 "mappings": "AAAA"
626 }"#;
627
628 let sm = SourceMapHermes::from_json(json).unwrap();
629 assert!(sm.get_function_map(0).is_none());
630 assert!(sm.get_scope_for_token(0, 0).is_none());
633 }
634
635 #[test]
636 fn ram_bundle_detection() {
637 let json = r#"{
638 "version": 3,
639 "sources": ["a.js"],
640 "names": [],
641 "mappings": "AAAA",
642 "x_facebook_offsets": [0, 100, null, 300]
643 }"#;
644
645 let sm = SourceMapHermes::from_json(json).unwrap();
646 assert!(sm.is_for_ram_bundle());
647
648 let offsets = sm.x_facebook_offsets().unwrap();
649 assert_eq!(offsets.len(), 4);
650 assert_eq!(offsets[0], Some(0));
651 assert_eq!(offsets[1], Some(100));
652 assert_eq!(offsets[2], None);
653 assert_eq!(offsets[3], Some(300));
654 }
655
656 #[test]
657 fn not_ram_bundle() {
658 let json = r#"{
659 "version": 3,
660 "sources": ["a.js"],
661 "names": [],
662 "mappings": "AAAA"
663 }"#;
664
665 let sm = SourceMapHermes::from_json(json).unwrap();
666 assert!(!sm.is_for_ram_bundle());
667 assert!(sm.x_facebook_offsets().is_none());
668 }
669
670 #[test]
671 fn metro_module_paths() {
672 let json = r#"{
673 "version": 3,
674 "sources": ["a.js"],
675 "names": [],
676 "mappings": "AAAA",
677 "x_metro_module_paths": ["./src/App.js", "./src/utils.js"]
678 }"#;
679
680 let sm = SourceMapHermes::from_json(json).unwrap();
681 let paths = sm.x_metro_module_paths().unwrap();
682 assert_eq!(paths, &["./src/App.js", "./src/utils.js"]);
683 }
684
685 #[test]
686 fn deref_to_sourcemap() {
687 let json = r#"{
688 "version": 3,
689 "sources": ["input.js"],
690 "names": ["x"],
691 "mappings": "AAAA"
692 }"#;
693
694 let sm = SourceMapHermes::from_json(json).unwrap();
695 assert_eq!(sm.sources.len(), 1);
697 assert_eq!(sm.source(0), "input.js");
698 assert_eq!(sm.names.len(), 1);
699 }
700
701 #[test]
702 fn into_inner() {
703 let json = r#"{
704 "version": 3,
705 "sources": ["input.js"],
706 "names": [],
707 "mappings": "AAAA"
708 }"#;
709
710 let sm = SourceMapHermes::from_json(json).unwrap();
711 let inner = sm.into_inner();
712 assert_eq!(inner.sources.len(), 1);
713 }
714
715 #[test]
716 fn roundtrip_serialization() {
717 let json = r#"{
718 "version": 3,
719 "sources": ["a.js"],
720 "names": [],
721 "mappings": "AAAA",
722 "x_facebook_sources": [
723 [{"names": ["<global>", "foo"], "mappings": "AAA,CCA"}]
724 ]
725 }"#;
726
727 let sm = SourceMapHermes::from_json(json).unwrap();
728 let output = sm.to_json();
729
730 let sm2 = SourceMapHermes::from_json(&output).unwrap();
732 assert_eq!(sm2.sources.len(), 1);
733 assert!(sm2.get_function_map(0).is_some());
734
735 let fm = sm2.get_function_map(0).unwrap();
736 assert_eq!(fm.names, vec!["<global>", "foo"]);
737 assert_eq!(fm.mappings.len(), 2);
738 }
739
740 #[test]
741 fn roundtrip_with_offsets_and_paths() {
742 let json = r#"{
743 "version": 3,
744 "sources": ["a.js"],
745 "names": [],
746 "mappings": "AAAA",
747 "x_facebook_offsets": [0, 100],
748 "x_metro_module_paths": ["./a.js"]
749 }"#;
750
751 let sm = SourceMapHermes::from_json(json).unwrap();
752 let output = sm.to_json();
753
754 let sm2 = SourceMapHermes::from_json(&output).unwrap();
755 assert!(sm2.is_for_ram_bundle());
756 assert_eq!(sm2.x_facebook_offsets().unwrap(), &[Some(0), Some(100)]);
757 assert_eq!(sm2.x_metro_module_paths().unwrap(), &["./a.js"]);
758 }
759
760 #[test]
761 fn empty_mappings_string() {
762 let json = r#"{
763 "version": 3,
764 "sources": ["a.js"],
765 "names": [],
766 "mappings": "AAAA",
767 "x_facebook_sources": [
768 [{"names": [], "mappings": ""}]
769 ]
770 }"#;
771
772 let sm = SourceMapHermes::from_json(json).unwrap();
773 let fm = sm.get_function_map(0).unwrap();
774 assert!(fm.names.is_empty());
775 assert!(fm.mappings.is_empty());
776 }
777
778 #[test]
779 fn invalid_function_map_missing_names() {
780 let json = r#"{
781 "version": 3,
782 "sources": ["a.js"],
783 "names": [],
784 "mappings": "AAAA",
785 "x_facebook_sources": [
786 [{"mappings": "AAA"}]
787 ]
788 }"#;
789
790 let err = SourceMapHermes::from_json(json).unwrap_err();
791 assert!(matches!(err, HermesError::InvalidFunctionMap(_)));
792 }
793
794 #[test]
795 fn invalid_function_map_missing_mappings() {
796 let json = r#"{
797 "version": 3,
798 "sources": ["a.js"],
799 "names": [],
800 "mappings": "AAAA",
801 "x_facebook_sources": [
802 [{"names": ["foo"]}]
803 ]
804 }"#;
805
806 let err = SourceMapHermes::from_json(json).unwrap_err();
807 assert!(matches!(err, HermesError::InvalidFunctionMap(_)));
808 }
809
810 #[test]
811 fn all_null_facebook_sources() {
812 let json = r#"{
813 "version": 3,
814 "sources": ["a.js", "b.js"],
815 "names": [],
816 "mappings": "AAAA",
817 "x_facebook_sources": [null, null]
818 }"#;
819
820 let sm = SourceMapHermes::from_json(json).unwrap();
821 assert!(sm.get_function_map(0).is_none());
822 assert!(sm.get_function_map(1).is_none());
823 }
824
825 #[test]
826 fn debug_format() {
827 let json = r#"{
828 "version": 3,
829 "sources": ["a.js"],
830 "names": [],
831 "mappings": "AAAA"
832 }"#;
833
834 let sm = SourceMapHermes::from_json(json).unwrap();
835 let debug = format!("{sm:?}");
836 assert!(debug.contains("SourceMapHermes"));
837 }
838
839 #[test]
840 fn error_display() {
841 let err = HermesError::InvalidFunctionMap("test error".to_string());
842 assert_eq!(err.to_string(), "invalid function map: test error");
843
844 let err = HermesError::Vlq(DecodeError::UnexpectedEof { offset: 5 });
845 let msg = err.to_string();
846 assert!(msg.contains("VLQ"));
847 }
848}