concinnity_host/store/blob/
data.rs1use concinnity_core::ecs::PayloadLocator;
8use concinnity_core::result::CnResult;
9
10enum BlobSlot {
17 Unloaded(String),
19 Loaded(Vec<u8>),
21 Released,
23}
24
25pub struct BlobData {
37 slots: Vec<BlobSlot>,
39 disk_backed: bool,
45}
46
47impl BlobData {
48 pub fn new(payload_sections: Vec<Option<Vec<u8>>>) -> Self {
53 let slots = payload_sections
54 .into_iter()
55 .map(|s| match s {
56 Some(bytes) => BlobSlot::Loaded(bytes),
57 None => BlobSlot::Released,
58 })
59 .collect();
60 Self {
61 slots,
62 disk_backed: false,
63 }
64 }
65
66 pub fn empty() -> Self {
68 Self {
69 slots: Vec::new(),
70 disk_backed: false,
71 }
72 }
73
74 pub(super) fn from_blob_files(blob0_payload: Vec<u8>, overflow_paths: Vec<String>) -> Self {
77 let mut slots = Vec::with_capacity(overflow_paths.len() + 1);
78 slots.push(BlobSlot::Loaded(blob0_payload));
79 slots.extend(overflow_paths.into_iter().map(BlobSlot::Unloaded));
80 Self {
81 slots,
82 disk_backed: true,
83 }
84 }
85
86 pub fn disk_backed(&self) -> bool {
89 self.disk_backed
90 }
91
92 pub fn read(&mut self, locator: &PayloadLocator) -> Result<&[u8], CnResult> {
98 let idx = locator.blob_index as usize;
99 let slot = self.slots.get_mut(idx).ok_or_else(|| {
100 tracing::error!("BlobData: blob {} is out of range", locator.blob_index);
101 CnResult::FileIo
102 })?;
103 if let BlobSlot::Unloaded(path) = slot {
104 tracing::debug!(
105 "BlobData: lazily loading overflow blob {}",
106 locator.blob_index
107 );
108 let bytes = super::read_payload_section(&path.clone())?;
109 *slot = BlobSlot::Loaded(bytes);
110 }
111
112 let section = match &self.slots[idx] {
113 BlobSlot::Loaded(bytes) => bytes,
114 BlobSlot::Released => {
115 tracing::error!("BlobData: blob {} has been released", locator.blob_index);
116 return Err(CnResult::FileIo);
117 }
118 BlobSlot::Unloaded(_) => return Err(CnResult::FileIo),
120 };
121
122 let start = locator.offset as usize;
123 let end = start.checked_add(locator.len as usize).ok_or_else(|| {
124 tracing::error!(
125 "BlobData: payload slice offset {} + len {} overflows in blob {}",
126 start,
127 locator.len,
128 locator.blob_index
129 );
130 CnResult::FileIo
131 })?;
132 section.get(start..end).ok_or_else(|| {
133 tracing::error!(
134 "BlobData: payload slice [{}, {}) out of bounds in blob {} (len={})",
135 start,
136 end,
137 locator.blob_index,
138 section.len()
139 );
140 CnResult::FileIo
141 })
142 }
143
144 pub fn release(&mut self, blob_index: u32) {
151 if let Some(slot) = self.slots.get_mut(blob_index as usize)
152 && !matches!(slot, BlobSlot::Released)
153 {
154 tracing::debug!("BlobData: releasing payload for blob {}", blob_index);
155 *slot = BlobSlot::Released;
156 }
157 }
158
159 pub fn release_all_resident(&mut self) -> usize {
168 let mut freed = 0;
169 for slot in &mut self.slots {
170 if let BlobSlot::Loaded(bytes) = slot {
171 freed += bytes.len();
172 *slot = BlobSlot::Released;
173 }
174 }
175 freed
176 }
177
178 #[cfg(test)]
181 pub(crate) fn is_loaded(&self, blob_index: u32) -> bool {
182 matches!(
183 self.slots.get(blob_index as usize),
184 Some(BlobSlot::Loaded(_))
185 )
186 }
187}
188
189impl concinnity_core::ecs::PayloadStore for BlobData {
192 fn read(&mut self, locator: &PayloadLocator) -> Result<&[u8], CnResult> {
193 BlobData::read(self, locator)
194 }
195
196 fn release(&mut self, blob_index: u32) {
197 BlobData::release(self, blob_index)
198 }
199
200 fn disk_backed(&self) -> bool {
201 BlobData::disk_backed(self)
202 }
203
204 fn release_all_resident(&mut self) -> usize {
205 BlobData::release_all_resident(self)
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use concinnity_core::SCHEMA_VERSION;
213 use concinnity_core::blob::{BlobMeta, encode_cnb};
214
215 fn locator(blob_index: u32, offset: u64, len: u64) -> PayloadLocator {
216 PayloadLocator {
217 blob_index,
218 offset,
219 len,
220 }
221 }
222
223 #[test]
224 fn disk_backed_defaults_false() {
225 assert!(!BlobData::empty().disk_backed());
226 assert!(!BlobData::new(vec![Some(vec![1, 2, 3])]).disk_backed());
227 }
228
229 #[test]
230 fn from_blob_files_is_disk_backed_with_blob0_resident() {
231 let bd = BlobData::from_blob_files(b"primary".to_vec(), vec!["1".into(), "2".into()]);
232 assert!(bd.disk_backed());
233 assert!(bd.is_loaded(0));
234 assert!(!bd.is_loaded(1));
236 assert!(!bd.is_loaded(2));
237 }
238
239 #[test]
240 fn read_lazily_loads_an_unloaded_overflow_blob() {
241 let dir = tempfile::tempdir().unwrap();
242 let path = dir.path().join("1").to_string_lossy().into_owned();
243 let image = encode_cnb(SCHEMA_VERSION, &BlobMeta::default(), b"hello world").unwrap();
244 std::fs::write(&path, image).expect("write blob");
245
246 let mut bd = BlobData::from_blob_files(Vec::new(), vec![path]);
247 assert!(!bd.is_loaded(1));
248
249 assert_eq!(bd.read(&locator(1, 6, 5)).expect("read ok"), b"world");
250 assert!(bd.is_loaded(1));
252 }
253
254 #[test]
255 fn read_errors_when_a_deferred_overflow_blob_is_missing() {
256 let mut bd = BlobData::from_blob_files(Vec::new(), vec!["/nonexistent/cn/1".into()]);
257 assert_eq!(bd.read(&locator(1, 0, 1)), Err(CnResult::FileIo));
258 }
259
260 #[test]
261 fn read_errors_on_released_blob() {
262 let mut bd = BlobData::new(vec![None]);
264 assert!(bd.read(&locator(0, 0, 1)).is_err());
265 }
266
267 #[test]
268 fn release_then_read_errors() {
269 let mut bd = BlobData::new(vec![Some(b"abcd".to_vec())]);
270 assert_eq!(bd.read(&locator(0, 0, 2)).expect("read ok"), b"ab");
271 bd.release(0);
272 assert!(!bd.is_loaded(0));
273 assert!(bd.read(&locator(0, 0, 2)).is_err());
274 }
275
276 #[test]
277 fn release_all_resident_frees_loaded_sections() {
278 let mut bd = BlobData::from_blob_files(b"abcd".to_vec(), vec!["/nonexistent/cn/1".into()]);
280 assert!(bd.is_loaded(0));
281 assert!(!bd.is_loaded(1));
282
283 let freed = bd.release_all_resident();
284 assert_eq!(freed, 4, "blob 0's four bytes were freed");
285 assert!(!bd.is_loaded(0));
286 assert!(bd.read(&locator(0, 0, 1)).is_err());
288 assert_eq!(bd.release_all_resident(), 0);
290 }
291
292 #[test]
293 fn read_errors_on_out_of_range_blob() {
294 let mut bd = BlobData::empty();
295 assert!(bd.read(&locator(3, 0, 1)).is_err());
296 }
297
298 #[test]
299 fn read_errors_when_the_locator_runs_past_the_section() {
300 let mut bd = BlobData::new(vec![Some(b"abcd".to_vec())]);
301 assert!(bd.read(&locator(0, 2, 99)).is_err());
302 assert!(bd.read(&locator(0, u64::MAX, 1)).is_err());
303 }
304}