use path_security::validate_path; use std::path::{Path, PathBuf}; pub fn parse_gemini_url(request: &str, expected_host: &str) -> Result { if let Some(url) = request.strip_prefix("gemini://") { let host_end = url.find('/').unwrap_or(url.len()); let host = &url[..host_end]; if host != expected_host { return Err(()); // Hostname mismatch } let path = if host_end < url.len() { &url[host_end..] } else { "/" }; Ok(path.trim().to_string()) } else { Err(()) } } pub fn resolve_file_path(path: &str, dir: &str) -> Result { let file_path_str = if path == "/" { "index.gmi".to_string() } else if path.ends_with('/') { format!("{}index.gmi", &path[1..]) } else { path[1..].to_string() }; match validate_path(Path::new(&file_path_str), Path::new(dir)) { Ok(safe_path) => Ok(safe_path), Err(_) => Err(()), } } pub fn get_mime_type(file_path: &Path) -> &str { if let Some(ext) = file_path.extension() { match ext.to_str() { Some("gmi") => "text/gemini", Some("txt") => "text/plain", Some("html") => "text/html", Some("png") => "image/png", Some("jpg") | Some("jpeg") => "image/jpeg", Some("webp") => "image/webp", Some("gif") => "image/gif", _ => "application/octet-stream", } } else { "application/octet-stream" } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_parse_gemini_url_valid() { assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/", "gemini.jeena.net"), Ok("/".to_string())); assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/posts/test", "gemini.jeena.net"), Ok("/posts/test".to_string())); } #[test] fn test_parse_gemini_url_invalid_host() { assert!(parse_gemini_url("gemini://foo.com/", "gemini.jeena.net").is_err()); } #[test] fn test_parse_gemini_url_no_prefix() { assert!(parse_gemini_url("http://gemini.jeena.net/", "gemini.jeena.net").is_err()); } #[test] fn test_resolve_file_path_root() { let temp_dir = TempDir::new().unwrap(); assert!(resolve_file_path("/", temp_dir.path().to_str().unwrap()).is_ok()); } #[test] fn test_resolve_file_path_directory() { let temp_dir = TempDir::new().unwrap(); std::fs::create_dir(temp_dir.path().join("test")).unwrap(); assert!(resolve_file_path("/test/", temp_dir.path().to_str().unwrap()).is_ok()); } #[test] fn test_resolve_file_path_file() { let temp_dir = TempDir::new().unwrap(); assert!(resolve_file_path("/test.gmi", temp_dir.path().to_str().unwrap()).is_ok()); } #[test] fn test_resolve_file_path_traversal() { let temp_dir = TempDir::new().unwrap(); assert!(resolve_file_path("/../etc/passwd", temp_dir.path().to_str().unwrap()).is_err()); } #[test] fn test_get_mime_type_gmi() { let path = Path::new("test.gmi"); assert_eq!(get_mime_type(path), "text/gemini"); } #[test] fn test_get_mime_type_png() { let path = Path::new("test.png"); assert_eq!(get_mime_type(path), "image/png"); } #[test] fn test_get_mime_type_unknown() { let path = Path::new("test.xyz"); assert_eq!(get_mime_type(path), "application/octet-stream"); } #[test] fn test_get_mime_type_no_extension() { let path = Path::new("test"); assert_eq!(get_mime_type(path), "application/octet-stream"); } }