///|
fn parse_windows_relative_path(
s : StringView,
start~ : Int,
root_eliminate~ : Bool,
) -> (WinDirectory, WinFile?) raise PathError {
let mut rest = s
let directory : Array[WinPathComponent] = []
let mut start = start
let file : WinFile? = for {
lexmatch rest with longest {
((("[^\\]+" as s) "\\") as all, r) => {
match s {
"." => ()
".." =>
match (directory.pop(), root_eliminate) {
(Some(WinPathComponent("..")), _) => {
directory.push(WinPathComponent(".."))
directory.push(WinPathComponent(".."))
}
(None, true) => ()
(None, false) => directory.push(WinPathComponent(".."))
(Some(_), _) => ()
}
_ => directory.push(WinPathComponent::from(s, start~))
}
start += all.char_length()
rest = r
}
"\.\." => {
match (directory.pop(), root_eliminate) {
(Some(WinPathComponent("..")), _) => {
directory.push(WinPathComponent(".."))
directory.push(WinPathComponent(".."))
}
(None, true) => ()
(None, false) => directory.push(WinPathComponent(".."))
(Some(_), _) => ()
}
break None
}
"\." => break None
(("[^\\]+" as base) "\." ("[^.\\]+" as extension)) =>
break Some(WinFile::new(base~, extension~))
("[^\\.]+" as base) => break Some(WinFile::new(base~))
"$" => break None
_ => raise PathError(start~, message="Invalid Windows path")
}
}
(Directory(FixedArray::from_array(directory)), file)
}
///|
fn parse_unix_relative_path(
s : StringView,
start~ : Int,
root_eliminate~ : Bool,
) -> (UnixDirectory, UnixFile?) raise PathError {
let mut rest = s
let directory : Array[UnixPathComponent] = []
let mut start = start
let file : UnixFile? = for {
lexmatch rest with longest {
((("[^/]+" as s) "/") as all, r) => {
match s {
"." => ()
".." =>
match (directory.pop(), root_eliminate) {
(Some(UnixPathComponent("..")), _) => {
directory.push(UnixPathComponent(".."))
directory.push(UnixPathComponent(".."))
}
(None, true) => ()
(None, false) => directory.push(UnixPathComponent(".."))
(Some(_), _) => ()
}
_ => directory.push(UnixPathComponent::from(s, start~))
}
start += all.char_length()
rest = r
}
"\.\." => {
match (directory.pop(), root_eliminate) {
(Some(UnixPathComponent("..")), _) => {
directory.push(UnixPathComponent(".."))
directory.push(UnixPathComponent(".."))
}
(None, true) => ()
(None, false) => directory.push(UnixPathComponent(".."))
(Some(_), _) => ()
}
break None
}
"\." => break None
(("[^/]+" as base) "\." ("[^/]+" as extension)) =>
break Some(UnixFile::new(base~, extension~))
("[^/.]+" as base) => break Some(UnixFile::new(base~))
"$" => break None
_ => raise PathError(start~, message="Invalid Unix path")
}
}
(Directory(FixedArray::from_array(directory)), file)
}
///|
fn win_path(
prefix? : WinPrefix = None,
directory? : WinDirectory = [],
file? : WinFile,
) -> Path {
WPath(WinPath::{ prefix, directory, file })
}
///|
/// Parsing path from string used prefix based detection to determine the path format style (Windows/Unix)
pub fn Path::parse(path : StringView) -> Path raise PathError {
lexmatch path with longest {
// this pattern exect match the whole path string
("\\\\\?\\" ("[^\\]+" as symlink)) =>
win_path(prefix=VerbatimSymlink(symlink=symlink.to_string()))
// this pattern exect match the whole path string
("\\\\\?UNC\\" ("[^\\]+" as hostname) "\\" ("[^\\]+" as shared_folder)) =>
win_path(
prefix=VerbatimUNC(
hostname=hostname.to_string(),
shared_folder=shared_folder.to_string(),
),
)
// this pattern exect match the whole path string
("\\\\" ("[^\\]+" as hostname) "\\" ("[^\\]+" as shared_folder)) =>
win_path(
prefix=UNC(
hostname=hostname.to_string(),
shared_folder=shared_folder.to_string(),
),
)
// this pattern exect match the whole path string
("\\\\\.\\" ("[^\\]+" as device)) =>
win_path(prefix=DeviceNS(device=device.to_string()))
// this pattern exect match the whole path string
("\\\\\?\\" ("[a-zA-Z]" as letter) ":\\") =>
win_path(prefix=VerbatimVolumeLetter(letter))
// this pattern exect match the whole path string
(("[a-zA-Z]" as letter) ":\\") => win_path(prefix=VolumeLetter(letter))
"\.\\" =>
// this pattern exect match the whole path string
win_path()
("\\\\\?\\" "\\|\.\.|\.") =>
raise PathError(
start=0,
message="Invalid Windows path: Verbatim prefix can use for relative path",
)
// this pattern exect match the whole path string
("\\\\\?\\Volume[{]" ("[^}]+" as guid) "[}]") =>
win_path(prefix=VerbatimVolumeGUID(guid.to_string()))
// this pattern exect match the whole path string
("\\\\\?UNC\\" ("[^\\]+" as hostname) "\\" ("[^\\]+" as shared_folder) "\\") =>
win_path(
prefix=VerbatimUNC(
hostname=hostname.to_string(),
shared_folder=shared_folder.to_string(),
),
)
(("\\\\\?\\" ("[^\\]+" as symlink) "\\") as prefix, rest) => {
// Warning: won't check the symlink name here
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=true,
)
win_path(
prefix=VerbatimSymlink(symlink=symlink.to_string()),
directory~,
file?,
)
}
(
(
"\\\\\?UNC\\"
("[^\\]+" as hostname)
"\\"
("[^\\]+" as shared_folder)
"\\"
) as prefix,
rest
) => {
// Warning: won't check the hostname and shared_folder here
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=true,
)
win_path(
prefix=VerbatimUNC(
hostname=hostname.to_string(),
shared_folder=shared_folder.to_string(),
),
directory~,
file?,
)
}
(
("\\\\" ("[^\\]+" as hostname) "\\" ("[^\\]+" as shared_folder) "\\") as prefix,
rest
) => {
// Warning: won't check the hostname and shared_folder here
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=true,
)
win_path(
prefix=UNC(
hostname=hostname.to_string(),
shared_folder=shared_folder.to_string(),
),
directory~,
file?,
)
}
(("\\\\\?\\Volume[{]" ("[^}]+" as guid) "[}]") as prefix, rest) => {
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=true,
)
win_path(prefix=VerbatimVolumeGUID(guid.to_string()), directory~, file?)
}
(("\\\\\.\\" ("[^\\]+" as device) "\\") as prefix, rest) => {
// check the device
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=true,
)
win_path(prefix=DeviceNS(device=device.to_string()), directory~, file?)
}
(("\\\\\?\\" ("[a-zA-Z]" as letter) ":\\") as prefix, rest) => {
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=true,
)
win_path(prefix=VerbatimVolumeLetter(letter), directory~, file?)
}
("\\\\\?\\" "\\|\.\.|\.", _) =>
raise PathError(
start=0,
message="Invalid Windows path: Verbatim prefix can use for relative path",
)
((("[a-zA-Z]" as letter) ":\\") as prefix, rest) => {
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=true,
)
win_path(prefix=VolumeLetter(letter), directory~, file?)
}
// current path
("\.\\" as prefix, rest) => {
let (directory, file) = parse_windows_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=false,
)
win_path(directory~, file?)
}
("[^\\]+\\", _) => {
let (directory, file) = parse_windows_relative_path(
path,
start=0,
root_eliminate=false,
)
win_path(directory~, file?)
}
// current path
("\./" as prefix, rest) => {
let (directory, file) = parse_unix_relative_path(
rest,
start=prefix.char_length(),
root_eliminate=false,
)
UPath(UnixPath::new(directory~, file?))
}
("[^/]+/", _) => {
let (directory, file) = parse_unix_relative_path(
path,
start=0,
root_eliminate=false,
)
UPath(UnixPath::new(directory~, file?))
}
// root path but without derive letter
("\\", rest) => {
let (directory, file) = parse_windows_relative_path(
rest,
start=1,
root_eliminate=true,
)
win_path(prefix=Root, directory~, file?)
}
// root path
("/", rest) => {
let (directory, file) = parse_unix_relative_path(
rest,
start=1,
root_eliminate=true,
)
UPath(UnixPath::new(is_absolute=true, directory~, file?))
}
_ => raise PathError(start=0, message="Invalid path format")
}
}
///|
pub fn[A : PlatformPath] File::new(
base~ : StringView,
extension? : StringView,
) -> File[A] raise PathError {
let ext = if extension is Some(extension) {
for i, ch in extension {
guard ch != '.' else {
raise PathError::PathError(
start=i,
message="Invalid file extension at position \{i}: extension '\{extension}' cannot contain '.' character",
)
}
}
Some(A::from(extension))
} else {
None
}
let base_component = A::from(base)
File::{ base: base_component, extension: ext }
}
///|
pub fn[A : PlatformPath] Directory::new(
components : ArrayView[StringView],
) -> Directory[A] raise PathError {
let result : Array[A] = []
for i = 0; i < components.length(); i = i + 1 {
result.push(A::from(components[i]))
}
Directory(FixedArray::from_array(result))
}
///|
fn WinPathComponent::from(
s : StringView,
start~ : Int,
) -> WinPathComponent raise PathError {
guard !s.is_empty() else {
raise PathError::PathError(
start~,
message="Windows path: empty path component not allowed",
)
}
guard !windows_reserved_names_set.contains(s) else {
raise PathError::PathError(
start~,
message="Windows path: '\{s}' is a reserved name (CON, PRN, AUX, NUL, COM1-9, LPT1-9)",
)
}
for i, ch in s {
guard !is_windows_reserved_character(ch) else {
raise PathError::PathError(
start=start + i,
message="Windows path: character '\{ch}' at position \{start+i} is not allowed (reserved characters: < > : \" / \\ | ? * and control characters)",
)
}
guard ch != '\\' else {
raise PathError::PathError(
start=start + i,
message="Windows path: backslash '\\' at position \{start+i} not allowed in path component",
)
}
}
WinPathComponent(s.to_string())
}
///|
fn UnixPathComponent::from(
s : StringView,
start~ : Int,
) -> UnixPathComponent raise PathError {
guard !s.is_empty() else {
raise PathError::PathError(
start~,
message="Unix path: empty path component not allowed",
)
}
guard s.get_char(0) != Some('-') else {
raise PathError::PathError(
start~,
message="Unix path: component '\{s}' cannot start with '-' (not portable according to POSIX standard)",
)
}
for i, ch in s {
guard ch != '/' else {
raise PathError::PathError(
start=start + i,
message="Unix path: forward slash '/' at position \{start + i} not allowed in path component",
)
}
guard ch != '\u0000' else {
raise PathError::PathError(
start=start + i,
message="Unix path: null character (\\u0000) at position \{start + i} not allowed in path component",
)
}
}
UnixPathComponent(s.to_string())
}
///|
pub impl PlatformPath for UnixPathComponent with from(s) {
guard !s.is_empty() else {
raise PathError::PathError(
start=0,
message="Unix path: empty path component not allowed",
)
}
guard s.get_char(0) != Some('-') else {
raise PathError::PathError(
start=0,
message="Unix path: component '\{s}' cannot start with '-' (not portable according to POSIX standard)",
)
}
for i, ch in s {
guard ch != '/' else {
raise PathError::PathError(
start=i,
message="Unix path: forward slash '/' at position \{i} not allowed in path component",
)
}
guard ch != '\u0000' else {
raise PathError::PathError(
start=i,
message="Unix path: null character (\\u0000) at position \{i} not allowed in path component",
)
}
}
UnixPathComponent(s.to_string())
}
///|
pub fn WinPath::new(
prefix? : WinPrefix = None,
directory~ : WinDirectory,
file? : WinFile,
) -> WinPath {
WinPath::{ prefix, directory, file }
}
///|
pub fn UnixPath::new(
is_absolute? : Bool = false,
directory~ : UnixDirectory,
file? : UnixFile,
) -> UnixPath {
UnixPath::{ is_absolute, directory, file }
}
///|
pub fn[T] Directory::op_get(self : Directory[T], index : Int) -> T = "%fixedarray.get"
///|
pub fn[T] Directory::length(self : Directory[T]) -> Int = "%fixedarray.length"
///|
pub fn[T] Directory::iter(self : Directory[T]) -> Iter[T] {
FixedArray::iter(self.0)
}
///|
#alias("_[_:_]")
pub fn[T] Directory::sub(
self : Directory[T],
start? : Int = 0,
end? : Int = self.length(),
) -> ArrayView[T] {
FixedArray::sub(self.0, start~, end~)
}
///|
pub fn WinPath::is_absolute(self : WinPath) -> Bool {
!self.is_relative()
}
///|
pub fn WinPath::is_relative(self : WinPath) -> Bool {
self.prefix is (Root | None)
}
///|
pub fn WinPath::is_root_path(self : WinPath) -> Bool {
!(self.prefix is None)
}
///|
#alias(is_root_path)
pub fn UnixPath::is_absolute(self : UnixPath) -> Bool {
self.is_absolute
}
///|
pub fn UnixPath::is_relative(self : UnixPath) -> Bool {
!self.is_absolute
}
///|
pub fn Path::is_absolute(self : Path) -> Bool {
match self {
UPath(path) => path.is_absolute()
WPath(path) => path.is_absolute()
}
}
///|
pub fn Path::is_relative(self : Path) -> Bool {
match self {
UPath(path) => path.is_relative()
WPath(path) => path.is_relative()
}
}
///|
pub fn Path::is_root_path(self : Path) -> Bool {
match self {
UPath(path) => path.is_absolute()
WPath(path) => path.is_root_path()
}
}
///|
pub fn Path::file(self : Path) -> File[String]? {
match self {
WPath({ file: Some({ base, extension: Some(ext) }), .. }) =>
Some(File::{ base: base.0, extension: Some(ext.0) })
WPath({ file: Some({ base, extension: None }), .. }) =>
Some(File::{ base: base.0, extension: None })
UPath({ file: Some({ base, extension: Some(ext) }), .. }) =>
Some(File::{ base: base.0, extension: Some(ext.0) })
UPath({ file: Some({ base, extension: None }), .. }) =>
Some(File::{ base: base.0, extension: None })
WPath({ file: None, .. }) => None
UPath({ file: None, .. }) => None
}
}
///|
pub fn Path::prefix(self : Path) -> String {
match self {
WPath({ prefix, .. }) => prefix.to_string()
UPath({ is_absolute: true, .. }) => "/"
UPath({ is_absolute: false, .. }) => ""
}
}
///|
pub fn Path::directory(self : Path) -> Directory[String] {
match self {
// if compiler can optimize this overhead, we not need use `"%identity"` intrinsic instruction
WPath({ directory, .. }) => directory.0.map(IsString::to_string)
UPath({ directory, .. }) => directory.0.map(IsString::to_string)
}
}
///|
pub fn[A : IsString] File::extension(self : File[A]) -> String? {
// if compiler can optimize this overhead, we not need use `"%identity"` intrinsic instruction
self.extension.map(IsString::to_string)
}