1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Container-backed object store (RFC 102 Stage 3). Public API (`FileObjectStore`, `ObjectReader`,
//! `ObjectWriter`) is unchanged from the loose-file implementation this replaces -- every other call
//! site in the workspace uses only that trait interface, so none of them needed to change. Only the
//! internals moved: reads and writes now go through `index.rs`'s lookup/write-protocol functions,
//! which target `container.rs`'s per-type container files instead of one file per object.
use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectEnvelope, ObjectId, ObjectType};
use crate::index::{lookup_object_location, read_object_envelope_at, write_object_to_container};
use crate::layout::RepositoryLayout;
/// Read-only object access boundary.
pub trait ObjectReader {
/// Read an object by ID.
fn read_object(&self, id: ObjectId) -> Result<Option<ObjectEnvelope>>;
}
/// Write object boundary.
pub trait ObjectWriter {
/// Write an object envelope after validation.
fn write_object(&mut self, envelope: &ObjectEnvelope) -> Result<ObjectId>;
}
/// File-backed object store.
#[derive(Debug, Clone)]
pub struct FileObjectStore {
layout: RepositoryLayout,
}
impl FileObjectStore {
/// Create a file object store for a repository layout.
#[must_use]
pub fn new(layout: RepositoryLayout) -> Self {
Self { layout }
}
/// Return the repository layout.
#[must_use]
pub fn layout(&self) -> &RepositoryLayout {
&self.layout
}
/// Return true if an object with this id and type is indexed.
#[must_use]
pub fn contains_object(&self, object_type: ObjectType, id: ObjectId) -> bool {
if object_type == ObjectType::RefUpdate {
return false;
}
matches!(
lookup_object_location(&self.layout, id),
Ok(Some(entry)) if entry.object_type == object_type
)
}
/// Read and require a specific object type.
pub fn read_typed(
&self,
id: ObjectId,
object_type: ObjectType,
) -> Result<Option<ObjectEnvelope>> {
let Some(envelope) = self.read_object(id)? else {
return Ok(None);
};
if envelope.object_type != object_type {
return Err(PrikkError::ObjectTypeMismatch {
expected: object_type.to_string(),
actual: envelope.object_type.to_string(),
});
}
Ok(Some(envelope))
}
}
impl ObjectReader for FileObjectStore {
fn read_object(&self, id: ObjectId) -> Result<Option<ObjectEnvelope>> {
let Some(entry) = lookup_object_location(&self.layout, id)? else {
return Ok(None);
};
// "One seek" (design §12/§10.3): the index already named exactly where this object is, so
// this decodes directly at that offset rather than scanning the container from the start.
let envelope = read_object_envelope_at(&self.layout, &entry)?;
let computed = envelope.object_id();
if computed != id {
// The read validation the ruling requires: the index is trusted for *location*, but the
// bytes found there are always checked against the id actually asked for by recomputing
// it from the decoded content -- free, since decoding already happened. A mismatch is
// reported, never silently accepted and never a fallback to scanning.
return Err(PrikkError::Integrity(format!(
"index entry for {id} resolves to an envelope with computed id {computed}"
)));
}
if envelope.object_type != entry.object_type {
return Err(PrikkError::Integrity(format!(
"index entry for {id} names type {}, envelope decoded as {}",
entry.object_type, envelope.object_type
)));
}
crate::format::validate_read_schema(self.layout.format(), &envelope)?;
Ok(Some(envelope))
}
}
impl ObjectWriter for FileObjectStore {
fn write_object(&mut self, envelope: &ObjectEnvelope) -> Result<ObjectId> {
if envelope.object_type == ObjectType::RefUpdate {
return Err(PrikkError::UnsupportedObjectType(
"RefUpdate is stored inline in ref logs for v1".to_string(),
));
}
self.layout.validate_format()?;
crate::format::validate_object_envelope(self.layout.format(), envelope)?;
// The write protocol (design §5, handoff §3) lives in `index.rs`, not here: append the
// object record to its container and make it durable, then and only then append the index
// entry. Stated at that call site too, not only here.
write_object_to_container(&self.layout, envelope.object_type, envelope)
}
}
// DC-71/DC-81: every test here sets up its scenario via real repository mutation
// (RepositoryLayout::init or equivalent), which is Linux/macOS-only; the module never compiles a
// test meaningful on any other platform.
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
mod tests;