Skip to content

Commit

Permalink
Made sure the following types also work with a "human readable" ser/de (
Browse files Browse the repository at this point in the history
#1738)

## Description

Made sure the following types also work with a "human readable" ser/de

- Hash
- HashAndFormat
- PublicKey
- Ticket

## Notes & open questions

Shall we just switch to hex for hashes so they are identical to b3sum?
Sorry, but I had to ask...

## Change checklist

- [ ] Self-review.
- [ ] Documentation updates if relevant.
- [ ] Tests if relevant.
  • Loading branch information
rklaehn authored Oct 26, 2023
1 parent c851fe1 commit b9ee93e
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 18 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions iroh-bytes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ tracing-futures = "0.2.5"
http-body = "0.4.5"
iroh-test = { path = "../iroh-test" }
proptest = "1.0.0"
serde_json = "1.0.107"
serde_test = "1.0.176"
tokio = { version = "1", features = ["macros", "test-util"] }

Expand Down
89 changes: 79 additions & 10 deletions iroh-bytes/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ impl Tag {
}

/// A hash and format pair
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct HashAndFormat {
/// The hash
pub hash: Hash,
Expand Down Expand Up @@ -165,6 +165,34 @@ impl FromStr for HashAndFormat {
}
}

impl Serialize for HashAndFormat {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(self.to_string().as_str())
} else {
(self.hash, self.format).serialize(serializer)
}
}
}

impl<'de> Deserialize<'de> for HashAndFormat {
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
} else {
let (hash, format) = <(Hash, BlobFormat)>::deserialize(deserializer)?;
Ok(Self { hash, format })
}
}
}

/// Hash type used throughout.
#[derive(PartialEq, Eq, Copy, Clone, Hash)]
pub struct Hash(blake3::Hash);
Expand Down Expand Up @@ -345,13 +373,17 @@ impl Serialize for Hash {
where
S: Serializer,
{
// Fixed-length structures, including arrays, are supported in Serde as tuples
// See: https://serde.rs/impl-serialize.html#serializing-a-tuple
let mut s = serializer.serialize_tuple(32)?;
for item in self.0.as_bytes() {
s.serialize_element(item)?;
if serializer.is_human_readable() {
serializer.serialize_str(self.to_string().as_str())
} else {
// Fixed-length structures, including arrays, are supported in Serde as tuples
// See: https://serde.rs/impl-serialize.html#serializing-a-tuple
let mut s = serializer.serialize_tuple(32)?;
for item in self.0.as_bytes() {
s.serialize_element(item)?;
}
s.end()
}
s.end()
}
}

Expand All @@ -360,7 +392,12 @@ impl<'de> Deserialize<'de> for Hash {
where
D: Deserializer<'de>,
{
deserializer.deserialize_tuple(32, HashVisitor)
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
} else {
deserializer.deserialize_tuple(32, HashVisitor)
}
}
}

