Browse Source

Merge pull request #339 from dotbmp/master

"core:path" Path library and "core:strings" `split` Utilities
gingerBill 5 years ago
parent
commit
f65fa0e4a6
5 changed files with 553 additions and 0 deletions
  1. 200 0
      core/path/path.odin
  2. 80 0
      core/path/path_linux.odin
  3. 114 0
      core/path/path_windows.odin
  4. 150 0
      core/strings/strings.odin
  5. 9 0
      core/sys/win32/kernel32.odin

+ 200 - 0
core/path/path.odin

@@ -0,0 +1,200 @@
+package path
+
+import "core:strings"
+import "core:unicode/utf8"
+
+
+// returns everything preceding the last path element
+dir :: proc(path: string, new := false, allocator := context.allocator) -> string {
+    if path == "" do return "";
+
+    for i := len(path) - 1; i >= 0; i -= 1 {
+        if path[i] == '/' || path[i] == '\\' {
+            if path[:i] == "" {
+                // path is root
+                return new ? strings.new_string(SEPARATOR_STRING, allocator) : SEPARATOR_STRING;
+            } else {
+                return new ? strings.new_string(path[:i], allocator) : path[:i];
+            }
+        }
+    }
+
+    // path doesn't contain any folder structure
+    return "";
+}
+
+// returns the final path element
+base :: proc(path: string, new := false, allocator := context.allocator) -> string {
+    if path == "" do return "";
+
+    end := len(path) - 1;
+
+    for i := end; i >= 0; i -= 1 {
+        switch path[i] {
+        case '/', '\\':
+            if i != end {
+                return new ? strings.new_string(path[i+1:], allocator) : path[i+1:];
+            } else {
+                end = i; // we don't want trailing slashes
+            }
+        }
+    }
+
+    // path doesn't contain any folder structure, return entire path
+    return new ? strings.new_string(path, allocator) : path;
+}
+
+// returns the final path element, excluding the file extension if there is one
+name :: proc(path: string, new := false, allocator := context.allocator) -> string {
+    if path == "" do return "";
+
+    dot := len(path);
+    end := dot - 1;
+
+    for i := end; i >= 0; i -= 1 {
+        switch path[i] {
+        case '.':       dot = (dot == end ? i : dot);
+        case '/', '\\': return new ? strings.new_string(path[i+1:dot], allocator) : path[i+1:dot];
+        }
+    }
+
+    // path doesn't contain any folder structure or file extensions; assumed to be a valid file name
+    return new ? strings.new_string(path, allocator) : path;
+}
+
+// returns the file extension, if there is one
+ext :: proc(path: string, new := false, allocator := context.allocator) -> string {
+    if path == "" do return "";
+
+    for i := len(path)-1; i >= 0; i -= 1 {
+        switch path[i] {
+        case '/', '\\': return "";
+        case '.':       return new ? strings.new_string(path[i+1:], allocator) : path[i+1:];
+        }
+    }
+
+    // path does not include a file extension
+    return "";
+}
+
+
+rel :: proc{rel_between, rel_current};
+
+// returns the relative path from one path to another
+rel_between :: proc(from, to: string, allocator := context.allocator) -> string {
+    if from == "" || to == "" do return "";
+
+    from = full(from, context.temp_allocator);
+    to   = full(to,   context.temp_allocator);
+
+    from_is_dir := is_dir(from);
+    to_is_dir   := is_dir(to);
+
+    index, slash := 0, 0;
+
+    for {
+        if index >= len(from) {
+            if index >= len(to) || (from_is_dir && index < len(to) && (to[index] == '/' || to[index] == '\\')) {
+                slash = index;
+            }
+
+            break;
+        }
+        else if index >= len(to) {
+            if index >= len(from) || (to_is_dir && index < len(from) && (from[index] == '/' || from[index] == '\\')) {
+                slash = index;
+            }
+
+            break;
+        }
+
+        lchar, skip := utf8.decode_rune_in_string(from[index:]);
+        rchar, _    := utf8.decode_rune_in_string(to[index:]);
+
+        if (lchar == '/' || lchar == '\\') && (rchar == '/' || lchar == '\\') {
+            slash = index;
+        }
+        else if lchar != rchar {
+            break;
+        }
+
+        index += skip;
+    }
+
+    if slash < 1 {
+        // there is no common path, use the absolute `to` path
+        return strings.new_string(to, allocator);
+    }
+
+    from_slashes, to_slashes := 0, 0;
+
+    if slash < len(from) {
+        from = from[slash+1:];
+        
+        if from_is_dir {
+            from_slashes += 1;
+        }
+    }
+    else {
+        from = "";
+    }
+
+    if slash < len(to) {
+        to = to[slash+1:];
+
+        if to_is_dir {
+            to_slashes += 1;
+        }
+    }
+    else {
+        to = "";
+    }
+
+    for char in from {
+        if char == '/' || char == '\\' {
+            from_slashes += 1;
+        }
+    }
+
+    for char in to {
+        if char == '/' || char == '\\' {
+            to_slashes += 1;
+        }
+    }
+
+    if from_slashes == 0 {
+        buffer := make([]byte, 2 + len(to), allocator);
+
+        buffer[0] = '.';
+        buffer[1] = SEPARATOR;
+        copy(buffer[2:], ([]byte)(to));
+
+        return string(buffer);
+    }
+    else {
+        buffer := make([]byte, from_slashes*3 + len(to), allocator);
+
+        for i in 0..from_slashes-1 {
+            buffer[i*3+0] = '.';
+            buffer[i*3+1] = '.';
+            buffer[i*3+2] = SEPARATOR;
+        }
+
+        copy(buffer[from_slashes*3:], ([]byte)(to));
+
+        return string(buffer);
+    }
+
+    return "";
+}
+
+// returns the relative path from the current directory to another path
+rel_current :: proc(to: string, allocator := context.temp_allocator) -> string {
+    return inline rel_between(current(context.temp_allocator), to, allocator);
+}
+
+
+// splits the path elements into slices of the original path string
+split :: proc(s: string, allocator := context.temp_allocator) -> []string #no_bounds_check {
+    return inline strings.split(s, []string{"\\", "/"}, true, allocator);
+}

