Skip to main content

ferritin_common/sources/
std.rs

1use crate::CrateName;
2use crate::RustdocData;
3use crate::navigator::CrateInfo;
4use crate::sources::CrateProvenance;
5use crate::sources::Source;
6use fieldwork::Fieldwork;
7use rustc_hash::FxHashMap;
8use rustdoc_types::{Crate, FORMAT_VERSION};
9use semver::Version;
10use semver::VersionReq;
11use std::borrow::Cow;
12use std::path::PathBuf;
13use std::process::Command;
14
15/// Descriptions for standard library crates
16const STD_DESCRIPTIONS: [(&str, &str); 6] = [
17    ("std", "The Rust Standard Library"),
18    ("alloc", "The Rust core allocation and collections library"),
19    ("core", "The Rust Core Library"),
20    (
21        "proc_macro",
22        "A support library for macro authors when defining new macros",
23    ),
24    (
25        "test",
26        "Support code for rustc's built in unit-test and micro-benchmarking framework",
27    ),
28    ("std_detect", ""), // we claim to have a "std_detect" crate that we then fail to load
29];
30
31/// Source for std library documentation (rustup-managed)
32#[derive(Debug, Clone, Fieldwork)]
33#[field(get)]
34pub struct StdSource {
35    docs_path: PathBuf,
36    rustc_version: Version,
37    crates: FxHashMap<&'static str, CrateInfo>,
38}
39
40impl StdSource {
41    /// Try to create a StdSource from the current rustup installation
42    pub fn from_rustup() -> Option<Self> {
43        let sysroot = Command::new("rustup")
44            .args(["run", "nightly", "rustc", "--print", "sysroot"])
45            .output()
46            .ok()?;
47
48        if !sysroot.status.success() {
49            return None;
50        }
51
52        let s = std::str::from_utf8(&sysroot.stdout).ok()?;
53        let docs_path = PathBuf::from(s.trim()).join("share/doc/rust/json/");
54        if !docs_path.exists() {
55            return None;
56        }
57
58        let version = Command::new("rustup")
59            .args(["run", "nightly", "rustc", "--version", "--verbose"])
60            .output()
61            .ok()?;
62
63        if !version.status.success() {
64            return None;
65        }
66
67        let rustc_version = std::str::from_utf8(&version.stdout)
68            .ok()?
69            .lines()
70            .find_map(|line| line.strip_prefix("release: "))?
71            .trim();
72
73        let rustc_version = Version::parse(rustc_version).ok()?;
74
75        let crates = STD_DESCRIPTIONS
76            .into_iter()
77            .map(|(name, description)| {
78                (
79                    name,
80                    CrateInfo {
81                        provenance: CrateProvenance::Std,
82                        version: Some(rustc_version.clone()),
83                        description: Some(description.to_string()),
84                        name: name.to_string(),
85                        default_crate: false,
86                        used_by: vec![],
87                        json_path: (name != "std_detect")
88                            .then(|| docs_path.join(format!("{name}.json"))),
89                    },
90                )
91            })
92            .collect();
93
94        Some(Self {
95            docs_path,
96            rustc_version,
97            crates,
98        })
99    }
100}
101
102impl Source for StdSource {
103    fn lookup<'a>(&'a self, name: &str, _version_req: &VersionReq) -> Option<Cow<'a, CrateInfo>> {
104        let canonical = self.canonicalize(name)?;
105        self.crates.get(&*canonical).map(Cow::Borrowed)
106    }
107
108    fn load(&self, crate_name: &str, _version: Option<&Version>) -> Option<RustdocData> {
109        let crate_info = self.lookup(crate_name, &VersionReq::STAR)?;
110        let json_path = crate_info.json_path.as_ref()?.to_owned();
111        let content = std::fs::read(&json_path).ok()?;
112
113        let Ok(FORMAT_VERSION) = sonic_rs::get_from_slice(&content, &["format_version"])
114            .ok()?
115            .as_raw_str()
116            .parse()
117        else {
118            return None;
119        };
120
121        let crate_data: Crate = sonic_rs::serde::from_slice(&content).ok()?;
122        Some(RustdocData {
123            crate_data,
124            name: crate_name.to_string(),
125            provenance: CrateProvenance::Std,
126            fs_path: json_path,
127            version: Some(self.rustc_version.clone()),
128            path_to_id: Default::default(),
129        })
130    }
131
132    fn list_available<'a>(&'a self) -> Box<dyn Iterator<Item = &'a CrateInfo> + '_> {
133        Box::new(
134            self.crates
135                .values()
136                .filter(|crate_info| crate_info.json_path.is_some()),
137        )
138    }
139
140    fn canonicalize(&self, input_name: &str) -> Option<CrateName<'static>> {
141        let canonical = match input_name {
142            "std" | "std_crate" => "std",
143            "core" | "core_crate" => "core",
144            "alloc" | "alloc_crate" => "alloc",
145            "proc_macro" | "proc_macro_crate" => "proc_macro",
146            "test" | "test_crate" => "test",
147            "std_detect" => "std_detect", // fake crate
148            _ => return None,
149        };
150
151        Some(CrateName::from(canonical))
152    }
153}