use anyhow::{Context, Result};
pub(crate) fn generate_content_addressed_session_id(json: &str) -> Result<String> {
use sha2::{Digest, Sha256};
let document: serde_json::Value =
serde_json::from_str(json).context("Failed to parse toolpath document")?;
let canonical = serde_json_canonicalizer::to_string(&document).context("serialize document")?;
let digest = Sha256::digest(canonical.as_bytes());
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&digest[..16]);
Ok(uuid::Builder::from_random_bytes(bytes)
.into_uuid()
.to_string())
}
pub(crate) fn parse_posix_dir(raw: &str) -> Result<String> {
use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf};
if raw.contains('\n') {
anyhow::bail!("the directory must be a single line (got {raw:?})");
}
let path = Utf8UnixPath::new(raw);
if !path.is_absolute() {
anyhow::bail!("the directory must be an absolute POSIX path (got {raw:?})");
}
let mut normalized = Utf8UnixPathBuf::new();
for component in path.components() {
match component {
Utf8UnixComponent::CurDir | Utf8UnixComponent::ParentDir => {
anyhow::bail!("the directory must not contain `.` or `..` (got {raw:?})")
}
component => normalized.push(component.as_str()),
}
}
let trimmed = raw
.strip_suffix('/')
.filter(|t| !t.is_empty())
.unwrap_or(raw);
if normalized.as_str() != trimmed {
anyhow::bail!("the directory must be in normalized form, {normalized} (got {raw:?})");
}
Ok(normalized.into_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_posix_dir_rejects_unnormalized_paths() {
for bad in ["relative/dir", "/a/../b", "/a/./b", "/a//b", "", "/a\nb"] {
assert!(parse_posix_dir(bad).is_err(), "{bad:?}");
}
assert_eq!(parse_posix_dir("/a/b/").unwrap(), "/a/b");
assert_eq!(parse_posix_dir("/").unwrap(), "/");
assert_eq!(parse_posix_dir("//").unwrap(), "/");
}
const DOC: &str = r#"{"a":1,"b":{"c":[1,2],"d":"x"}}"#;
const DOC_REORDERED: &str = "{ \"b\": {\"d\": \"x\", \"c\": [1, 2]}, \"a\": 1 }";
const DOC_CONTENT_ADDRESSED_ID: &str = "402a3ca5-2530-407e-9029-f96879adff54";
#[test]
fn generate_content_addressed_session_id_is_a_v4_uuid_of_the_key_sorted_document() {
let id = generate_content_addressed_session_id(DOC).unwrap();
assert_eq!(id, DOC_CONTENT_ADDRESSED_ID);
assert_eq!(
generate_content_addressed_session_id(DOC_REORDERED).unwrap(),
DOC_CONTENT_ADDRESSED_ID
);
assert_ne!(
generate_content_addressed_session_id(r#"{"a":2}"#).unwrap(),
DOC_CONTENT_ADDRESSED_ID
);
let uuid = uuid::Uuid::parse_str(&id).unwrap();
assert_eq!(uuid.get_version_num(), 4);
assert_eq!(uuid.get_variant(), uuid::Variant::RFC4122);
assert!(generate_content_addressed_session_id("not json").is_err());
}
}