From 971c55cf3050e688728a7df97a6b60df1942c7c3 Mon Sep 17 00:00:00 2001 From: Ben <89769190+benwalksaway@users.noreply.github.com> Date: Sat, 21 May 2022 00:16:29 +0200 Subject: [PATCH] os: add norm_path and abs_path function (#14435) --- .../workflows/v_apps_and_modules_compile.yml | 2 +- vlib/os/filepath.v | 174 +++++++++++++++++- vlib/os/filepath_test.v | 100 ++++++++++ vlib/v/builder/builder.v | 2 +- 4 files changed, 270 insertions(+), 8 deletions(-) diff --git a/.github/workflows/v_apps_and_modules_compile.yml b/.github/workflows/v_apps_and_modules_compile.yml index 0704cf79f9..a3b764f066 100644 --- a/.github/workflows/v_apps_and_modules_compile.yml +++ b/.github/workflows/v_apps_and_modules_compile.yml @@ -10,7 +10,7 @@ on: - "**.md" concurrency: - group: build-other-${{ github.event.pull_request.number || github.sha }} + group: build-v-apps-and-modules-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true jobs: diff --git a/vlib/os/filepath.v b/vlib/os/filepath.v index 2770d26e5b..c9d772a18a 100644 --- a/vlib/os/filepath.v +++ b/vlib/os/filepath.v @@ -1,13 +1,20 @@ 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 = `.` + fslash = `/` + bslash = `\\` + dot = `.` + qmark = `?` + dot_dot = '..' + empty = '' + dot_str = '.' ) // is_abs_path returns `true` if the given `path` is absolute. @@ -16,11 +23,152 @@ pub fn is_abs_path(path string) bool { return false } $if windows { - return is_device_path(path) || is_drive_rooted(path) || is_normal_path(path) + 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 '.' + } + rooted := is_abs_path(path) + volume := get_volume(path) + volume_len := volume.len + cpath := clean_path(path[volume_len..]) + if cpath.len == 0 && volume_len == 0 { + return '.' + } + 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 { + 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 '.' + } + return path_separator + } + if volume_len != 0 { + return volume + res + } + return res +} + +// 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 '' + } + 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 (back == -1 || is_slash(u8(back))) && curr == os.dot + && (peek == -1 || is_slash(u8(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 { @@ -32,7 +180,7 @@ fn win_volume_len(path string) int { return 2 } // its UNC path / DOS device path? - if path.len >= 5 && starts_w_slash_slash(path) && !is_slash(path[2]) { + 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]) { @@ -51,6 +199,20 @@ fn win_volume_len(path string) int { return 0 } +fn get_volume(path string) string { + $if !windows { + return '' + } + volume := path[..win_volume_len(path)] + if volume.len == 0 { + return '' + } + if volume[0] == os.fslash { + return volume.replace('/', '\\') + } + return volume +} + fn is_slash(b u8) bool { $if windows { return b == os.bslash || b == os.fslash @@ -58,7 +220,7 @@ fn is_slash(b u8) bool { return b == os.fslash } -fn is_device_path(path string) bool { +fn is_unc_path(path string) bool { return win_volume_len(path) >= 5 && starts_w_slash_slash(path) } diff --git a/vlib/os/filepath_test.v b/vlib/os/filepath_test.v index 0c74b7633c..0d65d8738d 100644 --- a/vlib/os/filepath_test.v +++ b/vlib/os/filepath_test.v @@ -27,3 +27,103 @@ fn test_is_abs_path() { assert !is_abs_path('./') assert !is_abs_path('.') } + +fn test_clean_path() { + $if windows { + assert clean_path(r'\\path\to\files/file.v') == r'\path\to\files\file.v' + assert clean_path(r'\/\//\/') == '\\' + assert clean_path(r'./path\\dir/\\./\/\\/file.v\.\\\.') == r'path\dir\file.v' + assert clean_path(r'\./path/dir\\file.exe') == r'\path\dir\file.exe' + assert clean_path(r'.') == '' + assert clean_path(r'./') == '' + assert clean_path(r'\./') == '\\' + assert clean_path(r'//\/\/////') == '\\' + return + } + assert clean_path('./../.././././//') == '../..' + assert clean_path('.') == '' + assert clean_path('./path/to/file.v//./') == 'path/to/file.v' + assert clean_path('./') == '' + assert clean_path('/.') == '/' + assert clean_path('//path/./to/.///files/file.v///') == '/path/to/files/file.v' + assert clean_path('path/./to/.///files/.././file.v///') == 'path/to/files/../file.v' + assert clean_path('\\') == '\\' + assert clean_path('//////////') == '/' +} + +fn test_norm_path() { + $if windows { + assert norm_path(r'C:/path/to//file.v\\') == r'C:\path\to\file.v' + assert norm_path(r'C:path\.\..\\\.\to//file.v') == r'C:to\file.v' + assert norm_path(r'D:path\.\..\..\\\\.\to//dir/..\') == r'D:..\to' + assert norm_path(r'D:/path\.\..\/..\file.v') == r'D:\file.v' + assert norm_path(r'') == '.' + assert norm_path(r'/') == '\\' + assert norm_path(r'\/') == '\\' + assert norm_path(r'path\../dir\..') == '.' + assert norm_path(r'.\.\') == '.' + assert norm_path(r'G:.\.\dir\././\.\.\\\\///to/././\file.v/./\\') == r'G:dir\to\file.v' + assert norm_path(r'G:\..\..\.\.\file.v\\\.') == r'G:\file.v' + assert norm_path(r'\\Server\share\\\dir/..\file.v\./.') == r'\\Server\share\file.v' + assert norm_path(r'\\.\device\\\dir/to/./file.v\.') == r'\\.\device\dir\to\file.v' + assert norm_path(r'C:dir/../dir2/../../../file.v') == r'C:..\..\file.v' + assert norm_path(r'\\.\C:\\\Users/\Documents//..') == r'\\.\C:\Users' + assert norm_path(r'\\.\C:\Users') == r'\\.\C:\Users' + assert norm_path(r'\\') == '\\' + assert norm_path(r'//') == '\\' + assert norm_path(r'\\\') == '\\' + assert norm_path(r'.') == '.' + assert norm_path(r'\\Server') == '\\Server' + assert norm_path(r'\\Server\') == '\\Server' + return + } + assert norm_path('/path/././../to/file//file.v/.') == '/to/file/file.v' + assert norm_path('path/././to/files/../../file.v/.') == 'path/file.v' + assert norm_path('path/././/../../to/file.v/.') == '../to/file.v' + assert norm_path('/path/././/../..///.././file.v/././') == '/file.v' + assert norm_path('path/././//../../../to/dir//.././file.v/././') == '../../to/file.v' + assert norm_path('path/../dir/..') == '.' + assert norm_path('../dir/..') == '..' + assert norm_path('/../dir/..') == '/' + assert norm_path('//././dir/../files/././/file.v') == '/files/file.v' + assert norm_path('/\\../dir/////////.') == '/\\../dir' + assert norm_path('/home/') == '/home' + assert norm_path('/home/////./.') == '/home' + assert norm_path('...') == '...' +} + +fn test_abs_path() { + wd := getwd() + wd_w_sep := wd + path_separator + $if windows { + assert abs_path('path/to/file.v') == '${wd_w_sep}path\\to\\file.v' + assert abs_path('path/to/file.v') == '${wd_w_sep}path\\to\\file.v' + assert abs_path('/') == r'\' + assert abs_path(r'C:\path\to\files\file.v') == r'C:\path\to\files\file.v' + assert abs_path(r'C:\/\path\.\to\../files\file.v\.\\\.\') == r'C:\path\files\file.v' + assert abs_path(r'\\Host\share\files\..\..\.') == r'\\Host\share\' + assert abs_path(r'\\.\HardDiskvolume2\files\..\..\.') == r'\\.\HardDiskvolume2\' + assert abs_path(r'\\?\share') == r'\\?\share' + assert abs_path(r'\\.\') == r'\' + assert abs_path(r'G:/\..\\..\.\.\file.v\\.\.\\\\') == r'G:\file.v' + assert abs_path('files') == '${wd_w_sep}files' + assert abs_path('') == wd + assert abs_path('.') == wd + assert abs_path('files/../file.v') == '${wd_w_sep}file.v' + assert abs_path('///') == r'\' + assert abs_path('/path/to/file.v') == r'\path\to\file.v' + assert abs_path('D:/') == r'D:\' + assert abs_path(r'\\.\HardiskVolume6') == r'\\.\HardiskVolume6' + return + } + assert abs_path('/') == '/' + assert abs_path('.') == wd + assert abs_path('files') == '${wd_w_sep}files' + assert abs_path('') == wd + assert abs_path('files/../file.v') == '${wd_w_sep}file.v' + assert abs_path('///') == '/' + assert abs_path('/path/to/file.v') == '/path/to/file.v' + assert abs_path('/path/to/file.v/../..') == '/path' + assert abs_path('path/../file.v/..') == wd + assert abs_path('///') == '/' +} diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index c635425043..ae6c23ae02 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -270,7 +270,7 @@ pub fn (b &Builder) import_graph() &depgraph.DepGraph { deps << 'builtin' if b.pref.backend == .c { // TODO JavaScript backend doesn't handle os for now - if b.pref.is_vsh && p.mod.name !in ['os', 'dl'] { + if b.pref.is_vsh && p.mod.name !in ['os', 'dl', 'strings.textscanner'] { deps << 'os' } }