binstalk_manifests/
cargo_crates_v1.rs

1//! Cargo's `.crates.toml` manifest.
2//!
3//! This manifest is used by Cargo to record which crates were installed by `cargo-install` and by
4//! other Cargo (first and third party) tooling to act upon these crates (e.g. upgrade them, list
5//! them, etc).
6//!
7//! Binstall writes to this manifest when installing a crate, for interoperability with the Cargo
8//! ecosystem.
9
10use std::{
11    collections::BTreeMap,
12    fs::File,
13    io::{self, Seek},
14    iter::IntoIterator,
15    path::{Path, PathBuf},
16};
17
18use beef::Cow;
19use compact_str::CompactString;
20use fs_lock::FileLock;
21use home::cargo_home;
22use miette::Diagnostic;
23use semver::Version;
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27use crate::helpers::create_if_not_exist;
28
29use super::crate_info::CrateInfo;
30
31mod crate_version_source;
32use crate_version_source::*;
33
34#[derive(Clone, Debug, Default, Deserialize, Serialize)]
35pub struct CratesToml<'a> {
36    #[serde(with = "tuple_vec_map")]
37    v1: Vec<(String, Cow<'a, [CompactString]>)>,
38}
39
40impl CratesToml<'_> {
41    pub fn default_path() -> Result<PathBuf, CratesTomlParseError> {
42        Ok(cargo_home()?.join(".crates.toml"))
43    }
44
45    pub fn load() -> Result<Self, CratesTomlParseError> {
46        Self::load_from_path(Self::default_path()?)
47    }
48
49    pub fn load_from_reader<R: io::Read>(mut reader: R) -> Result<Self, CratesTomlParseError> {
50        fn inner(reader: &mut dyn io::Read) -> Result<CratesToml<'static>, CratesTomlParseError> {
51            let mut vec = Vec::new();
52            reader.read_to_end(&mut vec)?;
53
54            if vec.is_empty() {
55                Ok(CratesToml::default())
56            } else {
57                toml_edit::de::from_slice(&vec).map_err(CratesTomlParseError::from)
58            }
59        }
60
61        inner(&mut reader)
62    }
63
64    pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, CratesTomlParseError> {
65        let path = path.as_ref();
66        let file = FileLock::new_shared(File::open(path)?)?.set_file_path(path);
67        Self::load_from_reader(file)
68    }
69
70    pub fn remove(&mut self, name: &str) {
71        self.v1.retain(|(s, _bin)| {
72            s.split_once(' ')
73                .map(|(crate_name, _rest)| crate_name != name)
74                .unwrap_or_default()
75        });
76    }
77
78    pub fn write(&self) -> Result<(), CratesTomlParseError> {
79        self.write_to_path(Self::default_path()?)
80    }
81
82    pub fn write_to_writer<W: io::Write>(&self, mut writer: W) -> Result<(), CratesTomlParseError> {
83        fn inner(
84            this: &CratesToml<'_>,
85            writer: &mut dyn io::Write,
86        ) -> Result<(), CratesTomlParseError> {
87            let data = toml_edit::ser::to_string_pretty(&this)?;
88            writer.write_all(data.as_bytes())?;
89            Ok(())
90        }
91
92        inner(self, &mut writer)
93    }
94
95    pub fn write_to_file(&self, file: &mut File) -> Result<(), CratesTomlParseError> {
96        self.write_to_writer(&mut *file)?;
97        let pos = file.stream_position()?;
98        file.set_len(pos)?;
99
100        Ok(())
101    }
102
103    pub fn write_to_path(&self, path: impl AsRef<Path>) -> Result<(), CratesTomlParseError> {
104        let path = path.as_ref();
105        let mut file = FileLock::new_exclusive(File::create(path)?)?.set_file_path(path);
106        self.write_to_file(&mut file)
107    }
108
109    pub fn append_to_file<'a, Iter>(file: &mut File, iter: Iter) -> Result<(), CratesTomlParseError>
110    where
111        Iter: IntoIterator<Item = &'a CrateInfo>,
112    {
113        fn inner(
114            file: &mut File,
115            iter: &mut dyn Iterator<Item = &CrateInfo>,
116        ) -> Result<(), CratesTomlParseError> {
117            let mut c1 = CratesToml::load_from_reader(&mut *file)?;
118
119            for metadata in iter {
120                let name = &metadata.name;
121                let version = &metadata.current_version;
122                let source = Source::from(&metadata.source);
123
124                c1.remove(name);
125                c1.v1.push((
126                    format!("{name} {version} ({source})"),
127                    Cow::borrowed(&metadata.bins),
128                ));
129            }
130
131            file.rewind()?;
132            c1.write_to_file(file)?;
133
134            Ok(())
135        }
136
137        inner(file, &mut iter.into_iter())
138    }
139
140    pub fn append_to_path<'a, Iter>(
141        path: impl AsRef<Path>,
142        iter: Iter,
143    ) -> Result<(), CratesTomlParseError>
144    where
145        Iter: IntoIterator<Item = &'a CrateInfo>,
146    {
147        let mut file = create_if_not_exist(path.as_ref())?;
148        Self::append_to_file(&mut file, iter)
149    }
150
151    pub fn append<'a, Iter>(iter: Iter) -> Result<(), CratesTomlParseError>
152    where
153        Iter: IntoIterator<Item = &'a CrateInfo>,
154    {
155        Self::append_to_path(Self::default_path()?, iter)
156    }
157
158    /// Return BTreeMap with crate name as key and its corresponding version
159    /// as value.
160    pub fn collect_into_crates_versions(
161        self,
162    ) -> Result<BTreeMap<CompactString, Version>, CratesTomlParseError> {
163        fn parse_name_ver(s: &str) -> Result<(CompactString, Version), CvsParseError> {
164            match s.splitn(3, ' ').collect::<Vec<_>>()[..] {
165                [name, version, _source] => Ok((CompactString::new(name), version.parse()?)),
166                _ => Err(CvsParseError::BadFormat),
167            }
168        }
169
170        self.v1
171            .into_iter()
172            .map(|(s, _bins)| parse_name_ver(&s).map_err(CratesTomlParseError::from))
173            .collect()
174    }
175}
176
177#[derive(Debug, Diagnostic, Error)]
178#[non_exhaustive]
179pub enum CratesTomlParseError {
180    #[error("I/O Error: {0}")]
181    Io(#[from] io::Error),
182
183    #[error("Failed to deserialize toml: {0}")]
184    TomlParse(Box<toml_edit::de::Error>),
185
186    #[error("Failed to serialie toml: {0}")]
187    TomlWrite(Box<toml_edit::ser::Error>),
188
189    #[error(transparent)]
190    CvsParse(Box<CvsParseError>),
191}
192
193impl From<CvsParseError> for CratesTomlParseError {
194    fn from(e: CvsParseError) -> Self {
195        CratesTomlParseError::CvsParse(Box::new(e))
196    }
197}
198
199impl From<toml_edit::ser::Error> for CratesTomlParseError {
200    fn from(e: toml_edit::ser::Error) -> Self {
201        CratesTomlParseError::TomlWrite(Box::new(e))
202    }
203}
204
205impl From<toml_edit::de::Error> for CratesTomlParseError {
206    fn from(e: toml_edit::de::Error) -> Self {
207        CratesTomlParseError::TomlParse(Box::new(e))
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::crate_info::CrateSource;
215
216    use detect_targets::TARGET;
217    use semver::Version;
218    use tempfile::TempDir;
219
220    #[test]
221    fn test_empty() {
222        let tempdir = TempDir::new().unwrap();
223        let path = tempdir.path().join("crates-v1.toml");
224
225        CratesToml::append_to_path(
226            &path,
227            &[CrateInfo {
228                name: "cargo-binstall".into(),
229                version_req: "*".into(),
230                current_version: Version::new(0, 11, 1),
231                source: CrateSource::cratesio_registry(),
232                target: TARGET.into(),
233                bins: vec!["cargo-binstall".into()],
234            }],
235        )
236        .unwrap();
237
238        let crates = CratesToml::load_from_path(&path)
239            .unwrap()
240            .collect_into_crates_versions()
241            .unwrap();
242
243        assert_eq!(crates.len(), 1);
244
245        assert_eq!(
246            crates.get("cargo-binstall").unwrap(),
247            &Version::new(0, 11, 1)
248        );
249
250        // Update
251        CratesToml::append_to_path(
252            &path,
253            &[CrateInfo {
254                name: "cargo-binstall".into(),
255                version_req: "*".into(),
256                current_version: Version::new(0, 12, 0),
257                source: CrateSource::cratesio_registry(),
258                target: TARGET.into(),
259                bins: vec!["cargo-binstall".into()],
260            }],
261        )
262        .unwrap();
263
264        let crates = CratesToml::load_from_path(&path)
265            .unwrap()
266            .collect_into_crates_versions()
267            .unwrap();
268
269        assert_eq!(crates.len(), 1);
270
271        assert_eq!(
272            crates.get("cargo-binstall").unwrap(),
273            &Version::new(0, 12, 0)
274        );
275    }
276
277    #[test]
278    fn test_empty_file() {
279        let tempdir = TempDir::new().unwrap();
280        let path = tempdir.path().join("crates-v1.toml");
281
282        File::create(&path).unwrap();
283
284        assert!(CratesToml::load_from_path(&path).unwrap().v1.is_empty());
285    }
286
287    #[test]
288    fn test_loading() {
289        let raw_data = br#"
290[v1]
291"alacritty 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["alacritty"]
292"cargo-audit 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-audit"]
293"cargo-binstall 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-binstall"]
294"cargo-criterion 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-criterion"]
295"cargo-edit 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-add", "cargo-rm", "cargo-set-version", "cargo-upgrade"]
296"cargo-expand 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-expand"]
297"cargo-geiger 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-geiger"]
298"cargo-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-hack"]
299"cargo-nextest 0.9.26 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-nextest"]
300"cargo-supply-chain 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-supply-chain"]
301"cargo-tarpaulin 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-tarpaulin"]
302"cargo-update 8.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-install-update", "cargo-install-update-config"]
303"cargo-watch 8.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-watch"]
304"cargo-with 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-with"]
305"cross 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = ["cross", "cross-util"]
306"irust 1.63.3 (registry+https://github.com/rust-lang/crates.io-index)" = ["irust"]
307"tokei 12.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["tokei"]
308"xargo 0.3.26 (registry+https://github.com/rust-lang/crates.io-index)" = ["xargo", "xargo-check"]
309        "#;
310
311        CratesToml::load_from_reader(raw_data.as_slice()).unwrap();
312    }
313}