blob: fd04272c2c8afefd02a469a32f2361f48409de27 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::errors::{MirrorConfigError, RepositoryParseError, RepositoryUrlParseError},
fidl_fuchsia_pkg as fidl,
fuchsia_url::pkg_url::RepoUrl,
http::Uri,
http_uri_ext::HttpUriExt as _,
serde::{Deserialize, Serialize},
std::convert::TryFrom,
std::{fmt, mem},
};
/// Convenience wrapper for the FIDL RepositoryStorageType.
#[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RepositoryStorageType {
/// Store the repository in-memory. This metadata will be lost if the process or device is
/// restarted.
Ephemeral,
/// Store the metadata to persitent mutable storage. This metadata will still be available if
/// the process or device is restarted.
Persistent,
}
/// Convenience wrapper for the FIDL RepositoryKeyConfig type
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase", tag = "type", content = "value", deny_unknown_fields)]
pub enum RepositoryKey {
Ed25519(#[serde(with = "hex_serde")] Vec<u8>),
}
/// Convenience wrapper for the FIDL MirrorConfig type
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct MirrorConfig {
mirror_url: http::Uri,
subscribe: bool,
blob_mirror_url: http::Uri,
}
impl MirrorConfig {
// Guaranteed to always have a `scheme`.
pub fn mirror_url(&self) -> &http::Uri {
&self.mirror_url
}
pub fn subscribe(&self) -> bool {
self.subscribe
}
// Guaranteed to always have a `scheme`.
pub fn blob_mirror_url(&self) -> &http::Uri {
&self.blob_mirror_url
}
}
/// Omit empty optional fields and omit blob_mirror_url if derivable from mirror_url.
impl serde::Serialize for MirrorConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
pub struct SerMirrorConfig<'a> {
#[serde(with = "uri_serde")]
mirror_url: &'a http::Uri,
subscribe: bool,
#[serde(skip_serializing_if = "Option::is_none")]
blob_mirror_url: Option<&'a str>,
}
let blob_mirror_url = normalize_blob_mirror_url(&self.mirror_url, &self.blob_mirror_url);
let blob_mirror_string: String;
let blob_mirror_url = if let Some(blob_mirror_url) = blob_mirror_url {
blob_mirror_string = blob_mirror_url.to_string();
Some(blob_mirror_string.as_ref())
} else {
None
};
SerMirrorConfig { mirror_url: &self.mirror_url, subscribe: self.subscribe, blob_mirror_url }
.serialize(serializer)
}
}
/// Derive blob_mirror_url from mirror_url if blob_mirror_url is not present.
impl<'de> serde::Deserialize<'de> for MirrorConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
pub struct DeMirrorConfig {
#[serde(with = "uri_serde")]
mirror_url: http::Uri,
subscribe: bool,
blob_mirror_url: Option<String>,
}
let DeMirrorConfig { mirror_url, subscribe, blob_mirror_url } =
DeMirrorConfig::deserialize(deserializer)?;
if mirror_url.scheme_str().is_none() {
return Err(serde::de::Error::custom(format!(
"mirror_url must have a scheme: {:?}",
mirror_url
)));
}
let blob_mirror_url = if let Some(blob_mirror_url) = blob_mirror_url {
blob_mirror_url.parse::<Uri>().map_err(|e| {
serde::de::Error::custom(format!("bad uri string: {:?}: {}", blob_mirror_url, e))
})?
} else {
blob_mirror_url_from_mirror_url(&mirror_url)
};
if blob_mirror_url.scheme_str().is_none() {
return Err(serde::de::Error::custom(format!(
"blob_mirror_url must have a scheme: {:?}",
blob_mirror_url
)));
}
Ok(Self { mirror_url, subscribe, blob_mirror_url })
}
}
/// Convenience wrapper for generating [MirrorConfig] values.
#[derive(Clone, Debug)]
pub struct MirrorConfigBuilder {
config: MirrorConfig,
}
impl MirrorConfigBuilder {
pub fn new(mirror_url: impl Into<http::Uri>) -> Result<Self, MirrorConfigError> {
let mirror_url = mirror_url.into();
if mirror_url.scheme().is_none() {
return Err(MirrorConfigError::MirrorUrlMissingScheme);
}
let blob_mirror_url = blob_mirror_url_from_mirror_url(&mirror_url);
Ok(MirrorConfigBuilder {
config: MirrorConfig { mirror_url, subscribe: false, blob_mirror_url },
})
}
pub fn mirror_url(
mut self,
mirror_url: impl Into<http::Uri>,
) -> Result<Self, (Self, MirrorConfigError)> {
self.config.mirror_url = mirror_url.into();
if self.config.mirror_url.scheme().is_none() {
return Err((self, MirrorConfigError::MirrorUrlMissingScheme));
}
Ok(self)
}
pub fn blob_mirror_url(
mut self,
blob_mirror_url: impl Into<http::Uri>,
) -> Result<Self, (Self, MirrorConfigError)> {
self.config.blob_mirror_url = blob_mirror_url.into();
if self.config.blob_mirror_url.scheme().is_none() {
return Err((self, MirrorConfigError::BlobMirrorUrlMissingScheme));
}
Ok(self)
}
pub fn subscribe(mut self, subscribe: bool) -> Self {
self.config.subscribe = subscribe;
self
}
pub fn build(self) -> MirrorConfig {
self.config
}
}
impl From<MirrorConfigBuilder> for MirrorConfig {
fn from(builder: MirrorConfigBuilder) -> Self {
builder.build()
}
}
impl TryFrom<fidl::MirrorConfig> for MirrorConfig {
type Error = RepositoryParseError;
fn try_from(other: fidl::MirrorConfig) -> Result<Self, RepositoryParseError> {
let mirror_url =
other.mirror_url.ok_or(RepositoryParseError::MirrorUrlMissing)?.parse::<Uri>()?;
if mirror_url.scheme().is_none() {
Err(MirrorConfigError::MirrorUrlMissingScheme)?
}
let blob_mirror_url = match other.blob_mirror_url {
None => blob_mirror_url_from_mirror_url(&mirror_url),
Some(s) => {
let url = s.parse::<http::Uri>()?;
if url.scheme().is_none() {
Err(MirrorConfigError::BlobMirrorUrlMissingScheme)?
}
url
}
};
Ok(Self {
mirror_url,
subscribe: other.subscribe.ok_or(RepositoryParseError::SubscribeMissing)?,
blob_mirror_url,
})
}
}
impl From<MirrorConfig> for fidl::MirrorConfig {
fn from(config: MirrorConfig) -> Self {
let blob_mirror_url =
normalize_blob_mirror_url(&config.mirror_url, &config.blob_mirror_url)
.map(|url| url.to_string());
Self {
mirror_url: Some(config.mirror_url.to_string()),
subscribe: Some(config.subscribe),
blob_mirror_url,
..Self::EMPTY
}
}
}
impl From<fidl::RepositoryStorageType> for RepositoryStorageType {
fn from(other: fidl::RepositoryStorageType) -> Self {
match other {
fidl::RepositoryStorageType::Ephemeral => RepositoryStorageType::Ephemeral,
fidl::RepositoryStorageType::Persistent => RepositoryStorageType::Persistent,
}
}
}
impl From<RepositoryStorageType> for fidl::RepositoryStorageType {
fn from(storage_type: RepositoryStorageType) -> Self {
match storage_type {
RepositoryStorageType::Ephemeral => fidl::RepositoryStorageType::Ephemeral,
RepositoryStorageType::Persistent => fidl::RepositoryStorageType::Persistent,
}
}
}
fn blob_mirror_url_from_mirror_url(mirror_url: &http::Uri) -> http::Uri {
// Safe because mirror_url has a scheme and "blobs" is a valid path segment.
mirror_url.to_owned().extend_dir_with_path("blobs").unwrap()
}
fn is_default_blob_mirror_url(mirror_url: &http::Uri, blob_mirror_url: &http::Uri) -> bool {
blob_mirror_url == &blob_mirror_url_from_mirror_url(mirror_url)
}
fn normalize_blob_mirror_url<'a>(
mirror_url: &http::Uri,
blob_mirror_url: &'a http::Uri,
) -> Option<&'a http::Uri> {
if is_default_blob_mirror_url(mirror_url, blob_mirror_url) {
None
} else {
Some(blob_mirror_url)
}
}
/// Convenience wrapper type for the autogenerated FIDL `RepositoryConfig`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryConfig {
repo_url: RepoUrl,
#[serde(default = "default_root_version")]
root_version: u32,
#[serde(default = "default_root_threshold")]
root_threshold: u32,
root_keys: Vec<RepositoryKey>,
mirrors: Vec<MirrorConfig>,
#[serde(default = "default_use_local_mirror")]
use_local_mirror: bool,
#[serde(default = "default_storage_type")]
repo_storage_type: RepositoryStorageType,
}
fn default_root_version() -> u32 {
1
}
fn default_root_threshold() -> u32 {
1
}
fn default_use_local_mirror() -> bool {
false
}
fn default_storage_type() -> RepositoryStorageType {
RepositoryStorageType::Ephemeral
}
impl RepositoryConfig {
pub fn repo_url(&self) -> &RepoUrl {
&self.repo_url
}
/// Insert the provided mirror, returning any previous mirror with the same URL.
pub fn insert_mirror(&mut self, mut mirror: MirrorConfig) -> Option<MirrorConfig> {
if let Some(m) = self.mirrors.iter_mut().find(|m| m.mirror_url == mirror.mirror_url) {
mem::swap(m, &mut mirror);
Some(mirror)
} else {
self.mirrors.push(mirror);
None
}
}
/// Remove the requested mirror by url, returning the removed mirror, if it existed.
pub fn remove_mirror(&mut self, mirror_url: &http::Uri) -> Option<MirrorConfig> {
if let Some(pos) = self.mirrors.iter().position(|m| &m.mirror_url == mirror_url) {
Some(self.mirrors.remove(pos))
} else {
None
}
}
/// Returns a slice of all mirrors.
pub fn mirrors(&self) -> &[MirrorConfig] {
&self.mirrors
}
/// Returns the initial trusted root version.
pub fn root_version(&self) -> u32 {
self.root_version
}
/// Returns the threshold of root keys needed to sign the initial root metadata before it is
/// considered trusted.
pub fn root_threshold(&self) -> u32 {
self.root_threshold
}
/// Returns a slice of all root keys.
pub fn root_keys(&self) -> &[RepositoryKey] {
&self.root_keys
}
pub fn use_local_mirror(&self) -> bool {
self.use_local_mirror
}
/// Returns the repository storage type.
pub fn repo_storage_type(&self) -> &RepositoryStorageType {
&self.repo_storage_type
}
}
impl TryFrom<fidl::RepositoryConfig> for RepositoryConfig {
type Error = RepositoryParseError;
fn try_from(other: fidl::RepositoryConfig) -> Result<Self, RepositoryParseError> {
let repo_url: RepoUrl = other
.repo_url
.ok_or(RepositoryParseError::RepoUrlMissing)?
.parse()
.map_err(|err| RepositoryParseError::InvalidRepoUrl(err))?;
let root_version = if let Some(root_version) = other.root_version {
if root_version < 1 {
return Err(RepositoryParseError::InvalidRootVersion(root_version));
}
root_version
} else {
1
};
let root_threshold = if let Some(root_threshold) = other.root_threshold {
if root_threshold < 1 {
return Err(RepositoryParseError::InvalidRootThreshold(root_threshold));
}
root_threshold
} else {
1
};
let storage_type =
other.storage_type.map(|r| r.into()).unwrap_or(RepositoryStorageType::Ephemeral);
Ok(Self {
repo_url: repo_url,
root_version: root_version,
root_threshold: root_threshold,
root_keys: other
.root_keys
.unwrap_or(vec![])
.into_iter()
.map(RepositoryKey::try_from)
.collect::<Result<_, _>>()?,
mirrors: other
.mirrors
.unwrap_or(vec![])
.into_iter()
.map(MirrorConfig::try_from)
.collect::<Result<_, _>>()?,
use_local_mirror: other.use_local_mirror.unwrap_or(false),
repo_storage_type: storage_type.into(),
})
}
}
impl From<RepositoryConfig> for fidl::RepositoryConfig {
fn from(config: RepositoryConfig) -> Self {
Self {
repo_url: Some(config.repo_url.to_string()),
root_version: Some(config.root_version),
root_threshold: Some(config.root_threshold),
root_keys: Some(config.root_keys.into_iter().map(RepositoryKey::into).collect()),
mirrors: Some(config.mirrors.into_iter().map(MirrorConfig::into).collect()),
use_local_mirror: Some(config.use_local_mirror),
storage_type: Some(RepositoryStorageType::into(config.repo_storage_type)),
..Self::EMPTY
}
}
}
impl From<RepositoryConfig> for RepositoryConfigBuilder {
fn from(config: RepositoryConfig) -> Self {
Self { config }
}
}
/// Convenience wrapper for generating [RepositoryConfig] values.
#[derive(Clone, Debug)]
pub struct RepositoryConfigBuilder {
config: RepositoryConfig,
}
impl RepositoryConfigBuilder {
pub fn new(repo_url: RepoUrl) -> Self {
RepositoryConfigBuilder {
config: RepositoryConfig {
repo_url,
root_version: 1,
root_threshold: 1,
root_keys: vec![],
mirrors: vec![],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Ephemeral,
},
}
}
pub fn repo_url(mut self, repo_url: RepoUrl) -> Self {
self.config.repo_url = repo_url;
self
}
pub fn root_version(mut self, root_version: u32) -> Self {
self.config.root_version = root_version;
self
}
pub fn root_threshold(mut self, root_threshold: u32) -> Self {
self.config.root_threshold = root_threshold;
self
}
pub fn add_root_key(mut self, key: RepositoryKey) -> Self {
self.config.root_keys.push(key);
self
}
pub fn add_mirror(mut self, mirror: impl Into<MirrorConfig>) -> Self {
self.config.mirrors.push(mirror.into());
self
}
pub fn use_local_mirror(mut self, use_local_mirror: bool) -> Self {
self.config.use_local_mirror = use_local_mirror;
self
}
pub fn repo_storage_type(mut self, repo_storage_type: RepositoryStorageType) -> Self {
self.config.repo_storage_type = repo_storage_type;
self
}
pub fn build(self) -> RepositoryConfig {
self.config
}
}
impl From<RepositoryConfigBuilder> for RepositoryConfig {
fn from(builder: RepositoryConfigBuilder) -> Self {
builder.build()
}
}
/// Wraper for serializing repository configs to the on-disk JSON format.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "version", content = "content", deny_unknown_fields)]
pub enum RepositoryConfigs {
#[serde(rename = "1")]
Version1(Vec<RepositoryConfig>),
}
impl TryFrom<fidl::RepositoryKeyConfig> for RepositoryKey {
type Error = RepositoryParseError;
fn try_from(id: fidl::RepositoryKeyConfig) -> Result<Self, RepositoryParseError> {
match id {
fidl::RepositoryKeyConfig::Ed25519Key(key) => Ok(RepositoryKey::Ed25519(key)),
_ => Err(RepositoryParseError::UnsupportedKeyType),
}
}
}
impl From<RepositoryKey> for fidl::RepositoryKeyConfig {
fn from(key: RepositoryKey) -> Self {
match key {
RepositoryKey::Ed25519(key) => fidl::RepositoryKeyConfig::Ed25519Key(key),
}
}
}
impl fmt::Debug for RepositoryKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let RepositoryKey::Ed25519(ref value) = self;
f.debug_tuple("Ed25519").field(&hex::encode(value)).finish()
}
}
#[derive(Debug, PartialEq, Clone)]
/// Convenience wrapper type for the autogenerated FIDL `RepositoryUrl`.
pub struct RepositoryUrl {
url: RepoUrl,
}
impl RepositoryUrl {
pub fn url(&self) -> &RepoUrl {
&self.url
}
}
impl From<RepoUrl> for RepositoryUrl {
fn from(url: RepoUrl) -> Self {
Self { url }
}
}
impl TryFrom<&fidl::RepositoryUrl> for RepositoryUrl {
type Error = RepositoryUrlParseError;
fn try_from(other: &fidl::RepositoryUrl) -> Result<Self, RepositoryUrlParseError> {
Ok(Self { url: other.url.parse().map_err(RepositoryUrlParseError::InvalidRepoUrl)? })
}
}
impl From<RepositoryUrl> for fidl::RepositoryUrl {
fn from(url: RepositoryUrl) -> Self {
Self { url: url.url.to_string() }
}
}
#[cfg(test)]
mod tests {
use {
super::*, assert_matches::assert_matches, proptest::prelude::*, serde_json::json,
std::convert::TryInto,
};
fn verify_json_serde<T>(expected_value: T, expected_json: serde_json::Value)
where
T: PartialEq + std::fmt::Debug,
T: serde::Serialize,
for<'de> T: serde::Deserialize<'de>,
{
let actual_json = serde_json::to_value(&expected_value).expect("value to serialize");
assert_eq!(actual_json, expected_json);
let actual_value = serde_json::from_value::<T>(actual_json).expect("json to deserialize");
assert_eq!(actual_value, expected_value);
}
#[test]
fn test_repository_key_serde() {
verify_json_serde(
RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),
json!({
"type": "ed25519",
"value": "f10f1003",
}),
);
verify_json_serde(
RepositoryKey::Ed25519(vec![0, 1, 2, 3]),
json!({
"type": "ed25519",
"value": "00010203",
}),
);
}
#[test]
fn test_repository_key_deserialize_with_bad_type() {
let json = r#"{"type":"bogus","value":"00010203"}"#;
let result = serde_json::from_str::<RepositoryKey>(json);
let error_message = result.unwrap_err().to_string();
assert!(
error_message.contains("unknown variant `bogus`"),
r#"Error message did not contain "unknown variant `bogus`", was "{}""#,
error_message
);
}
#[test]
fn test_repository_key_into_fidl() {
let key = RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]);
let as_fidl: fidl::RepositoryKeyConfig = key.into();
assert_eq!(as_fidl, fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3]))
}
#[test]
fn test_repository_key_from_fidl() {
let as_fidl = fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3]);
assert_matches!(
RepositoryKey::try_from(as_fidl),
Ok(RepositoryKey::Ed25519(v)) if v == vec![0xf1, 15, 16, 3]
);
}
#[test]
fn test_repository_key_from_fidl_with_bad_type() {
let as_fidl = fidl::RepositoryKeyConfig::unknown(999, Default::default());
assert_matches!(
RepositoryKey::try_from(as_fidl),
Err(RepositoryParseError::UnsupportedKeyType)
);
}
#[test]
fn test_repository_key_into_from_fidl_roundtrip() {
let key = RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]);
let as_fidl: fidl::RepositoryKeyConfig = key.clone().into();
assert_eq!(RepositoryKey::try_from(as_fidl).unwrap(), key,)
}
#[test]
fn test_mirror_config_serde() {
verify_json_serde(
MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap())
.unwrap()
.build(),
json!({
"mirror_url": "http://example.com/",
"subscribe": false,
}),
);
verify_json_serde(
MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
.unwrap()
.blob_mirror_url("http://example.com/subdir".parse::<Uri>().unwrap())
.unwrap()
.build(),
json!({
"mirror_url": "http://example.com/",
"blob_mirror_url": "http://example.com/subdir",
"subscribe": false,
}),
);
verify_json_serde(
MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
.unwrap()
.blob_mirror_url("http://example.com/blobs".parse::<Uri>().unwrap())
.unwrap()
.build(),
json!({
"mirror_url": "http://example.com/",
"subscribe": false,
}),
);
verify_json_serde(
MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
.unwrap()
.subscribe(true)
.build(),
json!({
"mirror_url": "http://example.com/",
"subscribe": true,
}),
);
{
// A mirror config with a previously-accepted "blob_key" still parses.
let json = json!({
"mirror_url": "http://example.com/",
"blob_key": {
"type": "aes",
"value": "00010203",
},
"subscribe": false,
});
assert_eq!(
serde_json::from_value::<MirrorConfig>(json).expect("json to deserialize"),
MirrorConfigBuilder::new("http://example.com".parse::<Uri>().unwrap())
.unwrap()
.build()
);
}
}
#[test]
fn test_mirror_config_deserialize_rejects_urls_without_schemes() {
let j = json!({
"mirror_url": "example.com",
"subscribe": false,
});
assert_matches!(
serde_json::from_value::<MirrorConfig>(j),
Err(e) if e.to_string().starts_with("mirror_url must have a scheme")
);
let j = json!({
"mirror_url": "https://example.com",
"subscribe": false,
"blob_mirror_url": Some("example.com")
});
assert_matches!(
serde_json::from_value::<MirrorConfig>(j),
Err(e) if e.to_string().starts_with("blob_mirror_url must have a scheme")
);
}
#[test]
fn test_mirror_config_into_fidl() {
let config = MirrorConfig {
mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/subdir/blobs".parse::<Uri>().unwrap(),
};
let as_fidl: fidl::MirrorConfig = config.into();
assert_eq!(
as_fidl,
fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo".into()),
subscribe: Some(true),
blob_mirror_url: Some("http://example.com/tuf/repo/subdir/blobs".into()),
..fidl::MirrorConfig::EMPTY
}
);
}
#[test]
fn test_mirror_config_into_fidl_normalizes_blob_mirror_url() {
let config = MirrorConfig {
mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
subscribe: false,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
};
let as_fidl: fidl::MirrorConfig = config.into();
assert_eq!(
as_fidl,
fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo".into()),
subscribe: Some(false),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
}
);
}
#[test]
fn test_mirror_config_from_fidl() {
let as_fidl = fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo".into()),
subscribe: Some(true),
blob_mirror_url: Some("http://example.com/tuf/repo/subdir/blobs".into()),
..fidl::MirrorConfig::EMPTY
};
assert_matches!(
MirrorConfig::try_from(as_fidl),
Ok(mirror_config) if mirror_config == MirrorConfig {
mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/subdir/blobs".parse::<Uri>().unwrap(),
}
);
}
#[test]
fn test_mirror_config_from_fidl_rejects_urls_without_schemes() {
let as_fidl = fidl::MirrorConfig {
mirror_url: Some("example.com".into()),
subscribe: Some(false),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
};
assert_matches!(
MirrorConfig::try_from(as_fidl),
Err(RepositoryParseError::MirrorConfig(MirrorConfigError::MirrorUrlMissingScheme))
);
let as_fidl = fidl::MirrorConfig {
mirror_url: Some("https://example.com".into()),
subscribe: Some(false),
blob_mirror_url: Some("example.com".into()),
..fidl::MirrorConfig::EMPTY
};
assert_matches!(
MirrorConfig::try_from(as_fidl),
Err(RepositoryParseError::MirrorConfig(MirrorConfigError::BlobMirrorUrlMissingScheme))
);
}
#[test]
fn test_mirror_config_from_fidl_populates_blob_mirror_url() {
let as_fidl = fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo/".into()),
subscribe: Some(false),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
};
assert_matches!(
MirrorConfig::try_from(as_fidl),
Ok(mirror_config) if mirror_config == MirrorConfig {
mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
subscribe: false,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
}
);
}
prop_compose! {
fn uri_with_adversarial_path()(path in "[p/]{0,6}") -> http::Uri
{
let mut parts = http::uri::Parts::default();
parts.scheme = Some(http::uri::Scheme::HTTP);
parts.authority = Some(http::uri::Authority::from_static("example.com"));
parts.path_and_query = Some(path.parse().unwrap());
http::Uri::from_parts(parts).unwrap()
}
}
proptest! {
#[test]
fn blob_mirror_url_from_mirror_url_produces_default_blob_mirror_urls(
mirror_url in uri_with_adversarial_path()
) {
let blob_mirror_url = blob_mirror_url_from_mirror_url(&mirror_url);
prop_assert!(is_default_blob_mirror_url(&mirror_url, &blob_mirror_url));
}
#[test]
fn normalize_blob_mirror_url_detects_default_blob_mirror_url(
mirror_url in uri_with_adversarial_path()
) {
let blob_mirror_url = blob_mirror_url_from_mirror_url(&mirror_url);
prop_assert_eq!(normalize_blob_mirror_url(&mirror_url, &blob_mirror_url), None);
// also, swapped parameters should never return None
prop_assert_ne!(normalize_blob_mirror_url(&blob_mirror_url, &mirror_url), None);
}
}
#[test]
fn test_mirror_config_into_from_fidl_roundtrip() {
let config = MirrorConfig {
mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
};
let as_fidl: fidl::MirrorConfig = config.clone().into();
assert_eq!(MirrorConfig::try_from(as_fidl).unwrap(), config);
}
#[test]
fn test_mirror_config_builder() {
let builder =
MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap()).unwrap();
assert_eq!(
builder.clone().build(),
MirrorConfig {
mirror_url: "http://example.com/".parse::<Uri>().unwrap(),
subscribe: false,
blob_mirror_url: "http://example.com/blobs".parse::<Uri>().unwrap(),
}
);
assert_eq!(
builder
.clone()
.blob_mirror_url("http://example.com/a/b".parse::<Uri>().unwrap())
.unwrap()
.build(),
MirrorConfig {
mirror_url: "http://example.com/".parse::<Uri>().unwrap(),
subscribe: false,
blob_mirror_url: "http://example.com/a/b".parse::<Uri>().unwrap(),
}
);
assert_eq!(
builder.clone().mirror_url("http://127.0.0.1".parse::<Uri>().unwrap()).unwrap().build(),
MirrorConfig {
mirror_url: "http://127.0.0.1".parse::<Uri>().unwrap(),
subscribe: false,
blob_mirror_url: "http://example.com/blobs".parse::<Uri>().unwrap(),
}
);
assert_eq!(
builder.subscribe(true).build(),
MirrorConfig {
mirror_url: "http://example.com".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/blobs".parse::<Uri>().unwrap(),
}
);
}
#[test]
fn test_mirror_config_builder_rejects_urls_without_schemes() {
assert_matches!(
MirrorConfigBuilder::new("example.com".parse::<Uri>().unwrap()),
Err(MirrorConfigError::MirrorUrlMissingScheme)
);
let builder =
MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap()).unwrap();
assert_matches!(
builder.mirror_url("example.com".parse::<Uri>().unwrap()),
Err((_, MirrorConfigError::MirrorUrlMissingScheme))
);
let builder =
MirrorConfigBuilder::new("http://example.com/".parse::<Uri>().unwrap()).unwrap();
assert_matches!(
builder.blob_mirror_url("example.com".parse::<Uri>().unwrap()),
Err((_, MirrorConfigError::BlobMirrorUrlMissingScheme))
);
}
#[test]
fn test_mirror_config_bad_uri() {
let as_fidl = fidl::MirrorConfig {
mirror_url: None,
subscribe: Some(false),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
};
assert_matches!(
MirrorConfig::try_from(as_fidl),
Err(RepositoryParseError::MirrorUrlMissing)
);
}
#[test]
fn test_repository_config_builder() {
let repo_url = RepoUrl::parse("fuchsia-pkg://fuchsia.com").unwrap();
let builder = RepositoryConfigBuilder::new(repo_url.clone());
assert_eq!(
builder.clone().build(),
RepositoryConfig {
repo_url: repo_url.clone(),
root_version: 1,
root_threshold: 1,
root_keys: vec![],
mirrors: vec![],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Ephemeral,
}
);
assert_eq!(
builder.clone().repo_storage_type(RepositoryStorageType::Persistent).build(),
RepositoryConfig {
repo_url: repo_url.clone(),
root_version: 1,
root_threshold: 1,
root_keys: vec![],
mirrors: vec![],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Persistent,
}
);
}
#[test]
fn test_repository_config_into_fidl() {
let config = RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 2,
root_threshold: 2,
root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3])],
mirrors: vec![MirrorConfig {
mirror_url: "http://example.com/tuf/repo".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
}],
use_local_mirror: true,
repo_storage_type: RepositoryStorageType::Ephemeral,
};
let as_fidl: fidl::RepositoryConfig = config.into();
assert_eq!(
as_fidl,
fidl::RepositoryConfig {
repo_url: Some("fuchsia-pkg://fuchsia.com".try_into().unwrap()),
root_version: Some(2),
root_threshold: Some(2),
root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
mirrors: Some(vec![fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo".into()),
subscribe: Some(true),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
}]),
use_local_mirror: Some(true),
storage_type: Some(fidl::RepositoryStorageType::Ephemeral),
..fidl::RepositoryConfig::EMPTY
}
);
}
#[test]
fn test_repository_config_from_fidl_without_storage_type() {
let as_fidl = fidl::RepositoryConfig {
repo_url: Some("fuchsia-pkg://fuchsia.com".try_into().unwrap()),
root_version: Some(1),
root_threshold: Some(1),
root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
mirrors: Some(vec![fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo/".into()),
subscribe: Some(true),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
}]),
use_local_mirror: None,
storage_type: None,
..fidl::RepositoryConfig::EMPTY
};
assert_matches!(
RepositoryConfig::try_from(as_fidl),
Ok(repository_config) if repository_config == RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 1,
root_threshold: 1,
root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
mirrors: vec![MirrorConfig {
mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
},],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Ephemeral,
}
);
}
#[test]
fn test_repository_config_from_fidl_with_storage_type() {
let as_fidl = fidl::RepositoryConfig {
repo_url: Some("fuchsia-pkg://fuchsia.com".try_into().unwrap()),
root_version: Some(1),
root_threshold: Some(1),
root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
mirrors: Some(vec![fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo/".into()),
subscribe: Some(true),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
}]),
use_local_mirror: None,
storage_type: Some(fidl::RepositoryStorageType::Persistent),
..fidl::RepositoryConfig::EMPTY
};
assert_matches!(
RepositoryConfig::try_from(as_fidl),
Ok(repository_config) if repository_config == RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 1,
root_threshold: 1,
root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
mirrors: vec![MirrorConfig {
mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
},],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Persistent,
}
);
}
#[test]
fn test_repository_config_from_fidl_without_version_and_threshold_and_use_local_mirror() {
let as_fidl = fidl::RepositoryConfig {
repo_url: Some("fuchsia-pkg://fuchsia.com".try_into().unwrap()),
root_version: None,
root_threshold: None,
root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
mirrors: Some(vec![fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo/".into()),
subscribe: Some(true),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
}]),
use_local_mirror: None,
storage_type: None,
..fidl::RepositoryConfig::EMPTY
};
assert_matches!(
RepositoryConfig::try_from(as_fidl),
Ok(repository_config) if repository_config == RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 1,
root_threshold: 1,
root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
mirrors: vec![MirrorConfig {
mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
},],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Ephemeral,
}
);
}
#[test]
fn test_repository_config_from_fidl_with_version_and_threshold_and_use_local_mirror() {
let as_fidl = fidl::RepositoryConfig {
repo_url: Some("fuchsia-pkg://fuchsia.com".try_into().unwrap()),
root_version: Some(2),
root_threshold: Some(2),
root_keys: Some(vec![fidl::RepositoryKeyConfig::Ed25519Key(vec![0xf1, 15, 16, 3])]),
mirrors: Some(vec![fidl::MirrorConfig {
mirror_url: Some("http://example.com/tuf/repo/".into()),
subscribe: Some(true),
blob_mirror_url: None,
..fidl::MirrorConfig::EMPTY
}]),
use_local_mirror: Some(true),
storage_type: None,
..fidl::RepositoryConfig::EMPTY
};
assert_matches!(
RepositoryConfig::try_from(as_fidl),
Ok(repository_config) if repository_config == RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 2,
root_threshold: 2,
root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3]),],
mirrors: vec![MirrorConfig {
mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
},],
use_local_mirror: true,
repo_storage_type: RepositoryStorageType::Ephemeral,
}
);
}
#[test]
fn test_repository_config_from_fidl_repo_url_missing() {
let as_fidl = fidl::RepositoryConfig {
repo_url: None,
root_version: None,
root_threshold: None,
root_keys: Some(vec![]),
mirrors: Some(vec![]),
use_local_mirror: None,
storage_type: None,
..fidl::RepositoryConfig::EMPTY
};
assert_matches!(
RepositoryConfig::try_from(as_fidl),
Err(RepositoryParseError::RepoUrlMissing)
);
}
#[test]
fn test_repository_config_into_from_fidl_roundtrip() {
let config = RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 2,
root_threshold: 2,
root_keys: vec![RepositoryKey::Ed25519(vec![0xf1, 15, 16, 3])],
mirrors: vec![MirrorConfig {
mirror_url: "http://example.com/tuf/repo/".parse::<Uri>().unwrap(),
subscribe: true,
blob_mirror_url: "http://example.com/tuf/repo/blobs".parse::<Uri>().unwrap(),
}],
use_local_mirror: true,
repo_storage_type: RepositoryStorageType::Ephemeral,
};
let as_fidl: fidl::RepositoryConfig = config.clone().into();
assert_eq!(RepositoryConfig::try_from(as_fidl).unwrap(), config);
}
#[test]
fn test_repository_config_deserialize_missing_root_version_and_threshold_and_use_local_mirror()
{
let json_value = json!({
"repo_url": "fuchsia-pkg://fuchsia.com",
"root_keys": [],
"mirrors": [],
});
let actual_config: RepositoryConfig = serde_json::from_value(json_value).unwrap();
assert_eq!(
actual_config,
RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 1,
root_threshold: 1,
root_keys: vec![],
mirrors: vec![],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Ephemeral,
},
);
}
// Validate we can still deserialize old configs that have the now-removed
// "update_package_url" field.
#[test]
fn test_repository_config_deserialize_ignores_update_package_url() {
let json_value = json!({
"repo_url": "fuchsia-pkg://fuchsia.com",
"root_keys": [],
"mirrors": [],
"update_package_url": "ignored-value",
});
let actual_config: RepositoryConfig = serde_json::from_value(json_value).unwrap();
assert_eq!(
actual_config,
RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 1,
root_threshold: 1,
root_keys: vec![],
mirrors: vec![],
use_local_mirror: false,
repo_storage_type: RepositoryStorageType::Ephemeral,
},
);
}
#[test]
fn test_repository_configs_serde_simple() {
verify_json_serde(
RepositoryConfigs::Version1(vec![RepositoryConfig {
repo_url: "fuchsia-pkg://fuchsia.com".try_into().unwrap(),
root_version: 1,
root_threshold: 1,
root_keys: vec![],
mirrors: vec![],
use_local_mirror: true,
repo_storage_type: RepositoryStorageType::Ephemeral,
}]),
json!({
"version": "1",
"content": [{
"repo_url": "fuchsia-pkg://fuchsia.com",
"root_version": 1,
"root_threshold": 1,
"root_keys": [],
"mirrors": [],
"use_local_mirror": true,
"repo_storage_type": "ephemeral",
}],
}),
);
}
#[test]
fn test_repository_url_into_fidl() {
let url = RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".parse().unwrap() };
let as_fidl: fidl::RepositoryUrl = url.into();
assert_eq!(as_fidl, fidl::RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".to_owned() });
}
#[test]
fn test_repository_url_from_fidl() {
let as_fidl = fidl::RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".to_owned() };
assert_matches!(
RepositoryUrl::try_from(&as_fidl),
Ok(RepositoryUrl { url }) if url == "fuchsia-pkg://fuchsia.com".parse().unwrap()
);
}
#[test]
fn test_repository_url_from_fidl_with_bad_url() {
let as_fidl = fidl::RepositoryUrl { url: "invalid-scheme://fuchsia.com".to_owned() };
assert_matches!(
RepositoryUrl::try_from(&as_fidl),
Err(RepositoryUrlParseError::InvalidRepoUrl(
fuchsia_url::pkg_url::ParseError::InvalidScheme
))
);
}
#[test]
fn test_repository_url_into_from_fidl_roundtrip() {
let url = RepositoryUrl { url: "fuchsia-pkg://fuchsia.com".parse().unwrap() };
let as_fidl: fidl::RepositoryUrl = url.clone().into();
assert_eq!(RepositoryUrl::try_from(&as_fidl).unwrap(), url);
}
}
mod hex_serde {
use {hex, serde::Deserialize};
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = hex::encode(bytes);
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
hex::decode(value.as_bytes())
.map_err(|e| serde::de::Error::custom(format!("bad hex value: {:?}: {}", value, e)))
}
}
mod uri_serde {
use {http::Uri, serde::Deserialize};
pub fn serialize<S>(uri: &http::Uri, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = uri.to_string();
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
value
.parse::<Uri>()
.map_err(|e| serde::de::Error::custom(format!("bad uri value: {:?}: {}", value, e)))
}
}