299 lines
7.1 KiB
V
299 lines
7.1 KiB
V
module os
|
|
|
|
import strings
|
|
import strings.textscanner
|
|
|
|
// Collection of useful functions for manipulation, validation and analysis of system paths.
|
|
// The following functions handle paths depending on the operating system,
|
|
// therefore results may be different for certain operating systems.
|
|
|
|
const (
|
|
fslash = `/`
|
|
bslash = `\\`
|
|
dot = `.`
|
|
qmark = `?`
|
|
fslash_str = '/'
|
|
dot_dot = '..'
|
|
empty_str = ''
|
|
dot_str = '.'
|
|
)
|
|
|
|
// is_abs_path returns `true` if the given `path` is absolute.
|
|
pub fn is_abs_path(path string) bool {
|
|
if path.len == 0 {
|
|
return false
|
|
}
|
|
$if windows {
|
|
return is_unc_path(path) || is_drive_rooted(path) || is_normal_path(path)
|
|
}
|
|
return path[0] == os.fslash
|
|
}
|
|
|
|
// abs_path joins the current working directory
|
|
// with the given `path` (if the `path` is relative)
|
|
// and returns the absolute path representation.
|
|
pub fn abs_path(path string) string {
|
|
wd := getwd()
|
|
if path.len == 0 {
|
|
return wd
|
|
}
|
|
npath := norm_path(path)
|
|
if npath == os.dot_str {
|
|
return wd
|
|
}
|
|
if !is_abs_path(npath) {
|
|
mut sb := strings.new_builder(npath.len)
|
|
sb.write_string(wd)
|
|
sb.write_string(path_separator)
|
|
sb.write_string(npath)
|
|
return norm_path(sb.str())
|
|
}
|
|
return npath
|
|
}
|
|
|
|
// norm_path returns the normalized version of the given `path`
|
|
// by resolving backlinks (..), turning forward slashes into
|
|
// back slashes on a Windows system and eliminating:
|
|
// - references to current directories (.)
|
|
// - redundant path separators
|
|
// - the last path separator
|
|
[direct_array_access]
|
|
pub fn norm_path(path string) string {
|
|
if path.len == 0 {
|
|
return os.dot_str
|
|
}
|
|
rooted := is_abs_path(path)
|
|
// get the volume name from the path
|
|
// if the current operating system is Windows
|
|
volume_len := win_volume_len(path)
|
|
mut volume := path[..volume_len]
|
|
if volume_len != 0 && volume.contains(os.fslash_str) {
|
|
volume = volume.replace(os.fslash_str, path_separator)
|
|
}
|
|
cpath := clean_path(path[volume_len..])
|
|
if cpath.len == 0 && volume_len == 0 {
|
|
return os.dot_str
|
|
}
|
|
spath := cpath.split(path_separator)
|
|
if os.dot_dot !in spath {
|
|
return if volume_len != 0 { volume + cpath } else { cpath }
|
|
}
|
|
// resolve backlinks (..)
|
|
spath_len := spath.len
|
|
mut sb := strings.new_builder(cpath.len)
|
|
if rooted {
|
|
sb.write_string(path_separator)
|
|
}
|
|
mut new_path := []string{cap: spath_len}
|
|
mut backlink_count := 0
|
|
for i := spath_len - 1; i >= 0; i-- {
|
|
part := spath[i]
|
|
if part == os.empty_str {
|
|
continue
|
|
}
|
|
if part == os.dot_dot {
|
|
backlink_count++
|
|
continue
|
|
}
|
|
if backlink_count != 0 {
|
|
backlink_count--
|
|
continue
|
|
}
|
|
new_path.prepend(part)
|
|
}
|
|
// append backlink(s) to the path if backtracking
|
|
// is not possible and the given path is not rooted
|
|
if backlink_count != 0 && !rooted {
|
|
for i in 0 .. backlink_count {
|
|
sb.write_string(os.dot_dot)
|
|
if new_path.len == 0 && i == backlink_count - 1 {
|
|
break
|
|
}
|
|
sb.write_string(path_separator)
|
|
}
|
|
}
|
|
sb.write_string(new_path.join(path_separator))
|
|
res := sb.str()
|
|
if res.len == 0 {
|
|
if volume_len != 0 {
|
|
return volume
|
|
}
|
|
if !rooted {
|
|
return os.dot_str
|
|
}
|
|
return path_separator
|
|
}
|
|
if volume_len != 0 {
|
|
return volume + res
|
|
}
|
|
return res
|
|
}
|
|
|
|
// existing_path returns the existing part of the given `path`.
|
|
// An error is returned if there is no existing part of the given `path`.
|
|
pub fn existing_path(path string) ?string {
|
|
err := error('path does not exist')
|
|
if path.len == 0 {
|
|
return err
|
|
}
|
|
if exists(path) {
|
|
return path
|
|
}
|
|
mut volume_len := 0
|
|
$if windows {
|
|
volume_len = win_volume_len(path)
|
|
}
|
|
if volume_len > 0 && is_slash(path[volume_len - 1]) {
|
|
volume_len++
|
|
}
|
|
mut sc := textscanner.new(path[volume_len..])
|
|
mut recent_path := path[..volume_len]
|
|
for sc.next() != -1 {
|
|
curr := u8(sc.current())
|
|
peek := sc.peek()
|
|
back := sc.peek_back()
|
|
if is_curr_dir_ref(back, curr, peek) {
|
|
continue
|
|
}
|
|
range := sc.ilen - sc.remaining() + volume_len
|
|
if is_slash(curr) && !is_slash(u8(peek)) {
|
|
recent_path = path[..range]
|
|
continue
|
|
}
|
|
if !is_slash(curr) && (peek == -1 || is_slash(u8(peek))) {
|
|
curr_path := path[..range]
|
|
if exists(curr_path) {
|
|
recent_path = curr_path
|
|
continue
|
|
}
|
|
if recent_path.len == 0 {
|
|
break
|
|
}
|
|
return recent_path
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// clean_path returns the "cleaned" version of the given `path`
|
|
// by turning forward slashes into back slashes
|
|
// on a Windows system and eliminating:
|
|
// - references to current directories (.)
|
|
// - redundant separators
|
|
// - the last path separator
|
|
fn clean_path(path string) string {
|
|
if path.len == 0 {
|
|
return os.empty_str
|
|
}
|
|
mut sb := strings.new_builder(path.len)
|
|
mut sc := textscanner.new(path)
|
|
for sc.next() != -1 {
|
|
curr := u8(sc.current())
|
|
back := sc.peek_back()
|
|
peek := sc.peek()
|
|
// skip current path separator if last byte was a path separator
|
|
if back != -1 && is_slash(u8(back)) && is_slash(curr) {
|
|
continue
|
|
}
|
|
// skip reference to current dir (.)
|
|
if is_curr_dir_ref(back, curr, peek) {
|
|
// skip if the next byte is a path separator
|
|
if peek != -1 && is_slash(u8(peek)) {
|
|
sc.skip_n(1)
|
|
}
|
|
continue
|
|
}
|
|
// turn foward slash into a back slash on a Windows system
|
|
$if windows {
|
|
if curr == os.fslash {
|
|
sb.write_u8(os.bslash)
|
|
continue
|
|
}
|
|
}
|
|
sb.write_u8(u8(sc.current()))
|
|
}
|
|
res := sb.str()
|
|
// eliminate the last path separator
|
|
if res.len > 1 && is_slash(res[res.len - 1]) {
|
|
return res[..res.len - 1]
|
|
}
|
|
return res
|
|
}
|
|
|
|
// win_volume_len returns the length of the
|
|
// Windows volume/drive from the given `path`.
|
|
fn win_volume_len(path string) int {
|
|
$if !windows {
|
|
return 0
|
|
}
|
|
plen := path.len
|
|
if plen < 2 {
|
|
return 0
|
|
}
|
|
if has_drive_letter(path) {
|
|
return 2
|
|
}
|
|
// its UNC path / DOS device path?
|
|
if plen >= 5 && starts_w_slash_slash(path) && !is_slash(path[2]) {
|
|
for i := 3; i < plen; i++ {
|
|
if is_slash(path[i]) {
|
|
if i + 1 >= plen || is_slash(path[i + 1]) {
|
|
break
|
|
}
|
|
i++
|
|
for ; i < plen; i++ {
|
|
if is_slash(path[i]) {
|
|
return i
|
|
}
|
|
}
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
fn is_slash(b u8) bool {
|
|
$if windows {
|
|
return b == os.bslash || b == os.fslash
|
|
}
|
|
return b == os.fslash
|
|
}
|
|
|
|
fn is_unc_path(path string) bool {
|
|
return win_volume_len(path) >= 5 && starts_w_slash_slash(path)
|
|
}
|
|
|
|
fn has_drive_letter(path string) bool {
|
|
return path.len >= 2 && path[0].is_letter() && path[1] == `:`
|
|
}
|
|
|
|
fn starts_w_slash_slash(path string) bool {
|
|
return path.len >= 2 && is_slash(path[0]) && is_slash(path[1])
|
|
}
|
|
|
|
fn is_drive_rooted(path string) bool {
|
|
return path.len >= 3 && has_drive_letter(path) && is_slash(path[2])
|
|
}
|
|
|
|
// is_normal_path returns `true` if the given
|
|
// `path` is NOT a network or Windows device path.
|
|
fn is_normal_path(path string) bool {
|
|
plen := path.len
|
|
if plen == 0 {
|
|
return false
|
|
}
|
|
return (plen == 1 && is_slash(path[0])) || (plen >= 2 && is_slash(path[0])
|
|
&& !is_slash(path[1]))
|
|
}
|
|
|
|
// is_curr_dir_ref returns `true` if the 3 given integer construct
|
|
// a reference to a current directory (.).
|
|
// NOTE: a negative integer means that no byte is present
|
|
fn is_curr_dir_ref(byte_one int, byte_two int, byte_three int) bool {
|
|
if u8(byte_two) != os.dot {
|
|
return false
|
|
}
|
|
return (byte_one < 0 || is_slash(u8(byte_one))) && (byte_three < 0 || is_slash(u8(byte_three)))
|
|
}
|