Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: serialize XorName to hex string #89

Merged
merged 1 commit into from Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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() {
b-zee marked this conversation as resolved.
Show resolved Hide resolved
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
}
}