#[must_use]
pub fn is_windows_absolute(path: &str) -> bool {
let bytes = path.as_bytes();
if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/')
{
return true;
}
if bytes.len() >= 2 && bytes[0] == b'\\' && bytes[1] == b'\\' {
return true;
}
false
}
#[must_use]
pub fn has_traversal(path: &str) -> bool {
path.split(['/', '\\']).any(|seg| seg == "..")
}
#[must_use]
pub fn is_safe_image_src(src: &str) -> bool {
if src.is_empty() {
return false;
}
if src.contains('\0') {
return false;
}
let trimmed = src.trim_start();
let normalised = trimmed.to_ascii_lowercase();
if normalised.starts_with('/') {
return false;
}
if is_windows_absolute(trimmed) {
return false;
}
if has_traversal(src) {
return false;
}
if let Some(colon_pos) = normalised.find(':') {
let before_colon = &normalised[..colon_pos];
if !before_colon.contains('/') {
return before_colon == "http" || before_colon == "https";
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn windows_drive_letter_backslash() {
assert!(is_windows_absolute(r"C:\photo.jpg"));
assert!(is_windows_absolute(r"D:\Users\photo.jpg"));
}
#[test]
fn windows_drive_letter_forward_slash() {
assert!(is_windows_absolute("C:/photo.jpg"));
}
#[test]
fn windows_unc_path() {
assert!(is_windows_absolute(r"\\server\share\photo.jpg"));
}
#[test]
fn relative_path_not_windows_absolute() {
assert!(!is_windows_absolute("images/photo.jpg"));
assert!(!is_windows_absolute("photo.jpg"));
}
#[test]
fn unix_absolute_not_windows_absolute() {
assert!(!is_windows_absolute("/etc/passwd"));
}
#[test]
fn forward_slash_traversal() {
assert!(has_traversal("../photo.jpg"));
assert!(has_traversal("images/../../etc/passwd"));
}
#[test]
fn backslash_traversal() {
assert!(has_traversal(r"..\photo.jpg"));
assert!(has_traversal(r"images\..\..\etc\passwd"));
}
#[test]
fn no_traversal() {
assert!(!has_traversal("images/photo.jpg"));
assert!(!has_traversal("photo.jpg"));
assert!(!has_traversal(r"images\photo.jpg"));
}
#[test]
fn double_dot_in_filename_not_traversal() {
assert!(!has_traversal("file..name.jpg"));
}
#[test]
fn safe_src_relative_paths() {
assert!(is_safe_image_src("photo.jpg"));
assert!(is_safe_image_src("images/photo.jpg"));
assert!(is_safe_image_src("path/to:file.jpg")); }
#[test]
fn safe_src_http_and_https() {
assert!(is_safe_image_src("http://example.com/photo.jpg"));
assert!(is_safe_image_src("https://example.com/photo.jpg"));
}
#[test]
fn safe_src_rejects_dangerous_schemes() {
assert!(!is_safe_image_src("javascript:alert(1)"));
assert!(!is_safe_image_src("data:image/png;base64,AAAA"));
assert!(!is_safe_image_src("file:///etc/passwd"));
assert!(!is_safe_image_src("blob:https://example.com/uuid"));
assert!(!is_safe_image_src("vbscript:msgbox"));
assert!(!is_safe_image_src("mhtml:file:///etc/passwd"));
}
#[test]
fn safe_src_rejects_absolute_paths() {
assert!(!is_safe_image_src("/etc/passwd"));
assert!(!is_safe_image_src(r"C:\Users\secret"));
assert!(!is_safe_image_src(r"\\server\share\file"));
}
#[test]
fn safe_src_rejects_empty_and_null() {
assert!(!is_safe_image_src(""));
assert!(!is_safe_image_src("photo\0.jpg"));
}
#[test]
fn safe_src_rejects_traversal() {
assert!(!is_safe_image_src("../photo.jpg"));
assert!(!is_safe_image_src("images/../../etc/passwd"));
}
#[test]
fn safe_src_case_insensitive_scheme() {
assert!(!is_safe_image_src("JavaScript:alert(1)"));
assert!(is_safe_image_src("HTTPS://example.com/photo.jpg"));
}
#[test]
fn safe_src_rejects_scheme_with_whitespace_prefix() {
assert!(!is_safe_image_src(" javascript:alert(1)"));
assert!(!is_safe_image_src("\tfile:///etc/passwd"));
}
}