symbolic_sourcemapcache/
lookup.rs

1use symbolic_common::AsSelf;
2use watto::{align_to, Pod, StringTable};
3
4use crate::{ScopeLookupResult, SourcePosition};
5
6use super::raw;
7
8/// A resolved Source Location with file, line, column and scope information.
9#[derive(Debug, PartialEq)]
10pub struct SourceLocation<'data> {
11    /// The source file this location belongs to.
12    file: Option<File<'data>>,
13    /// The source line.
14    line: u32,
15    /// The source column.
16    column: u32,
17    /// The `name` of the source location.
18    name: Option<&'data str>,
19    /// The scope containing this source location.
20    scope: ScopeLookupResult<'data>,
21}
22
23impl<'data> SourceLocation<'data> {
24    /// The source file this location belongs to.
25    pub fn file(&self) -> Option<File<'data>> {
26        self.file
27    }
28
29    /// The number of the source line.
30    pub fn line(&self) -> u32 {
31        self.line
32    }
33
34    /// The number of the source column.
35    pub fn column(&self) -> u32 {
36        self.column
37    }
38
39    /// The `name` of this source location as it is defined in the SourceMap.
40    ///
41    /// This can be useful when inferring the name of a scope from the callers call expression.
42    pub fn name(&self) -> Option<&'data str> {
43        self.name
44    }
45
46    /// The contents of the source line.
47    pub fn line_contents(&self) -> Option<&'data str> {
48        self.file().and_then(|file| file.line(self.line as usize))
49    }
50
51    /// The scope containing this source location.
52    pub fn scope(&self) -> ScopeLookupResult<'data> {
53        self.scope
54    }
55
56    /// The name of the source file this location belongs to.
57    pub fn file_name(&self) -> Option<&'data str> {
58        self.file.and_then(|file| file.name)
59    }
60
61    /// The source of the file this location belongs to.
62    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/// A cached SourceMap lookup index.
70///
71/// This allows quick lookup inside SourceMaps via the [`lookup`](Self::lookup) method.
72#[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    /// Parses a raw buffer containing a serialized [`SourceMapCache`].
104    #[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    /// Resolves a string reference to the pointed-to `&str` data.
153    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    /// Looks up a [`SourcePosition`] in the minified source and resolves it
171    /// to the original [`SourceLocation`].
172    #[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 the token has a lower minified line number,
181        // it actually belongs to the previous line. That means it should
182        // not match.
183        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 file, line, and column are all absent (== `u32::MAX`), this location is simply unmapped.
190        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    /// Returns an iterator over all files in the cache.
225    pub fn files(&'data self) -> Files<'data> {
226        Files::new(self)
227    }
228}
229
230/// An Error that can happen when parsing a [`SourceMapCache`].
231#[derive(thiserror::Error, Debug)]
232#[non_exhaustive]
233pub enum Error {
234    /// The file was generated by a system with different endianness.
235    #[error("endianness mismatch")]
236    WrongEndianness,
237    /// The file magic does not match.
238    #[error("wrong format magic")]
239    WrongFormat,
240    /// The format version in the header is wrong/unknown.
241    #[error("unknown SymCache version")]
242    WrongVersion,
243    /// The buffer has an invalid header.
244    #[error("invalid header")]
245    Header,
246    /// The buffer has invalid source positions.
247    #[error("invalid source positions")]
248    SourcePositions,
249    /// The buffer has invalid source locations.
250    #[error("invalid source locations")]
251    SourceLocations,
252    /// The buffer has an invalid string table.
253    #[error("invalid string bytes")]
254    StringBytes,
255    /// The buffer has invalid files.
256    #[error("invalid files")]
257    Files,
258    /// The buffer has invalid line offsets.
259    #[error("invalid line offsets")]
260    LineOffsets,
261}
262
263/// An original source file embedded in a [`SourceMapCache`].
264#[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    /// Returns the name of this file.
273    pub fn name(&self) -> Option<&'data str> {
274        self.name
275    }
276
277    /// Returns the source of this file.
278    pub fn source(&self) -> Option<&'data str> {
279        self.source
280    }
281
282    /// Returns the requested source line if possible.
283    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
294/// Iterator returned by [`SourceMapCache::files`].
295pub 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        // "foo";
406        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        // comment
412        // this should be unmapped
413        assert!(dbg!(cache.lookup(SourcePosition {
414            line: 0,
415            column: 17
416        }))
417        .is_none());
418
419        // "bar";
420        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}