profile_inspect/sourcemap/
resolver.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use sourcemap::SourceMap;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
9pub enum SourceMapError {
10 #[error("Failed to read source map file: {0}")]
11 Io(#[from] std::io::Error),
12
13 #[error("Failed to parse source map: {0}")]
14 Parse(#[from] sourcemap::Error),
15
16 #[error("Source map not found for: {0}")]
17 NotFound(String),
18}
19
20#[derive(Debug, Clone)]
22pub struct ResolvedLocation {
23 pub name: Option<String>,
25 pub file: String,
27 pub line: u32,
29 pub col: u32,
31}
32
33pub struct SourceMapResolver {
35 sourcemap_dirs: Vec<PathBuf>,
37 cache: HashMap<String, Option<SourceMap>>,
39}
40
41impl SourceMapResolver {
42 pub fn new(sourcemap_dirs: Vec<PathBuf>) -> Self {
44 Self {
45 sourcemap_dirs,
46 cache: HashMap::new(),
47 }
48 }
49
50 pub fn resolve(&mut self, url: &str, line: u32, col: u32) -> Option<ResolvedLocation> {
54 let sourcemap = self.load_sourcemap(url)?;
56
57 let line_0 = line.saturating_sub(1);
59 let col_0 = col.saturating_sub(1);
60
61 let token = sourcemap.lookup_token(line_0, col_0)?;
63
64 let src = token.get_source()?;
66 let src_line = token.get_src_line();
67 let src_col = token.get_src_col();
68
69 Some(ResolvedLocation {
70 name: token.get_name().map(String::from),
71 file: src.to_string(),
72 line: src_line + 1, col: src_col + 1,
74 })
75 }
76
77 fn load_sourcemap(&mut self, url: &str) -> Option<&SourceMap> {
79 if self.cache.contains_key(url) {
81 return self.cache.get(url)?.as_ref();
82 }
83
84 let sourcemap = self.find_and_load_sourcemap(url);
86 self.cache.insert(url.to_string(), sourcemap);
87 self.cache.get(url)?.as_ref()
88 }
89
90 fn find_and_load_sourcemap(&self, url: &str) -> Option<SourceMap> {
92 let filename = Self::extract_filename(url)?;
94
95 let patterns = [
97 format!("{filename}.map"),
98 filename.replace(".js", ".js.map"),
99 filename.replace(".mjs", ".mjs.map"),
100 ];
101
102 for dir in &self.sourcemap_dirs {
103 for pattern in &patterns {
104 let map_path = dir.join(pattern);
105 if map_path.exists() {
106 if let Ok(content) = std::fs::read_to_string(&map_path) {
107 if let Ok(sm) = SourceMap::from_slice(content.as_bytes()) {
108 return Some(sm);
109 }
110 }
111 }
112 }
113 }
114
115 self.try_inline_sourcemap(url)
117 }
118
119 fn extract_filename(url: &str) -> Option<String> {
121 let path = url.strip_prefix("file://").unwrap_or(url);
123
124 let path = path.split("://").last().unwrap_or(path);
126
127 Path::new(path)
129 .file_name()
130 .map(|s| s.to_string_lossy().to_string())
131 }
132
133 fn try_inline_sourcemap(&self, url: &str) -> Option<SourceMap> {
135 let path = url.strip_prefix("file://").unwrap_or(url);
137 let content = std::fs::read_to_string(path).ok()?;
138
139 let marker = "//# sourceMappingURL=data:application/json;base64,";
142 if let Some(idx) = content.find(marker) {
143 let base64_start = idx + marker.len();
144 let base64_end = content[base64_start..]
145 .find('\n')
146 .map_or(content.len(), |i| base64_start + i);
147 let base64_data = content[base64_start..base64_end].trim();
148
149 if let Ok(decoded) = Self::decode_base64(base64_data) {
152 if let Ok(sm) = SourceMap::from_slice(&decoded) {
153 return Some(sm);
154 }
155 }
156 }
157
158 None
159 }
160
161 fn decode_base64(input: &str) -> Result<Vec<u8>, ()> {
163 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
164
165 fn char_to_val(c: u8) -> Option<u8> {
166 ALPHABET.iter().position(|&x| x == c).map(|v| v as u8)
167 }
168
169 let input = input.as_bytes();
170 let mut output = Vec::with_capacity(input.len() * 3 / 4);
171 let mut buf = 0u32;
172 let mut bits = 0;
173
174 for &c in input {
175 if c == b'=' {
176 break;
177 }
178 let val = char_to_val(c).ok_or(())?;
179 buf = (buf << 6) | u32::from(val);
180 bits += 6;
181
182 if bits >= 8 {
183 bits -= 8;
184 output.push((buf >> bits) as u8);
185 buf &= (1 << bits) - 1;
186 }
187 }
188
189 Ok(output)
190 }
191}
192
193impl Default for SourceMapResolver {
194 fn default() -> Self {
195 Self::new(vec![])
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_extract_filename() {
205 assert_eq!(
206 SourceMapResolver::extract_filename("file:///path/to/file.js"),
207 Some("file.js".to_string())
208 );
209 assert_eq!(
210 SourceMapResolver::extract_filename("/path/to/bundle.min.js"),
211 Some("bundle.min.js".to_string())
212 );
213 assert_eq!(
214 SourceMapResolver::extract_filename("webpack://project/src/index.ts"),
215 Some("index.ts".to_string())
216 );
217 }
218
219 #[test]
220 fn test_base64_decode() {
221 let result = SourceMapResolver::decode_base64("SGVsbG8gV29ybGQ=");
222 assert_eq!(result, Ok(b"Hello World".to_vec()));
223 }
224}