fidl_fuchsia_pkg_ext/
repo.rs

1// Copyright 2019 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::{MirrorConfigError, RepositoryParseError, RepositoryUrlParseError};
6use fidl_fuchsia_pkg as fidl;
7use http::Uri;
8use http_uri_ext::HttpUriExt as _;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeSet;
12use std::{fmt, mem};
13use thiserror::Error;
14
15/// Convenience wrapper for the FIDL RepositoryStorageType.
16#[derive(
17    Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
18)]
19#[serde(rename_all = "snake_case")]
20pub enum RepositoryStorageType {
21    /// Store the repository in-memory. This metadata will be lost if the process or device is
22    /// restarted.
23    Ephemeral,
24
25    /// Store the metadata to persitent mutable storage. This metadata will still be available if
26    /// the process or device is restarted.
27    Persistent,
28}
29
30/// Convenience wrapper for the FIDL RepositoryRegistrationAliasConflictMode.
31#[derive(
32    Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
33)]
34#[serde(rename_all = "snake_case")]
35pub enum RepositoryRegistrationAliasConflictMode {
36    /// When conflicting aliases found during registration process, error out.
37    ErrorOut,
38
39    /// When conflicting aliases found during registration process, replace previous rule with
40    /// current.
41    Replace,
42}
43
44/// Convenience wrapper for the FIDL RepositoryKeyConfig type
45#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase", tag = "type", content = "value", deny_unknown_fields)]
47pub enum RepositoryKey {
48    Ed25519(#[serde(with = "hex_serde")] Vec<u8>),
49}
50
51/// Convenience wrapper for the FIDL MirrorConfig type
52#[derive(Clone, Debug, PartialEq, Eq, Hash)]
53pub struct MirrorConfig {
54    mirror_url: http::Uri,
55    subscribe: bool,
56    blob_mirror_url: http::Uri,
57}
58
59impl MirrorConfig {
60    // Guaranteed to always have a `scheme`.
61    pub fn mirror_url(&self) -> &http::Uri {
62        &self.mirror_url
63    }
64    pub fn subscribe(&self) -> bool {
65        self.subscribe
66    }
67
68    // Guaranteed to always have a `scheme`.
69    pub fn blob_mirror_url(&self) -> &http::Uri {
70        &self.blob_mirror_url
71    }
72}
73
74/// Omit empty optional fields and omit blob_mirror_url if derivable from mirror_url.
75impl serde::Serialize for MirrorConfig {
76    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
77    where
78        S: serde::Serializer,
79    {
80        #[derive(Serialize)]
81        pub struct SerMirrorConfig<'a> {
82            #[serde(with = "uri_serde")]
83            mirror_url: &'a http::Uri,
84            subscribe: bool,
85            #[serde(skip_serializing_if = "Option::is_none")]
86            blob_mirror_url: Option<&'a str>,
87        }
88
89        let blob_mirror_url = normalize_blob_mirror_url(&self.mirror_url, &self.blob_mirror_url);
90        let blob_mirror_string: String;
91        let blob_mirror_url = if let Some(blob_mirror_url) = blob_mirror_url {
92            blob_mirror_string = blob_mirror_url.to_string();
93            Some(blob_mirror_string.as_ref())
94        } else {
95            None
96        };
97        SerMirrorConfig { mirror_url: &self.mirror_url, subscribe: self.subscribe, blob_mirror_url }
98            .serialize(serializer)
99    }
100}
101
102/// Derive blob_mirror_url from mirror_url if blob_mirror_url is not present.
103impl<'de> serde::Deserialize<'de> for MirrorConfig {
104    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105    where
106        D: serde::Deserializer<'de>,
107    {
108        #[derive(Deserialize)]
109        pub struct DeMirrorConfig {
110            #[serde(with = "uri_serde")]
111            mirror_url: http::Uri,
112            subscribe: bool,
113            blob_mirror_url: Option<String>,
114        }
115
116        let DeMirrorConfig { mirror_url, subscribe, blob_mirror_url } =
117            DeMirrorConfig::deserialize(deserializer)?;
118
119        if mirror_url.scheme_str().is_none() {
120            return Err(serde::de::Error::custom(format!(
121                "mirror_url must have a scheme: {:?}",
122                mirror_url
123            )));
124        }
125
126        let blob_mirror_url = if let Some(blob_mirror_url) = blob_mirror_url {
127            blob_mirror_url.parse::<Uri>().map_err(|e| {
128                serde::de::Error::custom(format!("bad uri string: {:?}: {}", blob_mirror_url, e))
129            })?
130        } else {
131            blob_mirror_url_from_mirror_url(&mirror_url)
132        };
133
134        if blob_mirror_url.scheme_str().is_none() {
135            return Err(serde::de::Error::custom(format!(
136                "blob_mirror_url must have a scheme: {:?}",
137                blob_mirror_url
138            )));
139        }
140
141        Ok(Self { mirror_url, subscribe, blob_mirror_url })
142    }
143}
144
145/// Convenience wrapper for generating [MirrorConfig] values.
146#[derive(Clone, Debug)]
147pub struct MirrorConfigBuilder {
148    config: MirrorConfig,
149}
150
151impl MirrorConfigBuilder {
152    pub fn new(mirror_url: impl Into<http::Uri>) -> Result<Self, MirrorConfigError> {
153        let mirror_url = mirror_url.into();
154        if mirror_url.scheme().is_none() {
155            return Err(MirrorConfigError::MirrorUrlMissingScheme);
156        }
157        let blob_mirror_url = blob_mirror_url_from_mirror_url(&mirror_url);
158        Ok(MirrorConfigBuilder {
159            config: MirrorConfig { mirror_url, subscribe: false, blob_mirror_url },
160        })
161    }
162
163    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255347)
164    pub fn mirror_url(
165        mut self,
166        mirror_url: impl Into<http::Uri>,
167    ) -> Result<Self, (Self, MirrorConfigError)> {
168        self.config.mirror_url = mirror_url.into();
169        if self.config.mirror_url.scheme().is_none() {
170            return Err((self, MirrorConfigError::MirrorUrlMissingScheme));
171        }
172        Ok(self)
173    }
174
175    #[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401255347)
176    pub fn blob_mirror_url(
177        mut self,
178        blob_mirror_url: impl Into<http::Uri>,
179    ) -> Result<Self, (Self, MirrorConfigError)> {
180        self.config.blob_mirror_url = blob_mirror_url.into();
181        if self.config.blob_mirror_url.scheme().is_none() {
182            return Err((self, MirrorConfigError::BlobMirrorUrlMissingScheme));
183        }
184        Ok(self)
185    }
186
187    pub fn subscribe(mut self, subscribe: bool) -> Self {
188        self.config.subscribe = subscribe;
189        self
190    }
191
192    pub fn build(self) -> MirrorConfig {
193        self.config
194    }
195}
196
197impl From<MirrorConfigBuilder> for MirrorConfig {
198    fn from(builder: MirrorConfigBuilder) -> Self {
199        builder.build()
200    }
201}
202
203impl TryFrom<fidl::MirrorConfig> for MirrorConfig {
204    type Error = RepositoryParseError;
205    fn try_from(other: fidl::MirrorConfig) -> Result<Self, RepositoryParseError> {
206        let mirror_url =
207            other.mirror_url.ok_or(RepositoryParseError::MirrorUrlMissing)?.parse::<Uri>()?;
208        if mirror_url.scheme().is_none() {
209            Err(MirrorConfigError::MirrorUrlMissingScheme)?
210        }
211        let blob_mirror_url = match other.blob_mirror_url {
212            None => blob_mirror_url_from_mirror_url(&mirror_url),
213            Some(s) => {
214                let url = s.parse::<http::Uri>()?;
215                if url.scheme().is_none() {
216                    Err(MirrorConfigError::BlobMirrorUrlMissingScheme)?
217                }
218                url
219            }
220        };
221
222        Ok(Self {
223            mirror_url,
224            subscribe: other.subscribe.ok_or(RepositoryParseError::SubscribeMissing)?,
225            blob_mirror_url,
226        })
227    }
228}
229
230impl From<MirrorConfig> for fidl::MirrorConfig {
231    fn from(config: MirrorConfig) -> Self {
232        let blob_mirror_url =
233            normalize_blob_mirror_url(&config.mirror_url, &config.blob_mirror_url)
234                .map(|url| url.to_string());
235        Self {
236            mirror_url: Some(config.mirror_url.to_string()),
237            subscribe: Some(config.subscribe),
238            blob_mirror_url,
239            ..Default::default()
240        }
241    }
242}
243
244impl From<fidl::RepositoryStorageType> for RepositoryStorageType {
245    fn from(other: fidl::RepositoryStorageType) -> Self {
246        match other {
247            fidl::RepositoryStorageType::Ephemeral => RepositoryStorageType::Ephemeral,
248            fidl::RepositoryStorageType::Persistent => RepositoryStorageType::Persistent,
249        }
250    }
251}
252
253impl From<RepositoryStorageType> for fidl::RepositoryStorageType {
254    fn from(storage_type: RepositoryStorageType) -> Self {
255        match storage_type {
256            RepositoryStorageType::Ephemeral => fidl::RepositoryStorageType::Ephemeral,
257            RepositoryStorageType::Persistent => fidl::RepositoryStorageType::Persistent,
258        }
259    }
260}
261
262#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
263pub struct RepositoryTarget {
264    pub repo_name: String,
265    pub target_identifier: Option<String>,
266    pub aliases: Option<BTreeSet<String>>,
267    pub storage_type: Option<RepositoryStorageType>,
268}
269
270#[derive(Debug, Error)]
271pub enum RepositoryError {
272    #[error("the repository name is missing")]
273    MissingRepositoryName,
274
275    #[error("repository does not exist")]
276    NoMatchingRepository,
277
278    #[error("error communicating with target device")]
279    TargetCommunicationFailure,
280
281    #[error("error interacting with the target's RepositoryManager")]
282    RepositoryManagerError,
283
284    #[error("error iteracting with the target's RewriteEngine")]
285    RewriteEngineError,
286
287    #[error("unknown repository spec type")]
288    UnknownRepositorySpec,
289
290    #[error("repository spec is missing a required field")]
291    MissingRepositorySpecField,
292
293    #[error("some unspecified error during I/O")]
294    IoError,
295
296    #[error("some unspecified internal error")]
297    InternalError,
298
299    #[error("repository metadata is expired")]
300    ExpiredRepositoryMetadata,
301
302    #[error("repository registration does not exist")]
303    NoMatchingRegistration,
304
305    #[error("repository server is not running")]
306    ServerNotRunning,
307
308    #[error("invalid url")]
309    InvalidUrl,
310
311    #[error("repository server address already in use")]
312    ServerAddressAlreadyInUse,
313
314    #[error("package does not exist")]
315    NoMatchingPackage,
316
317    #[error("repository registration conflict")]
318    ConflictingRegistration,
319
320    #[error("repository metadata cannot be found")]
321    NoRepositoryMetadata,
322}
323
324fn blob_mirror_url_from_mirror_url(mirror_url: &http::Uri) -> http::Uri {
325    // Safe because mirror_url has a scheme and "blobs" is a valid path segment.
326    mirror_url.to_owned().extend_dir_with_path("blobs").unwrap()
327}
328
329fn is_default_blob_mirror_url(mirror_url: &http::Uri, blob_mirror_url: &http::Uri) -> bool {
330    blob_mirror_url == &blob_mirror_url_from_mirror_url(mirror_url)
331}
332
333fn normalize_blob_mirror_url<'a>(
334    mirror_url: &http::Uri,
335    blob_mirror_url: &'a http::Uri,
336) -> Option<&'a http::Uri> {
337    if is_default_blob_mirror_url(mirror_url, blob_mirror_url) {
338        None
339    } else {
340        Some(blob_mirror_url)
341    }
342}
343
344/// Convenience wrapper type for the autogenerated FIDL `RepositoryConfig`.
345#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
346pub struct RepositoryConfig {
347    repo_url: fuchsia_url::RepositoryUrl,
348    #[serde(default = "default_root_version")]
349    root_version: u32,
350    #[serde(default = "default_root_threshold")]
351    root_threshold: u32,
352    root_keys: Vec<RepositoryKey>,
353    mirrors: Vec<MirrorConfig>,
354    #[serde(default = "default_use_local_mirror")]
355    use_local_mirror: bool,
356    #[serde(default = "default_storage_type")]
357    repo_storage_type: RepositoryStorageType,
358}
359
360fn default_root_version() -> u32 {
361    1
362}
363
364fn default_root_threshold() -> u32 {
365    1
366}
367
368fn default_use_local_mirror() -> bool {
369    false
370}
371
372fn default_storage_type() -> RepositoryStorageType {
373    RepositoryStorageType::Ephemeral
374}
375
376impl RepositoryConfig {
377    pub fn repo_url(&self) -> &fuchsia_url::RepositoryUrl {
378        &self.repo_url
379    }
380
381    /// Insert the provided mirror, returning any previous mirror with the same URL.
382    pub fn insert_mirror(&mut self, mut mirror: MirrorConfig) -> Option<MirrorConfig> {
383        if let Some(m) = self.mirrors.iter_mut().find(|m| m.mirror_url == mirror.mirror_url) {
384            mem::swap(m, &mut mirror);
385            Some(mirror)
386        } else {
387            self.mirrors.push(mirror);
388            None
389        }
390    }
391
392    /// Remove the requested mirror by url, returning the removed mirror, if it existed.
393    pub fn remove_mirror(&mut self, mirror_url: &http::Uri) -> Option<MirrorConfig> {
394        if let Some(pos) = self.mirrors.iter().position(|m| &m.mirror_url == mirror_url) {
395            Some(self.mirrors.remove(pos))
396        } else {
397            None
398        }
399    }
400
401    /// Returns a slice of all mirrors.
402    pub fn mirrors(&self) -> &[MirrorConfig] {
403        &self.mirrors
404    }
405
406    /// Returns the initial trusted root version.
407    pub fn root_version(&self) -> u32 {
408        self.root_version
409    }
410
411    /// Returns the threshold of root keys needed to sign the initial root metadata before it is
412    /// considered trusted.
413    pub fn root_threshold(&self) -> u32 {
414        self.root_threshold
415    }
416
417    /// Returns a slice of all root keys.
418    pub fn root_keys(&self) -> &[RepositoryKey] {
419        &self.root_keys
420    }
421
422    pub fn use_local_mirror(&self) -> bool {
423        self.use_local_mirror
424    }
425
426    /// Returns the repository storage type.
427    pub fn repo_storage_type(&self) -> &RepositoryStorageType {
428        &self.repo_storage_type
429    }
430}
431
432impl TryFrom<fidl::RepositoryConfig> for RepositoryConfig {
433    type Error = RepositoryParseError;
434
435    fn try_from(other: fidl::RepositoryConfig) -> Result<Self, RepositoryParseError> {
436        let repo_url: fuchsia_url::RepositoryUrl = other
437            .repo_url
438            .ok_or(RepositoryParseError::RepoUrlMissing)?
439            .parse()
440            .map_err(|err| RepositoryParseError::InvalidRepoUrl(err))?;
441
442        let root_version = if let Some(root_version) = other.root_version {
443            if root_version < 1 {
444                return Err(RepositoryParseError::InvalidRootVersion(root_version));
445            }
446            root_version
447        } else {
448            1
449        };
450
451        let root_threshold = if let Some(root_threshold) = other.root_threshold {
452            if root_threshold < 1 {
453                return Err(RepositoryParseError::InvalidRootThreshold(root_threshold));
454            }
455            root_threshold
456        } else {
457            1
458        };
459
460        let storage_type =
461            other.storage_type.map(|r| r.into()).unwrap_or(RepositoryStorageType::Ephemeral);
462
463        Ok(Self {
464            repo_url: repo_url,
465            root_version: root_version,
466            root_threshold: root_threshold,
467            root_keys: other
468                .root_keys
469                .unwrap_or(vec![])
470                .into_iter()
471                .map(RepositoryKey::try_from)
472                .collect::<Result<_, _>>()?,
473            mirrors: other
474                .mirrors
475                .unwrap_or(vec![])
476                .into_iter()
477                .map(MirrorConfig::try_from)
478                .collect::<Result<_, _>>()?,
479            use_local_mirror: other.use_local_mirror.unwrap_or(false),
480            repo_storage_type: storage_type.into(),
481        })
482    }
483}
484
485impl From<RepositoryConfig> for fidl::RepositoryConfig {
486    fn from(config: RepositoryConfig) -> Self {
487        Self {
488            repo_url: Some(config.repo_url.to_string()),
489            root_version: Some(config.root_version),
490            root_threshold: Some(config.root_threshold),
491            root_keys: Some(config.root_keys.into_iter().map(RepositoryKey::into).collect()),
492            mirrors: Some(config.mirrors.into_iter().map(MirrorConfig::into).collect()),
493            use_local_mirror: Some(config.use_local_mirror),
494            storage_type: Some(RepositoryStorageType::into(config.repo_storage_type)),
495            ..Default::default()
496        }
497    }
498}
499
500impl From<RepositoryConfig> for RepositoryConfigBuilder {
501    fn from(config: RepositoryConfig) -> Self {
502        Self { config }
503    }
504}
505
506/// Convenience wrapper for generating [RepositoryConfig] values.
507#[derive(Clone, Debug)]
508pub struct RepositoryConfigBuilder {
509    config: RepositoryConfig,
510}
511
512impl RepositoryConfigBuilder {
513    pub fn new(repo_url: fuchsia_url::RepositoryUrl) -> Self {
514        RepositoryConfigBuilder {
515            config: RepositoryConfig {
516                repo_url,
517                root_version: 1,
518                root_threshold: 1,
519                root_keys: vec![],
520                mirrors: vec![],
521                use_local_mirror: false,
522                repo_storage_type: RepositoryStorageType::Ephemeral,
523            },
524        }
525    }
526
527    pub fn repo_url(mut self, repo_url: fuchsia_url::RepositoryUrl) -> Self {
528        self.config.repo_url = repo_url;
529        self
530    }
531
532    pub fn root_version(mut self, root_version: u32) -> Self {
533        self.config.root_version = root_version;
534        self
535    }
536
537    pub fn root_threshold(mut self, root_threshold: u32) -> Self {
538        self.config.root_threshold = root_threshold;
539        self
540    }
541
542    pub fn add_root_key(mut self, key: RepositoryKey) -> Self {
543        self.config.root_keys.push(key);
544        self
545    }
546
547    pub fn add_mirror(mut self, mirror: impl Into<MirrorConfig>) -> Self {
548        self.config.mirrors.push(mirror.into());
549        self
550    }
551
552    pub fn use_local_mirror(mut self, use_local_mirror: bool) -> Self {
553        self.config.use_local_mirror = use_local_mirror;
554        self
555    }
556
557    pub fn repo_storage_type(mut self, repo_storage_type: RepositoryStorageType) -> Self {
558        self.config.repo_storage_type = repo_storage_type;
559        self
560    }
561
562    pub fn build(self) -> RepositoryConfig {
563        self.config
564    }
565}
566
567impl From<RepositoryConfigBuilder> for RepositoryConfig {
568    fn from(builder: RepositoryConfigBuilder) -> Self {
569        builder.build()
570    }
571}
572
573/// Wraper for serializing repository configs to the on-disk JSON format.
574#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
575#[serde(tag = "version", content = "content", deny_unknown_fields)]
576pub enum RepositoryConfigs {
577    #[serde(rename = "1")]
578    Version1(Vec<RepositoryConfig>),
579}
580
581impl TryFrom<fidl::RepositoryKeyConfig> for RepositoryKey {
582    type Error = RepositoryParseError;
583    fn try_from(id: fidl::RepositoryKeyConfig) -> Result<Self, RepositoryParseError> {
584        match id {
585            fidl::RepositoryKeyConfig::Ed25519Key(key) => Ok(RepositoryKey::Ed25519(key)),
586            _ => Err(RepositoryParseError::UnsupportedKeyType),
587        }
588    }
589}
590
591impl From<RepositoryKey> for fidl::RepositoryKeyConfig {
592    fn from(key: RepositoryKey) -> Self {
593        match key {
594            RepositoryKey::Ed25519(key) => fidl::RepositoryKeyConfig::Ed25519Key(key),
595        }
596    }
597}
598
599impl fmt::Debug for RepositoryKey {
600    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601        let RepositoryKey::Ed25519(ref value) = self;
602        f.debug_tuple("Ed25519").field(&hex::encode(value)).finish()
603    }
604}
605
606#[derive(Debug, PartialEq, Clone)]
607/// Convenience wrapper type for the autogenerated FIDL `RepositoryUrl`.
608pub struct RepositoryUrl {
609    url: fuchsia_url::RepositoryUrl,
610}
611
612impl RepositoryUrl {
613    pub fn url(&self) -> &fuchsia_url::RepositoryUrl {
614        &self.url
615    }
616}
617
618impl From<fuchsia_url::RepositoryUrl> for RepositoryUrl {
619    fn from(url: fuchsia_url::RepositoryUrl) -> Self {
620        Self { url }
621    }
622}
623
624impl TryFrom<&fidl::RepositoryUrl> for RepositoryUrl {
625    type Error = RepositoryUrlParseError;
626
627    fn try_from(other: &fidl::RepositoryUrl) -> Result<Self, RepositoryUrlParseError> {
628        Ok(Self { url: other.url.parse().map_err(RepositoryUrlParseError::InvalidRepoUrl)? })
629    }
630}
631
632impl From<RepositoryUrl> for fidl::RepositoryUrl {
633    fn from(url: RepositoryUrl) -> Self {
634        Self { url: url.url.to_string() }
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use assert_matches::assert_matches;
642    use proptest::prelude::*;
643    use serde_json::json;
644    fn verify_json_serde<T>(expected_value: T, expected_json: serde_json::Value)
645    where
646        T: PartialEq + std::fmt::Debug,
647        T: serde::Serialize,
648        for<'de> T: serde::Deserialize<'de>,
649    {
650        let actual_json = serde_json::to_value(&expected_value).expect("value to serialize");
651        assert_eq!(actual_json, expected_json);
652        let actual_value = serde_json::from_value::<T>(actual_json).expect("json to deserialize");
653        assert_eq!(actual_value, expected_value);
654    }
655
656    #[test]
657    fn test_repository_key_serde() {
658        verify_json_serde(
659            RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),
660            json!({
661                "type": "ed25519",
662                "value": "f10f1003",
663            }),
664        );
665        verify_json_serde(
666            RepositoryKey::Ed25519(vec![0, 1, 2, 3]),
667            json!({
668                "type": "ed25519",
669                "value": "00010203",
670            }),
671        );
672    }
673
674    #[test]
675    fn test_repository_key_deserialize_with_bad_type() {
676        let json = r#"{"type":"bogus","value":"00010203"}"#;
677        let result = serde_json::from_str::<RepositoryKey>(json);
678        let error_message = result.unwrap_err().to_string();
679        assert!(
680            error_message.contains("unknown variant `bogus`"),
681            r#"Error message did not contain "unknown variant `bogus`", was "{}""#,
682            error_message
683        );
684    }
685
686    #[test]
687    fn test_repository_key_into_fidl() {
688        let key = RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]);
689        let as_fidl: fidl::RepositoryKeyConfig = key.into();
690        assert_eq!(as_fidl, fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3]))
691    }
692
693    #[test]
694    fn test_repository_key_from_fidl() {
695        let as_fidl = fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3]);
696        assert_matches!(
697            RepositoryKey::try_from(as_fidl),
698            Ok(RepositoryKey::Ed25519(v)) if v == vec![0xf1, 15, 16, 3]
699        );
700    }
701
702    #[test]
703    fn test_repository_key_from_fidl_with_bad_type() {
704        let as_fidl = fidl::RepositoryKeyConfig::unknown_variant_for_testing();
705        assert_matches!(
706            RepositoryKey::try_from(as_fidl),
707            Err(RepositoryParseError::UnsupportedKeyType)
708        );
709    }
710
711    #[test]
712    fn test_repository_key_into_from_fidl_roundtrip() {
713        let key = RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]);
714        let as_fidl: fidl::RepositoryKeyConfig = key.clone().into();
715        assert_eq!(RepositoryKey::try_from(as_fidl).unwrap(), key,)
716    }
717
718    #[test]
719    fn test_mirror_config_serde() {
720        verify_json_serde(
721            MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap())
722                .unwrap()
723                .build(),
724            json!({
725                "mirror_url": "http://example.com/",
726                "subscribe": false,
727            }),
728        );
729
730        verify_json_serde(
731            MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
732                .unwrap()
733                .blob_mirror_url("http://example.com/subdir".parse::<Uri>().unwrap())
734                .unwrap()
735                .build(),
736            json!({
737                "mirror_url": "http://example.com/",
738                "blob_mirror_url": "http://example.com/subdir",
739                "subscribe": false,
740            }),
741        );
742
743        verify_json_serde(
744            MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
745                .unwrap()
746                .blob_mirror_url("http://example.com/blobs".parse::<Uri>().unwrap())
747                .unwrap()
748                .build(),
749            json!({
750                "mirror_url": "http://example.com/",
751                "subscribe": false,
752            }),
753        );
754
755        verify_json_serde(
756            MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
757                .unwrap()
758                .subscribe(true)
759                .build(),
760            json!({
761                "mirror_url": "http://example.com/",
762                "subscribe": true,
763            }),
764        );
765
766        {
767            // A mirror config with a previously-accepted "blob_key" still parses.
768            let json = json!({
769                "mirror_url": "http://example.com/",
770                "blob_key": {
771                    "type": "aes",
772                    "value": "00010203",
773                },
774                "subscribe": false,
775            });
776            assert_eq!(
777                serde_json::from_value::<MirrorConfig>(json).expect("json to deserialize"),
778                MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
779                    .unwrap()
780                    .build()
781            );
782        }
783    }
784
785    #[test]
786    fn test_mirror_config_deserialize_rejects_urls_without_schemes() {
787        let j = json!({
788            "mirror_url": "example.com",
789            "subscribe": false,
790        });
791        assert_matches!(
792            serde_json::from_value::<MirrorConfig>(j),
793            Err(e) if e.to_string().starts_with("mirror_url must have a scheme")
794        );
795
796        let j = json!({
797            "mirror_url": "https://example.com",
798            "subscribe": false,
799            "blob_mirror_url": Some("example.com")
800        });
801        assert_matches!(
802            serde_json::from_value::<MirrorConfig>(j),
803            Err(e) if e.to_string().starts_with("blob_mirror_url must have a scheme")
804        );
805    }
806
807    #[test]
808    fn test_mirror_config_into_fidl() {
809        let config = MirrorConfig {
810            mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
811            subscribe: true,
812            blob_mirror_url: "http://example.com/tuf/repo/subdir/blobs".parse::<Uri>().unwrap(),
813        };
814        let as_fidl: fidl::MirrorConfig = config.into();
815        assert_eq!(
816            as_fidl,
817            fidl::MirrorConfig {
818                mirror_url: Some("http://example.com/tuf/repo".into()),
819                subscribe: Some(true),
820                blob_mirror_url: Some("http://example.com/tuf/repo/subdir/blobs".into()),
821                ..Default::default()
822            }
823        );
824    }
825
826    #[test]
827    fn test_mirror_config_into_fidl_normalizes_blob_mirror_url() {
828        let config = MirrorConfig {
829            mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
830            subscribe: false,
831            blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
832        };
833        let as_fidl: fidl::MirrorConfig = config.into();
834        assert_eq!(
835            as_fidl,
836            fidl::MirrorConfig {
837                mirror_url: Some("http://example.com/tuf/repo".into()),
838                subscribe: Some(false),
839                blob_mirror_url: None,
840                ..Default::default()
841            }
842        );
843    }
844
845    #[test]
846    fn test_mirror_config_from_fidl() {
847        let as_fidl = fidl::MirrorConfig {
848            mirror_url: Some("http://example.com/tuf/repo".into()),
849            subscribe: Some(true),
850            blob_mirror_url: Some("http://example.com/tuf/repo/subdir/blobs".into()),
851            ..Default::default()
852        };
853        assert_matches!(
854            MirrorConfig::try_from(as_fidl),
855            Ok(mirror_config) if mirror_config == MirrorConfig {
856                mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
857                subscribe: true,
858                blob_mirror_url: "http://example.com/tuf/repo/subdir/blobs".parse::<Uri>().unwrap(),
859            }
860        );
861    }
862
863    #[test]
864    fn test_mirror_config_from_fidl_rejects_urls_without_schemes() {
865        let as_fidl = fidl::MirrorConfig {
866            mirror_url: Some("example.com".into()),
867            subscribe: Some(false),
868            blob_mirror_url: None,
869            ..Default::default()
870        };
871        assert_matches!(
872            MirrorConfig::try_from(as_fidl),
873            Err(RepositoryParseError::MirrorConfig(MirrorConfigError::MirrorUrlMissingScheme))
874        );
875
876        let as_fidl = fidl::MirrorConfig {
877            mirror_url: Some("https://example.com".into()),
878            subscribe: Some(false),
879            blob_mirror_url: Some("example.com".into()),
880            ..Default::default()
881        };
882        assert_matches!(
883            MirrorConfig::try_from(as_fidl),
884            Err(RepositoryParseError::MirrorConfig(MirrorConfigError::BlobMirrorUrlMissingScheme))
885        );
886    }
887
888    #[test]
889    fn test_mirror_config_from_fidl_populates_blob_mirror_url() {
890        let as_fidl = fidl::MirrorConfig {
891            mirror_url: Some("http://example.com/tuf/repo/".into()),
892            subscribe: Some(false),
893            blob_mirror_url: None,
894            ..Default::default()
895        };
896        assert_matches!(
897            MirrorConfig::try_from(as_fidl),
898            Ok(mirror_config) if mirror_config == MirrorConfig {
899                mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
900                subscribe: false,
901                blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
902            }
903        );
904    }
905
906    prop_compose! {
907        fn uri_with_adversarial_path()(path in "[p/]{0,6}") -> http::Uri
908        {
909            let mut parts = http::uri::Parts::default();
910            parts.scheme = Some(http::uri::Scheme::HTTP);
911            parts.authority = Some(http::uri::Authority::from_static("example.com"));
912            parts.path_and_query = Some(path.parse().unwrap());
913            http::Uri::from_parts(parts).unwrap()
914        }
915    }
916
917    proptest! {
918        #![proptest_config(ProptestConfig{
919            // Disable persistence to avoid the warning for not running in the
920            // source code directory (since we're running on a Fuchsia target)
921            failure_persistence: None,
922            .. ProptestConfig::default()
923        })]
924
925        #[test]
926        fn blob_mirror_url_from_mirror_url_produces_default_blob_mirror_urls(
927            mirror_url in uri_with_adversarial_path()
928        ) {
929            let blob_mirror_url = blob_mirror_url_from_mirror_url(&mirror_url);
930            prop_assert!(is_default_blob_mirror_url(&mirror_url, &blob_mirror_url));
931        }
932
933        #[test]
934        fn normalize_blob_mirror_url_detects_default_blob_mirror_url(
935            mirror_url in uri_with_adversarial_path()
936        ) {
937            let blob_mirror_url = blob_mirror_url_from_mirror_url(&mirror_url);
938            prop_assert_eq!(normalize_blob_mirror_url(&mirror_url, &blob_mirror_url), None);
939            // also, swapped parameters should never return None
940            prop_assert_ne!(normalize_blob_mirror_url(&blob_mirror_url, &mirror_url), None);
941        }
942    }
943
944    #[test]
945    fn test_mirror_config_into_from_fidl_roundtrip() {
946        let config = MirrorConfig {
947            mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
948            subscribe: true,
949            blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
950        };
951        let as_fidl: fidl::MirrorConfig = config.clone().into();
952        assert_eq!(MirrorConfig::try_from(as_fidl).unwrap(), config);
953    }
954
955    #[test]
956    fn test_mirror_config_builder() {
957        let builder =
958            MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap()).unwrap();
959        assert_eq!(
960            builder.clone().build(),
961            MirrorConfig {
962                mirror_url: "http://example.com/".parse::<Uri>().unwrap(),
963                subscribe: false,
964                blob_mirror_url: "http://example.com/blobs".parse::<Uri>().unwrap(),
965            }
966        );
967        assert_eq!(
968            builder
969                .clone()
970                .blob_mirror_url("http://example.com/a/b".parse::<Uri>().unwrap())
971                .unwrap()
972                .build(),
973            MirrorConfig {
974                mirror_url: "http://example.com/".parse::<Uri>().unwrap(),
975                subscribe: false,
976                blob_mirror_url: "http://example.com/a/b".parse::<Uri>().unwrap(),
977            }
978        );
979        assert_eq!(
980            builder.clone().mirror_url("http://127.0.0.1".parse::<Uri>().unwrap()).unwrap().build(),
981            MirrorConfig {
982                mirror_url: "http://127.0.0.1".parse::<Uri>().unwrap(),
983                subscribe: false,
984                blob_mirror_url: "http://example.com/blobs".parse::<Uri>().unwrap(),
985            }
986        );
987        assert_eq!(
988            builder.subscribe(true).build(),
989            MirrorConfig {
990                mirror_url: "http://example.com".parse::<Uri>().unwrap(),
991                subscribe: true,
992                blob_mirror_url: "http://example.com/blobs".parse::<Uri>().unwrap(),
993            }
994        );
995    }
996
997    #[test]
998    fn test_mirror_config_builder_rejects_urls_without_schemes() {
999        assert_matches!(
1000            MirrorConfigBuilder::new("example.com".parse::<Uri>().unwrap()),
1001            Err(MirrorConfigError::MirrorUrlMissingScheme)
1002        );
1003
1004        let builder =
1005            MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap()).unwrap();
1006        assert_matches!(
1007            builder.mirror_url("example.com".parse::<Uri>().unwrap()),
1008            Err((_, MirrorConfigError::MirrorUrlMissingScheme))
1009        );
1010
1011        let builder =
1012            MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap()).unwrap();
1013        assert_matches!(
1014            builder.blob_mirror_url("example.com".parse::<Uri>().unwrap()),
1015            Err((_, MirrorConfigError::BlobMirrorUrlMissingScheme))
1016        );
1017    }
1018
1019    #[test]
1020    fn test_mirror_config_bad_uri() {
1021        let as_fidl = fidl::MirrorConfig {
1022            mirror_url: None,
1023            subscribe: Some(false),
1024            blob_mirror_url: None,
1025            ..Default::default()
1026        };
1027        assert_matches!(
1028            MirrorConfig::try_from(as_fidl),
1029            Err(RepositoryParseError::MirrorUrlMissing)
1030        );
1031    }
1032
1033    #[test]
1034    fn test_repository_config_builder() {
1035        let repo_url = fuchsia_url::RepositoryUrl::parse("fuchsia-pkg://fuchsia.com").unwrap();
1036        let builder = RepositoryConfigBuilder::new(repo_url.clone());
1037        assert_eq!(
1038            builder.clone().build(),
1039            RepositoryConfig {
1040                repo_url: repo_url.clone(),
1041                root_version: 1,
1042                root_threshold: 1,
1043                root_keys: vec![],
1044                mirrors: vec![],
1045                use_local_mirror: false,
1046                repo_storage_type: RepositoryStorageType::Ephemeral,
1047            }
1048        );
1049
1050        assert_eq!(
1051            builder.clone().repo_storage_type(RepositoryStorageType::Persistent).build(),
1052            RepositoryConfig {
1053                repo_url: repo_url.clone(),
1054                root_version: 1,
1055                root_threshold: 1,
1056                root_keys: vec![],
1057                mirrors: vec![],
1058                use_local_mirror: false,
1059                repo_storage_type: RepositoryStorageType::Persistent,
1060            }
1061        );
1062    }
1063
1064    #[test]
1065    fn test_repository_config_into_fidl() {
1066        let config = RepositoryConfig {
1067            repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1068            root_version: 2,
1069            root_threshold: 2,
1070            root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3])],
1071            mirrors: vec![MirrorConfig {
1072                mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
1073                subscribe: true,
1074                blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
1075            }],
1076            use_local_mirror: true,
1077            repo_storage_type: RepositoryStorageType::Ephemeral,
1078        };
1079        let as_fidl: fidl::RepositoryConfig = config.into();
1080        assert_eq!(
1081            as_fidl,
1082            fidl::RepositoryConfig {
1083                repo_url: Some("fuchsia-pkg://fuchsia.com".parse().unwrap()),
1084                root_version: Some(2),
1085                root_threshold: Some(2),
1086                root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
1087                mirrors: Some(vec![fidl::MirrorConfig {
1088                    mirror_url: Some("http://example.com/tuf/repo".into()),
1089                    subscribe: Some(true),
1090                    blob_mirror_url: None,
1091                    ..Default::default()
1092                }]),
1093                use_local_mirror: Some(true),
1094                storage_type: Some(fidl::RepositoryStorageType::Ephemeral),
1095                ..Default::default()
1096            }
1097        );
1098    }
1099
1100    #[test]
1101    fn test_repository_config_from_fidl_without_storage_type() {
1102        let as_fidl = fidl::RepositoryConfig {
1103            repo_url: Some("fuchsia-pkg://fuchsia.com".parse().unwrap()),
1104            root_version: Some(1),
1105            root_threshold: Some(1),
1106            root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
1107            mirrors: Some(vec![fidl::MirrorConfig {
1108                mirror_url: Some("http://example.com/tuf/repo/".into()),
1109                subscribe: Some(true),
1110                blob_mirror_url: None,
1111                ..Default::default()
1112            }]),
1113            use_local_mirror: None,
1114            storage_type: None,
1115            ..Default::default()
1116        };
1117        assert_matches!(
1118            RepositoryConfig::try_from(as_fidl),
1119            Ok(repository_config) if repository_config == RepositoryConfig {
1120                repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1121                root_version: 1,
1122                root_threshold: 1,
1123                root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
1124                mirrors: vec![MirrorConfig {
1125                    mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
1126                    subscribe: true,
1127                    blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
1128                },],
1129                use_local_mirror: false,
1130                repo_storage_type: RepositoryStorageType::Ephemeral,
1131            }
1132        );
1133    }
1134
1135    #[test]
1136    fn test_repository_config_from_fidl_with_storage_type() {
1137        let as_fidl = fidl::RepositoryConfig {
1138            repo_url: Some("fuchsia-pkg://fuchsia.com".parse().unwrap()),
1139            root_version: Some(1),
1140            root_threshold: Some(1),
1141            root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
1142            mirrors: Some(vec![fidl::MirrorConfig {
1143                mirror_url: Some("http://example.com/tuf/repo/".into()),
1144                subscribe: Some(true),
1145                blob_mirror_url: None,
1146                ..Default::default()
1147            }]),
1148            use_local_mirror: None,
1149            storage_type: Some(fidl::RepositoryStorageType::Persistent),
1150            ..Default::default()
1151        };
1152        assert_matches!(
1153            RepositoryConfig::try_from(as_fidl),
1154            Ok(repository_config) if repository_config == RepositoryConfig {
1155                repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1156                root_version: 1,
1157                root_threshold: 1,
1158                root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
1159                mirrors: vec![MirrorConfig {
1160                    mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
1161                    subscribe: true,
1162                    blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
1163                },],
1164                use_local_mirror: false,
1165                repo_storage_type: RepositoryStorageType::Persistent,
1166            }
1167        );
1168    }
1169
1170    #[test]
1171    fn test_repository_config_from_fidl_without_version_and_threshold_and_use_local_mirror() {
1172        let as_fidl = fidl::RepositoryConfig {
1173            repo_url: Some("fuchsia-pkg://fuchsia.com".parse().unwrap()),
1174            root_version: None,
1175            root_threshold: None,
1176            root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
1177            mirrors: Some(vec![fidl::MirrorConfig {
1178                mirror_url: Some("http://example.com/tuf/repo/".into()),
1179                subscribe: Some(true),
1180                blob_mirror_url: None,
1181                ..Default::default()
1182            }]),
1183            use_local_mirror: None,
1184            storage_type: None,
1185            ..Default::default()
1186        };
1187        assert_matches!(
1188            RepositoryConfig::try_from(as_fidl),
1189            Ok(repository_config) if repository_config == RepositoryConfig {
1190                repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1191                root_version: 1,
1192                root_threshold: 1,
1193                root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
1194                mirrors: vec![MirrorConfig {
1195                    mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
1196                    subscribe: true,
1197                    blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
1198                },],
1199                use_local_mirror: false,
1200                repo_storage_type: RepositoryStorageType::Ephemeral,
1201            }
1202        );
1203    }
1204
1205    #[test]
1206    fn test_repository_config_from_fidl_with_version_and_threshold_and_use_local_mirror() {
1207        let as_fidl = fidl::RepositoryConfig {
1208            repo_url: Some("fuchsia-pkg://fuchsia.com".parse().unwrap()),
1209            root_version: Some(2),
1210            root_threshold: Some(2),
1211            root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
1212            mirrors: Some(vec![fidl::MirrorConfig {
1213                mirror_url: Some("http://example.com/tuf/repo/".into()),
1214                subscribe: Some(true),
1215                blob_mirror_url: None,
1216                ..Default::default()
1217            }]),
1218            use_local_mirror: Some(true),
1219            storage_type: None,
1220            ..Default::default()
1221        };
1222        assert_matches!(
1223            RepositoryConfig::try_from(as_fidl),
1224            Ok(repository_config) if repository_config == RepositoryConfig {
1225                repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1226                root_version: 2,
1227                root_threshold: 2,
1228                root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
1229                mirrors: vec![MirrorConfig {
1230                    mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
1231                    subscribe: true,
1232                    blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
1233                },],
1234                use_local_mirror: true,
1235                repo_storage_type: RepositoryStorageType::Ephemeral,
1236            }
1237        );
1238    }
1239
1240    #[test]
1241    fn test_repository_config_from_fidl_repo_url_missing() {
1242        let as_fidl = fidl::RepositoryConfig {
1243            repo_url: None,
1244            root_version: None,
1245            root_threshold: None,
1246            root_keys: Some(vec![]),
1247            mirrors: Some(vec![]),
1248            use_local_mirror: None,
1249            storage_type: None,
1250            ..Default::default()
1251        };
1252        assert_matches!(
1253            RepositoryConfig::try_from(as_fidl),
1254            Err(RepositoryParseError::RepoUrlMissing)
1255        );
1256    }
1257
1258    #[test]
1259    fn test_repository_config_into_from_fidl_roundtrip() {
1260        let config = RepositoryConfig {
1261            repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1262            root_version: 2,
1263            root_threshold: 2,
1264            root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3])],
1265            mirrors: vec![MirrorConfig {
1266                mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
1267                subscribe: true,
1268                blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
1269            }],
1270            use_local_mirror: true,
1271            repo_storage_type: RepositoryStorageType::Ephemeral,
1272        };
1273        let as_fidl: fidl::RepositoryConfig = config.clone().into();
1274        assert_eq!(RepositoryConfig::try_from(as_fidl).unwrap(), config);
1275    }
1276
1277    #[test]
1278    fn test_repository_config_deserialize_missing_root_version_and_threshold_and_use_local_mirror()
1279    {
1280        let json_value = json!({
1281            "repo_url": "fuchsia-pkg://fuchsia.com",
1282            "root_keys": [],
1283            "mirrors": [],
1284        });
1285        let actual_config: RepositoryConfig = serde_json::from_value(json_value).unwrap();
1286
1287        assert_eq!(
1288            actual_config,
1289            RepositoryConfig {
1290                repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1291                root_version: 1,
1292                root_threshold: 1,
1293                root_keys: vec![],
1294                mirrors: vec![],
1295                use_local_mirror: false,
1296                repo_storage_type: RepositoryStorageType::Ephemeral,
1297            },
1298        );
1299    }
1300
1301    // Validate we can still deserialize old configs that have the now-removed
1302    // "update_package_url" field.
1303    #[test]
1304    fn test_repository_config_deserialize_ignores_update_package_url() {
1305        let json_value = json!({
1306            "repo_url": "fuchsia-pkg://fuchsia.com",
1307            "root_keys": [],
1308            "mirrors": [],
1309            "update_package_url": "ignored-value",
1310        });
1311        let actual_config: RepositoryConfig = serde_json::from_value(json_value).unwrap();
1312
1313        assert_eq!(
1314            actual_config,
1315            RepositoryConfig {
1316                repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1317                root_version: 1,
1318                root_threshold: 1,
1319                root_keys: vec![],
1320                mirrors: vec![],
1321                use_local_mirror: false,
1322                repo_storage_type: RepositoryStorageType::Ephemeral,
1323            },
1324        );
1325    }
1326
1327    #[test]
1328    fn test_repository_configs_serde_simple() {
1329        verify_json_serde(
1330            RepositoryConfigs::Version1(vec![RepositoryConfig {
1331                repo_url: "fuchsia-pkg://fuchsia.com".parse().unwrap(),
1332                root_version: 1,
1333                root_threshold: 1,
1334                root_keys: vec![],
1335                mirrors: vec![],
1336                use_local_mirror: true,
1337                repo_storage_type: RepositoryStorageType::Ephemeral,
1338            }]),
1339            json!({
1340                "version": "1",
1341                "content": [{
1342                    "repo_url": "fuchsia-pkg://fuchsia.com",
1343                    "root_version": 1,
1344                    "root_threshold": 1,
1345                    "root_keys": [],
1346                    "mirrors": [],
1347                    "use_local_mirror": true,
1348                    "repo_storage_type": "ephemeral",
1349                }],
1350            }),
1351        );
1352    }
1353
1354    #[test]
1355    fn test_repository_url_into_fidl() {
1356        let url = RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".parse().unwrap() };
1357        let as_fidl: fidl::RepositoryUrl = url.into();
1358        assert_eq!(as_fidl, fidl::RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".to_owned() });
1359    }
1360
1361    #[test]
1362    fn test_repository_url_from_fidl() {
1363        let as_fidl = fidl::RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".to_owned() };
1364        assert_matches!(
1365            RepositoryUrl::try_from(&as_fidl),
1366            Ok(RepositoryUrl { url }) if url == "fuchsia-pkg://fuchsia.com".parse().unwrap()
1367        );
1368    }
1369
1370    #[test]
1371    fn test_repository_url_from_fidl_with_bad_url() {
1372        let as_fidl = fidl::RepositoryUrl { url: "invalid-scheme://fuchsia.com".to_owned() };
1373        assert_matches!(
1374            RepositoryUrl::try_from(&as_fidl),
1375            Err(RepositoryUrlParseError::InvalidRepoUrl(fuchsia_url::ParseError::InvalidScheme))
1376        );
1377    }
1378
1379    #[test]
1380    fn test_repository_url_into_from_fidl_roundtrip() {
1381        let url = RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".parse().unwrap() };
1382        let as_fidl: fidl::RepositoryUrl = url.clone().into();
1383        assert_eq!(RepositoryUrl::try_from(&as_fidl).unwrap(), url);
1384    }
1385}
1386
1387mod hex_serde {
1388    use serde::Deserialize;
1389
1390    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
1391    where
1392        S: serde::Serializer,
1393    {
1394        let s = hex::encode(bytes);
1395        serializer.serialize_str(&s)
1396    }
1397
1398    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1399    where
1400        D: serde::Deserializer<'de>,
1401    {
1402        let value = String::deserialize(deserializer)?;
1403        hex::decode(value.as_bytes())
1404            .map_err(|e| serde::de::Error::custom(format!("bad hex value: {:?}: {}", value, e)))
1405    }
1406}
1407
1408mod uri_serde {
1409    use http::Uri;
1410    use serde::Deserialize;
1411
1412    pub fn serialize<S>(uri: &http::Uri, serializer: S) -> Result<S::Ok, S::Error>
1413    where
1414        S: serde::Serializer,
1415    {
1416        let s = uri.to_string();
1417        serializer.serialize_str(&s)
1418    }
1419
1420    pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error>
1421    where
1422        D: serde::Deserializer<'de>,
1423    {
1424        let value = String::deserialize(deserializer)?;
1425        value
1426            .parse::<Uri>()
1427            .map_err(|e| serde::de::Error::custom(format!("bad uri value: {:?}: {}", value, e)))
1428    }
1429}