use std::{fmt, sync::LazyLock}; use log::{debug, trace}; use regex::Regex; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AliasID(String); impl fmt::Display for AliasID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl AsRef<[u8]> for AliasID { fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } impl From for AliasID { fn from(value: String) -> Self { Self(value) } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ShareID(String); impl fmt::Display for ShareID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl From for ShareID { fn from(value: String) -> Self { Self(value) } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileID(String); impl fmt::Display for FileID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl TryFrom for FileID { type Error = crate::Error; fn try_from(value: String) -> crate::Result { /// Pattern breakdown: /// - `^([^:/?#]+)://` – scheme (anything but `:/?#`) + `"://"` /// - `([^/?#]+)` – authority/host (anything but `/?#`) /// - `/api/v2/alias/upload/` – literal path segment /// - `([^/]+)` – capture SID (one or more non-slash chars) /// - `/files/tus/` – literal path segment /// - `(?P[^/]+)` – capture FID (one or more non-slash chars) /// - `$` – end of string static UPLOAD_URL_RE: LazyLock = LazyLock::new(|| { trace!("compiling UPLOAD_URL_RE"); Regex::new( r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P[^/]+)$", ) .expect("Regex compilation failed") }); trace!("TryFrom {value:?}"); if let Some(fid) = UPLOAD_URL_RE .captures(&value) .and_then(|caps| caps.name("fid").map(|m| m.as_str())) { let result = Self(fid.to_owned()); debug!("{result:?}"); Ok(result) } else { Err(crate::Error::mismatch( ":///api/v2/alias/upload//files/tus/", value, )) } } } #[cfg(test)] mod tests { use super::*; #[test] fn valid_urls_produce_expected_file_id() { // a handful of valid‐looking URLs let cases = vec![ ( "http://example.com/api/v2/alias/upload/SID123/files/tus/FID456", "FID456", ), ( "https://my-host:8080/api/v2/alias/upload/another-SID/files/tus/some-file-id", "some-file-id", ), ( "custom+scheme://host/api/v2/alias/upload/x/files/tus/y", "y", ), ]; for (good, expected_fid) in cases { let s = good.to_string(); let file_id = FileID::try_from(s.clone()).expect("URL should parse successfully"); assert_eq!( file_id.0, expected_fid, "Expected `{}` → FileID({}), got {:?}", good, expected_fid, file_id ); } } #[test] fn invalid_urls_return_error() { let bad_inputs = vec![ // missing /api/v2/alias/upload "http://example.com/files/tus/FID", // missing /files/tus "http://example.com/api/v2/alias/upload/SID123/FID456", // trailing slash (doesn't match `$`) "http://example.com/api/v2/alias/upload/SID/files/tus/FID/", // empty fid "http://example.com/api/v2/alias/upload/SID/files/tus/", // random string "just-a-random-string", ]; for bad in bad_inputs { let err = FileID::try_from(bad.to_string()).expect_err("URL should not parse"); // make sure it's the Mismatch variant, and that it contains the original input match err { crate::Error::Mismatch { expected, actual } => { assert_eq!( expected, ":///api/v2/alias/upload//files/tus/", "Error should output expected format" ); assert_eq!(actual, bad.to_string(), "Error should echo back the input"); } _ => panic!("Expected Error::Mismatch for input `{bad}` but got {err:?}"), } } } }