/* * 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) -> Result, Box> { 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 { 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)] #[path = "tests/file_urls.rs"] mod tests;