1use symbolic_common::AsSelf;
2use watto::{align_to, Pod, StringTable};
3
4use crate::{ScopeLookupResult, SourcePosition};
5
6use super::raw;
7
8#[derive(Debug, PartialEq)]
10pub struct SourceLocation<'data> {
11 file: Option<File<'data>>,
13 line: u32,
15 column: u32,
17 name: Option<&'data str>,
19 scope: ScopeLookupResult<'data>,
21}
22
23impl<'data> SourceLocation<'data> {
24 pub fn file(&self) -> Option<File<'data>> {
26 self.file
27 }
28
29 pub fn line(&self) -> u32 {
31 self.line
32 }
33
34 pub fn column(&self) -> u32 {
36 self.column
37 }
38
39 pub fn name(&self) -> Option<&'data str> {
43 self.name
44 }
45
46 pub fn line_contents(&self) -> Option<&'data str> {
48 self.file().and_then(|file| file.line(self.line as usize))
49 }
50
51 pub fn scope(&self) -> ScopeLookupResult<'data> {
53 self.scope
54 }
55
56 pub fn file_name(&self) -> Option<&'data str> {
58 self.file.and_then(|file| file.name)
59 }
60
61 pub fn file_source(&self) -> Option<&'data str> {
63 self.file.and_then(|file| file.source)
64 }
65}
66
67type Result<T, E = Error> = std::result::Result<T, E>;
68
69#[derive(Clone)]
73pub struct SourceMapCache<'data> {
74 header: &'data raw::Header,
75 min_source_positions: &'data [raw::MinifiedSourcePosition],
76 orig_source_locations: &'data [raw::OriginalSourceLocation],
77 files: &'data [raw::File],
78 line_offsets: &'data [raw::LineOffset],
79 string_bytes: &'data [u8],
80}
81
82impl<'slf, 'a: 'slf> AsSelf<'slf> for SourceMapCache<'a> {
83 type Ref = SourceMapCache<'slf>;
84
85 fn as_self(&'slf self) -> &'slf Self::Ref {
86 self
87 }
88}
89
90impl std::fmt::Debug for SourceMapCache<'_> {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("SourceMapCache")
93 .field("version", &self.header.version)
94 .field("mappings", &self.header.num_mappings)
95 .field("files", &self.header.num_files)
96 .field("line_offsets", &self.header.num_line_offsets)
97 .field("string_bytes", &self.header.string_bytes)
98 .finish()
99 }
100}
101
102impl<'data> SourceMapCache<'data> {
103 #[tracing::instrument(level = "trace", name = "SourceMapCache::parse", skip_all)]
105 pub fn parse(buf: &'data [u8]) -> Result<Self> {
106 let (header, buf) = raw::Header::ref_from_prefix(buf).ok_or(Error::Header)?;
107
108 if header.magic == raw::SOURCEMAPCACHE_MAGIC_FLIPPED {
109 return Err(Error::WrongEndianness);
110 }
111 if header.magic != raw::SOURCEMAPCACHE_MAGIC {
112 return Err(Error::WrongFormat);
113 }
114 if header.version != raw::SOURCEMAPCACHE_VERSION {
115 return Err(Error::WrongVersion);
116 }
117
118 let (_, buf) = align_to(buf, 8).ok_or(Error::SourcePositions)?;
119 let num_mappings = header.num_mappings as usize;
120 let (min_source_positions, buf) =
121 raw::MinifiedSourcePosition::slice_from_prefix(buf, num_mappings)
122 .ok_or(Error::SourcePositions)?;
123
124 let (_, buf) = align_to(buf, 8).ok_or(Error::SourcePositions)?;
125 let (orig_source_locations, buf) =
126 raw::OriginalSourceLocation::slice_from_prefix(buf, num_mappings)
127 .ok_or(Error::SourceLocations)?;
128
129 let (_, buf) = align_to(buf, 8).ok_or(Error::Files)?;
130 let (files, buf) =
131 raw::File::slice_from_prefix(buf, header.num_files as usize).ok_or(Error::Files)?;
132
133 let (_, buf) = align_to(buf, 8).ok_or(Error::LineOffsets)?;
134 let (line_offsets, buf) =
135 raw::LineOffset::slice_from_prefix(buf, header.num_line_offsets as usize)
136 .ok_or(Error::LineOffsets)?;
137
138 let (_, buf) = align_to(buf, 8).ok_or(Error::StringBytes)?;
139 let string_bytes = header.string_bytes as usize;
140 let string_bytes = buf.get(..string_bytes).ok_or(Error::StringBytes)?;
141
142 Ok(Self {
143 header,
144 min_source_positions,
145 orig_source_locations,
146 files,
147 line_offsets,
148 string_bytes,
149 })
150 }
151
152 fn get_string(&self, offset: u32) -> Option<&'data str> {
154 StringTable::read(self.string_bytes, offset as usize).ok()
155 }
156
157 fn resolve_file(&self, raw_file: &raw::File) -> Option<File<'data>> {
158 let name = self.get_string(raw_file.name_offset);
159 let source = self.get_string(raw_file.source_offset);
160 let line_offsets = self
161 .line_offsets
162 .get(raw_file.line_offsets_start as usize..raw_file.line_offsets_end as usize)?;
163 Some(File {
164 name,
165 source,
166 line_offsets,
167 })
168 }
169
170 #[tracing::instrument(level = "trace", name = "SourceMapCache::lookup", skip_all)]
173 pub fn lookup(&self, sp: SourcePosition) -> Option<SourceLocation<'_>> {
174 let idx = match self.min_source_positions.binary_search(&sp.into()) {
175 Ok(idx) => idx,
176 Err(0) => return None,
177 Err(idx) => idx - 1,
178 };
179
180 if self.min_source_positions.get(idx)?.line < sp.line {
184 return None;
185 }
186
187 let sl = self.orig_source_locations.get(idx)?;
188
189 if sl.file_idx == raw::NO_FILE_SENTINEL && sl.line == u32::MAX && sl.column == u32::MAX {
191 return None;
192 }
193
194 let line = sl.line;
195 let column = sl.column;
196
197 let file = self
198 .files
199 .get(sl.file_idx as usize)
200 .and_then(|raw_file| self.resolve_file(raw_file));
201
202 let name = match sl.name_idx {
203 raw::NO_NAME_SENTINEL => None,
204 idx => self.get_string(idx),
205 };
206
207 let scope = match sl.scope_idx {
208 raw::GLOBAL_SCOPE_SENTINEL => ScopeLookupResult::Unknown,
209 raw::ANONYMOUS_SCOPE_SENTINEL => ScopeLookupResult::AnonymousScope,
210 idx => self
211 .get_string(idx)
212 .map_or(ScopeLookupResult::Unknown, ScopeLookupResult::NamedScope),
213 };
214
215 Some(SourceLocation {
216 file,
217 line,
218 column,
219 name,
220 scope,
221 })
222 }
223
224 pub fn files(&'data self) -> Files<'data> {
226 Files::new(self)
227 }
228}
229
230#[derive(thiserror::Error, Debug)]
232#[non_exhaustive]
233pub enum Error {
234 #[error("endianness mismatch")]
236 WrongEndianness,
237 #[error("wrong format magic")]
239 WrongFormat,
240 #[error("unknown SymCache version")]
242 WrongVersion,
243 #[error("invalid header")]
245 Header,
246 #[error("invalid source positions")]
248 SourcePositions,
249 #[error("invalid source locations")]
251 SourceLocations,
252 #[error("invalid string bytes")]
254 StringBytes,
255 #[error("invalid files")]
257 Files,
258 #[error("invalid line offsets")]
260 LineOffsets,
261}
262
263#[derive(Debug, PartialEq, Eq, Clone, Copy)]
265pub struct File<'data> {
266 name: Option<&'data str>,
267 source: Option<&'data str>,
268 line_offsets: &'data [raw::LineOffset],
269}
270
271impl<'data> File<'data> {
272 pub fn name(&self) -> Option<&'data str> {
274 self.name
275 }
276
277 pub fn source(&self) -> Option<&'data str> {
279 self.source
280 }
281
282 pub fn line(&self, line_no: usize) -> Option<&'data str> {
284 let source = self.source?;
285 let from = self.line_offsets.get(line_no).copied()?.0 as usize;
286 let next_line_no = line_no.checked_add(1);
287 let to = next_line_no
288 .and_then(|next_line_no| self.line_offsets.get(next_line_no))
289 .map_or(source.len(), |lo| lo.0 as usize);
290 source.get(from..to)
291 }
292}
293
294pub struct Files<'data> {
296 cache: &'data SourceMapCache<'data>,
297 raw_files: std::slice::Iter<'data, raw::File>,
298}
299
300impl<'data> Files<'data> {
301 fn new(cache: &'data SourceMapCache<'data>) -> Self {
302 let raw_files = cache.files.iter();
303 Self { cache, raw_files }
304 }
305}
306
307impl<'data> Iterator for Files<'data> {
308 type Item = File<'data>;
309
310 fn next(&mut self) -> Option<Self::Item> {
311 self.raw_files
312 .next()
313 .and_then(|raw_file| self.cache.resolve_file(raw_file))
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use crate::SourceMapCacheWriter;
321
322 #[test]
323 fn lines_empty_file() {
324 let source = "";
325 let mut line_offsets = Vec::new();
326 SourceMapCacheWriter::append_line_offsets(source, &mut line_offsets);
327
328 let file = File {
329 name: None,
330 source: Some(source),
331 line_offsets: &line_offsets,
332 };
333
334 assert_eq!(file.line(0), Some(""));
335 assert_eq!(file.line(1), None);
336 }
337
338 #[test]
339 fn lines_almost_empty_file() {
340 let source = "\n";
341 let mut line_offsets = Vec::new();
342 SourceMapCacheWriter::append_line_offsets(source, &mut line_offsets);
343
344 let file = File {
345 name: None,
346 source: Some(source),
347 line_offsets: &line_offsets,
348 };
349
350 assert_eq!(file.line(0), Some("\n"));
351 assert_eq!(file.line(1), Some(""));
352 assert_eq!(file.line(2), None);
353 }
354
355 #[test]
356 fn lines_several_lines() {
357 let source = "a\n\nb\nc";
358 let mut line_offsets = Vec::new();
359 SourceMapCacheWriter::append_line_offsets(source, &mut line_offsets);
360
361 let file = File {
362 name: None,
363 source: Some(source),
364 line_offsets: &line_offsets,
365 };
366
367 assert_eq!(file.line(0), Some("a\n"));
368 assert_eq!(file.line(1), Some("\n"));
369 assert_eq!(file.line(2), Some("b\n"));
370 assert_eq!(file.line(3), Some("c"));
371 }
372
373 #[test]
374 fn lines_several_lines_trailing_newline() {
375 let source = "a\n\nb\nc\n";
376 let mut line_offsets = Vec::new();
377 SourceMapCacheWriter::append_line_offsets(source, &mut line_offsets);
378
379 let file = File {
380 name: None,
381 source: Some(source),
382 line_offsets: &line_offsets,
383 };
384
385 assert_eq!(file.line(0), Some("a\n"));
386 assert_eq!(file.line(1), Some("\n"));
387 assert_eq!(file.line(2), Some("b\n"));
388 assert_eq!(file.line(3), Some("c\n"));
389 assert_eq!(file.line(4), Some(""));
390 }
391
392 #[test]
393 fn unmapped_token() {
394 let minified = r#""foo"; /*added by bundler*/ "bar";"#;
395 let sourcemap = r#"{"version":3,"file":"test.min.js","sources":["test.js"],"sourcesContent":["\"foo\":\n\"baz\";"],"names":[],"mappings":"AAAA,M,sBACA"}"#;
396
397 let mut buf = vec![];
398 SourceMapCacheWriter::new(minified, sourcemap)
399 .unwrap()
400 .serialize(&mut buf)
401 .unwrap();
402
403 let cache = SourceMapCache::parse(&buf).unwrap();
404
405 let foo = cache.lookup(SourcePosition { line: 0, column: 4 }).unwrap();
407 assert_eq!(foo.file_name().unwrap(), "test.js");
408 assert_eq!(foo.line, 0);
409 assert_eq!(foo.column, 0);
410
411 assert!(dbg!(cache.lookup(SourcePosition {
414 line: 0,
415 column: 17
416 }))
417 .is_none());
418
419 let bar = cache
421 .lookup(SourcePosition {
422 line: 0,
423 column: 30,
424 })
425 .unwrap();
426 assert_eq!(bar.file_name().unwrap(), "test.js");
427 assert_eq!(bar.line, 1);
428 assert_eq!(bar.column, 0);
429 }
430}