1use std::collections::HashMap;
26use std::ops::Range;
27
28use serde_json::Value;
29
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
32#[non_exhaustive]
33pub enum SourceMapError {
34 #[error("not a JSON object of source maps: {expected} expected at byte {at}")]
36 Malformed {
37 expected: &'static str,
39 at: usize,
41 },
42 #[error("the source map of {key} does not read: {reason}")]
44 Module {
45 key: String,
47 reason: String,
49 },
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54struct Slot {
55 key: String,
56 version: Option<String>,
58 value: Range<usize>,
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Eq)]
64pub struct SourceMaps {
65 slots: Vec<Slot>,
66 by_name: HashMap<String, Vec<usize>>,
69}
70
71impl SourceMaps {
72 pub fn index(bytes: &[u8]) -> Result<SourceMaps, SourceMapError> {
79 let mut maps = SourceMaps::default();
80 let mut scan = Scanner { bytes, at: 0 };
81 scan.expect(b'{', "'{'")?;
82 if scan.peek() == Some(b'}') {
83 return Ok(maps);
84 }
85 loop {
86 let key = scan.string()?;
87 scan.expect(b':', "':'")?;
88 let value = scan.value()?;
89 if let Ok(key) = serde_json::from_slice::<String>(&bytes[key]) {
91 maps.push(key, value);
92 }
93 match scan.peek() {
94 Some(b',') => scan.at += 1,
95 Some(b'}') => return Ok(maps),
96 _ => return Err(scan.malformed("',' or '}'")),
97 }
98 }
99 }
100
101 fn push(&mut self, key: String, value: Range<usize>) {
102 let (name, version) = ModuleKey::parse(&key).record_stem();
103 let slot = self.slots.len();
104 if let Some((package, path)) = name.split_once("/src/main/") {
106 self.by_name.entry(format!("{package}/{path}")).or_default().push(slot);
107 }
108 self.by_name.entry(name).or_default().push(slot);
109 self.slots.push(Slot { key, version, value });
110 }
111
112 pub fn len(&self) -> usize {
114 self.slots.len()
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.slots.is_empty()
120 }
121
122 pub fn keys(&self) -> impl Iterator<Item = &str> {
124 self.slots.iter().map(|slot| slot.key.as_str())
125 }
126
127 pub fn heap_bytes(&self) -> usize {
130 let slots: usize = self.slots.iter().map(|slot| slot.key.capacity() + slot.version.as_ref().map_or(0, String::capacity)).sum();
131 let names: usize = self.by_name.iter().map(|(name, slots)| name.capacity() + slots.capacity() * size_of::<usize>()).sum();
132 let buckets = self.by_name.capacity() * (size_of::<(String, Vec<usize>)>() + 1);
134 self.slots.capacity() * size_of::<Slot>() + slots + buckets + names
135 }
136
137 fn slot_of(&self, record: &str) -> Option<&Slot> {
138 let (name, version) = match record.strip_prefix('&') {
139 Some(rest) => {
141 let (name, version) = rest.split_once('&')?;
142 (name, Some(version).filter(|version| !version.is_empty()))
143 }
144 None => (record.split_once('/').map_or(record, |(_, rest)| rest), None),
146 };
147 let slots = self.by_name.get(name)?;
148 let mut slots = slots.iter().map(|&slot| &self.slots[slot]);
149 match version {
150 Some(version) => slots.find(|slot| slot.version.as_deref().is_none_or(|own| own == version)),
151 None => slots.next(),
152 }
153 }
154
155 pub fn module(&self, bytes: &[u8], record: &str) -> Option<Result<ModuleMap, SourceMapError>> {
163 let slot = self.slot_of(record)?;
164 let failed = |reason: String| SourceMapError::Module { key: slot.key.clone(), reason };
165 let value = bytes.get(slot.value.clone()).ok_or_else(|| failed("the file is shorter than it was".to_owned()));
166 Some(value.and_then(|value| ModuleMap::parse(value).map_err(failed)))
167 }
168}
169
170struct ModuleKey<'k> {
172 package: Option<(&'k str, &'k str)>,
174 path: &'k str,
177}
178
179impl<'k> ModuleKey<'k> {
180 fn parse(key: &'k str) -> ModuleKey<'k> {
181 let mut parts = key.splitn(4, '|');
182 match (parts.next(), parts.next(), parts.next(), parts.next()) {
183 (Some(_built), Some(package), Some(version), Some(path)) => ModuleKey { package: Some((package, version)), path },
184 _ => ModuleKey { package: None, path: key },
185 }
186 }
187
188 fn record_stem(&self) -> (String, Option<String>) {
191 let file = self.path.rsplit('/').next().unwrap_or(self.path);
192 let stem = match file.rsplit_once('.') {
193 Some((stem, _)) if !stem.is_empty() => &self.path[..self.path.len() - (file.len() - stem.len())],
194 _ => self.path,
195 };
196 match self.package {
197 Some((package, version)) => (format!("{package}/{stem}"), Some(version.to_owned())),
198 None => (stem.to_owned(), None),
199 }
200 }
201}
202
203struct Scanner<'b> {
205 bytes: &'b [u8],
206 at: usize,
207}
208
209impl Scanner<'_> {
210 fn malformed(&self, expected: &'static str) -> SourceMapError {
211 SourceMapError::Malformed { expected, at: self.at }
212 }
213
214 fn peek(&mut self) -> Option<u8> {
216 while let Some(&byte) = self.bytes.get(self.at) {
217 if !byte.is_ascii_whitespace() {
218 return Some(byte);
219 }
220 self.at += 1;
221 }
222 None
223 }
224
225 fn expect(&mut self, byte: u8, expected: &'static str) -> Result<(), SourceMapError> {
226 if self.peek() != Some(byte) {
227 return Err(self.malformed(expected));
228 }
229 self.at += 1;
230 Ok(())
231 }
232
233 fn string(&mut self) -> Result<Range<usize>, SourceMapError> {
235 let start = self.at;
236 self.expect(b'"', "a string")?;
237 while let Some(&byte) = self.bytes.get(self.at) {
238 self.at += 1;
239 match byte {
240 b'"' => return Ok(start..self.at),
241 b'\\' => self.at += 1,
243 _ => {}
244 }
245 }
246 Err(self.malformed("'\"'"))
247 }
248
249 fn value(&mut self) -> Result<Range<usize>, SourceMapError> {
251 let Some(first) = self.peek() else { return Err(self.malformed("a value")) };
252 let start = self.at;
253 if first == b'"' {
254 return self.string();
255 }
256 if first != b'{' && first != b'[' {
257 let rest = &self.bytes[start..];
259 let len = rest.iter().position(|byte| byte.is_ascii_whitespace() || matches!(byte, b',' | b'}' | b']' | b'"' | b':'));
260 self.at += len.unwrap_or(rest.len());
261 return if self.at == start { Err(self.malformed("a value")) } else { Ok(start..self.at) };
262 }
263 let mut depth = 0usize;
264 while let Some(&byte) = self.bytes.get(self.at) {
265 match byte {
266 b'"' => {
267 self.string()?;
268 continue;
269 }
270 b'{' | b'[' => depth += 1,
271 b'}' | b']' => {
272 depth -= 1;
273 if depth == 0 {
274 self.at += 1;
275 return Ok(start..self.at);
276 }
277 }
278 _ => {}
279 }
280 self.at += 1;
281 }
282 Err(self.malformed("']' or '}'"))
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
288#[non_exhaustive]
289pub struct SourceLine {
290 pub source: u32,
292 pub line: u32,
294}
295
296#[derive(Debug, Clone, Default, PartialEq, Eq)]
299#[non_exhaustive]
300pub struct ModuleMap {
301 pub sources: Vec<String>,
305 pub names: usize,
308 lines: Vec<Option<SourceLine>>,
311 mapped: Vec<u32>,
313 pub damaged: bool,
316}
317
318impl ModuleMap {
319 fn parse(value: &[u8]) -> Result<ModuleMap, String> {
320 let value: Value = serde_json::from_slice(value).map_err(|err| err.to_string())?;
321 let Some(mappings) = value.get("mappings").and_then(Value::as_str) else {
322 return Err("no \"mappings\" string".to_owned());
323 };
324 let sources = value.get("sources").and_then(Value::as_array).map_or_else(Vec::new, |sources| {
325 sources.iter().map(|source| source.as_str().unwrap_or_default().to_owned()).collect()
327 });
328 let names = value.get("names").and_then(Value::as_array).map_or(0, Vec::len);
329 let (lines, damaged) = decode_lines(mappings, sources.len());
330 let mut mapped: Vec<u32> = lines.iter().flatten().map(|line| line.source).collect();
331 mapped.sort_unstable();
332 mapped.dedup();
333 Ok(ModuleMap { sources, names, lines, mapped, damaged })
334 }
335
336 pub fn mapped_sources(&self) -> &[u32] {
341 &self.mapped
342 }
343
344 pub fn source_line(&self, compiled: u32) -> Option<SourceLine> {
348 self.lines.get(usize::try_from(compiled).ok()?).copied().flatten()
349 }
350
351 pub fn source_lines(&self) -> &[Option<SourceLine>] {
353 &self.lines
354 }
355}
356
357fn digit(byte: u8) -> Option<i64> {
359 let value = match byte {
360 b'A'..=b'Z' => byte - b'A',
361 b'a'..=b'z' => byte - b'a' + 26,
362 b'0'..=b'9' => byte - b'0' + 52,
363 b'+' => 62,
364 b'/' => 63,
365 _ => return None,
366 };
367 Some(i64::from(value))
368}
369
370fn segment(text: &[u8]) -> Option<([i64; 5], usize)> {
373 const CONTINUES: i64 = 32;
374 let mut fields = [0i64; 5];
375 let mut count = 0;
376 let (mut value, mut shift) = (0i64, 0u32);
377 for &byte in text {
378 let digit = digit(byte)?;
379 if shift > 40 || count == fields.len() {
381 return None;
382 }
383 value |= (digit & (CONTINUES - 1)) << shift;
384 if digit & CONTINUES != 0 {
385 shift += 5;
386 continue;
387 }
388 fields[count] = if value & 1 == 1 { -(value >> 1) } else { value >> 1 };
389 count += 1;
390 (value, shift) = (0, 0);
391 }
392 (shift == 0).then_some((fields, count))
393}
394
395fn decode_lines(mappings: &str, sources: usize) -> (Vec<Option<SourceLine>>, bool) {
398 let mut lines = Vec::new();
399 let (mut source, mut source_line) = (0i64, 0i64);
401 for line in mappings.as_bytes().split(|&byte| byte == b';') {
402 let mut first = None;
403 for text in line.split(|&byte| byte == b',').filter(|text| !text.is_empty()) {
404 let Some((fields, count)) = segment(text) else { return (lines, true) };
405 if count < 4 {
407 continue;
408 }
409 source = source.saturating_add(fields[1]);
410 source_line = source_line.saturating_add(fields[2]);
411 if first.is_none() {
412 let source = u32::try_from(source).ok().filter(|&source| (source as usize) < sources);
414 let line = u32::try_from(source_line).ok().and_then(|line| line.checked_add(1));
415 first = source.zip(line).map(|(source, line)| SourceLine { source, line });
416 }
417 }
418 lines.push(first);
419 }
420 (lines, false)
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 const TABLE: &str = r#"{
428 "entry|entry|1.0.0|src/main/ets/pages/Index.ts": {
429 "version": 3, "file": "Index.ets", "sourceRoot": "",
430 "sources": ["entry/src/main/ets/pages/Index.ets"], "names": [],
431 "mappings": ";;AAAA;AACA,IAAI;;AAGJ", "entry-package-info": "entry|1.0.0"},
432 "entry|@scope/lib|2.0.0|src/main/js/lib.js": {
433 "version": 3, "sources": ["oh_modules/@scope/lib/src/main/js/lib.js"], "names": ["a\"}"], "mappings": "AAEA"},
434 "entry|@scope/lib|3.1.0|src/main/js/lib.js": {"sources": [], "mappings": ""},
435 "entry|entry|1.0.0|src/main/ets/broken.ts": {"mappings": 7},
436 "other/src/main/ets/Old.ts": {"sources": ["other/src/main/ets/Old.ets"], "mappings": "AAAA;A*"}
437 }"#;
438
439 fn first(lines: &[Option<u32>]) -> Vec<Option<SourceLine>> {
441 lines.iter().map(|line| line.map(|line| SourceLine { source: 0, line })).collect()
442 }
443
444 #[test]
445 fn modules_are_found_by_record() {
446 let maps = SourceMaps::index(TABLE.as_bytes()).unwrap();
447 assert_eq!(maps.len(), 5);
448 assert_eq!(maps.keys().next(), Some("entry|entry|1.0.0|src/main/ets/pages/Index.ts"));
449 let module = |record: &str| maps.module(TABLE.as_bytes(), record);
450
451 let index = module("&entry/src/main/ets/pages/Index&").unwrap().unwrap();
452 assert_eq!((&index.sources[..], index.mapped_sources()), (&["entry/src/main/ets/pages/Index.ets".to_owned()][..], &[0][..]));
453 assert_eq!(index.source_lines(), first(&[None, None, Some(1), Some(2), None, Some(5)]));
455 assert_eq!((index.source_line(3).map(|at| at.line), index.source_line(4), index.source_line(99)), (Some(2), None, None));
456 assert!(!index.damaged && index.names == 0);
457 assert_eq!(module("com.example.app/entry/ets/pages/Index").unwrap().unwrap(), index);
459
460 let lib = module("&@scope/lib/src/main/js/lib&2.0.0").unwrap().unwrap();
461 assert_eq!((lib.source_line(0), lib.names), (Some(SourceLine { source: 0, line: 3 }), 1));
462 assert!(module("&@scope/lib/src/main/js/lib&3.1.0").unwrap().unwrap().mapped_sources().is_empty());
463 assert!(module("&@scope/lib/src/main/js/lib&9.9.9").is_none());
464
465 let old = module("&other/src/main/ets/Old&").unwrap().unwrap();
467 assert_eq!((old.source_lines(), old.damaged), (&first(&[Some(1)])[..], true));
468
469 assert!(matches!(module("&entry/src/main/ets/broken&"), Some(Err(SourceMapError::Module { .. }))));
470 assert!(module("&entry/src/main/ets/pages/Missing&").is_none() && module("&nonsense").is_none() && module("").is_none());
471 }
472
473 #[test]
474 fn the_index_knows_what_it_holds() {
475 assert_eq!(SourceMaps::default().heap_bytes(), 0);
476 let maps = SourceMaps::index(TABLE.as_bytes()).unwrap();
477 let keys: usize = maps.keys().map(str::len).sum();
478 assert!(maps.heap_bytes() > keys + maps.len() * size_of::<Slot>(), "{}", maps.heap_bytes());
480 assert!(maps.heap_bytes() < 16 * TABLE.len(), "{}", maps.heap_bytes());
481 }
482
483 #[test]
484 fn malformed_tables_are_errors_not_panics() {
485 assert_eq!(SourceMaps::index(b"{}").unwrap().len(), 0);
486 assert_eq!(SourceMaps::index(b" { \"a\" : [1, {\"b\": \"]\"}] , \"c\":null } ").unwrap().len(), 2);
487 for text in ["", "[]", "{", "{\"a\"", "{\"a\":", "{\"a\":{", "{\"a\":\"x", "{\"a\":1,", "{\"a\":1 \"b\":2}", "{\"a\\", "{,}"] {
488 assert!(matches!(SourceMaps::index(text.as_bytes()), Err(SourceMapError::Malformed { .. })), "{text}");
489 }
490 for end in 0..TABLE.len() {
492 let _ = SourceMaps::index(&TABLE.as_bytes()[..end]);
493 }
494 let maps = SourceMaps::index(TABLE.as_bytes()).unwrap();
496 assert!(matches!(maps.module(b"{}", "&entry/src/main/ets/pages/Index&"), Some(Err(_))));
497 }
498
499 #[test]
500 fn segments_decode_or_stop() {
501 assert_eq!(segment(b"AAgBC"), Some(([0, 0, 16, 1, 0], 4)));
502 assert_eq!(segment(b"D"), Some(([-1, 0, 0, 0, 0], 1)));
503 assert_eq!(segment(b"A*"), None);
504 assert_eq!(segment(b"gggggggggggggggg"), None);
506 assert_eq!(segment(b"AAAAAA"), None);
507 assert_eq!(segment(b"g"), None);
508 assert_eq!(decode_lines("AADA;AACA", 1), (first(&[None, Some(1)]), false));
510 }
511
512 #[test]
515 fn a_line_comes_from_the_source_its_first_segment_names() {
516 let at = |source, line| Some(SourceLine { source, line });
517 let mappings = "AAAA;ACIA,ADAA;A,ACAA;ACAA;AFCA";
521 let (lines, damaged) = decode_lines(mappings, 2);
522 assert_eq!((lines, damaged), (vec![at(0, 1), at(1, 5), at(1, 5), None, at(0, 6)], false));
523 assert_eq!(decode_lines("ADAA;ACCA", 2).0, vec![None, at(0, 2)]);
525
526 let table = format!(
527 r#"{{"a|a|1.0.0|src/main/ets/bundle.ts": {{"sources": ["a/one.ets", "a/unused.ets", "a/two.ets"], "mappings": "{}"}}}}"#,
528 "AAAA;AECA;ACAA"
529 );
530 let maps = SourceMaps::index(table.as_bytes()).unwrap();
531 let bundle = maps.module(table.as_bytes(), "&a/src/main/ets/bundle&").unwrap().unwrap();
532 assert_eq!(bundle.source_lines(), [at(0, 1), at(2, 2), None]);
533 assert_eq!(bundle.mapped_sources(), [0, 2]);
535 }
536}