2025-07-03 12:58:53 +00:00
|
|
|
use std::{fmt, sync::LazyLock};
|
2025-06-25 23:42:00 +00:00
|
|
|
|
2025-07-03 12:58:53 +00:00
|
|
|
use log::{debug, trace};
|
|
|
|
|
use regex::Regex;
|
2025-06-25 23:42:00 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
2025-07-01 15:24:26 +00:00
|
|
|
/// ID of a file in a Sharry share
|
|
|
|
|
///
|
|
|
|
|
/// - impl `Clone` as this is just a String
|
|
|
|
|
/// - impl `serde` for cachefile handling
|
|
|
|
|
/// - impl `Display` for formatting compatibility
|
|
|
|
|
/// - impl `AsRef<[u8]>` for hashing with `blake2b_simd`
|
2025-07-01 15:20:07 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
2025-06-25 23:42:00 +00:00
|
|
|
pub struct Uri(String);
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for Uri {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
f.write_str(&self.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AsRef<[u8]> for Uri {
|
|
|
|
|
fn as_ref(&self) -> &[u8] {
|
|
|
|
|
self.0.as_bytes()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 12:58:53 +00:00
|
|
|
impl From<String> for Uri {
|
|
|
|
|
fn from(value: String) -> Self {
|
|
|
|
|
fn parse_url(value: &str) -> Option<(String, String)> {
|
|
|
|
|
/// Pattern breakdown:
|
|
|
|
|
/// - `^(?P<scheme>[^:/?#]+)://` - capture scheme (anything but `:/?#`) + `"://"`
|
|
|
|
|
/// - `(?P<host>[^/?#]+)` - capture authority/host (anything but `/?#`)
|
|
|
|
|
/// - `(/.*)?` - maybe trailing slash and some path
|
|
|
|
|
/// - `$` - end of string
|
|
|
|
|
static SHARRY_URI_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
|
|
|
trace!("compiling SHARRY_URI_RE");
|
|
|
|
|
|
|
|
|
|
Regex::new(r"^(?P<scheme>[^:/?#]+)://(?P<host>[^/?#]+)(/.*)?$")
|
|
|
|
|
.expect("Regex compilation failed")
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
SHARRY_URI_RE.captures(value).map(|caps| {
|
|
|
|
|
let captured = |name| {
|
|
|
|
|
caps.name(name)
|
2025-07-03 14:21:02 +00:00
|
|
|
.unwrap_or_else(|| panic!("{name} not captured"))
|
2025-07-03 12:58:53 +00:00
|
|
|
.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)
|
|
|
|
|
}
|
2025-06-25 23:42:00 +00:00
|
|
|
}
|
2025-07-03 12:58:53 +00:00
|
|
|
}
|
2025-06-25 23:42:00 +00:00
|
|
|
|
2025-07-03 12:58:53 +00:00
|
|
|
impl Uri {
|
2025-07-03 13:03:26 +00:00
|
|
|
/// arbitrary endpoint in the Sharry API v2
|
2025-06-25 23:42:00 +00:00
|
|
|
fn endpoint(&self, path: fmt::Arguments) -> String {
|
|
|
|
|
let uri = format!("{}/api/v2/{path}", self.0);
|
|
|
|
|
trace!("endpoint: {uri:?}");
|
|
|
|
|
uri
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 13:03:26 +00:00
|
|
|
/// Sharry API endpoint to create a new share
|
2025-06-25 23:42:00 +00:00
|
|
|
pub fn share_create(&self) -> String {
|
|
|
|
|
self.endpoint(format_args!("alias/upload/new"))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 13:03:26 +00:00
|
|
|
/// Sharry API endpoint to ping a share's notification hook
|
2025-06-27 01:47:38 +00:00
|
|
|
pub fn share_notify(&self, share_id: &super::ShareID) -> String {
|
2025-06-25 23:42:00 +00:00
|
|
|
self.endpoint(format_args!("alias/mail/notify/{share_id}"))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 13:03:26 +00:00
|
|
|
/// Sharry API endpoint to create a new file inside a share
|
2025-06-27 01:47:38 +00:00
|
|
|
pub fn file_create(&self, share_id: &super::ShareID) -> String {
|
2025-06-25 23:42:00 +00:00
|
|
|
self.endpoint(format_args!("alias/upload/{share_id}/files/tus"))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 13:03:26 +00:00
|
|
|
/// Sharry API endpoint to push data into a file inside a share
|
2025-06-27 01:47:38 +00:00
|
|
|
pub fn file_patch(&self, share_id: &super::ShareID, file_id: &super::FileID) -> String {
|
2025-06-25 23:42:00 +00:00
|
|
|
self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}"))
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-27 16:59:31 +00:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2025-07-03 11:07:22 +00:00
|
|
|
use crate::{
|
|
|
|
|
check_trait,
|
|
|
|
|
sharry::{FileID, ShareID},
|
|
|
|
|
};
|
2025-06-27 16:59:31 +00:00
|
|
|
|
|
|
|
|
#[test]
|
2025-07-03 11:07:22 +00:00
|
|
|
fn basic_traits_working() {
|
2025-07-03 15:43:26 +00:00
|
|
|
let cases = [
|
2025-07-03 13:03:26 +00:00
|
|
|
// simple http host
|
2025-07-03 11:07:22 +00:00
|
|
|
"http://example.com",
|
2025-07-03 13:03:26 +00:00
|
|
|
// https host with port
|
2025-07-03 11:07:22 +00:00
|
|
|
"https://my-host:8080",
|
2025-07-03 13:03:26 +00:00
|
|
|
// custom scheme
|
2025-07-03 11:07:22 +00:00
|
|
|
"custom+scheme://host",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for uri_data in cases {
|
2025-07-03 13:05:56 +00:00
|
|
|
let uri = Uri(uri_data.to_string());
|
2025-07-03 11:07:22 +00:00
|
|
|
check_trait(&uri_data, &uri.to_string(), "Display", "Uri");
|
|
|
|
|
check_trait(&uri_data.as_bytes(), &uri.as_ref(), "AsRef<[u8]>", "Uri");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-07-03 13:03:26 +00:00
|
|
|
fn valid_urls_produce_expected_uri() {
|
2025-07-03 15:43:26 +00:00
|
|
|
let cases = [
|
2025-07-03 11:07:22 +00:00
|
|
|
// simple http host
|
2025-07-03 13:03:26 +00:00
|
|
|
("http://example.com", "http://example.com"),
|
2025-07-03 11:07:22 +00:00
|
|
|
// https host with port
|
2025-07-03 13:03:26 +00:00
|
|
|
("https://my-host:8080", "https://my-host:8080"),
|
|
|
|
|
// trailing slash
|
|
|
|
|
("scheme://host/", "scheme://host"),
|
|
|
|
|
// with path
|
|
|
|
|
("scheme://host/path/to/whatever", "scheme://host"),
|
2025-07-03 11:07:22 +00:00
|
|
|
// custom scheme
|
2025-07-03 13:03:26 +00:00
|
|
|
("custom+scheme://host", "custom+scheme://host"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (good, expected) in cases {
|
|
|
|
|
let uri = Uri::from(good.to_string());
|
2025-07-03 13:10:19 +00:00
|
|
|
check_trait(&expected, &uri.0, "From<String>", "Uri");
|
2025-07-03 13:03:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn invalid_urls_passed_through() {
|
2025-07-03 15:43:26 +00:00
|
|
|
let cases = [
|
2025-07-03 13:03:26 +00:00
|
|
|
// missing “://”
|
|
|
|
|
"http:/example.com",
|
|
|
|
|
// missing scheme
|
|
|
|
|
"://example.com",
|
|
|
|
|
// missing host
|
|
|
|
|
"http://",
|
|
|
|
|
"ftp://?query",
|
|
|
|
|
// totally malformed
|
|
|
|
|
"just-a-string",
|
|
|
|
|
"",
|
|
|
|
|
"///",
|
2025-06-27 16:59:31 +00:00
|
|
|
];
|
|
|
|
|
|
2025-07-03 13:03:26 +00:00
|
|
|
for bad in cases {
|
|
|
|
|
let uri = Uri::from(bad.to_string());
|
2025-07-03 13:10:19 +00:00
|
|
|
check_trait(&bad, &uri.0, "From<String>", "Uri");
|
2025-07-03 11:07:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_endpoint() {
|
2025-07-03 15:43:26 +00:00
|
|
|
let cases = [
|
2025-07-03 11:07:22 +00:00
|
|
|
// simple path
|
|
|
|
|
("path/to/something", "/api/v2/path/to/something"),
|
|
|
|
|
// underscores, hyphens, dots
|
|
|
|
|
("bob_smith-son.eve", "/api/v2/bob_smith-son.eve"),
|
|
|
|
|
// unicode
|
|
|
|
|
("漢字ユーザー", "/api/v2/漢字ユーザー"),
|
|
|
|
|
// empty path
|
|
|
|
|
("", "/api/v2/"),
|
|
|
|
|
// leading/trailing spaces
|
|
|
|
|
(" frank ", "/api/v2/ frank "),
|
|
|
|
|
// uppercase
|
|
|
|
|
("GUEST", "/api/v2/GUEST"),
|
|
|
|
|
// numeric
|
|
|
|
|
("12345", "/api/v2/12345"),
|
|
|
|
|
];
|
|
|
|
|
|
2025-07-03 13:05:56 +00:00
|
|
|
let uri = Uri("".to_string());
|
2025-07-03 11:07:22 +00:00
|
|
|
for (path, expected) in cases {
|
|
|
|
|
assert_eq!(&expected, &uri.endpoint(format_args!("{path}")));
|
2025-06-27 16:59:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-03 11:07:22 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_pub_endpoints() {
|
2025-07-03 13:05:56 +00:00
|
|
|
let uri = Uri("".to_string());
|
|
|
|
|
let share_id = ShareID("sid".to_string());
|
|
|
|
|
let file_id = FileID("fid".to_string());
|
2025-07-03 11:07:22 +00:00
|
|
|
|
|
|
|
|
assert_eq!("/api/v2/alias/upload/new", uri.share_create());
|
|
|
|
|
assert_eq!("/api/v2/alias/mail/notify/sid", uri.share_notify(&share_id));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
"/api/v2/alias/upload/sid/files/tus",
|
|
|
|
|
uri.file_create(&share_id)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
"/api/v2/alias/upload/sid/files/tus/fid",
|
|
|
|
|
uri.file_patch(&share_id, &file_id)
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-06-27 16:59:31 +00:00
|
|
|
}
|