///|
/// The rule that a file-tree path text breaks.
pub(all) enum FilePathRule {
/// The text is empty.
Empty
/// The text holds a NUL character.
Nul
/// Portable text holds `\`. Use `FileTreePath::from_native` for a Windows
/// path with native separators.
Backslash
/// A drive letter and `:` without `/` after them, such as `C:` or
/// `C:acme.toml`. Such a path is relative to the current directory of the
/// drive, so it has no fixed place in the tree.
DriveRelative
/// A path that starts with `//` and does not name a server and a share.
IncompleteUnc
/// A name for one entry is empty, `.` or `..`, or holds `/`.
NotAName
} derive(Eq, Debug)
///|
pub extend FilePathRule with Eq::{equal, not_equal}
///|
pub extend FilePathRule with @debug.Debug::{to_repr}
///|
/// The rule in words, for an error message.
pub fn FilePathRule::describe(self : FilePathRule) -> String {
match self {
Empty => "the path is empty"
Nul => "the path holds a NUL character"
Backslash => "the path holds \\; a portable path uses / separators"
DriveRelative => "a drive letter and : must have / after them, for example C:/"
IncompleteUnc => "a path that starts with // must name a server and a share"
NotAName => "an entry name must not be empty, . or .., and must not hold /"
}
}
///|
/// The root of a file-tree path.
priv enum PathRoot {
/// `/`
Posix
/// A drive root such as `C:/`, with its letter in the given case.
Drive(Char)
/// A UNC share root `//server/share`.
Unc(String, String)
/// `.`, the directory that a relative path starts from.
Current
} derive(Eq, Hash)
///|
/// A path in the file-tree protocol: a root and a list of names, with `/` as
/// the separator on every platform. The root is `/`, a drive root such as
/// `C:/`, a UNC share root such as `//server/share`, or `.` for a relative
/// path. Make a value with `parse` or `from_native`.
///
/// A path keeps the exact case and the order of its names. Empty names and `.`
/// names are left out, so `/w//a/./b/` is `/w/a/b`. A `..` name stays, because
/// the parent of a symbolic link on the disk is not the lexical parent. A path
/// does not tell that a file exists, can be read, or is in a browser mount.
pub struct FileTreePath {
priv root : PathRoot
priv names : Array[String]
} derive(Eq, Hash)
///|
pub extend FileTreePath with Eq::{equal, not_equal}
///|
pub extend FileTreePath with Hash::{hash, hash_combine}
///|
/// Parse the portable text form of a path. The text must not be empty, and it
/// must not hold a NUL character or `\`. A drive letter and `:` must have `/`
/// after them, and `//` must start a server and a share name.
pub fn FileTreePath::parse(text : String) -> FileTreePath raise ConfigError {
fn invalid(rule : FilePathRule) -> ConfigError {
InvalidFilePath(text~, rule~)
}
if text.is_empty() {
raise invalid(Empty)
}
for char in text {
if char == '\u{0}' {
raise invalid(Nul)
}
if char == '\\' {
raise invalid(Backslash)
}
}
let chars = Array::from_iter(text.iter())
let (root, rest) = if text.has_prefix("//") {
let parts = text[2:].split("/").map(part => part.to_owned()).collect()
guard parts is [server, share, .. rest] &&
!server.is_empty() &&
!share.is_empty() else {
raise invalid(IncompleteUnc)
}
(Unc(server, share), rest.to_owned())
} else if text.has_prefix("/") {
(Posix, text[1:].split("/").map(part => part.to_owned()).collect())
} else if chars.length() >= 2 && chars[0].is_ascii_alphabetic() && chars[1] == ':' {
guard chars.length() >= 3 && chars[2] == '/' else {
raise invalid(DriveRelative)
}
(Drive(chars[0]), text[3:].split("/").map(part => part.to_owned()).collect())
} else {
(Current, text.split("/").map(part => part.to_owned()).collect())
}
{ root, names: rest.filter(name => name != "" && name != ".") }
}
///|
/// Parse a path in the native form of `platform`. On Windows `\` becomes `/`
/// first, so `C:\w\app.toml` gives `C:/w/app.toml` and `\\server\share\a`
/// gives `//server/share/a`. A drive-relative path such as `C:w` stays an
/// error. On other platforms this is `parse`.
pub fn FileTreePath::from_native(
text : String,
platform : Platform,
) -> FileTreePath raise ConfigError {
match platform {
Windows =>
FileTreePath::parse(text.replace_all(old="\\", new="/")) catch {
InvalidFilePath(rule~, ..) => raise InvalidFilePath(text~, rule~)
error => raise error
}
Linux | MacOs => FileTreePath::parse(text)
}
}
///|
/// The file-tree path of the host path `path`. This is a checked conversion:
/// `AbsolutePath` uses `/` for drive and UNC paths, but it allows `\` and a NUL
/// character in a POSIX path, which a file-tree path does not.
pub fn FileTreePath::from_absolute(
path : @lander_core.AbsolutePath,
) -> FileTreePath raise ConfigError {
FileTreePath::parse(path.to_string())
}
///|
/// The text of the path in the native form of `platform`, for a file-system
/// call. Only Windows paths change: `/` becomes `\`.
pub fn FileTreePath::to_native(
self : FileTreePath,
platform : Platform,
) -> String {
let text = self.to_string()
match platform {
Windows => text.replace_all(old="/", new="\\")
Linux | MacOs => text
}
}
///|
/// The root of the path: `/`, a drive root, a UNC share root, or `.`.
pub fn FileTreePath::root(self : FileTreePath) -> FileTreePath {
{ root: self.root, names: [] }
}
///|
/// Whether the path is a root: `/`, a drive root, a UNC share root, or `.`.
pub fn FileTreePath::is_root(self : FileTreePath) -> Bool {
self.names.is_empty()
}
///|
/// Whether the path starts at `/`, a drive root, or a UNC share root, and not
/// at `.`.
pub fn FileTreePath::is_absolute(self : FileTreePath) -> Bool {
!(self.root is Current)
}
///|
/// The directory that holds the path, or `None` for a root. The parent of
/// `C:/acme.toml` is `C:/`, and the parent of `acme.toml` is `.`.
pub fn FileTreePath::parent(self : FileTreePath) -> FileTreePath? {
guard !self.names.is_empty() else { None }
Some({ root: self.root, names: self.names[:self.names.length() - 1].to_owned() })
}
///|
/// The last name of the path, or `None` for a root.
pub fn FileTreePath::name(self : FileTreePath) -> String? {
self.names.last()
}
///|
/// The names of the path below its root, in order.
pub fn FileTreePath::names(self : FileTreePath) -> Array[String] {
self.names.copy()
}
///|
/// The path of the entry `name` in the directory `self`. The name must not be
/// empty, `.` or `..`, and must not hold `/`, `\` or a NUL character.
pub fn FileTreePath::child(
self : FileTreePath,
name : String,
) -> FileTreePath raise ConfigError {
if name.is_empty() || name == "." || name == ".." || name.contains("/") {
raise InvalidFilePath(text=name, rule=NotAName)
}
if name.contains("\u{0}") {
raise InvalidFilePath(text=name, rule=Nul)
}
if name.contains("\\") {
raise InvalidFilePath(text=name, rule=Backslash)
}
{ root: self.root, names: [..self.names, name] }
}
///|
/// `other` when it is absolute, else `other` below `self`. No `..` name is
/// applied.
pub fn FileTreePath::resolve(
self : FileTreePath,
other : FileTreePath,
) -> FileTreePath {
guard other.root is Current else { other }
{ root: self.root, names: [..self.names, ..other.names] }
}
///|
/// Parse the portable text `relative` and resolve it below `self`. An absolute
/// text gives its own path.
pub fn FileTreePath::join(
self : FileTreePath,
relative : String,
) -> FileTreePath raise ConfigError {
self.resolve(FileTreePath::parse(relative))
}
///|
/// Whether `self` is `ancestor` or below it, name by name: `/w/config2` is not
/// below `/w/config`.
pub fn FileTreePath::is_within(
self : FileTreePath,
ancestor : FileTreePath,
) -> Bool {
self.root == ancestor.root &&
self.names.length() >= ancestor.names.length() &&
self.names[:ancestor.names.length()] == ancestor.names[:]
}
///|
/// The longest directory that holds every path in `paths`: the common root
/// and names of their directories. `None` when `paths` is empty or the paths
/// have different roots.
pub fn FileTreePath::common_dir(paths : Array[FileTreePath]) -> FileTreePath? {
guard paths is [first, .. rest] else { return None }
let mut dir = first.parent().unwrap_or(first)
for path in rest {
while !path.is_within(dir) {
guard dir.parent() is Some(parent) else { return None }
dir = parent
}
}
Some(dir)
}
///|
pub impl Show for FileTreePath with output(self, logger) {
match self.root {
Posix => logger.write_string("/")
Drive(letter) => {
logger.write_char(letter)
logger.write_string(":/")
}
Unc(server, share) => {
logger.write_string("//\{server}/\{share}")
if !self.names.is_empty() {
logger.write_string("/")
}
}
Current =>
// `./` stays before a first name that looks like a drive, such as
// `C:` or `C:x`, so the text form parses to the same relative path.
match self.names {
[] => logger.write_string(".")
[first, ..] if looks_like_drive(first) => logger.write_string("./")
_ => ()
}
}
logger.write_string(self.names.join("/"))
}
///|
pub extend FileTreePath with Show::{output, to_string}
///|
/// The debug form is the text form, so a test failure shows `"/w/acme.toml"`.
pub impl @debug.Debug for FileTreePath with to_repr(self) {
@debug.Repr(self.to_string())
}
///|
pub extend FileTreePath with @debug.Debug::{to_repr}
///|
/// Paths compare by their text form.
pub impl Compare for FileTreePath with compare(self, other) {
self.to_string().compare(other.to_string())
}
///|
pub extend FileTreePath with Compare::{compare, op_lt, op_le, op_gt, op_ge}
///|
/// Whether `name` starts with an ASCII letter and `:`, as a drive does.
fn looks_like_drive(name : String) -> Bool {
let chars = Array::from_iter(name.iter().take(2))
chars is [letter, ':'] && letter.is_ascii_alphabetic()
}