OXYGENHUB
# 5140e604192e oxygen / crates / oxy-core / src / store.rs
6.1 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::hash::{hash_object, header, ObjectRef, ObjectType};
6
use crate::objects::{Change, ObjectError};
7
use rand::RngCore;
8
use std::fs;
9
use std::io::{self, Write};
10
use std::path::{Path, PathBuf};
11
12
#[derive(Debug, thiserror::Error)]
13
pub enum StoreError {
14
    #[error("io: {0}")]
15
    Io(#[from] io::Error),
16
    #[error("corrupt object {0}: {1}")]
17
    Corrupt(ObjectRef, String),
18
    #[error("object {0}")]
19
    Object(#[from] ObjectError),
20
    #[error("object {0} is a {1}, expected {2}")]
21
    WrongType(ObjectRef, &'static str, &'static str),
22
    #[error("object {0} not found")]
23
    NotFound(ObjectRef),
24
}
25
26
pub type Result<T> = std::result::Result<T, StoreError>;
27
28
#[derive(Debug, Clone)]
29
pub struct Store {
30
    dir: PathBuf,
31
}
32
33
impl Store {
34
    pub fn open(dir: impl Into<PathBuf>) -> io::Result<Store> {
35
        let dir = dir.into();
36
        fs::create_dir_all(dir.join("loose"))?;
37
        Ok(Store { dir })
38
    }
39
40
    pub fn dir(&self) -> &Path {
41
        &self.dir
42
    }
43
44
    fn path_for(&self, r: &ObjectRef) -> PathBuf {
45
        let h = r.to_hex();
46
        self.dir.join("loose").join(&h[..2]).join(&h[2..])
47
    }
48
49
    pub fn has(&self, r: &ObjectRef) -> bool {
50
        self.path_for(r).is_file()
51
    }
52
53
    /// Store `body` as an object of type `ty`; idempotent.
54
    pub fn put(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> {
55
        let r = hash_object(ty, body);
56
        let path = self.path_for(&r);
57
        if path.is_file() {
58
            return Ok(r);
59
        }
60
        fs::create_dir_all(path.parent().unwrap())?;
61
        let mut nonce = [0u8; 8];
62
        rand::rngs::OsRng.fill_bytes(&mut nonce);
63
        let tmp = self.dir.join("loose").join(format!("tmp-{}", hex::encode(nonce)));
64
        {
65
            let mut f = fs::File::create(&tmp)?;
66
            f.write_all(&header(ty, body.len()))?;
67
            f.write_all(body)?;
68
            f.sync_all()?;
69
        }
70
        fs::rename(&tmp, &path)?;
71
        Ok(r)
72
    }
73
74
    /// Read and verify an object.
75
    pub fn get(&self, r: &ObjectRef) -> Result<(ObjectType, Vec<u8>)> {
76
        let path = self.path_for(r);
77
        let data = match fs::read(&path) {
78
            Ok(d) => d,
79
            Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(StoreError::NotFound(*r)),
80
            Err(e) => return Err(e.into()),
81
        };
82
        let nul = data.iter().position(|&b| b == 0).ok_or_else(|| StoreError::Corrupt(*r, "no header".into()))?;
83
        let head = std::str::from_utf8(&data[..nul]).map_err(|_| StoreError::Corrupt(*r, "header not UTF-8".into()))?;
84
        let mut parts = head.split(' ');
85
        let (Some("oxy1"), Some(ty), Some(len), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
86
            return Err(StoreError::Corrupt(*r, format!("bad header {head:?}")));
87
        };
88
        let ty = ObjectType::from_name(ty).ok_or_else(|| StoreError::Corrupt(*r, format!("unknown type {ty}")))?;
89
        let len: usize = len.parse().map_err(|_| StoreError::Corrupt(*r, "bad length".into()))?;
90
        let body = &data[nul + 1..];
91
        if body.len() != len {
92
            return Err(StoreError::Corrupt(*r, format!("length {} != header {len}", body.len())));
93
        }
94
        if hash_object(ty, body) != *r {
95
            return Err(StoreError::Corrupt(*r, "hash mismatch".into()));
96
        }
97
        Ok((ty, body.to_vec()))
98
    }
99
100
    pub fn get_typed(&self, r: &ObjectRef, want: ObjectType) -> Result<Vec<u8>> {
101
        let (ty, body) = self.get(r)?;
102
        if ty != want {
103
            return Err(StoreError::WrongType(*r, ty.name(), want.name()));
104
        }
105
        Ok(body)
106
    }
107
108
    pub fn put_change(&self, c: &Change) -> Result<ObjectRef> {
109
        self.put(ObjectType::Change, &c.encode())
110
    }
111
112
    pub fn get_change(&self, r: &ObjectRef) -> Result<Change> {
113
        Ok(Change::decode(&self.get_typed(r, ObjectType::Change)?)?)
114
    }
115
116
    pub fn put_chunk(&self, bytes: &[u8]) -> Result<ObjectRef> {
117
        self.put(ObjectType::Chunk, bytes)
118
    }
119
120
    /// Every object reference in the store (slow; for gc and tests).
121
    pub fn list(&self) -> io::Result<Vec<ObjectRef>> {
122
        let mut out = Vec::new();
123
        let loose = self.dir.join("loose");
124
        for d in fs::read_dir(&loose)? {
125
            let d = d?;
126
            if !d.file_type()?.is_dir() {
127
                continue;
128
            }
129
            let prefix = d.file_name().to_string_lossy().to_string();
130
            for f in fs::read_dir(d.path())? {
131
                let f = f?;
132
                let name = f.file_name().to_string_lossy().to_string();
133
                if let Some(r) = ObjectRef::from_hex(&format!("{prefix}{name}")) {
134
                    out.push(r);
135
                }
136
            }
137
        }
138
        out.sort();
139
        Ok(out)
140
    }
141
}
142
143
#[cfg(test)]
144
mod tests {
145
    use super::*;
146
147
    #[test]
148
    fn put_get_verify() {
149
        let dir = tempfile::tempdir().unwrap();
150
        let s = Store::open(dir.path().join("objects")).unwrap();
151
        let r = s.put(ObjectType::Chunk, b"hello").unwrap();
152
        assert!(s.has(&r));
153
        assert_eq!(s.get(&r).unwrap(), (ObjectType::Chunk, b"hello".to_vec()));
154
        assert_eq!(s.put(ObjectType::Chunk, b"hello").unwrap(), r);
155
        assert_eq!(s.list().unwrap(), vec![r]);
156
        assert!(matches!(s.get_typed(&r, ObjectType::Blob), Err(StoreError::WrongType(..))));
157
        assert!(matches!(s.get(&ObjectRef([9; 32])), Err(StoreError::NotFound(_))));
158
159
        // Tamper: flip a byte, read must fail.
160
        let path = s.path_for(&r);
161
        let mut data = fs::read(&path).unwrap();
162
        let last = data.len() - 1;
163
        data[last] ^= 1;
164
        fs::write(&path, data).unwrap();
165
        assert!(matches!(s.get(&r), Err(StoreError::Corrupt(..))));
166
    }
167
168
    #[test]
169
    fn change_round_trip_through_store() {
170
        let dir = tempfile::tempdir().unwrap();
171
        let s = Store::open(dir.path()).unwrap();
172
        let c = crate::objects::tests::sample_change();
173
        let r = s.put_change(&c).unwrap();
174
        assert_eq!(r, c.hash());
175
        assert_eq!(s.get_change(&r).unwrap(), c);
176
    }
177
}