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        // Purge carries separately authorized destructive evidence and must
29        // always reach the repository verification boundary.
30        (ObjectId::Hash(_), ObjectType::Purge) => Ok(false),
31        // StateVisibility is a per-state sidecar with append/merge
32        // semantics. Like Redaction, conservatively refetch and let the
33        // repository boundary validate + dedupe.
34        (ObjectId::StateId(_), ObjectType::StateVisibility) => Ok(false),
35        // Hosted registries are materialized snapshots with a liveness overlay;
36        // let the receiver validate and deduplicate the complete payload.
37        (ObjectId::Hash(_), ObjectType::KeyBinding) => Ok(false),
38        _ => Ok(false),
39    }
40}
41
42pub fn plan_object_availability(
43    store: &impl ObjectStore,
44    objects: &[ObjectInfo],
45) -> Result<ObjectAvailabilityPlan> {
46    let mut plan = ObjectAvailabilityPlan::default();
47
48    for info in objects {
49        if has_object(store, info)? {
50            plan.have_objects.push(info.id.clone());
51        } else {
52            plan.want_objects.push(info.id.clone());
53        }
54    }
55
56    Ok(plan)
57}
58
59impl ObjectAvailabilityPlan {
60    pub fn with_partial_fetch_allowed(mut self, allowed: bool) -> Self {
61        self.partial_fetch_allowed = allowed;
62        self
63    }
64
65    pub fn is_complete(&self) -> bool {
66        self.want_objects.is_empty()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use objects::{
73        object::{Blob, ContentHash, StateId, Tree},
74        store::{ObjectStore, Result as StoreResult},
75    };
76
77    use super::*;
78
79    #[derive(Default)]
80    struct DummyStore {
81        blob: Option<ContentHash>,
82        state: Option<StateId>,
83    }
84
85    impl ObjectStore for DummyStore {
86        fn get_blob(&self, _hash: &ContentHash) -> StoreResult<Option<Blob>> {
87            Ok(None)
88        }
89
90        fn put_blob(&self, _blob: &Blob) -> StoreResult<ContentHash> {
91            unreachable!("not used in test")
92        }
93
94        fn has_blob(&self, hash: &ContentHash) -> StoreResult<bool> {
95            Ok(self.blob == Some(*hash))
96        }
97
98        fn get_tree(&self, _hash: &ContentHash) -> StoreResult<Option<Tree>> {
99            Ok(None)
100        }
101
102        fn put_tree(&self, _tree: &Tree) -> StoreResult<ContentHash> {
103            unreachable!("not used in test")
104        }
105
106        fn has_tree(&self, _hash: &ContentHash) -> StoreResult<bool> {
107            Ok(false)
108        }
109
110        fn get_state(&self, _id: &StateId) -> StoreResult<Option<objects::object::State>> {
111            Ok(None)
112        }
113
114        fn put_state(&self, _state: &objects::object::State) -> StoreResult<()> {
115            unreachable!("not used in test")
116        }
117
118        fn has_state(&self, id: &StateId) -> StoreResult<bool> {
119            Ok(self.state == Some(*id))
120        }
121
122        fn list_states(&self) -> StoreResult<Vec<StateId>> {
123            Ok(vec![])
124        }
125
126        fn get_action(
127            &self,
128            _id: &objects::object::ActionId,
129        ) -> StoreResult<Option<objects::object::Action>> {
130            Ok(None)
131        }
132
133        fn put_action(
134            &self,
135            _action: &mut objects::object::Action,
136        ) -> StoreResult<objects::object::ActionId> {
137            unreachable!("not used in test")
138        }
139
140        fn list_actions(&self) -> StoreResult<Vec<objects::object::ActionId>> {
141            Ok(vec![])
142        }
143
144        fn list_blobs(&self) -> StoreResult<Vec<ContentHash>> {
145            Ok(vec![])
146        }
147
148        fn list_trees(&self) -> StoreResult<Vec<ContentHash>> {
149            Ok(vec![])
150        }
151    }
152
153    #[test]
154    fn test_plan_tracks_available_and_wanted_objects() {
155        let blob = Blob::new(b"hello".to_vec());
156        let blob_hash = blob.hash();
157        let store = DummyStore {
158            blob: Some(blob_hash),
159            state: None,
160        };
161        let missing_hash = ContentHash::from_bytes([7; 32]);
162        let objects = vec![
163            ObjectInfo {
164                id: ObjectId::Hash(blob_hash),
165                obj_type: ObjectType::Blob,
166                size: blob.size() as u64,
167                delta_base: None,
168            },
169            ObjectInfo {
170                id: ObjectId::Hash(missing_hash),
171                obj_type: ObjectType::Tree,
172                size: 0,
173                delta_base: None,
174            },
175        ];
176
177        let plan = plan_object_availability(&store, &objects).unwrap();
178
179        assert_eq!(plan.have_objects.len(), 1);
180        assert_eq!(plan.want_objects.len(), 1);
181        assert!(!plan.is_complete());
182    }
183
184    #[test]
185    fn missing_state_objects_are_requested() {
186        let store = DummyStore::default();
187        let state = StateId::from_bytes([9; 32]);
188        let objects = vec![ObjectInfo {
189            id: ObjectId::StateId(state),
190            obj_type: ObjectType::State,
191            size: 0,
192            delta_base: None,
193        }];
194
195        let plan = plan_object_availability(&store, &objects).unwrap();
196
197        assert!(plan.have_objects.is_empty());
198        assert_eq!(plan.want_objects, vec![ObjectId::StateId(state)]);
199    }
200
201    #[test]
202    fn immutable_state_objects_are_not_requested_when_present() {
203        let state = StateId::from_bytes([9; 32]);
204        let store = DummyStore {
205            state: Some(state),
206            ..DummyStore::default()
207        };
208        let objects = vec![ObjectInfo {
209            id: ObjectId::StateId(state),
210            obj_type: ObjectType::State,
211            size: 0,
212            delta_base: None,
213        }];
214
215        let plan = plan_object_availability(&store, &objects).unwrap();
216
217        assert_eq!(plan.have_objects, vec![ObjectId::StateId(state)]);
218        assert!(plan.want_objects.is_empty());
219    }
220
221    #[test]
222    fn test_partial_fetch_flag_helpers() {
223        let plan = ObjectAvailabilityPlan::default().with_partial_fetch_allowed(true);
224
225        assert!(plan.partial_fetch_allowed);
226        assert!(plan.is_complete());
227    }
228}