Skip to content

Commit

Permalink
refine did-url handling (percent encoding queries)
Browse files Browse the repository at this point in the history
Signed-off-by: George Mulhearn <[email protected]>
  • Loading branch information
gmulhearn-anonyome committed Dec 10, 2024
1 parent a123936 commit 3e4f1a2
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 23 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions aries/aries_vcx_ledger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
vdr_proxy_ledger = ["dep:indy-vdr-proxy-client"]
cheqd = ["dep:did_cheqd", "dep:did_resolver"]
cheqd = ["dep:did_cheqd", "dep:did_resolver", "dep:url"]

[dependencies]
aries_vcx_wallet = { path = "../aries_vcx_wallet" }
Expand All @@ -20,6 +20,7 @@ indy-vdr.workspace = true
indy-vdr-proxy-client = { workspace = true, optional = true }
did_cheqd = { path = "../../did_core/did_methods/did_cheqd", optional = true }
did_resolver = { path = "../../did_core/did_resolver", optional = true }
url = { version = "2.4.1", optional = true }
serde_json = "1.0.95"
public_key = { path = "../../did_core/public_key" }
async-trait = "0.1.68"
Expand All @@ -40,4 +41,3 @@ tokio = { version = "1.38.0", default-features = false, features = [
chrono = { version = "0.4", default-features = true }
mockall = "0.13.1"
uuid = { version = "1.4.1", default-features = false, features = ["v4"] }

16 changes: 11 additions & 5 deletions aries/aries_vcx_ledger/src/ledger/cheqd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use models::{
CheqdAnoncredsRevocationStatusList, CheqdAnoncredsSchema,
};
use serde::{Deserialize, Serialize};
use url::Url;

use super::base_ledger::{AnoncredsLedgerRead, AnoncredsLedgerSupport};
use crate::errors::error::{VcxLedgerError, VcxLedgerResult};
Expand Down Expand Up @@ -185,12 +186,17 @@ impl AnoncredsLedgerRead for CheqdAnoncredsLedgerRead {
DateTime::from_timestamp(timestamp as i64, 0).ok_or(VcxLedgerError::InvalidInput(
format!("input status list timestamp is not valid {timestamp}"),
))?;

// assemble query
let xml_dt = resource_dt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let query = format!(
"{did}?resourceType={STATUS_LIST_RESOURCE_TYPE}&resourceName={name}&\
resourceVersionTime={xml_dt}"
);
let query_url = DidUrl::parse(query)?;
let mut query = Url::parse(did)
.map_err(|e| VcxLedgerError::InvalidInput(format!("cannot parse DID as URL: {e}")))?;
query
.query_pairs_mut()
.append_pair("resourceType", STATUS_LIST_RESOURCE_TYPE)
.append_pair("resourceName", name)
.append_pair("resourceVersionTime", &xml_dt);
let query_url = DidUrl::parse(query.to_string())?;

let resource = self.resolver.resolve_resource(&query_url).await?;
self.check_resource_type(&resource, STATUS_LIST_RESOURCE_TYPE)?;
Expand Down
1 change: 1 addition & 0 deletions did_core/did_parser_nom/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
nom = "7.1.3"
serde = "1.0.160"
log = "0.4.16"
percent-encoding = "2"

[dev-dependencies]
serde_test = "1.0.176"
Expand Down
12 changes: 10 additions & 2 deletions did_core/did_parser_nom/src/did_url/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ impl DidUrl {
.iter()
.map(|(k, v)| {
(
self.did_url[k.clone()].to_string(),
self.did_url[v.clone()].to_string(),
query_percent_decode(&self.did_url[k.clone()]),
query_percent_decode(&self.did_url[v.clone()]),
)
})
.collect()
Expand Down Expand Up @@ -109,6 +109,14 @@ impl DidUrl {
}
}

/// Decode percent-encoded URL query item (application/x-www-form-urlencoded encoded).
/// Primary difference from general percent encoding is encoding of ' ' as '+'
fn query_percent_decode(input: &str) -> String {
percent_encoding::percent_decode_str(&input.replace('+', " "))
.decode_utf8_lossy()
.into_owned()
}

impl TryFrom<String> for DidUrl {
type Error = ParseError;

Expand Down
43 changes: 29 additions & 14 deletions did_core/did_parser_nom/src/did_url/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use std::collections::HashMap;
use nom::{
branch::alt,
bytes::complete::{tag, take_while1},
character::complete::{char, one_of},
character::complete::{char, one_of, satisfy},
combinator::{all_consuming, cut, opt, recognize, success},
multi::{many0, separated_list0},
sequence::{preceded, separated_pair},
IResult,
multi::{many0, many1, separated_list0},
sequence::{preceded, separated_pair, tuple},
AsChar, IResult,
};

type UrlPart<'a> = (&'a str, Option<Vec<(&'a str, &'a str)>>, Option<&'a str>);
Expand All @@ -27,14 +27,29 @@ fn is_sub_delims(c: char) -> bool {
"!$&'()*+,;=".contains(c)
}

// pct-encoded = "%" HEXDIG HEXDIG
fn pct_encoded(input: &str) -> IResult<&str, &str> {
recognize(tuple((
tag("%"),
satisfy(|c| c.is_hex_digit()),
satisfy(|c| c.is_hex_digit()),
)))(input)
}

// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
fn is_pchar(c: char) -> bool {
is_unreserved(c) || is_sub_delims(c) || ":@".contains(c)
fn pchar(input: &str) -> IResult<&str, &str> {
alt((
recognize(satisfy(is_unreserved)),
pct_encoded,
recognize(satisfy(is_sub_delims)),
tag(":"),
tag("@"),
))(input)
}

// segment = *pchar
fn segment(input: &str) -> IResult<&str, &str> {
take_while1(is_pchar)(input)
recognize(many1(pchar))(input)
}

// path-abempty = *( "/" segment )
Expand All @@ -44,17 +59,17 @@ fn path_abempty(input: &str) -> IResult<&str, &str> {

// fragment = *( pchar / "/" / "?" )
pub(super) fn fragment_parser(input: &str) -> IResult<&str, &str> {
fn is_fragment_char(c: char) -> bool {
is_pchar(c) || "/?".contains(c)
fn fragment_element(input: &str) -> IResult<&str, &str> {
alt(((pchar), tag("/"), tag("?")))(input)
}

take_while1(is_fragment_char)(input)
recognize(many1(fragment_element))(input)
}

// query = *( pchar / "/" / "?" )
fn query_key_value_pair(input: &str) -> IResult<&str, (&str, &str)> {
fn is_query_char(c: char) -> bool {
is_pchar(c) || "/?".contains(c)
fn query_element(input: &str) -> IResult<&str, &str> {
alt(((pchar), tag("/"), tag("?")))(input)
}

let (remaining, (key, value)) = cut(separated_pair(
Expand All @@ -63,9 +78,9 @@ fn query_key_value_pair(input: &str) -> IResult<&str, (&str, &str)> {
alt((take_while1(|c| !"&#?".contains(c)), success(""))),
))(input)?;

cut(all_consuming(take_while1(is_query_char)))(key)?;
cut(all_consuming(many1(query_element)))(key)?;
if !value.is_empty() {
cut(all_consuming(take_while1(is_query_char)))(value)?;
cut(all_consuming(many1(query_element)))(value)?;
}

Ok((remaining, (key, value)))
Expand Down
4 changes: 4 additions & 0 deletions did_core/did_parser_nom/tests/did_url/negative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ test_cases_negative! {
"did:example:123456789abcdefghi&query1=value1"
query_invalid_char:
"did:example:123456789abcdefghi?query1=v^lue1"
query_unfinished_pct_encoding:
"did:example:123456789?query=a%3&query2=b"
query_invalid_space_char:
"did:example:123456789?query=a b"
relative_empty_path: "/"
relative_empty_path_and_query: "/?"
relative_empty_path_and_fragment: "/#"
Expand Down
30 changes: 30 additions & 0 deletions did_core/did_parser_nom/tests/did_url/positive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,4 +384,34 @@ test_cases_positive! {
("resourceType".to_string(), "anonCredsCredDef".to_string()),
].into_iter().collect()
}
test_case30:
"did:cheqd:testnet:36e695a3-f133-46ec-ac1e-79900a927f67?resourceType=anonCredsStatusList&resourceName=Example+schema-default-0&resourceVersionTime=2024-12-10T04%3A13%3A50.000Z",
Some("did:cheqd:testnet:36e695a3-f133-46ec-ac1e-79900a927f67"),
Some("cheqd"),
Some("testnet"),
Some("36e695a3-f133-46ec-ac1e-79900a927f67"),
None,
None,
{
vec![
("resourceName".to_string(), "Example schema-default-0".to_string()),
("resourceType".to_string(), "anonCredsStatusList".to_string()),
("resourceVersionTime".to_string(), "2024-12-10T04:13:50.000Z".to_string()),
].into_iter().collect()
}
test_case31:
"did:example:123?foo+bar=123&bar%20foo=123%20123&h3%21%210%20=w%40rld%3D%3D",
Some("did:example:123"),
Some("example"),
None,
Some("123"),
None,
None,
{
vec![
("foo bar".to_string(), "123".to_string()),
("bar foo".to_string(), "123 123".to_string()),
("h3!!0 ".to_string(), "w@rld==".to_string())
].into_iter().collect()
}
}

0 comments on commit 3e4f1a2

Please sign in to comment.