Expand Down Expand Up @@ -527,11 +564,12 @@ impl NonSend {

#[cfg(test)]
mod tests {

use iroh_test::{assert_eq_hex, hexdump::parse_hexdump};

use super::*;

use serde_test::{assert_tokens, Token};
use serde_test::{assert_tokens, Configure, Token};

#[test]
fn test_display_parse_roundtrip() {
Expand Down Expand Up @@ -585,7 +623,12 @@ mod tests {
tokens.push(Token::TupleEnd);
assert_eq!(tokens.len(), 34);

assert_tokens(&hash, &tokens);
assert_tokens(&hash.compact(), &tokens);

let tokens = vec![Token::String(
"bafkr4ihkr4ld3m4gqkjf4reryxsy2s5tkbxprqkow6fin2iiyvreuzzab4",
)];
assert_tokens(&hash.readable(), &tokens);
}

#[test]
Expand All @@ -598,6 +641,16 @@ mod tests {
assert_eq!(ser.len(), 32);
}

#[test]
fn test_hash_json() {
let hash = Hash::new("hello");
let ser = serde_json::to_string(&hash).unwrap();
let de = serde_json::from_str(&ser).unwrap();
assert_eq!(hash, de);
// 59 bytes of base32 + 2 quotes
assert_eq!(ser.len(), 61);
}

#[test]
fn test_hash_and_format_parse() {
let hash = Hash::new("hello");
Expand All @@ -610,4 +663,20 @@ mod tests {
let actual = expected.to_string().parse::<HashAndFormat>().unwrap();
assert_eq!(expected, actual);
}

#[test]
fn test_hash_and_format_postcard() {
let haf = HashAndFormat::raw(Hash::new("hello"));
let ser = postcard::to_stdvec(&haf).unwrap();
let de = postcard::from_bytes(&ser).unwrap();
assert_eq!(haf, de);
}

#[test]
fn test_hash_and_format_json() {
let haf = HashAndFormat::raw(Hash::new("hello"));
let ser = serde_json::to_string(&haf).unwrap();
let de = serde_json::from_str(&ser).unwrap();
assert_eq!(haf, de);
}
}
1 change: 1 addition & 0 deletions iroh-net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ testdir = "0.8"
tokio = { version = "1", features = ["io-util", "sync", "rt", "net", "fs", "macros", "time", "test-util"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
iroh-test = { path = "../iroh-test" }
serde_json = "1.0.107"

[[bench]]
name = "key"
Expand Down
31 changes: 28 additions & 3 deletions iroh-net/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ impl Serialize for PublicKey {
where
S: serde::Serializer,
{
serializer.serialize_bytes(&self.0)
if serializer.is_human_readable() {
serializer.serialize_str(&self.to_string())
} else {
serializer.serialize_bytes(&self.0)
}
}
}

Expand All @@ -111,8 +115,13 @@ impl<'de> Deserialize<'de> for PublicKey {
where
D: serde::Deserializer<'de>,
{
let bytes: &serde_bytes::Bytes = serde::Deserialize::deserialize(deserializer)?;
Self::try_from(bytes.as_ref()).map_err(serde::de::Error::custom)
if deserializer.is_human_readable() {
let s: &str = serde::Deserialize::deserialize(deserializer)?;
Self::from_str(s).map_err(serde::de::Error::custom)
} else {
let bytes: &serde_bytes::Bytes = serde::Deserialize::deserialize(deserializer)?;
Self::try_from(bytes.as_ref()).map_err(serde::de::Error::custom)
}
}
}

Expand Down Expand Up @@ -408,6 +417,22 @@ mod tests {
assert_eq!(kp.to_bytes(), de.to_bytes());
}

#[test]
fn public_key_postcard() {
let key = PublicKey::from_bytes(&[0; 32]).unwrap();
let bytes = postcard::to_stdvec(&key).unwrap();
let key2: PublicKey = postcard::from_bytes(&bytes).unwrap();
assert_eq!(key, key2);
}

#[test]
fn public_key_json() {
let key = PublicKey::from_bytes(&[0; 32]).unwrap();
let bytes = serde_json::to_string(&key).unwrap();
let key2: PublicKey = serde_json::from_str(&bytes).unwrap();
assert_eq!(key, key2);
}

#[test]
fn test_display_from_str() {
let key = SecretKey::generate();
Expand Down
1 change: 1 addition & 0 deletions iroh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ nix = "0.26.2"
proptest = "1.2.0"
rand_chacha = "0.3.1"
regex = { version = "1.7.1", features = ["std"] }
serde_json = "1.0.107"
testdir = "0.8"
tokio = { version = "1", features = ["macros", "io-util", "rt"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Expand Down
58 changes: 53 additions & 5 deletions iroh/src/dial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub async fn dial(opts: Options) -> anyhow::Result<quinn::Connection> {
///
/// It is a single item which can be easily serialized and deserialized. The [`Display`]
/// and [`FromStr`] implementations serialize to base32.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ticket {
/// The provider to get a file from.
peer: PeerAddr,
Expand Down Expand Up @@ -172,6 +172,34 @@ impl FromStr for Ticket {
}
}

impl Serialize for Ticket {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.serialize_str(&self.to_string())
} else {
let Ticket {
peer,
format,
hash,
token,
} = self;
(peer, format, hash, token).serialize(serializer)
}
}
}

impl<'de> Deserialize<'de> for Ticket {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
} else {
let (peer, format, hash, token) = Deserialize::deserialize(deserializer)?;
Self::new(peer, hash, format, token).map_err(serde::de::Error::custom)
}
}
}

#[cfg(test)]
mod tests {
use std::net::SocketAddr;
Expand All @@ -180,20 +208,40 @@ mod tests {

use super::*;

#[test]
fn test_ticket_base32_roundtrip() {
fn make_ticket() -> Ticket {
let hash = blake3::hash(b"hi there");
let hash = Hash::from(hash);
let peer = SecretKey::generate().public();
let addr = SocketAddr::from_str("127.0.0.1:1234").unwrap();
let token = RequestToken::new(vec![1, 2, 3, 4, 5, 6]).unwrap();
let derp_region = Some(0);
let ticket = Ticket {
Ticket {
hash,
peer: PeerAddr::from_parts(peer, derp_region, vec![addr]),
token: Some(token),
format: BlobFormat::HashSeq,
};
}
}

#[test]
fn test_ticket_postcard() {
let ticket = make_ticket();
let bytes = postcard::to_stdvec(&ticket).unwrap();
let ticket2: Ticket = postcard::from_bytes(&bytes).unwrap();
assert_eq!(ticket2, ticket);
}

#[test]
fn test_ticket_json() {
let ticket = make_ticket();
let json = serde_json::to_string(&ticket).unwrap();
let ticket2: Ticket = serde_json::from_str(&json).unwrap();
assert_eq!(ticket2, ticket);
}

#[test]
fn test_ticket_base32_roundtrip() {
let ticket = make_ticket();
let base32 = ticket.to_string();
println!("Ticket: {base32}");
println!("{} bytes", base32.len());
Expand Down

0 comments on commit b9ee93e

Please sign in to comment.