+ 80 - 0
core/path/path_linux.odin

@@ -0,0 +1,80 @@
+package path
+
+foreign import libc "system:c"
+
+import "core:os"
+import "core:strings"
+
+
+MAX :: 4096; // @note(bp): apparently PATH_MAX is bullshit
+
+SEPARATOR        :: '/';
+SEPARATOR_STRING :: "/";
+
+
+@(private)
+null_term :: proc(str: string) -> string {
+    for c, i in str {
+        if c == '\x00' {
+            return str[:i];
+        }
+    }
+    return str;
+}
+
+
+full :: proc(path: string, allocator := context.temp_allocator) -> string {
+    cpath := strings.clone_to_cstring(path, context.temp_allocator);
+    
+    foreign libc {
+        realpath :: proc(path: cstring, resolved_path: ^u8) -> cstring ---;
+    }
+
+    buf := make([dynamic]u8, MAX, MAX, allocator);
+
+    cstr := realpath(cpath, &buf[0]);
+    for cstr == nil && os.get_last_error() == int(os.ENAMETOOLONG) {
+        resize(&buf, len(buf) + MAX);
+        cstr = realpath(cpath, &buf[0]);
+    }
+
+    return null_term(string(buf[:]));
+}
+
+current :: proc(allocator := context.temp_allocator) -> string {
+    foreign libc{
+        getcwd :: proc(buf: ^u8, size: int) -> cstring ---;
+    }
+
+    buf := make([dynamic]u8, MAX, MAX, allocator);
+
+    cstr := getcwd(&buf[0], len(buf));
+    for cstr == nil && os.get_last_error() == int(os.ENAMETOOLONG) {
+        resize(&buf, len(buf) + MAX);
+        cstr = getcwd(&buf[0], len(buf));
+    }
+
+    return null_term(string(buf[:]));
+}
+
+
+exists :: proc(path: string) -> bool {
+    if _, err := os.stat(path); err != os.ERROR_NONE {
+        return true;
+    }
+    return false;
+}
+
+is_dir :: proc(path: string) -> bool {
+    if stat, err := os.stat(path); err == os.ERROR_NONE {
+        return os.S_ISDIR(stat.mode);
+    }
+    return false;
+}
+
+is_file :: proc(path: string) -> bool {
+    if stat, err := os.stat(path); err == os.ERROR_NONE {
+        return os.S_ISREG(stat.mode);
+    }
+    return false;
+}

