///|
pub fn normalize_path(path : String) -> String raise NfsError {
  let parts = path_components(path)
  if parts.length() == 0 {
    "/"
  } else {
    parts.join("/")
  }
}

///|
pub fn join_path(parent : String, child : String) -> String raise NfsError {
  let parts = path_components(parent)
  for part in path_components(child) {
    parts.push(part)
  }
  if parts.length() == 0 {
    "/"
  } else {
    parts.join("/")
  }
}

///|
pub fn relative_path(base : String, path : String) -> String raise NfsError {
  let parent = path_components(base)
  let child = path_components(path)
  if child.length() < parent.length() {
    raise InvalidArgument("path is outside the requested subtree")
  }
  for i in 0.. String raise {
  if bytes.length() == 0 ||
    bytes.length() > 255 ||
    bytes.to_array().contains(0) ||
    bytes.to_array().contains(47) {
    raise ProtocolError("invalid directory entry name")
  }
  @utf8.decode(bytes) catch {
    _ => raise ProtocolError("path operations require UTF-8 filenames")
  }
}

///|
pub async fn Client::exists(self : Client, path : String) -> Bool {
  try self.resolve(path) catch {
    Status(2U) => false
    error => raise error
  } noraise {
    _ => true
  }
}

///|
pub async fn Client::ensure_directory(
  self : Client,
  path : String,
  mode? : UInt = 493U,
) -> Handle {
  if mode > 4095U {
    raise InvalidArgument("invalid Unix mode")
  }
  let parts = path_components(path)
  let mut handle = self.root_handle()
  let mut prefix = ""
  for name in parts {
    prefix = if prefix == "" { name } else { prefix + "/" + name }
    let next = self.lookup(handle, name) catch {
      Status(2U) =>
        // Another creator can win the race; only EXIST may use LOOKUP fallback.
        self.mkdir(prefix, mode~) catch {
          Status(17U) => self.lookup(handle, name)
          error => raise error
        }
      error => raise error
    }
    if self.getattr(next).kind != 2U {
      raise InvalidArgument("directory path contains a non-directory")
    }
    handle = next
  }
  handle
}

///|
/// Explicit link following; absolute targets are relative to the export root.
/// Targets escaping that root are rejected. resolve() itself never follows links.
pub async fn Client::canonical_path(
  self : Client,
  path : String,
  max_links? : Int = 40,
) -> String {
  if max_links < 0 || max_links > 1024 {
    raise InvalidArgument("invalid symbolic link limit")
  }
  let mut pending = path_components(path)
  let completed : Array[String] = []
  let mut handle = self.root_handle()
  let mut index = 0
  let mut links = 0
  while index < pending.length() {
    let name = pending[index]
    index += 1
    let (next, optional) = self.lookup_attributes(handle, name)
    let attributes = match optional {
      Some(value) => value
      None => self.getattr(next)
    }
    if attributes.kind == 5U {
      if links >= max_links {
        raise ProtocolError("symbolic link limit exceeded")
      }
      links += 1
      let target = @utf8.decode(self.readlink(next))
      if target.contains("\u{0000}") || target == "" {
        raise ProtocolError("invalid symbolic link target")
      }
      let replacement = if target.has_prefix("/") {
        []
      } else {
        completed.copy()
      }
      for part in target.split("/") {
        let part = part.to_owned()
        if part == ".." {
          if replacement.pop() is None {
            raise InvalidArgument("symbolic link escapes export root")
          }
        } else if part != "" && part != "." {
          replacement.push(part)
        }
      }
      for i in index.. Handle {
  self.resolve(self.canonical_path(path, max_links~))
}