diff --git a/rapx/src/analysis/senryx/visitor.rs b/rapx/src/analysis/senryx/visitor.rs index 018a77a0..202d748f 100644 --- a/rapx/src/analysis/senryx/visitor.rs +++ b/rapx/src/analysis/senryx/visitor.rs @@ -16,7 +16,11 @@ use crate::{ dominated_graph::FunctionSummary, symbolic_analysis::{AnaOperand, SymbolicDef, ValueDomain}, }, - utils::{draw_dot::render_dot_string, fn_info::*, show_mir::display_mir}, + utils::{ + draw_dot::{DotGraph, render_dot_string}, + fn_info::*, + show_mir::display_mir, + }, }, rap_debug, rap_warn, }; @@ -1715,6 +1719,7 @@ impl<'tcx> BodyVisitor<'tcx> { let name = format!("{}_path_{}", base_name, path_suffix); let dot_string = self.chains.to_dot_graph(); - render_dot_string(name, dot_string); + let dot_graph = DotGraph::new(name, dot_string); + render_dot_string(&dot_graph); } } diff --git a/rapx/src/analysis/upg/draw_dot.rs b/rapx/src/analysis/upg/draw_dot.rs deleted file mode 100644 index 4344c704..00000000 --- a/rapx/src/analysis/upg/draw_dot.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::fs::{File, remove_file}; -use std::io::Write; -use std::process::Command; - -// please ensure 'graphviz' has been installed -pub fn render_dot_graphs(dot_graphs: Vec<(String, String)>) { - Command::new("mkdir") - .args(["UPG"]) - .output() - .expect("Failed to create directory"); - - for (_index, dot) in dot_graphs.into_iter().enumerate() { - let file_name = format!("{}.dot", dot.0); - let mut file = File::create(&file_name).expect("Unable to create file"); - file.write_all(dot.1.as_bytes()) - .expect("Unable to write data"); - - Command::new("dot") - .args(["-Tpng", &file_name, "-o", &format!("UPG/{}.png", dot.0)]) - .output() - .expect("Failed to execute Graphviz dot command"); - - remove_file(&file_name).expect("Failed to delete .dot file"); - } -} - -pub fn render_dot_string(name: String, dot_graph: String) { - let file_name = format!("{}.dot", name); - let mut file = File::create(&file_name).expect("Unable to create file"); - file.write_all(dot_graph.as_bytes()) - .expect("Unable to write data"); - - Command::new("dot") - .args([ - "-Tpng", - &file_name, - "-o", - &format!("RAPx_bugs/{}.png", name), - ]) - .output() - .expect("Failed to execute Graphviz dot command"); - - remove_file(&file_name).expect("Failed to delete .dot file"); -} diff --git a/rapx/src/analysis/upg/mod.rs b/rapx/src/analysis/upg/mod.rs index bda9dd40..fad04d2b 100644 --- a/rapx/src/analysis/upg/mod.rs +++ b/rapx/src/analysis/upg/mod.rs @@ -1,7 +1,6 @@ /* * This module generates the unsafety propagation graph for each Rust module in the target crate. */ -pub mod draw_dot; pub mod fn_collector; pub mod hir_visitor; pub mod std_upg; @@ -9,8 +8,14 @@ pub mod upg_graph; pub mod upg_unit; use crate::{ - analysis::utils::{draw_dot::render_dot_graphs, fn_info::*}, - utils::source::{get_fn_name_byid, get_module_name}, + analysis::utils::{ + draw_dot::{DotGraph, render_dot_graphs, render_dot_graphs_html}, + fn_info::*, + }, + utils::{ + log::{span_to_filename, span_to_line_number}, + source::{get_fn_name_byid, get_module_name, get_span}, + }, }; use fn_collector::FnCollector; use hir_visitor::ContainsUnsafe; @@ -222,9 +227,27 @@ impl<'tcx> UPGAnalysis<'tcx> { let mut final_dots = Vec::new(); for (mod_name, data) in modules_data { let dot = data.upg_unit_string(&mod_name); - final_dots.push((mod_name, dot)); + let url_map = data + .nodes_def_ids() + .iter() + .filter_map(|def_id| { + get_span(self.tcx, *def_id).map(|span| { + let label = format!("{:?}", *def_id); + let mut filename = span_to_filename(span); + filename = filename + .strip_prefix("src/") + .unwrap_or(&filename) + .to_string(); + let line_number = span_to_line_number(span); + let url = format!("{}.html#{}", filename, line_number); + (label, url) + }) + }) + .collect(); + final_dots.push(DotGraph::new_with_url_map(mod_name, dot, url_map)); } rap_info!("{:?}", final_dots); // Output required for tests; do not change. - render_dot_graphs(final_dots); + render_dot_graphs(&final_dots); + render_dot_graphs_html(&final_dots); } } diff --git a/rapx/src/analysis/upg/std_upg.rs b/rapx/src/analysis/upg/std_upg.rs index 38f3721c..49b8c5d8 100644 --- a/rapx/src/analysis/upg/std_upg.rs +++ b/rapx/src/analysis/upg/std_upg.rs @@ -1,7 +1,8 @@ use super::{UPGAnalysis, upg_graph::UPGraph}; -use crate::analysis::{ - upg::draw_dot::render_dot_graphs, - utils::{fn_info::*, show_mir::display_mir}, +use crate::analysis::utils::{ + draw_dot::{DotGraph, render_dot_graphs}, + fn_info::*, + show_mir::display_mir, }; use rustc_hir::{Safety, def::DefKind, def_id::DefId}; use rustc_middle::{ @@ -28,13 +29,13 @@ impl<'tcx> UPGAnalysis<'tcx> { } pub fn render_dot(&mut self) { - let mut dot_strs = Vec::new(); + let mut dots = Vec::new(); for upg in &self.upgs { let dot_str = UPGraph::generate_dot_from_upg_unit(upg); let caller_name = get_cleaned_def_path_name(self.tcx, upg.caller.def_id); - dot_strs.push((caller_name, dot_str)); + dots.push(DotGraph::new(caller_name, dot_str)); } - render_dot_graphs(dot_strs); + render_dot_graphs(&dots); } pub fn get_chains(&mut self) { diff --git a/rapx/src/analysis/upg/upg_graph.rs b/rapx/src/analysis/upg/upg_graph.rs index 02567340..54a46327 100644 --- a/rapx/src/analysis/upg/upg_graph.rs +++ b/rapx/src/analysis/upg/upg_graph.rs @@ -266,4 +266,8 @@ impl UPGraph { // println!("{}", dot_str); dot_str } + + pub fn nodes_def_ids(&self) -> Vec { + self.nodes.keys().map(|def_id| *def_id).collect() + } } diff --git a/rapx/src/analysis/utils/assets/index.html.template b/rapx/src/analysis/utils/assets/index.html.template new file mode 100644 index 00000000..ab3a8e25 --- /dev/null +++ b/rapx/src/analysis/utils/assets/index.html.template @@ -0,0 +1,63 @@ + + + + + + + + +

