Skip to content

Commit

Permalink
Work around '#' escaping bug in bip21 crate
Browse files Browse the repository at this point in the history
The `pj` parameter of the BIP 21 URL is itself a URL which contains a
fragment.

This is not escaped by bip21 during serialization, which according to
RFC 3986 truncates the `pj` parameter, making everything that follows
part of the fragment.

Deserialization likewise ignores #, parsing it as part of the value
which round trips with the incorrect serialization behavior.
  • Loading branch information
nothingmuch committed Oct 23, 2024
1 parent 047ff35 commit 16e7fb7
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 7 deletions.
25 changes: 22 additions & 3 deletions payjoin/src/uri/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,26 @@ impl PayjoinExtras {
}

pub type Uri<'a, NetworkValidation> = bip21::Uri<'a, NetworkValidation, MaybePayjoinExtras>;
pub type PjUri<'a> = bip21::Uri<'a, NetworkChecked, PayjoinExtras>;
#[derive(Clone)]
pub struct PjUri<'a>(bip21::Uri<'a, NetworkChecked, PayjoinExtras>);

impl<'a> std::fmt::Display for PjUri<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// FIXME bip21 does not escape # in pj parameter
// work around this by overriding
let malformed_uri = format!("{}", self.0);
let escaped = malformed_uri.replacen("#", "%23", 1);
write!(f, "{}", escaped)
}
}

impl<'a> std::ops::Deref for PjUri<'a> {
type Target = bip21::Uri<'a, NetworkChecked, PayjoinExtras>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

mod sealed {
use bitcoin::address::NetworkChecked;
Expand All @@ -65,7 +84,7 @@ impl<'a> UriExt<'a> for Uri<'a, NetworkChecked> {
uri.label = self.label;
uri.message = self.message;

Ok(uri)
Ok(PjUri(uri))
}
MaybePayjoinExtras::Unsupported => {
let mut uri = bip21::Uri::new(self.address);
Expand Down Expand Up @@ -157,7 +176,7 @@ impl PjUriBuilder {
pj_uri.amount = self.amount;
pj_uri.label = self.label.map(Into::into);
pj_uri.message = self.message.map(Into::into);
pj_uri
PjUri(pj_uri)
}
}

Expand Down
19 changes: 15 additions & 4 deletions payjoin/src/uri/url_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,20 @@ mod tests {
#[test]
fn test_valid_v2_url_fragment_on_bip21() {
let uri = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=0.01\
&pj=https://example.com\
#ohttp%3DAQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw";
let uri = Uri::try_from(uri).unwrap().assume_checked().check_pj_supported().unwrap();
assert!(uri.extras.endpoint().ohttp().is_some());
&pj=https://example.com/\
%23ohttp%3DAQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw\
&pjos=0";
let pjuri = Uri::try_from(uri).unwrap().assume_checked().check_pj_supported().unwrap();

assert!(pjuri.extras.endpoint().ohttp().is_some());
assert_eq!(format!("{}", pjuri), uri);

let reordered = "bitcoin:12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX?amount=0.01\
&pjos=0&pj=https://example.com/\
%23ohttp%3DAQO6SMScPUqSo60A7MY6Ak2hDO0CGAxz7BLYp60syRu0gw";
let pjuri =
Uri::try_from(reordered).unwrap().assume_checked().check_pj_supported().unwrap();
assert!(pjuri.extras.endpoint().ohttp().is_some());
assert_eq!(format!("{}", pjuri), uri);
}
}

0 comments on commit 16e7fb7

Please sign in to comment.