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