From c98d52cc126de0f2b7623ad05b092a26f34ec0e1 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 14 Sep 2026 13:47:00 -0700 Subject: [PATCH] Preserve CLI server URL paths without a trailing slash --- src/client.rs | 4 +-- src/lib.rs | 2 ++ src/server_url.rs | 64 ++++++++++++++++++++++++++++++++++++++ src/subcommand/delete.rs | 4 +-- src/subcommand/download.rs | 4 +-- src/subcommand/gc.rs | 8 +++-- src/subcommand/upload.rs | 4 +-- 7 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 src/server_url.rs diff --git a/src/client.rs b/src/client.rs index 4598c55f..0438abaf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ use { pub(crate) struct Client { client: reqwest::blocking::Client, key: Option, - server: Url, + server: ServerUrl, } impl Client { @@ -65,7 +65,7 @@ impl Client { ) } - pub(crate) fn new(options: &Options, server: Url, auth: Option<&KeyName>) -> Result { + pub(crate) fn new(options: &Options, server: ServerUrl, auth: Option<&KeyName>) -> Result { install_default_crypto_provider()?; let client = reqwest::blocking::Client::builder() diff --git a/src/lib.rs b/src/lib.rs index ad6575fd..082281b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,6 +125,7 @@ use { rotation::Rotation, server::Server, server_error::ServerError, + server_url::ServerUrl, sign_options::SignOptions, signature_error::SignatureError, sort::Sort, @@ -413,6 +414,7 @@ mod resource_type; mod rotation; mod server; mod server_error; +mod server_url; mod sign_options; mod signature; mod signature_error; diff --git a/src/server_url.rs b/src/server_url.rs new file mode 100644 index 00000000..d31f1023 --- /dev/null +++ b/src/server_url.rs @@ -0,0 +1,64 @@ +use super::*; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct ServerUrl(Url); + +impl Deref for ServerUrl { + type Target = Url; + + fn deref(&self) -> &Url { + &self.0 + } +} + +impl FromStr for ServerUrl { + type Err = UrlError; + + fn from_str(s: &str) -> Result { + let mut url = CheckedUrl::check(s)?; + + url.path_segments_mut().unwrap().pop_if_empty().push(""); + + Ok(Self(url)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scheme() { + assert_eq!( + "ftp://foo".parse::().unwrap_err(), + UrlError::Scheme { + scheme: "ftp".into() + }, + ); + } + + #[test] + fn trailing_slash() { + assert_eq!( + "https://foo/bar".parse::().unwrap().as_str(), + "https://foo/bar/", + ); + assert_eq!( + "https://foo/bar/".parse::().unwrap().as_str(), + "https://foo/bar/", + ); + assert_eq!( + "https://foo".parse::().unwrap().as_str(), + "https://foo/", + ); + assert_eq!( + "https://foo/bar" + .parse::() + .unwrap() + .join("api/missing") + .unwrap() + .as_str(), + "https://foo/bar/api/missing", + ); + } +} diff --git a/src/subcommand/delete.rs b/src/subcommand/delete.rs index f17c7e65..471b3915 100644 --- a/src/subcommand/delete.rs +++ b/src/subcommand/delete.rs @@ -13,8 +13,8 @@ pub(crate) struct Delete { value_name = "FINGERPRINT" )] fingerprint: Option, - #[arg(help = "Delete from server at ", long, value_name = "URL", value_parser = CheckedUrl::check)] - server: Url, + #[arg(help = "Delete from server at ", long, value_name = "URL")] + server: ServerUrl, } impl Delete { diff --git a/src/subcommand/download.rs b/src/subcommand/download.rs index fa36eef2..3b62258a 100644 --- a/src/subcommand/download.rs +++ b/src/subcommand/download.rs @@ -24,8 +24,8 @@ pub(crate) struct Download { value_name = "FINGERPRINT" )] package: Option, - #[arg(help = "Download from server at ", long, value_name = "URL", value_parser = CheckedUrl::check)] - server: Url, + #[arg(help = "Download from server at ", long, value_name = "URL")] + server: ServerUrl, } impl Download { diff --git a/src/subcommand/gc.rs b/src/subcommand/gc.rs index 8c6dfa12..8633a3ac 100644 --- a/src/subcommand/gc.rs +++ b/src/subcommand/gc.rs @@ -4,8 +4,12 @@ use super::*; pub(crate) struct Gc { #[arg(help = "Authenticate with key ", long, value_name = "KEY")] auth: Option, - #[arg(help = "Delete unreferenced data on server at ", long, value_name = "URL", value_parser = CheckedUrl::check)] - server: Url, + #[arg( + help = "Delete unreferenced data on server at ", + long, + value_name = "URL" + )] + server: ServerUrl, } impl Gc { diff --git a/src/subcommand/upload.rs b/src/subcommand/upload.rs index fb022c1c..97666ef6 100644 --- a/src/subcommand/upload.rs +++ b/src/subcommand/upload.rs @@ -20,8 +20,8 @@ pub(crate) struct Upload { value_name = "PATH" )] input: Option, - #[arg(help = "Upload to server at ", long, value_name = "URL", value_parser = CheckedUrl::check)] - server: Url, + #[arg(help = "Upload to server at ", long, value_name = "URL")] + server: ServerUrl, } impl Upload {