use std::path::{Path, PathBuf};
use knot_types::RepoDid;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct RepoKey(String);
impl RepoKey {
pub(crate) fn of(repo: &RepoDid) -> Self {
Self(crate::plan::repo_key(repo.as_str()))
}
/// a name only counts if it re-encodes to itself. that keeps it one path component.
pub(crate) fn encoded(name: &str) -> Option<Self> {
let encodes_itself = crate::plan::repo_key(&decode(name)?) == name;
encodes_itself.then(|| Self(name.to_string()))
}
#[cfg(test)]
pub(crate) fn parse_did(did: &str) -> anyhow::Result<Self> {
Ok(Self::of(&RepoDid::new(did)?))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn dir(&self) -> PathBuf {
Path::new("repos").join(&self.0)
}
}
impl std::fmt::Display for RepoKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CobStateKey(String);
impl CobStateKey {
pub(crate) fn new(key: &str) -> anyhow::Result<Self> {
let shaped = !key.is_empty()
&& key.len() <= 128
&& key
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'));
anyhow::ensure!(shaped, "{key:?} is not a cob state key");
Ok(Self(key.to_string()))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
fn decode(name: &str) -> Option<String> {
let mut out = String::with_capacity(name.len());
let mut bytes = name.bytes();
while let Some(byte) = bytes.next() {
if byte != b'%' {
out.push(byte as char);
continue;
}
let mut hex = String::with_capacity(2);
hex.push(bytes.next()? as char);
hex.push(bytes.next()? as char);
out.push(u8::from_str_radix(&hex, 16).ok()? as char);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_key_is_one_path_component_of_its_own_did() {
let key = RepoKey::of(&RepoDid::new("did:plc:abc123").unwrap());
assert_eq!(key.to_string(), "did%3Aplc%3Aabc123");
assert_eq!(key.dir(), Path::new("repos/did%3Aplc%3Aabc123"));
assert_eq!(RepoKey::parse_did("did:plc:abc123").unwrap(), key);
}
#[test]
fn a_cob_state_key_is_one_file_name() {
assert_eq!(CobStateKey::new("1-abc123").unwrap().as_str(), "1-abc123");
for key in ["", "../escape", "a/b", "a b", &"x".repeat(129)] {
assert!(CobStateKey::new(key).is_err(), "{key:?} must not be a key");
}
}
#[test]
fn a_name_that_is_not_its_own_encoding_is_not_a_repo_key() {
for name in [
"did:plc:abc",
"../escape",
"dir/did%3Aplc%3Aabc",
"%zz",
"%4",
] {
assert_eq!(RepoKey::encoded(name), None, "{name} must not be a key");
}
let key = RepoKey::of(&RepoDid::new("did:plc:abc").unwrap());
assert_eq!(RepoKey::encoded(key.as_str()), Some(key));
}
#[test]
fn a_name_that_is_not_a_did_names_no_repository() {
for name in ["", "not a did", "../escape", "did:plc:a/b"] {
assert!(
RepoKey::parse_did(name).is_err(),
"{name} must not be a key"
);
}
}
}