use std::{ borrow, cmp, collections::{btree_set, BTreeSet}, fs, hash, io::{self, Seek, Write}, iter::{IntoIterator, Iterator}, path::{Path, PathBuf}, }; use compact_str::CompactString; use miette::Diagnostic; use semver::Version; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; use crate::{cargo_home, cratesio_url, create_if_not_exist, FileLock}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MetaData { pub name: CompactString, pub version_req: CompactString, pub current_version: Version, pub source: Source, pub target: CompactString, pub bins: Vec, } impl borrow::Borrow for MetaData { fn borrow(&self) -> &str { &self.name } } impl PartialEq for MetaData { fn eq(&self, other: &Self) -> bool { self.name == other.name } } impl Eq for MetaData {} impl PartialOrd for MetaData { fn partial_cmp(&self, other: &Self) -> Option { self.name.partial_cmp(&other.name) } } impl Ord for MetaData { fn cmp(&self, other: &Self) -> cmp::Ordering { self.name.cmp(&other.name) } } impl hash::Hash for MetaData { fn hash(&self, state: &mut H) where H: hash::Hasher, { self.name.hash(state) } } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub enum SourceType { Git, Path, Registry, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Source { pub source_type: SourceType, pub url: Url, } impl Source { pub fn cratesio_registry() -> Source { Self { source_type: SourceType::Registry, url: cratesio_url().clone(), } } } #[derive(Debug, Diagnostic, Error)] pub enum Error { #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] SerdeJsonParse(#[from] serde_json::Error), } pub fn append_to_path(path: impl AsRef, iter: Iter) -> Result<(), Error> where Iter: IntoIterator, { let mut file = FileLock::new_exclusive( fs::OpenOptions::new() .create(true) .append(true) .open(path)?, )?; write_to(&mut file, &mut iter.into_iter()) } pub fn append(iter: Iter) -> Result<(), Error> where Iter: IntoIterator, { append_to_path(default_path()?, iter) } pub fn write_to( file: &mut FileLock, iter: &mut dyn Iterator, ) -> Result<(), Error> { let writer = io::BufWriter::with_capacity(512, file); let mut ser = serde_json::Serializer::new(writer); for item in iter { item.serialize(&mut ser)?; } ser.into_inner().flush()?; Ok(()) } pub fn default_path() -> Result { Ok(cargo_home()?.join(".binstall-crates.toml")) } #[derive(Debug)] pub struct Records { file: FileLock, /// Use BTreeSet to dedup the metadata data: BTreeSet, } impl Records { fn load_impl(&mut self) -> Result<(), Error> { let reader = io::BufReader::with_capacity(1024, &mut self.file); let stream_deser = serde_json::Deserializer::from_reader(reader).into_iter(); for res in stream_deser { let item = res?; self.data.replace(item); } Ok(()) } pub fn load_from_path(path: impl AsRef) -> Result { let mut this = Self { file: FileLock::new_exclusive(create_if_not_exist(path.as_ref())?)?, data: BTreeSet::default(), }; this.load_impl()?; Ok(this) } pub fn load() -> Result { Self::load_from_path(default_path()?) } /// **Warning: This will overwrite all existing records!** pub fn overwrite(mut self) -> Result<(), Error> { self.file.rewind()?; write_to(&mut self.file, &mut self.data.into_iter())?; let len = self.file.stream_position()?; self.file.set_len(len)?; Ok(()) } pub fn get(&self, value: impl AsRef) -> Option<&MetaData> { self.data.get(value.as_ref()) } pub fn contains(&self, value: impl AsRef) -> bool { self.data.contains(value.as_ref()) } /// Adds a value to the set. /// If the set did not have an equal element present, true is returned. /// If the set did have an equal element present, false is returned, /// and the entry is not updated. pub fn insert(&mut self, value: MetaData) -> bool { self.data.insert(value) } pub fn replace(&mut self, value: MetaData) -> Option { self.data.replace(value) } pub fn remove(&mut self, value: impl AsRef) -> bool { self.data.remove(value.as_ref()) } pub fn take(&mut self, value: impl AsRef) -> Option { self.data.take(value.as_ref()) } pub fn len(&self) -> usize { self.data.len() } pub fn is_empty(&self) -> bool { self.data.is_empty() } } impl<'a> IntoIterator for &'a Records { type Item = &'a MetaData; type IntoIter = btree_set::Iter<'a, MetaData>; fn into_iter(self) -> Self::IntoIter { self.data.iter() } } #[cfg(test)] mod test { use super::*; use crate::target::TARGET; use miette::Result; use tempfile::TempDir; macro_rules! assert_records_eq { ($records:expr, $metadata_set:expr) => { assert_eq!($records.len(), $metadata_set.len()); for (record, metadata) in $records.into_iter().zip($metadata_set.iter()) { assert_eq!(record, metadata); } }; } #[test] fn rw_test() -> Result<()> { let target = CompactString::from(TARGET); let tempdir = TempDir::new().unwrap(); let path = tempdir.path().join("binstall-tests.json"); let metadata_vec = [ MetaData { name: "a".into(), version_req: "*".into(), current_version: Version::new(0, 1, 0), source: Source::cratesio_registry(), target: target.clone(), bins: vec!["1".into(), "2".into()], }, MetaData { name: "b".into(), version_req: "0.1.0".into(), current_version: Version::new(0, 1, 0), source: Source::cratesio_registry(), target: target.clone(), bins: vec!["1".into(), "2".into()], }, MetaData { name: "a".into(), version_req: "*".into(), current_version: Version::new(0, 2, 0), source: Source::cratesio_registry(), target, bins: vec!["1".into()], }, ]; append_to_path(&path, metadata_vec.clone())?; let mut iter = metadata_vec.into_iter(); iter.next().unwrap(); let mut metadata_set: BTreeSet<_> = iter.collect(); let mut records = Records::load_from_path(&path)?; assert_records_eq!(&records, &metadata_set); records.remove("b"); assert_eq!(records.len(), metadata_set.len() - 1); records.overwrite()?; metadata_set.remove("b"); let records = Records::load_from_path(&path)?; assert_records_eq!(&records, &metadata_set); Ok(()) } }