use std::collections::HashMap;
use std::path::{Path, PathBuf};
use sourcemap::SourceMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SourceMapError {
#[error("Failed to read source map file: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse source map: {0}")]
Parse(#[from] sourcemap::Error),
#[error("Source map not found for: {0}")]
NotFound(String),
}
#[derive(Debug, Clone)]
pub struct ResolvedLocation {
pub name: Option<String>,
pub file: String,
pub line: u32,
pub col: u32,
}
pub struct SourceMapResolver {
sourcemap_dirs: Vec<PathBuf>,
cache: HashMap<String, Option<SourceMap>>,
}
impl SourceMapResolver {
pub fn new(sourcemap_dirs: Vec<PathBuf>) -> Self {
Self {
sourcemap_dirs,
cache: HashMap::new(),
}
}
pub fn resolve(&mut self, url: &str, line: u32, col: u32) -> Option<ResolvedLocation> {
let sourcemap = self.load_sourcemap(url)?;
let line_0 = line.saturating_sub(1);
let col_0 = col.saturating_sub(1);
let token = sourcemap.lookup_token(line_0, col_0)?;
let src = token.get_source()?;
let src_line = token.get_src_line();
let src_col = token.get_src_col();
Some(ResolvedLocation {
name: token.get_name().map(String::from),
file: src.to_string(),
line: src_line + 1, col: src_col + 1,
})
}
fn load_sourcemap(&mut self, url: &str) -> Option<&SourceMap> {
if self.cache.contains_key(url) {
return self.cache.get(url)?.as_ref();
}
let sourcemap = self.find_and_load_sourcemap(url);
self.cache.insert(url.to_string(), sourcemap);
self.cache.get(url)?.as_ref()
}
fn find_and_load_sourcemap(&self, url: &str) -> Option<SourceMap> {
let filename = Self::extract_filename(url)?;
let patterns = [
format!("{filename}.map"),
filename.replace(".js", ".js.map"),
filename.replace(".mjs", ".mjs.map"),
];
for dir in &self.sourcemap_dirs {
for pattern in &patterns {
let map_path = dir.join(pattern);
if map_path.exists() {
if let Ok(content) = std::fs::read_to_string(&map_path) {
if let Ok(sm) = SourceMap::from_slice(content.as_bytes()) {
return Some(sm);
}
}
}
}
}
self.try_inline_sourcemap(url)
}
fn extract_filename(url: &str) -> Option<String> {
let path = url.strip_prefix("file://").unwrap_or(url);
let path = path.split("://").last().unwrap_or(path);
Path::new(path)
.file_name()
.map(|s| s.to_string_lossy().to_string())
}
fn try_inline_sourcemap(&self, url: &str) -> Option<SourceMap> {
let path = url.strip_prefix("file://").unwrap_or(url);
let content = std::fs::read_to_string(path).ok()?;
let marker = "//# sourceMappingURL=data:application/json;base64,";
if let Some(idx) = content.find(marker) {
let base64_start = idx + marker.len();
let base64_end = content[base64_start..]
.find('\n')
.map_or(content.len(), |i| base64_start + i);
let base64_data = content[base64_start..base64_end].trim();
if let Ok(decoded) = Self::decode_base64(base64_data) {
if let Ok(sm) = SourceMap::from_slice(&decoded) {
return Some(sm);
}
}
}
None
}
fn decode_base64(input: &str) -> Result<Vec<u8>, ()> {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn char_to_val(c: u8) -> Option<u8> {
ALPHABET.iter().position(|&x| x == c).map(|v| v as u8)
}
let input = input.as_bytes();
let mut output = Vec::with_capacity(input.len() * 3 / 4);
let mut buf = 0u32;
let mut bits = 0;
for &c in input {
if c == b'=' {
break;
}
let val = char_to_val(c).ok_or(())?;
buf = (buf << 6) | u32::from(val);
bits += 6;
if bits >= 8 {
bits -= 8;
output.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
}
Ok(output)
}
}
impl Default for SourceMapResolver {
fn default() -> Self {
Self::new(vec![])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_filename() {
assert_eq!(
SourceMapResolver::extract_filename("file:///path/to/file.js"),
Some("file.js".to_string())
);
assert_eq!(
SourceMapResolver::extract_filename("/path/to/bundle.min.js"),
Some("bundle.min.js".to_string())
);
assert_eq!(
SourceMapResolver::extract_filename("webpack://project/src/index.ts"),
Some("index.ts".to_string())
);
}
#[test]
fn test_base64_decode() {
let result = SourceMapResolver::decode_base64("SGVsbG8gV29ybGQ=");
assert_eq!(result, Ok(b"Hello World".to_vec()));
}
}