profile-inspect 0.1.2

Analyze V8 CPU and heap profiles from Node.js/Chrome DevTools
Documentation
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use sourcemap::SourceMap;
use thiserror::Error;

/// Errors that can occur during source map resolution
#[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),
}

/// Resolved source location
#[derive(Debug, Clone)]
pub struct ResolvedLocation {
    /// Original function name (if available)
    pub name: Option<String>,
    /// Original source file
    pub file: String,
    /// Original line number (1-based)
    pub line: u32,
    /// Original column number (1-based)
    pub col: u32,
}

/// Resolves minified locations to original source locations using source maps
pub struct SourceMapResolver {
    /// Directory containing source map files
    sourcemap_dirs: Vec<PathBuf>,
    /// Cached source maps by file URL
    cache: HashMap<String, Option<SourceMap>>,
}

impl SourceMapResolver {
    /// Create a new resolver with source map directories
    pub fn new(sourcemap_dirs: Vec<PathBuf>) -> Self {
        Self {
            sourcemap_dirs,
            cache: HashMap::new(),
        }
    }

    /// Try to resolve a minified location to the original source
    ///
    /// Returns None if no source map is found or the location cannot be resolved.
    pub fn resolve(&mut self, url: &str, line: u32, col: u32) -> Option<ResolvedLocation> {
        // Load source map for this URL
        let sourcemap = self.load_sourcemap(url)?;

        // Source maps use 0-based line/column numbers
        let line_0 = line.saturating_sub(1);
        let col_0 = col.saturating_sub(1);

        // Look up the token at this location
        let token = sourcemap.lookup_token(line_0, col_0)?;

        // Get the original position
        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, // Convert back to 1-based
            col: src_col + 1,
        })
    }

    /// Load a source map for the given URL
    fn load_sourcemap(&mut self, url: &str) -> Option<&SourceMap> {
        // Check cache first
        if self.cache.contains_key(url) {
            return self.cache.get(url)?.as_ref();
        }

        // Try to find the source map file
        let sourcemap = self.find_and_load_sourcemap(url);
        self.cache.insert(url.to_string(), sourcemap);
        self.cache.get(url)?.as_ref()
    }

    /// Find and load a source map for the given URL
    fn find_and_load_sourcemap(&self, url: &str) -> Option<SourceMap> {
        // Extract filename from URL
        let filename = Self::extract_filename(url)?;

        // Try common source map naming patterns
        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);
                        }
                    }
                }
            }
        }

        // Try looking for inline source map in the original file
        self.try_inline_sourcemap(url)
    }

    /// Extract filename from a URL or path
    fn extract_filename(url: &str) -> Option<String> {
        // Handle file:// URLs
        let path = url.strip_prefix("file://").unwrap_or(url);

        // Handle webpack:// and similar URLs
        let path = path.split("://").last().unwrap_or(path);

        // Get the filename
        Path::new(path)
            .file_name()
            .map(|s| s.to_string_lossy().to_string())
    }

    /// Try to load an inline source map from the file
    fn try_inline_sourcemap(&self, url: &str) -> Option<SourceMap> {
        // For now, just try to read the file and look for inline map
        let path = url.strip_prefix("file://").unwrap_or(url);
        let content = std::fs::read_to_string(path).ok()?;

        // Look for inline source map
        // Format: //# sourceMappingURL=data:application/json;base64,...
        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();

            // Decode base64
            // Use a simple base64 decode (or rely on sourcemap crate)
            if let Ok(decoded) = Self::decode_base64(base64_data) {
                if let Ok(sm) = SourceMap::from_slice(&decoded) {
                    return Some(sm);
                }
            }
        }

        None
    }

    /// Simple base64 decode
    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()));
    }
}