Skip to content

Commit

Permalink
Merge pull request #28 from alaviss/features/paths
Browse files Browse the repository at this point in the history
sys/paths: a cross-platform path manipulation library

This implementation stays on the simple side and only implements
enough for sys/exec usage.
  • Loading branch information
alaviss authored Sep 21, 2023
2 parents c632f88 + 086e973 commit fe482b7
Show file tree
Hide file tree
Showing 7 changed files with 977 additions and 0 deletions.
186 changes: 186 additions & 0 deletions src/sys/paths.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#
# Abstractions for operating system services
# Copyright (c) 2021 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

## Cross-platform utilities to manipulate path names. The manipulation routines
## work directly on the paths and does not check the file system.

import std/strutils
import strings

type
Path* {.requiresInit.} = distinct Nulless
## A distinct string representing an operating system path.
##
## For POSIX, the path will always be represented under the following rules:
##
## * Any `/..` at the beginning will be collasped into `/`.
##
## * Any `.` path element will be omitted, unless its the only element.
##
## * Any `//` will be converted to `/`.
##
## * Any trailing slash will be removed.
##
## For Windows, the path will always be represented under the following rules:
##
## * 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

ComponentSlice* = tuple
## A tuple describing a path component
kind: ComponentKind
slice: Slice[int] # The slice of the input string with the type

Component* = tuple
## A tuple describing a path component
kind: ComponentKind
path: Path # The slice with the 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
## The main path separator of the target operating system.

ValidSeparators* = ValidSeparatorsImpl
## The valid path separators of the target operating system.

converter toNulless*(p: Path): Nulless =
## One-way implicit conversion to nulless string.
##
## This allows read-only string operations to be done on `p`.
Nulless(p)

converter toString*(p: Path): string =
## One-way implicit conversion to string.
##
## This allows read-only string operations to be done on `p`.
string(p)

iterator componentSlices*(s: Path | Nulless): ComponentSlice =
## Parse `s` and yields its path components as slices of the input.
##
## Some normalizations are done:
##
## * Duplicated separators (ie. `//`) will be skipped.
##
## * Current directory (ie. `.`) will be skipped.
##
## * Previous directories relative to root (ie. `/..`) will be skipped.
##
## **Platform specific details**
##
## * Currently, `Prefix` is only yielded on Windows.
##
## * Windows' UNC prefix will be splitted into two parts, both branded as
## `Prefix`.
componentSlicesImpl()

iterator componentSlices*(s: string): ComponentSlice =
## Parse `s` and yields its path components as slices of the input.
##
## Overload of `componentSlices(Nulless) <#componentSlices,Nulless>`_ for
## strings.
##
## An error will be raised if `s` contains `NUL`.
for result in s.toNulless.componentSlices:
yield result

iterator components*(s: Path | Nulless): Component =
## Parse `s` and yields its path components.
##
## Some normalizations are done:
##
## * Duplicated separators (ie. `//`) will be skipped.
##
## * Current directory (ie. `.`) will be skipped.
##
## * Previous directories relative to root (ie. `/..`) will be skipped.
for kind, slice in s.componentSlices:
yield (kind, Path s[slice])

iterator components*(s: string): Component =
## Parse `s` and yields its path components as slices of the input.
##
## Overload of `components(Nulless) <#components,Nulless>`_ for
## strings.
##
## A `ValueError` will be raised if `s` contains `NUL`.
for result in s.toNulless.components:
yield result

func isAbsolute*(p: Path): bool {.inline, raises: [].} =
## Returns whether the path `p` is an absolute path.
isAbsoluteImpl()

func join*[T: string | Nulless | Path](base: var Path, parts: varargs[T])
{.raises: [ValueError].} =
## Join `parts` to `base` path.
##
## Each `parts` entry is treated as if its relative to the result of `base`
## 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 =
## Returns `p` as is. This is provided for generics.
p

func toPath*(s: string | Nulless): Path {.raises: [ValueError].} =
## Convert the string `s` into a `Path`.
##
## An empty string is assumed to be `.` (current directory).
##
## Raises `ValuesError` if an invalid character is found in `s`.
toPathImpl()

func `/`*[T: string | Nulless | Path](base, rel: T): Path
{.raises: [ValueError], inline.} =
## Returns a path formed by joining `base` with `rel`.
result = base.toPath
result.join rel
102 changes: 102 additions & 0 deletions src/sys/private/paths_posix.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#
# Abstractions for operating system services
# Copyright (c) 2021 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 strsliceutils

const
SeparatorImpl = '/'
ValidSeparatorsImpl = {SeparatorImpl}

template componentSlicesImpl() {.dirty.} =
var atRoot = false # Whether we are at root FS

for slice in s.splitSlices(ValidSeparators):
# If the slice is zero-length
if slice.len == 0:
# If it starts at index 0
if slice.a == 0:
# Then this is created by the root position (ie. the left side of "/").
yield (Root, 0 .. 0)
atRoot = true

# Otherwise its the same as "current directory" and can be omitted.
else:
discard "duplicated /, can be skipped"

else:
if s.slice(slice) == ".":
discard "current directory are skipped"
elif s.slice(slice) == "..":
# Only yield previous directory if its not the root FS
if not atRoot:
yield (PreviousDir, slice)
else:
atRoot = false
yield (Element, slice)

template isAbsoluteImpl(): bool {.dirty.} =
p[0] == Separator

template joinImpl() {.dirty.} =
# The joiner would join the path like this:
#
# base <- "a/b": "<base>/a/b"
#
# Which would result in:
#
# "." <- "a/b": "./a/b"
#
# which we do not want.
#
# To keep the logic simple, simply empty out the path here, which will be
# interpreted by the joiner as "relative to current dir" and will not insert
# a separator.
if base == ".":
base.string.setLen 0

var atRoot = base == "/"
for part in parts.items:
for kind, slice in part.componentSlices:
if kind == PreviousDir and atRoot:
discard "At root"
# Ignore "root" because all parts are supposed to be relative to the
# current base.
elif kind != Root:
# If the next position is not at the start of the path and there were
# no separator at the end of the current path.
if base.len > 0 and base[^1] != Separator:
# Insert a separator
base.string.add Separator

# Copy the slice to the current write position.
base.string.add part.slice(slice)
atRoot = 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

# If the path is absolute
if s.len > 0 and s.Path.isAbsolute:
# Set the base to `/`
result.string.add '/'
else:
# Set the base to `.`
result.string.add '.'

# Add the path into `s`
result.join s
Loading

0 comments on commit fe482b7

Please sign in to comment.