///|
/// 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)
  }
}