+
+ + + diff --git a/rapx/src/analysis/utils/data/std_sig.json b/rapx/src/analysis/utils/assets/std_sig.json similarity index 100% rename from rapx/src/analysis/utils/data/std_sig.json rename to rapx/src/analysis/utils/assets/std_sig.json diff --git a/rapx/src/analysis/utils/data/std_sps.json b/rapx/src/analysis/utils/assets/std_sps.json similarity index 100% rename from rapx/src/analysis/utils/data/std_sps.json rename to rapx/src/analysis/utils/assets/std_sps.json diff --git a/rapx/src/analysis/utils/data/std_sps_args.json b/rapx/src/analysis/utils/assets/std_sps_args.json similarity index 100% rename from rapx/src/analysis/utils/data/std_sps_args.json rename to rapx/src/analysis/utils/assets/std_sps_args.json diff --git a/rapx/src/analysis/utils/draw_dot.rs b/rapx/src/analysis/utils/draw_dot.rs index b98bc9be..a4f9103e 100644 --- a/rapx/src/analysis/utils/draw_dot.rs +++ b/rapx/src/analysis/utils/draw_dot.rs @@ -1,22 +1,59 @@ +use std::collections::HashMap; use std::fs::{File, remove_file}; use std::io::Write; use std::process::Command; +const HTML_TEMPLATE: &str = include_str!("assets/index.html.template"); + +#[derive(Debug)] +pub struct DotGraph { + pub name: String, + pub content: String, + pub url_map: HashMap, // from node label to URL path +} + +impl DotGraph { + pub fn new(name: String, content: String) -> Self { + Self { + name, + content, + url_map: HashMap::new(), + } + } + + pub fn new_with_url_map( + name: String, + content: String, + url_map: HashMap, + ) -> Self { + Self { + name, + content, + url_map, + } + } +} + // please ensure 'graphviz' has been installed -pub fn render_dot_graphs(dot_graphs: Vec<(String, String)>) { +pub fn render_dot_graphs(dot_graphs: &Vec) { Command::new("mkdir") .args(["UPG"]) .output() .expect("Failed to create directory"); - for (_index, dot) in dot_graphs.into_iter().enumerate() { - let file_name = format!("{}.dot", dot.0); + for dot_graph in dot_graphs.iter() { + let file_name = format!("{}.dot", dot_graph.name); let mut file = File::create(&file_name).expect("Unable to create file"); - file.write_all(dot.1.as_bytes()) + file.write_all(dot_graph.content.as_bytes()) .expect("Unable to write data"); Command::new("dot") - .args(["-Tpng", &file_name, "-o", &format!("UPG/{}.png", dot.0)]) + .args([ + "-Tpng", + &file_name, + "-o", + &format!("UPG/{}.png", dot_graph.name), + ]) .output() .expect("Failed to execute Graphviz dot command"); @@ -24,16 +61,38 @@ pub fn render_dot_graphs(dot_graphs: Vec<(String, String)>) { } } -pub fn render_dot_string(name: String, dot_graph: String) { +pub fn render_dot_graphs_html(dot_graphs: &Vec) { + Command::new("mkdir") + .args(["UPG"]) + .output() + .expect("Failed to create directory"); + + for dot_graph in dot_graphs.iter() { + let title = &dot_graph.name; + let dot = &dot_graph.content; + let url_map = serde_json::to_string_pretty(&dot_graph.url_map).unwrap(); + let html = HTML_TEMPLATE + .replace("{{TITLE}}", title) + .replace("{{DOT}}", dot) + .replace("{{URL_MAP}}", &url_map); + + let file_name = format!("UPG/{}.html", dot_graph.name); + let mut file = File::create(&file_name).expect("Unable to create file"); + file.write_all(html.as_bytes()) + .expect("Unable to write data"); + } +} + +pub fn render_dot_string(dot_graph: &DotGraph) { Command::new("mkdir") .args(["MIR_dot_graph"]) .output() .expect("Failed to create directory"); - let file_name = format!("{}.dot", name); + let file_name = format!("{}.dot", dot_graph.name); rap_debug!("render graph {:?}", file_name); let mut file = File::create(&file_name).expect("Unable to create file"); - file.write_all(dot_graph.as_bytes()) + file.write_all(dot_graph.content.as_bytes()) .expect("Unable to write data"); Command::new("dot") @@ -41,7 +100,7 @@ pub fn render_dot_string(name: String, dot_graph: String) { "-Tpng", &file_name, "-o", - &format!("MIR_dot_graph/{}.png", name), + &format!("MIR_dot_graph/{}.png", dot_graph.name), ]) .output() .expect("Failed to execute Graphviz dot command"); diff --git a/rapx/src/analysis/utils/fn_info.rs b/rapx/src/analysis/utils/fn_info.rs index 8628e6a8..5b2b1be5 100644 --- a/rapx/src/analysis/utils/fn_info.rs +++ b/rapx/src/analysis/utils/fn_info.rs @@ -2,9 +2,10 @@ use super::draw_dot::render_dot_string; use crate::analysis::{ core::dataflow::{DataFlowAnalysis, default::DataFlowAnalyzer}, senryx::{ - contracts::{property, property::PropertyContract}, + contracts::property::{self, PropertyContract}, matcher::parse_unsafe_api, }, + utils::draw_dot::DotGraph, }; use crate::def_id::*; use crate::{rap_debug, rap_warn}; @@ -143,19 +144,20 @@ pub fn get_cleaned_def_path_name_ori(tcx: TyCtxt, def_id: DefId) -> String { pub fn get_sp_tags_json() -> serde_json::Value { let json_data: serde_json::Value = - serde_json::from_str(include_str!("data/std_sps.json")).expect("Unable to parse JSON"); + serde_json::from_str(include_str!("assets/std_sps.json")).expect("Unable to parse JSON"); json_data } pub fn get_std_api_signature_json() -> serde_json::Value { let json_data: serde_json::Value = - serde_json::from_str(include_str!("data/std_sig.json")).expect("Unable to parse JSON"); + serde_json::from_str(include_str!("assets/std_sig.json")).expect("Unable to parse JSON"); json_data } pub fn get_sp_tags_and_args_json() -> serde_json::Value { let json_data: serde_json::Value = - serde_json::from_str(include_str!("data/std_sps_args.json")).expect("Unable to parse JSON"); + serde_json::from_str(include_str!("assets/std_sps_args.json")) + .expect("Unable to parse JSON"); json_data } @@ -1471,7 +1473,8 @@ pub fn generate_mir_cfg_dot<'tcx>( } dot_content.push_str("}\n"); let name = get_cleaned_def_path_name(tcx, def_id); - render_dot_string(name, dot_content); + let dot_graph = DotGraph::new(name, dot_content); + render_dot_string(&dot_graph); rap_debug!("render dot for {:?}", def_id); Ok(()) } diff --git a/rapx/src/utils/source.rs b/rapx/src/utils/source.rs index e2cd04e9..030e1353 100644 --- a/rapx/src/utils/source.rs +++ b/rapx/src/utils/source.rs @@ -1,7 +1,7 @@ use rustc_hir::{self, Node::*, def::DefKind}; use rustc_middle::ty::TyCtxt; use rustc_span::{ - FileName, + FileName, Span, def_id::{CrateNum, DefId}, symbol::Symbol, }; @@ -62,11 +62,17 @@ pub fn get_name(tcx: TyCtxt<'_>, def_id: DefId) -> Option { None } -pub fn get_filename(tcx: TyCtxt<'_>, def_id: DefId) -> Option { - // Get the HIR node corresponding to the DefId +pub fn get_span(tcx: TyCtxt<'_>, def_id: DefId) -> Option { if let Some(local_id) = def_id.as_local() { let hir_id = tcx.local_def_id_to_hir_id(local_id); - let span = tcx.hir_span(hir_id); + return Some(tcx.hir_span(hir_id)); + } + None +} + +pub fn get_filename(tcx: TyCtxt<'_>, def_id: DefId) -> Option { + // Get the HIR node corresponding to the DefId + if let Some(span) = get_span(tcx, def_id) { let source_map = tcx.sess.source_map(); // Retrieve the file name diff --git a/serve.sh b/serve.sh new file mode 100755 index 00000000..72b895b8 --- /dev/null +++ b/serve.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +usage() { + cat < | -k) [-p 端口] + +参数: + -d Rust 项目根目录(执行构建并启动服务) + -k 清理指定端口上的服务进程并退出 + -p HTTP 服务端口(可选,默认: 8080) + +行为说明: + - 使用 -d 时: + * 自动清理指定端口上的已有服务 + * 在项目目录中构建 UPG 与 Rust 文档 + * 启动本地 HTTP 服务用于浏览文档与 UPG + + - 使用 -k 时: + * 仅清理指定端口上的服务进程 +EOF + exit 1 +} + +set -e + +PORT=8080 +DIR="" +KILL=0 + +while getopts "d:p:k" opt; do + case $opt in + d) DIR="$OPTARG" ;; + p) PORT="$OPTARG" ;; + k) KILL=1 ;; + *) usage ;; + esac +done + +# 至少有 -d 或 -k +[ -z "$DIR" ] && [ "$KILL" -eq 0 ] && usage + +# 只要 -d 或 -k,就清理端口 +if [ "$KILL" -eq 1 ] || [ -n "$DIR" ]; then + lsof -ti :"$PORT" | xargs -r kill -9 + echo "端口 $PORT 已清理" +fi + +# 仅 -k 时结束 +[ -z "$DIR" ] && exit 0 +[ ! -d "$DIR" ] && { echo "错误: 目录不存在"; exit 1; } + +cd "$DIR" + +MOD=$(basename "$PWD") +BASE_URL="http://127.0.0.1:$PORT" +SRC_URL="$BASE_URL/target/doc/src/$MOD/" + +echo "生成 UPG..." +cargo rapx -upg + +echo "构建文档..." +cargo clean +cargo doc --no-deps --document-private-items + +[ -d UPG ] && sed -i "s|{{BASE_URL}}|$SRC_URL|g" UPG/*.html + +echo "文档: $BASE_URL/target/doc/$MOD/" +echo "UPG : $BASE_URL/UPG" + +python -m http.server "$PORT" >/dev/null 2>&1 & +echo "Server PID: $!"