Skip to content

Commit

Permalink
sys/paths(windows): implements Windows version
Browse files Browse the repository at this point in the history
  • Loading branch information
alaviss committed Sep 18, 2023
1 parent 817a723 commit d6593b9
Show file tree
Hide file tree
Showing 3 changed files with 505 additions and 0 deletions.
35 changes: 35 additions & 0 deletions src/sys/paths.nim
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,34 @@ type
##
## * Any trailing slash will be removed.
##
## For Windows, the path will always be represented under the following rules:
##

Check failure on line 30 in src/sys/paths.nim

View workflow job for this annotation

GitHub Actions / linux on i386 (Nim devel)

'`' expected

Check failure on line 30 in src/sys/paths.nim

View workflow job for this annotation

GitHub Actions / linux on amd64 (Nim devel)

'`' expected

Check failure on line 30 in src/sys/paths.nim

View workflow job for this annotation

GitHub Actions / macos on amd64 (Nim devel)

'`' expected

Check failure on line 30 in src/sys/paths.nim

View workflow job for this annotation

GitHub Actions / linux on i386 (Nim devel)

'`' expected

Check failure on line 30 in src/sys/paths.nim

View workflow job for this annotation

GitHub Actions / linux on amd64 (Nim devel)

'`' expected

Check failure on line 30 in src/sys/paths.nim

View workflow job for this annotation

GitHub Actions / macos on amd64 (Nim devel)

'`' expected
## * Any `/` separator will be converted to `\`.
##
## * Any `\..` at the root component will be collasped into `\\`.
##
## * Any `.` path element will be omitted, unless its the only element or is necessary
## for disambiguation.
##
## * Any `\\` will be converted to `\`, unless they occur at the beginning of the path.
##
## * Any trailing backslash will be removed if they are not significant.
##
## * For DOS paths, the drive letter is always in uppercase.
##
## This type does not support Windows' native NT paths (paths starting with `\??`) and
## will treat them as relative paths. The `\\?` prefix should be used to
## handle them instead.
##
## The path is never an empty string.

ComponentKind* {.pure.} = enum
## The type of path component
Prefix ## The prefix in which a rooted path will start from.
##
## A path might have more than one prefix (ie. UNC host and shares).
## In such cases the prefixes can be concatenated into one using
## the `Separator`.
Root
PreviousDir
Element
Expand All @@ -46,6 +70,10 @@ type

when defined(posix):
include private/paths_posix
elif defined(windows):
include private/paths_windows
else:
{.error: "This module has not been ported to your operating system.".}

const
Separator* = SeparatorImpl
Expand Down Expand Up @@ -123,6 +151,13 @@ func join*[T: string | Nulless | Path](base: var Path, parts: varargs[T])
## joined with prior entries.
##
## If any of `parts` contains `NUL`, `ValueError` will be raised.
##
## **Platform specific details**
##
## * On Windows, drive-relative paths can only be created if the base itself is
## pointing to a drive-relative entry (ie. `C:relative`). A bare drive like `C:`
## will always be joined into a drive-absolute path to reduce the surprise factor.
##
joinImpl()

func toPath*(p: sink Path): Path =
Expand Down
252 changes: 252 additions & 0 deletions src/sys/private/paths_windows.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#
# Abstractions for operating system services
# Copyright (c) 2023 Leorize
#
# Licensed under the terms of the MIT license which can be found in
# the file "license.txt" included with this distribution. Alternatively,
# the full text can be found at: https://spdx.org/licenses/MIT.html

import ".."/strings

import strsliceutils

const
SeparatorImpl = '\\'
ValidSeparatorsImpl = {SeparatorImpl, '/'}

template componentSlicesImpl() {.dirty.} =
type
State = enum
Start
MaybeRoot
FoundPrefix
DosDrivePrefix
UncPrefix
AtRoot
PathElement

var state = Start

for slice in s.splitSlices(ValidSeparators):
var
stay = true
slice = slice
while stay:
stay = false
case state
of Start:
# Drive letter and maybe a path component
if s.slice(slice).hasDosDrive:
yield (Prefix, 0 .. 1)
state = DosDrivePrefix
if slice.len > 2:
# This is a relative path (ie. C:abc)
# Trim the drive letter and switch gear
slice = slice.a + 2 .. slice.b
state = PathElement
stay = true

# Single \
elif slice.len == 0:
state = MaybeRoot

else:
state = PathElement
stay = true

of MaybeRoot:
# Double \
if slice.len == 0:
state = FoundPrefix
else:
yield (Root, 0 ..< 0)
state = AtRoot
stay = true

of FoundPrefix:
# Special prefix for NT and DOS paths
if slice.len == 1 and s[slice.a] in {'.', '?'}:
state = AtRoot
yield (Prefix, 0 .. slice.b)
yield (Root, 0 ..< 0)
# UNC otherwise
else:
state = UncPrefix
yield (Prefix, 0 .. slice.b)

of DosDrivePrefix:
# There is something after the DOS drive, this is a rooted path
yield (Root, 0 ..< 0)
state = AtRoot
stay = true

