1use std::{
5 fs,
6 path::{Path, PathBuf},
7};
8
9use heddle_format::compression::{header_uncompressed_size, is_compressed};
10use tracing::{debug, instrument, trace};
11
12use super::{
13 FsStore,
14 fs_io::{list_hashes_from_dir, read_file_bytes, read_file_header},
15 fs_paths::{
16 action_path, actions_dir, blobs_dir, hash_path, redaction_path, redactions_dir, state_path,
17 state_visibility_dir, state_visibility_path, states_dir, trees_dir,
18 },
19};
20use crate::{
21 object::{Action, ActionId, Blob, ChangeId, ContentHash, State, Tree},
22 store::{
23 HeddleError, ObjectStore, Result, codec,
24 pack::{ObjectType, PackManager, PackObjectId},
25 },
26};
27
28const BLOB_HEADER_PEEK: usize = 13;
36
37fn validate_loaded_tree(tree: Tree) -> Result<Tree> {
38 tree.validate()?;
39 Ok(tree)
40}
41
42fn validate_blob_bytes(data: &[u8], hash: ContentHash) -> Result<()> {
43 let mut hasher = ContentHash::typed_hasher("blob", data.len() as u64);
44 hasher.update(data);
45 let found = ContentHash::from_bytes(hasher.finalize().into());
46 if found != hash {
47 return Err(HeddleError::Corruption {
48 expected: hash,
49 found,
50 });
51 }
52
53 Ok(())
54}
55
56fn validate_tree_serialized(data: &[u8], hash: ContentHash) -> Result<Tree> {
57 let tree = codec::decode_tree_serialized(data)?;
58 let tree = validate_loaded_tree(tree)?;
59 let found = tree.hash();
60 if found != hash {
61 return Err(HeddleError::Corruption {
62 expected: hash,
63 found,
64 });
65 }
66
67 Ok(tree)
68}
69
70fn validate_loaded_state(requested_id: &ChangeId, state: State) -> Result<State> {
71 if state.change_id != *requested_id {
72 return Err(HeddleError::InvalidObject(format!(
73 "state change_id mismatch: requested {}, found {}",
74 requested_id, state.change_id
75 )));
76 }
77
78 Ok(state)
79}
80
81fn validate_state_serialized(data: &[u8], id: ChangeId) -> Result<State> {
82 let state: State = rmp_serde::from_slice(data)?;
83 validate_loaded_state(&id, state)
84}
85
86fn validate_loaded_action(requested_id: &ActionId, action: Action) -> Result<Action> {
87 let found_id = action.compute_id();
88 if found_id != *requested_id {
89 return Err(HeddleError::InvalidObject(format!(
90 "action id mismatch: requested {}, found {}",
91 requested_id, found_id
92 )));
93 }
94
95 Ok(action)
96}
97
98fn validate_action_serialized(data: &[u8], id: ActionId) -> Result<Action> {
99 let action: Action = rmp_serde::from_slice(data)?;
100 validate_loaded_action(&id, action)
101}
102
103fn validate_and_list_pack(reader: &crate::store::pack::PackReader) -> Result<Vec<PackObjectId>> {
111 let ids = reader.list_ids();
112 for id in &ids {
113 let Some((obj_type, data)) = reader.get_object_bytes(id)? else {
114 continue;
115 };
116 validate_pack_entry(id, obj_type, data.as_ref())?;
117 }
118 Ok(ids)
119}
120
121fn state_entries_from_pack(
122 reader: &crate::store::pack::PackReader,
123 ids: &[PackObjectId],
124) -> Result<Vec<(ChangeId, Vec<u8>)>> {
125 let mut states = Vec::new();
126 for id in ids {
127 let PackObjectId::ChangeId(change_id) = id else {
128 continue;
129 };
130 let Some((obj_type, data)) = reader.get_object(id)? else {
131 continue;
132 };
133 if obj_type != ObjectType::State {
134 return Err(HeddleError::InvalidObject(format!(
135 "pack id {} is indexed as {:?}, expected State",
136 change_id.to_string_full(),
137 obj_type
138 )));
139 }
140 validate_state_serialized(&data, *change_id)?;
141 states.push((*change_id, data));
142 }
143 Ok(states)
144}
145
146fn validate_pack_entry(id: &PackObjectId, obj_type: ObjectType, data: &[u8]) -> Result<()> {
147 match (id, obj_type) {
148 (PackObjectId::Hash(hash), ObjectType::Blob) => validate_blob_bytes(data, *hash),
149 (PackObjectId::Hash(hash), ObjectType::Tree) => {
150 validate_tree_serialized(data, *hash).map(|_| ())
151 }
152 (PackObjectId::Hash(hash), ObjectType::Action) => {
153 validate_action_serialized(data, ActionId::from_hash(*hash)).map(|_| ())
154 }
155 (PackObjectId::ChangeId(change_id), ObjectType::State) => {
156 validate_state_serialized(data, *change_id).map(|_| ())
157 }
158 _ => Err(HeddleError::InvalidObject(format!(
159 "unsupported native pack object: {:?} {:?}",
160 id, obj_type
161 ))),
162 }
163}
164
165impl FsStore {
166 fn cache_recent_blob(&self, hash: ContentHash, blob: &Blob) {
168 if blob.content().len() > super::fs_store::RECENT_BLOB_CACHE_MAX_BYTES {
169 return;
170 }
171 if let Ok(mut cache) = self.recent_blobs.write() {
172 cache.insert(hash, blob.clone());
173 }
174 }
175
176 fn try_get_blob_once(&self, hash: &ContentHash) -> Result<Option<Blob>> {
179 if let Ok(mut cache) = self.recent_blobs.write()
182 && let Some(blob) = cache.get(hash)
183 {
184 trace!("Found blob in recent object cache");
185 return Ok(Some(blob.clone()));
186 }
187
188 if let Ok(manager) = self.pack_manager().read()
189 && let Some((obj_type, data)) = manager.get_hashed_object(hash)?
190 && obj_type == ObjectType::Blob
191 {
192 trace!("Found blob in packfile");
193 let blob = Blob::new(data);
201 self.cache_recent_blob(*hash, &blob);
202 return Ok(Some(blob));
203 }
204
205 let path = hash_path(&blobs_dir(&self.root), hash);
206 match read_file_bytes(&path)? {
207 Some(data) => {
208 trace!(size = data.as_slice().len(), "Blob data read");
209 let content = codec::decode_blob_content(data.as_slice())?;
210 let blob = Blob::new(content);
211 if blob.hash() != *hash {
218 return Err(HeddleError::Corruption {
219 expected: *hash,
220 found: blob.hash(),
221 });
222 }
223 self.cache_recent_blob(*hash, &blob);
224 Ok(Some(blob))
225 }
226 None => Ok(None),
227 }
228 }
229
230 fn loose_or_packed(
235 &self,
236 loose_path: &Path,
237 in_pack: impl FnOnce(&PackManager) -> bool,
238 ) -> Result<bool> {
239 if loose_path.exists() {
240 return Ok(true);
241 }
242 if let Ok(manager) = self.pack_manager().read() {
243 return Ok(in_pack(&manager));
244 }
245 Ok(false)
246 }
247
248 fn try_has_blob_once(&self, hash: &ContentHash) -> Result<bool> {
249 if let Ok(cache) = self.recent_blobs.read()
254 && cache.contains(hash)
255 {
256 return Ok(true);
257 }
258 let path = hash_path(&blobs_dir(&self.root), hash);
259 self.loose_or_packed(&path, |m| m.has_object(hash))
260 }
261
262 fn try_get_blob_size_once(&self, hash: &ContentHash) -> Result<Option<u64>> {
277 if let Ok(mut cache) = self.recent_blobs.write()
278 && let Some(blob) = cache.get(hash)
279 {
280 return Ok(Some(blob.content().len() as u64));
281 }
282
283 let path = hash_path(&blobs_dir(&self.root), hash);
284 if let Some((header, file_len)) = read_file_header(&path, BLOB_HEADER_PEEK)? {
285 if let Some(size) = header_uncompressed_size(&header) {
286 return Ok(Some(size));
287 }
288 return Ok(Some(file_len));
291 }
292
293 if let Ok(manager) = self.pack_manager().read()
294 && let Some(size) = manager.get_hashed_object_size(hash)?
295 {
296 return Ok(Some(size));
297 }
298 Ok(None)
299 }
300
301 fn try_get_tree_once(&self, hash: &ContentHash) -> Result<Option<Tree>> {
302 if let Ok(mut cache) = self.recent_trees.write()
306 && let Some(tree) = cache.get(hash)
307 {
308 trace!("Found tree in recent object cache");
309 return Ok(Some(tree.clone()));
310 }
311
312 let path = hash_path(&trees_dir(&self.root), hash);
316 if path.exists()
317 && let Some(data) = read_file_bytes(&path)?
318 {
319 trace!(size = data.as_slice().len(), "Tree data read");
320 let tree = validate_loaded_tree(codec::decode_tree(data.as_slice())?)?;
321 if tree.hash() != *hash {
322 return Err(HeddleError::Corruption {
323 expected: *hash,
324 found: tree.hash(),
325 });
326 }
327 if let Ok(mut cache) = self.recent_trees.write() {
328 cache.insert(*hash, tree.clone());
329 }
330 return Ok(Some(tree));
331 }
332
333 if let Ok(manager) = self.pack_manager().read()
334 && let Some((obj_type, data)) = manager.get_hashed_object(hash)?
335 && obj_type == ObjectType::Tree
336 {
337 trace!("Found tree in packfile");
338 let tree = validate_loaded_tree(codec::decode_tree_serialized(&data)?)?;
339 if tree.hash() != *hash {
340 return Err(HeddleError::Corruption {
341 expected: *hash,
342 found: tree.hash(),
343 });
344 }
345 if let Ok(mut cache) = self.recent_trees.write() {
346 cache.insert(*hash, tree.clone());
347 }
348 return Ok(Some(tree));
349 }
350 Ok(None)
351 }
352
353 fn try_get_tree_serialized_once(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
354 let path = hash_path(&trees_dir(&self.root), hash);
355 if path.exists()
356 && let Some(data) = read_file_bytes(&path)?
357 {
358 return Ok(Some(codec::decode_tree_body(data.as_slice())?));
359 }
360
361 if let Ok(manager) = self.pack_manager().read()
362 && let Some((obj_type, data)) = manager.get_hashed_object(hash)?
363 && obj_type == ObjectType::Tree
364 {
365 return Ok(Some(data));
366 }
367
368 Ok(None)
369 }
370
371 fn try_has_tree_once(&self, hash: &ContentHash) -> Result<bool> {
372 if let Ok(cache) = self.recent_trees.read()
375 && cache.contains(hash)
376 {
377 return Ok(true);
378 }
379 let path = hash_path(&trees_dir(&self.root), hash);
380 self.loose_or_packed(&path, |m| m.has_object(hash))
381 }
382
383 fn try_get_state_once(&self, id: &ChangeId) -> Result<Option<State>> {
384 if let Ok(mut cache) = self.recent_states.write()
388 && let Some(state) = cache.get(id)
389 {
390 trace!("Found state in recent object cache");
391 return Ok(Some(state.clone()));
392 }
393
394 let path = state_path(&self.root, id);
404 if let Some(data) = read_file_bytes(&path)? {
405 trace!(
406 size = data.as_slice().len(),
407 "State read from loose object (shadows any packed copy)"
408 );
409 let state = validate_loaded_state(id, codec::decode_state(data.as_slice())?)?;
410 if let Ok(mut cache) = self.recent_states.write() {
411 cache.insert(*id, state.clone());
412 }
413 return Ok(Some(state));
414 }
415
416 if let Ok(manager) = self.pack_manager().read()
417 && let Some((obj_type, data)) = manager.get_object(&PackObjectId::ChangeId(*id))?
418 && obj_type == ObjectType::State
419 {
420 trace!("Found state in packfile");
421 let state = validate_loaded_state(id, rmp_serde::from_slice(&data)?)?;
422 if let Ok(mut cache) = self.recent_states.write() {
423 cache.insert(*id, state.clone());
424 }
425 return Ok(Some(state));
426 }
427
428 Ok(None)
429 }
430
431 fn try_has_state_once(&self, id: &ChangeId) -> Result<bool> {
432 if let Ok(cache) = self.recent_states.read()
435 && cache.contains(id)
436 {
437 return Ok(true);
438 }
439 let path = state_path(&self.root, id);
440 self.loose_or_packed(&path, |m| m.has_object_id(&PackObjectId::ChangeId(*id)))
441 }
442}
443
444impl ObjectStore for FsStore {
445 fn clear_recent_caches(&self) {
446 self.clear_recent_object_caches();
447 }
448
449 fn get_blob_bytes(&self, hash: &ContentHash) -> Result<Option<bytes::Bytes>> {
456 if let Ok(manager) = self.pack_manager().read()
457 && let Some((obj_type, data)) = manager.get_hashed_object_bytes(hash)?
458 && obj_type == crate::store::pack::ObjectType::Blob
459 {
460 return Ok(Some(data));
461 }
462 Ok(self
463 .get_blob(hash)?
464 .map(|blob| bytes::Bytes::from(blob.into_content())))
465 }
466
467 #[instrument(skip(self), fields(hash = %hash.short()))]
468 fn get_blob(&self, hash: &ContentHash) -> Result<Option<Blob>> {
469 if let Some(blob) = self.try_get_blob_once(hash)? {
470 return Ok(Some(blob));
471 }
472 if self.reload_packs_if_stale()?
477 && let Some(blob) = self.try_get_blob_once(hash)?
478 {
479 return Ok(Some(blob));
480 }
481 trace!("Blob not found");
482 Ok(None)
483 }
484
485 #[instrument(skip(self, blob), fields(size = blob.content().len()))]
486 fn put_blob(&self, blob: &Blob) -> Result<ContentHash> {
487 let hash = blob.hash();
488 let path = hash_path(&blobs_dir(&self.root), &hash);
489
490 if !path.exists() {
491 let data = codec::encode_blob_content(blob.content(), &self.compression)?;
492 trace!(compressed_size = data.len(), "Writing blob");
493 self.write_loose_object_atomic(&path, &data)?;
494 } else {
495 trace!("Blob already exists, skipping write");
496 }
497 self.cache_recent_blob(hash, blob);
498
499 Ok(hash)
500 }
501
502 #[instrument(skip(self, blob), fields(hash = %hash.short()))]
503 fn put_blob_with_hash(&self, blob: &Blob, hash: ContentHash) -> Result<ContentHash> {
504 if blob.hash() != hash {
505 return Err(HeddleError::Corruption {
506 expected: hash,
507 found: blob.hash(),
508 });
509 }
510
511 let path = hash_path(&blobs_dir(&self.root), &hash);
512
513 if !path.exists() {
514 let data = codec::encode_blob_content(blob.content(), &self.compression)?;
515 trace!(
516 compressed_size = data.len(),
517 "Writing blob with precomputed hash"
518 );
519 self.write_loose_object_atomic(&path, &data)?;
520 }
521 self.cache_recent_blob(hash, blob);
522
523 Ok(hash)
524 }
525
526 #[instrument(skip(self, data), fields(hash = %hash.short(), size = data.len()))]
527 fn put_blob_bytes_with_hash(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
528 validate_blob_bytes(data, hash)?;
529
530 let path = hash_path(&blobs_dir(&self.root), &hash);
531 if !path.exists() {
532 trace!(
533 size = data.len(),
534 "Writing raw blob bytes with precomputed hash"
535 );
536 self.write_loose_object_atomic(&path, data)?;
537 }
538 self.cache_recent_blob(hash, &Blob::from_slice(data));
539
540 Ok(hash)
541 }
542
543 #[instrument(skip(self), fields(hash = %hash.short()))]
544 fn has_blob(&self, hash: &ContentHash) -> Result<bool> {
545 if self.try_has_blob_once(hash)? {
546 return Ok(true);
547 }
548 if self.reload_packs_if_stale()? {
549 return self.try_has_blob_once(hash);
550 }
551 Ok(false)
552 }
553
554 fn loose_blob_path(&self, hash: &ContentHash) -> Option<PathBuf> {
571 let path = hash_path(&blobs_dir(&self.root), hash);
572 if let Ok(verified) = self.verified_loose_blobs.read()
577 && verified.contains(hash)
578 && path.exists()
579 {
580 return Some(path);
581 }
582
583 let (header, _) = read_file_header(&path, BLOB_HEADER_PEEK).ok().flatten()?;
596 if is_compressed(&header) {
597 return None;
598 }
599 let bytes = read_file_bytes(&path).ok().flatten()?;
600 let actual = ContentHash::compute_typed("blob", bytes.as_slice());
601 if actual != *hash {
602 return None;
606 }
607 if let Ok(mut verified) = self.verified_loose_blobs.write() {
608 verified.insert(*hash, ());
609 }
610 Some(path)
611 }
612
613 #[instrument(skip(self), fields(hash = %hash.short()))]
626 fn promote_to_loose_uncompressed(&self, hash: &ContentHash) -> Result<bool> {
627 let path = hash_path(&blobs_dir(&self.root), hash);
628
629 if let Some((header, _)) = read_file_header(&path, 9)?
631 && !is_compressed(&header)
632 {
633 trace!("Blob already loose+uncompressed; skipping promotion");
634 return Ok(false);
635 }
636
637 let blob = self.get_blob(hash)?.ok_or_else(|| {
641 HeddleError::NotFound(format!(
642 "blob {} not found in store; cannot promote to loose-uncompressed",
643 hash
644 ))
645 })?;
646
647 debug!(
660 size = blob.content().len(),
661 "Promoting blob to loose-uncompressed canonical store"
662 );
663 self.write_loose_object_cache(&path, blob.content())?;
664 if let Ok(mut verified) = self.verified_loose_blobs.write() {
665 verified.insert(*hash, ());
666 }
667 Ok(true)
668 }
669
670 #[instrument(skip(self), fields(hash = %hash.short()))]
671 fn blob_size(&self, hash: &ContentHash) -> Result<Option<u64>> {
672 if let Some(size) = self.try_get_blob_size_once(hash)? {
673 return Ok(Some(size));
674 }
675 if self.reload_packs_if_stale()?
679 && let Some(size) = self.try_get_blob_size_once(hash)?
680 {
681 return Ok(Some(size));
682 }
683 Ok(None)
684 }
685
686 #[instrument(skip(self), fields(hash = %hash.short()))]
687 fn get_tree(&self, hash: &ContentHash) -> Result<Option<Tree>> {
688 if let Some(tree) = self.try_get_tree_once(hash)? {
689 return Ok(Some(tree));
690 }
691 if self.reload_packs_if_stale()?
692 && let Some(tree) = self.try_get_tree_once(hash)?
693 {
694 return Ok(Some(tree));
695 }
696 trace!("Tree not found");
697 Ok(None)
698 }
699
700 #[instrument(skip(self), fields(hash = %hash.short()))]
701 fn get_tree_serialized(&self, hash: &ContentHash) -> Result<Option<Vec<u8>>> {
702 if let Some(data) = self.try_get_tree_serialized_once(hash)? {
703 return Ok(Some(data));
704 }
705 if self.reload_packs_if_stale()?
706 && let Some(data) = self.try_get_tree_serialized_once(hash)?
707 {
708 return Ok(Some(data));
709 }
710 Ok(None)
711 }
712
713 #[instrument(skip(self, tree), fields(entry_count = tree.entries().len()))]
714 fn put_tree(&self, tree: &Tree) -> Result<ContentHash> {
715 let hash = tree.hash();
716 let path = hash_path(&trees_dir(&self.root), &hash);
717
718 if !path.exists() {
719 let (_, data) = codec::encode_tree(tree, &self.compression)?;
720 trace!(compressed_size = data.len(), "Writing tree");
721 self.write_loose_object_atomic(&path, &data)?;
722 } else {
723 trace!("Tree already exists, skipping write");
724 }
725 if let Ok(mut cache) = self.recent_trees.write() {
726 cache.insert(hash, tree.clone());
727 }
728
729 Ok(hash)
730 }
731
732 #[instrument(skip(self, data), fields(hash = %hash.short(), size = data.len()))]
733 fn put_tree_serialized(&self, data: &[u8], hash: ContentHash) -> Result<ContentHash> {
734 let tree = validate_tree_serialized(data, hash)?;
735
736 let path = hash_path(&trees_dir(&self.root), &hash);
737 let should_write = match read_file_bytes(&path)? {
738 Some(existing) => codec::decode_tree_body(existing.as_slice())? != data,
739 None => true,
740 };
741 if should_write {
742 trace!(size = data.len(), "Writing raw serialized tree");
743 self.write_loose_object_atomic(&path, data)?;
744 }
745 if let Ok(mut cache) = self.recent_trees.write() {
746 cache.insert(hash, tree);
747 }
748
749 Ok(hash)
750 }
751
752 #[instrument(skip(self), fields(hash = %hash.short()))]
753 fn has_tree(&self, hash: &ContentHash) -> Result<bool> {
754 if self.try_has_tree_once(hash)? {
755 return Ok(true);
756 }
757 if self.reload_packs_if_stale()? {
758 return self.try_has_tree_once(hash);
759 }
760 Ok(false)
761 }
762
763 #[instrument(skip(self), fields(id = %id.short()))]
764 fn get_state(&self, id: &ChangeId) -> Result<Option<State>> {
765 if let Some(state) = self.try_get_state_once(id)? {
766 return Ok(Some(state));
767 }
768 if self.reload_packs_if_stale()?
769 && let Some(state) = self.try_get_state_once(id)?
770 {
771 return Ok(Some(state));
772 }
773 trace!("State not found");
774 Ok(None)
775 }
776
777 #[instrument(skip(self, state), fields(id = %state.change_id.short()))]
778 fn put_state(&self, state: &State) -> Result<()> {
779 let path = state_path(&self.root, &state.change_id);
780 let data = codec::encode_state(state, &self.compression)?;
781 trace!(compressed_size = data.len(), "Writing state");
782 self.write_loose_object_atomic(&path, &data)?;
783 if let Ok(mut cache) = self.recent_states.write() {
784 cache.insert(state.change_id, state.clone());
785 }
786 Ok(())
787 }
788
789 #[instrument(skip(self, data), fields(id = %id.short(), size = data.len()))]
790 fn put_state_serialized(&self, data: &[u8], id: ChangeId) -> Result<()> {
791 let state = validate_state_serialized(data, id)?;
792 let path = state_path(&self.root, &id);
793 trace!(size = data.len(), "Writing raw serialized state");
794 self.write_loose_object_atomic(&path, data)?;
795 if let Ok(mut cache) = self.recent_states.write() {
796 cache.insert(id, state);
797 }
798 Ok(())
799 }
800
801 #[instrument(skip(self), fields(id = %id.short()))]
802 fn has_state(&self, id: &ChangeId) -> Result<bool> {
803 if self.try_has_state_once(id)? {
804 return Ok(true);
805 }
806 if self.reload_packs_if_stale()? {
807 return self.try_has_state_once(id);
808 }
809 Ok(false)
810 }
811
812 #[instrument(skip(self))]
813 fn list_states(&self) -> Result<Vec<ChangeId>> {
814 self.reload_packs_if_stale()?;
815
816 let dir = states_dir(&self.root);
817 if !dir.exists() {
818 return Ok(Vec::new());
819 }
820
821 let mut states = Vec::new();
822 for entry in fs::read_dir(&dir)? {
823 let entry = entry?;
824 let path = entry.path();
825 if let Some(name) = path.file_stem()
826 && let Some(name_str) = name.to_str()
827 && let Ok(id) = ChangeId::parse(name_str)
828 {
829 states.push(id);
830 }
831 }
832 if let Ok(manager) = self.pack_manager().read() {
833 for id in manager.list_all_ids()? {
834 if let PackObjectId::ChangeId(change_id) = id
835 && !states.contains(&change_id)
836 {
837 states.push(change_id);
838 }
839 }
840 }
841 debug!(count = states.len(), "Listed states");
842 Ok(states)
843 }
844
845 #[instrument(skip(self), fields(id = %id))]
846 fn get_action(&self, id: &ActionId) -> Result<Option<Action>> {
847 let path = action_path(&self.root, id);
848 if !path.exists()
849 && let Ok(manager) = self.pack_manager().read()
850 && let Some((obj_type, data)) = manager.get_hashed_object(id.as_hash())?
851 && obj_type == ObjectType::Action
852 {
853 trace!("Found action in packfile");
854 let action = validate_loaded_action(id, rmp_serde::from_slice(&data)?)?;
855 return Ok(Some(action));
856 }
857 match read_file_bytes(&path)? {
858 Some(data) => {
859 trace!(size = data.as_slice().len(), "Action data read");
860 let action = validate_loaded_action(id, codec::decode_action(data.as_slice())?)?;
861 Ok(Some(action))
862 }
863 None => {
864 trace!("Action not found");
865 Ok(None)
866 }
867 }
868 }
869
870 #[instrument(skip(self, action))]
871 fn put_action(&self, action: &mut Action) -> Result<ActionId> {
872 let id = action.id();
873 let path = action_path(&self.root, &id);
874
875 if !path.exists() {
876 let (_, data) = codec::encode_action(action, &self.compression)?;
877 trace!(id = %id, compressed_size = data.len(), "Writing action");
878 self.write_loose_object_atomic(&path, &data)?;
879 }
880
881 Ok(id)
882 }
883
884 #[instrument(skip(self))]
885 fn list_actions(&self) -> Result<Vec<ActionId>> {
886 let dir = actions_dir(&self.root);
887 let mut actions = Vec::new();
888 if dir.exists() {
889 for entry in fs::read_dir(&dir)? {
890 let entry = entry?;
891 let path = entry.path();
892 if let Some(name) = path.file_stem()
893 && let Some(name_str) = name.to_str()
894 && let Ok(hash) = ContentHash::from_hex(name_str)
895 {
896 actions.push(ActionId::from_hash(hash));
897 }
898 }
899 }
900 if let Ok(manager) = self.pack_manager().read() {
901 for id in manager.list_all_ids()? {
902 if let PackObjectId::Hash(hash) = id
903 && !actions.iter().any(|action_id| action_id.as_hash() == &hash)
904 && let Some((obj_type, _)) = manager.get_hashed_object(&hash)?
905 && obj_type == ObjectType::Action
906 {
907 actions.push(ActionId::from_hash(hash));
908 }
909 }
910 }
911 debug!(count = actions.len(), "Listed actions");
912 Ok(actions)
913 }
914
915 #[instrument(skip(self))]
916 fn list_blobs(&self) -> Result<Vec<ContentHash>> {
917 let dir = blobs_dir(&self.root);
918 let mut blobs = list_hashes_from_dir(&dir)?;
919 if let Ok(manager) = self.pack_manager().read() {
920 for id in manager.list_all_ids()? {
921 if let PackObjectId::Hash(hash) = id
922 && !blobs.contains(&hash)
923 && let Some((obj_type, _)) = manager.get_hashed_object(&hash)?
924 && obj_type == ObjectType::Blob
925 {
926 blobs.push(hash);
927 }
928 }
929 }
930 Ok(blobs)
931 }
932
933 #[instrument(skip(self))]
934 fn list_trees(&self) -> Result<Vec<ContentHash>> {
935 let dir = trees_dir(&self.root);
936 let mut trees = list_hashes_from_dir(&dir)?;
937 if let Ok(manager) = self.pack_manager().read() {
938 for id in manager.list_all_ids()? {
939 if let PackObjectId::Hash(hash) = id
940 && !trees.contains(&hash)
941 && let Some((obj_type, _)) = manager.get_hashed_object(&hash)?
942 && obj_type == ObjectType::Tree
943 {
944 trees.push(hash);
945 }
946 }
947 }
948 Ok(trees)
949 }
950
951 #[instrument(skip(self))]
952 fn pack_objects(&self, aggressive: bool) -> Result<(u64, u64)> {
953 self.pack_objects_impl(aggressive)
954 }
955
956 #[instrument(skip(self), fields(id = ?id))]
957 fn get_pack_object(&self, id: &PackObjectId) -> Result<Option<(ObjectType, Vec<u8>)>> {
958 if let Ok(manager) = self.pack_manager().read()
959 && let Some((obj_type, data)) = manager.get_object(id)?
960 {
961 return Ok(Some((obj_type, data)));
962 }
963
964 match id {
965 PackObjectId::Hash(hash) => {
966 if let Some(blob) = self.get_blob(hash)? {
967 return Ok(Some((ObjectType::Blob, blob.content().to_vec())));
968 }
969 if let Some(tree) = self.get_tree(hash)? {
970 return Ok(Some((ObjectType::Tree, rmp_serde::to_vec_named(&tree)?)));
971 }
972 if let Some(action) = self.get_action(&ActionId::from_hash(*hash))? {
973 return Ok(Some((
974 ObjectType::Action,
975 rmp_serde::to_vec_named(&action)?,
976 )));
977 }
978 Ok(None)
979 }
980 PackObjectId::ChangeId(change_id) => {
981 if let Some(state) = self.get_state(change_id)? {
982 Ok(Some((ObjectType::State, rmp_serde::to_vec_named(&state)?)))
983 } else {
984 Ok(None)
985 }
986 }
987 }
988 }
989
990 #[instrument(skip(self, pack_data, index_data))]
991 fn install_pack(&self, pack_data: &[u8], index_data: &[u8]) -> Result<Vec<PackObjectId>> {
992 let reader = crate::store::pack::PackReader::from_slice(pack_data, index_data)?;
993 let ids = validate_and_list_pack(&reader)?;
994 let state_entries = state_entries_from_pack(&reader, &ids)?;
995 self.install_pack_files(pack_data, index_data)?;
996 for (id, data) in state_entries {
997 self.put_state_serialized(&data, id)?;
998 }
999 self.clear_recent_object_caches();
1000 Ok(ids)
1001 }
1002
1003 #[instrument(skip(self, blobs), fields(count = blobs.len()))]
1004 fn put_blobs_packed(&self, blobs: Vec<(crate::object::ContentHash, Vec<u8>)>) -> Result<()> {
1005 self.put_blobs_packed_impl(blobs)
1006 }
1007
1008 #[instrument(skip(self))]
1009 fn install_pack_streaming(
1010 &self,
1011 pack_path: &std::path::Path,
1012 index_path: &std::path::Path,
1013 ) -> Result<Vec<PackObjectId>> {
1014 let ids = {
1020 let reader = crate::store::pack::PackReader::open(pack_path, index_path)?;
1021 validate_and_list_pack(&reader)?
1022 };
1023 let state_entries = {
1024 let reader = crate::store::pack::PackReader::open(pack_path, index_path)?;
1025 state_entries_from_pack(&reader, &ids)?
1026 };
1027 self.install_pack_files_streaming(pack_path, index_path)?;
1028 for (id, data) in state_entries {
1029 self.put_state_serialized(&data, id)?;
1030 }
1031 Ok(ids)
1032 }
1033
1034 #[instrument(skip(self))]
1035 fn prune_loose_objects(&self) -> Result<(u64, u64)> {
1036 self.prune_loose_objects_impl()
1037 }
1038
1039 #[instrument(skip(self))]
1040 fn begin_snapshot_write_batch(&self) -> Result<()> {
1041 self.begin_snapshot_write_batch_impl()
1042 }
1043
1044 #[instrument(skip(self))]
1045 fn flush_snapshot_write_batch(&self) -> Result<()> {
1046 self.flush_snapshot_write_batch_impl()
1047 }
1048
1049 #[instrument(skip(self))]
1050 fn abort_snapshot_write_batch(&self) {
1051 self.abort_snapshot_write_batch_impl();
1052 }
1053
1054 fn has_redactions_for_blob(&self, blob: &ContentHash) -> Result<bool> {
1055 Ok(redaction_path(&self.root, blob).exists())
1056 }
1057
1058 fn get_redactions_bytes_for_blob(&self, blob: &ContentHash) -> Result<Option<Vec<u8>>> {
1059 let path = redaction_path(&self.root, blob);
1060 match fs::read(&path) {
1061 Ok(bytes) => Ok(Some(bytes)),
1062 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
1063 Err(err) => Err(HeddleError::Io(err)),
1064 }
1065 }
1066
1067 fn put_redactions_bytes_for_blob(&self, blob: &ContentHash, bytes: &[u8]) -> Result<()> {
1068 let dir = redactions_dir(&self.root);
1069 if !dir.exists() {
1070 crate::fs_atomic::create_dir_all_durable(&dir)?;
1071 }
1072 let path = redaction_path(&self.root, blob);
1073 crate::fs_atomic::write_file_atomic(&path, bytes)?;
1074 Ok(())
1075 }
1076
1077 fn list_blobs_with_redactions(&self) -> Result<Vec<ContentHash>> {
1078 let dir = redactions_dir(&self.root);
1079 if !dir.exists() {
1080 return Ok(Vec::new());
1081 }
1082 let mut out = Vec::new();
1083 for entry in fs::read_dir(&dir)? {
1084 let entry = entry?;
1085 let path = entry.path();
1086 if path.extension().and_then(|e| e.to_str()) != Some("bin") {
1087 continue;
1088 }
1089 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
1090 continue;
1091 };
1092 if let Ok(hash) = ContentHash::from_hex(stem) {
1093 out.push(hash);
1094 }
1095 }
1096 Ok(out)
1097 }
1098
1099 fn has_state_visibility_for_state(&self, state: &ChangeId) -> Result<bool> {
1100 Ok(state_visibility_path(&self.root, state).exists())
1101 }
1102
1103 fn get_state_visibility_bytes_for_state(&self, state: &ChangeId) -> Result<Option<Vec<u8>>> {
1104 let path = state_visibility_path(&self.root, state);
1105 match fs::read(&path) {
1106 Ok(bytes) => Ok(Some(bytes)),
1107 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
1108 Err(err) => Err(HeddleError::Io(err)),
1109 }
1110 }
1111
1112 fn put_state_visibility_bytes_for_state(&self, state: &ChangeId, bytes: &[u8]) -> Result<()> {
1113 let dir = state_visibility_dir(&self.root);
1114 if !dir.exists() {
1115 crate::fs_atomic::create_dir_all_durable(&dir)?;
1116 }
1117 let path = state_visibility_path(&self.root, state);
1118 crate::fs_atomic::write_file_atomic(&path, bytes)?;
1119 Ok(())
1120 }
1121
1122 fn list_states_with_visibility(&self) -> Result<Vec<ChangeId>> {
1123 let dir = state_visibility_dir(&self.root);
1124 if !dir.exists() {
1125 return Ok(Vec::new());
1126 }
1127 let mut out = Vec::new();
1128 for entry in fs::read_dir(&dir)? {
1129 let entry = entry?;
1130 let path = entry.path();
1131 if path.extension().and_then(|e| e.to_str()) != Some("bin") {
1132 continue;
1133 }
1134 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
1135 continue;
1136 };
1137 if let Ok(state) = ChangeId::parse(stem) {
1138 out.push(state);
1139 }
1140 }
1141 Ok(out)
1142 }
1143}