opendp_tooling/proven/
filesystem.rs

1use std::{collections::HashMap, env, ffi::OsStr, path::PathBuf};
2
3use darling::{Error, Result};
4use regex::Regex;
5
6/// Traverses the filesystem, starting at src_dir, looking for .tex files.
7/// If more than one file is discovered with the same name, the value becomes None
8pub fn find_proof_paths(
9    src_dir: &std::path::Path,
10) -> std::io::Result<HashMap<String, Option<String>>> {
11    let mut proof_paths = HashMap::new();
12    find_unique_file_names_with_extension(&mut proof_paths, &OsStr::new("tex"), src_dir, src_dir)?;
13    Ok(proof_paths)
14}
15
16/// Writes a collection of proof paths to {OUT_DIR}/proof_paths.json.
17pub fn write_proof_paths(proof_paths: &HashMap<String, Option<String>>) -> Result<()> {
18    std::fs::write(
19        get_out_dir()?.join("proof_paths.json"),
20        serde_json::to_string(proof_paths).map_err(Error::custom)?,
21    )
22    .map_err(Error::custom)
23}
24
25/// Load proof paths from {OUT_DIR}/proof_paths.json.
26/// Assumes the file was written in the build script.
27pub fn load_proof_paths() -> Result<HashMap<String, Option<String>>> {
28    serde_json::from_str(
29        &std::fs::read_to_string(get_out_dir()?.join("proof_paths.json")).map_err(Error::custom)?,
30    )
31    .map_err(Error::custom)
32}
33
34/// The inner function for find_proof_paths
35fn find_unique_file_names_with_extension(
36    matches: &mut HashMap<String, Option<String>>,
37    file_extension: &OsStr,
38    root_dir: &std::path::Path,
39    dir: &std::path::Path,
40) -> std::io::Result<()> {
41    if dir.is_dir() {
42        for entry in std::fs::read_dir(dir)? {
43            let path = entry?.path();
44            if path.is_dir() {
45                find_unique_file_names_with_extension(matches, file_extension, root_dir, &path)?;
46            } else {
47                if path.extension() != Some(file_extension) {
48                    continue;
49                }
50                if let Some(file_name) = path.file_stem() {
51                    matches
52                        .entry(file_name.to_string_lossy().to_string())
53                        // replaces the Option with None, because the name is no longer unique
54                        .and_modify(|v| drop(v.take()))
55                        .or_insert_with(|| {
56                            Some(
57                                path.strip_prefix(root_dir)
58                                    .expect("unreachable")
59                                    .to_string_lossy()
60                                    .to_string(),
61                            )
62                        });
63                }
64            };
65        }
66    }
67    Ok(())
68}
69
70pub fn get_src_dir() -> Result<PathBuf> {
71    let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
72        .ok_or_else(|| Error::custom("Failed to determine location of Cargo.toml."))?;
73    Ok(PathBuf::from(manifest_dir).join("src"))
74}
75
76fn get_out_dir() -> Result<PathBuf> {
77    let manifest_dir =
78        std::env::var_os("OUT_DIR").ok_or_else(|| Error::custom("Failed to determine OUT_DIR."))?;
79    Ok(PathBuf::from(manifest_dir))
80}
81
82pub fn make_proof_link(
83    source_dir: PathBuf,
84    mut relative_path: PathBuf,
85    repo_path: PathBuf,
86) -> Result<String> {
87    // construct absolute path
88    let absolute_path = source_dir.join(&relative_path);
89
90    if !absolute_path.exists() {
91        return Err(Error::custom(format!("{absolute_path:?} does not exist!")));
92    }
93
94    // link to the pdf, not the tex
95    relative_path.set_extension("pdf");
96
97    // link from sphinx and rustdoc to latex
98    let proof_uri = if let Ok(sphinx_port) = env::var("OPENDP_SPHINX_PORT") {
99        format!("http://localhost:{sphinx_port}")
100    } else {
101        // find the docs uri
102        let docs_uri = env::var("OPENDP_REMOTE_SPHINX_URI")
103            .unwrap_or_else(|_| "https://docs.opendp.org".to_string());
104
105        // find the version
106        let version = env!("CARGO_PKG_VERSION");
107        let docs_ref = get_docs_ref(version);
108
109        format!("{docs_uri}/en/{docs_ref}")
110    };
111
112    Ok(format!(
113        "{proof_uri}/proofs/{repo_path}/{relative_path}",
114        proof_uri = proof_uri,
115        repo_path = repo_path.display(),
116        relative_path = relative_path.display()
117    ))
118}
119
120fn get_docs_ref(version: &str) -> String {
121    // docs.opendp.org has tags for stable versions, but only a single branch for beta & nightly.
122    let channel = get_channel(version);
123    match channel.as_str() {
124        "stable" => format!("v{version}"), // For stable, we have tags.
125        "dev" => "nightly".to_string(),    // Will be replaced by the @versioned decorator.
126        _ => channel, // For beta & nightly, we don't have tags, just a single branch.
127    }
128}
129
130fn get_channel(version: &str) -> String {
131    let re = Regex::new(r"^(\d+\.\d+\.\d+)(?:-(dev|nightly|beta)(?:\.(.+))?)?$").unwrap();
132    if let Some(caps) = re.captures(version) {
133        let channel = caps.get(2);
134        return channel.map_or("stable", |m| m.as_str()).to_string();
135    }
136    "unknown".to_string()
137}