///|
/// Reasons an archive path cannot be used for safe extraction.
pub(all) enum ArchivePathError {
EmptyPath
AbsolutePath
Backslash
Colon
EmptyComponent
CurrentComponent
ParentComponent
} derive(Eq, Debug)
///|
fn strip_trailing_slashes(path : String) -> String {
let mut result = path
while result.has_suffix("/") {
result = result[0:result.length() - 1].to_owned()
}
result
}
///|
/// Validates a cpio pathname for safe extraction beneath a target directory.
///
/// Archive paths must be relative, use `/` as the separator, and contain no
/// `.` or `..` components.
pub fn validate_archive_path(
path : StringView,
) -> Result[Unit, ArchivePathError] {
let text = path.to_owned()
if text.is_empty() {
return Err(EmptyPath)
}
if text.has_prefix("/") {
return Err(AbsolutePath)
}
if text.contains("\\") {
return Err(Backslash)
}
if text.contains(":") {
return Err(Colon)
}
let normalized = strip_trailing_slashes(text)
if normalized.is_empty() {
return Err(EmptyPath)
}
for component in normalized.split("/") {
let part = component.to_owned()
if part.is_empty() {
return Err(EmptyComponent)
}
if part == "." {
return Err(CurrentComponent)
}
if part == ".." {
return Err(ParentComponent)
}
}
Ok(())
}
///|
/// Converts a validated archive pathname to a filesystem path under `root`.
pub fn archive_path_to_fs(
root : String,
path : StringView,
) -> Result[String, ArchivePathError] {
match validate_archive_path(path) {
Ok(_) => ()
Err(error) => return Err(error)
}
let normalized = strip_trailing_slashes(path.to_owned())
if root.has_suffix("/") || root.has_suffix("\\") {
Ok(root + normalized)
} else {
Ok(root + "/" + normalized)
}
}