OXYGENHUB
# 5b9c6ad45f6e oxygen / crates / oxy-core / src / store.rs
10.2 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 1 month ago source blame history raw
1
//! Loose object store (spec §13.1): one file per object under
2
//! `objects/loose/<hh>/<rest>`, written atomically and verified on read.
3
//! Packs come later.
4
5
use crate::attest::Attest;
6
use crate::hash::{hash_object, header, ObjectRef, ObjectType};
7
use crate::identity::{Identity, Signature};
8
use crate::objects::{Change, ObjectError, StateObject};
9
use rand::RngCore;
10
use std::fs;
11
use std::io::{self, Write};
12
use std::path::{Path, PathBuf};
13
14
#[derive(Debug, thiserror::Error)]
15
pub enum StoreError {
16
    #[error("io: {0}")]
17
    Io(#[from] io::Error),
18
    #[error("corrupt object {0}: {1}")]
19
    Corrupt(ObjectRef, String),
20
    #[error("object {0}")]
21
    Object(#[from] ObjectError),
22
    #[error("object {0} is a {1}, expected {2}")]
23
    WrongType(ObjectRef, &'static str, &'static str),
24
    #[error("object {0} not found")]
25
    NotFound(ObjectRef),
26
}
27
28
pub type Result<T> = std::result::Result<T, StoreError>;
29
30
#[derive(Debug, Clone)]
31
pub struct Store {
32
    dir: PathBuf,
33
}
34
35
impl Store {
36
    pub fn open(dir: impl Into<PathBuf>) -> io::Result<Store> {
37
        let dir = dir.into();
38
        fs::create_dir_all(dir.join("loose"))?;
39
        Ok(Store { dir })
40
    }
41
42
    pub fn dir(&self) -> &Path {
43
        &self.dir
44
    }
45
46
    fn path_for(&self, r: &ObjectRef) -> PathBuf {
47
        let h = r.to_hex();
48
        self.dir.join("loose").join(&h[..2]).join(&h[2..])
49
    }
50
51
    pub fn has(&self, r: &ObjectRef) -> bool {
52
        self.path_for(r).is_file()
53
    }
54
55
    /// Store `body` as an object of type `ty`; idempotent.
56
    pub fn put(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> {
57
        let r = hash_object(ty, body);
58
        let path = self.path_for(&r);
59
        if path.is_file() {
60
            return Ok(r);
61
        }
62
        fs::create_dir_all(path.parent().unwrap())?;
63
        let mut nonce = [0u8; 8];
64
        rand::rngs::OsRng.fill_bytes(&mut nonce);
65
        let tmp = self.dir.join("loose").join(format!("tmp-{}", hex::encode(nonce)));
66
        {
67
            let mut f = fs::File::create(&tmp)?;
68
            f.write_all(&header(ty, body.len()))?;
69
            f.write_all(body)?;
70
            f.sync_all()?;
71
        }
72
        fs::rename(&tmp, &path)?;
73
        Ok(r)
74
    }
75
76
    /// Read and verify an object.
77
    pub fn get(&self, r: &ObjectRef) -> Result<(ObjectType, Vec<u8>)> {
78
        let path = self.path_for(r);
79
        let data = match fs::read(&path) {
80
            Ok(d) => d,
81
            Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(StoreError::NotFound(*r)),
82
            Err(e) => return Err(e.into()),
83
        };
84
        let nul = data.iter().position(|&b| b == 0).ok_or_else(|| StoreError::Corrupt(*r, "no header".into()))?;
85
        let head = std::str::from_utf8(&data[..nul]).map_err(|_| StoreError::Corrupt(*r, "header not UTF-8".into()))?;
86
        let mut parts = head.split(' ');
87
        let (Some("oxy1"), Some(ty), Some(len), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
88
            return Err(StoreError::Corrupt(*r, format!("bad header {head:?}")));
89
        };
90
        let ty = ObjectType::from_name(ty).ok_or_else(|| StoreError::Corrupt(*r, format!("unknown type {ty}")))?;
91
        let len: usize = len.parse().map_err(|_| StoreError::Corrupt(*r, "bad length".into()))?;
92
        let body = &data[nul + 1..];
93
        if body.len() != len {
94
            return Err(StoreError::Corrupt(*r, format!("length {} != header {len}", body.len())));
95
        }
96
        if hash_object(ty, body) != *r {
97
            return Err(StoreError::Corrupt(*r, "hash mismatch".into()));
98
        }
99
        Ok((ty, body.to_vec()))
100
    }
101
102
    pub fn get_typed(&self, r: &ObjectRef, want: ObjectType) -> Result<Vec<u8>> {
103
        let (ty, body) = self.get(r)?;
104
        if ty != want {
105
            return Err(StoreError::WrongType(*r, ty.name(), want.name()));
106
        }
107
        Ok(body)
108
    }
109
110
    pub fn put_change(&self, c: &Change) -> Result<ObjectRef> {
111
        self.put(ObjectType::Change, &c.encode())
112
    }
113
114
    pub fn get_change(&self, r: &ObjectRef) -> Result<Change> {
115
        Ok(Change::decode(&self.get_typed(r, ObjectType::Change)?)?)
116
    }
117
118
    pub fn put_chunk(&self, bytes: &[u8]) -> Result<ObjectRef> {
119
        self.put(ObjectType::Chunk, bytes)
120
    }
121
122
    pub fn put_state(&self, st: &StateObject) -> Result<ObjectRef> {
123
        self.put(ObjectType::State, &st.encode())
124
    }
125
126
    pub fn get_state(&self, r: &ObjectRef) -> Result<StateObject> {
127
        Ok(StateObject::decode(&self.get_typed(r, ObjectType::State)?)?)
128
    }
129
130
    pub fn put_attest(&self, a: &Attest) -> Result<ObjectRef> {
131
        self.put(ObjectType::Attest, &a.encode())
132
    }
133
134
    pub fn get_attest(&self, r: &ObjectRef) -> Result<Attest> {
135
        Ok(Attest::decode(&self.get_typed(r, ObjectType::Attest)?)?)
136
    }
137
138
    fn seal_index_path(&self, change: &ObjectRef) -> PathBuf {
139
        let h = change.to_hex();
140
        self.dir.join("seals").join(&h[..2]).join(&h[2..])
141
    }
142
143
    /// Record that seal attestation `attest` covers `change`.
144
    pub fn index_seal(&self, change: &ObjectRef, attest: &ObjectRef) -> Result<()> {
145
        let mut seals = self.seals_for(change)?;
146
        if seals.contains(attest) {
147
            return Ok(());
148
        }
149
        seals.push(*attest);
150
        seals.sort();
151
        let path = self.seal_index_path(change);
152
        fs::create_dir_all(path.parent().unwrap())?;
153
        let text: String = seals.iter().map(|s| format!("{}\n", s.to_hex())).collect();
154
        let tmp = path.with_extension("tmp");
155
        fs::write(&tmp, text)?;
156
        fs::rename(&tmp, &path)?;
157
        Ok(())
158
    }
159
160
    /// Seal attestations known to cover `change`.
161
    pub fn seals_for(&self, change: &ObjectRef) -> Result<Vec<ObjectRef>> {
162
        match fs::read_to_string(self.seal_index_path(change)) {
163
            Ok(t) => Ok(t.lines().filter_map(ObjectRef::from_hex).collect()),
164
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(vec![]),
165
            Err(e) => Err(e.into()),
166
        }
167
    }
168
169
    pub fn put_identity(&self, id: &Identity) -> Result<ObjectRef> {
170
        self.put(ObjectType::Identity, &id.encode())
171
    }
172
173
    pub fn get_identity(&self, r: &ObjectRef) -> Result<Identity> {
174
        Ok(Identity::decode(&self.get_typed(r, ObjectType::Identity)?)?)
175
    }
176
177
    fn sig_path(&self, r: &ObjectRef) -> PathBuf {
178
        let h = r.to_hex();
179
        self.dir.join("sigs").join(&h[..2]).join(&h[2..])
180
    }
181
182
    /// Signatures stored alongside object `r` (spec §5.4). Empty if none.
183
    pub fn signatures(&self, r: &ObjectRef) -> Result<Vec<Signature>> {
184
        match fs::read(self.sig_path(r)) {
185
            Ok(b) => Ok(Signature::decode_list(&b)?),
186
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(vec![]),
187
            Err(e) => Err(e.into()),
188
        }
189
    }
190
191
    /// Attach a signature to object `r`, replacing any earlier one by the same key.
192
    pub fn put_signature(&self, r: &ObjectRef, sig: &Signature) -> Result<()> {
193
        let mut sigs = self.signatures(r)?;
194
        sigs.retain(|s| s.key != sig.key);
195
        sigs.push(sig.clone());
196
        sigs.sort_by_key(|s| s.key);
197
        let path = self.sig_path(r);
198
        fs::create_dir_all(path.parent().unwrap())?;
199
        let mut nonce = [0u8; 8];
200
        rand::rngs::OsRng.fill_bytes(&mut nonce);
201
        let tmp = self.dir.join(format!("sigs-tmp-{}", hex::encode(nonce)));
202
        {
203
            let mut f = fs::File::create(&tmp)?;
204
            f.write_all(&Signature::encode_list(&sigs))?;
205
            f.sync_all()?;
206
        }
207
        fs::rename(&tmp, &path)?;
208
        Ok(())
209
    }
210
211
    /// Every object reference in the store (slow; for gc and tests).
212
    pub fn list(&self) -> io::Result<Vec<ObjectRef>> {
213
        let mut out = Vec::new();
214
        let loose = self.dir.join("loose");
215
        for d in fs::read_dir(&loose)? {
216
            let d = d?;
217
            if !d.file_type()?.is_dir() {
218
                continue;
219
            }
220
            let prefix = d.file_name().to_string_lossy().to_string();
221
            for f in fs::read_dir(d.path())? {
222
                let f = f?;
223
                let name = f.file_name().to_string_lossy().to_string();
224
                if let Some(r) = ObjectRef::from_hex(&format!("{prefix}{name}")) {
225
                    out.push(r);
226
                }
227
            }
228
        }
229
        out.sort();
230
        Ok(out)
231
    }
232
}
233
234
#[cfg(test)]
235
mod tests {
236
    use super::*;
237
238
    #[test]
239
    fn put_get_verify() {
240
        let dir = tempfile::tempdir().unwrap();
241
        let s = Store::open(dir.path().join("objects")).unwrap();
242
        let r = s.put(ObjectType::Chunk, b"hello").unwrap();
243
        assert!(s.has(&r));
244
        assert_eq!(s.get(&r).unwrap(), (ObjectType::Chunk, b"hello".to_vec()));
245
        assert_eq!(s.put(ObjectType::Chunk, b"hello").unwrap(), r);
246
        assert_eq!(s.list().unwrap(), vec![r]);
247
        assert!(matches!(s.get_typed(&r, ObjectType::Blob), Err(StoreError::WrongType(..))));
248
        assert!(matches!(s.get(&ObjectRef([9; 32])), Err(StoreError::NotFound(_))));
249
250
        // Tamper: flip a byte, read must fail.
251
        let path = s.path_for(&r);
252
        let mut data = fs::read(&path).unwrap();
253
        let last = data.len() - 1;
254
        data[last] ^= 1;
255
        fs::write(&path, data).unwrap();
256
        assert!(matches!(s.get(&r), Err(StoreError::Corrupt(..))));
257
    }
258
259
    #[test]
260
    fn signatures_are_stored_beside_objects() {
261
        use crate::identity::{Domain, SecretKey};
262
        let dir = tempfile::tempdir().unwrap();
263
        let s = Store::open(dir.path()).unwrap();
264
        let r = s.put(ObjectType::Chunk, b"x").unwrap();
265
        assert!(s.signatures(&r).unwrap().is_empty());
266
        let sk = SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] };
267
        let id = sk.identity("a", "b", 0);
268
        let key = s.put_identity(&id).unwrap();
269
        assert_eq!(s.get_identity(&key).unwrap(), id);
270
        let sig = sk.sign(Domain::Change, &key, &r);
271
        s.put_signature(&r, &sig).unwrap();
272
        s.put_signature(&r, &sig).unwrap();
273
        assert_eq!(s.signatures(&r).unwrap(), vec![sig]);
274
        // The object itself is untouched by its signatures.
275
        assert_eq!(s.get(&r).unwrap().1, b"x");
276
    }
277
278
    #[test]
279
    fn change_round_trip_through_store() {
280
        let dir = tempfile::tempdir().unwrap();
281
        let s = Store::open(dir.path()).unwrap();
282
        let c = crate::objects::tests::sample_change();
283
        let r = s.put_change(&c).unwrap();
284
        assert_eq!(r, c.hash());
285
        assert_eq!(s.get_change(&r).unwrap(), c);
286
    }
287
}