system_image/
anchored_packages.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::errors::AnchoredPackagesError;
6use fuchsia_pkg::package_sets::{AnchoredPackageMap, AnchoredPackageSetType, PackageProperties};
7use fuchsia_url::PinnedAbsolutePackageUrl;
8use std::collections::BTreeMap;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct AnchoredPackages {
12    contents: AnchoredPackageMap,
13}
14
15impl AnchoredPackages {
16    /// Create a new empty instance of `AnchoredPackages`.
17    pub fn new() -> Self {
18        Self { contents: AnchoredPackageMap::new() }
19    }
20
21    /// Clears all entries of a package set type (if provided) or the entire structure of an
22    /// existing, possibly populated instance of `AnchoredPackages`.
23    pub fn clear(&mut self, package_set_type: Option<AnchoredPackageSetType>) -> &mut Self {
24        match package_set_type {
25            None => self.contents = AnchoredPackageMap::new(),
26            Some(t) => {
27                let _ = self.contents.remove(&t);
28            }
29        }
30        self
31    }
32
33    // Insert a package specified by a PinnedAbsolutePackageUrl and the associated PackageSetType.
34    pub fn insert(
35        &mut self,
36        package_set_type: AnchoredPackageSetType,
37        url: PinnedAbsolutePackageUrl,
38    ) -> Result<(), AnchoredPackagesError> {
39        let (u, h) = url.into_unpinned_and_hash();
40        if let Some(map) = self.contents.get_mut(&package_set_type) {
41            if map.contains_key(&u) {
42                return Err(AnchoredPackagesError::DuplicateNotSupported(u));
43            }
44            map.insert(u, PackageProperties { hash: h });
45        } else {
46            self.contents
47                .insert(package_set_type, BTreeMap::from([(u, PackageProperties { hash: h })]));
48        }
49        Ok(())
50    }
51
52    /// Create a new instance of `AnchoredPackages` from parsing a json.
53    /// If there are no anchored packages, `file_contents` must be empty.
54    pub(crate) fn from_json(file_contents: &[u8]) -> Result<Self, AnchoredPackagesError> {
55        if file_contents.is_empty() {
56            return Ok(Self { contents: AnchoredPackageMap::new() });
57        }
58        let contents = parse_json(file_contents)?;
59        Ok(Self { contents })
60    }
61
62    /// Returns the mapping for a given package set type as pinned absolute package URLs
63    pub fn as_pinned(
64        &self,
65        package_set_type: AnchoredPackageSetType,
66    ) -> Vec<PinnedAbsolutePackageUrl> {
67        match self.contents.get(&package_set_type) {
68            Some(t) => t
69                .iter()
70                .map(|x| PinnedAbsolutePackageUrl::from_unpinned(x.0.clone(), x.1.hash))
71                .collect(),
72            None => vec![],
73        }
74    }
75
76    /// Serializes the complete mapping of all anchored packages to a writer
77    pub fn serialize(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
78        if self.contents.is_empty() {
79            return Ok(());
80        }
81        serde_json::to_writer(writer, &self.contents)
82    }
83}
84
85impl Default for AnchoredPackages {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91fn parse_json(contents: &[u8]) -> Result<AnchoredPackageMap, AnchoredPackagesError> {
92    serde_json::from_slice(contents).map_err(AnchoredPackagesError::JsonError)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use assert_matches::assert_matches;
99    use fuchsia_hash::Hash;
100    use fuchsia_url::{AbsolutePackageUrl, UnpinnedAbsolutePackageUrl};
101    use std::str::FromStr;
102
103    fn populated_anchored_packages() -> (AnchoredPackages, &'static str) {
104        let json = r#" {
105            "anchored_on_demand": {
106                "fuchsia-pkg://fuchsia.com/package0": {
107                    "hash": "0000000000000000000000000000000000000000000000000000000000000000"
108                },
109                "fuchsia-pkg://fuchsia.com/package1": {
110                    "hash": "1111111111111111111111111111111111111111111111111111111111111111"
111                }
112            },
113            "anchored_permanent": {
114                "fuchsia-pkg://fuchsia.com/package2": {
115                    "hash": "2222222222222222222222222222222222222222222222222222222222222222"
116                },
117                "fuchsia-pkg://fuchsia.com/package3": {
118                    "hash": "3333333333333333333333333333333333333333333333333333333333333333"
119                }
120            }
121         }"#;
122
123        let mut anchored_packages_map = BTreeMap::new();
124        anchored_packages_map.insert(AnchoredPackageSetType::OnDemand, BTreeMap::new());
125        anchored_packages_map.insert(AnchoredPackageSetType::Permanent, BTreeMap::new());
126        anchored_packages_map.get_mut(&AnchoredPackageSetType::OnDemand).unwrap().insert(
127            UnpinnedAbsolutePackageUrl::from_str("fuchsia-pkg://fuchsia.com/package1").unwrap(),
128            PackageProperties {
129                hash: Hash::from_str(
130                    "1111111111111111111111111111111111111111111111111111111111111111",
131                )
132                .unwrap(),
133            },
134        );
135        anchored_packages_map.get_mut(&AnchoredPackageSetType::OnDemand).unwrap().insert(
136            UnpinnedAbsolutePackageUrl::from_str("fuchsia-pkg://fuchsia.com/package0").unwrap(),
137            PackageProperties {
138                hash: Hash::from_str(
139                    "0000000000000000000000000000000000000000000000000000000000000000",
140                )
141                .unwrap(),
142            },
143        );
144        anchored_packages_map.get_mut(&AnchoredPackageSetType::Permanent).unwrap().insert(
145            UnpinnedAbsolutePackageUrl::from_str("fuchsia-pkg://fuchsia.com/package3").unwrap(),
146            PackageProperties {
147                hash: Hash::from_str(
148                    "3333333333333333333333333333333333333333333333333333333333333333",
149                )
150                .unwrap(),
151            },
152        );
153        anchored_packages_map.get_mut(&AnchoredPackageSetType::Permanent).unwrap().insert(
154            UnpinnedAbsolutePackageUrl::from_str("fuchsia-pkg://fuchsia.com/package2").unwrap(),
155            PackageProperties {
156                hash: Hash::from_str(
157                    "2222222222222222222222222222222222222222222222222222222222222222",
158                )
159                .unwrap(),
160            },
161        );
162        (AnchoredPackages { contents: anchored_packages_map }, json)
163    }
164
165    #[test]
166    fn empty_anchored_package_list_in_json() {
167        let empty: &[u8] = &[0; 0];
168        let r = AnchoredPackages::from_json(empty);
169        assert!(r.is_ok());
170    }
171
172    #[test]
173    fn default_anchored_package_structure() {
174        let empty = AnchoredPackages::new();
175        let default = AnchoredPackages { ..Default::default() };
176        assert_eq!(empty, default);
177    }
178
179    #[test]
180    fn insert_into_anchored_packages_structure() {
181        let mut packages = AnchoredPackages::new();
182        assert!(packages.insert(AnchoredPackageSetType::Automatic,
183            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package0?hash=0000000000000000000000000000000000000000000000000000000000000000").unwrap()).is_ok());
184    }
185
186    #[test]
187    fn clear_non_empty_anchored_package_structure() {
188        let mut packages = AnchoredPackages::new();
189        assert!(packages.insert(AnchoredPackageSetType::Automatic,
190            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package0?hash=0000000000000000000000000000000000000000000000000000000000000000").unwrap()).is_ok());
191        assert!(packages.insert(AnchoredPackageSetType::OnDemand,
192            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package1?hash=1111111111111111111111111111111111111111111111111111111111111111").unwrap()).is_ok());
193        packages.clear(None);
194        let empty = AnchoredPackages::new();
195        assert_eq!(empty, packages);
196    }
197
198    #[test]
199    fn partially_clear_non_empty_anchored_package_structure() {
200        let mut packages = AnchoredPackages::new();
201        assert!(packages.insert(AnchoredPackageSetType::Automatic,
202            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package0?hash=0000000000000000000000000000000000000000000000000000000000000000").unwrap()).is_ok());
203        assert!(packages.insert(AnchoredPackageSetType::OnDemand,
204            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package1?hash=1111111111111111111111111111111111111111111111111111111111111111").unwrap()).is_ok());
205        packages.clear(Some(AnchoredPackageSetType::Automatic));
206        let mut packages_cmp = AnchoredPackages::new();
207        assert_ne!(packages_cmp, packages);
208        assert!(packages_cmp.insert(AnchoredPackageSetType::OnDemand,
209            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package1?hash=1111111111111111111111111111111111111111111111111111111111111111").unwrap()).is_ok());
210        assert_eq!(packages_cmp, packages);
211    }
212
213    #[test]
214    fn insert_into_structure_and_json_deserialize_yield_identical_mappings() {
215        let json = r#" {
216            "anchored_automatic": {
217                "fuchsia-pkg://fuchsia.com/package0": {
218                    "hash": "0000000000000000000000000000000000000000000000000000000000000000"
219                }
220            },
221            "anchored_on_demand": {
222                "fuchsia-pkg://fuchsia.com/package1": {
223                    "hash": "1111111111111111111111111111111111111111111111111111111111111111"
224                }
225            }
226        }"#;
227        let from_json = AnchoredPackages::from_json(json.as_bytes()).unwrap();
228        let mut from_insert = AnchoredPackages::new();
229        assert!(from_insert.insert(AnchoredPackageSetType::Automatic,
230            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package0?hash=0000000000000000000000000000000000000000000000000000000000000000").unwrap()).is_ok());
231        assert!(from_insert.insert(AnchoredPackageSetType::OnDemand,
232            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package1?hash=1111111111111111111111111111111111111111111111111111111111111111").unwrap()).is_ok());
233        assert_eq!(from_json, from_insert);
234    }
235
236    #[test]
237    fn missing_hash_in_anchored_packages() {
238        let json = r#" {
239            "anchored_automatic": {
240                "fuchsia-pkg://fuchsia.com/package0": {
241                    "wrong_key": "0000000000000000000000000000000000000000000000000000000000000000"
242                },
243                "fuchsia-pkg://fuchsia.com/package1": {
244                    "hash": "1111111111111111111111111111111111111111111111111111111111111111"
245                }
246            }
247        }"#;
248
249        assert_matches!(
250            AnchoredPackages::from_json(json.as_bytes()),
251            Err(AnchoredPackagesError::JsonError(_))
252        );
253    }
254
255    #[test]
256    fn pinned_package_url_in_anchored_package_list() {
257        let json = r#" {
258            "anchored_automatic": {
259                "fuchsia-pkg://fuchsia.com/package0?hash=0000000000000000000000000000000000000000000000000000000000000000": {
260                    "hash": "0000000000000000000000000000000000000000000000000000000000000000"
261                }
262            }
263        }"#;
264
265        assert_matches!(
266            AnchoredPackages::from_json(json.as_bytes()),
267            Err(AnchoredPackagesError::JsonError(_))
268        );
269    }
270
271    #[test]
272    fn multiple_package_sets_in_list() {
273        let (expect, json) = populated_anchored_packages();
274        let got = AnchoredPackages::from_json(json.as_bytes()).unwrap();
275        assert_eq!(got, expect);
276    }
277
278    #[test]
279    fn additional_unused_properties_in_anchored_packages_list() {
280        let json = r#" {
281            "anchored_automatic": {
282                "fuchsia-pkg://fuchsia.com/package0": {
283                    "hash": "0000000000000000000000000000000000000000000000000000000000000000",
284                    "foo": "0"
285                },
286                "fuchsia-pkg://fuchsia.com/package1": {
287                    "hash": "1111111111111111111111111111111111111111111111111111111111111111",
288                    "bar": "1"
289                }
290            }
291        }"#;
292        let got = AnchoredPackages::from_json(json.as_bytes()).unwrap();
293        let mut expect = AnchoredPackages::new();
294        assert!(expect.insert(AnchoredPackageSetType::Automatic,
295            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package1?hash=1111111111111111111111111111111111111111111111111111111111111111").unwrap()).is_ok());
296        assert!(expect.insert(AnchoredPackageSetType::Automatic,
297            PinnedAbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package0?hash=0000000000000000000000000000000000000000000000000000000000000000").unwrap()).is_ok());
298        assert_eq!(got, expect);
299    }
300
301    #[test]
302    fn anchored_packages_as_pinned_urls() {
303        let (_, json) = populated_anchored_packages();
304        let mut got_on_demand = AnchoredPackages::from_json(json.as_bytes())
305            .unwrap()
306            .as_pinned(AnchoredPackageSetType::OnDemand);
307        got_on_demand.sort();
308        let mut got_permanent = AnchoredPackages::from_json(json.as_bytes())
309            .unwrap()
310            .as_pinned(AnchoredPackageSetType::Permanent);
311        got_permanent.sort();
312        let expected_on_demand = vec![
313            AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package0?hash=0000000000000000000000000000000000000000000000000000000000000000").unwrap().pinned().unwrap(),
314            AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package1?hash=1111111111111111111111111111111111111111111111111111111111111111").unwrap().pinned().unwrap(),
315        ];
316        let expected_permanent = vec![
317            AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package2?hash=2222222222222222222222222222222222222222222222222222222222222222").unwrap().pinned().unwrap(),
318            AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/package3?hash=3333333333333333333333333333333333333333333333333333333333333333").unwrap().pinned().unwrap(),
319        ];
320        assert_eq!(got_on_demand, expected_on_demand);
321        assert_eq!(got_permanent, expected_permanent);
322    }
323
324    #[test]
325    fn test_serialize_deserialize_round_trip() {
326        let mut bytes = vec![];
327
328        let (packages, _) = populated_anchored_packages();
329        packages.serialize(&mut bytes).unwrap();
330
331        assert_eq!(AnchoredPackages::from_json(&bytes).unwrap(), packages);
332    }
333}