use std::path::Path;
use super::backend::{ContentStore, LocalContentStore, StoreBackend};
use super::body_artifact::{parse_note_body_artifact, validate_note_body_artifact_bytes};
use super::object_artifact::{
ObjectArtifact, decode_and_validate_object_artifact, object_content_ref_for_hash,
};
use crate::error::{Result, ShoreError};
use crate::model::ObjectId;
use crate::session::evidence::RelationProofManifestV1;
use crate::storage::{CreateOutcome, RemoveOutcome};
pub(crate) struct ContentArtifacts {
store: Box<dyn ContentStore>,
}
impl ContentArtifacts {
pub(crate) fn local(store_dir: &Path) -> Self {
Self {
store: Box::new(LocalContentStore::new(store_dir)),
}
}
pub(crate) fn from_backend(backend: &StoreBackend) -> Self {
Self {
store: backend.content_store(),
}
}
pub(crate) fn put_object(
&self,
content_ref: &str,
bytes: &[u8],
artifact: ObjectArtifact,
) -> Result<ObjectArtifact> {
match self.store.put_once(content_ref, bytes)? {
CreateOutcome::Created => Ok(artifact),
CreateOutcome::AlreadyExists => {
let existing_bytes = self.store.get(content_ref)?;
let existing = decode_and_validate_object_artifact(&existing_bytes)?;
if existing.snapshot == artifact.snapshot {
Ok(existing)
} else {
Err(ShoreError::Message(format!(
"object artifact conflict for {}",
artifact.snapshot.object_id.as_str()
)))
}
}
}
}
pub(crate) fn put_relation_proof(
&self,
proof: &RelationProofManifestV1,
) -> Result<CreateOutcome> {
proof.validate()?;
let hash = proof
.evidence_sha256
.strip_prefix("sha256:")
.ok_or_else(|| {
ShoreError::Message("relation proof hash is not canonical".to_owned())
})?;
let content_ref = format!("artifacts/proofs/{hash}.json");
let bytes = serde_json::to_vec(proof)?;
match self.store.put_once(&content_ref, &bytes)? {
CreateOutcome::Created => Ok(CreateOutcome::Created),
CreateOutcome::AlreadyExists => {
let existing: RelationProofManifestV1 =
serde_json::from_slice(&self.store.get(&content_ref)?)?;
existing.validate()?;
if existing == *proof {
Ok(CreateOutcome::AlreadyExists)
} else {
Err(ShoreError::Message(format!(
"relation proof conflict for {}",
proof.evidence_sha256
)))
}
}
}
}
pub(crate) fn read_object_bytes(
&self,
content_ref: &str,
object_id: &ObjectId,
) -> Result<Vec<u8>> {
#[cfg(any(test, feature = "longitudinal-counting"))]
crate::bench_support::longitudinal::record_object_artifact_read_attempt();
match self.store.get_if_exists(content_ref)? {
Some(bytes) => {
#[cfg(any(test, feature = "longitudinal-counting"))]
crate::bench_support::longitudinal::record_object_artifact_bytes(bytes.len());
Ok(bytes)
}
None => Err(missing_object_artifact(object_id)),
}
}
pub(crate) fn read_object_bytes_if_exists(&self, content_ref: &str) -> Result<Option<Vec<u8>>> {
#[cfg(any(test, feature = "longitudinal-counting"))]
crate::bench_support::longitudinal::record_object_artifact_read_attempt();
let bytes = self.store.get_if_exists(content_ref)?;
#[cfg(any(test, feature = "longitudinal-counting"))]
if let Some(bytes) = &bytes {
crate::bench_support::longitudinal::record_object_artifact_bytes(bytes.len());
}
Ok(bytes)
}
pub(crate) fn import_object(
&self,
object_id: &ObjectId,
expected_content_hash: &str,
bytes: &[u8],
) -> Result<CreateOutcome> {
let artifact = decode_and_validate_object_artifact(bytes)?;
if artifact.snapshot.object_id != *object_id {
return Err(ShoreError::Message(format!(
"object artifact locator mismatch for {}",
object_id.as_str()
)));
}
if artifact.content_hash != expected_content_hash {
return Err(ShoreError::Message(format!(
"object artifact content hash mismatch for {expected_content_hash}"
)));
}
let content_ref = object_content_ref_for_hash(expected_content_hash);
match self.store.put_once(&content_ref, bytes)? {
CreateOutcome::Created => Ok(CreateOutcome::Created),
CreateOutcome::AlreadyExists => {
let existing = decode_and_validate_object_artifact(&self.store.get(&content_ref)?)?;
if existing == artifact {
Ok(CreateOutcome::AlreadyExists)
} else {
Err(ShoreError::Message(format!(
"object artifact conflict for {}",
object_id.as_str()
)))
}
}
}
}
pub(crate) fn put_note_body(&self, content_ref: &str, bytes: &[u8]) -> Result<CreateOutcome> {
match self.store.put_once(content_ref, bytes)? {
CreateOutcome::Created => Ok(CreateOutcome::Created),
CreateOutcome::AlreadyExists => {
let existing = self.store.get(content_ref)?;
if existing == bytes {
Ok(CreateOutcome::AlreadyExists)
} else {
Err(ShoreError::Message(format!(
"note body artifact conflict for {content_ref}"
)))
}
}
}
}
pub(crate) fn import_body(
&self,
content_ref: &str,
expected_content_hash: &str,
bytes: &[u8],
) -> Result<CreateOutcome> {
let artifact =
validate_note_body_artifact_bytes(content_ref, expected_content_hash, bytes)?;
match self.store.put_once(content_ref, bytes)? {
CreateOutcome::Created => Ok(CreateOutcome::Created),
CreateOutcome::AlreadyExists => {
let existing = validate_note_body_artifact_bytes(
content_ref,
expected_content_hash,
&self.store.get(content_ref)?,
)?;
if existing == artifact {
Ok(CreateOutcome::AlreadyExists)
} else {
Err(ShoreError::Message(format!(
"note body artifact conflict for {expected_content_hash}"
)))
}
}
}
}
pub(crate) fn read_note_body_bytes(
&self,
content_ref: &str,
expected_content_hash: &str,
) -> Result<Vec<u8>> {
#[cfg(any(test, feature = "longitudinal-counting"))]
crate::bench_support::longitudinal::record_body_artifact_read_attempt();
match self.store.get_if_exists(content_ref)? {
Some(bytes) => {
#[cfg(any(test, feature = "longitudinal-counting"))]
crate::bench_support::longitudinal::record_body_artifact_bytes(bytes.len());
validate_note_body_artifact_bytes(content_ref, expected_content_hash, &bytes)?;
Ok(bytes)
}
None => Err(ShoreError::Message(format!(
"missing artifact {expected_content_hash}; import referenced artifacts before reading"
))),
}
}
pub(crate) fn read_note_body(&self, content_ref: &str) -> Result<String> {
#[cfg(any(test, feature = "longitudinal-counting"))]
crate::bench_support::longitudinal::record_body_artifact_read_attempt();
match self.store.get_if_exists(content_ref)? {
Some(bytes) => {
#[cfg(any(test, feature = "longitudinal-counting"))]
crate::bench_support::longitudinal::record_body_artifact_bytes(bytes.len());
Ok(parse_note_body_artifact(&bytes)?.body)
}
None => Err(ShoreError::Message(format!(
"missing artifact {content_ref}; import referenced artifacts before reading"
))),
}
}
pub(crate) fn list_refs(&self, prefix: &str) -> Result<Vec<String>> {
self.store.list(prefix)
}
pub(crate) fn get_if_exists(&self, content_ref: &str) -> Result<Option<Vec<u8>>> {
self.store.get_if_exists(content_ref)
}
pub(crate) fn remove(&self, content_ref: &str) -> Result<RemoveOutcome> {
self.store.remove(content_ref)
}
}
fn missing_object_artifact(object_id: &ObjectId) -> ShoreError {
ShoreError::Message(format!(
"missing artifact for snapshot {}; import referenced artifacts before reading",
object_id.as_str()
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bench_support::longitudinal::LongitudinalCountingScopeV1;
use crate::canonical_hash::sha256_bytes_hex;
use crate::model::{DiffSnapshot, ReviewId};
use crate::session::store::body_artifact::NoteBodyEnvelope;
use crate::session::store::object_artifact::build_object_artifact_v2;
fn each_backend() -> Vec<(Option<tempfile::TempDir>, StoreBackend)> {
let root = tempfile::tempdir().unwrap();
let store_dir = root.path().join(".pointbreak/data");
vec![
(Some(root), StoreBackend::Local(store_dir)),
(None, StoreBackend::memory()),
]
}
fn valid_object() -> (ObjectArtifact, Vec<u8>) {
let artifact =
build_object_artifact_v2(DiffSnapshot::empty(ReviewId::new("review:test"))).unwrap();
let bytes = serde_json::to_vec(&artifact).unwrap();
(artifact, bytes)
}
fn counting_scope(byte: char) -> LongitudinalCountingScopeV1 {
LongitudinalCountingScopeV1::new(std::iter::repeat_n(byte, 64).collect::<String>())
.expect("valid counting scope")
}
#[test]
fn counting_calibrates_inline_external_body_and_selected_object_reads() {
let root = tempfile::tempdir().unwrap();
let backend = StoreBackend::Local(root.path().join(".pointbreak/data"));
let content = ContentArtifacts::from_backend(&backend);
let inline = counting_scope('5');
{
let _guard = inline.enter();
let outcome =
super::super::body_artifact::stage_body_artifact(b"inline").expect("inline body");
assert!(matches!(
outcome,
super::super::body_artifact::BodyArtifactOutcome::Inline { .. }
));
}
assert_eq!(inline.snapshot().counters.body_artifact_reads, 0);
let body = "x".repeat(super::super::body_artifact::BODY_INLINE_LIMIT + 1);
let outcome =
super::super::body_artifact::stage_body_artifact(body.as_bytes()).expect("body stages");
let super::super::body_artifact::BodyArtifactOutcome::Artifact {
relative_path,
body_envelope,
..
} = outcome
else {
panic!("body should externalize");
};
let body_bytes = body_envelope.to_json_bytes().expect("body bytes");
content
.put_note_body(&relative_path, &body_bytes)
.expect("body stored");
let external = counting_scope('6');
{
let _guard = external.enter();
assert_eq!(
content.read_note_body(&relative_path).expect("body reads"),
body
);
}
let external = external.snapshot().counters;
assert_eq!(external.body_artifact_reads, 1);
assert_eq!(external.body_bytes_read, body_bytes.len() as u64);
assert_eq!(external.object_artifact_reads, 0);
let (artifact, object_bytes) = valid_object();
let object_id = artifact.snapshot.object_id.clone();
let object_ref = object_content_ref_for_hash(&artifact.content_hash);
content
.put_object(&object_ref, &object_bytes, artifact)
.expect("object stored");
let object = counting_scope('7');
{
let _guard = object.enter();
assert_eq!(
content
.read_object_bytes(&object_ref, &object_id)
.expect("object reads"),
object_bytes
);
}
let object = object.snapshot().counters;
assert_eq!(object.object_artifact_reads, 1);
assert_eq!(object.object_bytes_read, object_bytes.len() as u64);
assert_eq!(object.body_artifact_reads, 0);
}
#[test]
fn object_put_dedup_and_read_validate_hold_over_every_backend() {
let (artifact, bytes) = valid_object();
let object_id = artifact.snapshot.object_id.clone();
let content_ref = "artifacts/objects/test.json";
for (_guard, backend) in each_backend() {
let content = ContentArtifacts::from_backend(&backend);
assert_eq!(
content
.put_object(content_ref, &bytes, artifact.clone())
.unwrap(),
artifact
);
assert_eq!(
content
.put_object(content_ref, &bytes, artifact.clone())
.unwrap(),
artifact
);
let read = content.read_object_bytes(content_ref, &object_id).unwrap();
assert_eq!(read, bytes);
assert_eq!(
decode_and_validate_object_artifact(&read).unwrap(),
artifact
);
let other = build_object_artifact_v2(DiffSnapshot::new(
ReviewId::new("review:other"),
ObjectId::new("other"),
Vec::new(),
))
.unwrap();
let other_bytes = serde_json::to_vec(&other).unwrap();
assert!(
content
.put_object(content_ref, &other_bytes, other)
.unwrap_err()
.to_string()
.contains("conflict")
);
assert_eq!(
content
.read_object_bytes_if_exists("artifacts/objects/missing.json")
.unwrap(),
None
);
assert!(
content
.read_object_bytes("artifacts/objects/missing.json", &object_id)
.unwrap_err()
.to_string()
.contains("import referenced artifacts")
);
}
}
#[test]
fn put_note_body_creates_then_dedups_over_every_backend() {
let valid = NoteBodyEnvelope::new("the staged body".to_owned())
.to_json_bytes()
.unwrap();
let content_ref = "artifacts/notes/staged.json";
for (_guard, backend) in each_backend() {
let content = ContentArtifacts::from_backend(&backend);
assert_eq!(
content.put_note_body(content_ref, &valid).unwrap(),
CreateOutcome::Created
);
assert_eq!(
content.put_note_body(content_ref, &valid).unwrap(),
CreateOutcome::AlreadyExists
);
assert_eq!(
content.read_note_body(content_ref).unwrap(),
"the staged body"
);
let divergent = NoteBodyEnvelope::new("a different body".to_owned())
.to_json_bytes()
.unwrap();
assert!(
content
.put_note_body(content_ref, &divergent)
.unwrap_err()
.to_string()
.contains("conflict")
);
}
}
#[test]
fn note_body_read_parses_and_validates_over_every_backend() {
let valid = NoteBodyEnvelope::new("the body".to_owned())
.to_json_bytes()
.unwrap();
let wrong_schema = br#"{"schema":"wrong","version":1,"body":"x"}"#;
for (_guard, backend) in each_backend() {
let store = backend.content_store();
store.put_once("artifacts/notes/good.json", &valid).unwrap();
store
.put_once("artifacts/notes/bad.json", wrong_schema)
.unwrap();
let content = ContentArtifacts::from_backend(&backend);
assert_eq!(
content.read_note_body("artifacts/notes/good.json").unwrap(),
"the body"
);
assert!(
content
.read_note_body("artifacts/notes/bad.json")
.unwrap_err()
.to_string()
.contains("Unsupported note body artifact")
);
assert!(
content
.read_note_body("artifacts/notes/missing.json")
.unwrap_err()
.to_string()
.contains("import referenced artifacts")
);
}
}
#[test]
fn read_note_body_bytes_validates_and_returns_raw_bytes_over_every_backend() {
let body = "the exported body";
let valid = NoteBodyEnvelope::new(body.to_owned())
.to_json_bytes()
.unwrap();
let hash = format!("sha256:{}", sha256_bytes_hex(body.as_bytes()));
let content_ref = format!("artifacts/notes/{}.json", sha256_bytes_hex(body.as_bytes()));
for (_guard, backend) in each_backend() {
backend
.content_store()
.put_once(&content_ref, &valid)
.unwrap();
let content = ContentArtifacts::from_backend(&backend);
assert_eq!(
content.read_note_body_bytes(&content_ref, &hash).unwrap(),
valid
);
let wrong_hash = "sha256:".to_owned() + &"0".repeat(64);
assert!(
content
.read_note_body_bytes(&content_ref, &wrong_hash)
.unwrap_err()
.to_string()
.contains("hash mismatch")
);
assert!(
content
.read_note_body_bytes("artifacts/notes/missing.json", &hash)
.unwrap_err()
.to_string()
.contains("import referenced artifacts")
);
}
}
#[test]
fn import_object_validates_locator_hash_and_dedups_or_conflicts_over_every_backend() {
let (artifact, bytes) = valid_object();
let object_id = artifact.snapshot.object_id.clone();
let expected = artifact.content_hash.clone();
for (_guard, backend) in each_backend() {
let content = ContentArtifacts::from_backend(&backend);
assert_eq!(
content
.import_object(&object_id, &expected, &bytes)
.unwrap(),
CreateOutcome::Created
);
assert_eq!(
content
.import_object(&object_id, &expected, &bytes)
.unwrap(),
CreateOutcome::AlreadyExists
);
let wrong = "sha256:".to_owned() + &"0".repeat(64);
assert!(
content
.import_object(&object_id, &wrong, &bytes)
.unwrap_err()
.to_string()
.contains("content hash mismatch")
);
let other = build_object_artifact_v2(DiffSnapshot::new(
ReviewId::new("review:other"),
ObjectId::new("obj:sha256:other"),
Vec::new(),
))
.unwrap();
let other_bytes = serde_json::to_vec(&other).unwrap();
assert!(
content
.import_object(&object_id, &other.content_hash, &other_bytes)
.unwrap_err()
.to_string()
.contains("locator mismatch")
);
let collide = ObjectId::new("obj:sha256:collide");
let stored = build_object_artifact_v2(DiffSnapshot::new(
ReviewId::new("review:a"),
collide.clone(),
Vec::new(),
))
.unwrap();
let incoming = build_object_artifact_v2(DiffSnapshot::new(
ReviewId::new("review:b"),
collide.clone(),
Vec::new(),
))
.unwrap();
assert_ne!(
stored.content_hash, incoming.content_hash,
"the fixtures diverge"
);
content
.import_object(
&collide,
&stored.content_hash,
&serde_json::to_vec(&stored).unwrap(),
)
.unwrap();
assert_eq!(
content
.import_object(
&collide,
&incoming.content_hash,
&serde_json::to_vec(&incoming).unwrap()
)
.unwrap(),
CreateOutcome::Created
);
let stored_ref = object_content_ref_for_hash(&stored.content_hash);
let incoming_ref = object_content_ref_for_hash(&incoming.content_hash);
let stored_read = decode_and_validate_object_artifact(
&content.read_object_bytes(&stored_ref, &collide).unwrap(),
)
.unwrap();
let incoming_read = decode_and_validate_object_artifact(
&content.read_object_bytes(&incoming_ref, &collide).unwrap(),
)
.unwrap();
assert_eq!(stored_read, stored);
assert_eq!(incoming_read, incoming);
}
}
#[test]
fn import_body_validates_and_dedups_or_conflicts_over_every_backend() {
let body = "imported body";
let valid = NoteBodyEnvelope::new(body.to_owned())
.to_json_bytes()
.unwrap();
let hash = format!("sha256:{}", sha256_bytes_hex(body.as_bytes()));
let content_ref = format!("artifacts/notes/{}.json", sha256_bytes_hex(body.as_bytes()));
for (_guard, backend) in each_backend() {
let content = ContentArtifacts::from_backend(&backend);
assert_eq!(
content.import_body(&content_ref, &hash, &valid).unwrap(),
CreateOutcome::Created
);
assert_eq!(
content.import_body(&content_ref, &hash, &valid).unwrap(),
CreateOutcome::AlreadyExists
);
let pretty =
serde_json::to_vec_pretty(&NoteBodyEnvelope::new(body.to_owned())).unwrap();
assert_ne!(pretty, valid, "the fixture is byte-different");
assert_eq!(
content.import_body(&content_ref, &hash, &pretty).unwrap(),
CreateOutcome::AlreadyExists
);
let wrong_schema = br#"{"schema":"wrong","version":1,"body":"x"}"#;
assert!(
content
.import_body(&content_ref, &hash, wrong_schema)
.unwrap_err()
.to_string()
.contains("Unsupported note body artifact")
);
}
}
#[test]
fn read_then_decode_rejects_tampered_object_bytes_over_every_backend() {
let (artifact, bytes) = valid_object();
let object_id = artifact.snapshot.object_id.clone();
let content_ref = "artifacts/objects/test.json";
let mut json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
json["contentHash"] = serde_json::json!(
"sha256:0000000000000000000000000000000000000000000000000000000000000000"
);
let tampered = serde_json::to_vec(&json).unwrap();
for (_guard, backend) in each_backend() {
backend
.content_store()
.put_raw(content_ref, &tampered)
.unwrap();
let content = ContentArtifacts::from_backend(&backend);
let read = content.read_object_bytes(content_ref, &object_id).unwrap();
let error = decode_and_validate_object_artifact(&read)
.expect_err("tampered object bytes are rejected on decode");
assert!(error.to_string().contains("content hash"));
}
}
}