diff --git a/ucan/Cargo.toml b/ucan/Cargo.toml index 3b95c177..afdc8df7 100644 --- a/ucan/Cargo.toml +++ b/ucan/Cargo.toml @@ -39,6 +39,10 @@ thiserror = { workspace = true, default-features = false } tracing = { workspace = true, default-features = false, features = ["attributes"] } varsig = { path = "../varsig", default-features = false, features = ["dag_cbor", "ed25519"] } +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" +wasm-bindgen = "0.2" + [dev-dependencies] arbitrary = { workspace = true, optional = false } base64 = "0.22.1" diff --git a/ucan/src/crypto.rs b/ucan/src/crypto.rs index 6327ae1f..4af06312 100644 --- a/ucan/src/crypto.rs +++ b/ucan/src/crypto.rs @@ -1,4 +1,3 @@ //! Helpers for cryptographic operations. pub mod nonce; -pub mod signed; diff --git a/ucan/src/crypto/signed.rs b/ucan/src/crypto/signed.rs deleted file mode 100644 index adbe7b54..00000000 --- a/ucan/src/crypto/signed.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Signed payload wrapper. - -/// Signed payload wrapper. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Signed { - varsig_header: V, - payload: T, - signature: S, -} diff --git a/ucan/src/delegation.rs b/ucan/src/delegation.rs index 85749bd9..ff01f527 100644 --- a/ucan/src/delegation.rs +++ b/ucan/src/delegation.rs @@ -1,7 +1,7 @@ //! UCAN Delegation //! //! The spec for UCAN Delegations can be found at -//! [the GitHub repo](https://github.com/ucan-wg/invocation/). +//! [the GitHub repo](https://github.com/ucan-wg/delegation/). pub mod builder; pub mod policy; @@ -378,6 +378,7 @@ where let command = command.ok_or_else(|| de::Error::missing_field("cmd"))?; let policy = policy.ok_or_else(|| de::Error::missing_field("pol"))?; let nonce = nonce.ok_or_else(|| de::Error::missing_field("nonce"))?; + let expiration = expiration.ok_or_else(|| de::Error::missing_field("exp"))?; Ok(DelegationPayload { issuer, @@ -386,7 +387,7 @@ where command, policy, nonce, - expiration: expiration.unwrap_or(None), + expiration, not_before: not_before.unwrap_or(None), meta: meta.unwrap_or_default(), }) @@ -430,7 +431,7 @@ mod tests { .issuer(iss.clone()) .audience(aud) .subject(DelegatedSubject::Specific(sub)) - .command(vec!["read".to_string(), "write".to_string()]); + .command_from_str("/read/write")?; let delegation = builder.try_build()?; @@ -485,7 +486,7 @@ mod tests { issuer: iss, audience: aud, subject: DelegatedSubject::Any, - command: Command::new(vec!["/".to_string()]), + command: Command::parse("/")?, policy: vec![], expiration: None, not_before: None, diff --git a/ucan/src/delegation/builder.rs b/ucan/src/delegation/builder.rs index de3a62fc..466cccb6 100644 --- a/ucan/src/delegation/builder.rs +++ b/ucan/src/delegation/builder.rs @@ -2,7 +2,7 @@ use super::{policy::predicate::Predicate, subject::DelegatedSubject}; use crate::{ - command::Command, + command::{Command, CommandParseError}, crypto::nonce::Nonce, did::{Did, DidSigner}, envelope::{Envelope, EnvelopePayload}, @@ -138,13 +138,13 @@ impl< } } - /// Sets the command of the [`Delegation`]. - pub fn command(self, command: Vec) -> DelegationBuilder { + /// Sets the command of the [`Delegation`] from a pre-validated [`Command`]. + pub fn command(self, command: Command) -> DelegationBuilder { DelegationBuilder { issuer: self.issuer, audience: self.audience, subject: self.subject, - command: Command::new(command), + command, policy: self.policy, expiration: self.expiration, not_before: self.not_before, @@ -154,6 +154,18 @@ impl< } } + /// Parses a command string and sets it on the [`Delegation`]. + /// + /// # Errors + /// + /// Returns [`CommandParseError`] if the command string is invalid. + pub fn command_from_str( + self, + s: &str, + ) -> Result, CommandParseError> { + Ok(self.command(Command::parse(s)?)) + } + /// Sets the policy of the [`Delegation`]. #[must_use] pub fn policy(self, policy: Vec) -> Self { diff --git a/ucan/src/delegation/policy/predicate.rs b/ucan/src/delegation/policy/predicate.rs index 7de06ba9..b66365bc 100644 --- a/ucan/src/delegation/policy/predicate.rs +++ b/ucan/src/delegation/policy/predicate.rs @@ -19,7 +19,7 @@ use arbitrary::{self, Arbitrary, Unstructured}; #[cfg(any(test, feature = "test_utils"))] use crate::ipld::InternalIpld; -/// Validtor for [`Ipld`] values. +/// Validator for [`Ipld`] values. #[derive(Debug, Clone, PartialEq)] pub enum Predicate { /// Selector equality check @@ -37,7 +37,7 @@ pub enum Predicate { /// Selector less than or equal check LessThanOrEqual(Select, Number), - /// Seelctor `like` matcher check (glob patterns) + /// Selector `like` matcher check (glob patterns) Like(Select, String), /// Negation @@ -51,7 +51,7 @@ pub enum Predicate { /// Universal quantification over a collection /// - /// "For all elements of a collection" (∀x ∈ xs) the precicate must hold + /// "For all elements of a collection" (∀x ∈ xs) the predicate must hold All(Select, Box), /// Existential quantification over a collection @@ -111,10 +111,18 @@ impl Serialize for Predicate { triple.end() } Self::Not(inner) => { - let mut tuple = serializer.serialize_tuple(2)?; - tuple.serialize_element(&"not")?; - tuple.serialize_element(inner)?; - tuple.end() + if let Predicate::Equal(lhs, rhs) = inner.as_ref() { + let mut triple = serializer.serialize_tuple(3)?; + triple.serialize_element(&"!=")?; + triple.serialize_element(lhs)?; + triple.serialize_element(rhs)?; + triple.end() + } else { + let mut tuple = serializer.serialize_tuple(2)?; + tuple.serialize_element(&"not")?; + tuple.serialize_element(inner)?; + tuple.end() + } } Self::And(inner) => { let mut tuple = serializer.serialize_tuple(2)?; @@ -178,6 +186,13 @@ impl<'de> Deserialize<'de> for Predicate { de::Error::invalid_length(2, &"expected an Ipld value") })?, )), + "!=" => Ok(Predicate::Not(Box::new(Predicate::Equal( + seq.next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &"expected a selector"))?, + seq.next_element()?.ok_or_else(|| { + de::Error::invalid_length(2, &"expected an Ipld value") + })?, + )))), ">" => Ok(Predicate::GreaterThan( seq.next_element()? .ok_or_else(|| de::Error::invalid_length(1, &"expected a selector"))?, @@ -241,7 +256,8 @@ impl<'de> Deserialize<'de> for Predicate { _ => Err(de::Error::unknown_variant( &op, &[ - "==", ">", ">=", "<", "<=", "like", "not", "and", "or", "all", "any", + "==", "!=", ">", ">=", "<", "<=", "like", "not", "and", "or", "all", + "any", ], )), } @@ -592,7 +608,7 @@ pub enum FromIpldError { /// Invalid String selector. #[error("Invalid String selector {0:?}")] - InvalidStringSelector( as FromStr>::Err), + InvalidStringSelector( as FromStr>::Err), /// Cannot parse [`Number`]. #[error("Cannot parse Number {0:?}")] @@ -642,10 +658,23 @@ impl From for Ipld { lhs.into(), rhs.into(), ]), - Predicate::Not(inner) => { - let unboxed = *inner; - Ipld::List(vec![Ipld::String("not".to_string()), unboxed.into()]) - } + Predicate::Not(inner) => match *inner { + Predicate::Equal(lhs, rhs) => { + Ipld::List(vec![Ipld::String("!=".to_string()), lhs.into(), rhs]) + } + other @ (Predicate::GreaterThan(_, _) + | Predicate::GreaterThanOrEqual(_, _) + | Predicate::LessThan(_, _) + | Predicate::LessThanOrEqual(_, _) + | Predicate::Like(_, _) + | Predicate::Not(_) + | Predicate::And(_) + | Predicate::Or(_) + | Predicate::All(_, _) + | Predicate::Any(_, _)) => { + Ipld::List(vec![Ipld::String("not".to_string()), other.into()]) + } + }, Predicate::And(inner) => { let inner_ipld: Vec = inner.into_iter().map(Into::into).collect(); vec![Ipld::String("and".to_string()), inner_ipld.into()].into() @@ -1392,4 +1421,36 @@ mod tests { Ok(()) } } + + mod roundtrip { + use super::*; + + #[test_log::test] + fn test_not_equal_dagcbor_roundtrip() -> TestResult { + let pred = Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".foo")?, + Ipld::Integer(42), + ))); + + let cbor = serde_ipld_dagcbor::to_vec(&pred)?; + let back: Predicate = serde_ipld_dagcbor::from_slice(&cbor)?; + + assert_eq!(back, pred); + Ok(()) + } + + #[test_log::test] + fn test_not_equal_ipld_roundtrip() -> TestResult { + let pred = Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".bar")?, + Ipld::String("hello".into()), + ))); + + let ipld: Ipld = pred.clone().into(); + let back = Predicate::try_from(ipld)?; + + assert_eq!(back, pred); + Ok(()) + } + } } diff --git a/ucan/src/delegation/policy/selector/filter.rs b/ucan/src/delegation/policy/selector/filter.rs index 7e56ca71..1ca9992d 100644 --- a/ucan/src/delegation/policy/selector/filter.rs +++ b/ucan/src/delegation/policy/selector/filter.rs @@ -39,6 +39,15 @@ pub enum Filter { /// Extract a field from a map (e.g. `["key"]` or `.key`). Field(String), + /// Extract a slice from a list or bytes (e.g. `[2:5]`, `[2:]`, `[:5]`, `[0:-2]`). + Slice { + /// Inclusive start index (None = beginning). + start: Option, + + /// Exclusive end index (None = end). + end: Option, + }, + /// Extract values from a collection (e.g. `.[]`). Values, @@ -53,9 +62,10 @@ impl Filter { match (self, other) { (Filter::ArrayIndex(a), Filter::ArrayIndex(b)) => a == b, (Filter::Field(a), Filter::Field(b)) => a == b, - (Filter::Values, Filter::Values) => true, - (Filter::ArrayIndex(_a), Filter::Values) => true, - (Filter::Field(_k), Filter::Values) => true, + ( + Filter::Values | Filter::ArrayIndex(_) | Filter::Field(_) | Filter::Slice { .. }, + Filter::Values, + ) => true, (Filter::Try(a), Filter::Try(b)) => a.is_in(b), _ => false, } @@ -73,7 +83,7 @@ impl Filter { false } } - Filter::ArrayIndex(_) | Filter::Values | Filter::Try(_) => false, + Filter::ArrayIndex(_) | Filter::Slice { .. } | Filter::Values | Filter::Try(_) => false, } } } @@ -103,6 +113,17 @@ impl fmt::Display for Filter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Filter::ArrayIndex(i) => write!(f, "[{i}]"), + Filter::Slice { start, end } => { + f.write_char('[')?; + if let Some(s) = start { + write!(f, "{s}")?; + } + f.write_char(':')?; + if let Some(e) = end { + write!(f, "{e}")?; + } + f.write_char(']') + } Filter::Field(k) => { // Be conservative: only use dot form for safe identifiers. let dot_ok = self.is_dot_field() @@ -167,13 +188,37 @@ pub fn parse_try_dot_field(input: &str) -> IResult<&str, Filter> { context("try", p).parse(input) } +fn parse_opt_signed_int(i: &str) -> IResult<&str, Option> { + nom::combinator::opt(map_res( + nom::combinator::recognize(preceded(nom::combinator::opt(tag("-")), digit1)), + i32::from_str, + )) + .parse(i) +} + +fn parse_slice_inner(i: &str) -> IResult<&str, Filter> { + let (i, start) = parse_opt_signed_int(i)?; + let (i, _) = char(':').parse(i)?; + let (i, end) = parse_opt_signed_int(i)?; + Ok((i, Filter::Slice { start, end })) +} + +/// Parses a slice expression, e.g. `[2:5]`, `[2:]`, `[:5]`, or `[:]`. +/// +/// # Errors +/// +/// Returns a `nom` error if the parser fails to match. +pub fn parse_slice(input: &str) -> IResult<&str, Filter> { + context("slice", delimited(char('['), parse_slice_inner, char(']'))).parse(input) +} + /// Parses a filter not ending in `?`, e.g. `["foo"]`, `.foo`, or `[2]`. /// /// # Errors /// /// Returns a `nom` error if the parser fails to match. pub fn parse_non_try(input: &str) -> IResult<&str, Filter> { - let p = alt((parse_values, parse_field, parse_array_index)); + let p = alt((parse_values, parse_slice, parse_field, parse_array_index)); context("non_try", p).parse(input) } @@ -369,6 +414,15 @@ impl Serialize for Filter { seq.end() } + // `[ "slice", , ]` + Filter::Slice { start, end } => { + let mut seq = serializer.serialize_seq(Some(3))?; + seq.serialize_element("slice")?; + seq.serialize_element(&start)?; + seq.serialize_element(&end)?; + seq.end() + } + // `[ "values" ]` Filter::Values => { let mut seq = serializer.serialize_seq(Some(1))?; @@ -400,7 +454,7 @@ impl<'de> Deserialize<'de> for Filter { fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> fmt::Result { write!( f, - r#"a tagged sequence like ["idx", 3], ["field", "foo"], ["values"], or ["try", ...]"# + r#"a tagged sequence like ["idx", 3], ["field", "foo"], ["slice", 0, 5], ["values"], or ["try", ...]"# ) } @@ -425,6 +479,15 @@ impl<'de> Deserialize<'de> for Filter { .ok_or_else(|| A::Error::custom("missing field"))?; Ok(Filter::Field(k)) } + "slice" => { + let start: Option = seq + .next_element()? + .ok_or_else(|| A::Error::custom("missing slice start"))?; + let end: Option = seq + .next_element()? + .ok_or_else(|| A::Error::custom("missing slice end"))?; + Ok(Filter::Slice { start, end }) + } "values" => Ok(Filter::Values), "try" => { let inner: Filter = seq @@ -641,11 +704,18 @@ impl<'a> Arbitrary<'a> for Filter { enum Pick { ArrayIndex, Field, + Slice, Values, Try, } - match u.choose(&[Pick::ArrayIndex, Pick::Field, Pick::Values, Pick::Try])? { + match u.choose(&[ + Pick::ArrayIndex, + Pick::Field, + Pick::Slice, + Pick::Values, + Pick::Try, + ])? { Pick::ArrayIndex => { let i = u.int_in_range(0..=99)?; Ok(Filter::ArrayIndex(i)) @@ -654,6 +724,11 @@ impl<'a> Arbitrary<'a> for Filter { let s = u.arbitrary::()?; Ok(Filter::Field(s)) } + Pick::Slice => { + let start = u.arbitrary::>()?; + let end = u.arbitrary::>()?; + Ok(Filter::Slice { start, end }) + } Pick::Values => Ok(Filter::Values), Pick::Try => { let inner = Filter::arbitrary(u)?; @@ -943,6 +1018,114 @@ mod tests { pretty::assert_eq!(got, Ok(Filter::Try(Box::new(Filter::ArrayIndex(42))))); } + #[test_log::test] + fn test_slice_both() { + let got = Filter::from_str("[2:5]"); + pretty::assert_eq!( + got, + Ok(Filter::Slice { + start: Some(2), + end: Some(5) + }) + ); + } + + #[test_log::test] + fn test_slice_start_only() { + let got = Filter::from_str("[2:]"); + pretty::assert_eq!( + got, + Ok(Filter::Slice { + start: Some(2), + end: None + }) + ); + } + + #[test_log::test] + fn test_slice_end_only() { + let got = Filter::from_str("[:5]"); + pretty::assert_eq!( + got, + Ok(Filter::Slice { + start: None, + end: Some(5) + }) + ); + } + + #[test_log::test] + fn test_slice_both_negative() { + let got = Filter::from_str("[0:-2]"); + pretty::assert_eq!( + got, + Ok(Filter::Slice { + start: Some(0), + end: Some(-2) + }) + ); + } + + #[test_log::test] + fn test_slice_open() { + let got = Filter::from_str("[:]"); + pretty::assert_eq!( + got, + Ok(Filter::Slice { + start: None, + end: None + }) + ); + } + + #[test_log::test] + fn test_slice_negative_start() { + let got = Filter::from_str("[-3:]"); + pretty::assert_eq!( + got, + Ok(Filter::Slice { + start: Some(-3), + end: None + }) + ); + } + + #[test_log::test] + fn test_slice_display_roundtrip() { + let cases = vec![ + Filter::Slice { + start: Some(2), + end: Some(5), + }, + Filter::Slice { + start: Some(2), + end: None, + }, + Filter::Slice { + start: None, + end: Some(5), + }, + Filter::Slice { + start: None, + end: None, + }, + Filter::Slice { + start: Some(0), + end: Some(-2), + }, + Filter::Slice { + start: Some(-3), + end: None, + }, + ]; + + for filter in cases { + let displayed = filter.to_string(); + let parsed = Filter::from_str(&displayed); + pretty::assert_eq!(parsed, Ok(filter)); + } + } + #[test_log::test] fn test_fails_bare_try() { let got = Filter::from_str("?"); diff --git a/ucan/src/delegation/policy/selector/select.rs b/ucan/src/delegation/policy/selector/select.rs index ea4044d4..0bb781fd 100644 --- a/ucan/src/delegation/policy/selector/select.rs +++ b/ucan/src/delegation/policy/selector/select.rs @@ -3,6 +3,20 @@ use super::{error::SelectorErrorReason, filter::Filter, Selectable, Selector, SelectorError}; use alloc::{string::ToString, vec, vec::Vec}; use core::{cmp::Ordering, fmt, marker::PhantomData, str::FromStr}; + +/// Resolve Python-style slice indices into a `(start, end)` pair clamped to `0..len`. +fn resolve_slice_indices(start: Option, end: Option, len: usize) -> (usize, usize) { + let resolve = |idx: i32, len: usize| -> usize { + if idx >= 0 { + (idx.unsigned_abs() as usize).min(len) + } else { + len.saturating_sub(idx.unsigned_abs() as usize) + } + }; + let s = start.map_or(0, |i| resolve(i, len)); + let e = end.map_or(len, |i| resolve(i, len)); + (s, e.max(s)) +} use ipld_core::ipld::Ipld; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; @@ -112,7 +126,7 @@ impl Select { )); } - let idx: usize = if *i <= 0 { + let idx: usize = if *i >= 0 { i.unsigned_abs() as usize } else { xs.len() - i.unsigned_abs() as usize @@ -128,12 +142,46 @@ impl Select { )) .cloned() } + Ipld::Bytes(bs) => { + if bs.len() > (i32::MAX as usize) { + return Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::IndexOutOfBounds, + ), + )); + } + + if i.unsigned_abs() as usize > bs.len() { + return Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::IndexOutOfBounds, + ), + )); + } + + let idx: usize = if *i >= 0 { + i.unsigned_abs() as usize + } else { + bs.len() - i.unsigned_abs() as usize + }; + + bs.get(idx).map(|b| Ipld::Integer(i128::from(*b))).ok_or(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::IndexOutOfBounds, + ), + )) + } Ipld::Null | Ipld::Bool(_) | Ipld::Integer(_) | Ipld::Float(_) | Ipld::String(_) - | Ipld::Bytes(_) | Ipld::Map(_) | Ipld::Link(_) => Err(( is_try, @@ -174,6 +222,30 @@ impl Select { Ok((result?, seen_ops.clone(), is_try)) } + Filter::Slice { start, end } => { + let result = match ipld { + Ipld::List(xs) => { + let (s, e) = resolve_slice_indices(*start, *end, xs.len()); + Ok(Ipld::List(xs.get(s..e).unwrap_or_default().to_vec())) + } + Ipld::Bytes(bs) => { + let (s, e) = resolve_slice_indices(*start, *end, bs.len()); + Ok(Ipld::Bytes(bs.get(s..e).unwrap_or_default().to_vec())) + } + Ipld::Null + | Ipld::Bool(_) + | Ipld::Integer(_) + | Ipld::Float(_) + | Ipld::String(_) + | Ipld::Map(_) + | Ipld::Link(_) => Err(( + is_try, + SelectorError::from_refs(&seen_ops, SelectorErrorReason::NotAList), + )), + }; + + Ok((result?, seen_ops.clone(), is_try)) + } Filter::Values => { let result = match ipld { Ipld::List(xs) => Ok(Ipld::List(xs)), @@ -259,6 +331,7 @@ mod tests { use super::*; use crate::ipld::InternalIpld; + #[allow(clippy::expect_used)] mod get { use super::*; use crate::ipld::eq_with_float_nans_and_infinities; @@ -266,7 +339,7 @@ mod tests { use proptest_arbitrary_interop::arb; proptest! { - #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })] + #![proptest_config(ProptestConfig { cases: 32, ..ProptestConfig::default() })] #[test_log::test] fn test_identity(data in arb::()) { let selector = Select::::from_str(".")?; @@ -275,7 +348,7 @@ mod tests { } proptest! { - #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })] + #![proptest_config(ProptestConfig { cases: 32, ..ProptestConfig::default() })] #[test_log::test] fn test_try_missing_is_null(data in arb::()) { let selector = Select::::from_str(".foo?")?; @@ -291,15 +364,200 @@ mod tests { } } + #[test_log::test] + fn test_slice_list() { + let data = Ipld::List(vec![ + Ipld::Integer(10), + Ipld::Integer(20), + Ipld::Integer(30), + Ipld::Integer(40), + ]); + let selector = Select::::from_str(".[1:3]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!( + result, + Ipld::List(vec![Ipld::Integer(20), Ipld::Integer(30)]) + ); + } + + #[test_log::test] + fn test_slice_list_open_end() { + let data = Ipld::List(vec![ + Ipld::Integer(10), + Ipld::Integer(20), + Ipld::Integer(30), + ]); + let selector = Select::::from_str(".[1:]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!( + result, + Ipld::List(vec![Ipld::Integer(20), Ipld::Integer(30)]) + ); + } + + #[test_log::test] + fn test_slice_list_open_start() { + let data = Ipld::List(vec![ + Ipld::Integer(10), + Ipld::Integer(20), + Ipld::Integer(30), + ]); + let selector = Select::::from_str(".[:2]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!( + result, + Ipld::List(vec![Ipld::Integer(10), Ipld::Integer(20)]) + ); + } + + #[test_log::test] + fn test_slice_negative_end() { + let data = Ipld::List(vec![ + Ipld::Integer(10), + Ipld::Integer(20), + Ipld::Integer(30), + ]); + let selector = Select::::from_str(".[0:-1]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!( + result, + Ipld::List(vec![Ipld::Integer(10), Ipld::Integer(20)]) + ); + } + + #[test_log::test] + fn test_byte_index() { + let data = Ipld::Bytes(vec![0xd6, 0xa9, 0xc1, 0x8c, 0xf8, 0xc4]); + let selector = Select::::from_str(".[3]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::Integer(0x8c)); + } + + #[test_log::test] + fn test_byte_slice() { + let data = Ipld::Bytes(vec![0xd6, 0xa9, 0xc1, 0x8c, 0xf8, 0xc4]); + let selector = Select::::from_str(".[1:3]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::Bytes(vec![0xa9, 0xc1])); + } + + #[test_log::test] + fn test_slice_both_negative() { + let data = Ipld::List(vec![ + Ipld::Integer(10), + Ipld::Integer(20), + Ipld::Integer(30), + Ipld::Integer(40), + Ipld::Integer(50), + ]); + let selector = Select::::from_str(".[-3:-1]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!( + result, + Ipld::List(vec![Ipld::Integer(30), Ipld::Integer(40)]) + ); + } + + #[test_log::test] + fn test_slice_negative_start_open_end() { + let data = Ipld::List(vec![ + Ipld::Integer(10), + Ipld::Integer(20), + Ipld::Integer(30), + ]); + let selector = Select::::from_str(".[-2:]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!( + result, + Ipld::List(vec![Ipld::Integer(20), Ipld::Integer(30)]) + ); + } + + #[test_log::test] + fn test_slice_full_copy() { + let data = Ipld::List(vec![Ipld::Integer(10), Ipld::Integer(20)]); + let selector = Select::::from_str(".[:]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, data); + } + + #[test_log::test] + fn test_slice_empty_when_start_ge_end() { + let data = Ipld::List(vec![ + Ipld::Integer(10), + Ipld::Integer(20), + Ipld::Integer(30), + ]); + let selector = Select::::from_str(".[2:1]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::List(vec![])); + } + + #[test_log::test] + fn test_slice_out_of_bounds_clamps() { + let data = Ipld::List(vec![Ipld::Integer(10), Ipld::Integer(20)]); + let selector = Select::::from_str(".[0:100]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, data); + } + + #[test_log::test] + fn test_byte_negative_index() { + let data = Ipld::Bytes(vec![0xAA, 0xBB, 0xCC]); + let selector = Select::::from_str(".[-1]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::Integer(0xCC)); + } + + #[test_log::test] + fn test_byte_slice_negative() { + let data = Ipld::Bytes(vec![0xAA, 0xBB, 0xCC, 0xDD]); + let selector = Select::::from_str(".[-2:]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::Bytes(vec![0xCC, 0xDD])); + } + + #[test_log::test] + fn test_byte_index_out_of_bounds_with_try() { + let data = Ipld::Bytes(vec![0xAA, 0xBB]); + let selector = Select::::from_str(".[99]?").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::Null); + } + + #[test_log::test] + fn test_slice_on_non_list_fails() { + let data = Ipld::Integer(42); + let selector = Select::::from_str(".[0:2]").expect("parse"); + assert!(selector.get(&data).is_err()); + } + + #[test_log::test] + fn test_slice_on_non_list_with_try_returns_null() { + let data = Ipld::Integer(42); + let selector = Select::::from_str(".[0:2]?").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::Null); + } + + #[test_log::test] + fn test_byte_index_spec_example() { + // From the spec: bytes 0xd6a9c18cf8c4, selector .[3] => 0x8c = 140 + let data = Ipld::Bytes(vec![0xd6, 0xa9, 0xc1, 0x8c, 0xf8, 0xc4]); + let selector = Select::::from_str(".[3]").expect("parse"); + let result = selector.get(&data).expect("get"); + assert_eq!(result, Ipld::Integer(140)); + } + proptest! { - #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })] + #![proptest_config(ProptestConfig { cases: 32, ..ProptestConfig::default() })] #[test_log::test] fn test_try_missing_plus_trailing_is_null(data in arb::(), more in arb::>()) { let mut filters = vec![Filter::Try(Box::new(Filter::Field("foo".into())))]; for f in &more { match f { - Filter::Try(_) | Filter::Values => {} + Filter::Try(_) | Filter::Values | Filter::Slice { .. } => {} other @ (Filter::ArrayIndex(_) | Filter::Field(_)) => filters.push(other.clone()), } } diff --git a/ucan/src/invocation.rs b/ucan/src/invocation.rs index 68984c99..b5fddd43 100644 --- a/ucan/src/invocation.rs +++ b/ucan/src/invocation.rs @@ -264,7 +264,7 @@ impl InvocationPayload { let mut expected_issuer = self.subject(); for proof in proofs { - if proof.subject().allows(self.subject()) { + if !proof.subject().allows(self.subject()) { return Err(CheckFailed::SubjectNotAllowedByProof); } @@ -314,7 +314,7 @@ pub enum CheckFailed { WaitingOnPromise(#[from] WaitingOn), /// Error indicating that the command in the invocation does not match the command in the proof - #[error("command mismatch: expected {expected:?}, found {expected:?}")] + #[error("command mismatch: expected {expected:?}, found {found:?}")] CommandMismatch { /// The expected command expected: Command, @@ -388,7 +388,7 @@ mod tests { .issuer(iss.clone()) .audience(aud) .subject(sub) - .command(vec!["read".to_string(), "write".to_string()]) + .command_from_str("/read/write")? .proofs(vec![]); let invocation = builder.try_build()?; diff --git a/ucan/src/invocation/builder.rs b/ucan/src/invocation/builder.rs index 77569744..017d87da 100644 --- a/ucan/src/invocation/builder.rs +++ b/ucan/src/invocation/builder.rs @@ -1,7 +1,7 @@ //! Typesafe builder for [`InvocationPayload`]. use crate::{ - command::Command, + command::{Command, CommandParseError}, crypto::nonce::Nonce, did::{Did, DidSigner}, envelope::{Envelope, EnvelopePayload}, @@ -162,17 +162,17 @@ impl< } } - /// Sets the `command` field of the invocation. + /// Sets the `command` field of the invocation from a pre-validated [`Command`]. #[must_use] pub fn command( self, - command: Vec, + command: Command, ) -> InvocationBuilder { InvocationBuilder { issuer: self.issuer, audience: self.audience, subject: self.subject, - command: Command::new(command), + command, arguments: self.arguments, proofs: self.proofs, cause: self.cause, @@ -184,6 +184,19 @@ impl< } } + /// Parses a command string and sets it on the [`Invocation`]. + /// + /// # Errors + /// + /// Returns [`CommandParseError`] if the command string is invalid. + pub fn command_from_str( + self, + s: &str, + ) -> Result, CommandParseError> + { + Ok(self.command(Command::parse(s)?)) + } + /// Sets the `arguments` field of the invocation. #[must_use] pub fn arguments( @@ -347,7 +360,7 @@ impl InvocationBuilder super::InvocationPayload { let nonce = self.nonce.unwrap_or_else(|| { diff --git a/ucan/src/ipld.rs b/ucan/src/ipld.rs index 38dcf63d..39cd71d5 100644 --- a/ucan/src/ipld.rs +++ b/ucan/src/ipld.rs @@ -1,6 +1,6 @@ //! Internal IPLD representation. //! -//! This is here becuase `ipld-core` doesn't implement various traits. +//! This is here because `ipld-core` doesn't implement various traits. //! It is not a simple newtype wrapper because IPLD has recursive values, //! and this implementation is simpler. If it is a performance bottleneck, //! please let the maintainers know. diff --git a/ucan/src/lib.rs b/ucan/src/lib.rs index 7f059b73..a1769be7 100644 --- a/ucan/src/lib.rs +++ b/ucan/src/lib.rs @@ -17,8 +17,6 @@ pub mod envelope; pub mod invocation; pub mod number; pub mod promise; -// pub mod receipt; TODO Reenable after first release -pub mod task; pub mod time; pub mod unset; diff --git a/ucan/src/receipt.rs b/ucan/src/receipt.rs deleted file mode 100644 index cba3e946..00000000 --- a/ucan/src/receipt.rs +++ /dev/null @@ -1 +0,0 @@ -//! Receipts from running an [`Invocation`]. diff --git a/ucan/src/task.rs b/ucan/src/task.rs deleted file mode 100644 index 9433a8cc..00000000 --- a/ucan/src/task.rs +++ /dev/null @@ -1 +0,0 @@ -//! Tasks (like a distributed function call). diff --git a/ucan/src/time/timestamp.rs b/ucan/src/time/timestamp.rs index 5cb7b8ac..a23ea969 100644 --- a/ucan/src/time/timestamp.rs +++ b/ucan/src/time/timestamp.rs @@ -72,7 +72,7 @@ impl Timestamp { #[allow(clippy::expect_used)] pub fn now() -> Timestamp { Self::new(SystemTime::now()) - .expect("the current time to be somtime in the 3rd millenium CE") + .expect("the current time to be sometime in the 3rd millennium CE") } /// Get a timestamp 5 minutes from now. @@ -85,7 +85,7 @@ impl Timestamp { #[allow(clippy::expect_used)] pub fn five_minutes_from_now() -> Timestamp { Self::new(SystemTime::now() + Duration::from_secs(5 * 60)) - .expect("the current time to be somtime in the 3rd millenium CE") + .expect("the current time to be sometime in the 3rd millennium CE") } /// Get a timestamp 5 years from now. @@ -98,7 +98,7 @@ impl Timestamp { #[allow(clippy::expect_used)] pub fn five_years_from_now() -> Timestamp { Self::new(SystemTime::now() + Duration::from_secs(5 * 365 * 24 * 60 * 60)) - .expect("the current time to be somtime in the 3rd millenium CE") + .expect("the current time to be sometime in the 3rd millennium CE") } /// Convert a [`Timestamp`] to a [Unix timestamp]. @@ -116,19 +116,38 @@ impl Timestamp { } } -#[cfg(all(target_arch = "wasm32", feature = "std"))] -#[wasm_bindgen] +#[cfg(target_arch = "wasm32")] impl Timestamp { - /// Lift a [`js_sys::Date`] into a Rust [`Timestamp`] - pub fn from_date(date_time: js_sys::Date) -> Result { - let millis = date_time.get_time() as u64; - let secs: u64 = (millis / 1000) as u64; - Timestamp::from_unix(secs).map_err(Into::into) + /// Lift a [`js_sys::Date`] into a Rust [`Timestamp`]. + /// + /// # Errors + /// + /// Returns [`OutOfRangeError`] if the date exceeds the 2⁵³ second bound. + pub fn from_date(date_time: &js_sys::Date) -> Result { + let millis = date_time.get_time(); + if millis < 0.0 { + return Err(OutOfRangeError::BeforeEpoch); + } + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + let secs = (millis / 1000.0) as u64; + Timestamp::from_unix(secs) } - /// Lower the [`Timestamp`] to a [`js_sys::Date`] - pub fn to_date(&self) -> js_sys::Date { - js_sys::Date::new(&JsValue::from(u128::from(self.0) * 1000)) + /// Lower the [`Timestamp`] to a [`js_sys::Date`]. + /// + /// # Errors + /// + /// Returns [`OutOfRangeError`] if the timestamp in _milliseconds_ + /// would exceed the 2⁵³ safe-integer bound for JavaScript `Date`. + pub fn to_date(&self) -> Result { + // 2⁵³ / 1000 = max seconds that fit in a JS Date without precision loss + const MAX_SAFE_SECS: u64 = 0x001F_FFFF_FFFF_FFFF / 1000; + if self.0 > MAX_SAFE_SECS { + return Err(OutOfRangeError::TooLarge(self.0)); + } + #[allow(clippy::cast_precision_loss)] + let millis = self.0 as f64 * 1000.0; + Ok(js_sys::Date::new(&wasm_bindgen::JsValue::from(millis))) } } diff --git a/varsig/src/hash.rs b/varsig/src/hash.rs index 18965885..734c0178 100644 --- a/varsig/src/hash.rs +++ b/varsig/src/hash.rs @@ -1,7 +1,7 @@ //! Multihash algorithms. //! //! This is separate from the `multihash-codetable` crate -//! becuase we don't need any of the actual hashing functionality. +//! because we don't need any of the actual hashing functionality. /// Multihash Prefix pub trait Multihasher { diff --git a/varsig/src/header/traits.rs b/varsig/src/header/traits.rs deleted file mode 100644 index 8b137891..00000000 --- a/varsig/src/header/traits.rs +++ /dev/null @@ -1 +0,0 @@ -