blob: 81598b747e4fb1d99e371c9a0f2d531cdd4424f1 [file] [log] [blame]
// Copyright 2018 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::{
BlobIdFromSliceError, BlobIdParseError, CupMissingField, ResolutionContextError,
},
fidl_fuchsia_pkg as fidl,
proptest_derive::Arbitrary,
serde::{Deserialize, Serialize},
std::{fmt, str},
typed_builder::TypedBuilder,
};
pub(crate) const BLOB_ID_SIZE: usize = 32;
/// Convenience wrapper type for the autogenerated FIDL `BlobId`.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Arbitrary)]
pub struct BlobId(#[serde(with = "hex_serde")] [u8; BLOB_ID_SIZE]);
impl BlobId {
/// Parse a `BlobId` from a string containing 32 lower-case hex encoded bytes.
///
/// # Examples
/// ```
/// let s = "00112233445566778899aabbccddeeffffeeddccbbaa99887766554433221100";
/// assert_eq!(
/// BlobId::parse(s),
/// s.parse()
/// );
/// ```
pub fn parse(s: &str) -> Result<Self, BlobIdParseError> {
s.parse()
}
/// Obtain a slice of bytes representing the `BlobId`.
pub fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
}
impl str::FromStr for BlobId {
type Err = BlobIdParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = hex::decode(s)?;
if bytes.len() != BLOB_ID_SIZE {
return Err(BlobIdParseError::InvalidLength(bytes.len()));
}
if s.chars().any(|c| c.is_uppercase()) {
return Err(BlobIdParseError::CannotContainUppercase);
}
let mut res: [u8; BLOB_ID_SIZE] = [0; BLOB_ID_SIZE];
res.copy_from_slice(&bytes[..]);
Ok(Self(res))
}
}
impl From<[u8; BLOB_ID_SIZE]> for BlobId {
fn from(bytes: [u8; BLOB_ID_SIZE]) -> Self {
Self(bytes)
}
}
impl From<fidl::BlobId> for BlobId {
fn from(id: fidl::BlobId) -> Self {
Self(id.merkle_root)
}
}
impl From<BlobId> for fidl::BlobId {
fn from(id: BlobId) -> Self {
Self { merkle_root: id.0 }
}
}
impl From<fuchsia_hash::Hash> for BlobId {
fn from(hash: fuchsia_hash::Hash) -> Self {
Self(hash.into())
}
}
impl TryFrom<&[u8]> for BlobId {
type Error = BlobIdFromSliceError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
Ok(Self(bytes.try_into().map_err(|_| Self::Error::InvalidLength(bytes.len()))?))
}
}
impl From<BlobId> for fuchsia_hash::Hash {
fn from(id: BlobId) -> Self {
id.0.into()
}
}
impl fmt::Display for BlobId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&hex::encode(self.0))
}
}
impl fmt::Debug for BlobId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_string())
}
}
/// Convenience wrapper type for the autogenerated FIDL `BlobInfo`.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Arbitrary)]
pub struct BlobInfo {
pub blob_id: BlobId,
pub length: u64,
}
impl BlobInfo {
pub fn new(blob_id: BlobId, length: u64) -> Self {
BlobInfo { blob_id: blob_id, length: length }
}
}
impl From<fidl::BlobInfo> for BlobInfo {
fn from(info: fidl::BlobInfo) -> Self {
BlobInfo { blob_id: info.blob_id.into(), length: info.length }
}
}
impl From<BlobInfo> for fidl::BlobInfo {
fn from(info: BlobInfo) -> Self {
Self { blob_id: info.blob_id.into(), length: info.length }
}
}
/// Convenience wrapper type for the autogenerated FIDL `ResolutionContext`.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ResolutionContext {
blob_id: Option<BlobId>,
}
impl ResolutionContext {
/// Creates an empty resolution context.
pub fn new() -> Self {
Self { blob_id: None }
}
/// The ResolutionContext's optional blob id.
pub fn blob_id(&self) -> Option<&BlobId> {
self.blob_id.as_ref()
}
}
impl From<BlobId> for ResolutionContext {
fn from(blob_id: BlobId) -> Self {
Self { blob_id: Some(blob_id) }
}
}
impl From<fuchsia_hash::Hash> for ResolutionContext {
fn from(hash: fuchsia_hash::Hash) -> Self {
Self { blob_id: Some(hash.into()) }
}
}
impl TryFrom<&[u8]> for ResolutionContext {
type Error = ResolutionContextError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
if bytes.is_empty() {
Ok(Self { blob_id: None })
} else {
Ok(Self { blob_id: Some(bytes.try_into().map_err(Self::Error::InvalidBlobId)?) })
}
}
}
impl TryFrom<&fidl::ResolutionContext> for ResolutionContext {
type Error = ResolutionContextError;
fn try_from(context: &fidl::ResolutionContext) -> Result<Self, Self::Error> {
Self::try_from(context.bytes.as_slice())
}
}
impl From<ResolutionContext> for Vec<u8> {
fn from(context: ResolutionContext) -> Self {
match context.blob_id {
Some(blob_id) => blob_id.as_bytes().to_vec(),
None => vec![],
}
}
}
impl From<ResolutionContext> for fidl::ResolutionContext {
fn from(context: ResolutionContext) -> Self {
Self { bytes: context.into() }
}
}
mod hex_serde {
use {super::BLOB_ID_SIZE, hex::FromHex, serde::Deserialize};
pub fn serialize<S>(bytes: &[u8; BLOB_ID_SIZE], 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<[u8; BLOB_ID_SIZE], D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
<[u8; BLOB_ID_SIZE]>::from_hex(value.as_bytes())
.map_err(|e| serde::de::Error::custom(format!("bad hex value: {:?}: {}", value, e)))
}
}
#[derive(Clone, Debug, Eq, PartialEq, TypedBuilder)]
pub struct CupData {
#[builder(default, setter(into))]
pub request: Vec<u8>,
#[builder(default, setter(into))]
pub key_id: u64,
#[builder(default, setter(into))]
pub nonce: [u8; 32],
#[builder(default, setter(into))]
pub response: Vec<u8>,
#[builder(default, setter(into))]
pub signature: Vec<u8>,
}
impl From<CupData> for fidl::CupData {
fn from(c: CupData) -> Self {
fidl::CupData {
request: Some(c.request),
key_id: Some(c.key_id),
nonce: Some(c.nonce),
response: Some(c.response),
signature: Some(c.signature),
..Default::default()
}
}
}
impl TryFrom<fidl::CupData> for CupData {
type Error = CupMissingField;
fn try_from(c: fidl::CupData) -> Result<Self, Self::Error> {
Ok(CupData::builder()
.request(c.request.ok_or(CupMissingField::Request)?)
.response(c.response.ok_or(CupMissingField::Response)?)
.key_id(c.key_id.ok_or(CupMissingField::KeyId)?)
.nonce(c.nonce.ok_or(CupMissingField::Nonce)?)
.signature(c.signature.ok_or(CupMissingField::Signature)?)
.build())
}
}
#[cfg(test)]
mod test_blob_id {
use {super::*, assert_matches::assert_matches, proptest::prelude::*};
prop_compose! {
fn invalid_hex_char()(c in "[[:ascii:]&&[^0-9a-fA-F]]") -> char {
assert_eq!(c.len(), 1);
c.chars().next().unwrap()
}
}
proptest! {
#![proptest_config(ProptestConfig{
// Disable persistence to avoid the warning for not running in the
// source code directory (since we're running on a Fuchsia target)
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn parse_is_inverse_of_display(ref id in "[0-9a-f]{64}") {
prop_assert_eq!(
id,
&format!("{}", id.parse::<BlobId>().unwrap())
);
}
#[test]
fn parse_is_inverse_of_debug(ref id in "[0-9a-f]{64}") {
prop_assert_eq!(
id,
&format!("{:?}", id.parse::<BlobId>().unwrap())
);
}
#[test]
fn parse_rejects_uppercase(ref id in "[0-9A-F]{64}") {
prop_assert_eq!(
id.parse::<BlobId>(),
Err(BlobIdParseError::CannotContainUppercase)
);
}
#[test]
fn parse_rejects_unexpected_characters(mut id in "[0-9a-f]{64}", c in invalid_hex_char(), index in 0usize..63) {
id.remove(index);
id.insert(index, c);
prop_assert_eq!(
id.parse::<BlobId>(),
Err(BlobIdParseError::FromHexError(
hex::FromHexError::InvalidHexCharacter { c: c, index: index }
))
);
}
#[test]
fn parse_expects_even_sized_strings(ref id in "[0-9a-f]([0-9a-f]{2})*") {
prop_assert_eq!(
id.parse::<BlobId>(),
Err(BlobIdParseError::FromHexError(hex::FromHexError::OddLength))
);
}
#[test]
fn parse_expects_precise_count_of_bytes(ref id in "([0-9a-f]{2})*") {
prop_assume!(id.len() != BLOB_ID_SIZE * 2);
prop_assert_eq!(
id.parse::<BlobId>(),
Err(BlobIdParseError::InvalidLength(id.len() / 2))
);
}
#[test]
fn fidl_conversions_are_inverses(id: BlobId) {
let temp : fidl::BlobId = id.into();
prop_assert_eq!(
id,
BlobId::from(temp)
);
}
}
#[test]
fn try_from_slice_rejects_invalid_length() {
assert_matches!(
BlobId::try_from([0u8; BLOB_ID_SIZE - 1].as_slice()),
Err(BlobIdFromSliceError::InvalidLength(31))
);
assert_matches!(
BlobId::try_from([0u8; BLOB_ID_SIZE + 1].as_slice()),
Err(BlobIdFromSliceError::InvalidLength(33))
);
}
#[test]
fn try_from_slice_succeeds() {
let bytes = [1u8; 32];
assert_eq!(BlobId::try_from(bytes.as_slice()).unwrap().as_bytes(), bytes.as_slice());
}
}
#[cfg(test)]
mod test_resolution_context {
use {super::*, assert_matches::assert_matches};
#[test]
fn try_from_slice_succeeds() {
assert_eq!(
ResolutionContext::try_from([].as_slice()).unwrap(),
ResolutionContext { blob_id: None }
);
assert_eq!(
ResolutionContext::try_from([1u8; 32].as_slice()).unwrap(),
ResolutionContext { blob_id: Some([1u8; 32].into()) }
);
}
#[test]
fn try_from_slice_fails() {
assert_matches!(
ResolutionContext::try_from([1u8].as_slice()),
Err(ResolutionContextError::InvalidBlobId(_))
);
}
#[test]
fn into_vec() {
assert_eq!(Vec::from(ResolutionContext::new()), Vec::<u8>::new());
assert_eq!(Vec::from(ResolutionContext::from(BlobId::from([1u8; 32]))), vec![1u8; 32]);
}
#[test]
fn try_from_fidl_succeeds() {
assert_eq!(
ResolutionContext::try_from(&fidl::ResolutionContext { bytes: vec![] }).unwrap(),
ResolutionContext { blob_id: None }
);
assert_eq!(
ResolutionContext::try_from(&fidl::ResolutionContext { bytes: vec![1u8; 32] }).unwrap(),
ResolutionContext { blob_id: Some([1u8; 32].into()) }
);
}
#[test]
fn try_from_fidl_fails() {
assert_matches!(
ResolutionContext::try_from(&fidl::ResolutionContext { bytes: vec![1u8; 1] }),
Err(ResolutionContextError::InvalidBlobId(_))
);
}
#[test]
fn into_fidl() {
assert_eq!(
fidl::ResolutionContext::from(ResolutionContext::new()),
fidl::ResolutionContext { bytes: vec![] }
);
assert_eq!(
fidl::ResolutionContext::from(ResolutionContext::from(BlobId::from([1u8; 32]))),
fidl::ResolutionContext { bytes: vec![1u8; 32] }
);
}
}