1use std::{
5 collections::HashSet,
6 fs::{self, File, OpenOptions},
7 path::{Path, PathBuf},
8};
9
10use fs2::FileExt;
11use heddle_format::compression::{header_uncompressed_size, is_compressed};
12use tracing::{debug, instrument, trace};
13
14use super::{
15 FsStore,
16 fs_io::{list_hashes_from_dir, read_file_bytes, read_file_header},
17 fs_paths::{
18 action_path, actions_dir, annotated_tags_dir, blobs_dir, hash_path, partial_tree_path,
19 partial_trees_dir, redaction_path, redactions_dir, state_attachment_index_lock_path,
20 state_attachment_index_path, state_attachment_path, state_attachments_dir, state_path,
21 state_visibility_dir, state_visibility_path, states_dir, tree_lineage_path, trees_dir,
22 },
23};
24use crate::{
25 object::{
26 Action, ActionId, AnnotatedTag, Blob, BytesTreeSource, ContentHash, FileTreeSource,
27 OpenedTreeBody, State, StateAttachment, StateAttachmentId, StateId, TREE_CANONICAL_MAGIC,
28 TREE_DELTA_HEADER_LEN, TREE_DELTA_MAGIC, TREE_LEAN_MAGIC, TREE_SALTED_MAGIC, Tree,
29 TreeByteSource, TreeEntry, TreeEntryReader, TreeResumeCursor, decode_tree_delta_header,
30 decode_tree_delta_header_prefix, is_delta_tree, is_redacted_tree, is_salted_tree,
31 is_streamable_tree,
32 },
33 store::{
34 HeddleError, ObjectCacheControl, ObjectStore, Result, SidecarStore,
35 SnapshotCommitDescriptor, TreeWrite, codec,
36 codec::{EncodedTree, TreeDeltaBase, TreeEncodingKind, TreeLineage},
37 delta_source::DeltaTreeSource,
38 pack::{ObjectType, PackManager, PackObjectId},
39 },
40};
41
42const BLOB_HEADER_PEEK: usize = 13;
50
51fn pack_is_corrupt(validation: Result<()>) -> Result<bool> {
52 match validation {
53 Ok(()) => Ok(false),
54 Err(HeddleError::InvalidObject(_) | HeddleError::Corruption { .. }) => Ok(true),
55 Err(error) => Err(error),
56 }
57}
58
59fn validate_loaded_tree(tree: Tree) -> Result<Tree> {
60 tree.validate()?;
61 Ok(tree)
62}
63
64fn streamable_body(tree: &Tree) -> Result<Vec<u8>> {
67 if tree.has_git_layout() {
68 Ok(tree.encode_canonical()?)
69 } else {
70 Ok(tree.encode_lean()?)
71 }
72}
73
74fn salted_tree_not_streamable(tree_id: &ContentHash) -> HeddleError {
81 HeddleError::InvalidObject(format!(
82 "tree {tree_id} is an HSR1 salted (v4) tree; the paging reader cannot \
83 stream it — decode it eagerly via get_tree"
84 ))
85}
86
87fn validate_blob_bytes(data: &[u8], hash: ContentHash) -> Result<()> {
88 let mut hasher = ContentHash::typed_hasher("blob", data.len() as u64);
89 hasher.update(data);
90 let found = ContentHash::from_bytes(hasher.finalize().into());
91 if found != hash {
92 return Err(HeddleError::Corruption {
93 expected: hash,
94 found,
95 });
96 }
97
98 Ok(())
99}
100
101fn validate_tree_serialized(data: &[u8], hash: ContentHash) -> Result<Tree> {
102 codec::decode_tree_serialized_with_key(data, hash, None)
105}
106
107fn validate_annotated_tag(data: &[u8], hash: ContentHash) -> Result<AnnotatedTag> {
108 let tag = AnnotatedTag::decode_current_msgpack(data)
109 .map_err(|error| HeddleError::InvalidObject(error.to_string()))?;
110 if tag.hash() != hash {
111 return Err(HeddleError::Corruption {
112 expected: hash,
113 found: tag.hash(),
114 });
115 }
116 Ok(tag)
117}
118
119fn validate_loaded_state(requested_id: &StateId, mut state: State) -> Result<State> {
120 if !state.accepts_stored_id(requested_id) {
121 return Err(HeddleError::InvalidObject(format!(
122 "state id mismatch: requested {requested_id}, computed {}",
123 state.id()
124 )));
125 }
126 state.state_id = *requested_id;
127 Ok(state)
128}
129
130pub(super) fn validate_state_serialized(data: &[u8], id: StateId) -> Result<State> {
131 let state = State::decode_current_msgpack(data)?;
132 validate_loaded_state(&id, state)
133}
134
135fn validate_loaded_action(requested_id: &ActionId, action: Action) -> Result<Action> {
136 let found_id = action.compute_id();
137 if found_id != *requested_id {
138 return Err(HeddleError::InvalidObject(format!(
139 "action id mismatch: requested {}, found {}",
140 requested_id, found_id
141 )));
142 }
143
144 Ok(action)
145}
146
147fn validate_action_serialized(data: &[u8], id: ActionId) -> Result<Action> {
148 let action: Action = rmp_serde::from_slice(data)?;
149 validate_loaded_action(&id, action)
150}
151
152trait EnumerationCounter {
153 fn membership_check(&mut self);
154 fn header_read(&mut self);
155}
156
157struct NoopEnumerationCounter;
158
159impl EnumerationCounter for NoopEnumerationCounter {
160 fn membership_check(&mut self) {}
161 fn header_read(&mut self) {}
162}
163
164fn append_packed_hashes_with_counter(
165 hashes: &mut Vec<ContentHash>,
166 manager: &PackManager,
167 expected_type: ObjectType,
168 counter: &mut impl EnumerationCounter,
169) -> Result<()> {
170 let mut known: HashSet<_> = hashes.iter().copied().collect();
171 for id in manager.list_all_ids()? {
172 let hash = match id {
173 PackObjectId::Hash(hash) if expected_type != ObjectType::AnnotatedTag => hash,
174 PackObjectId::AnnotatedTag(hash) if expected_type == ObjectType::AnnotatedTag => hash,
175 PackObjectId::Hash(_) | PackObjectId::StateId(_) | PackObjectId::AnnotatedTag(_) => {
176 continue;
177 }
178 };
179 counter.membership_check();
180 if known.contains(&hash) {
181 continue;
182 }
183 counter.header_read();
184 let found_type = if expected_type == ObjectType::AnnotatedTag {
185 manager
186 .get_object(&PackObjectId::AnnotatedTag(hash))?
187 .map(|(object_type, _)| object_type)
188 } else {
189 manager.get_hashed_object_type(&hash)?
190 };
191 if found_type == Some(expected_type) {
192 known.insert(hash);
193 hashes.push(hash);
194 }
195 }
196 Ok(())
197}
198
199fn append_packed_hashes(
200 hashes: &mut Vec<ContentHash>,
201 manager: &PackManager,
202 expected_type: ObjectType,
203) -> Result<()> {
204 append_packed_hashes_with_counter(hashes, manager, expected_type, &mut NoopEnumerationCounter)
205}
206
207fn append_unique_states(
208 states: &mut Vec<StateId>,
209 known: &mut HashSet<StateId>,
210 incoming: impl IntoIterator<Item = StateId>,
211) {
212 for id in incoming {
213 if known.insert(id) {
214 states.push(id);
215 }
216 }
217}
218
219impl FsStore {
220 fn states_needing_loose_copies(
232 &self,
233 reader: &crate::store::pack::PackReader,
234 ids: &[PackObjectId],
235 ) -> Result<Vec<(StateId, Vec<u8>)>> {
236 self.reload_packs_if_stale()?;
239 let mut held = HashSet::new();
240 for id in ids {
241 if let PackObjectId::StateId(state) = id
242 && self.try_has_state_once(state)?
243 {
244 held.insert(*state);
245 }
246 }
247 if held.is_empty() {
248 return Ok(Vec::new());
249 }
250 let mut states = state_entries_from_pack(reader, ids)?;
251 states.retain(|(id, _)| held.contains(id));
252 Ok(states)
253 }
254
255 fn write_packed_state_mirrors_batch(&self, states: Vec<(StateId, Vec<u8>)>) -> Result<()> {
259 if states.is_empty() {
260 return Ok(());
261 }
262
263 self.begin_snapshot_write_batch_impl()?;
264 for (id, data) in states {
265 if let Err(error) = ObjectStore::put_state_serialized(self, &data, id) {
266 self.abort_snapshot_write_batch_impl();
267 return Err(error);
268 }
269 }
270 if let Err(error) = self.flush_snapshot_write_batch_impl() {
271 self.abort_snapshot_write_batch_impl();
272 return Err(error);
273 }
274 Ok(())
275 }
276
277 fn with_state_attachment_index_lock<T>(
278 &self,
279 state: &StateId,
280 operation: impl FnOnce() -> Result<T>,
281 ) -> Result<T> {
282 let path = state_attachment_index_lock_path(&self.root, state);
283 if let Some(parent) = path.parent() {
284 fs::create_dir_all(parent)?;
285 }
286 let file = OpenOptions::new()
287 .create(true)
288 .truncate(false)
289 .read(true)
290 .write(true)
291 .open(path)?;
292 file.lock_exclusive()?;
293 let result = operation();
294 file.unlock()?;
295 result
296 }
297
298 fn collect_state_attachment_ids(&self, state: &StateId) -> Result<Vec<StateAttachmentId>> {
299 let mut ids = Vec::new();
300 let dir = state_attachments_dir(&self.root, state);
301 if let Ok(entries) = fs::read_dir(dir) {
302 for entry in entries {
303 let attachment =
304 StateAttachment::decode_current_msgpack(&fs::read(entry?.path())?)?;
305 if attachment.state_id != *state {
306 return Err(HeddleError::InvalidObject(
307 "state attachment stored under wrong state".to_string(),
308 ));
309 }
310 ids.push(attachment.id());
311 }
312 }
313 if let Ok(manager) = self.pack_manager().read() {
314 for pack_id in manager.list_all_ids()? {
315 let PackObjectId::Hash(hash) = pack_id else {
316 continue;
317 };
318 let Some((ObjectType::StateAttachment, bytes)) =
319 manager.get_hashed_object(&hash)?
320 else {
321 continue;
322 };
323 let attachment = StateAttachment::decode_current_msgpack(&bytes)?;
324 if attachment.state_id == *state {
325 ids.push(attachment.id());
326 }
327 }
328 }
329 ids.sort();
330 ids.dedup();
331 Ok(ids)
332 }
333
334 fn rebuild_state_attachment_index(&self, state: &StateId) -> Result<Vec<StateAttachmentId>> {
335 #[cfg(test)]
336 fs::write(
337 state_attachment_index_path(&self.root, state).with_extension("rebuild-marker"),
338 b"rebuilt",
339 )?;
340 let ids = self.collect_state_attachment_ids(state)?;
341 let path = state_attachment_index_path(&self.root, state);
342 self.write_loose_object_atomic(&path, &rmp_serde::to_vec_named(&ids)?)?;
343 Ok(ids)
344 }
345
346 pub(super) fn materialize_packed_attachment_index(
351 &self,
352 state: &StateId,
353 packed_ids: &[StateAttachmentId],
354 state_was_present: bool,
355 ) -> Result<()> {
356 if packed_ids.is_empty() {
357 return Ok(());
358 }
359 self.with_state_attachment_index_lock(state, || {
360 let path = state_attachment_index_path(&self.root, state);
361 let mut ids = if state_was_present {
362 match read_file_bytes(&path)? {
363 Some(bytes) => rmp_serde::from_slice(bytes.as_slice())?,
364 None => self.collect_state_attachment_ids(state)?,
365 }
366 } else {
367 Vec::new()
368 };
369 ids.extend_from_slice(packed_ids);
370 ids.sort();
371 ids.dedup();
372 self.write_reconstructible_cache(&path, &rmp_serde::to_vec_named(&ids)?)?;
373 Ok(())
374 })
375 }
376}
377
378fn validate_and_list_pack(
381 store: &FsStore,
382 reader: &crate::store::pack::PackReader,
383) -> Result<Vec<PackObjectId>> {
384 validate_pack(store, reader)?;
385 reader.list_ids()
386}
387
388fn validate_pack(store: &FsStore, reader: &crate::store::pack::PackReader) -> Result<()> {
389 visit_validated_pack(store, reader, |_, _, _| Ok(()))
390}
391
392fn visit_validated_pack(
393 store: &FsStore,
394 reader: &crate::store::pack::PackReader,
395 mut visitor: impl FnMut(PackObjectId, ObjectType, &[u8]) -> Result<()>,
396) -> Result<()> {
397 reader.visit_objects(|id, object_type, data| {
398 if let (PackObjectId::Hash(hash), ObjectType::Tree) = (id, object_type)
399 && is_delta_tree(data)
400 {
401 let header = decode_tree_delta_header(data)?;
402 if header.anchor == hash {
403 return Err(HeddleError::InvalidObject(
404 "HDC1 result id must differ from its anchor id".to_string(),
405 ));
406 }
407 let anchor_body = match reader.get_object(&PackObjectId::Hash(header.anchor))? {
408 Some((ObjectType::Tree, body)) => Some(body),
409 Some((kind, _)) => {
410 return Err(HeddleError::InvalidObject(format!(
411 "HDC1 anchor {} is indexed as {kind:?}, expected Tree",
412 header.anchor
413 )));
414 }
415 None => store.try_get_tree_serialized_once(&header.anchor)?,
416 }
417 .ok_or_else(|| HeddleError::NotFound(format!("tree delta anchor {}", header.anchor)))?;
418 if is_delta_tree(&anchor_body) {
419 return Err(HeddleError::InvalidObject(
420 "HDC1 anchor must be materialized; delta chains are forbidden".to_string(),
421 ));
422 }
423 let anchor = codec::decode_tree_serialized_with_key(&anchor_body, header.anchor, None)?;
424 codec::decode_tree_serialized_with_key(data, hash, Some(&anchor))?;
425 } else {
426 validate_pack_entry(&id, object_type, data)?;
427 }
428 visitor(id, object_type, data)
429 })
430}
431
432fn state_entries_from_pack(
433 reader: &crate::store::pack::PackReader,
434 ids: &[PackObjectId],
435) -> Result<Vec<(StateId, Vec<u8>)>> {
436 let mut states = Vec::new();
437 let expected = ids.iter().copied().collect::<HashSet<_>>();
438 reader.visit_objects(|id, object_type, data| {
439 if !expected.contains(&id) {
440 return Err(HeddleError::InvalidObject(
441 "pack visitor yielded an unindexed object".into(),
442 ));
443 }
444 if let PackObjectId::StateId(state_id) = id {
445 if object_type != ObjectType::State {
446 return Err(HeddleError::InvalidObject(format!(
447 "pack id {} is indexed as {object_type:?}, expected State",
448 state_id.to_string_full()
449 )));
450 }
451 validate_state_serialized(data, state_id)?;
452 states.push((state_id, data.to_vec()));
453 }
454 Ok(())
455 })?;
456 Ok(states)
457}
458
459fn attachment_entries_from_pack(
460 reader: &crate::store::pack::PackReader,
461 ids: &[PackObjectId],
462) -> Result<Vec<StateAttachment>> {
463 let mut attachments = Vec::new();
464 let expected = ids.iter().copied().collect::<HashSet<_>>();
465 reader.visit_objects(|id, object_type, data| {
466 if expected.contains(&id) && object_type == ObjectType::StateAttachment {
467 attachments.push(StateAttachment::decode_current_msgpack(data)?);
468 }
469 Ok(())
470 })?;
471 Ok(attachments)
472}
473
474pub(super) fn validate_pack_entry(
475 id: &PackObjectId,
476 obj_type: ObjectType,
477 data: &[u8],
478) -> Result<()> {
479 match (id, obj_type) {
480 (PackObjectId::Hash(hash), ObjectType::Blob) => validate_blob_bytes(data, *hash),
481 (PackObjectId::AnnotatedTag(hash), ObjectType::AnnotatedTag) => {
482 validate_annotated_tag(data, *hash).map(|_| ())
483 }
484 (PackObjectId::Hash(hash), ObjectType::Tree) => {
485 validate_tree_serialized(data, *hash).map(|_| ())
486 }
487 (PackObjectId::Hash(hash), ObjectType::Action) => {
488 validate_action_serialized(data, ActionId::from_hash(*hash)).map(|_| ())
489 }
490 (PackObjectId::StateId(change_id), ObjectType::State) => {
491 validate_state_serialized(data, *change_id).map(|_| ())
492 }
493 (PackObjectId::Hash(hash), ObjectType::StateAttachment) => {
494 let attachment = StateAttachment::decode_current_msgpack(data)?;
495 if attachment.id().as_hash() != hash {
496 return Err(HeddleError::InvalidObject(
497 "state attachment pack id mismatch".to_string(),
498 ));
499 }
500 Ok(())
501 }
502 (PackObjectId::Hash(hash), ObjectType::SnapshotCommit) => {
503 let artifact: crate::store::SnapshotCommitArtifact = rmp_serde::from_slice(data)?;
504 artifact.validate()?;
505 if artifact.id() != *hash {
506 return Err(HeddleError::InvalidObject(
507 "snapshot commit artifact pack id mismatch".to_string(),
508 ));
509 }
510 Ok(())
511 }
512 (_, ObjectType::TimelineOperation) => Err(HeddleError::InvalidObject(
513 "timeline operations belong in the timeline pack store".to_string(),
514 )),
515 _ => Err(HeddleError::InvalidObject(format!(
516 "unsupported native pack object: {:?} {:?}",
517 id, obj_type
518 ))),
519 }
520}
521
522impl FsStore {
523 fn cache_recent_blob(&self, hash: ContentHash, blob: &Blob) {
525 if blob.content().len() > super::fs_store::RECENT_BLOB_CACHE_MAX_BYTES {
526 return;
527 }
528 if let Ok(mut cache) = self.recent_blobs.write() {
529 cache.insert(hash, blob.clone());
530 }
531 }
532
533 fn cache_recent_tree(&self, hash: ContentHash, tree: &Tree) {
534 if let Ok(mut cache) = self.recent_trees.write() {
535 cache.insert(hash, tree.clone());
536 }
537 }
538
539 fn cache_recent_state(&self, id: StateId, state: &State) {
540 if let Ok(mut cache) = self.recent_states.write() {
541 cache.insert(id, state.clone());
542 }
543 }
544
545 fn recent_blob(&self, hash: &ContentHash) -> Option<Blob> {
546 self.recent_blobs
547 .read()
548 .ok()
549 .and_then(|cache| cache.get(hash).cloned())
550 }
551
552 fn recent_tree(&self, hash: &ContentHash) -> Option<Tree> {
553 self.recent_trees
554 .read()
555 .ok()
556 .and_then(|cache| cache.get(hash).cloned())
557 }
558
559 fn recent_state(&self, id: &StateId) -> Option<State> {
560 self.recent_states
561 .read()
562 .ok()
563 .and_then(|cache| cache.get(id).cloned())
564 }
565
566 fn try_get_blob_once(&self, hash: &ContentHash) -> Result<Option<Blob>> {
569 if let Ok(cache) = self.recent_blobs.read()
572 && let Some(blob) = cache.get(hash)
573 {
574 trace!("Found blob in recent object cache");
575 return Ok(Some(blob.clone()));
576 }
577
578 if let Ok(manager) = self.pack_manager().read()
579 && let Some((obj_type, data)) = manager.get_hashed_object(hash)?
580 && obj_type == ObjectType::Blob
581 {
582 trace!("Found blob in packfile");
583 validate_blob_bytes(&data, *hash)?;
584 let blob = Blob::new(data);
585 heddle_perf_contract::record_object_decode();
586 self.cache_recent_blob(*hash, &blob);
587 return Ok(Some(blob));
588 }
589
590 let path = hash_path(&blobs_dir(&self.root), hash);
591 match read_file_bytes(&path)? {
592 Some(data) => {
593 trace!(size = data.as_slice().len(), "Blob data read");
594 let content = codec::decode_blob_content(data.as_slice())?;
595 let blob = Blob::new(content);
596 heddle_perf_contract::record_object_decode();
597 if blob.hash() != *hash {
604 return Err(HeddleError::Corruption {
605 expected: *hash,
606 found: blob.hash(),
607 });
608 }
609 self.cache_recent_blob(*hash, &blob);
610 Ok(Some(blob))
611 }
612 None => Ok(None),
613 }
614 }
615
616 fn loose_or_packed(
621 &self,
622 loose_path: &Path,
623 in_pack: impl FnOnce(&PackManager) -> bool,
624 ) -> Result<bool> {
625 if loose_path.exists() {
626 return Ok(true);
627 }
628 if let Ok(manager) = self.pack_manager().read() {
629 return Ok(in_pack(&manager));
630 }
631 Ok(false)
632 }
633
634 fn try_has_blob_once(&self, hash: &ContentHash) -> Result<bool> {
635 let path = hash_path(&blobs_dir(&self.root), hash);
639 self.loose_or_packed(&path, |m| m.has_object(hash))
640 }
641
642 fn try_get_blob_size_once(&self, hash: &ContentHash) -> Result<Option<u64>> {
657 if let Ok(cache) = self.recent_blobs.read()
658 && let Some(blob) = cache.get(hash)
659 {
660 return Ok(Some(blob.content().len() as u64));
661 }
662
663 let path = hash_path(&blobs_dir(&self.root), hash);
664 if let Some((header, file_len)) = read_file_header(&path, BLOB_HEADER_PEEK)? {
665 if let Some(size) = header_uncompressed_size(&header) {
666 return Ok(Some(size));
667 }
668 return Ok(Some(file_len));
671 }
672
673 if let Ok(manager) = self.pack_manager().read()
674 && let Some(size) = manager.get_hashed_object_size(hash)?
675 {
676 return Ok(Some(size));
677 }
678 Ok(None)
679 }
680
681 fn try_open_tree_once(
682 &self,
683 tree_id: &ContentHash,
684 cursor: Option<&TreeResumeCursor>,
685 ) -> Result<Option<TreeEntryReader<OpenedTreeBody>>> {
686 let path = hash_path(&trees_dir(&self.root), tree_id);
687 if path.exists()
688 && let Some((header, len)) = read_file_header(&path, TREE_CANONICAL_MAGIC.len())?
689 {
690 if header.starts_with(TREE_CANONICAL_MAGIC) || header.starts_with(TREE_LEAN_MAGIC) {
691 let file = File::open(&path)?;
692 return Ok(Some(TreeEntryReader::open(
693 OpenedTreeBody::File(FileTreeSource::sequential_verify(file, len)),
694 *tree_id,
695 cursor,
696 )?));
697 }
698 if header.starts_with(TREE_DELTA_MAGIC) {
699 let file = File::open(&path)?;
700 return self.open_delta_tree_source(
701 *tree_id,
702 cursor,
703 OpenedTreeBody::File(FileTreeSource::sequential_verify(file, len)),
704 );
705 }
706 if header.starts_with(TREE_SALTED_MAGIC) {
707 return Err(salted_tree_not_streamable(tree_id));
708 }
709 }
710 if path.exists()
711 && let Some(data) = read_file_bytes(&path)?
712 {
713 let body = codec::decode_tree_body(data.as_slice())?;
714 if is_streamable_tree(&body) {
715 return Ok(Some(TreeEntryReader::open(
716 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(body)),
717 *tree_id,
718 cursor,
719 )?));
720 }
721 if is_delta_tree(&body) {
722 return self.open_delta_tree_source(
723 *tree_id,
724 cursor,
725 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(body)),
726 );
727 }
728 if is_salted_tree(&body) {
729 return Err(salted_tree_not_streamable(tree_id));
730 }
731 }
732 let packed = if let Ok(manager) = self.pack_manager().read() {
733 manager.get_hashed_object(tree_id)?
734 } else {
735 None
736 };
737 if let Some((ObjectType::Tree, data)) = packed {
738 if is_streamable_tree(&data) {
739 return Ok(Some(TreeEntryReader::open(
740 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(data)),
741 *tree_id,
742 cursor,
743 )?));
744 }
745 if is_delta_tree(&data) {
746 return self.open_delta_tree_source(
747 *tree_id,
748 cursor,
749 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(data)),
750 );
751 }
752 if is_salted_tree(&data) {
753 return Err(salted_tree_not_streamable(tree_id));
754 }
755 }
756 let npk_tree = if let Ok(manager) = self.npk1_manager().read() {
757 manager.get_tree(tree_id)?
758 } else {
759 None
760 };
761 if let Some(tree) = npk_tree {
762 return Ok(Some(TreeEntryReader::open(
763 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(streamable_body(&tree)?)),
764 *tree_id,
765 cursor,
766 )?));
767 }
768 Ok(None)
769 }
770
771 fn open_delta_tree_source(
772 &self,
773 tree_id: ContentHash,
774 cursor: Option<&TreeResumeCursor>,
775 mut delta: OpenedTreeBody,
776 ) -> Result<Option<TreeEntryReader<OpenedTreeBody>>> {
777 let object_len = usize::try_from(delta.len())
778 .map_err(|_| HeddleError::InvalidObject("HDC1 body exceeds usize".to_string()))?;
779 let mut header_bytes = [0u8; TREE_DELTA_HEADER_LEN];
780 delta.read_exact_at(0, &mut header_bytes)?;
781 let header = decode_tree_delta_header_prefix(&header_bytes, object_len)?;
782 let anchor = self
783 .try_open_materialized_tree_once(&header.anchor)?
784 .ok_or_else(|| HeddleError::NotFound(format!("tree delta anchor {}", header.anchor)))?;
785 let source = DeltaTreeSource::open(delta, anchor)?;
786 Ok(Some(TreeEntryReader::open(
787 OpenedTreeBody::Dynamic(Box::new(source)),
788 tree_id,
789 cursor,
790 )?))
791 }
792
793 fn try_open_materialized_tree_once(
794 &self,
795 tree_id: &ContentHash,
796 ) -> Result<Option<TreeEntryReader<OpenedTreeBody>>> {
797 let path = hash_path(&trees_dir(&self.root), tree_id);
798 if path.exists()
799 && let Some((header, len)) = read_file_header(&path, TREE_CANONICAL_MAGIC.len())?
800 {
801 if header.starts_with(TREE_DELTA_MAGIC) {
802 return Err(HeddleError::InvalidObject(
803 "HDC1 anchor must be materialized; delta chains are forbidden".to_string(),
804 ));
805 }
806 if header.starts_with(TREE_CANONICAL_MAGIC) || header.starts_with(TREE_LEAN_MAGIC) {
807 let file = File::open(&path)?;
808 return Ok(Some(TreeEntryReader::open(
809 OpenedTreeBody::File(FileTreeSource::sequential_verify(file, len)),
810 *tree_id,
811 None,
812 )?));
813 }
814 }
815 if path.exists()
816 && let Some(data) = read_file_bytes(&path)?
817 {
818 let body = codec::decode_tree_body(data.as_slice())?;
819 if is_delta_tree(&body) {
820 return Err(HeddleError::InvalidObject(
821 "HDC1 anchor must be materialized; delta chains are forbidden".to_string(),
822 ));
823 }
824 if is_streamable_tree(&body) {
825 return Ok(Some(TreeEntryReader::open(
826 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(body)),
827 *tree_id,
828 None,
829 )?));
830 }
831 }
832 let packed = if let Ok(manager) = self.pack_manager().read() {
833 manager.get_hashed_object(tree_id)?
834 } else {
835 None
836 };
837 if let Some((ObjectType::Tree, data)) = packed {
838 if is_delta_tree(&data) {
839 return Err(HeddleError::InvalidObject(
840 "HDC1 anchor must be materialized; delta chains are forbidden".to_string(),
841 ));
842 }
843 if is_streamable_tree(&data) {
844 return Ok(Some(TreeEntryReader::open(
845 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(data)),
846 *tree_id,
847 None,
848 )?));
849 }
850 }
851 let npk_tree = if let Ok(manager) = self.npk1_manager().read() {
852 manager.get_tree(tree_id)?
853 } else {
854 None
855 };
856 if let Some(tree) = npk_tree {
857 return Ok(Some(TreeEntryReader::open(
858 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(streamable_body(&tree)?)),
859 *tree_id,
860 None,
861 )?));
862 }
863 if let Some(source) = &self.external_source
864 && let Some(tree) = source.get_tree(tree_id)?
865 {
866 return Ok(Some(TreeEntryReader::open(
867 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(streamable_body(&tree)?)),
868 *tree_id,
869 None,
870 )?));
871 }
872 Ok(None)
873 }
874
875 fn try_get_tree_once(&self, hash: &ContentHash) -> Result<Option<Tree>> {
876 if let Ok(cache) = self.recent_trees.read()
880 && let Some(tree) = cache.get(hash)
881 {
882 trace!("Found tree in recent object cache");
883 return Ok(Some(tree.clone()));
884 }
885
886 let path = hash_path(&trees_dir(&self.root), hash);
890 if path.exists()
891 && let Some(data) = read_file_bytes(&path)?
892 {
893 trace!(size = data.as_slice().len(), "Tree data read");
894 let body = codec::decode_tree_body(data.as_slice())?;
895 let tree = validate_loaded_tree(self.decode_tree_storage_body(*hash, &body)?)?;
896 heddle_perf_contract::record_object_decode();
897 if tree.hash() != *hash {
898 return Err(HeddleError::Corruption {
899 expected: *hash,
900 found: tree.hash(),
901 });
902 }
903 if let Ok(mut cache) = self.recent_trees.write() {
904 cache.insert(*hash, tree.clone());
905 }
906 return Ok(Some(tree));
907 }
908
909 if let Ok(manager) = self.npk1_manager().read()
910 && let Some(tree) = manager.get_tree(hash)?
911 {
912 trace!("Found tree in NPK1 pack");
913 heddle_perf_contract::record_object_decode();
914 self.cache_recent_tree(*hash, &tree);
915 return Ok(Some(tree));
916 }
917 if let Ok(manager) = self.pack_manager().read()
918 && let Some((obj_type, data)) = manager.get_hashed_object(hash)?
919 && obj_type == ObjectType::Tree
920 {
921 trace!("Found tree in packfile");
922 let tree = validate_loaded_tree(self.decode_tree_storage_body(*hash, &data)?)?;
923 heddle_perf_contract::record_object_decode();
924 if tree.hash() != *hash {
925 return Err(HeddleError::Corruption {
926 expected: *hash,
927 found: tree.hash(),
928 });
929 }
930 if let Ok(mut cache) = self.recent_trees.write() {
931 cache.insert(*hash, tree.clone());
932 }
933 return Ok(Some(tree));
934 }
935 Ok(None)
936 }
937
938 fn try_get_tree_entry_once(&self, hash: &ContentHash, name: &str) -> Result<Option<TreeEntry>> {
939 if let Some(tree) = self.recent_tree(hash) {
940 return Ok(tree.get(name).cloned());
941 }
942 let path = hash_path(&trees_dir(&self.root), hash);
943 if path.exists() {
944 return Ok(self
945 .try_get_tree_once(hash)?
946 .and_then(|tree| tree.get(name).cloned()));
947 }
948 if let Ok(manager) = self.npk1_manager().read()
949 && manager.has_tree(hash)?
950 {
951 return manager.get_entry(hash, name);
952 }
953 if let Ok(manager) = self.pack_manager().read()
954 && manager.has_object(hash)
955 {
956 return Ok(self
957 .try_get_tree_once(hash)?
958 .and_then(|tree| tree.get(name).cloned()));
959 }
960 Ok(None)
961 }
962
963 pub(super) fn try_get_tree_serialized_once(
964 &self,
965 hash: &ContentHash,
966 ) -> Result<Option<Vec<u8>>> {
967 let path = hash_path(&trees_dir(&self.root), hash);
968 if path.exists()
969 && let Some(data) = read_file_bytes(&path)?
970 {
971 return Ok(Some(codec::decode_tree_body(data.as_slice())?));
972 }
973
974 if let Ok(manager) = self.npk1_manager().read()
975 && let Some(tree) = manager.get_tree(hash)?
976 {
977 return tree.encode_lean().map(Some).map_err(HeddleError::from);
978 }
979
980 if let Ok(manager) = self.pack_manager().read()
981 && let Some((obj_type, data)) = manager.get_hashed_object(hash)?
982 && obj_type == ObjectType::Tree
983 {
984 return Ok(Some(data));
985 }
986
987 Ok(None)
988 }
989
990 pub(super) fn decode_tree_storage_body(&self, hash: ContentHash, data: &[u8]) -> Result<Tree> {
991 let anchor = if is_delta_tree(data) {
992 let header = decode_tree_delta_header(data)?;
993 let anchor = if let Some(anchor_body) =
994 self.try_get_tree_serialized_once(&header.anchor)?
995 {
996 if is_delta_tree(&anchor_body) {
997 return Err(HeddleError::InvalidObject(
998 "HDC1 anchor must be materialized; delta chains are forbidden".to_string(),
999 ));
1000 }
1001 let tree =
1002 codec::decode_tree_serialized_with_key(&anchor_body, header.anchor, None)?;
1003 self.cache_recent_tree(header.anchor, &tree);
1004 Some(tree)
1005 } else if let Some(tree) = self.recent_tree(&header.anchor) {
1006 Some(tree)
1009 } else if let Some(source) = &self.external_source {
1010 source.get_tree(&header.anchor)?
1011 } else {
1012 None
1013 };
1014 Some(anchor.ok_or_else(|| {
1015 HeddleError::NotFound(format!("tree delta anchor {}", header.anchor))
1016 })?)
1017 } else {
1018 None
1019 };
1020 codec::decode_tree_serialized_with_key(data, hash, anchor.as_ref())
1021 }
1022
1023 pub(super) fn encode_tree_write(&self, write: &TreeWrite) -> Result<EncodedTree> {
1024 let Some(parent) = write.parent else {
1025 return codec::encode_tree_hot(&write.tree, None);
1026 };
1027 let Some(parent_body) = self.try_get_tree_serialized_once(&parent)? else {
1028 return codec::encode_tree_hot(&write.tree, None);
1029 };
1030 let base = if is_delta_tree(&parent_body) {
1031 let header = decode_tree_delta_header(&parent_body)?;
1032 let Some(lineage) = self.read_tree_lineage(&parent)? else {
1033 return codec::encode_tree_hot(&write.tree, None);
1034 };
1035 if lineage.anchor != header.anchor || lineage.depth == 0 {
1036 return codec::encode_tree_hot(&write.tree, None);
1037 }
1038 let Some(anchor_body) = self.try_get_tree_serialized_once(&lineage.anchor)? else {
1039 return codec::encode_tree_hot(&write.tree, None);
1040 };
1041 if is_delta_tree(&anchor_body) {
1042 return Err(HeddleError::InvalidObject(
1043 "HDC1 lineage points to another delta".to_string(),
1044 ));
1045 }
1046 let anchor =
1047 codec::decode_tree_serialized_with_key(&anchor_body, lineage.anchor, None)?;
1048 Some((lineage.anchor, anchor, lineage.depth))
1049 } else {
1050 let anchor = codec::decode_tree_serialized_with_key(&parent_body, parent, None)?;
1051 Some((parent, anchor, 0))
1052 };
1053 let Some((anchor_id, anchor, parent_depth)) = base else {
1054 return codec::encode_tree_hot(&write.tree, None);
1055 };
1056 codec::encode_tree_hot(
1057 &write.tree,
1058 Some(TreeDeltaBase {
1059 anchor_id,
1060 anchor: &anchor,
1061 parent_depth,
1062 }),
1063 )
1064 }
1065
1066 fn read_tree_lineage(&self, hash: &ContentHash) -> Result<Option<TreeLineage>> {
1067 let Some(bytes) = read_file_bytes(&tree_lineage_path(&self.root, hash))? else {
1068 return Ok(None);
1069 };
1070 let data = bytes.as_slice();
1071 if data.len() != 33 {
1072 return Ok(None);
1073 }
1074 let anchor = match data[..32].try_into() {
1075 Ok(bytes) => ContentHash::from_bytes(bytes),
1076 Err(_) => return Ok(None),
1077 };
1078 let depth = data[32];
1079 if depth == 0 || depth >= crate::object::TREE_DELTA_ANCHOR_INTERVAL {
1080 return Ok(None);
1081 }
1082 Ok(Some(TreeLineage { anchor, depth }))
1083 }
1084
1085 pub(super) fn remember_tree_encoding(
1086 &self,
1087 hash: ContentHash,
1088 kind: TreeEncodingKind,
1089 ) -> Result<()> {
1090 let TreeEncodingKind::Delta { anchor, depth, .. } = kind else {
1091 return Ok(());
1092 };
1093 let mut bytes = Vec::with_capacity(33);
1094 bytes.extend_from_slice(anchor.as_bytes());
1095 bytes.push(depth);
1096 self.write_reconstructible_cache(&tree_lineage_path(&self.root, &hash), &bytes)
1097 }
1098
1099 fn try_has_tree_once(&self, hash: &ContentHash) -> Result<bool> {
1100 let path = hash_path(&trees_dir(&self.root), hash);
1104 if self.loose_or_packed(&path, |m| m.has_object(hash))? {
1105 return Ok(true);
1106 }
1107 if let Ok(manager) = self.npk1_manager().read() {
1108 return manager.has_tree(hash);
1109 }
1110 Ok(false)
1111 }
1112
1113 fn try_get_state_once(&self, id: &StateId) -> Result<Option<State>> {
1114 if let Ok(cache) = self.recent_states.read()
1119 && let Some(state) = cache.get(id)
1120 {
1121 trace!("Found state in recent object cache");
1122 return Ok(Some(state.clone()));
1123 }
1124
1125 let path = state_path(&self.root, id);
1126 if let Some(data) = read_file_bytes(&path)? {
1127 trace!(size = data.as_slice().len(), "State read from loose object");
1128 let state = validate_loaded_state(id, codec::decode_state(data.as_slice())?)?;
1129 heddle_perf_contract::record_object_decode();
1130 if let Ok(mut cache) = self.recent_states.write() {
1131 cache.insert(*id, state.clone());
1132 }
1133 return Ok(Some(state));
1134 }
1135
1136 if let Ok(manager) = self.pack_manager().read()
1137 && let Some((obj_type, data)) = manager.get_object(&PackObjectId::StateId(*id))?
1138 && obj_type == ObjectType::State
1139 {
1140 trace!("Found state in packfile");
1141 let state = validate_loaded_state(id, State::decode_current_msgpack(&data)?)?;
1142 heddle_perf_contract::record_object_decode();
1143 if let Ok(mut cache) = self.recent_states.write() {
1144 cache.insert(*id, state.clone());
1145 }
1146 return Ok(Some(state));
1147 }
1148
1149 Ok(None)
1150 }
1151
1152 fn try_has_state_once(&self, id: &StateId) -> Result<bool> {
1153 if let Ok(cache) = self.recent_states.read()
1156 && cache.contains(id)
1157 {
1158 return Ok(true);
1159 }
1160 let path = state_path(&self.root, id);
1161 self.loose_or_packed(&path, |m| m.has_object_id(&PackObjectId::StateId(*id)))
1162 }
1163
1164 fn try_get_action_once(&self, id: &ActionId) -> Result<Option<Action>> {
1165 let path = action_path(&self.root, id);
1166 if let Some(data) = read_file_bytes(&path)? {
1167 trace!(size = data.as_slice().len(), "Action data read");
1168 return Ok(Some(validate_loaded_action(
1169 id,
1170 codec::decode_action(data.as_slice())?,
1171 )?));
1172 }
1173 if let Ok(manager) = self.pack_manager().read()
1174 && let Some((ObjectType::Action, data)) = manager.get_hashed_object(id.as_hash())?
1175 {
1176 trace!("Found action in packfile");
1177 return Ok(Some(validate_loaded_action(
1178 id,
1179 rmp_serde::from_slice(&data)?,
1180 )?));
1181 }
1182 Ok(None)
1183 }
1184
1185 fn try_get_state_attachment_once(
1186 &self,
1187 state: &StateId,
1188 id: &StateAttachmentId,
1189 ) -> Result<Option<StateAttachment>> {
1190 let path = state_attachment_path(&self.root, state, id);
1191 let file_bytes = read_file_bytes(&path)?;
1192 if let Some(bytes) = file_bytes.as_ref() {
1193 let attachment = StateAttachment::decode_current_msgpack(bytes.as_slice())?;
1194 return Self::validate_state_attachment(attachment, state, id).map(Some);
1195 }
1196 if let Ok(manager) = self.pack_manager().read()
1197 && let Some((ObjectType::StateAttachment, pack_bytes)) =
1198 manager.get_hashed_object(id.as_hash())?
1199 {
1200 let attachment = StateAttachment::decode_current_msgpack(&pack_bytes)?;
1201 return Self::validate_state_attachment(attachment, state, id).map(Some);
1202 }
1203 Ok(None)
1204 }
1205
1206 fn validate_state_attachment(
1207 attachment: StateAttachment,
1208 state: &StateId,
1209 id: &StateAttachmentId,
1210 ) -> Result<StateAttachment> {
1211 if attachment.state_id != *state || attachment.id() != *id {
1212 return Err(HeddleError::InvalidObject(
1213 "state attachment address does not match content".to_string(),
1214 ));
1215 }
1216 Ok(attachment)
1217 }
1218}
1219
1220impl FsStore {
1221 #[doc(hidden)]
1223 pub fn snapshot_commit_recovery_descriptors(&self) -> Result<Vec<SnapshotCommitDescriptor>> {
1224 self.reload_packs_if_stale()?;
1225 let manager = self
1226 .pack_manager()
1227 .read()
1228 .map_err(|_| HeddleError::Config("Failed to acquire pack manager lock".to_string()))?;
1229 manager.snapshot_commit_recovery_descriptors()
1230 }
1231
1232 #[doc(hidden)]
1236 pub fn snapshot_commit_descriptors(&self) -> Result<Vec<SnapshotCommitDescriptor>> {
1237 self.reload_packs_if_stale()?;
1238 let manager = self
1239 .pack_manager()
1240 .read()
1241 .map_err(|_| HeddleError::Config("Failed to acquire pack manager lock".to_string()))?;
1242 manager.snapshot_commit_descriptors()
1243 }
1244
1245 #[doc(hidden)]
1248 pub fn snapshot_commit_descriptor_for_state(
1249 &self,
1250 state: &StateId,
1251 ) -> Result<Option<SnapshotCommitDescriptor>> {
1252 self.reload_packs_if_stale()?;
1253 let manager = self
1254 .pack_manager()
1255 .read()
1256 .map_err(|_| HeddleError::Config("Failed to acquire pack manager lock".to_string()))?;
1257 manager.snapshot_commit_descriptor_for_state(state)
1258 }
1259
1260 pub fn clear_recent_caches(&self) {
1264 self.clear_recent_object_caches();
1265 }
1266
1267 #[instrument(skip(self))]
1269 pub fn pack_objects(&self, delta_search: bool) -> Result<(u64, u64)> {
1270 self.pack_objects_impl(delta_search)
1271 }
1272
1273 #[instrument(skip(self))]
1275 pub fn prune_loose_objects(&self) -> Result<(u64, u64)> {
1276 self.prune_loose_objects_impl()
1277 }
1278
1279 pub fn discard_corrupt_clone_packs(&self) -> Result<usize> {
1282 let packs = super::fs_paths::packs_dir(&self.root);
1283 let mut removed = 0;
1284 for entry in match fs::read_dir(&packs) {
1285 Ok(entries) => entries,
1286 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
1287 Err(error) => return Err(error.into()),
1288 } {
1289 let path = entry?.path();
1290 match path.extension().and_then(|value| value.to_str()) {
1291 Some("pack") => {
1292 let index = path.with_extension("idx");
1293 let validation =
1294 crate::store::pack::PackReader::open(&path, &index, &self.root.join("tmp"))
1295 .and_then(|reader| validate_pack(self, &reader));
1296 if pack_is_corrupt(validation)? {
1297 fs::remove_file(&path)?;
1298 fs::remove_file(&index)?;
1299 removed += 1;
1300 }
1301 }
1302 Some("npk") if pack_is_corrupt(super::npk1::Npk1Pack::open(&path).map(|_| ()))? => {
1303 fs::remove_file(&path)?;
1304 removed += 1;
1305 }
1306 _ => {}
1307 }
1308 }
1309 if removed > 0 {
1310 self.reload_packs()?;
1311 self.clear_recent_object_caches();
1312 }
1313 Ok(removed)
1314 }
1315}
1316
1317impl ObjectCacheControl for FsStore {
1318 fn clear_recent_caches(&self) {
1319 FsStore::clear_recent_caches(self);
1320 }
1321}
1322
1323impl ObjectStore for FsStore {
1324 fn get_annotated_tag(&self, hash: &ContentHash) -> Result<Option<AnnotatedTag>> {
1325 let path = hash_path(&annotated_tags_dir(&self.root), hash);
1326 if let Some(data) = read_file_bytes(&path)? {
1327 return validate_annotated_tag(data.as_slice(), *hash).map(Some);
1328 }
1329 self.reload_packs_if_stale()?;
1330 if let Ok(manager) = self.pack_manager().read()
1331 && let Some((ObjectType::AnnotatedTag, data)) =
1332 manager.get_object(&PackObjectId::AnnotatedTag(*hash))?
1333 {
1334 return validate_annotated_tag(&data, *hash).map(Some);
1335 }
1336 Ok(None)
1337 }
1338
1339 fn put_annotated_tag(&self, tag: &AnnotatedTag) -> Result<ContentHash> {
1340 let hash = tag.hash();
1341 let path = hash_path(&annotated_tags_dir(&self.root), &hash);
1342 if !path.exists() {
1343 self.write_loose_object_atomic(&path, &tag.encode_current_msgpack())?;
1344 }
1345 Ok(hash)
1346 }
1347
1348 fn list_annotated_tags(&self) -> Result<Vec<ContentHash>> {
1349 self.reload_packs_if_stale()?;
1350 let mut hashes = list_hashes_from_dir(&annotated_tags_dir(&self.root))?;
1351 if let Ok(manager) = self.pack_manager().read() {
1352 append_packed_hashes(&mut hashes, &manager, ObjectType::AnnotatedTag)?;
1353 }
1354 Ok(hashes)
1355 }
1356
1357 fn get_blob_bytes(&self, hash: &ContentHash) -> Result<Option<bytes::Bytes>> {
1364 if let Ok(manager) = self.pack_manager().read()
1365 && let Some((obj_type, data)) = manager.get_hashed_object_bytes(hash)?
1366 && obj_type == crate::store::pack::ObjectType::Blob
1367 {
1368 validate_blob_bytes(data.as_ref(), *hash)?;
1369 return Ok(Some(data));
1370 }
1371 Ok(self
1372 .get_blob(hash)?
1373 .map(|blob| bytes::Bytes::from(blob.into_content())))
1374 }
1375
1376 #[instrument(skip(self), fields(hash = %hash.short()))]
1377 fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>> {
1378 if let Some(blob) = self.recent_blob(hash) {
1379 return Ok(Some(blob));
1380 }
1381 if let Some(blob) = self.try_get_blob_once(hash)? {
1382 return Ok(Some(blob));
1383 }
1384 if self.reload_packs_if_stale()?
1389 && let Some(blob) = self.try_get_blob_once(hash)?
1390 {
1391 return Ok(Some(blob));
1392 }
1393 if let Some(source) = &self.external_source
1394 && let Some(blob) = source.get_blob(hash)?
1395 {
1396 self.cache_recent_blob(*hash, &blob);
1397 return Ok(Some(blob));
1398 }
1399 trace!("Blob not found");
1400 Ok(None)
1401 }
1402
1403 #[instrument(skip(self, blob), fields(size = blob.content().len()))]
1404 fn put_blob(&self, blob: &Blob) -> Result<ContentHash> {
1405 let hash = blob.hash();
1406 let path = hash_path(&blobs_dir(&self.root), &hash);
1407
1408 if !path.exists() {
1409 let data = codec::encode_blob_content(blob.content(), &self.compression)?;
1410 trace!(compressed_size = data.len(), "Writing blob");
1411 self.write_loose_object_atomic(&path, &data)?;
1412 } else {
1413 trace!("Blob already exists, skipping write");
1414 }
1415 self.cache_recent_blob(hash, blob);
1416
1417 Ok(hash)
1418 }
1419
1420 #[instrument(skip(self, blob), fields(hash = %hash.short()))]
1421 fn put_blob_with_hash(&self, blob: &Blob, hash: ContentHash) -> Result<ContentHash> {
1422 if blob.hash() != hash {
1423 return Err(HeddleError::Corruption {
1424 expected: hash,
1425 found: blob.hash(),
1426 });
1427 }
1428
1429 let path = hash_path(&blobs_dir(&self.root), &hash);
1430
1431 if !path.exists() {
1432 let data = codec::encode_blob_content(blob.content(), &self.compression)?;
1433 trace!(
1434 compressed_size = data.len(),
1435 "Writing blob with precomputed hash"
1436 );
1437 self.write_loose_object_atomic(&path, &data)?;
1438 }
1439 self.cache_recent_blob(hash, blob);
1440
1441 Ok(hash)
1442 }
1443
1444 #[instrument(skip(self, data), fields(hash = %hash.short(), size = data.len()))]
1445 fn put_blob_bytes_with_hash(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
1446 validate_blob_bytes(data, hash)?;
1447
1448 let path = hash_path(&blobs_dir(&self.root), &hash);
1449 if !path.exists() {
1450 trace!(
1451 size = data.len(),
1452 "Writing raw blob bytes with precomputed hash"
1453 );
1454 self.write_loose_object_atomic(&path, data)?;
1455 }
1456 self.cache_recent_blob(hash, &Blob::from_slice(data));
1457
1458 Ok(hash)
1459 }
1460
1461 #[instrument(skip(self), fields(hash = %hash.short()))]
1462 fn has_blob(&self, hash: &ContentHash) -> Result<bool> {
1463 if ObjectStore::has_blob_locally(self, hash)? {
1464 return Ok(true);
1465 }
1466 if let Some(source) = &self.external_source {
1467 if self.recent_blob(hash).is_some() {
1468 return Ok(true);
1469 }
1470 if let Some(blob) = source.get_blob(hash)? {
1471 self.cache_recent_blob(*hash, &blob);
1472 return Ok(true);
1473 }
1474 }
1475 Ok(false)
1476 }
1477
1478 fn has_blob_locally(&self, hash: &ContentHash) -> Result<bool> {
1479 if self.try_has_blob_once(hash)? {
1480 return Ok(true);
1481 }
1482 Ok(self.reload_packs_if_stale()? && self.try_has_blob_once(hash)?)
1483 }
1484
1485 fn loose_blob_path(&self, hash: &ContentHash) -> Option<PathBuf> {
1502 let path = hash_path(&blobs_dir(&self.root), hash);
1503 if let Ok(verified) = self.verified_loose_blobs.read()
1508 && verified.contains(hash)
1509 && path.exists()
1510 {
1511 return Some(path);
1512 }
1513
1514 let (header, _) = read_file_header(&path, BLOB_HEADER_PEEK).ok().flatten()?;
1527 if is_compressed(&header) {
1528 return None;
1529 }
1530 let bytes = read_file_bytes(&path).ok().flatten()?;
1531 let actual = ContentHash::compute_typed("blob", bytes.as_slice());
1532 if actual != *hash {
1533 return None;
1537 }
1538 if let Ok(mut verified) = self.verified_loose_blobs.write() {
1539 verified.insert(*hash, ());
1540 }
1541 Some(path)
1542 }
1543
1544 #[instrument(skip(self), fields(hash = %hash.short()))]
1557 fn promote_to_loose_uncompressed(&self, hash: &ContentHash) -> Result<bool> {
1558 let path = hash_path(&blobs_dir(&self.root), hash);
1559
1560 if !ObjectStore::has_blob_locally(self, hash)?
1564 && let Some(source) = &self.external_source
1565 && (self.recent_blob(hash).is_some() || source.get_blob(hash)?.is_some())
1566 {
1567 return Ok(false);
1568 }
1569
1570 if let Some((header, _)) = read_file_header(&path, 9)?
1572 && !is_compressed(&header)
1573 {
1574 trace!("Blob already loose+uncompressed; skipping promotion");
1575 return Ok(false);
1576 }
1577
1578 let blob = self.get_blob(hash)?.ok_or_else(|| {
1582 HeddleError::NotFound(format!(
1583 "blob {} not found in store; cannot promote to loose-uncompressed",
1584 hash
1585 ))
1586 })?;
1587
1588 debug!(
1601 size = blob.content().len(),
1602 "Promoting blob to loose-uncompressed canonical store"
1603 );
1604 self.write_loose_object_cache(&path, blob.content())?;
1605 if let Ok(mut verified) = self.verified_loose_blobs.write() {
1606 verified.insert(*hash, ());
1607 }
1608 Ok(true)
1609 }
1610
1611 #[instrument(skip(self), fields(hash = %hash.short()))]
1612 fn blob_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
1613 if let Some(size) = self.try_get_blob_size_once(hash)? {
1614 return Ok(Some(size));
1615 }
1616 if self.reload_packs_if_stale()?
1620 && let Some(size) = self.try_get_blob_size_once(hash)?
1621 {
1622 return Ok(Some(size));
1623 }
1624 if let Some(source) = &self.external_source {
1625 if let Some(blob) = self.recent_blob(hash) {
1626 return Ok(Some(blob.content().len() as u64));
1627 }
1628 if let Some(blob) = source.get_blob(hash)? {
1629 let size = blob.content().len() as u64;
1630 self.cache_recent_blob(*hash, &blob);
1631 return Ok(Some(size));
1632 }
1633 }
1634 Ok(None)
1635 }
1636
1637 #[instrument(skip(self), fields(hash = %hash.short()))]
1638 fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>> {
1639 if let Some(tree) = self.recent_tree(hash) {
1640 return Ok(Some(tree));
1641 }
1642 if let Some(tree) = self.try_get_tree_once(hash)? {
1643 return Ok(Some(tree));
1644 }
1645 if self.reload_packs_if_stale()?
1646 && let Some(tree) = self.try_get_tree_once(hash)?
1647 {
1648 return Ok(Some(tree));
1649 }
1650 if let Some(source) = &self.external_source
1651 && let Some(tree) = source.get_tree(hash)?
1652 {
1653 self.cache_recent_tree(*hash, &tree);
1654 return Ok(Some(tree));
1655 }
1656 trace!("Tree not found");
1657 Ok(None)
1658 }
1659
1660 #[instrument(skip(self), fields(hash = %hash.short(), name))]
1661 fn get_tree_entry(&self, hash: &ContentHash, name: &str) -> Result<Option<TreeEntry>> {
1662 if let Some(entry) = self.try_get_tree_entry_once(hash, name)? {
1663 return Ok(Some(entry));
1664 }
1665 if self.reload_packs_if_stale()?
1666 && let Some(entry) = self.try_get_tree_entry_once(hash, name)?
1667 {
1668 return Ok(Some(entry));
1669 }
1670 if let Some(source) = &self.external_source
1671 && let Some(tree) = source.get_tree(hash)?
1672 {
1673 let entry = tree.get(name).cloned();
1674 self.cache_recent_tree(*hash, &tree);
1675 return Ok(entry);
1676 }
1677 Ok(None)
1678 }
1679
1680 #[instrument(skip(self), fields(hash = %hash.short()))]
1681 fn get_tree_serialized(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
1682 if let Some(data) = self.try_get_tree_serialized_once(hash)? {
1683 return Ok(Some(data));
1684 }
1685 if self.reload_packs_if_stale()?
1686 && let Some(data) = self.try_get_tree_serialized_once(hash)?
1687 {
1688 return Ok(Some(data));
1689 }
1690 let external_tree = if let Some(tree) = self.recent_tree(hash) {
1691 Some(tree)
1692 } else if let Some(source) = &self.external_source {
1693 let tree = source.get_tree(hash)?;
1694 if let Some(tree) = &tree {
1695 self.cache_recent_tree(*hash, tree);
1696 }
1697 tree
1698 } else {
1699 None
1700 };
1701 if let Some(tree) = external_tree {
1702 return tree.encode_canonical().map(Some).map_err(HeddleError::from);
1703 }
1704 Ok(None)
1705 }
1706
1707 fn open_tree(
1708 &self,
1709 tree_id: &ContentHash,
1710 cursor: Option<&TreeResumeCursor>,
1711 ) -> Result<Option<TreeEntryReader<OpenedTreeBody>>> {
1712 if let Some(reader) = self.try_open_tree_once(tree_id, cursor)? {
1713 return Ok(Some(reader));
1714 }
1715 if self.reload_packs_if_stale()?
1716 && let Some(reader) = self.try_open_tree_once(tree_id, cursor)?
1717 {
1718 return Ok(Some(reader));
1719 }
1720 if let Some(data) = ObjectStore::get_tree_serialized(self, tree_id)? {
1721 let body = if is_streamable_tree(&data) {
1722 data
1723 } else if data.starts_with(TREE_DELTA_MAGIC) {
1724 self.get_tree(tree_id)?
1725 .ok_or_else(|| HeddleError::NotFound(format!("tree {tree_id}")))?
1726 .encode_lean()?
1727 } else if is_salted_tree(&data) {
1728 return Err(salted_tree_not_streamable(tree_id));
1729 } else {
1730 return Ok(None);
1731 };
1732 return Ok(Some(TreeEntryReader::open(
1733 OpenedTreeBody::Bytes(BytesTreeSource::sequential_verify(body)),
1734 *tree_id,
1735 cursor,
1736 )?));
1737 }
1738 Ok(None)
1739 }
1740
1741 #[instrument(skip(self, tree), fields(entry_count = tree.entries().len()))]
1742 fn put_tree(&self, tree: &Tree) -> Result<ContentHash> {
1743 let hash = tree.hash();
1744 let path = hash_path(&trees_dir(&self.root), &hash);
1745
1746 if !ObjectStore::has_tree_locally(self, &hash)? {
1751 let (_, data) = codec::encode_tree(tree, &self.compression)?;
1752 trace!(compressed_size = data.len(), "Writing tree");
1753 self.write_loose_object_atomic(&path, &data)?;
1754 } else {
1755 trace!("Tree already exists, skipping write");
1756 }
1757 if let Ok(mut cache) = self.recent_trees.write() {
1758 cache.insert(hash, tree.clone());
1759 }
1760
1761 self.remove_partial_tree(&hash)?;
1767
1768 Ok(hash)
1769 }
1770
1771 #[instrument(skip(self, data), fields(hash = %hash.short(), size = data.len()))]
1772 fn put_tree_serialized(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
1773 if is_redacted_tree(data) {
1776 self.put_partial_tree(&hash, data)?;
1777 return Ok(hash);
1778 }
1779 let tree = validate_loaded_tree(self.decode_tree_storage_body(hash, data)?)?;
1780
1781 let path = hash_path(&trees_dir(&self.root), &hash);
1782 let should_write = match read_file_bytes(&path)? {
1783 Some(existing) => codec::decode_tree_body(existing.as_slice())? != data,
1784 None => true,
1785 };
1786 if should_write {
1787 trace!(size = data.len(), "Writing raw serialized tree");
1788 self.write_loose_object_atomic(&path, data)?;
1789 }
1790 if let Ok(mut cache) = self.recent_trees.write() {
1791 cache.insert(hash, tree);
1792 }
1793
1794 self.remove_partial_tree(&hash)?;
1797
1798 Ok(hash)
1799 }
1800
1801 #[instrument(skip(self), fields(hash = %hash.short()))]
1802 fn has_tree(&self, hash: &ContentHash) -> Result<bool> {
1803 if ObjectStore::has_tree_locally(self, hash)? {
1804 return Ok(true);
1805 }
1806 if let Some(source) = &self.external_source {
1807 if self.recent_tree(hash).is_some() {
1808 return Ok(true);
1809 }
1810 if let Some(tree) = source.get_tree(hash)? {
1811 self.cache_recent_tree(*hash, &tree);
1812 return Ok(true);
1813 }
1814 }
1815 Ok(false)
1816 }
1817
1818 fn has_tree_locally(&self, hash: &ContentHash) -> Result<bool> {
1819 if self.try_has_tree_once(hash)? {
1820 return Ok(true);
1821 }
1822 Ok(self.reload_packs_if_stale()? && self.try_has_tree_once(hash)?)
1823 }
1824
1825 fn has_partial_tree(&self, hash: &ContentHash) -> Result<bool> {
1826 Ok(partial_tree_path(&self.root, hash).exists())
1827 }
1828
1829 fn get_partial_tree_bytes(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
1830 let path = partial_tree_path(&self.root, hash);
1831 match fs::read(&path) {
1832 Ok(bytes) => Ok(Some(bytes)),
1833 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
1834 Err(err) => Err(HeddleError::Io(err)),
1835 }
1836 }
1837
1838 fn put_partial_tree_bytes(&self, hash: &ContentHash, bytes: &[u8]) -> Result<()> {
1839 let dir = partial_trees_dir(&self.root);
1840 if !dir.exists() {
1841 crate::fs_atomic::create_dir_all_durable(&dir)?;
1842 }
1843 let path = partial_tree_path(&self.root, hash);
1844 crate::fs_atomic::write_file_atomic(&path, bytes)?;
1845 Ok(())
1846 }
1847
1848 fn list_partial_trees(&self) -> Result<Vec<ContentHash>> {
1849 let dir = partial_trees_dir(&self.root);
1850 if !dir.exists() {
1851 return Ok(Vec::new());
1852 }
1853 let mut out = Vec::new();
1854 for entry in fs::read_dir(&dir)? {
1855 let entry = entry?;
1856 let path = entry.path();
1857 if path.extension().and_then(|e| e.to_str()) != Some("bin") {
1858 continue;
1859 }
1860 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
1861 continue;
1862 };
1863 if let Ok(hash) = ContentHash::from_hex(stem) {
1864 out.push(hash);
1865 }
1866 }
1867 Ok(out)
1868 }
1869
1870 fn remove_partial_tree(&self, hash: &ContentHash) -> Result<()> {
1871 let path = partial_tree_path(&self.root, hash);
1872 match fs::remove_file(&path) {
1873 Ok(()) => Ok(()),
1874 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
1875 Err(err) => Err(HeddleError::Io(err)),
1876 }
1877 }
1878
1879 #[instrument(skip(self), fields(id = %id.short()))]
1880 fn get_state(&self, id: &StateId) -> Result<Option<State>> {
1881 if let Some(state) = self.recent_state(id) {
1882 return Ok(Some(state));
1883 }
1884 if let Some(state) = self.try_get_state_once(id)? {
1885 return Ok(Some(state));
1886 }
1887 if self.reload_packs_if_stale()?
1888 && let Some(state) = self.try_get_state_once(id)?
1889 {
1890 return Ok(Some(state));
1891 }
1892 if let Some(source) = &self.external_source
1893 && let Some(state) = source.get_state(id)?
1894 {
1895 self.cache_recent_state(*id, &state);
1896 return Ok(Some(state));
1897 }
1898 trace!("State not found");
1899 Ok(None)
1900 }
1901
1902 #[instrument(skip(self, state), fields(id = %state.id().short()))]
1903 fn put_state(&self, state: &State) -> Result<()> {
1904 let state_id = state.id();
1905 let path = state_path(&self.root, &state_id);
1906 let data = codec::encode_state(state, &self.compression)?;
1907 trace!(compressed_size = data.len(), "Writing state");
1908 self.write_loose_object_atomic(&path, &data)?;
1909 if let Ok(mut cache) = self.recent_states.write() {
1910 let mut cached = state.clone();
1911 cached.state_id = state_id;
1912 cache.insert(state_id, cached);
1913 }
1914 Ok(())
1915 }
1916
1917 #[instrument(skip(self, data), fields(id = %id.short(), size = data.len()))]
1918 fn put_state_serialized(&self, data: &[u8], id: StateId) -> Result<()> {
1919 let state = validate_state_serialized(data, id)?;
1920 let path = state_path(&self.root, &id);
1921 trace!(size = data.len(), "Writing raw serialized state");
1922 self.write_loose_object_atomic(&path, data)?;
1923 if let Ok(mut cache) = self.recent_states.write() {
1924 cache.insert(id, state);
1925 }
1926 Ok(())
1927 }
1928
1929 #[instrument(skip(self), fields(id = %id.short()))]
1930 fn has_state(&self, id: &StateId) -> Result<bool> {
1931 if self.try_has_state_once(id)? {
1932 return Ok(true);
1933 }
1934 if self.reload_packs_if_stale()? && self.try_has_state_once(id)? {
1935 return Ok(true);
1936 }
1937 if let Some(source) = &self.external_source {
1938 if self.recent_state(id).is_some() {
1939 return Ok(true);
1940 }
1941 if let Some(state) = source.get_state(id)? {
1942 self.cache_recent_state(*id, &state);
1943 return Ok(true);
1944 }
1945 }
1946 Ok(false)
1947 }
1948
1949 #[instrument(skip(self))]
1950 fn list_states(&self) -> Result<Vec<StateId>> {
1951 self.reload_packs_if_stale()?;
1952
1953 let mut states = Vec::new();
1954 let mut known = HashSet::new();
1955 let dir = states_dir(&self.root);
1956 if dir.exists() {
1957 for entry in fs::read_dir(&dir)? {
1958 let entry = entry?;
1959 let path = entry.path();
1960 if let Some(name) = path.file_stem()
1961 && let Some(name_str) = name.to_str()
1962 && let Ok(id) = StateId::parse(name_str)
1963 && known.insert(id)
1964 {
1965 states.push(id);
1966 }
1967 }
1968 }
1969 if let Ok(manager) = self.pack_manager().read() {
1970 append_unique_states(
1971 &mut states,
1972 &mut known,
1973 manager
1974 .list_all_ids()?
1975 .into_iter()
1976 .filter_map(|id| match id {
1977 PackObjectId::StateId(state) => Some(state),
1978 PackObjectId::Hash(_) | PackObjectId::AnnotatedTag(_) => None,
1979 }),
1980 );
1981 }
1982 if let Some(source) = &self.external_source {
1983 append_unique_states(&mut states, &mut known, source.list_states()?);
1984 }
1985 debug!(count = states.len(), "Listed states");
1986 Ok(states)
1987 }
1988
1989 fn get_state_attachment(
1990 &self,
1991 state: &StateId,
1992 id: &StateAttachmentId,
1993 ) -> Result<Option<StateAttachment>> {
1994 if let Some(attachment) = self.try_get_state_attachment_once(state, id)? {
1995 return Ok(Some(attachment));
1996 }
1997 if self.reload_packs_if_stale()? {
1998 return self.try_get_state_attachment_once(state, id);
1999 }
2000 Ok(None)
2001 }
2002
2003 fn put_state_attachment(&self, attachment: &StateAttachment) -> Result<StateAttachmentId> {
2004 let id = attachment.id();
2005 self.with_state_attachment_index_lock(&attachment.state_id, || {
2006 let index_path = state_attachment_index_path(&self.root, &attachment.state_id);
2007 let mut ids: Vec<StateAttachmentId> = match read_file_bytes(&index_path)? {
2008 Some(bytes) => rmp_serde::from_slice(bytes.as_slice())?,
2009 None => self.rebuild_state_attachment_index(&attachment.state_id)?,
2010 };
2011 if !ids.contains(&id) {
2012 ids.push(id);
2013 ids.sort();
2014 self.write_loose_object_atomic(&index_path, &rmp_serde::to_vec_named(&ids)?)?;
2015 }
2016 let path = state_attachment_path(&self.root, &attachment.state_id, &id);
2017 self.write_loose_object_atomic(&path, &attachment.encode_current_msgpack()?)?;
2018 Ok(id)
2019 })
2020 }
2021
2022 fn list_state_attachments(&self, state: &StateId) -> Result<Vec<StateAttachment>> {
2023 self.with_state_attachment_index_lock(state, || {
2024 let index_path = state_attachment_index_path(&self.root, state);
2025 let mut ids: Vec<StateAttachmentId> = match read_file_bytes(&index_path)? {
2026 Some(bytes) => rmp_serde::from_slice(bytes.as_slice())?,
2027 None => self.rebuild_state_attachment_index(state)?,
2028 };
2029 let mut attachments = Vec::new();
2030 let mut stale = false;
2031 for id in &ids {
2032 match self.get_state_attachment(state, id)? {
2033 Some(attachment) => attachments.push(attachment),
2034 None => stale = true,
2035 }
2036 }
2037 if stale {
2038 ids = self.rebuild_state_attachment_index(state)?;
2039 attachments.clear();
2040 for id in ids {
2041 let attachment = self.get_state_attachment(state, &id)?.ok_or_else(|| {
2042 HeddleError::InvalidObject(format!(
2043 "rebuilt state attachment index references missing {id}"
2044 ))
2045 })?;
2046 attachments.push(attachment);
2047 }
2048 }
2049 Ok(attachments)
2050 })
2051 }
2052
2053 #[instrument(skip(self), fields(id = %id))]
2054 fn get_action(&self, id: &ActionId) -> Result<Option<Action>> {
2055 if let Some(action) = self.try_get_action_once(id)? {
2056 return Ok(Some(action));
2057 }
2058 if self.reload_packs_if_stale()? {
2059 return self.try_get_action_once(id);
2060 }
2061 trace!("Action not found");
2062 Ok(None)
2063 }
2064
2065 #[instrument(skip(self, action))]
2066 fn put_action(&self, action: &mut Action) -> Result<ActionId> {
2067 let id = action.id();
2068 let path = action_path(&self.root, &id);
2069
2070 if !path.exists() {
2071 let (_, data) = codec::encode_action(action, &self.compression)?;
2072 trace!(id = %id, compressed_size = data.len(), "Writing action");
2073 self.write_loose_object_atomic(&path, &data)?;
2074 }
2075
2076 Ok(id)
2077 }
2078
2079 #[instrument(skip(self))]
2080 fn list_actions(&self) -> Result<Vec<ActionId>> {
2081 self.reload_packs_if_stale()?;
2082 let dir = actions_dir(&self.root);
2083 let mut action_hashes = Vec::new();
2084 if dir.exists() {
2085 for entry in fs::read_dir(&dir)? {
2086 let entry = entry?;
2087 let path = entry.path();
2088 if let Some(name) = path.file_stem()
2089 && let Some(name_str) = name.to_str()
2090 && let Ok(hash) = ContentHash::from_hex(name_str)
2091 {
2092 action_hashes.push(hash);
2093 }
2094 }
2095 }
2096 if let Ok(manager) = self.pack_manager().read() {
2097 append_packed_hashes(&mut action_hashes, &manager, ObjectType::Action)?;
2098 }
2099 let actions = action_hashes
2100 .into_iter()
2101 .map(ActionId::from_hash)
2102 .collect::<Vec<_>>();
2103 debug!(count = actions.len(), "Listed actions");
2104 Ok(actions)
2105 }
2106
2107 #[instrument(skip(self))]
2108 fn list_blobs(&self) -> Result<Vec<ContentHash>> {
2109 self.reload_packs_if_stale()?;
2110 let dir = blobs_dir(&self.root);
2111 let mut blobs = list_hashes_from_dir(&dir)?;
2112 if let Ok(manager) = self.pack_manager().read() {
2113 append_packed_hashes(&mut blobs, &manager, ObjectType::Blob)?;
2114 }
2115 Ok(blobs)
2116 }
2117
2118 #[instrument(skip(self))]
2119 fn list_trees(&self) -> Result<Vec<ContentHash>> {
2120 self.reload_packs_if_stale()?;
2121 let dir = trees_dir(&self.root);
2122 let mut trees = list_hashes_from_dir(&dir)?;
2123 if let Ok(manager) = self.pack_manager().read() {
2124 append_packed_hashes(&mut trees, &manager, ObjectType::Tree)?;
2125 }
2126 if let Ok(manager) = self.npk1_manager().read() {
2127 trees.extend(manager.list_ids()?);
2128 }
2129 trees.sort();
2130 trees.dedup();
2131 Ok(trees)
2132 }
2133
2134 #[instrument(skip(self), fields(id = ?id))]
2135 fn get_pack_object(&self, id: &PackObjectId) -> Result<Option<(ObjectType, Vec<u8>)>> {
2136 if let Ok(manager) = self.pack_manager().read()
2137 && let Some((obj_type, data)) = manager.get_object(id)?
2138 {
2139 return Ok(Some((obj_type, data)));
2140 }
2141
2142 match id {
2143 PackObjectId::AnnotatedTag(hash) => Ok(self
2144 .get_annotated_tag(hash)?
2145 .map(|tag| (ObjectType::AnnotatedTag, tag.encode_current_msgpack()))),
2146 PackObjectId::Hash(hash) => {
2147 if let Some(blob) = self.get_blob(hash)? {
2148 return Ok(Some((ObjectType::Blob, blob.into_content())));
2149 }
2150 if let Some(tree_data) = self.get_tree_serialized(hash)? {
2154 return Ok(Some((ObjectType::Tree, tree_data)));
2155 }
2156 if let Some(action) = self.get_action(&ActionId::from_hash(*hash))? {
2157 return Ok(Some((
2158 ObjectType::Action,
2159 rmp_serde::to_vec_named(&action)?,
2160 )));
2161 }
2162 Ok(None)
2163 }
2164 PackObjectId::StateId(change_id) => {
2165 if let Some(state) = self.get_state(change_id)? {
2166 Ok(Some((ObjectType::State, state.encode_current_msgpack()?)))
2167 } else {
2168 Ok(None)
2169 }
2170 }
2171 }
2172 }
2173
2174 #[instrument(skip(self, pack_data, index_data))]
2175 fn install_pack(&self, pack_data: &[u8], index_data: &[u8]) -> Result<Vec<PackObjectId>> {
2176 let reader = crate::store::pack::PackReader::from_slice(
2177 pack_data,
2178 index_data,
2179 &self.root.join("tmp"),
2180 )?;
2181 let ids = validate_and_list_pack(self, &reader)?;
2182 let state_entries = self.states_needing_loose_copies(&reader, &ids)?;
2183 let attachment_entries = attachment_entries_from_pack(&reader, &ids)?;
2184 self.install_pack_files(pack_data, index_data)?;
2185 self.write_packed_state_mirrors_batch(state_entries)?;
2186 for attachment in attachment_entries {
2187 self.put_state_attachment(&attachment)?;
2188 }
2189 self.clear_recent_object_caches();
2190 Ok(ids)
2191 }
2192
2193 #[instrument(skip(self, blobs), fields(count = blobs.len()))]
2194 fn put_blobs_packed(&self, blobs: Vec<(crate::object::ContentHash, Vec<u8>)>) -> Result<()> {
2195 self.put_blobs_packed_impl(blobs)
2196 }
2197
2198 #[instrument(skip(self, blobs, tree, state), fields(blob_count = blobs.len()))]
2199 fn put_snapshot_objects_packed(
2200 &self,
2201 blobs: Vec<(ContentHash, Vec<u8>)>,
2202 tree: &Tree,
2203 state: &State,
2204 ) -> Result<()> {
2205 self.put_snapshot_objects_packed_impl(
2206 blobs,
2207 Vec::new(),
2208 &TreeWrite::anchor(tree.clone()),
2209 state,
2210 Vec::new(),
2211 None,
2212 )
2213 .map(|_| ())
2214 }
2215
2216 fn put_snapshot_objects_and_attachments_packed(
2217 &self,
2218 blobs: Vec<(ContentHash, Vec<u8>)>,
2219 tree: &Tree,
2220 state: &State,
2221 attachments: Vec<StateAttachment>,
2222 ) -> Result<()> {
2223 self.put_snapshot_objects_packed_impl(
2224 blobs,
2225 Vec::new(),
2226 &TreeWrite::anchor(tree.clone()),
2227 state,
2228 attachments,
2229 None,
2230 )
2231 .map(|_| ())
2232 }
2233
2234 #[instrument(skip(self))]
2235 fn install_pack_streaming(
2236 &self,
2237 pack_path: &Path,
2238 index_path: &Path,
2239 ) -> Result<crate::store::pack::PackInventory> {
2240 use std::io::{BufReader, BufWriter, Read, Write};
2241 let directory =
2242 crate::store::pack::ScratchDir::new(&self.root.join("tmp"), "pack-install-")?;
2243 let scratch = directory.path();
2244 let inventory =
2245 crate::store::pack::PackInventory::copy_from_index(index_path, &self.root.join("tmp"))?;
2246 self.reload_packs_if_stale()?;
2251 let mut metadata = tempfile::NamedTempFile::new_in(scratch)?;
2252 let pack_name;
2253 {
2254 let reader = crate::store::pack::PackReader::open(pack_path, index_path, scratch)?;
2255 let mut writer = BufWriter::new(metadata.as_file_mut());
2256 visit_validated_pack(self, &reader, |id, kind, data| {
2257 let retain = match id {
2258 PackObjectId::StateId(state) => self.try_has_state_once(&state)?,
2259 _ => kind == ObjectType::StateAttachment,
2260 };
2261 if retain {
2262 let mut key = Vec::with_capacity(33);
2263 id.encode_tagged(&mut key);
2264 writer.write_all(&key)?;
2265 writer.write_all(&(data.len() as u64).to_be_bytes())?;
2266 writer.write_all(data)?;
2267 }
2268 Ok(())
2269 })?;
2270 writer.flush()?;
2271 pack_name = reader.representation_hash().to_hex();
2274 }
2275 self.install_pack_files_streaming(pack_path, index_path, &pack_name)?;
2276 if metadata.as_file().metadata()?.len() == 0 {
2277 return Ok(inventory);
2278 }
2279 let mut input = BufReader::new(File::open(metadata.path())?);
2280 self.begin_snapshot_write_batch_impl()?;
2281 let result = (|| {
2282 loop {
2283 let mut header = [0; 41];
2284 if input.read(&mut header[..1])? == 0 {
2285 break;
2286 }
2287 input.read_exact(&mut header[1..])?;
2288 let (id, _) = PackObjectId::decode_tagged(&header[..33])?;
2289 let length = usize::try_from(u64::from_be_bytes(header[33..].try_into().map_err(
2290 |_| HeddleError::InvalidObject("metadata length truncated".into()),
2291 )?))
2292 .map_err(|_| {
2293 HeddleError::InvalidObject("metadata length exceeds platform".into())
2294 })?;
2295 let mut data = vec![0; length];
2296 input.read_exact(&mut data)?;
2297 match id {
2298 PackObjectId::StateId(state) => {
2299 ObjectStore::put_state_serialized(self, &data, state)?
2300 }
2301 _ => {
2302 self.put_state_attachment(&StateAttachment::decode_current_msgpack(
2303 &data,
2304 )?)?;
2305 }
2306 }
2307 }
2308 self.flush_snapshot_write_batch_impl()
2309 })();
2310 if result.is_err() {
2311 self.abort_snapshot_write_batch_impl();
2312 }
2313 result?;
2314 Ok(inventory)
2315 }
2316
2317 #[instrument(skip(self))]
2318 fn begin_snapshot_write_batch(&self) -> Result<()> {
2319 self.begin_snapshot_write_batch_impl()
2320 }
2321
2322 #[instrument(skip(self))]
2323 fn flush_snapshot_write_batch(&self) -> Result<()> {
2324 self.flush_snapshot_write_batch_impl()
2325 }
2326
2327 #[instrument(skip(self))]
2328 fn abort_snapshot_write_batch(&self) {
2329 self.abort_snapshot_write_batch_impl();
2330 }
2331}
2332
2333impl SidecarStore for FsStore {
2334 fn has_redactions_for_blob(&self, blob: &ContentHash) -> Result<bool> {
2335 Ok(redaction_path(&self.root, blob).exists())
2336 }
2337
2338 fn get_redactions_bytes_for_blob(&self, blob: &ContentHash) -> Result<Option<Vec<u8>>> {
2339 let path = redaction_path(&self.root, blob);
2340 match fs::read(&path) {
2341 Ok(bytes) => Ok(Some(bytes)),
2342 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
2343 Err(err) => Err(HeddleError::Io(err)),
2344 }
2345 }
2346
2347 fn put_redactions_bytes_for_blob(&self, blob: &ContentHash, bytes: &[u8]) -> Result<()> {
2348 let dir = redactions_dir(&self.root);
2349 if !dir.exists() {
2350 crate::fs_atomic::create_dir_all_durable(&dir)?;
2351 }
2352 let path = redaction_path(&self.root, blob);
2353 crate::fs_atomic::write_file_atomic(&path, bytes)?;
2354 Ok(())
2355 }
2356
2357 fn list_blobs_with_redactions(&self) -> Result<Vec<ContentHash>> {
2358 let dir = redactions_dir(&self.root);
2359 if !dir.exists() {
2360 return Ok(Vec::new());
2361 }
2362 let mut out = Vec::new();
2363 for entry in fs::read_dir(&dir)? {
2364 let entry = entry?;
2365 let path = entry.path();
2366 if path.extension().and_then(|e| e.to_str()) != Some("bin") {
2367 continue;
2368 }
2369 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
2370 continue;
2371 };
2372 if let Ok(hash) = ContentHash::from_hex(stem) {
2373 out.push(hash);
2374 }
2375 }
2376 Ok(out)
2377 }
2378
2379 fn has_state_visibility_for_state(&self, state: &StateId) -> Result<bool> {
2380 Ok(state_visibility_path(&self.root, state).exists())
2381 }
2382
2383 fn get_state_visibility_bytes_for_state(&self, state: &StateId) -> Result<Option<Vec<u8>>> {
2384 let path = state_visibility_path(&self.root, state);
2385 match fs::read(&path) {
2386 Ok(bytes) => Ok(Some(bytes)),
2387 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
2388 Err(err) => Err(HeddleError::Io(err)),
2389 }
2390 }
2391
2392 fn put_state_visibility_bytes_for_state(&self, state: &StateId, bytes: &[u8]) -> Result<()> {
2393 let dir = state_visibility_dir(&self.root);
2394 if !dir.exists() {
2395 crate::fs_atomic::create_dir_all_durable(&dir)?;
2396 }
2397 let path = state_visibility_path(&self.root, state);
2398 crate::fs_atomic::write_file_atomic(&path, bytes)?;
2399 Ok(())
2400 }
2401
2402 fn list_states_with_visibility(&self) -> Result<Vec<StateId>> {
2403 let dir = state_visibility_dir(&self.root);
2404 if !dir.exists() {
2405 return Ok(Vec::new());
2406 }
2407 let mut out = Vec::new();
2408 for entry in fs::read_dir(&dir)? {
2409 let entry = entry?;
2410 let path = entry.path();
2411 if path.extension().and_then(|e| e.to_str()) != Some("bin") {
2412 continue;
2413 }
2414 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
2415 continue;
2416 };
2417 if let Ok(state) = StateId::parse(stem) {
2418 out.push(state);
2419 }
2420 }
2421 Ok(out)
2422 }
2423}
2424
2425#[cfg(test)]
2426mod state_attachment_tests {
2427 use std::sync::Arc;
2428
2429 use chrono::Utc;
2430
2431 use super::*;
2432 use crate::{
2433 object::{Attribution, Principal, StateAttachmentBody},
2434 store::{CompressionConfig, pack::PackBuilder},
2435 };
2436
2437 fn fixture(store: &FsStore) -> (State, StateAttachment) {
2438 let tree = store.put_tree(&Tree::new()).unwrap();
2439 let attribution = Attribution::human(Principal::new("Test", "test@example.com"));
2440 let state = State::new(tree, vec![], attribution.clone());
2441 store.put_state(&state).unwrap();
2442 let attachment = StateAttachment {
2443 state_id: state.id(),
2444 body: StateAttachmentBody::Context(ContentHash::compute(b"context")),
2445 attribution,
2446 created_at: Utc::now(),
2447 supersedes: None,
2448 };
2449 (state, attachment)
2450 }
2451
2452 #[test]
2453 fn concurrent_attachment_writes_keep_every_index_entry() {
2454 let temp = tempfile::TempDir::new().unwrap();
2455 let store = Arc::new(FsStore::new(temp.path()));
2456 let (state, base) = fixture(&store);
2457 let mut threads = Vec::new();
2458 for byte in 0..16u8 {
2459 let store = Arc::clone(&store);
2460 let mut attachment = base.clone();
2461 attachment.body = StateAttachmentBody::Context(ContentHash::compute(&[byte]));
2462 threads.push(std::thread::spawn(move || {
2463 store.put_state_attachment(&attachment).unwrap();
2464 }));
2465 }
2466 for thread in threads {
2467 thread.join().unwrap();
2468 }
2469 assert_eq!(store.list_state_attachments(&state.id()).unwrap().len(), 16);
2470 }
2471
2472 #[test]
2473 fn missing_index_rebuilds_from_loose_objects() {
2474 let temp = tempfile::TempDir::new().unwrap();
2475 let store = FsStore::new(temp.path());
2476 let (state, attachment) = fixture(&store);
2477 store.put_state_attachment(&attachment).unwrap();
2478 fs::remove_file(state_attachment_index_path(&store.root, &state.id())).unwrap();
2479 assert_eq!(
2480 store.list_state_attachments(&state.id()).unwrap(),
2481 vec![attachment]
2482 );
2483 }
2484
2485 #[test]
2486 fn packed_attachment_uses_state_index_for_lookup() {
2487 let temp = tempfile::TempDir::new().unwrap();
2488 let store = FsStore::new(temp.path());
2489 let (state, attachment) = fixture(&store);
2490 let mut builder = PackBuilder::new(CompressionConfig::default());
2491 builder.add(
2492 *attachment.id().as_hash(),
2493 ObjectType::StateAttachment,
2494 rmp_serde::to_vec_named(&attachment).unwrap(),
2495 );
2496 let (pack, index, _) = builder.build().unwrap();
2497 store.install_pack(&pack, &index).unwrap();
2498 fs::remove_file(state_attachment_path(
2499 &store.root,
2500 &state.id(),
2501 &attachment.id(),
2502 ))
2503 .unwrap();
2504 let rebuild_marker =
2505 state_attachment_index_path(&store.root, &state.id()).with_extension("rebuild-marker");
2506 let _ = fs::remove_file(&rebuild_marker);
2507 assert_eq!(
2508 store.list_state_attachments(&state.id()).unwrap(),
2509 vec![attachment.clone()]
2510 );
2511 assert_eq!(
2512 store.list_state_attachments(&state.id()).unwrap(),
2513 vec![attachment]
2514 );
2515 assert!(!rebuild_marker.exists());
2516 }
2517}
2518
2519#[cfg(test)]
2520mod enumeration_tests {
2521 use heddle_format::{compression::CompressionConfig, delta::DeltaEncoder};
2522 use tempfile::TempDir;
2523
2524 use super::*;
2525 use crate::store::pack::{
2526 PackBuilder, PackContainerSpec, PackIndex, append_container_checksum,
2527 encode_tagged_entry_parts, write_container_header,
2528 };
2529
2530 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
2531 struct TestEnumerationMetrics {
2532 membership_checks: u64,
2533 header_reads: u64,
2534 full_object_decodes: u64,
2535 }
2536
2537 impl EnumerationCounter for TestEnumerationMetrics {
2538 fn membership_check(&mut self) {
2539 self.membership_checks += 1;
2540 }
2541
2542 fn header_read(&mut self) {
2543 self.header_reads += 1;
2544 }
2545 }
2546
2547 fn install_pack_files(
2548 dir: &TempDir,
2549 name: &str,
2550 pack_data: &[u8],
2551 index_data: &[u8],
2552 ) -> PackManager {
2553 fs::write(dir.path().join(format!("{name}.pack")), pack_data).unwrap();
2554 fs::write(dir.path().join(format!("{name}.idx")), index_data).unwrap();
2555 PackManager::new(dir.path().to_path_buf(), dir.path().join("tmp"))
2556 }
2557
2558 fn raw_mixed_manager() -> (TempDir, PackManager, Vec<(ContentHash, ObjectType)>) {
2559 let dir = TempDir::new().unwrap();
2560 let objects = [
2561 (ObjectType::Blob, b"packed blob".as_slice()),
2562 (ObjectType::Tree, b"packed tree".as_slice()),
2563 (ObjectType::Action, b"packed action".as_slice()),
2564 ];
2565 let mut builder = PackBuilder::new(CompressionConfig::disabled());
2566 let mut classified = Vec::new();
2567 for (obj_type, data) in objects {
2568 let hash = ContentHash::compute_typed("enumeration-test", data);
2569 builder.add(hash, obj_type, data.to_vec());
2570 classified.push((hash, obj_type));
2571 }
2572 let (pack, index, _) = builder.build().unwrap();
2573 let manager = install_pack_files(&dir, "mixed", &pack, &index);
2574 (dir, manager, classified)
2575 }
2576
2577 fn delta_chain_manager() -> (TempDir, PackManager, Vec<ContentHash>) {
2578 const SPEC: PackContainerSpec = PackContainerSpec {
2579 magic: b"LMPK",
2580 version: 4,
2581 };
2582 let dir = TempDir::new().unwrap();
2583 let base = b"delta-chain base payload ".repeat(64);
2584 let mut middle = base.clone();
2585 middle[200..208].copy_from_slice(b"middle!!");
2586 let mut tip = middle.clone();
2587 tip[900..908].copy_from_slice(b"tip!!!!!");
2588 let bodies = [&base, &middle, &tip];
2589 let hashes = bodies
2590 .iter()
2591 .map(|body| ContentHash::compute_typed("blob", body))
2592 .collect::<Vec<_>>();
2593 let middle_delta = DeltaEncoder::encode(&base, &middle);
2594 let tip_delta = DeltaEncoder::encode(&middle, &tip);
2595
2596 let mut pack = Vec::new();
2597 let mut index = PackIndex::new();
2598 write_container_header(&mut pack, SPEC, 3);
2599 for (position, payload) in [
2600 base.as_slice(),
2601 middle_delta.as_slice(),
2602 tip_delta.as_slice(),
2603 ]
2604 .into_iter()
2605 .enumerate()
2606 {
2607 index.add(PackObjectId::Hash(hashes[position]), pack.len() as u64);
2608 let (stored_type, base_id) = if position == 0 {
2609 (ObjectType::Blob, None)
2610 } else {
2611 (
2612 ObjectType::Delta,
2613 Some(PackObjectId::Hash(hashes[position - 1])),
2614 )
2615 };
2616 encode_tagged_entry_parts(
2617 &mut pack,
2618 PackObjectId::Hash(hashes[position]),
2619 stored_type,
2620 bodies[position].len(),
2621 base_id,
2622 payload,
2623 )
2624 .unwrap();
2625 }
2626 index.sort();
2627 append_container_checksum(&mut pack);
2628 let manager = install_pack_files(&dir, "delta-chain", &pack, &index.to_bytes());
2629 (dir, manager, hashes)
2630 }
2631
2632 fn legacy_append_packed_hashes(
2633 hashes: &mut Vec<ContentHash>,
2634 manager: &PackManager,
2635 expected_type: ObjectType,
2636 ) -> Result<TestEnumerationMetrics> {
2637 let mut metrics = TestEnumerationMetrics::default();
2638 for id in manager.list_all_ids()? {
2639 let PackObjectId::Hash(hash) = id else {
2640 continue;
2641 };
2642 let mut already_listed = false;
2643 for listed in hashes.iter() {
2644 metrics.membership_checks += 1;
2645 if listed == &hash {
2646 already_listed = true;
2647 break;
2648 }
2649 }
2650 if already_listed {
2651 continue;
2652 }
2653 metrics.full_object_decodes += 1;
2654 if let Some((obj_type, _)) = manager.get_hashed_object(&hash)?
2655 && obj_type == expected_type
2656 {
2657 hashes.push(hash);
2658 }
2659 }
2660 Ok(metrics)
2661 }
2662
2663 fn assert_new_matches_legacy(
2664 label: &str,
2665 manager: &PackManager,
2666 loose: Vec<ContentHash>,
2667 expected_type: ObjectType,
2668 ) {
2669 let mut new = loose.clone();
2670 let mut new_metrics = TestEnumerationMetrics::default();
2671 append_packed_hashes_with_counter(&mut new, manager, expected_type, &mut new_metrics)
2672 .unwrap();
2673 let mut legacy = loose;
2674 legacy_append_packed_hashes(&mut legacy, manager, expected_type).unwrap();
2675 assert_eq!(new, legacy, "fixture {label} changed output or ordering");
2676 assert_eq!(new_metrics.full_object_decodes, 0, "fixture {label}");
2677 }
2678
2679 #[test]
2680 fn type_only_enumeration_matches_full_decode_across_fixture_set() {
2681 let empty_dir = TempDir::new().unwrap();
2682 let empty = PackManager::new(empty_dir.path().to_path_buf(), empty_dir.path().join("tmp"));
2683 let loose_hash = ContentHash::compute(b"loose only");
2684 assert_new_matches_legacy("loose-only", &empty, vec![loose_hash], ObjectType::Blob);
2685
2686 let (_raw_dir, raw, classified) = raw_mixed_manager();
2687 for expected_type in [ObjectType::Blob, ObjectType::Tree, ObjectType::Action] {
2688 assert_new_matches_legacy("packed-only", &raw, Vec::new(), expected_type);
2689 assert_new_matches_legacy(
2690 "mixed",
2691 &raw,
2692 vec![ContentHash::compute_typed("loose", &[expected_type as u8])],
2693 expected_type,
2694 );
2695 let duplicate = classified
2696 .iter()
2697 .find_map(|(hash, obj_type)| (*obj_type == expected_type).then_some(*hash))
2698 .unwrap();
2699 assert_new_matches_legacy(
2700 "duplicate-loose-packed",
2701 &raw,
2702 vec![duplicate],
2703 expected_type,
2704 );
2705 }
2706 for (hash, expected_type) in classified {
2707 assert_eq!(
2708 raw.get_hashed_object_type(&hash).unwrap(),
2709 raw.get_hashed_object(&hash)
2710 .unwrap()
2711 .map(|(obj_type, _)| obj_type)
2712 );
2713 assert_eq!(
2714 raw.get_hashed_object_type(&hash).unwrap(),
2715 Some(expected_type)
2716 );
2717 }
2718
2719 let (_delta_dir, delta, delta_hashes) = delta_chain_manager();
2720 assert_new_matches_legacy("two-link-delta-chain", &delta, Vec::new(), ObjectType::Blob);
2721 for hash in delta_hashes {
2722 assert_eq!(
2723 delta.get_hashed_object_type(&hash).unwrap(),
2724 Some(ObjectType::Blob)
2725 );
2726 assert_eq!(
2727 delta.get_hashed_object_type(&hash).unwrap(),
2728 delta
2729 .get_hashed_object(&hash)
2730 .unwrap()
2731 .map(|(obj_type, _)| obj_type)
2732 );
2733 }
2734 }
2735
2736 #[test]
2737 fn structural_counter_rejects_vec_scan_and_full_decode_negative_control() {
2738 let (_dir, manager, _) = raw_mixed_manager();
2739 let loose = (0..64u8)
2740 .map(|byte| ContentHash::compute_typed("loose", &[byte]))
2741 .collect::<Vec<_>>();
2742 let packed_hashes = manager
2743 .list_all_ids()
2744 .unwrap()
2745 .into_iter()
2746 .filter(|id| matches!(id, PackObjectId::Hash(_)))
2747 .count() as u64;
2748
2749 for expected_type in [ObjectType::Blob, ObjectType::Tree, ObjectType::Action] {
2750 let mut optimized = loose.clone();
2751 let mut optimized_metrics = TestEnumerationMetrics::default();
2752 append_packed_hashes_with_counter(
2753 &mut optimized,
2754 &manager,
2755 expected_type,
2756 &mut optimized_metrics,
2757 )
2758 .unwrap();
2759 assert_eq!(optimized_metrics.membership_checks, packed_hashes);
2760 assert_eq!(optimized_metrics.header_reads, packed_hashes);
2761 assert_eq!(optimized_metrics.full_object_decodes, 0);
2762
2763 let mut legacy = loose.clone();
2764 let legacy_metrics =
2765 legacy_append_packed_hashes(&mut legacy, &manager, expected_type).unwrap();
2766 assert!(legacy_metrics.membership_checks >= loose.len() as u64 * packed_hashes);
2767 assert_eq!(legacy_metrics.full_object_decodes, packed_hashes);
2768 assert!(
2769 !(legacy_metrics.membership_checks <= packed_hashes
2770 && legacy_metrics.full_object_decodes == 0),
2771 "negative control unexpectedly passed the structural contract: {legacy_metrics:?}"
2772 );
2773 }
2774 }
2775
2776 #[test]
2777 fn state_union_preserves_first_seen_order_at_scale() {
2778 let first = (0..20_000u32)
2779 .map(|value| {
2780 StateId::from_bytes(*ContentHash::compute(&value.to_le_bytes()).as_bytes())
2781 })
2782 .collect::<Vec<_>>();
2783 let second = first[10_000..]
2784 .iter()
2785 .copied()
2786 .chain((20_000..30_000u32).map(|value| {
2787 StateId::from_bytes(*ContentHash::compute(&value.to_le_bytes()).as_bytes())
2788 }))
2789 .collect::<Vec<_>>();
2790 let mut states = Vec::new();
2791 let mut known = HashSet::new();
2792
2793 append_unique_states(&mut states, &mut known, first.iter().copied());
2794 append_unique_states(&mut states, &mut known, second);
2795
2796 assert_eq!(states.len(), 30_000);
2797 assert_eq!(&states[..20_000], first.as_slice());
2798 }
2799}