some changes
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
||||
* Source-Available Code / Dual-Licensed.
|
||||
*
|
||||
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
||||
* Commercial or production use requires a valid paid license.
|
||||
* See LICENSE file in repository root for details.
|
||||
*/
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
const FILE_ROUTE_PREFIX: &str = "/f/";
|
||||
|
||||
pub fn normalize_public_base(value: Option<String>) -> Result<Option<String>, Box<dyn Error>> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let candidate = if value.starts_with("http://") || value.starts_with("https://") {
|
||||
value.trim_end_matches('/').to_owned()
|
||||
} else {
|
||||
format!("https://{}", value.trim_end_matches('/'))
|
||||
};
|
||||
let authority = candidate
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| candidate.strip_prefix("http://"))
|
||||
.ok_or("FILES_PUBLIC_URL must use http:// or https://")?;
|
||||
|
||||
if authority.is_empty()
|
||||
|| authority.chars().any(|character| {
|
||||
matches!(character, '/' | '\\' | '?' | '#' | '@') || character.is_whitespace()
|
||||
})
|
||||
{
|
||||
return Err(
|
||||
"FILES_PUBLIC_URL must be a domain or HTTP(S) origin without a path, query, credentials, or fragment"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some(candidate))
|
||||
}
|
||||
|
||||
pub fn canonical_file_path(value: &str) -> Option<String> {
|
||||
let value = value.trim();
|
||||
let path = if value.starts_with('/') {
|
||||
value
|
||||
} else {
|
||||
let (_, after_scheme) = value.split_once("://")?;
|
||||
let path_start = after_scheme.find('/')?;
|
||||
&after_scheme[path_start..]
|
||||
};
|
||||
let path = path
|
||||
.split(|character| matches!(character, '?' | '#'))
|
||||
.next()
|
||||
.unwrap_or(path);
|
||||
if !path.starts_with(FILE_ROUTE_PREFIX) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut parts = path.trim_start_matches('/').split('/');
|
||||
let route = parts.next()?;
|
||||
let token = parts.next()?;
|
||||
let filename = parts.next()?;
|
||||
if route != "f" || token.is_empty() || filename.is_empty() || parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(format!("/f/{token}/{filename}"))
|
||||
}
|
||||
|
||||
pub fn stored_file_path(token: &str, filename: &str) -> String {
|
||||
format!("/f/{token}/{filename}")
|
||||
}
|
||||
|
||||
pub fn public_file_url(public_base: Option<&str>, stored_url: &str) -> String {
|
||||
let Some(canonical) = canonical_file_path(stored_url) else {
|
||||
return stored_url.to_owned();
|
||||
};
|
||||
match public_base {
|
||||
Some(base) => format!("{}{canonical}", base.trim_end_matches('/')),
|
||||
None => canonical,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn normalizes_bare_domain_and_http_origins() {
|
||||
assert_eq!(
|
||||
normalize_public_base(Some("files.note.example.com".into())).unwrap(),
|
||||
Some("https://files.note.example.com".into())
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_public_base(Some("http://localhost:3001/".into())).unwrap(),
|
||||
Some("http://localhost:3001".into())
|
||||
);
|
||||
assert_eq!(normalize_public_base(Some(" ".into())).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_origin_public_urls() {
|
||||
assert!(normalize_public_base(Some("ftp://files.example.com".into())).is_err());
|
||||
assert!(normalize_public_base(Some("https://files.example.com/path".into())).is_err());
|
||||
assert!(normalize_public_base(Some("https://user@files.example.com".into())).is_err());
|
||||
assert!(normalize_public_base(Some("files.example.com\\path".into())).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_canonical_path_from_relative_and_absolute_urls() {
|
||||
assert_eq!(
|
||||
canonical_file_path("/f/token/image.png"),
|
||||
Some("/f/token/image.png".into())
|
||||
);
|
||||
assert_eq!(
|
||||
canonical_file_path("https://files.example.com/f/token/image.png"),
|
||||
Some("/f/token/image.png".into())
|
||||
);
|
||||
assert_eq!(
|
||||
canonical_file_path("https://files.example.com/f/token/image.png?download=1"),
|
||||
Some("/f/token/image.png".into())
|
||||
);
|
||||
assert_eq!(canonical_file_path("/files/token/image.png"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switches_between_custom_origin_and_application_path() {
|
||||
let stored = "/f/token/manual.pdf";
|
||||
assert_eq!(public_file_url(None, stored), stored);
|
||||
assert_eq!(
|
||||
public_file_url(Some("https://files.example.com"), stored),
|
||||
"https://files.example.com/f/token/manual.pdf"
|
||||
);
|
||||
assert_eq!(
|
||||
public_file_url(None, "https://old.example.com/f/token/manual.pdf"),
|
||||
stored
|
||||
);
|
||||
assert_eq!(
|
||||
public_file_url(Some("https://files.example.com"), "/invalid/path"),
|
||||
"/invalid/path"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user