Skip to content

Commit

Permalink
Merge pull request #89 from maidsafe/feat-serialize-hex-string
Browse files Browse the repository at this point in the history
feat: serialize XorName to hex string
  • Loading branch information
davidrusu committed Aug 4, 2022
2 parents 87cc5af + 9d54992 commit f6ed0b0
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 19 deletions.
14 changes: 1 addition & 13 deletions .github/workflows/pr.yml
Expand Up @@ -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')"
Expand Down Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions Cargo.toml
Expand Up @@ -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"

Expand All @@ -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"

Expand Down
32 changes: 32 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions src/lib.rs
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
11 changes: 7 additions & 4 deletions src/prefix.rs
Expand Up @@ -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 {
Expand Down
210 changes: 210 additions & 0 deletions 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, s: &str) -> Result<Self::Value, E>
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 = <XorNameDerived as Deserialize>::deserialize(deserializer)?;
Ok(XorName(x.0))
}
}

impl Serialize for Prefix {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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 (\"<xor hex string>/<prefix>\")"
)
}

fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
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::<usize>()
.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 = <PrefixDerived as Deserialize>::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<Token> {
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<Token> {
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
}
}

0 comments on commit f6ed0b0

Please sign in to comment.