///|
/// Reject escapes rather than normalizing them through the host root.
fn normalize(path : String) -> String raise {
  if path.has_prefix("/") ||
    path.contains("\\") ||
    path.contains(":") ||
    path.contains("\u0000") {
    raise @scheme.Invalid("Expected a portable relative path: " + path)
  }
  let parts : Array[String] = []
  for part in path.split("/") {
    let part = part.to_owned()
    if part == ".." {
      raise @scheme.Invalid("Path escapes its root: " + path)
    }
    if part != "" && part != "." {
      parts.push(part)
    }
  }
  parts.join("/")
}

///|
fn join(root : String, path : String) -> String raise {
  let path = normalize(path)
  if root == "" {
    path
  } else if path == "" {
    root
  } else {
    root + "/" + path
  }
}

///|
fn within(root : String, path : String) -> Bool {
  root == "" || path.has_prefix(root + "/")
}

///|
fn relative(root : String, path : String) -> String raise {
  if !within(root, path) {
    raise @scheme.Invalid("Host listed a path outside source root: " + path)
  }
  if root == "" {
    path
  } else {
    path[root.length() + 1:].to_owned()
  }
}

///|
fn parent(path : String) -> String {
  let parts = path.split("/").map(StringView::to_owned).collect()
  parts[:parts.length() - 1].to_owned().join("/")
}

///|
fn stem(path : String) -> String {
  let parts = path.split("/").map(StringView::to_owned).collect()
  let file = parts[parts.length() - 1]
  let bits = file.split(".").map(StringView::to_owned).collect()
  let name = if bits.length() > 1 {
    bits[:bits.length() - 1].to_owned().join(".")
  } else {
    file
  }
  parts[parts.length() - 1] = name
  parts.join("/")
}

///|
/// Project metadata and filenames use conventional spelling; IR uses canonical names.
fn canonical_path(text : String) -> String raise {
  @ids.canonical_path(text)
}