Skip to main content

shardline_server/
object_store.rs

1#[cfg(test)]
2use std::path::PathBuf;
3#[cfg(test)]
4use std::sync::{LazyLock, Mutex};
5use std::{
6    fs::File,
7    io::{BufReader, ErrorKind, Read},
8    path::Path,
9};
10
11use shardline_index::{
12    FileChunkRecord, FileRecord, FileRecordInvariantError, FileRecordStorageLayout,
13};
14use shardline_protocol::ByteRange;
15pub use shardline_server_core::ServerObjectStore;
16pub use shardline_server_core::ServerObjectStoreError;
17use shardline_storage::{ObjectKey, ObjectMetadata, ObjectPrefix, ObjectStore};
18
19use crate::error::{IndexError, ObjectStoreError};
20use crate::{
21    ObjectStorageAdapter, ServerConfig, ServerError, ServerFrontend, chunk_store::chunk_object_key,
22    server_frontend::append_referenced_term_bytes,
23};
24
25#[cfg(test)]
26type LocalObjectReadHook = Box<dyn FnOnce() + Send>;
27
28#[cfg(test)]
29struct LocalObjectReadHookRegistration {
30    path: PathBuf,
31    hook: LocalObjectReadHook,
32}
33
34#[cfg(test)]
35type LocalObjectReadHookSlot = Option<LocalObjectReadHookRegistration>;
36
37#[cfg(test)]
38static BEFORE_LOCAL_OBJECT_READ_HOOK: LazyLock<Mutex<LocalObjectReadHookSlot>> =
39    LazyLock::new(|| Mutex::new(None));
40
41impl From<ServerObjectStoreError> for ServerError {
42    fn from(value: ServerObjectStoreError) -> Self {
43        match value {
44            ServerObjectStoreError::NotFound => Self::NotFound,
45            ServerObjectStoreError::Overflow => Self::Overflow,
46            ServerObjectStoreError::InvalidContentHash => Self::InvalidContentHash,
47            ServerObjectStoreError::StoredObjectLengthMismatch => {
48                Self::ObjectStore(ObjectStoreError::StoredLengthMismatch)
49            }
50            ServerObjectStoreError::Local(e) => Self::ObjectStore(ObjectStoreError::Local(e)),
51            ServerObjectStoreError::S3(e) => Self::ObjectStore(ObjectStoreError::S3(e)),
52            ServerObjectStoreError::Io(e) => Self::Io(e),
53            ServerObjectStoreError::NumericConversion(e) => Self::NumericConversion(e),
54        }
55    }
56}
57
58pub(crate) fn visit_object_prefix(
59    object_store: &ServerObjectStore,
60    prefix: &ObjectPrefix,
61    mut visitor: impl FnMut(ObjectMetadata) -> Result<(), ServerError>,
62) -> Result<(), ServerError> {
63    object_store.visit_prefix(prefix, &mut visitor)
64}
65
66pub(crate) fn read_full_object(
67    object_store: &ServerObjectStore,
68    object_key: &ObjectKey,
69    length: u64,
70) -> Result<Vec<u8>, ServerError> {
71    if length == 0 {
72        return Ok(Vec::new());
73    }
74
75    if let ServerObjectStore::Local(store) = object_store {
76        let path = store.path_for_key(object_key);
77        let file = store.open_object_file(object_key)?;
78        let actual_length = validate_local_object_length(&file, length)?;
79        return read_open_local_object(&path, file, actual_length);
80    }
81
82    let end = length.checked_sub(1).ok_or(ServerError::Overflow)?;
83    let range = ByteRange::new(0, end).map_err(|_error| ServerError::Overflow)?;
84    Ok(object_store.read_range(object_key, range)?)
85}
86
87pub(crate) fn reconstruct_local_file_bytes(
88    object_store: &ServerObjectStore,
89    chunks: &[FileChunkRecord],
90    capacity: usize,
91) -> Result<Vec<u8>, ServerError> {
92    let mut output = Vec::with_capacity(capacity);
93    for chunk in chunks {
94        let expected_offset = u64::try_from(output.len())?;
95        if chunk.offset != expected_offset {
96            return Err(ServerError::Index(IndexError::FileRecordInvariant(
97                FileRecordInvariantError::NonContiguousChunkOffsets,
98            )));
99        }
100        let object_key = chunk_object_key(&chunk.hash)?;
101        let ServerObjectStore::Local(store) = object_store else {
102            return Err(ServerError::NotFound);
103        };
104        let path = store.path_for_key(&object_key);
105        let file = store.open_object_file(&object_key)?;
106        read_open_local_object_append(&path, file, chunk.length, &mut output)?;
107    }
108    if output.len() != capacity {
109        return Err(ServerError::ObjectStore(
110            ObjectStoreError::StoredLengthMismatch,
111        ));
112    }
113    Ok(output)
114}
115
116pub(crate) fn reconstruct_file_record_bytes(
117    object_store: &ServerObjectStore,
118    frontends: &[ServerFrontend],
119    record: &FileRecord,
120) -> Result<Vec<u8>, ServerError> {
121    let capacity = usize::try_from(record.total_bytes)?;
122    match record.storage_layout() {
123        FileRecordStorageLayout::ReferencedObjectTerms => {
124            reconstruct_referenced_object_file_bytes(object_store, frontends, record, capacity)
125        }
126        FileRecordStorageLayout::StoredChunks => {
127            reconstruct_chunk_file_bytes(object_store, &record.chunks, capacity)
128        }
129    }
130}
131
132fn reconstruct_chunk_file_bytes(
133    object_store: &ServerObjectStore,
134    chunks: &[FileChunkRecord],
135    capacity: usize,
136) -> Result<Vec<u8>, ServerError> {
137    if object_store.local_root().is_some() {
138        return reconstruct_local_file_bytes(object_store, chunks, capacity);
139    }
140
141    let mut output = Vec::with_capacity(capacity);
142    for chunk in chunks {
143        let object_key = chunk_object_key(&chunk.hash)?;
144        let bytes = read_full_object(object_store, &object_key, chunk.length)?;
145        output.extend_from_slice(&bytes);
146    }
147    if output.len() != capacity {
148        return Err(ServerError::ObjectStore(
149            ObjectStoreError::StoredLengthMismatch,
150        ));
151    }
152
153    Ok(output)
154}
155
156fn reconstruct_referenced_object_file_bytes(
157    object_store: &ServerObjectStore,
158    frontends: &[ServerFrontend],
159    record: &FileRecord,
160    capacity: usize,
161) -> Result<Vec<u8>, ServerError> {
162    let mut output = Vec::with_capacity(capacity);
163    for term in &record.chunks {
164        let term_start = u64::try_from(output.len())?;
165        if term.offset != term_start {
166            return Err(ServerError::Index(IndexError::FileRecordInvariant(
167                FileRecordInvariantError::NonContiguousChunkOffsets,
168            )));
169        }
170        append_referenced_term_bytes(frontends, object_store, term, &mut output)?;
171        let term_end = term
172            .offset
173            .checked_add(term.length)
174            .ok_or(ServerError::Overflow)?;
175        if u64::try_from(output.len())? != term_end {
176            return Err(ServerError::ObjectStore(
177                ObjectStoreError::StoredLengthMismatch,
178            ));
179        }
180    }
181    if output.len() != capacity {
182        return Err(ServerError::ObjectStore(
183            ObjectStoreError::StoredLengthMismatch,
184        ));
185    }
186
187    Ok(output)
188}
189
190fn read_open_local_object(path: &Path, file: File, length: u64) -> Result<Vec<u8>, ServerError> {
191    let capacity = usize::try_from(length)?;
192    let mut output = Vec::with_capacity(capacity);
193    read_open_local_object_append(path, file, length, &mut output)?;
194    if output.len() != capacity {
195        return Err(ServerError::ObjectStore(
196            ObjectStoreError::StoredLengthMismatch,
197        ));
198    }
199    Ok(output)
200}
201
202fn read_open_local_object_append(
203    path: &Path,
204    file: File,
205    expected_length: u64,
206    output: &mut Vec<u8>,
207) -> Result<(), ServerError> {
208    validate_local_object_length(&file, expected_length)?;
209    run_before_local_object_read_hook(path);
210    let start_len = output.len();
211    let mut reader = BufReader::new(file);
212    let mut limited = reader.by_ref().take(expected_length);
213    if let Err(error) = limited.read_to_end(output) {
214        if error.kind() == ErrorKind::UnexpectedEof {
215            return Err(ServerError::ObjectStore(
216                ObjectStoreError::StoredLengthMismatch,
217            ));
218        }
219
220        return Err(ServerError::Io(error));
221    }
222    let read_len = output
223        .len()
224        .checked_sub(start_len)
225        .ok_or(ServerError::Overflow)?;
226    if u64::try_from(read_len)? != expected_length {
227        return Err(ServerError::ObjectStore(
228            ObjectStoreError::StoredLengthMismatch,
229        ));
230    }
231    let mut trailing_byte = [0_u8; 1];
232    match reader.read(&mut trailing_byte) {
233        Ok(0) => {}
234        Ok(_observed) => {
235            return Err(ServerError::ObjectStore(
236                ObjectStoreError::StoredLengthMismatch,
237            ));
238        }
239        Err(error) if error.kind() == ErrorKind::UnexpectedEof => {
240            return Err(ServerError::ObjectStore(
241                ObjectStoreError::StoredLengthMismatch,
242            ));
243        }
244        Err(error) => return Err(ServerError::Io(error)),
245    }
246    validate_local_object_length(&reader.into_inner(), expected_length)?;
247
248    Ok(())
249}
250
251fn validate_local_object_length(file: &File, expected_length: u64) -> Result<u64, ServerError> {
252    let actual_length = file.metadata()?.len();
253    if actual_length != expected_length {
254        return Err(ServerError::ObjectStore(
255            ObjectStoreError::StoredLengthMismatch,
256        ));
257    }
258
259    Ok(actual_length)
260}
261
262#[cfg(test)]
263fn run_before_local_object_read_hook(path: &Path) {
264    let hook = match BEFORE_LOCAL_OBJECT_READ_HOOK.lock() {
265        Ok(mut guard) => take_local_object_read_hook_for_path(&mut guard, path),
266        Err(poisoned) => take_local_object_read_hook_for_path(&mut poisoned.into_inner(), path),
267    };
268    if let Some(hook) = hook {
269        hook();
270    }
271}
272
273#[cfg(test)]
274fn take_local_object_read_hook_for_path(
275    slot: &mut LocalObjectReadHookSlot,
276    path: &Path,
277) -> Option<LocalObjectReadHook> {
278    if !matches!(slot, Some(registration) if registration.path == path) {
279        return None;
280    }
281    slot.take().map(|registration| registration.hook)
282}
283
284#[cfg(not(test))]
285const fn run_before_local_object_read_hook(_path: &Path) {}
286
287#[cfg(test)]
288fn set_before_local_object_read_hook(path: PathBuf, hook: impl FnOnce() + Send + 'static) {
289    let mut slot = match BEFORE_LOCAL_OBJECT_READ_HOOK.lock() {
290        Ok(guard) => guard,
291        Err(poisoned) => poisoned.into_inner(),
292    };
293    *slot = Some(LocalObjectReadHookRegistration {
294        path,
295        hook: Box::new(hook),
296    });
297}
298
299pub(crate) fn object_store_from_config(
300    config: &ServerConfig,
301) -> Result<ServerObjectStore, ServerError> {
302    match config.object_storage_adapter() {
303        ObjectStorageAdapter::Local => {
304            Ok(ServerObjectStore::local(config.root_dir().join("chunks"))?)
305        }
306        ObjectStorageAdapter::S3 => {
307            let s3_config = config
308                .s3_object_store_config()
309                .ok_or(ServerError::ObjectStore(ObjectStoreError::MissingS3Config))?
310                .clone();
311            Ok(ServerObjectStore::s3(s3_config)?)
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use std::{
319        fs::{File, OpenOptions},
320        io::Write,
321    };
322
323    use super::{read_open_local_object_append, set_before_local_object_read_hook};
324    use crate::ServerError;
325    use crate::error::ObjectStoreError;
326
327    #[test]
328    fn local_object_read_rejects_growth_after_length_validation_without_retaining_growth_bytes() {
329        let mut object = tempfile::NamedTempFile::new().unwrap();
330        object.write_all(b"abcd").unwrap();
331        object.as_file().sync_all().unwrap();
332
333        let reader = File::open(object.path()).unwrap();
334        let mut writer = OpenOptions::new().append(true).open(object.path()).unwrap();
335        set_before_local_object_read_hook(object.path().to_path_buf(), move || {
336            writer.write_all(b"efgh").unwrap();
337            writer.sync_all().unwrap();
338        });
339
340        let mut output = Vec::new();
341        let result = read_open_local_object_append(object.path(), reader, 4, &mut output);
342
343        assert!(matches!(
344            result,
345            Err(ServerError::ObjectStore(
346                ObjectStoreError::StoredLengthMismatch
347            ))
348        ));
349        assert_eq!(output, b"abcd");
350    }
351}