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 partial_fetch_allowed: bool,
11}
12
13pub fn has_object(store: &impl ObjectStore, info: &ObjectInfo) -> Result<bool> {
14    match (&info.id, info.obj_type) {
15        (ObjectId::Hash(hash), ObjectType::Blob) => Ok(store.has_blob(hash)?),
16        (ObjectId::Hash(hash), ObjectType::Tree) => Ok(store.has_tree(hash)?),
17        (ObjectId::StateId(state_id), ObjectType::State) => Ok(store.has_state(state_id)?),
18        (ObjectId::StateAttachment { state, id, kind: _ }, ObjectType::StateAttachment) => {
19            Ok(store.get_state_attachment(state, id)?.is_some())
20        }
21        // Redactions are keyed by the redacted blob's hash. Two senders
22        // can declare different redactions on the same blob (different
23        // reason / signature / timestamp), so we conservatively report
24        // "do not have" and always re-fetch — `accept_wire_redactions`
25        // deduplicates via the content-addressed `put_redaction`
26        // idempotency rule. Cheap to refetch; correct under merge.
27        (ObjectId::Hash(_), ObjectType::Redaction) => Ok(false),
28        // StateVisibility is a per-state sidecar with append/merge
29        // semantics. Like Redaction, conservatively refetch and let the
30        // repository boundary validate + dedupe.
31        (ObjectId::StateId(_), ObjectType::StateVisibility) => Ok(false),
32        // Hosted registries are materialized snapshots with a liveness overlay;
33        // let the receiver validate and deduplicate the complete payload.
34        (ObjectId::Hash(_), ObjectType::KeyBinding) => Ok(false),
35        _ => Ok(false),
36    }
37}
38
39pub fn plan_object_availability(
40    store: &impl ObjectStore,
41    objects: &[ObjectInfo],
42) -> Result<ObjectAvailabilityPlan> {
43    let mut plan = ObjectAvailabilityPlan::default();
44
45    for info in objects {
46        if has_object(store, info)? {
47            plan.have_objects.push(info.id.clone());
48        } else {
49            plan.want_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    }
65}
66
67#[cfg(test)]
68mod tests {
69    use objects::{
70        object::{Blob, ContentHash, StateId, Tree},
71        store::{ObjectStore, Result as StoreResult},
72    };
73
74    use super::*;
75
76    #[derive(Default)]
77    struct DummyStore {
78        blob: Option<ContentHash>,
79        state: Option<StateId>,
80    }
81
82    impl ObjectStore for DummyStore {
83        fn get_blob(&self, _hash: &ContentHash) -> StoreResult<Option<Blob>> {
84            Ok(None)
85        }
86
87        fn put_blob(&self, _blob: &Blob) -> StoreResult<ContentHash> {
88            unreachable!("not used in test")
89        }
90
91        fn has_blob(&self, hash: &ContentHash) -> StoreResult<bool> {
92            Ok(self.blob == Some(*hash))
93        }
94
95        fn get_tree(&self, _hash: &ContentHash) -> StoreResult<Option<Tree>> {
96            Ok(None)
97        }
98
99        fn put_tree(&self, _tree: &Tree) -> StoreResult<ContentHash> {
100            unreachable!("not used in test")
101        }
102
103        fn has_tree(&self, _hash: &ContentHash) -> StoreResult<bool> {
104            Ok(false)
105        }
106
107        fn get_state(&self, _id: &StateId) -> StoreResult<Option<objects::object::State>> {
108            Ok(None)
109        }
110
111        fn put_state(&self, _state: &objects::object::State) -> StoreResult<()> {
112            unreachable!("not used in test")
113        }
114
115        fn has_state(&self, id: &StateId) -> StoreResult<bool> {
116            Ok(self.state == Some(*id))
117        }
118
119        fn list_states(&self) -> StoreResult<Vec<StateId>> {
120            Ok(vec![])
121        }
122
123        fn get_action(
124            &self,
125            _id: &objects::object::ActionId,
126        ) -> StoreResult<Option<objects::object::Action>> {
127            Ok(None)
128        }
129
130        fn put_action(
131            &self,
132            _action: &mut objects::object::Action,
133        ) -> StoreResult<objects::object::ActionId> {
134            unreachable!("not used in test")
135        }
136
137        fn list_actions(&self) -> StoreResult<Vec<objects::object::ActionId>> {
138            Ok(vec![])
139        }
140
141        fn list_blobs(&self) -> StoreResult<Vec<ContentHash>> {
142            Ok(vec![])
143        }
144
145        fn list_trees(&self) -> StoreResult<Vec<ContentHash>> {
146            Ok(vec![])
147        }
148    }
149
150    #[test]
151    fn test_plan_tracks_available_and_wanted_objects() {
152        let blob = Blob::new(b"hello".to_vec());
153        let blob_hash = blob.hash();
154        let store = DummyStore {
155            blob: Some(blob_hash),
156            state: None,
157        };
158        let missing_hash = ContentHash::from_bytes([7; 32]);
159        let objects = vec![
160            ObjectInfo {
161                id: ObjectId::Hash(blob_hash),
162                obj_type: ObjectType::Blob,
163                size: blob.size() as u64,
164                delta_base: None,
165            },
166            ObjectInfo {
167                id: ObjectId::Hash(missing_hash),
168                obj_type: ObjectType::Tree,
169                size: 0,
170                delta_base: None,
171            },
172        ];
173
174        let plan = plan_object_availability(&store, &objects).unwrap();
175
176        assert_eq!(plan.have_objects.len(), 1);
177        assert_eq!(plan.want_objects.len(), 1);
178        assert!(!plan.is_complete());
179    }
180
181    #[test]
182    fn missing_state_objects_are_requested() {
183        let store = DummyStore::default();
184        let state = StateId::from_bytes([9; 32]);
185        let objects = vec![ObjectInfo {
186            id: ObjectId::StateId(state),
187            obj_type: ObjectType::State,
188            size: 0,
189            delta_base: None,
190        }];
191
192        let plan = plan_object_availability(&store, &objects).unwrap();
193
194        assert!(plan.have_objects.is_empty());
195        assert_eq!(plan.want_objects, vec![ObjectId::StateId(state)]);
196    }
197
198    #[test]
199    fn immutable_state_objects_are_not_requested_when_present() {
200        let state = StateId::from_bytes([9; 32]);
201        let store = DummyStore {
202            state: Some(state),
203            ..DummyStore::default()
204        };
205        let objects = vec![ObjectInfo {
206            id: ObjectId::StateId(state),
207            obj_type: ObjectType::State,
208            size: 0,
209            delta_base: None,
210        }];
211
212        let plan = plan_object_availability(&store, &objects).unwrap();
213
214        assert_eq!(plan.have_objects, vec![ObjectId::StateId(state)]);
215        assert!(plan.want_objects.is_empty());
216    }
217
218    #[test]
219    fn test_partial_fetch_flag_helpers() {
220        let plan = ObjectAvailabilityPlan::default().with_partial_fetch_allowed(true);
221
222        assert!(plan.partial_fetch_allowed);
223        assert!(plan.is_complete());
224    }
225}