1use 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 file = FileLock::new_shared(File::open(path)?)?;
66 Self::load_from_reader(file)
67 }
68
69 pub fn remove(&mut self, name: &str) {
70 self.v1.retain(|(s, _bin)| {
71 s.split_once(' ')
72 .map(|(crate_name, _rest)| crate_name != name)
73 .unwrap_or_default()
74 });
75 }
76
77 pub fn write(&self) -> Result<(), CratesTomlParseError> {
78 self.write_to_path(Self::default_path()?)
79 }
80
81 pub fn write_to_writer<W: io::Write>(&self, mut writer: W) -> Result<(), CratesTomlParseError> {
82 fn inner(
83 this: &CratesToml<'_>,
84 writer: &mut dyn io::Write,
85 ) -> Result<(), CratesTomlParseError> {
86 let data = toml_edit::ser::to_string_pretty(&this)?;
87 writer.write_all(data.as_bytes())?;
88 Ok(())
89 }
90
91 inner(self, &mut writer)
92 }
93
94 pub fn write_to_file(&self, file: &mut File) -> Result<(), CratesTomlParseError> {
95 self.write_to_writer(&mut *file)?;
96 let pos = file.stream_position()?;
97 file.set_len(pos)?;
98
99 Ok(())
100 }
101
102 pub fn write_to_path(&self, path: impl AsRef<Path>) -> Result<(), CratesTomlParseError> {
103 let mut file = FileLock::new_exclusive(File::create(path)?)?;
104 self.write_to_file(&mut file)
105 }
106
107 pub fn append_to_file<'a, Iter>(file: &mut File, iter: Iter) -> Result<(), CratesTomlParseError>
108 where
109 Iter: IntoIterator<Item = &'a CrateInfo>,
110 {
111 fn inner(
112 file: &mut File,
113 iter: &mut dyn Iterator<Item = &CrateInfo>,
114 ) -> Result<(), CratesTomlParseError> {
115 let mut c1 = CratesToml::load_from_reader(&mut *file)?;
116
117 for metadata in iter {
118 let name = &metadata.name;
119 let version = &metadata.current_version;
120 let source = Source::from(&metadata.source);
121
122 c1.remove(name);
123 c1.v1.push((
124 format!("{name} {version} ({source})"),
125 Cow::borrowed(&metadata.bins),
126 ));
127 }
128
129 file.rewind()?;
130 c1.write_to_file(file)?;
131
132 Ok(())
133 }
134
135 inner(file, &mut iter.into_iter())
136 }
137
138 pub fn append_to_path<'a, Iter>(
139 path: impl AsRef<Path>,
140 iter: Iter,
141 ) -> Result<(), CratesTomlParseError>
142 where
143 Iter: IntoIterator<Item = &'a CrateInfo>,
144 {
145 let mut file = FileLock::new_exclusive(create_if_not_exist(path.as_ref())?)?;
146 Self::append_to_file(&mut file, iter)
147 }
148
149 pub fn append<'a, Iter>(iter: Iter) -> Result<(), CratesTomlParseError>
150 where
151 Iter: IntoIterator<Item = &'a CrateInfo>,
152 {
153 Self::append_to_path(Self::default_path()?, iter)
154 }
155
156 pub fn collect_into_crates_versions(
159 self,
160 ) -> Result<BTreeMap<CompactString, Version>, CratesTomlParseError> {
161 fn parse_name_ver(s: &str) -> Result<(CompactString, Version), CvsParseError> {
162 match s.splitn(3, ' ').collect::<Vec<_>>()[..] {
163 [name, version, _source] => Ok((CompactString::new(name), version.parse()?)),
164 _ => Err(CvsParseError::BadFormat),
165 }
166 }
167
168 self.v1
169 .into_iter()
170 .map(|(s, _bins)| parse_name_ver(&s).map_err(CratesTomlParseError::from))
171 .collect()
172 }
173}
174
175#[derive(Debug, Diagnostic, Error)]
176#[non_exhaustive]
177pub enum CratesTomlParseError {
178 #[error("I/O Error: {0}")]
179 Io(#[from] io::Error),
180
181 #[error("Failed to deserialize toml: {0}")]
182 TomlParse(Box<toml_edit::de::Error>),
183
184 #[error("Failed to serialie toml: {0}")]
185 TomlWrite(Box<toml_edit::ser::Error>),
186
187 #[error(transparent)]
188 CvsParse(Box<CvsParseError>),
189}
190
191impl From<CvsParseError> for CratesTomlParseError {
192 fn from(e: CvsParseError) -> Self {
193 CratesTomlParseError::CvsParse(Box::new(e))
194 }
195}
196
197impl From<toml_edit::ser::Error> for CratesTomlParseError {
198 fn from(e: toml_edit::ser::Error) -> Self {
199 CratesTomlParseError::TomlWrite(Box::new(e))
200 }
201}
202
203impl From<toml_edit::de::Error> for CratesTomlParseError {
204 fn from(e: toml_edit::de::Error) -> Self {
205 CratesTomlParseError::TomlParse(Box::new(e))
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::crate_info::CrateSource;
213
214 use detect_targets::TARGET;
215 use semver::Version;
216 use tempfile::TempDir;
217
218 #[test]
219 fn test_empty() {
220 let tempdir = TempDir::new().unwrap();
221 let path = tempdir.path().join("crates-v1.toml");
222
223 CratesToml::append_to_path(
224 &path,
225 &[CrateInfo {
226 name: "cargo-binstall".into(),
227 version_req: "*".into(),
228 current_version: Version::new(0, 11, 1),
229 source: CrateSource::cratesio_registry(),
230 target: TARGET.into(),
231 bins: vec!["cargo-binstall".into()],
232 }],
233 )
234 .unwrap();
235
236 let crates = CratesToml::load_from_path(&path)
237 .unwrap()
238 .collect_into_crates_versions()
239 .unwrap();
240
241 assert_eq!(crates.len(), 1);
242
243 assert_eq!(
244 crates.get("cargo-binstall").unwrap(),
245 &Version::new(0, 11, 1)
246 );
247
248 CratesToml::append_to_path(
250 &path,
251 &[CrateInfo {
252 name: "cargo-binstall".into(),
253 version_req: "*".into(),
254 current_version: Version::new(0, 12, 0),
255 source: CrateSource::cratesio_registry(),
256 target: TARGET.into(),
257 bins: vec!["cargo-binstall".into()],
258 }],
259 )
260 .unwrap();
261
262 let crates = CratesToml::load_from_path(&path)
263 .unwrap()
264 .collect_into_crates_versions()
265 .unwrap();
266
267 assert_eq!(crates.len(), 1);
268
269 assert_eq!(
270 crates.get("cargo-binstall").unwrap(),
271 &Version::new(0, 12, 0)
272 );
273 }
274
275 #[test]
276 fn test_empty_file() {
277 let tempdir = TempDir::new().unwrap();
278 let path = tempdir.path().join("crates-v1.toml");
279
280 File::create(&path).unwrap();
281
282 assert!(CratesToml::load_from_path(&path).unwrap().v1.is_empty());
283 }
284
285 #[test]
286 fn test_loading() {
287 let raw_data = br#"
288[v1]
289"alacritty 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["alacritty"]
290"cargo-audit 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-audit"]
291"cargo-binstall 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-binstall"]
292"cargo-criterion 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-criterion"]
293"cargo-edit 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-add", "cargo-rm", "cargo-set-version", "cargo-upgrade"]
294"cargo-expand 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-expand"]
295"cargo-geiger 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-geiger"]
296"cargo-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-hack"]
297"cargo-nextest 0.9.26 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-nextest"]
298"cargo-supply-chain 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-supply-chain"]
299"cargo-tarpaulin 0.20.1 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-tarpaulin"]
300"cargo-update 8.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-install-update", "cargo-install-update-config"]
301"cargo-watch 8.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-watch"]
302"cargo-with 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["cargo-with"]
303"cross 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = ["cross", "cross-util"]
304"irust 1.63.3 (registry+https://github.com/rust-lang/crates.io-index)" = ["irust"]
305"tokei 12.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = ["tokei"]
306"xargo 0.3.26 (registry+https://github.com/rust-lang/crates.io-index)" = ["xargo", "xargo-check"]
307 "#;
308
309 CratesToml::load_from_reader(raw_data.as_slice()).unwrap();
310 }
311}