use std::fmt;
use super::PackchainError;
use super::schema::{ChainSegment, Sha40};
pub(crate) const CHAIN_JSON_SUFFIX: &str = "/chain.json";
#[must_use]
pub(crate) fn is_chain_json_key(key: &str) -> bool {
key.ends_with(CHAIN_JSON_SUFFIX)
}
#[must_use]
pub(crate) fn pack_key_from_relative(prefix: Option<&str>, bucket_relative_pack: &str) -> String {
crate::keys::join(prefix, bucket_relative_pack)
}
#[must_use]
pub(crate) fn ref_path_from_chain_key(prefix: Option<&str>, key: &str) -> Option<String> {
let without_suffix = key.strip_suffix(CHAIN_JSON_SUFFIX)?;
match prefix {
None | Some("") => Some(without_suffix.to_owned()),
Some(p) => without_suffix
.strip_prefix(p)
.and_then(|s| s.strip_prefix('/'))
.map(str::to_owned),
}
}
#[must_use]
pub(crate) fn sha_from_pack_key(pack: &str) -> Option<Sha40> {
let (parent, basename) = pack.rsplit_once('/')?;
let sha = basename.strip_suffix(".pack")?;
if parent != "packs" && !parent.ends_with("/packs") {
return None;
}
Sha40::try_new(sha).ok()
}
pub(crate) fn segment_pack_sha(segment: &ChainSegment) -> Result<Sha40, PackchainError> {
sha_from_pack_key(&segment.pack).ok_or_else(|| PackchainError::MalformedPackEntry {
offset: 0,
reason: format!(
"chain segment pack key `{}` is not of the form `[<prefix>/]packs/<sha>.pack`",
segment.pack,
),
})
}
pub(crate) fn chain_key(prefix: Option<&str>, ref_name: impl fmt::Display) -> String {
match prefix {
Some(p) if !p.is_empty() => format!("{p}/{ref_name}/chain.json"),
_ => format!("{ref_name}/chain.json"),
}
}
pub(crate) fn path_index_key(prefix: Option<&str>, ref_name: impl fmt::Display) -> String {
match prefix {
Some(p) if !p.is_empty() => format!("{p}/{ref_name}/path-index.json"),
_ => format!("{ref_name}/path-index.json"),
}
}
#[must_use]
pub(crate) fn pack_sha_from_full_key(prefix: Option<&str>, key: &str) -> Option<Sha40> {
let unprefixed = match prefix {
Some(p) if !p.is_empty() => key.strip_prefix(p).and_then(|s| s.strip_prefix('/'))?,
_ => key,
};
let stem = unprefixed
.strip_prefix("packs/")
.and_then(|s| s.strip_suffix(".pack").or_else(|| s.strip_suffix(".idx")))?;
Sha40::try_new(stem).ok()
}
pub(crate) fn pack_key(prefix: Option<&str>, content_sha: &Sha40) -> String {
let sha = content_sha.as_str();
match prefix {
Some(p) if !p.is_empty() => format!("{p}/packs/{sha}.pack"),
_ => format!("packs/{sha}.pack"),
}
}
pub(crate) fn pack_idx_key(prefix: Option<&str>, content_sha: &Sha40) -> String {
let sha = content_sha.as_str();
match prefix {
Some(p) if !p.is_empty() => format!("{p}/packs/{sha}.idx"),
_ => format!("packs/{sha}.idx"),
}
}
#[cfg(test)]
mod tests {
use super::*;
const SHA: &str = "abcdef0123456789abcdef0123456789abcdef01";
const REF: &str = "refs/heads/main";
fn sha40() -> Sha40 {
Sha40::try_new(SHA).unwrap()
}
#[test]
fn chain_key_with_prefix() {
assert_eq!(
chain_key(Some("acme"), REF),
format!("acme/{REF}/chain.json"),
);
}
#[test]
fn chain_key_without_prefix() {
assert_eq!(chain_key(None, REF), format!("{REF}/chain.json"));
}
#[test]
fn chain_key_empty_prefix_matches_none() {
assert_eq!(chain_key(Some(""), REF), chain_key(None, REF));
}
#[test]
fn path_index_key_with_prefix() {
assert_eq!(
path_index_key(Some("acme"), REF),
format!("acme/{REF}/path-index.json"),
);
}
#[test]
fn path_index_key_without_prefix() {
assert_eq!(path_index_key(None, REF), format!("{REF}/path-index.json"));
}
#[test]
fn pack_key_with_prefix() {
let sha = sha40();
assert_eq!(
pack_key(Some("acme"), &sha),
format!("acme/packs/{SHA}.pack")
);
}
#[test]
fn pack_key_without_prefix() {
let sha = sha40();
assert_eq!(pack_key(None, &sha), format!("packs/{SHA}.pack"));
}
#[test]
fn pack_idx_key_with_prefix() {
let sha = sha40();
assert_eq!(
pack_idx_key(Some("acme"), &sha),
format!("acme/packs/{SHA}.idx"),
);
}
#[test]
fn pack_idx_key_without_prefix() {
let sha = sha40();
assert_eq!(pack_idx_key(None, &sha), format!("packs/{SHA}.idx"));
}
#[test]
fn pack_and_idx_share_basename() {
let sha = sha40();
let pack = pack_key(Some("acme"), &sha);
let idx = pack_idx_key(Some("acme"), &sha);
assert_eq!(
pack.strip_suffix(".pack").unwrap(),
idx.strip_suffix(".idx").unwrap()
);
}
#[test]
fn is_chain_json_key_accepts_prefixed_and_unprefixed_keys() {
assert!(is_chain_json_key("repo/refs/heads/main/chain.json"));
assert!(is_chain_json_key("refs/heads/main/chain.json"));
assert!(is_chain_json_key("refs/heads/feature/x/chain.json"));
}
#[test]
fn is_chain_json_key_rejects_siblings() {
assert!(!is_chain_json_key("repo/refs/heads/main/path-index.json"));
assert!(!is_chain_json_key(&format!(
"repo/refs/heads/main/{SHA}.bundle"
)));
assert!(!is_chain_json_key("repo/refs/heads/main/chain.json.bak"));
}
#[test]
fn sha_from_pack_key_handles_prefixed_and_unprefixed() {
let sha = sha_from_pack_key(&format!("packs/{SHA}.pack")).expect("unprefixed");
assert_eq!(sha.as_str(), SHA);
let sha = sha_from_pack_key(&format!("acme/repo/packs/{SHA}.pack")).expect("prefixed");
assert_eq!(sha.as_str(), SHA);
}
#[test]
fn sha_from_pack_key_returns_none_for_malformed() {
assert!(sha_from_pack_key(&format!("packs/{SHA}")).is_none());
assert!(sha_from_pack_key("packs/abcdef0123456789abcdef0123456789abcdef0.pack").is_none());
assert!(sha_from_pack_key("packs/zbcdef0123456789abcdef0123456789abcdef01.pack").is_none());
}
#[test]
fn sha_from_pack_key_rejects_non_packs_parent() {
assert!(sha_from_pack_key(&format!("{SHA}.pack")).is_none());
assert!(sha_from_pack_key(&format!("../{SHA}.pack")).is_none());
assert!(sha_from_pack_key(&format!("../etc/{SHA}.pack")).is_none());
assert!(sha_from_pack_key(&format!("evil/{SHA}.pack")).is_none());
assert!(sha_from_pack_key(&format!("packs-other/{SHA}.pack")).is_none());
assert!(sha_from_pack_key(&format!("acme/packsfake/{SHA}.pack")).is_none());
assert!(sha_from_pack_key(&format!("acme/repo/{SHA}.pack")).is_none());
}
#[test]
fn pack_sha_from_full_key_matches_pack_and_idx_with_prefix() {
let key = format!("acme/packs/{SHA}.pack");
assert_eq!(
pack_sha_from_full_key(Some("acme"), &key).unwrap().as_str(),
SHA,
);
let idx = format!("acme/packs/{SHA}.idx");
assert_eq!(
pack_sha_from_full_key(Some("acme"), &idx).unwrap().as_str(),
SHA,
);
}
#[test]
fn pack_sha_from_full_key_matches_without_prefix() {
let key = format!("packs/{SHA}.pack");
assert_eq!(pack_sha_from_full_key(None, &key).unwrap().as_str(), SHA);
assert_eq!(
pack_sha_from_full_key(Some(""), &key).unwrap().as_str(),
SHA,
);
let idx = format!("packs/{SHA}.idx");
assert_eq!(pack_sha_from_full_key(None, &idx).unwrap().as_str(), SHA);
assert_eq!(
pack_sha_from_full_key(Some(""), &idx).unwrap().as_str(),
SHA,
);
}
#[test]
fn pack_sha_from_full_key_rejects_prefix_mismatch() {
let key = format!("packs/{SHA}.pack");
assert!(pack_sha_from_full_key(Some("acme"), &key).is_none());
let key = format!("acme-other/packs/{SHA}.pack");
assert!(pack_sha_from_full_key(Some("acme"), &key).is_none());
let key = format!("acme/packs/{SHA}.pack");
assert!(pack_sha_from_full_key(None, &key).is_none());
let idx = format!("acme/packs/{SHA}.idx");
assert!(pack_sha_from_full_key(None, &idx).is_none());
}
#[test]
fn pack_sha_from_full_key_rejects_malformed_shapes() {
assert!(pack_sha_from_full_key(None, &format!("blobs/{SHA}.pack")).is_none());
assert!(pack_sha_from_full_key(None, &format!("packs/{SHA}")).is_none());
assert!(
pack_sha_from_full_key(None, "packs/abcdef0123456789abcdef0123456789abcdef0.pack")
.is_none()
);
assert!(pack_sha_from_full_key(None, &format!("packs/{SHA}f.pack")).is_none());
assert!(
pack_sha_from_full_key(None, "packs/ABCDEF0123456789ABCDEF0123456789ABCDEF01.pack")
.is_none()
);
}
#[test]
fn pack_sha_from_full_key_rejects_degenerate_shapes() {
assert!(pack_sha_from_full_key(None, "").is_none());
assert!(pack_sha_from_full_key(Some("acme"), "").is_none());
assert!(pack_sha_from_full_key(None, "packs/.pack").is_none());
assert!(pack_sha_from_full_key(None, "packs/.idx").is_none());
assert!(pack_sha_from_full_key(None, &format!("packs/{SHA}.pack/extra")).is_none());
assert!(pack_sha_from_full_key(None, &format!("packs/{SHA}.idx/extra")).is_none());
assert!(pack_sha_from_full_key(None, &format!("../packs/{SHA}.pack")).is_none());
assert!(pack_sha_from_full_key(None, &format!("../packs/{SHA}.idx")).is_none());
}
#[test]
fn segment_pack_sha_maps_malformed_to_malformed_pack_entry() {
let segment = super::super::schema::ChainSegment {
sha: Sha40::try_new(SHA).unwrap(),
parent_sha: None,
pack: format!("packs/{SHA}"),
bytes: 4_096,
};
let err = segment_pack_sha(&segment).unwrap_err();
assert!(
matches!(err, PackchainError::MalformedPackEntry { offset: 0, .. }),
"expected MalformedPackEntry, got {err:?}",
);
}
}