pub(crate) struct TemplateResourceUtils;
impl TemplateResourceUtils {
pub(crate) fn clean_path(path: Option<&str>) -> Option<String> {
let path = path?;
let unix_path = path.replace('\\', "/");
if unix_path.is_empty() || (!unix_path.contains("/.") && !unix_path.contains("//")) {
return Some(unix_path);
}
let root_based = unix_path.starts_with('/');
let traversal_path = if root_based {
unix_path
} else {
format!("/{unix_path}")
};
let mut valid_tokens = Vec::new();
let mut top_count = 0_usize;
for token in traversal_path.split('/').rev() {
if token.is_empty() || token == "." {
continue;
}
if token == ".." {
top_count += 1;
} else if top_count > 0 {
top_count -= 1;
} else {
valid_tokens.push(token);
}
}
let mut cleaned = String::new();
for token in valid_tokens {
cleaned.insert_str(0, token);
cleaned.insert(0, '/');
}
for _ in 0..top_count {
cleaned.insert_str(0, "/..");
}
if !root_based {
cleaned.remove(0);
}
Some(cleaned)
}
pub(crate) fn compute_relative_location(location: &str, relative_location: &str) -> String {
if let Some(separator_pos) = location.rfind('/') {
let mut relative = String::with_capacity(location.len() + relative_location.len());
relative.push_str(&location[..separator_pos]);
if !relative_location.starts_with('/') {
relative.push('/');
}
relative.push_str(relative_location);
return relative;
}
relative_location.to_owned()
}
pub(crate) fn compute_base_name(path: Option<&str>) -> Option<String> {
let path = path?;
if path.is_empty() {
return None;
}
let base_path = path.strip_suffix('/').unwrap_or(path);
if let Some(slash_pos) = base_path.rfind('/') {
if let Some(dot_pos) = base_path.rfind('.')
&& dot_pos > slash_pos + 1
{
return Some(base_path[slash_pos + 1..dot_pos].to_owned());
}
return Some(base_path[slash_pos + 1..].to_owned());
}
if let Some(dot_pos) = base_path.rfind('.') {
return Some(base_path[..dot_pos].to_owned());
}
(!base_path.is_empty()).then(|| base_path.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::TemplateResourceUtils;
#[test]
fn computes_relative_locations_with_java_directory_rules() {
let cases = [
("/", "/", "/"),
("/", "something", "/something"),
("/something", "/", "/"),
("something", "/", "/"),
("something/else", "/", "something/"),
("something/else/more", "/", "something/else/"),
("something/else/more", "less", "something/else/less"),
(
"something/else/more.html",
"more.properties",
"something/else/more.properties",
),
(
"something/else/more.html",
"more_es.properties",
"something/else/more_es.properties",
),
(
"something/else/more.html",
"../more_es.properties",
"something/else/../more_es.properties",
),
(
"something/else/more.html",
"../../more_es.properties",
"something/else/../../more_es.properties",
),
];
for (location, relative_location, expected) in cases {
assert_eq!(
TemplateResourceUtils::compute_relative_location(location, relative_location),
expected
);
}
}
#[test]
fn cleans_paths_with_the_upstream_reverse_traversal_algorithm() {
let cases = [
("/", "/"),
("something", "something"),
("/something", "/something"),
("something/else", "something/else"),
("//something//else", "/something/else"),
("//something//a//..//else", "/something/else"),
("something/else/more", "something/else/more"),
("something/else//more", "something/else/more"),
("something/else/../more", "something/more"),
("something/else/./more", "something/else/more"),
("../something/else/./more", "../something/else/more"),
("./something/else/./more", "something/else/more"),
("something/else/more.html", "something/else/more.html"),
("../something/else/more.html", "../something/else/more.html"),
("../something/else/more.html/..", "../something/else"),
(
"something/else/more.html/../../more_es.properties",
"something/more_es.properties",
),
("windows\\folder\\file.html", "windows/folder/file.html"),
("", ""),
(".", "."),
("..", ".."),
("./", "./"),
];
assert_eq!(TemplateResourceUtils::clean_path(None), None);
for (path, expected) in cases {
assert_eq!(
TemplateResourceUtils::clean_path(Some(path)),
Some(expected.to_owned())
);
}
}
#[test]
#[should_panic(expected = "cannot remove a char")]
fn preserves_java_failure_when_a_relative_path_is_fully_cancelled() {
let _ = TemplateResourceUtils::clean_path(Some("segment/.."));
}
#[test]
fn computes_base_names_with_java_dot_and_slash_boundaries() {
let cases = [
(None, None),
(Some(""), None),
(Some("/"), None),
(Some("something"), Some("something")),
(Some("/something"), Some("something")),
(Some("something/else"), Some("else")),
(Some("//something//else"), Some("else")),
(Some("//something//a//..//else"), Some("else")),
(Some("something/else/more"), Some("more")),
(Some("something/else//more"), Some("more")),
(Some("something/else/../more"), Some("more")),
(Some("something/else/./more"), Some("more")),
(Some("../something/else/./more"), Some("more")),
(Some("./something/else/./more"), Some("more")),
(Some("something/else/more.html"), Some("more")),
(Some("../something/else/more.html"), Some("more")),
(Some("../something/else/more.html/.."), Some(".")),
(
Some("something/else/more.html/../../more_es.properties"),
Some("more_es"),
),
(Some("more.html"), Some("more")),
(Some(".hidden"), Some("")),
(Some("folder/.hidden"), Some(".hidden")),
(Some("folder/"), Some("folder")),
];
for (path, expected) in cases {
assert_eq!(
TemplateResourceUtils::compute_base_name(path),
expected.map(str::to_owned)
);
}
}
}