of UncPrefix:
if slice.len > 0:
state = AtRoot
yield (Prefix, slice)
yield (Root, 0 ..< 0)

of AtRoot:
if s.slice(slice) == "." or s.slice(slice) == "..":
discard ". and .. at root is still root"
elif slice.len > 0:
state = PathElement
yield (Element, slice)

of PathElement:
if slice.len > 0:
if s.slice(slice) == "..":
yield (PreviousDir, slice)
elif s.slice(slice) != ".":
yield (Element, slice)

case state
# Nothing after we found '\' or '\\'
# Then it's just a root.
of MaybeRoot, FoundPrefix:
yield (Root, 0 ..< 0)
# Incomplete UNC path
# Cap it off with a root.
of UncPrefix:
yield (Root, 0 ..< 0)

func isNotDos(p: Path | Nulless | openarray[char]): bool =
## Returns whether `p` is not a DOS path.
p.len > 1 and p.slice[0] == '\\' and p.slice[1] == '\\'

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on i386 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

Check failure on line 115 in src/sys/private/paths_windows.nim

View workflow job for this annotation

GitHub Actions / windows on amd64 (Nim devel)

type mismatch

func hasDosDrive(p: Path | Nulless | openarray[char]): bool =
## Returns whether `p` is prefixed with a DOS drive.
p.len > 1 and p[1] == ":"

func isIncompleteUnc(p: Path): bool =
## Returns whether `p` is an UNC path without a share.
if not p.isNotDos:
return false

for kind, slice in p.componentSlices:
case kind
of Prefix:
if p.slice(slice) == r"\\." or p.slice(slice) == r"\\?":
return false

result = not result
else:
break

template isAbsoluteImpl(): bool {.dirty.} =
p.isNotDos() or (p.len > 2 and p.slice(1..2) == r":\")

template joinImpl() {.dirty.} =
# Temporary empty out the base if it's the current directory.
#
# It will be inserted for disambiguation as needed.
if base == ".":
base.string.setLen 0

var needTrailingSep = base.isIncompleteUnc
for part in parts.items:
for kind, slice in part.componentSlices:
case kind
of Root:
discard "All should be relative to current base"
of Prefix:
if part.slice(slice) == r"\\." or part.slice(slice) == r"//.":
discard "Skipped to avoid redundant current-directory symbols"
elif part.slice(slice).isNotDos:
# Treat these like a regular subfolder, for example:
#
# * `a` join `\\?\C:` => `a\?\C:`
# * `.` join `\\?\C:` => `?\C:`
#
# At this point, only these can be found:
#
# * `\\?`
# * `\\string without backslash`
#
# Skip the first two (back)slashes
let slice = slice.a + 2 .. slice.b

# If the result looks like it starts with a DOS drive and the path is empty
if part.slice(slice).hasDosDrive and base.len == 0:
# Add `.\` to disambiguates
base.string.add r".\"

base.string.add part.slice(slice)
elif slice.len > 0:
# The prefix is either a DOS drive or UNC share
#
# If the result looks like it starts with a DOS drive and the path is empty
if part.slice(slice).hasDosDrive and base.len == 0:
# Disambiguates with `.\`
base.string.add r".\"

base.string.add part.slice(slice)
else:
# If the result looks like it starts with a DOS drive and the path is empty
if part.slice(slice).hasDosDrive and base.len == 0:
# Add `.\` to disambiguate
base.string.add r".\"

# If the next position is not at the start of the path and there were
# no separator at the end of the current path.
elif base.len > 0 and base[^1] != Separator:
# Insert a separator
base.string.add Separator

base.string.add part.slice(slice)

if needTrailingSep and base[^1] != Separator:
base.string.add Separator
needTrailingSep = false

# If the path is empty
if base.len == 0:
# Set it to "."
base.string.add '.'

template toPathImpl() {.dirty.} =
result = Path:
# Create a new buffer with the length of `s`.
var path = newString(s.len)
# Set the length to zero, which lets us keep the buffer.
path.setLen 0
path

var afterPrefix = false
for kind, slice in s.componentSlices:
case kind
of Prefix:
if s.slice(slice).isNotDos:
# Skips the first two (back)slashes
let slice = slice.a + 2 .. slice.b
# Add our own
path.add r"\\"
# And the rest
path.add s.slice(slice)
elif s.slice(slice).hasDosDrive:
# Normalize the drive by uppercasing it
path.add: toAsciiUpper s[slice.a]
path.add ':'
else:
# UNC share name
path.add '\\'
path.add s.slice(slice)

afterPrefix = true
of Root:
path.add Separator

afterPrefix = false
else:
# Add separator as needed
if afterPrefix:
discard "Don't add separator after a prefix to handle drive-relative paths"
elif path.len > 0 and path[^1] != Separator:
path.add Separator
# Disambiguates an element that looked like a drive
elif path.len == 0 and s.slice(slice).hasDosDrive:
path.add r".\"

path.add s.slice(slice)

afterPrefix = false
Loading

0 comments on commit d6593b9

Please sign in to comment.