use super::{
CODE_FILE_GROUP, CODE_FILE_NAME, CODE_IMAGE_FORMAT, CODE_IMAGE_QUALITY, CODE_LISTEN_ADDR,
CODE_SCHEMA_NAME, CODE_SHA256, CODE_UUID,
};
use super::{is_disabled, new_error, validate_ascii_name};
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::Path;
use uuid::Uuid;
use validator::ValidationError;
type Result<T> = std::result::Result<T, ValidationError>;
fn check_listen_port(port: u16) -> Result<()> {
if port == 0 {
return Err(new_error(CODE_LISTEN_ADDR, "port cannot be 0".to_string()));
}
Ok(())
}
pub fn x_listen_addr(addr: &str) -> Result<()> {
if is_disabled(CODE_LISTEN_ADDR) {
return Ok(());
}
if let Some(value) = addr.strip_prefix(':') {
let port = value.parse::<u16>().map_err(|_| {
new_error(
CODE_LISTEN_ADDR,
"port must be a number between 1 and 65535".to_string(),
)
})?;
return check_listen_port(port);
}
if let Ok(socket_addr) = addr.parse::<SocketAddr>() {
return check_listen_port(socket_addr.port());
}
let mut addrs = addr
.to_socket_addrs()
.map_err(|_| new_error(CODE_LISTEN_ADDR, "invalid address format".to_string()))?;
let Some(first) = addrs.next() else {
return Err(new_error(
CODE_LISTEN_ADDR,
"no valid address found".to_string(),
));
};
check_listen_port(first.port())
}
pub fn x_uuid(value: &str) -> Result<()> {
if is_disabled(CODE_UUID) {
return Ok(());
}
if value.len() != 36 || Uuid::try_parse(value).is_err() {
return Err(new_error(CODE_UUID, "invalid uuid format".to_string()));
}
Ok(())
}
pub fn x_sha256(value: &str) -> Result<()> {
if is_disabled(CODE_SHA256) {
return Ok(());
}
if value.len() != 64 || !value.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(new_error(CODE_SHA256, "invalid sha256 format".to_string()));
}
Ok(())
}
const FILE_NAME_MAX_LEN: usize = 255;
pub fn x_file_name(name: &str) -> Result<()> {
if is_disabled(CODE_FILE_NAME) {
return Ok(());
}
if name.is_empty() {
return Err(new_error(
CODE_FILE_NAME,
"file name cannot be empty".to_string(),
));
}
if name.len() > FILE_NAME_MAX_LEN {
return Err(new_error(
CODE_FILE_NAME,
format!("file name must be at most {FILE_NAME_MAX_LEN} characters"),
));
}
if name.chars().any(|c| c.is_ascii_control()) {
return Err(new_error(
CODE_FILE_NAME,
"file name must not contain control characters".to_string(),
));
}
if name.contains('/') || name.contains('\\') {
return Err(new_error(
CODE_FILE_NAME,
"file name must not contain path separators".to_string(),
));
}
if name.split('.').any(|seg| seg == "..") || name == ".." || name == "." {
return Err(new_error(
CODE_FILE_NAME,
"file name must not contain path traversal".to_string(),
));
}
if name.starts_with('.') {
return Err(new_error(
CODE_FILE_NAME,
"file name must not start with '.'".to_string(),
));
}
if Path::new(name).extension().is_none() {
return Err(new_error(
CODE_FILE_NAME,
"file name must have an extension".to_string(),
));
}
Ok(())
}
pub fn x_file_group(group: &str) -> Result<()> {
if is_disabled(CODE_FILE_GROUP) {
return Ok(());
}
validate_ascii_name(group, CODE_FILE_GROUP, 100, "file group")
}
pub fn x_image_format(format: &str) -> Result<()> {
if is_disabled(CODE_IMAGE_FORMAT) {
return Ok(());
}
if !["avif", "webp", "png", "jpeg"].contains(&format) {
return Err(new_error(
CODE_IMAGE_FORMAT,
"invalid image format".to_string(),
));
}
Ok(())
}
pub fn x_image_quality(quality: u8) -> Result<()> {
if is_disabled(CODE_IMAGE_QUALITY) {
return Ok(());
}
if !(50..=100).contains(&quality) {
return Err(new_error(
CODE_IMAGE_QUALITY,
"image quality must be between 50 and 100".to_string(),
));
}
Ok(())
}
pub fn x_schema_name(name: &str) -> Result<()> {
if is_disabled(CODE_SCHEMA_NAME) {
return Ok(());
}
validate_ascii_name(name, CODE_SCHEMA_NAME, 50, "schema name")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uuid_requires_real_format_not_just_length() {
assert!(x_uuid("67e55044-10b1-426f-9247-bb680e5fe0c8").is_ok());
assert!(x_uuid("67E55044-10B1-426F-9247-BB680E5FE0C8").is_ok());
assert!(x_uuid(&"a".repeat(36)).is_err());
assert!(x_uuid("67e550441-0b1-426f-9247-bb680e5fe0c8").is_err());
assert!(x_uuid("67e55044-10b1-426f-9247-bb680e5fe0cZ").is_err());
assert!(x_uuid("67e5504410b1426f9247bb680e5fe0c8").is_err());
assert!(x_uuid("{67e55044-10b1-426f-9247-bb680e5fe0c8}").is_err());
assert!(x_uuid("").is_err());
}
#[test]
fn sha256_requires_hex_not_just_length() {
assert!(x_sha256(&"a".repeat(64)).is_ok());
assert!(x_sha256(&"F".repeat(64)).is_ok());
assert!(
x_sha256("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824").is_ok()
);
assert!(x_sha256(&"z".repeat(64)).is_err());
assert!(x_sha256(&"a".repeat(63)).is_err());
assert!(x_sha256(&"a".repeat(65)).is_err());
assert!(x_sha256("").is_err());
}
#[test]
fn listen_addr_accepts_ip_literals_without_dns() {
assert!(x_listen_addr(":8080").is_ok());
assert!(x_listen_addr("127.0.0.1:8080").is_ok());
assert!(x_listen_addr("0.0.0.0:3000").is_ok());
assert!(x_listen_addr("[::1]:8080").is_ok());
assert!(x_listen_addr("[::]:8080").is_ok());
}
#[test]
fn listen_addr_rejects_port_zero_consistently() {
assert!(x_listen_addr(":0").is_err());
assert!(x_listen_addr("0.0.0.0:0").is_err());
assert!(x_listen_addr("[::1]:0").is_err());
}
#[test]
fn file_name_rejects_traversal_and_control_chars() {
assert!(x_file_name("67e55044-10b1-426f-9247-bb680e5fe0c8.png").is_ok());
assert!(x_file_name("report.tar.gz").is_ok());
for bad in [
"../../etc/passwd.txt",
"..",
".",
"a/b.png",
"a\\b.png",
".hidden.png",
"bad\nname.png",
"bad\0name.png",
] {
assert!(x_file_name(bad).is_err(), "{bad:?} 应被拒绝");
}
assert!(x_file_name("noext").is_err());
assert!(x_file_name("").is_err());
assert!(x_file_name(&format!("{}.png", "a".repeat(FILE_NAME_MAX_LEN))).is_err());
}
#[test]
fn group_and_schema_names_use_identifier_allowlist() {
assert!(x_file_group("avatars").is_ok());
assert!(x_file_group("team-a_2024").is_ok());
assert!(x_schema_name("user").is_ok());
for bad in ["a/b", "a b", "a\nb", "a:b", "组"] {
assert!(x_file_group(bad).is_err(), "{bad:?} 应被拒绝");
assert!(x_schema_name(bad).is_err(), "{bad:?} 应被拒绝");
}
assert!(x_file_group("").is_err());
}
#[test]
fn listen_addr_rejects_malformed_port() {
assert!(x_listen_addr(":not-a-number").is_err());
assert!(x_listen_addr(":70000").is_err());
}
}