pub(crate) fn path_extension(path: &str) -> Option<String> {
let filename = path.rsplit('/').next().unwrap_or(path);
let (stem, ext) = filename.rsplit_once('.')?;
if stem.is_empty() {
return None;
}
Some(ext.to_ascii_lowercase())
}
pub(crate) fn path_extension_lower(path: &str) -> String {
let path = path.split(['?', '#']).next().unwrap_or(path);
let filename = path.rsplit('/').next().unwrap_or(path);
match filename.rsplit_once('.') {
Some((_, ext)) if !ext.is_empty() => ext.to_ascii_lowercase(),
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ext_plain_file() {
assert_eq!(path_extension("file.md"), Some("md".to_owned()));
}
#[test]
fn ext_uppercase_lowercased() {
assert_eq!(path_extension("photo.JPG"), Some("jpg".to_owned()));
}
#[test]
fn ext_with_dir() {
assert_eq!(path_extension("dir/sub/file.mp4"), Some("mp4".to_owned()));
}
#[test]
fn ext_dotfile_returns_none() {
assert_eq!(path_extension(".gitignore"), None);
assert_eq!(path_extension("dir/.env"), None);
}
#[test]
fn ext_no_dot_returns_none() {
assert_eq!(path_extension("README"), None);
}
#[test]
fn ext_query_string_not_stripped() {
assert_ne!(path_extension("file.mp3?v=2"), Some("mp3".to_owned()));
}
#[test]
fn lower_plain_file() {
assert_eq!(path_extension_lower("file.md"), "md");
}
#[test]
fn lower_uppercase_lowercased() {
assert_eq!(path_extension_lower("photo.JPG"), "jpg");
}
#[test]
fn lower_with_dir() {
assert_eq!(path_extension_lower("dir/file.mp4"), "mp4");
}
#[test]
fn lower_no_extension() {
assert_eq!(path_extension_lower("noext"), "");
}
#[test]
fn lower_strips_query() {
assert_eq!(path_extension_lower("track.mp3?v=2"), "mp3");
}
#[test]
fn lower_strips_fragment() {
assert_eq!(path_extension_lower("page.html#section"), "html");
}
#[test]
fn lower_strips_query_and_fragment() {
assert_eq!(path_extension_lower("file.ogg?v=1#t=30"), "ogg");
}
#[test]
fn lower_empty_string() {
assert_eq!(path_extension_lower(""), "");
}
}