use std::path::{Component, Path, PathBuf};
use super::atomic::sha256_hex;
use super::compose::split_front_matter;
pub fn resolve_within_content_root(content_root: &Path, rel_path: &str) -> anyhow::Result<PathBuf> {
if Path::new(rel_path).is_absolute() {
return Err(anyhow::anyhow!(
"[content_store::read] rejected absolute path"
));
}
let mut abs = content_root.to_path_buf();
for component in rel_path.split('/') {
if component.is_empty() || component == "." {
continue;
}
match Path::new(component).components().next() {
Some(Component::Normal(_)) => abs.push(component),
_ => {
return Err(anyhow::anyhow!(
"[content_store::read] rejected unsafe path component"
));
}
}
}
if abs.exists() {
let canon_root = content_root
.canonicalize()
.unwrap_or_else(|_| content_root.to_path_buf());
let canon_abs = abs
.canonicalize()
.map_err(|e| anyhow::anyhow!("[content_store::read] canonicalize failed: {e}"))?;
if !canon_abs.starts_with(&canon_root) {
return Err(anyhow::anyhow!(
"[content_store::read] resolved path escapes content_root"
));
}
}
Ok(abs)
}
#[derive(Debug, Clone)]
pub struct ChunkFileContents {
pub body: String,
pub sha256: String,
}
pub fn read_chunk_file(abs_path: &Path) -> anyhow::Result<ChunkFileContents> {
let raw = std::fs::read(abs_path).map_err(|e| anyhow::anyhow!("read {:?}: {e}", abs_path))?;
let content = std::str::from_utf8(&raw)
.map_err(|e| anyhow::anyhow!("invalid UTF-8 in {:?}: {e}", abs_path))?;
let (_fm, body) = split_front_matter(content)
.ok_or_else(|| anyhow::anyhow!("no front-matter in {:?}", abs_path))?;
let sha256 = sha256_hex(body.as_bytes());
Ok(ChunkFileContents {
body: body.to_string(),
sha256,
})
}
pub fn verify_chunk_file(abs_path: &Path, expected_sha256: &str) -> anyhow::Result<bool> {
let contents = read_chunk_file(abs_path)?;
Ok(contents.sha256 == expected_sha256)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyResult {
Ok,
Mismatch { actual: String },
Missing,
}
pub fn read_summary_file(abs_path: &Path) -> anyhow::Result<ChunkFileContents> {
read_chunk_file(abs_path)
}
pub fn verify_summary_file(abs_path: &Path, expected_sha256: &str) -> anyhow::Result<VerifyResult> {
if !abs_path.exists() {
return Ok(VerifyResult::Missing);
}
let contents = read_summary_file(abs_path)?;
if contents.sha256 == expected_sha256 {
Ok(VerifyResult::Ok)
} else {
Ok(VerifyResult::Mismatch {
actual: contents.sha256,
})
}
}
#[cfg(test)]
#[path = "read_tests.rs"]
mod tests;