diff --git a/src/appstate.rs b/src/appstate.rs index 99ea3d4..5590e8e 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -62,7 +62,7 @@ impl AppState { if let Some(upl) = self.inner.peek_uploading() { if bar.length().is_none() { bar.set_length(upl.get_size()); - bar.set_message(upl.get_name().to_owned()); + bar.set_message(upl.get_name().to_string()); bar.enable_steady_tick(Duration::from_millis(100)); } diff --git a/src/cachefile.rs b/src/cachefile.rs index 15b18e9..36fed9c 100644 --- a/src/cachefile.rs +++ b/src/cachefile.rs @@ -169,23 +169,22 @@ impl CacheFile { } pub fn rewind_chunk(mut self) -> Option { - self.uploading = Some( - self.uploading - .take() - .expect("rewind_chunk called while not uploading") - .rewind()?, - ); + let upl = self + .uploading + .take() + .expect("rewind_chunk called while not uploading"); + self.uploading = Some(upl.rewind()?); Some(self) } pub fn abort_upload(&mut self) { - self.files.push_front( - self.uploading - .take() - .expect("abort_upload called while not uploading") - .abort(), - ); + let upl = self + .uploading + .take() + .expect("abort_upload called while not uploading"); + + self.files.push_front(upl.abort()); } pub fn share_notify(&self, client: &impl Client) -> crate::Result<()> { diff --git a/src/cli.rs b/src/cli.rs index eb754ba..854dbc8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,11 +2,7 @@ use std::{convert::Infallible, fmt, io, time::Duration}; use base64ct::{Base64UrlUnpadded, Encoding}; use blake2b_simd::Params as Blake2b; -use clap::{ - Parser, - builder::{PossibleValuesParser, TypedValueParser}, - value_parser, -}; +use clap::{Parser, builder::TypedValueParser, value_parser}; use log::LevelFilter; use crate::{ @@ -25,14 +21,6 @@ pub struct Cli { )] timeout: Duration, - /// Protocol for Sharry instance - #[arg( - short, long, - default_value = "https", value_name = "VARIANT", - value_parser = PossibleValuesParser::new(["http", "https"]), - )] - protocol: String, - /// Number of times actions are retried #[arg(short, long, default_value_t = 5, value_name = "N")] retry_limit: u32, @@ -118,7 +106,7 @@ impl Cli { #[must_use] pub fn get_uri(&self) -> Uri { - Uri::new(&self.protocol, &self.url) + Uri::from(self.url.clone()) } #[must_use] diff --git a/src/file/uploading.rs b/src/file/uploading.rs index 290f494..8ac6eea 100644 --- a/src/file/uploading.rs +++ b/src/file/uploading.rs @@ -46,13 +46,11 @@ impl Uploading { self.offset } - pub fn rewind(self) -> Option { + pub fn rewind(mut self) -> Option { if let Some(last_offset) = self.last_offset { - Some(Self { - last_offset: None, - offset: last_offset, - ..self - }) + self.last_offset = None; + self.offset = last_offset; + Some(self) } else { warn!("attempted to rewind twice"); None diff --git a/src/sharry/ids.rs b/src/sharry/ids.rs index b95ef20..782d743 100644 --- a/src/sharry/ids.rs +++ b/src/sharry/ids.rs @@ -86,7 +86,7 @@ impl TryFrom for FileID { .captures(&value) .and_then(|caps| caps.name("fid").map(|m| m.as_str())) { - let result = Self(fid.to_owned()); + let result = Self(fid.to_string()); debug!("{result:?}"); Ok(result) @@ -158,7 +158,8 @@ mod tests { ]; for (good, expected_fid) in cases { - let file_id = FileID::try_from(good.to_owned()).expect("URL should parse successfully"); + let file_id = + FileID::try_from(good.to_string()).expect("URL should parse successfully"); assert_eq!( file_id.0, expected_fid, "Expected `{good}` → FileID({expected_fid}), got {file_id:?}", diff --git a/src/sharry/uri.rs b/src/sharry/uri.rs index 65ba6b0..9f80184 100644 --- a/src/sharry/uri.rs +++ b/src/sharry/uri.rs @@ -1,6 +1,7 @@ -use std::fmt; +use std::{fmt, sync::LazyLock}; -use log::trace; +use log::{debug, trace}; +use regex::Regex; use serde::{Deserialize, Serialize}; /// ID of a file in a Sharry share @@ -24,35 +25,70 @@ impl AsRef<[u8]> for Uri { } } -impl Uri { -/// create a new Sharry URI - pub fn new(scheme: impl fmt::Display, host: impl fmt::Display) -> Self { - Self(format!("{scheme}://{host}")) - } +impl From for Uri { + fn from(value: String) -> Self { + fn parse_url(value: &str) -> Option<(String, String)> { + /// Pattern breakdown: + /// - `^(?P[^:/?#]+)://` - capture scheme (anything but `:/?#`) + `"://"` + /// - `(?P[^/?#]+)` - capture authority/host (anything but `/?#`) + /// - `(/.*)?` - maybe trailing slash and some path + /// - `$` - end of string + static SHARRY_URI_RE: LazyLock = LazyLock::new(|| { + trace!("compiling SHARRY_URI_RE"); -/// arbitrary endpoint in the Sharry API v2 + Regex::new(r"^(?P[^:/?#]+)://(?P[^/?#]+)(/.*)?$") + .expect("Regex compilation failed") + }); + + SHARRY_URI_RE.captures(value).map(|caps| { + let captured = |name| { + caps.name(name) + .expect(&format!("{name} not captured")) + .as_str() + .to_string() + }; + + (captured("scheme"), captured("host")) + }) + } + + trace!("TryFrom {value:?}"); + + if let Some((scheme, host)) = parse_url(&value) { + let result = Self(format!("{scheme}://{host}")); + debug!("{result:?}"); + + result + } else { + Self(value) + } + } +} + +impl Uri { + /// arbitrary endpoint in the Sharry API v2 fn endpoint(&self, path: fmt::Arguments) -> String { let uri = format!("{}/api/v2/{path}", self.0); trace!("endpoint: {uri:?}"); uri } -/// Sharry API endpoint to create a new share + /// Sharry API endpoint to create a new share pub fn share_create(&self) -> String { self.endpoint(format_args!("alias/upload/new")) } -/// Sharry API endpoint to ping a share's notification hook + /// Sharry API endpoint to ping a share's notification hook pub fn share_notify(&self, share_id: &super::ShareID) -> String { self.endpoint(format_args!("alias/mail/notify/{share_id}")) } -/// Sharry API endpoint to create a new file inside a share + /// Sharry API endpoint to create a new file inside a share pub fn file_create(&self, share_id: &super::ShareID) -> String { self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) } -/// Sharry API endpoint to push data into a file inside a share + /// Sharry API endpoint to push data into a file inside a share pub fn file_patch(&self, share_id: &super::ShareID, file_id: &super::FileID) -> String { self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}")) } @@ -69,11 +105,11 @@ mod tests { #[test] fn basic_traits_working() { let cases = vec![ -// simple http host + // simple http host "http://example.com", -// https host with port + // https host with port "https://my-host:8080", -// custom scheme + // custom scheme "custom+scheme://host", ]; @@ -85,19 +121,45 @@ mod tests { } #[test] - fn test_new() { + fn valid_urls_produce_expected_uri() { let cases = vec![ // simple http host - ("http", "example.com", "http://example.com"), + ("http://example.com", "http://example.com"), // https host with port - ("https", "my-host:8080", "https://my-host:8080"), + ("https://my-host:8080", "https://my-host:8080"), + // trailing slash + ("scheme://host/", "scheme://host"), + // with path + ("scheme://host/path/to/whatever", "scheme://host"), // custom scheme - ("custom+scheme", "host", "custom+scheme://host"), + ("custom+scheme://host", "custom+scheme://host"), ]; - for (scheme, host, expected) in cases { - let uri = Uri::new(scheme, host); - assert_eq!(&expected, &uri.to_string()); + for (good, expected) in cases { + let uri = Uri::from(good.to_string()); + check_trait(&expected, &uri.to_string(), "From", "Uri"); + } + } + + #[test] + fn invalid_urls_passed_through() { + let cases = vec![ + // missing “://” + "http:/example.com", + // missing scheme + "://example.com", + // missing host + "http://", + "ftp://?query", + // totally malformed + "just-a-string", + "", + "///", + ]; + + for bad in cases { + let uri = Uri::from(bad.to_string()); + check_trait(&bad, &uri.to_string(), "From", "Uri"); } }