2025-06-25 23:42:00 +00:00
|
|
|
|
use std::{fmt, sync::LazyLock};
|
|
|
|
|
|
|
|
|
|
|
|
use log::{debug, trace};
|
|
|
|
|
|
use regex::Regex;
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
2025-06-27 01:47:38 +00:00
|
|
|
|
#[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<String> 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<String> for ShareID {
|
|
|
|
|
|
fn from(value: String) -> Self {
|
|
|
|
|
|
Self(value)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-25 23:42:00 +00:00
|
|
|
|
|
|
|
|
|
|
#[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<String> for FileID {
|
2025-06-27 02:03:20 +00:00
|
|
|
|
type Error = crate::Error;
|
2025-06-25 23:42:00 +00:00
|
|
|
|
|
2025-06-27 02:03:20 +00:00
|
|
|
|
fn try_from(value: String) -> crate::Result<Self> {
|
2025-06-25 23:42:00 +00:00
|
|
|
|
/// 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<fid>[^/]+)` – capture FID (one or more non-slash chars)
|
|
|
|
|
|
/// - `$` – end of string
|
|
|
|
|
|
static UPLOAD_URL_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
|
|
|
|
trace!("compiling UPLOAD_URL_RE");
|
|
|
|
|
|
|
|
|
|
|
|
Regex::new(
|
|
|
|
|
|
r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$",
|
|
|
|
|
|
)
|
|
|
|
|
|
.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 {
|
2025-06-27 02:03:20 +00:00
|
|
|
|
Err(crate::Error::mismatch(
|
2025-06-26 09:59:59 +00:00
|
|
|
|
"<proto>://<base>/api/v2/alias/upload/<share>/files/tus/<file>",
|
|
|
|
|
|
value,
|
|
|
|
|
|
))
|
2025-06-25 23:42:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-06-26 10:06:43 +00:00
|
|
|
|
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
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-25 23:42:00 +00:00
|
|
|
|
|
2025-06-26 10:06:43 +00:00
|
|
|
|
#[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 {
|
2025-06-27 02:03:20 +00:00
|
|
|
|
crate::Error::Mismatch { expected, actual } => {
|
2025-06-26 10:06:43 +00:00
|
|
|
|
assert_eq!(
|
|
|
|
|
|
expected, "<proto>://<base>/api/v2/alias/upload/<share>/files/tus/<file>",
|
|
|
|
|
|
"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:?}"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-25 23:42:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|