Skip to content

Commit

Permalink
refactor: handle nonce as incoming string/arraybuf (#611)
Browse files Browse the repository at this point in the history
Includes:
* flexibly handling normcore-json or dag-json, but more flexible for the
former.
   * will cbor cid Nonce as String if it came in that way or bytes. 
   * cbor as input over the websocket listeners (vs just json).
   * Additional testing across the board, including with cbor.
  • Loading branch information
Zeeshan Lakhani committed Mar 13, 2024
1 parent e024b34 commit a60b8d8
Show file tree
Hide file tree
Showing 17 changed files with 644 additions and 82 deletions.
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ private
.DS_Store
homestar-functions/**/out/*.png
homestar-wasm/out
homestar-invocation/test_*
homestar-runtime/fixtures/test_*
homestar-invocation/test*
homestar-runtime/fixtures/test*
homestar-runtime/tests/fixtures/*.toml
homestar-workflow/fixtures/test_*
homestar-workflow/fixtures/test*
.zed
result-alejandra
report.json
Expand Down
17 changes: 4 additions & 13 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions homestar-invocation/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pub enum Error<T> {
/// `Display` methods through to an underlying error.
#[error("cannot convert from Ipld structure: {0}")]
FromIpld(#[from] libipld::error::SerdeError),
/// Error with a [libipld::multibase] encoding/decoding.
#[error("failed to decode/encode structure: {0}")]
FromMultibase(#[from] libipld::multibase::Error),
/// Invalid match discriminant or enumeration.
#[error("invalid discriminant {0:#?}")]
InvalidDiscriminant(T),
Expand Down
46 changes: 45 additions & 1 deletion homestar-invocation/src/ipld/dag_cbor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
use crate::{consts::DAG_CBOR, Error, Unit};
use libipld::{
cbor::DagCborCodec,
json::DagJsonCodec,
multihash::{Code, MultihashDigest},
prelude::Codec,
prelude::{Codec, Decode},
Cid, Ipld,
};
use std::{
fs,
io::{Cursor, Write},
};

/// Trait for DagCbor-related encode/decode.
pub trait DagCbor
Expand All @@ -21,6 +26,45 @@ where
let hash = Code::Sha3_256.digest(&bytes);
Ok(Cid::new_v1(DAG_CBOR, hash))
}

/// Serialize `Self` to JSON bytes.
fn to_dag_json(self) -> Result<Vec<u8>, Error<Unit>> {
let ipld: Ipld = self.into();
Ok(DagJsonCodec.encode(&ipld)?)
}

/// Serialize `Self` to JSON [String].
fn to_dagjson_string(self) -> Result<String, Error<Unit>> {
let encoded = self.to_dag_json()?;
// JSON spec requires UTF-8 support
let s = std::str::from_utf8(&encoded)?;
Ok(s.to_string())
}

/// Serialize `Self` to CBOR bytes.
fn to_cbor(self) -> Result<Vec<u8>, Error<Unit>> {
let ipld: Ipld = self.into();
Ok(DagCborCodec.encode(&ipld)?)
}

/// Deserialize `Self` from CBOR bytes.
fn from_cbor(data: &[u8]) -> Result<Self, Error<Unit>>
where
Self: TryFrom<Ipld>,
{
let ipld = Ipld::decode(DagCborCodec, &mut Cursor::new(data))?;
let from_ipld = Self::try_from(ipld).map_err(|_err| {
Error::<Unit>::UnexpectedIpldType(Ipld::String(
"Failed to convert Ipld to expected type".to_string(),
))
})?;
Ok(from_ipld)
}

/// Serialize `Self` to a CBOR file.
fn to_cbor_file(self, filename: String) -> Result<(), Error<Unit>> {
Ok(fs::File::create(filename)?.write_all(&self.to_cbor()?)?)
}
}

/// Trait for DagCbor-related encode/decode for references.
Expand Down
145 changes: 126 additions & 19 deletions homestar-invocation/src/task/instruction/nonce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use generic_array::{
use libipld::{multibase::Base::Base32HexLower, Ipld};
use schemars::{
gen::SchemaGenerator,
schema::{InstanceType, Metadata, Schema, SchemaObject, SingleOrVec},
schema::{InstanceType, Metadata, Schema, SchemaObject, SingleOrVec, StringValidation},
JsonSchema,
};
use serde::{Deserialize, Serialize};
Expand All @@ -23,13 +23,22 @@ use uuid::Uuid;
type Nonce96 = GenericArray<u8, U12>;
type Nonce128 = GenericArray<u8, U16>;

/// Incoming type for nonce conversion.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum IncomingTyp {
/// Nonce incoming as a string.
String,
/// Nonce incoming as bytes.
Bytes,
}

/// Enumeration over allowed `nonce` types.
#[derive(Clone, Debug, PartialEq, EnumAsInner, Serialize, Deserialize)]
pub enum Nonce {
/// 96-bit, 12-byte nonce, e.g. [xid].
Nonce96(Nonce96),
Nonce96(Nonce96, IncomingTyp),
/// 128-bit, 16-byte nonce.
Nonce128(Nonce128),
Nonce128(Nonce128, IncomingTyp),
/// No Nonce attributed.
Empty,
}
Expand All @@ -38,22 +47,37 @@ impl Nonce {
/// Default generator, outputting a [xid] nonce, which is a 96-bit, 12-byte
/// nonce.
pub fn generate() -> Self {
Nonce::Nonce96(*GenericArray::from_slice(xid::new().as_bytes()))
Nonce::Nonce96(
*GenericArray::from_slice(xid::new().as_bytes()),
IncomingTyp::Bytes,
)
}

/// Generate a default, 128-bit, 16-byte nonce via [Uuid::new_v4()].
pub fn generate_128() -> Self {
Nonce::Nonce128(*GenericArray::from_slice(Uuid::new_v4().as_bytes()))
Nonce::Nonce128(
*GenericArray::from_slice(Uuid::new_v4().as_bytes()),
IncomingTyp::Bytes,
)
}

/// Convert the nonce to a byte vector.
pub fn to_vec(&self) -> Vec<u8> {
match self {
Nonce::Nonce96(nonce, _) => nonce.to_vec(),
Nonce::Nonce128(nonce, _) => nonce.to_vec(),
Nonce::Empty => vec![],
}
}
}

impl fmt::Display for Nonce {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Nonce::Nonce96(nonce) => {
Nonce::Nonce96(nonce, _) => {
write!(f, "{}", Base32HexLower.encode(nonce.as_slice()))
}
Nonce::Nonce128(nonce) => {
Nonce::Nonce128(nonce, _) => {
write!(f, "{}", Base32HexLower.encode(nonce.as_slice()))
}
Nonce::Empty => write!(f, ""),
Expand All @@ -64,8 +88,20 @@ impl fmt::Display for Nonce {
impl From<Nonce> for Ipld {
fn from(nonce: Nonce) -> Self {
match nonce {
Nonce::Nonce96(nonce) => Ipld::Bytes(nonce.to_vec()),
Nonce::Nonce128(nonce) => Ipld::Bytes(nonce.to_vec()),
Nonce::Nonce96(nonce, typ) => {
if let IncomingTyp::Bytes = typ {
Ipld::Bytes(nonce.to_vec())
} else {
Ipld::String(Base32HexLower.encode(nonce.as_slice()))
}
}
Nonce::Nonce128(nonce, typ) => {
if let IncomingTyp::Bytes = typ {
Ipld::Bytes(nonce.to_vec())
} else {
Ipld::String(Base32HexLower.encode(nonce.as_slice()))
}
}
Nonce::Empty => Ipld::String("".to_string()),
}
}
Expand All @@ -75,14 +111,37 @@ impl TryFrom<Ipld> for Nonce {
type Error = Error<Unit>;

fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
if let Ipld::Bytes(v) = ipld {
match v.len() {
12 => Ok(Nonce::Nonce96(*GenericArray::from_slice(&v))),
16 => Ok(Nonce::Nonce128(*GenericArray::from_slice(&v))),
other_ipld => Err(Error::unexpected_ipld(other_ipld.to_owned().into())),
match ipld {
Ipld::String(s) if s.is_empty() => Ok(Nonce::Empty),
Ipld::String(s) => {
let bytes = Base32HexLower.decode(s)?;
match bytes.len() {
12 => Ok(Nonce::Nonce96(
*GenericArray::from_slice(&bytes),
IncomingTyp::String,
)),
16 => Ok(Nonce::Nonce128(
*GenericArray::from_slice(&bytes),
IncomingTyp::String,
)),
other => Err(Error::unexpected_ipld(other.to_owned().into())),
}
}
} else {
Ok(Nonce::Empty)
Ipld::Bytes(v) => match v.len() {
12 => Ok(Nonce::Nonce96(
*GenericArray::from_slice(&v),
IncomingTyp::Bytes,
)),
16 => Ok(Nonce::Nonce128(
*GenericArray::from_slice(&v),
IncomingTyp::Bytes,
)),
other_ipld => {
println!("other_ipld: {:?}", v.len());
Err(Error::unexpected_ipld(other_ipld.to_owned().into()))
}
},
_ => Ok(Nonce::Empty),
}
}
}
Expand Down Expand Up @@ -122,9 +181,23 @@ impl JsonSchema for Nonce {
..Default::default()
};

let non_empty_string = SchemaObject {
instance_type: Some(SingleOrVec::Single(InstanceType::String.into())),
metadata: Some(Box::new(Metadata {
description: Some("A 12-byte or 16-byte nonce encoded as a string, which expects to be decoded with Base32hex lower".to_string()),
..Default::default()
})),
string: Some(Box::new(StringValidation {
min_length: Some(1),
..Default::default()
})),
..Default::default()
};

schema.subschemas().one_of = Some(vec![
gen.subschema_for::<schema::IpldBytesStub>(),
Schema::Object(empty_string),
Schema::Object(non_empty_string),
]);

schema.into()
Expand All @@ -141,7 +214,7 @@ mod test {
let gen = Nonce::generate();
let ipld = Ipld::from(gen.clone());

let inner = if let Nonce::Nonce96(nonce) = gen {
let inner = if let Nonce::Nonce96(nonce, _) = gen {
Ipld::Bytes(nonce.to_vec())
} else {
panic!("No conversion!")
Expand All @@ -156,7 +229,7 @@ mod test {
let gen = Nonce::generate_128();
let ipld = Ipld::from(gen.clone());

let inner = if let Nonce::Nonce128(nonce) = gen {
let inner = if let Nonce::Nonce128(nonce, _) = gen {
Ipld::Bytes(nonce.to_vec())
} else {
panic!("No conversion!")
Expand Down Expand Up @@ -192,11 +265,45 @@ mod test {

assert_eq!(bytes, b);
assert_eq!(ipld, Ipld::Bytes(b.to_vec()));
assert_eq!(nonce, Nonce::Nonce128(*GenericArray::from_slice(b)));
assert_eq!(
nonce,
Nonce::Nonce128(*GenericArray::from_slice(b), IncomingTyp::Bytes)
);
assert_eq!(nonce, Nonce::try_from(ipld.clone()).unwrap());

let nonce: Nonce = ipld.clone().try_into().unwrap();
let ipld = Ipld::from(nonce.clone());
assert_eq!(ipld, Ipld::Bytes(b.to_vec()));
}

#[test]
fn nonce_as_string_roundtrip() {
let nonce = Nonce::generate();
let string = nonce.to_string();
let from_string = Nonce::try_from(Ipld::String(string.clone())).unwrap();

assert_eq!(nonce.to_vec(), from_string.to_vec());
assert_eq!(string, nonce.to_string());
}

#[test]
fn json_nonce_string_roundtrip() {
let in_nnc = "1sod60ml6g26mfhsrsa0";
let json = json!({
"nnc": in_nnc
});

let ipld: Ipld = DagJsonCodec.decode(json.to_string().as_bytes()).unwrap();
let Ipld::Map(map) = ipld else {
panic!("IPLD is not a map");
};
let nnc = map.get("nnc").unwrap();
let nnc: Nonce = Nonce::try_from(nnc.clone()).unwrap();
assert_eq!(nnc.to_string(), in_nnc);
let nonce = Nonce::Nonce96(
*GenericArray::from_slice(Base32HexLower.decode(in_nnc).unwrap().as_slice()),
IncomingTyp::String,
);
assert_eq!(nnc, nonce);
}
}
4 changes: 2 additions & 2 deletions homestar-invocation/src/test_utils/invocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ pub fn wasm_instruction_with_nonce<'a, T>() -> (Instruction<'a, T>, NonceBytes)
]))),
nonce.clone(),
),
nonce.as_nonce96().unwrap().to_vec(),
nonce.to_vec(),
)
}

Expand All @@ -139,7 +139,7 @@ pub fn instruction_with_nonce<'a, T>() -> (Instruction<'a, T>, NonceBytes) {
Input::Ipld(Ipld::List(vec![Ipld::Bool(true)])),
nonce.clone(),
),
nonce.as_nonce96().unwrap().to_vec(),
nonce.to_vec(),
)
}

Expand Down
Loading

0 comments on commit a60b8d8

Please sign in to comment.