use std::path::Path;
use thiserror::Error;
use crate::manifest::Manifest;
pub const OBJECTS_DIR: &str = ".objects";
pub const MANIFESTS_DIR: &str = ".manifests";
#[must_use]
pub fn object_path(checksum: &str) -> String {
sharded_path(OBJECTS_DIR, checksum)
}
#[must_use]
pub fn manifest_path(snapshot_id: &str) -> String {
sharded_path(MANIFESTS_DIR, snapshot_id)
}
fn sharded_path(prefix: &str, hex: &str) -> String {
let s0 = char_slice(hex, 0, 3);
let s1 = char_slice(hex, 3, 6);
let s2 = char_slice(hex, 6, 9);
let rest = char_slice(hex, 9, hex.len());
format!("{prefix}/{s0}/{s1}/{s2}/{rest}")
}
fn char_slice(s: &str, start: usize, end: usize) -> &str {
let len = s.len();
let start = start.min(len);
let end = end.min(len);
&s[start..end]
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StoreError {
#[error("manifest not found: {id}")]
ManifestNotFound {
id: String,
},
#[error("object not found: {checksum}")]
ObjectNotFound {
checksum: String,
},
#[error("integrity check failed for {address}: expected {expected}, got {actual}")]
Integrity {
address: String,
expected: String,
actual: String,
},
#[error("failed to parse manifest: {0}")]
Parse(#[from] crate::manifest::ParseError),
#[error("store I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("store backend error: {message}")]
Backend {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
}
pub trait Store {
fn get_manifest(&self, id: &str) -> Result<Manifest, StoreError>;
fn fetch_files(&self, manifest: &Manifest, dest: &Path) -> Result<(), StoreError>;
fn push(&self, manifest: &Manifest, source: &Path) -> Result<(), StoreError>;
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "49dc870df1de7fd60794cebce449f5ccdae575affaa67a24b62acb03e039db92";
#[test]
fn store_object_path_matches_oracle_sharding() {
assert_eq!(
object_path(SAMPLE),
".objects/49d/c87/0df/1de7fd60794cebce449f5ccdae575affaa67a24b62acb03e039db92"
);
}
#[test]
fn store_manifest_path_matches_oracle_sharding() {
assert_eq!(
manifest_path(SAMPLE),
".manifests/49d/c87/0df/1de7fd60794cebce449f5ccdae575affaa67a24b62acb03e039db92"
);
}
#[test]
fn store_sharding_slices_three_three_three_rest() {
let h = SAMPLE;
let expected = format!(
".objects/{}/{}/{}/{}",
&h[0..3],
&h[3..6],
&h[6..9],
&h[9..]
);
assert_eq!(object_path(h), expected);
assert_eq!(&h[0..3], "49d");
assert_eq!(&h[3..6], "c87");
assert_eq!(&h[6..9], "0df");
}
#[test]
fn store_path_prefixes_are_dot_objects_and_dot_manifests() {
assert!(object_path(SAMPLE).starts_with(".objects/"));
assert!(manifest_path(SAMPLE).starts_with(".manifests/"));
}
#[test]
fn store_sharding_uses_forward_slashes_with_four_components_after_prefix() {
let p = object_path(SAMPLE);
let parts: Vec<&str> = p.split('/').collect();
assert_eq!(parts.len(), 5);
assert_eq!(parts[0], ".objects");
assert_eq!(parts[1].len(), 3);
assert_eq!(parts[2].len(), 3);
assert_eq!(parts[3].len(), 3);
assert_eq!(parts[4].len(), SAMPLE.len() - 9);
}
#[test]
fn store_sharding_clamps_short_inputs_like_bash() {
assert_eq!(object_path(""), ".objects////");
assert_eq!(object_path("ab"), ".objects/ab///");
assert_eq!(object_path("abcd"), ".objects/abc/d//");
assert_eq!(object_path("abcdefghij"), ".objects/abc/def/ghi/j");
}
struct NoopStore;
impl Store for NoopStore {
fn get_manifest(&self, id: &str) -> Result<Manifest, StoreError> {
Err(StoreError::ManifestNotFound { id: id.to_owned() })
}
fn fetch_files(&self, _manifest: &Manifest, _dest: &Path) -> Result<(), StoreError> {
Ok(())
}
fn push(&self, _manifest: &Manifest, _source: &Path) -> Result<(), StoreError> {
Ok(())
}
}
#[test]
fn store_trait_is_object_safe_and_implementable() {
let store: Box<dyn Store> = Box::new(NoopStore);
let dyn_ref: &dyn Store = store.as_ref();
let manifest = Manifest::new();
assert!(dyn_ref
.fetch_files(&manifest, Path::new("/tmp/snapdir-dest"))
.is_ok());
assert!(dyn_ref
.push(&manifest, Path::new("/tmp/snapdir-src"))
.is_ok());
match dyn_ref.get_manifest("deadbeef") {
Err(StoreError::ManifestNotFound { id }) => assert_eq!(id, "deadbeef"),
other => panic!("expected ManifestNotFound, got {other:?}"),
}
}
#[test]
fn store_error_parse_is_from_manifest_parse_error() {
let parse_err = Manifest::parse("F 700").unwrap_err();
let store_err: StoreError = parse_err.into();
assert!(matches!(store_err, StoreError::Parse(_)));
}
}