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}")]
35 Malformed {
36 expected: &'static str,
38 at: usize,
39 },
40 #[error("the source map of {key} does not read: {reason}")]
41 Module { key: String, reason: String },
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46struct Slot {
47 key: String,
48 version: Option<String>,
50 value: Range<usize>,
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
56pub struct SourceMaps {
57 slots: Vec<Slot>,
58 by_name: HashMap<String, Vec<usize>>,
61}
62
63impl SourceMaps {
64 pub fn index(bytes: &[u8]) -> Result<SourceMaps, SourceMapError> {
71 let mut maps = SourceMaps::default();
72 let mut scan = Scanner { bytes, at: 0 };
73 scan.expect(b'{', "'{'")?;
74 if scan.peek() == Some(b'}') {
75 return Ok(maps);
76 }
77 loop {
78 let key = scan.string()?;
79 scan.expect(b':', "':'")?;
80 let value = scan.value()?;
81 if let Ok(key) = serde_json::from_slice::<String>(&bytes[key]) {
83 maps.push(key, value);
84 }
85 match scan.peek() {
86 Some(b',') => scan.at += 1,
87 Some(b'}') => return Ok(maps),
88 _ => return Err(scan.malformed("',' or '}'")),
89 }
90 }
91 }
92
93 fn push(&mut self, key: String, value: Range<usize>) {
94 let (name, version) = ModuleKey::parse(&key).record_stem();
95 let slot = self.slots.len();
96 if let Some((package, path)) = name.split_once("/src/main/") {
98 self.by_name.entry(format!("{package}/{path}")).or_default().push(slot);
99 }
100 self.by_name.entry(name).or_default().push(slot);
101 self.slots.push(Slot { key, version, value });
102 }
103
104 pub fn len(&self) -> usize {
106 self.slots.len()
107 }
108
109 pub fn is_empty(&self) -> bool {
110 self.slots.is_empty()
111 }
112
113 pub fn keys(&self) -> impl Iterator<Item = &str> {
115 self.slots.iter().map(|slot| slot.key.as_str())
116 }
117
118 pub fn heap_bytes(&self) -> usize {
121 let slots: usize = self.slots.iter().map(|slot| slot.key.capacity() + slot.version.as_ref().map_or(0, String::capacity)).sum();
122 let names: usize = self.by_name.iter().map(|(name, slots)| name.capacity() + slots.capacity() * size_of::<usize>()).sum();
123 let buckets = self.by_name.capacity() * (size_of::<(String, Vec<usize>)>() + 1);
125 self.slots.capacity() * size_of::<Slot>() + slots + buckets + names
126 }
127
128 fn slot_of(&self, record: &str) -> Option<&Slot> {
129 let (name, version) = match record.strip_prefix('&') {
130 Some(rest) => {
132 let (name, version) = rest.split_once('&')?;
133 (name, Some(version).filter(|version| !version.is_empty()))
134 }
135 None => (record.split_once('/').map_or(record, |(_, rest)| rest), None),
137 };
138 let slots = self.by_name.get(name)?;
139 let mut slots = slots.iter().map(|&slot| &self.slots[slot]);
140 match version {
141 Some(version) => slots.find(|slot| slot.version.as_deref().is_none_or(|own| own == version)),
142 None => slots.next(),
143 }
144 }
145
146 pub fn module(&self, bytes: &[u8], record: &str) -> Option<Result<ModuleMap, SourceMapError>> {
154 let slot = self.slot_of(record)?;
155 let failed = |reason: String| SourceMapError::Module { key: slot.key.clone(), reason };
156 let value = bytes.get(slot.value.clone()).ok_or_else(|| failed("the file is shorter than it was".to_owned()));
157 Some(value.and_then(|value| ModuleMap::parse(value).map_err(failed)))
158 }
159}
160
161struct ModuleKey<'k> {
163 package: Option<(&'k str, &'k str)>,
165 path: &'k str,
168}
169
170impl<'k> ModuleKey<'k> {
171 fn parse(key: &'k str) -> ModuleKey<'k> {
172 let mut parts = key.splitn(4, '|');
173 match (parts.next(), parts.next(), parts.next(), parts.next()) {
174 (Some(_built), Some(package), Some(version), Some(path)) => ModuleKey { package: Some((package, version)), path },
175 _ => ModuleKey { package: None, path: key },
176 }
177 }
178
179 fn record_stem(&self) -> (String, Option<String>) {
182 let file = self.path.rsplit('/').next().unwrap_or(self.path);
183 let stem = match file.rsplit_once('.') {
184 Some((stem, _)) if !stem.is_empty() => &self.path[..self.path.len() - (file.len() - stem.len())],
185 _ => self.path,
186 };
187 match self.package {
188 Some((package, version)) => (format!("{package}/{stem}"), Some(version.to_owned())),
189 None => (stem.to_owned(), None),
190 }
191 }
192}
193
194struct Scanner<'b> {
196 bytes: &'b [u8],
197 at: usize,
198}
199
200impl Scanner<'_> {
201 fn malformed(&self, expected: &'static str) -> SourceMapError {
202 SourceMapError::Malformed { expected, at: self.at }
203 }
204
205 fn peek(&mut self) -> Option<u8> {
207 while let Some(&byte) = self.bytes.get(self.at) {
208 if !byte.is_ascii_whitespace() {
209 return Some(byte);
210 }
211 self.at += 1;
212 }
213 None
214 }
215
216 fn expect(&mut self, byte: u8, expected: &'static str) -> Result<(), SourceMapError> {
217 if self.peek() != Some(byte) {
218 return Err(self.malformed(expected));
219 }
220 self.at += 1;
221 Ok(())
222 }
223
224 fn string(&mut self) -> Result<Range<usize>, SourceMapError> {
226 let start = self.at;
227 self.expect(b'"', "a string")?;
228 while let Some(&byte) = self.bytes.get(self.at) {
229 self.at += 1;
230 match byte {
231 b'"' => return Ok(start..self.at),
232 b'\\' => self.at += 1,
234 _ => {}
235 }
236 }
237 Err(self.malformed("'\"'"))
238 }
239
240 fn value(&mut self) -> Result<Range<usize>, SourceMapError> {
242 let Some(first) = self.peek() else { return Err(self.malformed("a value")) };
243 let start = self.at;
244 if first == b'"' {
245 return self.string();
246 }
247 if first != b'{' && first != b'[' {
248 let rest = &self.bytes[start..];
250 let len = rest.iter().position(|byte| byte.is_ascii_whitespace() || matches!(byte, b',' | b'}' | b']' | b'"' | b':'));
251 self.at += len.unwrap_or(rest.len());
252 return if self.at == start { Err(self.malformed("a value")) } else { Ok(start..self.at) };
253 }
254 let mut depth = 0usize;
255 while let Some(&byte) = self.bytes.get(self.at) {
256 match byte {
257 b'"' => {
258 self.string()?;
259 continue;
260 }
261 b'{' | b'[' => depth += 1,
262 b'}' | b']' => {
263 depth -= 1;
264 if depth == 0 {
265 self.at += 1;
266 return Ok(start..self.at);
267 }
268 }
269 _ => {}
270 }
271 self.at += 1;
272 }
273 Err(self.malformed("']' or '}'"))
274 }
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
279pub struct SourceLine {
280 pub source: u32,
282 pub line: u32,
284}
285
286#[derive(Debug, Clone, Default, PartialEq, Eq)]
289pub struct ModuleMap {
290 pub sources: Vec<String>,
294 pub names: usize,
297 lines: Vec<Option<SourceLine>>,
300 mapped: Vec<u32>,
302 pub damaged: bool,
305}
306
307impl ModuleMap {
308 fn parse(value: &[u8]) -> Result<ModuleMap, String> {
309 let value: Value = serde_json::from_slice(value).map_err(|err| err.to_string())?;
310 let Some(mappings) = value.get("mappings").and_then(Value::as_str) else {
311 return Err("no \"mappings\" string".to_owned());
312 };
313 let sources = value.get("sources").and_then(Value::as_array).map_or_else(Vec::new, |sources| {
314 sources.iter().map(|source| source.as_str().unwrap_or_default().to_owned()).collect()
316 });
317 let names = value.get("names").and_then(Value::as_array).map_or(0, Vec::len);
318 let (lines, damaged) = decode_lines(mappings, sources.len());
319 let mut mapped: Vec<u32> = lines.iter().flatten().map(|line| line.source).collect();
320 mapped.sort_unstable();
321 mapped.dedup();
322 Ok(ModuleMap { sources, names, lines, mapped, damaged })
323 }
324
325 pub fn mapped_sources(&self) -> &[u32] {
330 &self.mapped
331 }
332
333 pub fn source_line(&self, compiled: u32) -> Option<SourceLine> {
337 self.lines.get(usize::try_from(compiled).ok()?).copied().flatten()
338 }
339
340 pub fn source_lines(&self) -> &[Option<SourceLine>] {
342 &self.lines
343 }
344}
345
346fn digit(byte: u8) -> Option<i64> {
348 let value = match byte {
349 b'A'..=b'Z' => byte - b'A',
350 b'a'..=b'z' => byte - b'a' + 26,
351 b'0'..=b'9' => byte - b'0' + 52,
352 b'+' => 62,
353 b'/' => 63,
354 _ => return None,
355 };
356 Some(i64::from(value))
357}
358
359fn segment(text: &[u8]) -> Option<([i64; 5], usize)> {
362 const CONTINUES: i64 = 32;
363 let mut fields = [0i64; 5];
364 let mut count = 0;
365 let (mut value, mut shift) = (0i64, 0u32);
366 for &byte in text {
367 let digit = digit(byte)?;
368 if shift > 40 || count == fields.len() {
370 return None;
371 }
372 value |= (digit & (CONTINUES - 1)) << shift;
373 if digit & CONTINUES != 0 {
374 shift += 5;
375 continue;
376 }
377 fields[count] = if value & 1 == 1 { -(value >> 1) } else { value >> 1 };
378 count += 1;
379 (value, shift) = (0, 0);
380 }
381 (shift == 0).then_some((fields, count))
382}
383
384fn decode_lines(mappings: &str, sources: usize) -> (Vec<Option<SourceLine>>, bool) {
387 let mut lines = Vec::new();
388 let (mut source, mut source_line) = (0i64, 0i64);
390 for line in mappings.as_bytes().split(|&byte| byte == b';') {
391 let mut first = None;
392 for text in line.split(|&byte| byte == b',').filter(|text| !text.is_empty()) {
393 let Some((fields, count)) = segment(text) else { return (lines, true) };
394 if count < 4 {
396 continue;
397 }
398 source = source.saturating_add(fields[1]);
399 source_line = source_line.saturating_add(fields[2]);
400 if first.is_none() {
401 let source = u32::try_from(source).ok().filter(|&source| (source as usize) < sources);
403 let line = u32::try_from(source_line).ok().and_then(|line| line.checked_add(1));
404 first = source.zip(line).map(|(source, line)| SourceLine { source, line });
405 }
406 }
407 lines.push(first);
408 }
409 (lines, false)
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 const TABLE: &str = r#"{
417 "entry|entry|1.0.0|src/main/ets/pages/Index.ts": {
418 "version": 3, "file": "Index.ets", "sourceRoot": "",
419 "sources": ["entry/src/main/ets/pages/Index.ets"], "names": [],
420 "mappings": ";;AAAA;AACA,IAAI;;AAGJ", "entry-package-info": "entry|1.0.0"},
421 "entry|@scope/lib|2.0.0|src/main/js/lib.js": {
422 "version": 3, "sources": ["oh_modules/@scope/lib/src/main/js/lib.js"], "names": ["a\"}"], "mappings": "AAEA"},
423 "entry|@scope/lib|3.1.0|src/main/js/lib.js": {"sources": [], "mappings": ""},
424 "entry|entry|1.0.0|src/main/ets/broken.ts": {"mappings": 7},
425 "other/src/main/ets/Old.ts": {"sources": ["other/src/main/ets/Old.ets"], "mappings": "AAAA;A*"}
426 }"#;
427
428 fn first(lines: &[Option<u32>]) -> Vec<Option<SourceLine>> {
430 lines.iter().map(|line| line.map(|line| SourceLine { source: 0, line })).collect()
431 }
432
433 #[test]
434 fn modules_are_found_by_record() {
435 let maps = SourceMaps::index(TABLE.as_bytes()).unwrap();
436 assert_eq!(maps.len(), 5);
437 assert_eq!(maps.keys().next(), Some("entry|entry|1.0.0|src/main/ets/pages/Index.ts"));
438 let module = |record: &str| maps.module(TABLE.as_bytes(), record);
439
440 let index = module("&entry/src/main/ets/pages/Index&").unwrap().unwrap();
441 assert_eq!((&index.sources[..], index.mapped_sources()), (&["entry/src/main/ets/pages/Index.ets".to_owned()][..], &[0][..]));
442 assert_eq!(index.source_lines(), first(&[None, None, Some(1), Some(2), None, Some(5)]));
444 assert_eq!((index.source_line(3).map(|at| at.line), index.source_line(4), index.source_line(99)), (Some(2), None, None));
445 assert!(!index.damaged && index.names == 0);
446 assert_eq!(module("com.example.app/entry/ets/pages/Index").unwrap().unwrap(), index);
448
449 let lib = module("&@scope/lib/src/main/js/lib&2.0.0").unwrap().unwrap();
450 assert_eq!((lib.source_line(0), lib.names), (Some(SourceLine { source: 0, line: 3 }), 1));
451 assert!(module("&@scope/lib/src/main/js/lib&3.1.0").unwrap().unwrap().mapped_sources().is_empty());
452 assert!(module("&@scope/lib/src/main/js/lib&9.9.9").is_none());
453
454 let old = module("&other/src/main/ets/Old&").unwrap().unwrap();
456 assert_eq!((old.source_lines(), old.damaged), (&first(&[Some(1)])[..], true));
457
458 assert!(matches!(module("&entry/src/main/ets/broken&"), Some(Err(SourceMapError::Module { .. }))));
459 assert!(module("&entry/src/main/ets/pages/Missing&").is_none() && module("&nonsense").is_none() && module("").is_none());
460 }
461
462 #[test]
463 fn the_index_knows_what_it_holds() {
464 assert_eq!(SourceMaps::default().heap_bytes(), 0);
465 let maps = SourceMaps::index(TABLE.as_bytes()).unwrap();
466 let keys: usize = maps.keys().map(str::len).sum();
467 assert!(maps.heap_bytes() > keys + maps.len() * size_of::<Slot>(), "{}", maps.heap_bytes());
469 assert!(maps.heap_bytes() < 16 * TABLE.len(), "{}", maps.heap_bytes());
470 }
471
472 #[test]
473 fn malformed_tables_are_errors_not_panics() {
474 assert_eq!(SourceMaps::index(b"{}").unwrap().len(), 0);
475 assert_eq!(SourceMaps::index(b" { \"a\" : [1, {\"b\": \"]\"}] , \"c\":null } ").unwrap().len(), 2);
476 for text in ["", "[]", "{", "{\"a\"", "{\"a\":", "{\"a\":{", "{\"a\":\"x", "{\"a\":1,", "{\"a\":1 \"b\":2}", "{\"a\\", "{,}"] {
477 assert!(matches!(SourceMaps::index(text.as_bytes()), Err(SourceMapError::Malformed { .. })), "{text}");
478 }
479 for end in 0..TABLE.len() {
481 let _ = SourceMaps::index(&TABLE.as_bytes()[..end]);
482 }
483 let maps = SourceMaps::index(TABLE.as_bytes()).unwrap();
485 assert!(matches!(maps.module(b"{}", "&entry/src/main/ets/pages/Index&"), Some(Err(_))));
486 }
487
488 #[test]
489 fn segments_decode_or_stop() {
490 assert_eq!(segment(b"AAgBC"), Some(([0, 0, 16, 1, 0], 4)));
491 assert_eq!(segment(b"D"), Some(([-1, 0, 0, 0, 0], 1)));
492 assert_eq!(segment(b"A*"), None);
493 assert_eq!(segment(b"gggggggggggggggg"), None);
495 assert_eq!(segment(b"AAAAAA"), None);
496 assert_eq!(segment(b"g"), None);
497 assert_eq!(decode_lines("AADA;AACA", 1), (first(&[None, Some(1)]), false));
499 }
500
501 #[test]
504 fn a_line_comes_from_the_source_its_first_segment_names() {
505 let at = |source, line| Some(SourceLine { source, line });
506 let mappings = "AAAA;ACIA,ADAA;A,ACAA;ACAA;AFCA";
510 let (lines, damaged) = decode_lines(mappings, 2);
511 assert_eq!((lines, damaged), (vec![at(0, 1), at(1, 5), at(1, 5), None, at(0, 6)], false));
512 assert_eq!(decode_lines("ADAA;ACCA", 2).0, vec![None, at(0, 2)]);
514
515 let table = format!(
516 r#"{{"a|a|1.0.0|src/main/ets/bundle.ts": {{"sources": ["a/one.ets", "a/unused.ets", "a/two.ets"], "mappings": "{}"}}}}"#,
517 "AAAA;AECA;ACAA"
518 );
519 let maps = SourceMaps::index(table.as_bytes()).unwrap();
520 let bundle = maps.module(table.as_bytes(), "&a/src/main/ets/bundle&").unwrap().unwrap();
521 assert_eq!(bundle.source_lines(), [at(0, 1), at(2, 2), None]);
522 assert_eq!(bundle.mapped_sources(), [0, 2]);
524 }
525}