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
46fn 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 Box::new(package_candidates.filter(move |p| p.manifest_path.starts_with(&path)))
145 } else {
146 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]
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}