+ 114 - 0
core/path/path_windows.odin

@@ -0,0 +1,114 @@
+package path
+
+import "core:strings"
+import "core:sys/win32"
+
+
+SEPARATOR        :: '\\';
+SEPARATOR_STRING :: "\\";
+
+
+@(private)
+null_term :: proc"contextless"(str: string) -> string {
+    for c, i in str {
+        if c == '\x00' {
+            return str[:i];
+        }
+    }
+    return str;
+}
+
+
+long :: proc(path: string, allocator := context.temp_allocator) -> string {
+    c_path := win32.utf8_to_wstring(path, context.temp_allocator);
+    length := win32.get_long_path_name_w(c_path, nil, 0);
+
+    if length == 0 do return "";
+
+    buf := make([]u16, length, context.temp_allocator);
+
+    win32.get_long_path_name_w(c_path, win32.Wstring(&buf[0]), length);
+
+    res := win32.ucs2_to_utf8(buf[:length], allocator);
+
+    return null_term(res);
+}
+
+short :: proc(path: string, allocator := context.temp_allocator) -> string {
+    c_path := win32.utf8_to_wstring(path, context.temp_allocator);
+    length := win32.get_short_path_name_w(c_path, nil, 0);
+
+    if length == 0 do return "";
+    
+    buf := make([]u16, length, context.temp_allocator);
+
+    win32.get_short_path_name_w(c_path, win32.Wstring(&buf[0]), length);
+
+    res := win32.ucs2_to_utf8(buf[:length], allocator);
+
+    return null_term(res);
+}
+
+full :: proc(path: string, allocator := context.temp_allocator) -> string {
+    c_path := win32.utf8_to_wstring(path, context.temp_allocator);
+    length := win32.get_full_path_name_w(c_path, 0, nil, nil);
+
+    if length == 0 do return "";
+
+    buf := make([]u16, length, context.temp_allocator);
+
+    win32.get_full_path_name_w(c_path, length, win32.Wstring(&buf[0]), nil);
+
+    res := win32.ucs2_to_utf8(buf[:length], allocator);
+
+    return null_term(res);
+}
+
+current :: proc(allocator := context.temp_allocator) -> string {
+    length := win32.get_current_directory_w(0, nil);
+
+    if length == 0 do return "";
+
+    buf := make([]u16, length, context.temp_allocator);
+
+    win32.get_current_directory_w(length, win32.Wstring(&buf[0]));
+
+    res := win32.ucs2_to_utf8(buf[:length], allocator);
+
+    return strings.trim_null(res);
+}
+
+
+exists :: proc(path: string) -> bool {
+    c_path  := win32.utf8_to_wstring(path, context.temp_allocator);
+    attribs := win32.get_file_attributes_w(c_path);
+
+    return i32(attribs) != win32.INVALID_FILE_ATTRIBUTES;
+}
+
+is_dir :: proc(path: string) -> bool {
+    c_path  := win32.utf8_to_wstring(path, context.temp_allocator);
+    attribs := win32.get_file_attributes_w(c_path);
+
+    return (i32(attribs) != win32.INVALID_FILE_ATTRIBUTES) && (attribs & win32.FILE_ATTRIBUTE_DIRECTORY == win32.FILE_ATTRIBUTE_DIRECTORY);
+}
+
+is_file :: proc(path: string) -> bool {
+    c_path  := win32.utf8_to_wstring(path, context.temp_allocator);
+    attribs := win32.get_file_attributes_w(c_path);
+
+    return (i32(attribs) != win32.INVALID_FILE_ATTRIBUTES) && (attribs & win32.FILE_ATTRIBUTE_DIRECTORY != win32.FILE_ATTRIBUTE_DIRECTORY);
+}
+
+
+drive :: proc(path: string, new := false, allocator := context.allocator) -> string {
+    if len(path) >= 3 {
+        letter := path[:2];
+
+        if path[1] == ':' && (path[2] == '\\' || path[2] == '/') {
+            return new ? strings.new_string(path[:2], allocator) : path[:2];
+        }
+    }
+
+    return "";
+}

