v/vlib/os/filepath.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)))
}