source-map-tauri 0.3.0

Static Tauri app scanner that emits Meilisearch-ready NDJSON
Documentation
use std::path::Path;

use regex::Regex;
use serde_json::{Map, Value};

use crate::{
    config::{normalize_path, ResolvedConfig},
    ids::document_id,
    model::{ArtifactDoc, WarningDoc},
    security::apply_artifact_security,
};

fn line_number(text: &str, offset: usize) -> u32 {
    text[..offset].bytes().filter(|byte| *byte == b'\n').count() as u32 + 1
}

fn new_artifact(
    config: &ResolvedConfig,
    path: &Path,
    kind: &str,
    name: &str,
    line: u32,
) -> ArtifactDoc {
    let source_path = normalize_path(&config.root, path);
    ArtifactDoc {
        id: document_id(
            &config.repo,
            kind,
            Some(&source_path),
            Some(line),
            Some(name),
        ),
        repo: config.repo.clone(),
        kind: kind.to_owned(),
        side: Some("frontend".to_owned()),
        language: crate::frontend::language_for_path(path),
        name: Some(name.to_owned()),
        display_name: Some(name.to_owned()),
        source_path: Some(source_path),
        line_start: Some(line),
        line_end: Some(line),
        column_start: None,
        column_end: None,
        package_name: None,
        comments: Vec::new(),
        tags: Vec::new(),
        related_symbols: Vec::new(),
        related_tests: Vec::new(),
        risk_level: "low".to_owned(),
        risk_reasons: Vec::new(),
        contains_phi: false,
        has_related_tests: false,
        updated_at: chrono::Utc::now().to_rfc3339(),
        data: Map::new(),
    }
}

fn warning(
    config: &ResolvedConfig,
    path: &Path,
    warning_type: &str,
    message: &str,
    line: u32,
) -> WarningDoc {
    let source_path = normalize_path(&config.root, path);
    WarningDoc {
        id: document_id(
            &config.repo,
            "warning",
            Some(&source_path),
            Some(line),
            Some(warning_type),
        ),
        repo: config.repo.clone(),
        kind: "warning".to_owned(),
        warning_type: warning_type.to_owned(),
        severity: "warning".to_owned(),
        message: message.to_owned(),
        source_path: Some(source_path),
        line_start: Some(line),
        related_id: None,
        risk_level: "medium".to_owned(),
        remediation: None,
        updated_at: chrono::Utc::now().to_rfc3339(),
    }
}

pub fn extract_calls(
    config: &ResolvedConfig,
    path: &Path,
    text: &str,
    guest_binding: bool,
) -> (Vec<ArtifactDoc>, Vec<WarningDoc>) {
    let invoke_re = Regex::new(
        r#"(?:\binvoke|\btauri\.invoke|\bwindow\.__TAURI__\.invoke)\(\s*['"]([^'"]+)['"]"#,
    )
    .expect("valid regex");
    let event_re = Regex::new(r#"\b(listen|once|emit)\(\s*['"]([^'"]+)['"]"#).expect("valid regex");
    let channel_re =
        Regex::new(r"\b(?:const|let)\s+([A-Za-z0-9_]+)\s*=\s*new\s+Channel").expect("valid regex");
    let export_fn =
        Regex::new(r"(?m)^\s*export\s+async\s+function\s+([A-Za-z0-9_]+)").expect("valid regex");

    let mut artifacts = Vec::new();
    let mut warnings = Vec::new();

    for capture in invoke_re.captures_iter(text) {
        let whole = capture.get(0).expect("match");
        let invoke_key = capture.get(1).expect("key").as_str();
        let line = line_number(text, whole.start());
        let name = invoke_key
            .split('|')
            .next_back()
            .unwrap_or(invoke_key)
            .split(':')
            .next_back()
            .unwrap_or(invoke_key);
        let mut doc = new_artifact(config, path, "tauri_invoke", name, line);
        doc.display_name = Some(format!("invoke {invoke_key}"));
        doc.tags = vec!["tauri invoke".to_owned()];
        doc.data.insert(
            "invoke_key".to_owned(),
            Value::String(invoke_key.to_owned()),
        );
        doc.data
            .insert("command_name".to_owned(), Value::String(name.to_owned()));
        if let Some(plugin_name) = invoke_key
            .strip_prefix("plugin:")
            .and_then(|value| value.split('|').next())
        {
            doc.data.insert(
                "plugin_name".to_owned(),
                Value::String(plugin_name.to_owned()),
            );
        }
        apply_artifact_security(&mut doc);
        artifacts.push(doc);
    }

    for capture in channel_re.captures_iter(text) {
        let whole = capture.get(0).expect("match");
        let channel_name = capture.get(1).expect("name").as_str();
        let line = line_number(text, whole.start());
        let mut doc = new_artifact(config, path, "tauri_channel", channel_name, line);
        doc.display_name = Some(format!("Channel {channel_name}"));
        doc.data.insert(
            "channel_name".to_owned(),
            Value::String(channel_name.to_owned()),
        );
        apply_artifact_security(&mut doc);
        artifacts.push(doc);
    }

    for capture in event_re.captures_iter(text) {
        let whole = capture.get(0).expect("match");
        let verb = capture.get(1).expect("verb").as_str();
        let event_name = capture.get(2).expect("event").as_str();
        let line = line_number(text, whole.start());
        let kind = match verb {
            "emit" => "tauri_event_emit",
            _ => "tauri_event_listener",
        };
        let mut doc = new_artifact(config, path, kind, event_name, line);
        doc.data.insert(
            "event_name".to_owned(),
            Value::String(event_name.to_owned()),
        );
        doc.tags = vec!["event".to_owned()];
        apply_artifact_security(&mut doc);
        artifacts.push(doc);
    }

    if guest_binding {
        for capture in export_fn.captures_iter(text) {
            let whole = capture.get(0).expect("match");
            let export_name = capture.get(1).expect("name").as_str();
            let line = line_number(text, whole.start());
            let mut doc = new_artifact(config, path, "tauri_plugin_binding", export_name, line);
            doc.display_name = Some(format!("{export_name} plugin binding"));
            doc.data.insert(
                "plugin_export".to_owned(),
                Value::String(export_name.to_owned()),
            );
            if let Some(call) = invoke_re.captures(text) {
                let invoke_key = call.get(1).expect("invoke key").as_str();
                doc.data.insert(
                    "invoke_key".to_owned(),
                    Value::String(invoke_key.to_owned()),
                );
                if let Some(plugin_name) = invoke_key
                    .strip_prefix("plugin:")
                    .and_then(|value| value.split('|').next())
                {
                    doc.data.insert(
                        "plugin_name".to_owned(),
                        Value::String(plugin_name.to_owned()),
                    );
                }
            }
            apply_artifact_security(&mut doc);
            artifacts.push(doc);
        }
    }

    let dynamic_invoke = Regex::new(
        r"\b(?:invoke|tauri\.invoke|window\.__TAURI__\.invoke)\(\s*([A-Za-z_][A-Za-z0-9_]*)",
    )
    .expect("valid regex");
    for capture in dynamic_invoke.captures_iter(text) {
        let variable = capture.get(1).expect("name").as_str();
        let line = line_number(text, capture.get(0).expect("match").start());
        warnings.push(warning(
            config,
            path,
            "dynamic_invoke",
            &format!("Cannot statically resolve Tauri command name from {variable}"),
            line,
        ));
    }

    (artifacts, warnings)
}