Skip to main content

profile_inspect/sourcemap/
resolver.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use sourcemap::SourceMap;
5use thiserror::Error;
6
7/// Errors that can occur during source map resolution
8#[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/// Resolved source location
21#[derive(Debug, Clone)]
22pub struct ResolvedLocation {
23    /// Original function name (if available)
24    pub name: Option<String>,
25    /// Original source file
26    pub file: String,
27    /// Original line number (1-based)
28    pub line: u32,
29    /// Original column number (1-based)
30    pub col: u32,
31}
32
33/// Resolves minified locations to original source locations using source maps
34pub struct SourceMapResolver {
35    /// Directory containing source map files
36    sourcemap_dirs: Vec<PathBuf>,
37    /// Cached source maps by file URL
38    cache: HashMap<String, Option<SourceMap>>,
39}
40
41impl SourceMapResolver {
42    /// Create a new resolver with source map directories
43    pub fn new(sourcemap_dirs: Vec<PathBuf>) -> Self {
44        Self {
45            sourcemap_dirs,
46            cache: HashMap::new(),
47        }
48    }
49
50    /// Try to resolve a minified location to the original source
51    ///
52    /// Returns None if no source map is found or the location cannot be resolved.
53    pub fn resolve(&mut self, url: &str, line: u32, col: u32) -> Option<ResolvedLocation> {
54        // Load source map for this URL
55        let sourcemap = self.load_sourcemap(url)?;
56
57        // Source maps use 0-based line/column numbers
58        let line_0 = line.saturating_sub(1);
59        let col_0 = col.saturating_sub(1);
60
61        // Look up the token at this location
62        let token = sourcemap.lookup_token(line_0, col_0)?;
63
64        // Get the original position
65        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, // Convert back to 1-based
73            col: src_col + 1,
74        })
75    }
76
77    /// Load a source map for the given URL
78    fn load_sourcemap(&mut self, url: &str) -> Option<&SourceMap> {
79        // Check cache first
80        if self.cache.contains_key(url) {
81            return self.cache.get(url)?.as_ref();
82        }
83
84        // Try to find the source map file
85        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    /// Find and load a source map for the given URL
91    fn find_and_load_sourcemap(&self, url: &str) -> Option<SourceMap> {
92        // Extract filename from URL
93        let filename = Self::extract_filename(url)?;
94
95        // Try common source map naming patterns
96        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        // Try looking for inline source map in the original file
116        self.try_inline_sourcemap(url)
117    }
118
119    /// Extract filename from a URL or path
120    fn extract_filename(url: &str) -> Option<String> {
121        // Handle file:// URLs
122        let path = url.strip_prefix("file://").unwrap_or(url);
123
124        // Handle webpack:// and similar URLs
125        let path = path.split("://").last().unwrap_or(path);
126
127        // Get the filename
128        Path::new(path)
129            .file_name()
130            .map(|s| s.to_string_lossy().to_string())
131    }
132
133    /// Try to load an inline source map from the file
134    fn try_inline_sourcemap(&self, url: &str) -> Option<SourceMap> {
135        // For now, just try to read the file and look for inline map
136        let path = url.strip_prefix("file://").unwrap_or(url);
137        let content = std::fs::read_to_string(path).ok()?;
138
139        // Look for inline source map
140        // Format: //# sourceMappingURL=data:application/json;base64,...
141        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            // Decode base64
150            // Use a simple base64 decode (or rely on sourcemap crate)
151            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    /// Simple base64 decode
162    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}