From 9d54992cdf519d66524fd9fdbedb53780133c183 Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Wed, 3 Aug 2022 13:40:12 +0200 Subject: [PATCH] feat: serialize to string Serialize `XorName` to a string with hex representation, only if serializing to a human readable format like with `serde_json` or `toml`. This is guarded behind a feature as it adds a dependency to the `hex` crate. --- .github/workflows/pr.yml | 14 +-- Cargo.toml | 13 +++ README.md | 32 ++++++ src/lib.rs | 9 +- src/prefix.rs | 11 +- src/serialize.rs | 210 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 270 insertions(+), 19 deletions(-) create mode 100644 src/serialize.rs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0fa37c6c..bf5db185 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -40,18 +40,6 @@ jobs: # Run Clippy. - name: Clippy checks run: cargo clippy --all-targets --all-features - - check_pr_size: - if: "!startsWith(github.event.pull_request.title, 'Automated version bump')" - name: Check PR size doesn't break set limit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: '0' - - uses: maidsafe/pr_size_checker@v2 - with: - max_lines_changed: 200 coverage: if: "!startsWith(github.event.pull_request.title, 'Automated version bump')" @@ -151,7 +139,7 @@ jobs: # Run the tests. - name: Cargo test - run: cargo test --release + run: cargo test --all-features --release # Test publish using --dry-run. test-publish: diff --git a/Cargo.toml b/Cargo.toml index 6f07e6db..b22309b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,11 @@ license = "MIT OR BSD-3-Clause" readme = "README.md" repository = "https://github.com/maidsafe/xor_name" +[features] +default = ["serialize-hex"] +# Serialize `XorName` into a hex string if serializing into human-readable format +serialize-hex = ["hex", "serde_test"] + [dependencies] rand_core = "0.6.3" @@ -26,6 +31,14 @@ rand_core = "0.6.3" default-features = false features = [ "derive" ] + [dependencies.serde_test] + version = "1" + optional = true + + [dependencies.hex] + version = "0.4" + optional = true + [dev-dependencies] bincode = "1.2.1" diff --git a/README.md b/README.md index 6ba34b4c..cbb08ee4 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,38 @@ XorName is an array that is useful for calculations in DHT | [MaidSafe website](http://maidsafe.net) | [SAFE Network Forum](https://safenetforum.org/) | |:-------:|:-------:| +## Serialization + +`XorName` and `Prefix` can be serialized into a human-readable hex string, instead of as a `u8` array. To enable this, activate the `serialize-hex` feature. This also allows for these structures to be serialised when used as a key in a map like `HashMap`, because most formats only allow keys to be strings, instead of more complex types. + +A struct like this: +```rust +#[derive(Serialize, Deserialize)] +struct MyStruct { + prefix: Prefix, + xor_name: XorName, +} +``` + +Will yield this JSON +```json +{ + "prefix": "8a817b6d791f4b00000000000000000000000000000000000000000000000000/56", + "xor_name": "8a817b6d791f4bae4117ac7ae15a88cd2c62fba0b040972ce885f1a47625dea1" +} +``` + +instead of +```json +{ + "prefix": { + "bit_count": 56, + "name": [141,199,202,57,183,222,153,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + }, + "xor_name": [141,199,202,57,183,222,153,14,185,67,253,100,133,71,118,221,133,170,130,195,58,66,105,105,60,87,179,110,7,73,237,143] +} +``` + ## License This SAFE Network library is dual-licensed under the Modified BSD ([LICENSE-BSD](LICENSE-BSD) https://opensource.org/licenses/BSD-3-Clause) or the MIT license ([LICENSE-MIT](LICENSE-MIT) https://opensource.org/licenses/MIT) at your option. diff --git a/src/lib.rs b/src/lib.rs index a297aa0c..1279f5bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,7 +57,6 @@ use core::{cmp::Ordering, fmt, ops}; pub use prefix::Prefix; pub use rand; use rand::distributions::{Distribution, Standard}; -use serde::{Deserialize, Serialize}; use tiny_keccak::{Hasher, Sha3}; /// Creates XorName with the given leading bytes and the rest filled with zeroes. @@ -91,6 +90,8 @@ macro_rules! format { } mod prefix; +#[cfg(feature = "serialize-hex")] +mod serialize; /// Constant byte length of `XorName`. pub const XOR_NAME_LEN: usize = 32; @@ -103,7 +104,11 @@ pub const XOR_NAME_LEN: usize = 32; /// i. e. the points with IDs `x` and `y` are considered to have distance `x xor y`. /// /// [1]: https://en.wikipedia.org/wiki/Kademlia#System_details -#[derive(Eq, Copy, Clone, Default, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +#[derive(Eq, Copy, Clone, Default, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr( + not(feature = "serialize-hex"), + derive(serde::Serialize, serde::Deserialize) +)] pub struct XorName(pub [u8; XOR_NAME_LEN]); impl XorName { diff --git a/src/prefix.rs b/src/prefix.rs index 8f3bec5c..5940cc46 100644 --- a/src/prefix.rs +++ b/src/prefix.rs @@ -15,14 +15,17 @@ use core::{ ops::RangeInclusive, str::FromStr, }; -use serde::{Deserialize, Serialize}; /// A section prefix, i.e. a sequence of bits specifying the part of the network's name space /// consisting of all names that start with this sequence. -#[derive(Clone, Copy, Default, Eq, Deserialize, Serialize)] +#[derive(Clone, Copy, Default, Eq)] +#[cfg_attr( + not(feature = "serialize-hex"), + derive(serde::Serialize, serde::Deserialize) +)] pub struct Prefix { - bit_count: u16, - name: XorName, + pub(crate) bit_count: u16, + pub(crate) name: XorName, } impl Prefix { diff --git a/src/serialize.rs b/src/serialize.rs new file mode 100644 index 00000000..932189f2 --- /dev/null +++ b/src/serialize.rs @@ -0,0 +1,210 @@ +use crate::{Prefix, XorName}; +use serde::{ + de::{self, Visitor}, + ser::SerializeStruct, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::fmt; + +impl Serialize for XorName { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Return string with hexadecimal representation + if serializer.is_human_readable() { + return serializer.serialize_str(&hex::encode(self.0)); + } + + // Default serialization. + serializer.serialize_newtype_struct("XorName", &self.0) + } +} + +impl<'de> Deserialize<'de> for XorName { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + struct XorNameHexStrVisitor; + impl<'de> Visitor<'de> for XorNameHexStrVisitor { + type Value = XorName; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "32 byte hex string") + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + let buffer = <[u8; 32] as hex::FromHex>::from_hex(s) + .map_err(|e| E::custom(std::format!("hex decoding ({})", e)))?; + Ok(XorName(buffer)) + } + } + return deserializer.deserialize_str(XorNameHexStrVisitor); + } + + #[derive(Deserialize)] + #[serde(rename = "XorName")] + struct XorNameDerived([u8; 32]); + let x = ::deserialize(deserializer)?; + Ok(XorName(x.0)) + } +} + +impl Serialize for Prefix { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + let hex_str = hex::encode(&self.name); + let bit_count = self.bit_count; + let s = std::format!("{hex_str}/{bit_count}"); + + return serializer.serialize_str(&s); + } + + let mut s = serializer.serialize_struct("Prefix", 2)?; + s.serialize_field("bit_count", &self.bit_count)?; + s.serialize_field("name", &self.name)?; + s.end() + } +} +impl<'de> Deserialize<'de> for Prefix { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + struct PrefixVisitor; + impl<'de> Visitor<'de> for PrefixVisitor { + type Value = Prefix; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "prefix in string format (\"/\")" + ) + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + let mut split = s.split('/'); + + let hex_str = split + .next() + .ok_or_else(|| de::Error::custom("`str::split` logic error"))?; + let bit_count = split + .next() + .ok_or_else(|| de::Error::custom("no `/` symbol encountered"))?; + + let k: [u8; 32] = hex::FromHex::from_hex(hex_str) + .map_err(|_| de::Error::custom("invalid 32 byte hex string"))?; + let bit_count = bit_count + .parse::() + .map_err(|_e| de::Error::custom("bit_count is not a valid `usize`"))?; + + Ok(Prefix::new(bit_count, XorName(k))) + } + } + return deserializer.deserialize_str(PrefixVisitor); + } + + #[derive(Deserialize)] + #[serde(rename = "Prefix")] + struct PrefixDerived { + bit_count: u16, + name: XorName, + } + let p = ::deserialize(deserializer)?; + Ok(Prefix { + bit_count: p.bit_count, + name: p.name, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde_test::*; + + /// `XorName` with derived `Serialize` impl. Used to compare against. + #[derive(PartialEq, Debug, serde::Serialize, Deserialize)] + struct XorNameDerived([u8; 32]); + + /// `Prefix` with derived `Serialize` impl. Used to compare against. + #[derive(PartialEq, Debug, serde::Serialize, Deserialize)] + struct PrefixDerived { + bit_count: u16, + name: XorNameDerived, + } + + #[test] + fn xorname_ser_de() { + let xor = XorName([0xAA; 32]); + let xor_derived = XorNameDerived([0xAA; 32]); + + let xor_hex_str = static_str("aa".repeat(32)); + assert_tokens(&xor.readable(), &[Token::Str(xor_hex_str)]); + + assert_tokens(&xor.compact(), &xor_tokens("XorName")); + // Verify our `Serialize` impl is same as when it would be derived + assert_tokens(&xor_derived.compact(), &xor_tokens("XorNameDerived")); + } + + #[test] + fn prefix_ser_de() { + let prefix = Prefix { + bit_count: 14, + name: XorName([0xAA; 32]), + }; + let prefix_derived = PrefixDerived { + bit_count: 14, + name: XorNameDerived([0xAA; 32]), + }; + + let xor_hex_str = static_str("aa".repeat(32) + "/14"); + assert_tokens(&prefix.readable(), &[Token::Str(xor_hex_str)]); + + assert_tokens(&prefix.compact(), &prefix_tokens("Prefix", "XorName")); + // Verify our `Serialize` impl is same as when it would be derived + assert_tokens( + &prefix_derived.compact(), + &prefix_tokens("PrefixDerived", "XorNameDerived"), + ); + } + + // Little helper to leak a &str to obtain a static str (`Token::Str` requires &'static str) + fn static_str(s: String) -> &'static str { + Box::leak(s.into_boxed_str()) + } + + // Compact/derived representation of `XorName` + fn xor_tokens(name: &'static str) -> Vec { + let mut a = vec![]; + a.extend_from_slice(&[Token::NewtypeStruct { name }, Token::Tuple { len: 32 }]); + a.extend_from_slice(&[Token::U8(0xAA); 32]); // Repeat a U8 Token 32 times + a.extend_from_slice(&[Token::TupleEnd]); + a + } + + // Compact/derived representation of `Prefix` + fn prefix_tokens(name: &'static str, name2: &'static str) -> Vec { + let mut v = vec![ + Token::Struct { name, len: 2 }, + Token::Str("bit_count"), + Token::U16(14), + Token::Str("name"), + ]; + v.extend_from_slice(&xor_tokens(name2)); + v.extend_from_slice(&[Token::StructEnd]); + v + } +}