use crate::{Error, IndexKrate, KrateName, Path, PathBuf};
use smol_str::SmolStr;
#[cfg(feature = "local-builder")]
pub mod builder;
#[derive(Debug, thiserror::Error)]
pub enum LocalRegistryError {
#[error("missing version {version} for crate {name}")]
MissingVersion {
name: String,
version: SmolStr,
},
#[error("checksum mismatch for {name}-{version}.crate")]
ChecksumMismatch {
name: String,
version: SmolStr,
},
}
pub struct LocalRegistry {
path: PathBuf,
}
impl LocalRegistry {
pub fn open(path: PathBuf, validate: bool) -> Result<Self, Error> {
if validate {
Self::validate(&path)?;
}
Ok(Self { path })
}
pub fn validate(path: &Path) -> Result<(), Error> {
let rd = std::fs::read_dir(path).map_err(|err| Error::IoPath(err, path.to_owned()))?;
let index_root = path.join("index");
if !index_root.exists() {
return Err(Error::IoPath(
std::io::Error::new(
std::io::ErrorKind::NotFound,
"unable to find index directory",
),
index_root,
));
}
let mut indexed = std::collections::BTreeMap::new();
for entry in rd {
let Ok(entry) = entry else { continue; };
if entry.file_type().map_or(true, |ft| !ft.is_file()) {
continue;
}
let Ok(path) = PathBuf::from_path_buf(entry.path()) else { continue; };
let Some(fname) = path.file_name() else { continue; };
let Some((crate_name, version)) = crate_file_components(fname) else { continue; };
let index_entry = if let Some(ie) = indexed.get(crate_name) {
ie
} else {
let krate_name: crate::KrateName<'_> = crate_name.try_into()?;
let path = index_root.join(krate_name.relative_path(None));
let index_contents =
std::fs::read(&path).map_err(|err| Error::IoPath(err, path.clone()))?;
let ik = IndexKrate::from_slice(&index_contents)?;
indexed.insert(crate_name.to_owned(), ik);
indexed.get(crate_name).unwrap()
};
let index_vers = index_entry
.versions
.iter()
.find(|kv| kv.version == version)
.ok_or_else(|| LocalRegistryError::MissingVersion {
name: crate_name.to_owned(),
version: version.into(),
})?;
let file =
std::fs::File::open(&path).map_err(|err| Error::IoPath(err, path.clone()))?;
if !validate_checksum::<{ 8 * 1024 }>(&file, &index_vers.checksum)
.map_err(|err| Error::IoPath(err, path.clone()))?
{
return Err(LocalRegistryError::ChecksumMismatch {
name: crate_name.to_owned(),
version: version.into(),
}
.into());
}
}
Ok(())
}
#[inline]
pub fn cached_krate(&self, name: KrateName<'_>) -> Result<Option<IndexKrate>, Error> {
let index_path = make_path(&self.path, name);
let buf = match std::fs::read(&index_path) {
Ok(buf) => buf,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(Error::IoPath(err, index_path)),
};
Ok(Some(IndexKrate::from_slice(&buf)?))
}
}
pub struct LocalRegistryBuilder {
path: PathBuf,
}
impl LocalRegistryBuilder {
pub fn create(path: PathBuf) -> Result<Self, Error> {
if path.exists() {
let count = std::fs::read_dir(&path)?.count();
if count != 0 {
return Err(Error::IoPath(
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("{count} entries already exist at the specified path"),
),
path,
));
}
} else {
std::fs::create_dir_all(&path)?;
}
std::fs::create_dir_all(path.join("index"))?;
Ok(Self { path })
}
pub fn insert(&self, krate: &IndexKrate, krates: &[ValidKrate<'_>]) -> Result<u64, Error> {
let index_path = make_path(&self.path, krate.name().try_into()?);
if index_path.exists() {
return Err(Error::IoPath(
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"crate has already been inserted",
),
index_path,
));
}
let mut written = {
if let Err(err) = std::fs::create_dir_all(index_path.parent().unwrap()) {
return Err(Error::IoPath(err, index_path));
}
let mut index_entry =
std::fs::File::create(&index_path).map_err(|err| Error::IoPath(err, index_path))?;
krate.write_json_lines(&mut index_entry)?;
use std::io::Seek;
index_entry.stream_position().unwrap_or_default()
};
for krate in krates {
let krate_fname = format!("{}-{}.crate", krate.iv.name, krate.iv.version);
let krate_path = self.path.join(krate_fname);
std::fs::write(&krate_path, &krate.buff)
.map_err(|err| Error::IoPath(err, krate_path))?;
written += krate.buff.len() as u64;
}
Ok(written)
}
#[inline]
pub fn finalize(self, validate: bool) -> Result<LocalRegistry, Error> {
LocalRegistry::open(self.path, validate)
}
}
pub struct ValidKrate<'iv> {
buff: bytes::Bytes,
iv: &'iv crate::IndexVersion,
}
impl<'iv> ValidKrate<'iv> {
pub fn validate(
buff: impl Into<bytes::Bytes>,
expected: &'iv crate::IndexVersion,
) -> Result<Self, Error> {
let buff = buff.into();
let computed = {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&buff);
hasher.finalize()
};
if computed.as_slice() != expected.checksum.0 {
return Err(LocalRegistryError::ChecksumMismatch {
name: expected.name.to_string(),
version: expected.version.clone(),
}
.into());
}
Ok(Self { buff, iv: expected })
}
}
#[inline]
pub fn validate_checksum<const N: usize>(
mut stream: impl std::io::Read,
chksum: &crate::krate::Chksum,
) -> Result<bool, std::io::Error> {
use sha2::{Digest, Sha256};
let mut buffer = [0u8; N];
let mut hasher = Sha256::new();
loop {
let read = stream.read(&mut buffer)?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
let computed = hasher.finalize();
Ok(computed.as_slice() == chksum.0)
}
#[inline]
pub fn crate_file_components(name: &str) -> Option<(&str, &str)> {
let name = name.strip_suffix(".crate")?;
let dot = name.find('.')?;
let dash_sep = name[..dot].rfind('-')?;
Some((&name[..dash_sep], &name[dash_sep + 1..]))
}
#[inline]
fn make_path(root: &Path, name: KrateName<'_>) -> PathBuf {
let rel_path = name.relative_path(None);
let mut index_path = PathBuf::with_capacity(root.as_str().len() + 7 + rel_path.len());
index_path.push(root);
index_path.push("index");
index_path.push(rel_path);
index_path
}
#[cfg(test)]
mod test {
#[test]
fn gets_components() {
use super::crate_file_components as cfc;
assert_eq!(cfc("cc-1.0.75.crate").unwrap(), ("cc", "1.0.75"));
assert_eq!(
cfc("cfg-expr-0.1.0-dev+1234.crate").unwrap(),
("cfg-expr", "0.1.0-dev+1234")
);
assert_eq!(
cfc("android_system_properties-0.1.5.crate").unwrap(),
("android_system_properties", "0.1.5")
);
}
}