From 47622eb762e802a9caa2f37d8162eaaf2f9aa9ca Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 10 Apr 2026 16:25:26 +0000 Subject: fix: percent-encode identifier in nostr:// URLs and GRASP HTTP paths Repository identifiers can contain any characters per NIP-01 d-tag rules. Encode them in nostr:// clone URLs (display and parse) and in GRASP //.git paths, aligning with NIP-34 and GRASP-01. --- src/lib/git/nostr_url.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++---- src/lib/repo_ref.rs | 8 ++++--- 2 files changed, 62 insertions(+), 8 deletions(-) (limited to 'src/lib') diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs index c26c56a..d6ee24e 100644 --- a/src/lib/git/nostr_url.rs +++ b/src/lib/git/nostr_url.rs @@ -95,7 +95,7 @@ impl fmt::Display for NostrUrlDecoded { ) )?; } - write!(f, "{}", self.coordinate.identifier) + write!(f, "{}", urlencoding::encode(&self.coordinate.identifier)) } } @@ -178,10 +178,11 @@ impl NostrUrlDecoded { } else { let npub_or_nip05 = part.to_owned(); parts.remove(0); - let identifier = parts - .pop() - .context("nostr url must have an identifier eg. nostr://npub123/repo-identifier")? - .to_string(); + let identifier = urlencoding::decode(parts.pop().context( + "nostr url must have an identifier eg. nostr://npub123/repo-identifier", + )?) + .context("could not percent-decode identifier in nostr git url")? + .into_owned(); for relay in parts { let mut decoded = urlencoding::decode(relay) .context("could not parse relays in nostr git url")? @@ -1057,6 +1058,31 @@ mod tests { Ok(()) } + #[test] + fn identifier_with_spaces_is_percent_encoded() -> Result<()> { + assert_eq!( + format!("{}", NostrUrlDecoded { + original_string: String::new(), + coordinate: Nip19Coordinate { + coordinate: Coordinate { + identifier: "my repo".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + }, + relays: vec![], + }, + protocol: None, + ssh_key_file: None, + nip05: None, + }), + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/my%20repo", + ); + Ok(()) + } + #[test] fn with_protocol() -> Result<()> { assert_eq!( @@ -1352,6 +1378,32 @@ mod tests { Ok(()) } + #[tokio::test] + async fn percent_encoded_identifier_is_decoded() -> Result<()> { + let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/my%20repo".to_string(); + let decoded = NostrUrlDecoded::parse_and_resolve(&url, &None).await?; + assert_eq!(decoded.coordinate.identifier, "my repo"); + Ok(()) + } + + #[tokio::test] + async fn percent_encoded_identifier_round_trips() -> Result<()> { + // parse a URL with an encoded identifier, then re-display it and get the same + // URL back + let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/my%20repo".to_string(); + let decoded = NostrUrlDecoded::parse_and_resolve(&url, &None).await?; + // Display re-encodes, but original_string is stored so we need a fresh struct + let redisplayed = format!( + "{}", + NostrUrlDecoded { + original_string: String::new(), + ..decoded + } + ); + assert_eq!(redisplayed, url); + Ok(()) + } + #[tokio::test] async fn with_server_protocol() -> Result<()> { let url = "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string(); diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs index c0f9136..c4dd820 100644 --- a/src/lib/repo_ref.rs +++ b/src/lib/repo_ref.rs @@ -14,6 +14,7 @@ use nostr::{ }; use nostr_sdk::{Kind, NostrSigner, RelayUrl, Timestamp, Url}; use serde::{Deserialize, Serialize}; +use urlencoding::encode as pct_encode; #[cfg(not(test))] use crate::client::Client; @@ -661,7 +662,7 @@ pub fn detect_existing_grasp_servers( } let clone_url_is_grasp_server_format = if let Ok(npub) = extract_npub(url) { - url.contains(&format!("/{npub}/{identifier}.git")) + url.contains(&format!("/{npub}/{}.git", pct_encode(identifier))) } else { false }; @@ -809,8 +810,9 @@ pub fn format_grasp_server_url_as_clone_url( "https://" }; Ok(format!( - "{prefix}{grasp_server_url}/{}/{identifier}.git", - public_key.to_bech32()? + "{prefix}{grasp_server_url}/{}/{}.git", + public_key.to_bech32()?, + pct_encode(identifier) )) } -- cgit v1.2.3