Skip to content

Commit

Permalink
feat (yaml2candid): Support blobs input as hex or base64 (#106)
Browse files Browse the repository at this point in the history
# Motivation
`idl2json` supports encoding blobs as hex but the reverse is not true:
`yaml2candid` does not accept byte arrays expressed as hex. This is
inconvenient, to put it mildly.

# Changes
- Support blobs expressed as hex or base64.

# Tests
- Unit tests are included with 100% code coverage of the added lines.
  • Loading branch information
bitdivine authored Jul 15, 2024
1 parent 54f4256 commit c2f5323
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 14 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ resolver = "2"
version = "0.10.1"

[workspace.dependencies]
base64 = { version = "0.22.1" }
candid = { version = "0.10.8" }
candid_parser = { version = "0.1.0" }
num-bigint = { version = "0.4.5" }
hex = { version = "0.4.3" }
num-bigint = { version = "0.4.5" }
5 changes: 2 additions & 3 deletions src/idl2json/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ readme = "../../README.md"

categories = ["encoding", "parsing"]
keywords = ["internet-computer", "idl", "candid", "dfinity", "json"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
candid = { workspace = true }
candid_parser = { workspace = true }
clap = { version = "4", features = [ "derive" ], optional = true }
serde_json = "^1.0"
sha2 = { version = "0.10.8", optional = true }
clap = { version = "4", features = [ "derive" ], optional = true }

[dev-dependencies]
json-patch = "0.2.7"
serde = "1"
num-bigint = "0.4.6"
serde = "1"

[features]
default = ["crypto"]
Expand Down
5 changes: 2 additions & 3 deletions src/idl2json_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,20 @@ readme = "../../README.md"

categories = ["encoding", "parsing"]
keywords = ["internet-computer", "idl", "candid", "dfinity", "json"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[[bin]]
name = "idl2json"
path = "src/main.rs"

[dependencies]
anyhow = "1"
candid = { workspace = true }
candid_parser = { workspace = true }
clap = { version = "4.5.8", features = [ "derive" ] }
fn-error-context = "0.2.1"
idl2json = { path = "../idl2json", version = "0.10.1", features = ["clap", "crypto"] }
serde_json = "^1.0"
idl2json = { path = "../idl2json", version="0.10.1", features = ["clap", "crypto"] }
anyhow = "1"

[build-dependencies]
anyhow = "1"
Expand Down
9 changes: 5 additions & 4 deletions src/yaml2candid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
name = "yaml2candid"
version = { workspace = true }
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1"
base64 = { workspace = true }
candid = { workspace = true }
candid_parser = { workspace = true }
hex = { workspace = true }
num-bigint = { workspace = true }
serde = "1"
serde_yaml = "0.9"
anyhow = "1"
num-bigint = { workspace = true }

[dev-dependencies]
pretty_assertions = "1.4.0"
pretty_assertions = "1.4.0"
22 changes: 20 additions & 2 deletions src/yaml2candid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#![deny(clippy::panic)]
#![deny(clippy::unwrap_used)]
use anyhow::{anyhow, bail, Context};
use base64::Engine as _;
use candid::{
types::value::{IDLField, IDLValue, VariantValue},
Principal,
Expand Down Expand Up @@ -251,8 +252,25 @@ impl Yaml2Candid {
.enumerate()
.map(|(index, val)| self.convert(typ, val).with_context(|| format!("Failed to convert element #{index}")))
.collect::<Result<Vec<_>, _>>()?,
))} else {
bail!("Expected a sequence for vec type, got: {data:?}")
))} else if matches!(**typ, IDLType::PrimT(candid_parser::types::PrimType::Nat8)) {
// Byte vectors MAY be represented in different ways, such as hex or base64 strings
const BASE_64_PREFIX: &str="base64,";
const HEX_PREFIX: &str="0x";
if let YamlValue::String(value) = data {
if let Some(base64_str) = value.strip_prefix(BASE_64_PREFIX) {
let bytes = base64::engine::general_purpose::STANDARD.decode(base64_str).with_context(|| "Failed to interpret base64 string in JSON/YAML as Candid byte vector")?;
Ok(IDLValue::Vec(bytes.iter().map(|&b| IDLValue::Nat8(b)).collect::<Vec<_>>()))
} else if let Some(hex_str) = value.strip_prefix(HEX_PREFIX) {
let bytes = hex::decode(hex_str).with_context(|| "Failed to interpret hex string in JSON/YAML as Candid byte vector")?;
Ok(IDLValue::Vec(bytes.iter().map(|&b| IDLValue::Nat8(b)).collect::<Vec<_>>()))
} else {
bail!("Unknown encoding for byte vector as string starting: {} Please prefix string encoded byte vectors with one of '{BASE_64_PREFIX}' and '{HEX_PREFIX}'.", &value[0..6])
}
} else {
bail!("Expected vector of bytes encoded as one of: Vec<number>, hex string prefixed with '{HEX_PREFIX}', base64 string prefixed with '{BASE_64_PREFIX}'.");
}
} else {
bail!("Expected a sequence of {typ:?}, got: {data:?}")
},
IDLType::VariantT(types) => if let YamlValue::Mapping(value) = data {
types
Expand Down
79 changes: 79 additions & 0 deletions src/yaml2candid/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Tests converting from YAML to Candid, especially extreme values of primitive types.
#![allow(clippy::panic)] // Tests are allowed to panic!
use anyhow::Context;
use candid::types::{value::IDLField, Label};
use candid_parser::types::TypeField;
Expand Down Expand Up @@ -494,6 +495,38 @@ fn can_convert() {
IDLValue::Int8(3),
]),
},
TestVec {
description: "hex encoded blob",
typ: IDLType::VecT(Box::new(IDLType::PrimT(
candid_parser::types::PrimType::Nat8,
))),
data: YamlValue::from("0x010203090a1000"),
expected_result: IDLValue::Vec(vec![
IDLValue::Nat8(1),
IDLValue::Nat8(2),
IDLValue::Nat8(3),
IDLValue::Nat8(9),
IDLValue::Nat8(10),
IDLValue::Nat8(16),
IDLValue::Nat8(0),
]),
},
TestVec {
description: "base64 encoded blob",
typ: IDLType::VecT(Box::new(IDLType::PrimT(
candid_parser::types::PrimType::Nat8,
))),
data: YamlValue::from("base64,AQIDCQoQAA=="),
expected_result: IDLValue::Vec(vec![
IDLValue::Nat8(1),
IDLValue::Nat8(2),
IDLValue::Nat8(3),
IDLValue::Nat8(9),
IDLValue::Nat8(10),
IDLValue::Nat8(16),
IDLValue::Nat8(0),
]),
},
TestVec {
description: "Some(5) in canonical form",
typ: IDLType::OptT(Box::new(IDLType::PrimT(
Expand Down Expand Up @@ -659,3 +692,49 @@ fn can_convert() {
assert_named_conversion_is(&converter, &typ, &data, expected_result, description);
}
}

/// A conversion that should fail.
struct ErrorTestVec {
typ: IDLType,
data: YamlValue,
expected_error: &'static str,
}
impl ErrorTestVec {
pub fn should_fail(&self) {
let Self {
typ,
data,
expected_error,
} = self;
let converter = Yaml2Candid::default();
let result = converter.convert(typ, data);
if let Err(e) = result {
assert!(e.to_string().contains(expected_error))
} else {
panic!("Converting {data:?} to {typ:?} should fail.");
}
}
}

#[test]
fn unsupported_blob_encoding_should_fail() {
ErrorTestVec{
typ: IDLType::VecT(Box::new(IDLType::PrimT(
candid_parser::types::PrimType::Nat8,
))),
data: YamlValue::from("010203090a1000"),
expected_error: "Unknown encoding for byte vector as string starting: 010203 Please prefix string encoded byte vectors"
}.should_fail();
}

#[test]
fn unsupported_blob_type_should_fail() {
ErrorTestVec {
typ: IDLType::VecT(Box::new(IDLType::PrimT(
candid_parser::types::PrimType::Nat8,
))),
data: YamlValue::from(9),
expected_error: "Expected vector of bytes encoded as one of",
}
.should_fail();
}
1 change: 0 additions & 1 deletion src/yaml2candid_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name = "yaml2candid_cli"
version = { workspace = true }
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
Expand Down

0 comments on commit c2f5323

Please sign in to comment.