Browse Source

Add `package path/filepath`; Add `os.stat` for windows (TODO: unix)

gingerBill 4 years ago
parent
commit
8cc5cd1494

+ 105 - 0
core/os/dir_windows.odin

@@ -0,0 +1,105 @@
+package os
+
+import win32 "core:sys/windows"
+import "core:strings"
+import "core:time"
+
+read_dir :: proc(fd: Handle, n: int) -> (fi: []File_Info, err: Errno) {
+	find_data_to_file_info :: proc(base_path: string, d: ^win32.WIN32_FIND_DATAW) -> (fi: File_Info) {
+		// Ignore "." and ".."
+		if d.cFileName[0] == '.' && d.cFileName[1] == 0 {
+			return;
+		}
+		if d.cFileName[0] == '.' && d.cFileName[1] == '.' && d.cFileName[2] == 0 {
+			return;
+		}
+		path := strings.concatenate({base_path, `\`, win32.utf16_to_utf8(d.cFileName[:])});
+		fi.fullpath = path;
+		fi.name = basename(path);
+		fi.size = i64(d.nFileSizeHigh)<<32 + i64(d.nFileSizeLow);
+
+		if d.dwFileAttributes & win32.FILE_ATTRIBUTE_READONLY != 0 {
+			fi.mode |= 0o444;
+		} else {
+			fi.mode |= 0o666;
+		}
+
+		is_sym := false;
+		if d.dwFileAttributes & win32.FILE_ATTRIBUTE_REPARSE_Point == 0 {
+			is_sym = false;
+		} else {
+			is_sym = d.dwReserved0 == win32.IO_REPARSE_TAG_SYMLINK || d.dwReserved0 == win32.IO_REPARSE_TAG_MOUNT_POINT;
+		}
+
+		if is_sym {
+			fi.mode |= File_Mode_Sym_Link;
+		} else {
+			if d.dwFileAttributes & win32.FILE_ATTRIBUTE_DIRECTORY != 0 {
+				fi.mode |= 0o111 | File_Mode_Dir;
+			}
+
+			// fi.mode |= file_type_mode(h);
+		}
+
+		fi.creation_time     = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftCreationTime));
+		fi.modification_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastWriteTime));
+		fi.access_time       = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastAccessTime));
+
+		fi.is_dir = fi.mode & File_Mode_Dir != 0;
+		return;
+	}
+
+	if fd == 0 {
+		return nil, ERROR_INVALID_HANDLE;
+	}
+
+	h := win32.HANDLE(fd);
+
+	dir_fi, _ := stat_from_file_information("", h);
+	if !dir_fi.is_dir {
+		return nil, ERROR_FILE_IS_NOT_DIR;
+	}
+
+	n := n;
+	size := n;
+	if n <= 0 {
+		n = -1;
+		size = 100;
+	}
+	dfi := make([dynamic]File_Info, 0, size);
+
+	wpath: []u16;
+	wpath, err = cleanpath_from_handle_u16(fd);
+	if len(wpath) == 0 || err != ERROR_NONE {
+		return;
+	}
+	wpath_search := make([]u16, len(wpath)+3, context.temp_allocator);
+	copy(wpath_search, wpath);
+	wpath_search[len(wpath)+0] = '\\';
+	wpath_search[len(wpath)+1] = '*';
+	wpath_search[len(wpath)+2] = 0;
+
+	path := cleanpath_from_buf(wpath);
+
+	find_data := &win32.WIN32_FIND_DATAW{};
+	find_handle := win32.FindFirstFileW(raw_data(wpath_search), find_data);
+	defer win32.FindClose(find_handle);
+	for n != 0 && find_handle != nil {
+		fi: File_Info;
+		fi = find_data_to_file_info(path, find_data);
+		if fi.name != "" {
+			append(&dfi, fi);
+			n -= 1;
+		}
+
+		if !win32.FindNextFileW(find_handle, find_data) {
+			e := Errno(win32.GetLastError());
+			if e == ERROR_NO_MORE_FILES {
+				break;
+			}
+			return dfi[:], e;
+		}
+	}
+
+	return dfi[:], ERROR_NONE;
+}

+ 3 - 1
core/os/os_windows.odin

@@ -32,10 +32,12 @@ ERROR_FILE_NOT_FOUND:         Errno : 2;
 ERROR_PATH_NOT_FOUND:         Errno : 3;
 ERROR_ACCESS_DENIED:          Errno : 5;
 ERROR_INVALID_HANDLE:         Errno : 6;
+ERROR_NOT_ENOUGH_MEMORY:      Errno : 8;
 ERROR_NO_MORE_FILES:          Errno : 18;
 ERROR_HANDLE_EOF:             Errno : 38;
 ERROR_NETNAME_DELETED:        Errno : 64;
 ERROR_FILE_EXISTS:            Errno : 80;
+ERROR_INVALID_PARAMETER:      Errno : 87;
 ERROR_BROKEN_PIPE:            Errno : 109;
 ERROR_BUFFER_OVERFLOW:        Errno : 111;
 ERROR_INSUFFICIENT_BUFFER:    Errno : 122;
@@ -106,7 +108,7 @@ open :: proc(path: string, mode: int = O_RDONLY, perm: int = 0) -> (Handle, Errn
 		create_mode = win32.OPEN_EXISTING;
 	}
 	wide_path := win32.utf8_to_wstring(path);
-	handle := Handle(win32.CreateFileW(auto_cast wide_path, access, share_mode, sa, create_mode, win32.FILE_ATTRIBUTE_NORMAL, nil));
+	handle := Handle(win32.CreateFileW(auto_cast wide_path, access, share_mode, sa, create_mode, win32.FILE_ATTRIBUTE_NORMAL|win32.FILE_FLAG_BACKUP_SEMANTICS, nil));
 	if handle != INVALID_HANDLE {
 		return handle, ERROR_NONE;
 	}

+ 27 - 0
core/os/stat.odin

@@ -0,0 +1,27 @@
+package os
+
+import "core:time"
+
+
+File_Info :: struct {
+	fullpath: string,
+	name:     string,
+	size:     i64,
+	mode:     File_Mode,
+	is_dir:   bool,
+	creation_time:     time.Time,
+	modification_time: time.Time,
+	access_time:       time.Time,
+}
+
+file_info_delete :: proc(fi: File_Info) {
+	delete(fi.fullpath);
+}
+
+File_Mode :: distinct u32;
+
+File_Mode_Dir         :: File_Mode(1<<16);
+File_Mode_Named_Pipe  :: File_Mode(1<<17);
+File_Mode_Device      :: File_Mode(1<<18);
+File_Mode_Char_Device :: File_Mode(1<<19);
+File_Mode_Sym_Link    :: File_Mode(1<<20);

+ 197 - 0
core/os/stat_windows.odin

@@ -0,0 +1,197 @@
+package os
+
+import "core:time"
+import win32 "core:sys/windows"
+import "core:runtime"
+import "core:strings"
+
+stat :: proc(fd: Handle) -> (File_Info, Errno) {
+	if fd == 0 {
+		return {}, ERROR_INVALID_HANDLE;
+	}
+	path, err := cleanpath_from_handle(fd);
+	if err != ERROR_NONE {
+		return {}, err;
+	}
+
+	h := win32.HANDLE(fd);
+	switch win32.GetFileType(h) {
+	case win32.FILE_TYPE_PIPE, win32.FILE_TYPE_CHAR:
+		fi: File_Info;
+		fi.fullpath = path;
+		fi.name = basename(path);
+		fi.mode |= file_type_mode(h);
+		return fi, ERROR_NONE;
+	}
+
+	return stat_from_file_information(path, h);
+}
+
+
+@(private)
+cleanpath_strip_prefix :: proc(buf: []u16) -> []u16 {
+	buf := buf;
+	N := 0;
+	for c, i in buf {
+		if c == 0 { break; }
+		N = i+1;
+	}
+	buf = buf[:N];
+
+	if len(buf) >= 4 {
+		if buf[0] == '\\'
+		&& buf[1] == '\\'
+		&& buf[2] == '?'
+		&& buf[3] == '\\' {
+			buf = buf[4:];
+		}
+	}
+	return buf;
+}
+
+@(private)
+cleanpath_from_handle :: proc(fd: Handle) -> (string, Errno) {
+	if fd == 0 {
+		return "", ERROR_INVALID_HANDLE;
+	}
+	h := win32.HANDLE(fd);
+
+	MAX_PATH := win32.DWORD(260) + 1;
+	buf: []u16;
+	for {
+		buf = make([]u16, MAX_PATH, context.temp_allocator);
+		err := win32.GetFinalPathNameByHandleW(h, raw_data(buf), MAX_PATH, 0);
+		switch Errno(err) {
+		case ERROR_PATH_NOT_FOUND, ERROR_INVALID_PARAMETER:
+			return "", Errno(err);
+		case ERROR_NOT_ENOUGH_MEMORY:
+			MAX_PATH = MAX_PATH*2 + 1;
+			continue;
+		}
+		break;
+	}
+	return cleanpath_from_buf(buf), ERROR_NONE;
+}
+@(private)
+cleanpath_from_handle_u16 :: proc(fd: Handle) -> ([]u16, Errno) {
+	if fd == 0 {
+		return nil, ERROR_INVALID_HANDLE;
+	}
+	h := win32.HANDLE(fd);
+
+	MAX_PATH := win32.DWORD(260) + 1;
+	buf: []u16;
+	for {
+		buf = make([]u16, MAX_PATH, context.temp_allocator);
+		err := win32.GetFinalPathNameByHandleW(h, raw_data(buf), MAX_PATH, 0);
+		switch Errno(err) {
+		case ERROR_PATH_NOT_FOUND, ERROR_INVALID_PARAMETER:
+			return nil, Errno(err);
+		case ERROR_NOT_ENOUGH_MEMORY:
+			MAX_PATH = MAX_PATH*2 + 1;
+			continue;
+		}
+		break;
+	}
+	return cleanpath_strip_prefix(buf), ERROR_NONE;
+}
+@(private)
+cleanpath_from_buf :: proc(buf: []u16) -> string {
+	buf := buf;
+	buf = cleanpath_strip_prefix(buf);
+	return win32.utf16_to_utf8(buf, context.allocator);
+}
+
+@(private)
+basename :: proc(name: string) -> (base: string) {
+	name := name;
+	if len(name) > 3 && name[:3] == `\\?` {
+		name = name[3:];
+	}
+
+	if len(name) == 2 && name[1] == ':' {
+		return ".";
+	} else if len(name) > 2 && name[1] == ':' {
+		name = name[2:];
+	}
+	i := len(name)-1;
+
+	for ; i > 0 && (name[i] == '/' || name[i] == '\\'); i -= 1 {
+		name = name[:i];
+	}
+	for i -= 1; i >= 0; i -= 1 {
+		if name[i] == '/' || name[i] == '\\' {
+			name = name[i+1:];
+			break;
+		}
+	}
+	return name;
+}
+
+@(private)
+file_type_mode :: proc(h: win32.HANDLE) -> File_Mode {
+	switch win32.GetFileType(h) {
+	case win32.FILE_TYPE_PIPE:
+		return File_Mode_Named_Pipe;
+	case win32.FILE_TYPE_CHAR:
+		return File_Mode_Device | File_Mode_Char_Device;
+	}
+	return 0;
+}
+
+@(private)
+stat_from_file_information :: proc(path: string, h: win32.HANDLE) -> (File_Info, Errno) {
+	d: win32.BY_HANDLE_FILE_INFORMATION;
+	if !win32.GetFileInformationByHandle(h, &d) {
+		err := Errno(win32.GetLastError());
+		return {}, err;
+
+	}
+
+	ti: win32.FILE_ATTRIBUTE_TAG_INFO;
+	if !win32.GetFileInformationByHandleEx(h, .FileAttributeTagInfo, &ti, size_of(ti)) {
+		err := win32.GetLastError();
+		if err != u32(ERROR_INVALID_PARAMETER) {
+			return {}, Errno(err);
+		}
+		// Indicate this is a symlink on FAT file systems
+		ti.ReparseTag = 0;
+	}
+
+	fi: File_Info;
+
+	fi.fullpath = path;
+	fi.name = basename(path);
+	fi.size = i64(d.nFileSizeHigh)<<32 + i64(d.nFileSizeLow);
+
+	if ti.FileAttributes & win32.FILE_ATTRIBUTE_READONLY != 0 {
+		fi.mode |= 0o444;
+	} else {
+		fi.mode |= 0o666;
+	}
+
+	is_sym := false;
+	if ti.FileAttributes & win32.FILE_ATTRIBUTE_REPARSE_Point == 0 {
+		is_sym = false;
+	} else {
+		is_sym = ti.ReparseTag == win32.IO_REPARSE_TAG_SYMLINK || ti.ReparseTag == win32.IO_REPARSE_TAG_MOUNT_POINT;
+	}
+
+	if is_sym {
+		fi.mode |= File_Mode_Sym_Link;
+	} else {
+		if ti.FileAttributes & win32.FILE_ATTRIBUTE_DIRECTORY != 0 {
+			fi.mode |= 0o111 | File_Mode_Dir;
+		}
+
+		fi.mode |= file_type_mode(h);
+	}
+
+	fi.creation_time     = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftCreationTime));
+	fi.modification_time = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastWriteTime));
+	fi.access_time       = time.unix(0, win32.FILETIME_as_unix_nanoseconds(d.ftLastAccessTime));
+
+	fi.is_dir = fi.mode & File_Mode_Dir != 0;
+
+	return fi, ERROR_NONE;
+}

+ 350 - 0
core/path/filepath/match.odin

@@ -0,0 +1,350 @@
+package filepath
+
+import "core:os"
+import "core:sort"
+import "core:strings"
+import "core:unicode/utf8"
+
+Match_Error :: enum {
+	None,
+	Syntax_Error,
+}
+
+// match states whether "name" matches the shell pattern
+// Pattern syntax is:
+//	pattern:
+//		{term}
+//	term:
+//		'*'	        matches any sequence of non-/ characters
+//		'?'             matches any single non-/ character
+//		'[' ['^']  { character-range } ']'
+//		                character classification (cannot be empty)
+//		c               matches character c (c != '*', '?', '\\', '[')
+//		'\\' c          matches character c
+//
+//	character-range
+//		c               matches character c (c != '\\', '-', ']')
+//		'\\' c          matches character c
+//		lo '-' hi       matches character c for lo <= c <= hi
+//
+// match requires that the pattern matches the entirety of the name, not just a substring
+// The only possible error returned is .Syntax_Error
+//
+// NOTE(bill): This is effectively the shell pattern matching system found
+//
+match :: proc(pattern, name: string) -> (matched: bool, err: Match_Error) {
+	pattern, name := pattern, name;
+	pattern_loop: for len(pattern) > 0 {
+		star: bool;
+		chunk: string;
+		star, chunk, pattern = scan_chunk(pattern);
+		if star && chunk == "" {
+			return !strings.contains(name, SEPARATOR_STRING), .None;
+		}
+
+		t: string;
+		ok: bool;
+		t, ok, err = match_chunk(chunk, name);
+
+		if ok && (len(t) == 0 || len(pattern) > 0) {
+			name = t;
+			continue;
+		}
+		if err != .None {
+			return;
+		}
+		if star {
+			for i := 0; i < len(name) && name[i] != SEPARATOR; i += 1 {
+				t, ok, err = match_chunk(chunk, name[i+1:]);
+				if ok {
+					if len(pattern) == 0 && len(t) > 0 {
+						continue;
+					}
+					name = t;
+					continue pattern_loop;
+				}
+				if err != .None {
+					return;
+				}
+			}
+		}
+
+		return false, .None;
+	}
+
+	return len(name) == 0, .None;
+}
+
+
+@(private="file")
+scan_chunk :: proc(pattern: string) -> (star: bool, chunk, rest: string) {
+	pattern := pattern;
+	for len(pattern) > 0 && pattern[0] == '*' {
+		pattern = pattern[1:];
+		star = true;
+	}
+	in_range := false;
+	i: int;
+
+	scan_loop: for i = 0; i < len(pattern); i += 1 {
+		switch pattern[i] {
+		case '\\':
+			when ODIN_OS != "windows" {
+				if i+1 < len(pattern) {
+					i += 1;
+				}
+			}
+		case '[':
+			in_range = true;
+		case ']':
+			in_range = false;
+		case '*':
+			if !in_range {
+				break scan_loop;
+			}
+
+		}
+	}
+	return star, pattern[:i], pattern[i:];
+}
+
+@(private="file")
+match_chunk :: proc(chunk, s: string) -> (rest: string, ok: bool, err: Match_Error) {
+	chunk, s := chunk, s;
+	for len(chunk) > 0 {
+		if len(s) == 0 {
+			return;
+		}
+		switch chunk[0] {
+		case '[':
+			r, w := utf8.decode_rune_in_string(s);
+			s = s[w:];
+			chunk = chunk[1:];
+			is_negated := false;
+			if len(chunk) > 0 && chunk[0] == '^' {
+				is_negated = true;
+				chunk = chunk[1:];
+			}
+			match := false;
+			range_count := 0;
+			for {
+				if len(chunk) > 0 && chunk[0] == ']' && range_count > 0 {
+					chunk = chunk[1:];
+					break;
+				}
+				lo, hi: rune;
+				if lo, chunk, err = get_escape(chunk); err != .None {
+					return;
+				}
+				hi = lo;
+				if chunk[0] == '-' {
+					if hi, chunk, err = get_escape(chunk[1:]); err != .None {
+						return;
+					}
+				}
+
+				if lo <= r && r <= hi {
+					match = true;
+				}
+				range_count += 1;
+			}
+			if match == is_negated {
+				return;
+			}
+
+		case '?':
+			if s[0] == SEPARATOR {
+				return;
+			}
+			_, w := utf8.decode_rune_in_string(s);
+			s = s[w:];
+			chunk = chunk[1:];
+
+		case '\\':
+			when ODIN_OS != "windows" {
+				chunk = chunk[1:];
+				if len(chunk) == 0 {
+					err = .Syntax_Error;
+					return;
+				}
+			}
+			fallthrough;
+		case:
+			if chunk[0] != s[0] {
+				return;
+			}
+			s = s[1:];
+			chunk = chunk[1:];
+
+		}
+	}
+	return s, true, .None;
+}
+
+@(private="file")
+get_escape :: proc(chunk: string) -> (r: rune, next_chunk: string, err: Match_Error) {
+	if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
+		err = .Syntax_Error;
+		return;
+	}
+	chunk := chunk;
+	if chunk[0] == '\\' && ODIN_OS != "windows" {
+		chunk = chunk[1:];
+		if len(chunk) == 0 {
+			err = .Syntax_Error;
+			return;
+		}
+	}
+
+	w: int;
+	r, w = utf8.decode_rune_in_string(chunk);
+	if r == utf8.RUNE_ERROR && w == 1 {
+		err = .Syntax_Error;
+	}
+
+	next_chunk = chunk[w:];
+	if len(next_chunk) == 0 {
+		err = .Syntax_Error;
+	}
+
+	return;
+}
+
+
+
+// glob returns the names of all files matching pattern or nil if there are no matching files
+// The syntax of patterns is the same as "match".
+// The pattern may describe hierarchical names such as /usr/*/bin (assuming '/' is a separator)
+//
+// glob ignores file system errors
+//
+glob :: proc(pattern: string, allocator := context.allocator) -> (matches: []string, err: Match_Error) {
+	if !has_meta(pattern) {
+		// TODO(bill): os.lstat on here to check for error
+		m := make([]string, 1, allocator);
+		m[0] = pattern;
+		return m[:], .None;
+	}
+
+	temp_buf: [8]byte;
+
+	dir, file := split(pattern);
+	volume_len := 0;
+	when ODIN_OS == "windows" {
+		volume_len, dir = clean_glob_path_windows(dir, temp_buf[:]);
+	} else {
+		dir = clean_glob_path(dir);
+	}
+
+	if !has_meta(dir[volume_len:]) {
+		m, e := _glob(dir, file, nil);
+		return m[:], e;
+	}
+
+	m: []string;
+	m, err = glob(dir);
+	if err != .None {
+		return;
+	}
+	dmatches := make([dynamic]string, 0, 0, allocator);
+	for d in m {
+		dmatches, err = _glob(d, file, &dmatches);
+		if err != .None {
+			break;
+		}
+	}
+	if len(dmatches) > 0 {
+		matches = dmatches[:];
+	}
+	return;
+}
+_glob :: proc(dir, pattern: string, matches: ^[dynamic]string) -> (m: [dynamic]string, e: Match_Error) {
+	if matches != nil {
+		m = matches^;
+	} else {
+		m = make([dynamic]string, 0, 0, context.allocator);
+	}
+
+
+	d, derr := os.open(dir);
+	if derr != 0 {
+		return;
+	}
+	defer os.close(d);
+
+	fi, ferr := os.stat(d);
+	if ferr != 0 {
+		os.file_info_delete(fi);
+		return;
+	}
+	if !fi.is_dir {
+		os.file_info_delete(fi);
+		return;
+	}
+
+
+	fis, _ := os.read_dir(d, -1);
+	sort.quick_sort_proc(fis, proc(a, b: os.File_Info) -> int {
+		return sort.compare_strings(a.name, b.name);
+	});
+	defer {
+		for fi in fis {
+			os.file_info_delete(fi);
+		}
+		delete(fis);
+	}
+
+	for fi in fis {
+		n := fi.name;
+		matched, err := match(pattern, n);
+		if err != nil {
+			return m, err;
+		}
+		if matched {
+			append(&m, join(dir, n));
+		}
+	}
+	return;
+}
+
+@(private)
+has_meta :: proc(path: string) -> bool {
+	when ODIN_OS == "windows" {
+		CHARS :: `*?[`;
+	} else {
+		CHARS :: `*?[\`;
+	}
+	return strings.contains_any(path, CHARS);
+}
+
+@(private)
+clean_glob_path :: proc(path: string) -> string {
+	switch path {
+	case "":
+		return ".";
+	case SEPARATOR_STRING:
+		return path;
+	}
+	return path[:len(path)-1];
+}
+
+
+@(private)
+clean_glob_path_windows :: proc(path: string, temp_buf: []byte) -> (prefix_len: int, cleaned: string) {
+	vol_len := volume_name_len(path);
+	switch {
+	case path == "":
+		return 0, ".";
+	case vol_len+1 == len(path) && is_separator(path[len(path)-1]): // /, \, C:\, C:/
+		return vol_len+1, path;
+	case vol_len == len(path) && len(path) == 2: // C:
+		copy(temp_buf[:], path);
+		temp_buf[2] = '.';
+		return vol_len, string(temp_buf[:3]);
+	}
+
+	if vol_len >= len(path) {
+		vol_len = len(path) -1;
+	}
+	return vol_len, path[:len(path)-1];
+}

+ 199 - 0
core/path/filepath/path.odin

@@ -0,0 +1,199 @@
+package filepath
+
+import "core:os"
+import "core:strings"
+
+// is_separator checks whether the byte is a valid separator character
+is_separator :: proc(c: byte) -> bool {
+	switch c {
+	case '/':  return true;
+	case '\\': return ODIN_OS == "windows";
+	}
+	return false;
+}
+
+@(private)
+is_slash :: proc(c: byte) -> bool {
+	return c == '\\' || c == '/';
+}
+
+split :: proc(path: string) -> (dir, file: string) {
+	vol := volume_name(path);
+	i := len(path) - 1;
+	for i >= len(vol) && !is_separator(path[i]) {
+		i -= 1;
+	}
+	return path[:i+1], path[i+1:];
+}
+
+volume_name :: proc(path: string) -> string {
+	return path[:volume_name_len(path)];
+}
+
+volume_name_len :: proc(path: string) -> int {
+	if len(path) < 2 {
+		return 0;
+	}
+	c := path[0];
+	if path[1] == ':' {
+		switch c {
+		case 'a'..'z', 'A'..'Z':
+			return 2;
+		}
+	}
+
+	if l := len(path); l >= 5 && is_slash(path[0]) && is_slash(path[1]) &&
+		!is_slash(path[2]) && path[2] != '.' {
+		for n := 3; n < l-1; n += 1 {
+			if is_slash(path[n]) {
+				n += 1;
+				if !is_slash(path[n]) {
+					if path[n] == '.' {
+						break;
+					}
+				}
+				for ; n < l; n += 1 {
+					if is_slash(path[n]) {
+						break;
+					}
+				}
+				return n;
+			}
+			break;
+		}
+	}
+	return 0;
+}
+
+
+clean :: proc(path: string, allocator := context.allocator) -> string {
+	context.allocator = allocator;
+
+	path := path;
+	original_path := path;
+	vol_len := volume_name_len(path);
+	path = path[vol_len:];
+
+	if path == "" {
+		if vol_len > 1 && original_path[1] != ':' {
+			return from_slash(original_path);
+		}
+		return strings.concatenate({original_path, "."});
+	}
+
+	rooted := is_separator(path[0]);
+
+	n := len(path);
+	out := &Lazy_Buffer{
+		s = path,
+		vol_and_path = original_path,
+		vol_len = vol_len,
+	};
+
+	r, dot_dot := 0, 0;
+	if rooted {
+		lazy_buffer_append(out, '/');
+		r, dot_dot = 1, 1;
+	}
+
+	for r < n {
+		switch {
+		case is_separator(path[r]):
+			r += 1;
+		case path[r] == '.' && (r+1 == n || is_separator(path[r+1])):
+			r += 1;
+		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || is_separator(path[r+2])):
+			r += 2;
+			switch {
+			case out.w > dot_dot:
+				out.w -= 1;
+				for out.w > dot_dot && !is_separator(lazy_buffer_index(out, out.w)) {
+					out.w -= 1;
+				}
+			case !rooted:
+				if out.w > 0 {
+					lazy_buffer_append(out, '/');
+				}
+				lazy_buffer_append(out, '.');
+				lazy_buffer_append(out, '.');
+				dot_dot = out.w;
+			}
+		case:
+			if rooted && out.w != 1 || !rooted && out.w != 0 {
+				lazy_buffer_append(out, '/');
+			}
+			for ; r < n && !is_separator(path[r]); r += 1 {
+				lazy_buffer_append(out, path[r]);
+			}
+
+		}
+	}
+
+	if out.w == 0 {
+		lazy_buffer_append(out, '.');
+	}
+
+	s := lazy_buffer_string(out);
+	cleaned := from_slash(s);
+	return cleaned;
+}
+
+from_slash :: proc(path: string, allocator := context.allocator) -> string {
+	if SEPARATOR == '/' {
+		return path;
+	}
+	s, ok := strings.replace_all(path, "/", SEPARATOR_STRING, allocator);
+	if !ok {
+		s = strings.clone(s, allocator);
+	}
+	return s;
+}
+
+
+
+/*
+	Lazy_Buffer is a lazily made path buffer
+	When it does allocate, it uses the context.allocator
+ */
+@(private)
+Lazy_Buffer :: struct {
+	s: string,
+	b: []byte,
+	w: int, // write index
+	vol_and_path: string,
+	vol_len:      int,
+}
+
+@(private)
+lazy_buffer_index :: proc(lb: ^Lazy_Buffer, i: int) -> byte {
+	if lb.b != nil {
+		return lb.b[i];
+	}
+	return lb.s[i];
+}
+@(private)
+lazy_buffer_append :: proc(lb: ^Lazy_Buffer, c: byte) {
+	if lb.b == nil {
+		if lb.w < len(lb.s) && lb.s[lb.w] == c {
+			lb.w += 1;
+			return;
+		}
+		lb.b = make([]byte, len(lb.s));
+		copy(lb.b, lb.s[:lb.w]);
+	}
+	lb.b[lb.w] = c;
+	lb.w += 1;
+}
+@(private)
+lazy_buffer_string :: proc(lb: ^Lazy_Buffer) -> string {
+	if lb.b == nil {
+		return strings.clone(lb.vol_and_path[:lb.vol_len+lb.w]);
+	}
+
+	x := lb.vol_and_path[:lb.vol_len];
+	y := string(lb.b[:lb.w]);
+	z := make([]byte, len(x)+len(y));
+	copy(z, x);
+	copy(z[len(x):], y);
+	return string(z);
+}

+ 5 - 0
core/path/filepath/path_unix.odin

@@ -0,0 +1,5 @@
+//+build linux, darwin, freebsd
+package filepath
+
+SEPARATOR :: '/';
+SEPARATOR_STRING :: `/`;

+ 71 - 0
core/path/filepath/path_windows.odin

@@ -0,0 +1,71 @@
+package filepath
+
+import "core:strings"
+
+SEPARATOR :: '\\';
+SEPARATOR_STRING :: `\`;
+
+
+reserved_names := []string{
+	"CON", "PRN", "AUX", "NUL",
+	"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+	"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
+};
+
+is_reserved_name :: proc(path: string) -> bool {
+	if len(path) == 0 {
+		return false;
+	}
+	for reserved in reserved_names {
+		if strings.equal_fold(path, reserved) {
+			return true;
+		}
+	}
+	return false;
+}
+
+is_UNC :: proc(path: string) -> bool {
+	return volume_name_len(path) > 2;
+}
+
+join :: proc(elems: ..string, allocator := context.allocator) -> string {
+	for e, i in elems {
+		if e != "" {
+			return join_non_empty(elems[i:]);
+		}
+	}
+	return "";
+}
+
+join_non_empty :: proc(elems: []string) -> string {
+	if len(elems[0]) == 2 && elems[0][1] == ':' {
+		i := 1;
+		for ; i < len(elems); i += 1 {
+			if elems[i] != "" {
+				break;
+			}
+		}
+		s := strings.join(elems[i:], SEPARATOR_STRING, context.temp_allocator);
+		s = strings.concatenate({elems[0], s}, context.temp_allocator);
+		return clean(s);
+	}
+
+	s := strings.join(elems, SEPARATOR_STRING, context.temp_allocator);
+	p := clean(s);
+	if !is_UNC(p) {
+		return p;
+	}
+
+	head := clean(elems[0], context.temp_allocator);
+	if is_UNC(head) {
+		return p;
+	}
+	delete(p); // It is not needed now
+
+	tail := clean(strings.join(elems[1:], SEPARATOR_STRING, context.temp_allocator), context.temp_allocator);
+	if head[len(head)-1] == SEPARATOR {
+		return strings.concatenate({head, tail});
+	}
+
+	return strings.concatenate({head, SEPARATOR_STRING, tail});
+}

+ 12 - 12
core/path/path.odin

@@ -4,8 +4,8 @@ import "core:strings"
 import "core:runtime"
 import "core:unicode/utf8"
 
-// is_separator_byte checks whether the byte is a valid separator character
-is_separator_byte :: proc(c: byte) -> bool {
+// is_separator checks whether the byte is a valid separator character
+is_separator :: proc(c: byte) -> bool {
 	switch c {
 	case '/':  return true;
 	case '\\': return ODIN_OS == "windows";
@@ -23,7 +23,7 @@ is_abs :: proc(path: string) -> bool {
 		if len(path) > 2 {
 			switch path[0] {
 			case 'A'..'Z', 'a'..'z':
-				return path[1] == ':' && is_separator_byte(path[2]);
+				return path[1] == ':' && is_separator(path[2]);
 			}
 		}
 	}
@@ -48,7 +48,7 @@ base :: proc(path: string, new := false, allocator := context.allocator) -> (las
 
 	path := path;
 
-	for len(path) > 0 && is_separator_byte(path[len(path)-1]) {
+	for len(path) > 0 && is_separator(path[len(path)-1]) {
 		path = path[:len(path)-1];
 	}
 	if i := strings.last_index_any(path, OS_SEPARATORS); i >= 0 {
@@ -103,7 +103,7 @@ clean :: proc(path: string, allocator := context.allocator) -> string {
 		return strings.clone(".");
 	}
 
-	// NOTE(bill): do not use is_separator_byte because window paths do not follow this convention
+	// NOTE(bill): do not use is_separator because window paths do not follow this convention
 	rooted := path[0] == '/';
 	n := len(path);
 
@@ -118,16 +118,16 @@ clean :: proc(path: string, allocator := context.allocator) -> string {
 
 	for r < n {
 		switch {
-		case is_separator_byte(path[r]):
+		case is_separator(path[r]):
 			r += 1;
-		case path[r] == '.' && (r+1 == n || is_separator_byte(path[r+1])):
+		case path[r] == '.' && (r+1 == n || is_separator(path[r+1])):
 			r += 1;
-		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || is_separator_byte(path[r+2])):
+		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || is_separator(path[r+2])):
 			r += 2;
 			switch {
 			case out.w > dot_dot:
 				out.w -= 1;
-				for out.w > dot_dot && !is_separator_byte(lazy_buffer_index(out, out.w)) {
+				for out.w > dot_dot && !is_separator(lazy_buffer_index(out, out.w)) {
 					out.w -= 1;
 				}
 
@@ -143,7 +143,7 @@ clean :: proc(path: string, allocator := context.allocator) -> string {
 			if rooted && out.w != 1 || !rooted && out.w != 0 {
 				lazy_buffer_append(out, '/');
 			}
-			for ; r < n && !is_separator_byte(path[r]); r += 1 {
+			for ; r < n && !is_separator(path[r]); r += 1 {
 				lazy_buffer_append(out, path[r]);
 			}
 		}
@@ -173,7 +173,7 @@ join :: proc(elems: ..string, allocator := context.allocator) -> string {
 // The extension is the suffix beginning at the file fot in the last slash separated element of "path"
 // The path is empty if there is no dot
 ext :: proc(path: string, new := false, allocator := context.allocator) -> string {
-	for i := len(path)-1; i >= 0 && !is_separator_byte(path[i]); i -= 1 {
+	for i := len(path)-1; i >= 0 && !is_separator(path[i]); i -= 1 {
 		if path[i] == '.' {
 			res := path[i:];
 			if new {
@@ -194,7 +194,7 @@ name :: proc(path: string, new := false, allocator := context.allocator) -> (nam
 		name = strings.clone(name, allocator);
 	}
 
-	for i := len(file)-1; i >= 0 && !is_separator_byte(file[i]); i -= 1 {
+	for i := len(file)-1; i >= 0 && !is_separator(file[i]); i -= 1 {
 		if file[i] == '.' {
 			name = file[:i];
 			return;

+ 2 - 0
core/sys/windows/kernel32.odin

@@ -273,4 +273,6 @@ foreign kernel32 {
 	GetLongPathNameW  :: proc(short, long: LPCWSTR, len: DWORD) -> DWORD ---
 	GetShortPathNameW :: proc(long, short: LPCWSTR, len: DWORD) -> DWORD ---
 
+	GetFinalPathNameByHandleW :: proc(hFile: HANDLE, lpszFilePath: LPCWSTR, cchFilePath: DWORD, dwFlags: DWORD) -> DWORD ---
+
 }

+ 11 - 0
core/sys/windows/types.odin

@@ -83,6 +83,7 @@ FILE_ATTRIBUTE_NORMAL: DWORD : 0x00000080;
 FILE_ATTRIBUTE_TEMPORARY: DWORD : 0x00000100;
 FILE_ATTRIBUTE_SPARSE_FILE: DWORD : 0x00000200;
 FILE_ATTRIBUTE_REPARSE_Point: DWORD : 0x00000400;
+FILE_ATTRIBUTE_REPARSE_POINT: DWORD : 0x00000400;
 FILE_ATTRIBUTE_COMPRESSED: DWORD : 0x00000800;
 FILE_ATTRIBUTE_OFFLINE: DWORD : 0x00001000;
 FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: DWORD : 0x00002000;
@@ -535,6 +536,11 @@ FILETIME :: struct {
 	dwHighDateTime: DWORD,
 }
 
+FILETIME_as_unix_nanoseconds :: proc "contextless" (ft: FILETIME) -> i64 {
+	t := i64(u64(ft.dwLowDateTime) | u64(ft.dwHighDateTime) << 32);
+	return (t - 0x019db1ded53e8000) * 100;
+}
+
 OVERLAPPED :: struct {
 	Internal: ^c_ulong,
 	InternalHigh: ^c_ulong,
@@ -675,6 +681,11 @@ FILE_STANDARD_INFO :: struct {
 	Directory: BOOLEAN,
 }
 
+FILE_ATTRIBUTE_TAG_INFO :: struct {
+	FileAttributes: DWORD,
+	ReparseTag: DWORD,
+}
+
 
 
 // https://docs.microsoft.com/en-gb/windows/win32/api/sysinfoapi/ns-sysinfoapi-system_info

+ 14 - 0
core/time/time.odin

@@ -135,6 +135,20 @@ read_cycle_counter :: proc() -> u64 {
 }
 
 
+unix :: proc(sec: i64, nsec: i64) -> Time {
+	sec, nsec := sec, nsec;
+	if nsec < 0 || nsec >= 1e9 {
+		n := nsec / 1e9;
+		sec += n;
+		nsec -= n * 1e9;
+		if nsec < 0 {
+			nsec += 1e9;
+			sec -= 1;
+		}
+	}
+	return Time{(sec*1e9 + nsec) + UNIX_TO_INTERNAL};
+}
+
 
 
 ABSOLUTE_ZERO_YEAR :: i64(-292277022399); // Day is chosen so that 2001-01-01 is Monday in the calculations

+ 1 - 5
core/time/time_windows.odin

@@ -6,12 +6,8 @@ IS_SUPPORTED :: true;
 
 now :: proc() -> Time {
 	file_time: win32.FILETIME;
-
 	win32.GetSystemTimeAsFileTime(&file_time);
-
-	ft := i64(u64(file_time.dwLowDateTime) | u64(file_time.dwHighDateTime) << 32);
-
-	ns := (ft - 0x019db1ded53e8000) * 100;
+	ns := win32.FILETIME_as_unix_nanoseconds(file_time);
 	return Time{_nsec=ns};
 }