Skip to main content

wire/
object_availability.rs

1// SPDX-License-Identifier: Apache-2.0
2use objects::store::ObjectStore;
3
4use crate::{ObjectId, ObjectInfo, ObjectType, Result};
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
7pub struct ObjectAvailabilityPlan {
8    pub have_objects: Vec<ObjectId>,
9    pub want_objects: Vec<ObjectId>,
10    pub present_objects: Vec<ObjectId>,
11    pub missing_objects: Vec<ObjectId>,
12    pub resumable_objects: Vec<ObjectId>,
13    pub lazy_objects: Vec<ObjectId>,
14    pub partial_fetch_allowed: bool,
15}
16
17pub fn has_object(store: &impl ObjectStore, info: &ObjectInfo) -> Result<bool> {
18    match (&info.id, info.obj_type) {
19        (ObjectId::Hash(hash), ObjectType::Blob) => Ok(store.has_blob(hash)?),
20        (ObjectId::Hash(hash), ObjectType::Tree) => Ok(store.has_tree(hash)?),
21        (ObjectId::ChangeId(id), ObjectType::State) => Ok(store.has_state(id)?),
22        // Redactions are keyed by the redacted blob's hash. Two senders
23        // can declare different redactions on the same blob (different
24        // reason / signature / timestamp), so we conservatively report
25        // "do not have" and always re-fetch — `accept_wire_redactions`
26        // deduplicates via the content-addressed `put_redaction`
27        // idempotency rule. Cheap to refetch; correct under merge.
28        (ObjectId::Hash(_), ObjectType::Redaction) => Ok(false),
29        // StateVisibility is a per-state sidecar with append/merge
30        // semantics. Like Redaction, conservatively refetch and let the
31        // repository boundary validate + dedupe.
32        (ObjectId::ChangeId(_), ObjectType::StateVisibility) => Ok(false),
33        _ => Ok(false),
34    }
35}
36
37pub fn plan_object_availability(
38    store: &impl ObjectStore,
39    objects: &[ObjectInfo],
40) -> Result<ObjectAvailabilityPlan> {
41    let mut plan = ObjectAvailabilityPlan::default();
42
43    for info in objects {
44        if has_object(store, info)? {
45            plan.have_objects.push(info.id.clone());
46            plan.present_objects.push(info.id.clone());
47        } else {
48            plan.want_objects.push(info.id.clone());
49            plan.missing_objects.push(info.id.clone());
50        }
51    }
52
53    Ok(plan)
54}
55
56impl ObjectAvailabilityPlan {
57    pub fn with_partial_fetch_allowed(mut self, allowed: bool) -> Self {
58        self.partial_fetch_allowed = allowed;
59        self
60    }
61
62    pub fn is_complete(&self) -> bool {
63        self.want_objects.is_empty()
64            && self.missing_objects.is_empty()
65            && self.resumable_objects.is_empty()
66            && self.lazy_objects.is_empty()
67    }
68
69    pub fn has_partial_fetch_candidates(&self) -> bool {
70        !self.resumable_objects.is_empty() || !self.lazy_objects.is_empty()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use objects::{
77        object::{Blob, ChangeId, ContentHash, Tree},
78        store::{ObjectStore, Result as StoreResult},
79    };
80
81    use super::*;
82
83    #[derive(Default)]
84    struct DummyStore {
85        blob: Option<ContentHash>,
86    }
87
88    impl ObjectStore for DummyStore {
89        fn get_blob(&self, _hash: &ContentHash) -> StoreResult<Option<Blob>> {
90            Ok(None)
91        }
92
93        fn put_blob(&self, _blob: &Blob) -> StoreResult<ContentHash> {
94            unreachable!("not used in test")
95        }
96
97        fn has_blob(&self, hash: &ContentHash) -> StoreResult<bool> {
98            Ok(self.blob == Some(*hash))
99        }
100
101        fn get_tree(&self, _hash: &ContentHash) -> StoreResult<Option<Tree>> {
102            Ok(None)
103        }
104
105        fn put_tree(&self, _tree: &Tree) -> StoreResult<ContentHash> {
106            unreachable!("not used in test")
107        }
108
109        fn has_tree(&self, _hash: &ContentHash) -> StoreResult<bool> {
110            Ok(false)
111        }
112
113        fn get_state(&self, _id: &ChangeId) -> StoreResult<Option<objects::object::State>> {
114            Ok(None)
115        }
116
117        fn put_state(&self, _state: &objects::object::State) -> StoreResult<()> {
118            unreachable!("not used in test")
119        }
120
121        fn has_state(&self, _id: &ChangeId) -> StoreResult<bool> {
122            Ok(false)
123        }
124
125        fn list_states(&self) -> StoreResult<Vec<ChangeId>> {
126            Ok(vec![])
127        }
128
129        fn get_action(
130            &self,
131            _id: &objects::object::ActionId,
132        ) -> StoreResult<Option<objects::object::Action>> {
133            Ok(None)
134        }
135
136        fn put_action(
137            &self,
138            _action: &mut objects::object::Action,
139        ) -> StoreResult<objects::object::ActionId> {
140            unreachable!("not used in test")
141        }
142
143        fn list_actions(&self) -> StoreResult<Vec<objects::object::ActionId>> {
144            Ok(vec![])
145        }
146
147        fn list_blobs(&self) -> StoreResult<Vec<ContentHash>> {
148            Ok(vec![])
149        }
150
151        fn list_trees(&self) -> StoreResult<Vec<ContentHash>> {
152            Ok(vec![])
153        }
154    }
155
156    #[test]
157    fn test_plan_tracks_present_and_missing_objects() {
158        let blob = Blob::new(b"hello".to_vec());
159        let blob_hash = blob.hash();
160        let store = DummyStore {
161            blob: Some(blob_hash),
162        };
163        let missing_hash = ContentHash::from_bytes([7; 32]);
164        let objects = vec![
165            ObjectInfo {
166                id: ObjectId::Hash(blob_hash),
167                obj_type: ObjectType::Blob,
168                size: blob.size() as u64,
169                delta_base: None,
170            },
171            ObjectInfo {
172                id: ObjectId::Hash(missing_hash),
173                obj_type: ObjectType::Tree,
174                size: 0,
175                delta_base: None,
176            },
177        ];
178
179        let plan = plan_object_availability(&store, &objects).unwrap();
180
181        assert_eq!(plan.have_objects.len(), 1);
182        assert_eq!(plan.want_objects.len(), 1);
183        assert_eq!(plan.present_objects.len(), 1);
184        assert_eq!(plan.missing_objects.len(), 1);
185        assert!(!plan.is_complete());
186    }
187
188    #[test]
189    fn test_partial_fetch_flag_helpers() {
190        let plan = ObjectAvailabilityPlan::default().with_partial_fetch_allowed(true);
191
192        assert!(plan.partial_fetch_allowed);
193        assert!(!plan.has_partial_fetch_candidates());
194        assert!(plan.is_complete());
195    }
196}