+ 150 - 0
core/strings/strings.odin

@@ -641,6 +641,7 @@ trim_space :: proc(s: string) -> string {
 	return trim_right_space(trim_left_space(s));
 	return trim_right_space(trim_left_space(s));
 }
 }
 
 
+
 trim_left_null :: proc(s: string) -> string {
 trim_left_null :: proc(s: string) -> string {
 	return trim_left_proc(s, is_null);
 	return trim_left_proc(s, is_null);
 }
 }
@@ -653,6 +654,155 @@ trim_null :: proc(s: string) -> string {
 	return trim_right_null(trim_left_null(s));
 	return trim_right_null(trim_left_null(s));
 }
 }
 
 
+
+// returns a slice of sub-strings into `s`
+// `allocator` is used only for the slice
+// `skip_empty=true` does not return zero-length substrings
+split :: proc{split_single, split_multi};
+
+split_single :: proc(s, substr: string, skip_empty := false, allocator := context.temp_allocator) -> []string #no_bounds_check {
+    if s == "" || substr == "" do return nil;
+
+    sublen := len(substr);
+    shared := len(s) - sublen;
+
+    if shared <= 0 {
+        return nil;
+    }
+
+    // number, index, last
+    n, i, l := 0, 0, 0;
+
+    // count results
+	first_pass: for i <= shared {
+        if s[i:i+sublen] == substr {
+            if !skip_empty || i - l > 0 {
+                n += 1;
+            }
+
+            i += sublen;
+            l  = i;
+        } else {
+            _, skip := utf8.decode_rune_in_string(s[i:]);
+            i += skip;
+        }
+    }
+
+    if !skip_empty || len(s) - l > 0 { 
+        n += 1;
+    }
+
+    if n < 1 {
+    	// no results
+        return nil;
+    }
+
+    buf := make([]string, n, allocator);
+
+    n, i, l = 0, 0, 0;
+
+    // slice results
+    second_pass: for i <= shared {
+        if s[i:i+sublen] == substr {
+            if !skip_empty || i - l > 0 {
+                buf[n] = s[l:i];
+                n += 1;
+            }
+
+            i += sublen;
+            l  = i;
+        } else {
+            _, skip := utf8.decode_rune_in_string(s[i:]);
+            i += skip;
+        }
+    }
+
+    if !skip_empty || len(s) - l > 0 {
+        buf[n] = s[l:];
+    }
+
+    return buf;
+}
+
+split_multi :: proc(s: string, substrs: []string, skip_empty := false, allocator := context.temp_allocator) -> []string #no_bounds_check {
+    if s == "" || len(substrs) <= 0 {
+    	return nil;
+    }
+
+    sublen := len(substrs[0]);
+    
+    for substr in substrs[1:] {
+    	sublen = min(sublen, len(substr));
+    }
+
+    shared := len(s) - sublen;
+
+    if shared <= 0 {
+        return nil;
+    }
+
+    // number, index, last
+    n, i, l := 0, 0, 0;
+
+    // count results
+    first_pass: for i <= shared {
+    	for substr in substrs {
+		    if s[i:i+sublen] == substr {
+		        if !skip_empty || i - l > 0 {
+		            n += 1;
+		        }
+
+		        i += sublen;
+		        l  = i;
+
+		        continue first_pass;
+		    }
+    	}
+	    
+	    _, skip := utf8.decode_rune_in_string(s[i:]);
+        i += skip;
+    }
+
+    if !skip_empty || len(s) - l > 0 { 
+        n += 1;
+    }
+
+    if n < 1 {
+    	// no results
+        return nil;
+    }
+
+    buf := make([]string, n, allocator);
+
+    n, i, l = 0, 0, 0;
+
+    // slice results
+    second_pass: for i <= shared {
+    	for substr in substrs {
+		    if s[i:i+sublen] == substr {
+		        if !skip_empty || i - l > 0 {
+		            buf[n] = s[l:i];
+		            n += 1;
+		        }
+
+		        i += sublen;
+		        l  = i;
+
+		        continue second_pass;
+		    }
+    	}
+
+	    _, skip := utf8.decode_rune_in_string(s[i:]);
+	    i += skip;
+    }
+
+    if !skip_empty || len(s) - l > 0 {
+        buf[n] = s[l:];
+    }
+
+    return buf;
+}
+
 // scrub scruvs invalid utf-8 characters and replaces them with the replacement string
 // scrub scruvs invalid utf-8 characters and replaces them with the replacement string
 // Adjacent invalid bytes are only replaced once
 // Adjacent invalid bytes are only replaced once
 scrub :: proc(s: string, replacement: string, allocator := context.allocator) -> string {
 scrub :: proc(s: string, replacement: string, allocator := context.allocator) -> string {

+ 9 - 0
core/sys/win32/kernel32.odin

@@ -183,6 +183,15 @@ foreign kernel32 {
 	@(link_name="FreeLibrary")    free_library     :: proc(h: Hmodule) -> Bool ---;
 	@(link_name="FreeLibrary")    free_library     :: proc(h: Hmodule) -> Bool ---;
 	@(link_name="GetProcAddress") get_proc_address :: proc(h: Hmodule, c_str: cstring) -> rawptr ---;
 	@(link_name="GetProcAddress") get_proc_address :: proc(h: Hmodule, c_str: cstring) -> rawptr ---;
 
 
+	@(link_name="GetFullPathNameA")  get_full_path_name_a  :: proc(filename: cstring, buffer_length: u32, buffer: cstring, file_part: ^Wstring) -> u32 ---;
+	@(link_name="GetFullPathNameW")  get_full_path_name_w  :: proc(filename: Wstring, buffer_length: u32, buffer: Wstring, file_part: ^Wstring) -> u32 ---;
+	@(link_name="GetLongPathNameA")  get_long_path_name_a  :: proc(short, long: cstring, len: u32) -> u32 ---;
+	@(link_name="GetLongPathNameW")  get_long_path_name_w  :: proc(short, long: Wstring, len: u32) -> u32 ---;
+	@(link_name="GetShortPathNameA") get_short_path_name_a :: proc(long, short: cstring, len: u32) -> u32 ---;
+	@(link_name="GetShortPathNameW") get_short_path_name_w :: proc(long, short: Wstring, len: u32) -> u32 ---;
+
+	@(link_name="GetCurrentDirectoryA") get_current_directory_a :: proc(buffer_length: u32, buffer: cstring) -> u32 ---;
+	@(link_name="GetCurrentDirectoryW") get_current_directory_w :: proc(buffer_length: u32, buffer: Wstring) -> u32 ---;
 }
 }
 
 
 Memory_Basic_Information :: struct {
 Memory_Basic_Information :: struct {