trustfall_rustdoc/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::path::Path;
4
5use serde::Deserialize;
6use thiserror::Error;
7
8mod parser;
9mod query;
10mod versioned;
11
12use versioned::supported_versions;
13pub use {
14    parser::load_rustdoc,
15    versioned::{VersionedIndex, VersionedRustdocAdapter, VersionedStorage},
16};
17
18#[derive(Deserialize)]
19struct RustdocFormatVersion {
20    format_version: u32,
21}
22
23#[non_exhaustive]
24#[derive(Debug, Error)]
25pub enum LoadingError {
26    #[error("failed to parse 'cargo metadata' output: {0}")]
27    MetadataParsing(String),
28
29    #[error("failed to read rustdoc JSON file: {0}")]
30    RustdocIoError(String, std::io::Error),
31
32    #[error("unable to detect rustdoc 'format_version' key in file: {0}")]
33    RustdocFormatDetection(String, anyhow::Error),
34
35    #[error("failed to parse rustdoc JSON format v{0} file: {1}")]
36    RustdocParsing(u32, String, anyhow::Error),
37
38    #[error("unsupported rustdoc format v{0} for file: {1}\n(supported formats are {list})",
39        list = supported_versions().iter().map(|v| format!("v{v}")).collect::<Vec<_>>().join(", "))]
40    UnsupportedFormat(u32, String),
41
42    #[error("unexpected error: {0}")]
43    Other(#[from] anyhow::Error),
44}
45
46/// The last characters of a rustdoc file should be: ,"format_version":NUM}.
47/// In this case, we can rapidly extract the version by simply reading the last
48/// few bytes.
49///
50/// Returns an error if the last characters do not match the prescribed format, otherwise
51/// returns the version.
52fn detect_rustdoc_format_version_fast_path(
53    path: &Path,
54    file_data: &str,
55) -> Result<u32, LoadingError> {
56    let error_closure = |s: &'static str| {
57        LoadingError::RustdocFormatDetection(path.display().to_string(), anyhow::anyhow!(s))
58    };
59
60    let start = file_data[file_data.len() - 23..]
61        .rfind(",")
62        .ok_or_else(|| error_closure("Fast path failed: comma not found in last 23 bytes."))?;
63
64    let version_string: &str = &file_data[start..];
65    let sep_idx = version_string
66        .rfind(":")
67        .ok_or_else(|| error_closure("Fast path failed: no colon follows the final comma."))?;
68
69    let final_idx = version_string.rfind("}").ok_or_else(|| {
70        error_closure("Fast path failed: file does not end with a close bracket.")
71    })?;
72
73    if !version_string[..sep_idx].ends_with("\"format_version\"") {
74        Err(error_closure(
75            "Fast path failed: final key is not \"format_version\"",
76        ))
77    } else {
78        version_string[sep_idx + 1..final_idx]
79            .parse::<u32>()
80            .map_err(|_| error_closure("Fast path failed: version number is invalid."))
81    }
82}
83
84fn detect_rustdoc_format_version(path: &Path, file_data: &str) -> Result<u32, LoadingError> {
85    let version = detect_rustdoc_format_version_fast_path(path, file_data);
86
87    match version {
88        Ok(version_num) => Ok(version_num),
89        Err(_) => {
90            let version = serde_json::from_str::<RustdocFormatVersion>(file_data).map_err(|e| {
91                LoadingError::RustdocFormatDetection(
92                    path.display().to_string(),
93                    anyhow::Error::from(e),
94                )
95            })?;
96            Ok(version.format_version)
97        }
98    }
99}
100
101fn parse_or_report_error<T>(
102    path: &Path,
103    file_data: &str,
104    format_version: u32,
105) -> Result<T, LoadingError>
106where
107    T: for<'a> Deserialize<'a>,
108{
109    serde_json::from_str(file_data).map_err(|e| {
110        LoadingError::RustdocParsing(
111            format_version,
112            path.display().to_string(),
113            anyhow::Error::from(e),
114        )
115    })
116}
117
118fn get_package_metadata(
119    metadata: cargo_metadata::Metadata,
120) -> Result<cargo_metadata::Package, LoadingError> {
121    let dependencies = &metadata
122        .root_package()
123        .ok_or_else(|| {
124            LoadingError::MetadataParsing("no root package found in 'cargo metadata' output".into())
125        })?
126        .dependencies;
127    if dependencies.len() != 1 {
128        return Err(LoadingError::MetadataParsing("the metadata unexpectedly contained more than one dependency; we expected our target package to be the only dependency".into()));
129    }
130    let dependency = dependencies
131        .first()
132        .expect("no first dependency, even though we just checked the count");
133    let dependency_name = dependency.name.clone();
134    let dependency_path = dependency.path.clone();
135    let dependency_version = dependency.req.clone();
136
137    let package_candidates = metadata
138        .packages
139        .into_iter()
140        .filter(|p| p.name.as_str() == dependency_name);
141
142    let mut package_candidates: Box<dyn Iterator<Item = _>> = if let Some(path) = dependency_path {
143        // We're using a path dependency.
144        Box::new(package_candidates.filter(move |p| p.manifest_path.starts_with(&path)))
145    } else {
146        // We're using a version number dependency.
147        Box::new(package_candidates.filter(move |p| dependency_version.matches(&p.version)))
148    };
149
150    let Some(package) = package_candidates.next() else {
151        return Err(LoadingError::MetadataParsing(format!(
152            "failed to find package metadata for package {dependency_name}"
153        )));
154    };
155    if package_candidates.next().is_some() {
156        return Err(LoadingError::MetadataParsing(format!(
157            "ambiguous package metadata found for {dependency_name}"
158        )));
159    }
160
161    Ok(package)
162}
163
164#[cfg(test)]
165mod tests {
166    use std::path::PathBuf;
167
168    use super::*;
169
170    // Test that format version succeeds both with and without the fast path.
171    #[test]
172    fn test_rustdoc_format_version() {
173        let fast_file_data = r#"{"test":10,"format_version":10}"#;
174        let test_path = PathBuf::from("");
175        match detect_rustdoc_format_version(&test_path, fast_file_data) {
176            Ok(version_num) => assert_eq!(version_num, 10),
177            Err(err) => panic!("Format version detection failed with error {err}"),
178        }
179
180        let slow_file_data = r#"{"format_version":10,"test":10}"#;
181        match detect_rustdoc_format_version(&test_path, slow_file_data) {
182            Ok(version_num) => assert_eq!(version_num, 10),
183            Err(err) => panic!("Format version detection failed with error {err}"),
184        }
185    }
186}