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}