#![doc(
html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
)]
use std::collections::BTreeSet;
use std::fs::File;
use std::path::PathBuf;
use clap::{command, Parser};
use serde::Serialize;
use syn::{ItemEnum, ItemStruct};
use walkdir::WalkDir;
use crate::item_collector::AstItemCollector;
use crate::pii_finder::FieldsWithAttribute;
pub mod item_collector;
pub mod pii_finder;
#[derive(Clone)]
pub enum EnumOrStruct {
Struct(ItemStruct),
Enum(ItemEnum),
}
fn find_rs_files(dir: &PathBuf) -> Vec<std::path::PathBuf> {
let walker = WalkDir::new(dir).into_iter();
let mut rs_files = Vec::new();
for entry in walker.filter_map(walkdir::Result::ok) {
if !entry.path().to_string_lossy().contains("src") {
continue;
}
if entry.file_type().is_file() && entry.path().extension().map_or(false, |ext| ext == "rs")
{
rs_files.push(entry.into_path());
}
}
rs_files
}
#[derive(Debug, Parser, Default)]
#[command(verbatim_doc_comment)]
pub struct Cli {
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(short, long)]
pub path: Option<PathBuf>,
#[arg(short, long)]
pub item: Option<String>,
#[arg(long, default_value = "true")]
pub pii_values: Vec<String>,
}
impl Cli {
pub fn run(self) -> anyhow::Result<()> {
let path = match self.path.clone() {
Some(path) => {
if !path.join("Cargo.toml").exists() {
anyhow::bail!("Please provide the path to a rust crate/workspace");
}
path
}
None => std::env::current_dir()?,
};
let types_and_use_statements = {
let rust_file_paths = find_rs_files(&path);
AstItemCollector::collect(&rust_file_paths)?
};
let pii_types =
types_and_use_statements.find_pii_fields(self.item.as_deref(), &self.pii_values)?;
let output_vec = Output::from_btreeset(pii_types);
match self.output {
Some(ref path) => serde_json::to_writer_pretty(File::create(path)?, &output_vec)?,
None => serde_json::to_writer_pretty(std::io::stdout(), &output_vec)?,
};
Ok(())
}
}
#[derive(Serialize, Default, Debug)]
struct Output {
path: String,
additional_properties: bool,
}
impl Output {
fn new(pii_type: FieldsWithAttribute) -> Self {
let mut output = Self {
additional_properties: pii_type.attributes.contains_key("additional_properties"),
..Default::default()
};
output
.path
.push_str(&pii_type.type_and_fields[0].qualified_type_name);
let mut iter = pii_type.type_and_fields.iter().peekable();
while let Some(path) = iter.next() {
if !(output.additional_properties && iter.peek().is_none()) {
output.path.push_str(&format!(".{}", path.field_ident));
}
}
output.path = output.path.replace("{{Unnamed}}.", "");
output
}
fn from_btreeset(pii_types: BTreeSet<FieldsWithAttribute>) -> Vec<Self> {
let mut output_vec = vec![];
for pii in pii_types {
output_vec.push(Output::new(pii));
}
output_vec.sort_by(|a, b| a.path.cmp(&b.path));
output_vec
}
}
fn print_error(error: &anyhow::Error) {
eprintln!("Error: {error}");
let mut cause = error.source();
while let Some(ref e) = cause {
eprintln!(" caused by: {e}");
cause = e.source();
}
}
fn main() {
let cli = Cli::parse();
match cli.run() {
Ok(()) => (),
Err(error) => {
print_error(&error);
std::process::exit(1);
}
}
}
#[cfg(test)]
mod tests {
use path_slash::PathBufExt;
use crate::item_collector::TypesAndScopedPaths;
use super::*;
const RUST_TEST_CRATE: &str = "../../tests/test_pii_docs";
fn get_types_and_use_statements() -> TypesAndScopedPaths {
let rust_crate = PathBuf::from_slash(RUST_TEST_CRATE);
let rust_file_paths = find_rs_files(&rust_crate);
AstItemCollector::collect(&rust_file_paths).unwrap()
}
#[cfg(not(target_os = "windows"))]
#[test]
fn test_find_rs_files() {
let rust_crate = PathBuf::from_slash(RUST_TEST_CRATE);
let mut rust_file_paths = find_rs_files(&rust_crate);
rust_file_paths.sort_unstable();
insta::assert_debug_snapshot!(rust_file_paths);
}
#[test]
fn test_single_type() {
let types_and_use_statements = get_types_and_use_statements();
let pii_types = types_and_use_statements
.find_pii_fields(Some("test_pii_docs::SubStruct"), &vec!["true".to_string()])
.unwrap();
let output = Output::from_btreeset(pii_types);
insta::assert_debug_snapshot!(output);
}
#[test]
fn test_scoped_paths() {
let types_and_use_statements = get_types_and_use_statements();
let TypesAndScopedPaths { scoped_paths, .. } = types_and_use_statements;
insta::assert_debug_snapshot!(scoped_paths);
}
#[test]
fn test_pii_true() {
let types_and_use_statements = get_types_and_use_statements();
let pii_types = types_and_use_statements
.find_pii_fields(None, &vec!["true".to_string()])
.unwrap();
let output = Output::from_btreeset(pii_types);
insta::assert_debug_snapshot!(output);
}
#[test]
fn test_pii_false() {
let types_and_use_statements = get_types_and_use_statements();
let pii_types = types_and_use_statements
.find_pii_fields(None, &vec!["false".to_string()])
.unwrap();
let output = Output::from_btreeset(pii_types);
insta::assert_debug_snapshot!(output);
}
#[test]
fn test_pii_all() {
let types_and_use_statements = get_types_and_use_statements();
let pii_types = types_and_use_statements
.find_pii_fields(
None,
&vec!["true".to_string(), "false".to_string(), "maybe".to_string()],
)
.unwrap();
let output = Output::from_btreeset(pii_types);
insta::assert_debug_snapshot!(output);
}
#[test]
fn test_pii_retain_additional_properties_truth_table()
{
let types_and_use_statements = get_types_and_use_statements();
let pii_types = types_and_use_statements
.find_pii_fields(None, &vec!["truth_table_test".to_string()])
.unwrap();
let output = Output::from_btreeset(pii_types);
insta::assert_debug_snapshot!(output);
}
}