binstalk_manifests/
binstall_crates_v1.rs

1//! Binstall's `crates-v1.json` manifest.
2//!
3//! This manifest is used by Binstall to record which crates were installed, and may be used by
4//! other (third party) tooling to act upon these crates (e.g. upgrade them, list them, etc).
5//!
6//! The format is a series of JSON object concatenated together. It is _not_ NLJSON, though writing
7//! NLJSON to the file will be understood fine.
8
9use std::{
10    borrow::Borrow,
11    cmp,
12    collections::{btree_set, BTreeSet},
13    fs,
14    io::{self, Seek, Write},
15    iter::{IntoIterator, Iterator},
16    path::{Path, PathBuf},
17};
18
19use compact_str::CompactString;
20use fs_lock::FileLock;
21use home::cargo_home;
22use miette::Diagnostic;
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25
26use crate::{crate_info::CrateInfo, helpers::create_if_not_exist};
27
28/// Buffer size for loading and writing binstall_crates_v1 manifest.
29const BUFFER_SIZE: usize = 4096 * 5;
30
31#[derive(Debug, Diagnostic, Error)]
32#[non_exhaustive]
33pub enum Error {
34    #[error("I/O Error: {0}")]
35    Io(#[from] io::Error),
36
37    #[error("Failed to parse json: {0}")]
38    SerdeJsonParse(#[from] serde_json::Error),
39}
40
41pub fn append_to_path<Iter, T>(path: impl AsRef<Path>, iter: Iter) -> Result<(), Error>
42where
43    Iter: IntoIterator<Item = T>,
44    Data: From<T>,
45{
46    let path = path.as_ref();
47    let mut file = create_if_not_exist(path)?;
48    // Move the cursor to EOF
49    file.seek(io::SeekFrom::End(0))?;
50
51    write_to(&mut file, &mut iter.into_iter().map(Data::from))
52}
53
54pub fn append<Iter, T>(iter: Iter) -> Result<(), Error>
55where
56    Iter: IntoIterator<Item = T>,
57    Data: From<T>,
58{
59    append_to_path(default_path()?, iter)
60}
61
62pub fn write_to(file: &mut FileLock, iter: &mut dyn Iterator<Item = Data>) -> Result<(), Error> {
63    let writer = io::BufWriter::with_capacity(BUFFER_SIZE, file);
64
65    let mut ser = serde_json::Serializer::new(writer);
66
67    for item in iter {
68        item.serialize(&mut ser)?;
69    }
70
71    ser.into_inner().flush()?;
72
73    Ok(())
74}
75
76pub fn default_path() -> Result<PathBuf, Error> {
77    let dir = cargo_home()?.join("binstall");
78
79    fs::create_dir_all(&dir)?;
80
81    Ok(dir.join("crates-v1.json"))
82}
83
84#[derive(Debug, Deserialize, Serialize)]
85pub struct Data {
86    #[serde(flatten)]
87    pub crate_info: CrateInfo,
88
89    /// Forwards compatibility. Unknown keys from future versions
90    /// will be stored here and retained when the file is saved.
91    ///
92    /// We use an `Vec` here since it is never accessed in Rust.
93    #[serde(flatten, with = "tuple_vec_map")]
94    pub other: Vec<(CompactString, serde_json::Value)>,
95}
96
97impl From<CrateInfo> for Data {
98    fn from(crate_info: CrateInfo) -> Self {
99        Self {
100            crate_info,
101            other: Vec::new(),
102        }
103    }
104}
105
106impl From<Data> for CrateInfo {
107    fn from(data: Data) -> Self {
108        data.crate_info
109    }
110}
111
112impl Borrow<str> for Data {
113    fn borrow(&self) -> &str {
114        &self.crate_info.name
115    }
116}
117
118impl PartialEq for Data {
119    fn eq(&self, other: &Self) -> bool {
120        self.crate_info.name == other.crate_info.name
121    }
122}
123impl PartialEq<CrateInfo> for Data {
124    fn eq(&self, other: &CrateInfo) -> bool {
125        self.crate_info.name == other.name
126    }
127}
128impl PartialEq<Data> for CrateInfo {
129    fn eq(&self, other: &Data) -> bool {
130        self.name == other.crate_info.name
131    }
132}
133impl Eq for Data {}
134
135impl PartialOrd for Data {
136    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
137        Some(self.cmp(other))
138    }
139}
140
141impl Ord for Data {
142    fn cmp(&self, other: &Self) -> cmp::Ordering {
143        self.crate_info.name.cmp(&other.crate_info.name)
144    }
145}
146
147#[derive(Debug)]
148pub struct Records {
149    file: FileLock,
150    /// Use BTreeSet to dedup the metadata
151    data: BTreeSet<Data>,
152}
153
154impl Records {
155    fn load_impl(&mut self) -> Result<(), Error> {
156        let reader = io::BufReader::with_capacity(BUFFER_SIZE, &mut self.file);
157        let stream_deser = serde_json::Deserializer::from_reader(reader).into_iter();
158
159        for res in stream_deser {
160            let item = res?;
161
162            self.data.replace(item);
163        }
164
165        Ok(())
166    }
167
168    pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
169        let mut this = Self {
170            file: create_if_not_exist(path.as_ref())?,
171            data: BTreeSet::default(),
172        };
173        this.load_impl()?;
174        Ok(this)
175    }
176
177    pub fn load() -> Result<Self, Error> {
178        Self::load_from_path(default_path()?)
179    }
180
181    /// **Warning: This will overwrite all existing records!**
182    pub fn overwrite(mut self) -> Result<(), Error> {
183        self.file.rewind()?;
184        write_to(&mut self.file, &mut self.data.into_iter())?;
185
186        let len = self.file.stream_position()?;
187        self.file.set_len(len)?;
188
189        Ok(())
190    }
191
192    pub fn get(&self, value: impl AsRef<str>) -> Option<&CrateInfo> {
193        self.data.get(value.as_ref()).map(|data| &data.crate_info)
194    }
195
196    pub fn contains(&self, value: impl AsRef<str>) -> bool {
197        self.data.contains(value.as_ref())
198    }
199
200    /// Adds a value to the set.
201    /// If the set did not have an equal element present, true is returned.
202    /// If the set did have an equal element present, false is returned,
203    /// and the entry is not updated.
204    pub fn insert(&mut self, value: CrateInfo) -> bool {
205        self.data.insert(Data::from(value))
206    }
207
208    /// Return the previous `CrateInfo` for the package if there is any.
209    pub fn replace(&mut self, value: CrateInfo) -> Option<CrateInfo> {
210        self.data.replace(Data::from(value)).map(CrateInfo::from)
211    }
212
213    pub fn remove(&mut self, value: impl AsRef<str>) -> bool {
214        self.data.remove(value.as_ref())
215    }
216
217    pub fn take(&mut self, value: impl AsRef<str>) -> Option<CrateInfo> {
218        self.data.take(value.as_ref()).map(CrateInfo::from)
219    }
220
221    pub fn len(&self) -> usize {
222        self.data.len()
223    }
224
225    pub fn is_empty(&self) -> bool {
226        self.data.is_empty()
227    }
228}
229
230impl<'a> IntoIterator for &'a Records {
231    type Item = &'a Data;
232
233    type IntoIter = btree_set::Iter<'a, Data>;
234
235    fn into_iter(self) -> Self::IntoIter {
236        self.data.iter()
237    }
238}
239
240#[cfg(test)]
241mod test {
242    use super::*;
243    use crate::crate_info::CrateSource;
244
245    use compact_str::CompactString;
246    use detect_targets::TARGET;
247    use semver::Version;
248    use tempfile::NamedTempFile;
249
250    macro_rules! assert_records_eq {
251        ($records:expr, $metadata_set:expr) => {
252            assert_eq!($records.len(), $metadata_set.len());
253            for (record, metadata) in $records.into_iter().zip($metadata_set.iter()) {
254                assert_eq!(record, metadata);
255            }
256        };
257    }
258
259    #[test]
260    fn rw_test() {
261        let target = CompactString::from(TARGET);
262
263        let named_tempfile = NamedTempFile::new().unwrap();
264        let path = named_tempfile.path();
265
266        let metadata_vec = [
267            CrateInfo {
268                name: "a".into(),
269                version_req: "*".into(),
270                current_version: Version::new(0, 1, 0),
271                source: CrateSource::cratesio_registry(),
272                target: target.clone(),
273                bins: vec!["1".into(), "2".into()],
274            },
275            CrateInfo {
276                name: "b".into(),
277                version_req: "0.1.0".into(),
278                current_version: Version::new(0, 1, 0),
279                source: CrateSource::cratesio_registry(),
280                target: target.clone(),
281                bins: vec!["1".into(), "2".into()],
282            },
283            CrateInfo {
284                name: "a".into(),
285                version_req: "*".into(),
286                current_version: Version::new(0, 2, 0),
287                source: CrateSource::cratesio_registry(),
288                target: target.clone(),
289                bins: vec!["1".into()],
290            },
291        ];
292
293        append_to_path(path, metadata_vec.clone()).unwrap();
294
295        let mut iter = metadata_vec.into_iter();
296        iter.next().unwrap();
297
298        let mut metadata_set: BTreeSet<_> = iter.collect();
299
300        let mut records = Records::load_from_path(path).unwrap();
301        assert_records_eq!(&records, &metadata_set);
302
303        assert!(records.remove("b"));
304        metadata_set.remove("b");
305        assert_eq!(records.len(), metadata_set.len());
306        records.overwrite().unwrap();
307
308        let records = Records::load_from_path(path).unwrap();
309        assert_records_eq!(&records, &metadata_set);
310        // Drop the exclusive file lock
311        drop(records);
312
313        let new_metadata = CrateInfo {
314            name: "b".into(),
315            version_req: "0.1.0".into(),
316            current_version: Version::new(0, 1, 1),
317            source: CrateSource::cratesio_registry(),
318            target,
319            bins: vec!["1".into(), "2".into()],
320        };
321        append_to_path(path, [new_metadata.clone()]).unwrap();
322        metadata_set.insert(new_metadata);
323
324        let records = Records::load_from_path(path).unwrap();
325        assert_records_eq!(&records, &metadata_set);
326    }
327}