std: ossymlinks.expandSymlink via reparse-point parsing (#25701)

This PR implements `expandSymlink` on Windows with POSIX readlink
semantics: it expands exactly one hop and returns the stored link target
without resolving the full chain.

The main design question was whether Windows symlink expansion should be
built on path-finalization APIs such as `GetFinalPathNameByHandleW`, or
on direct reparse-point inspection. Current `expandSymlink` is a
single-hop "what target is stored in this link object?" operation and
most of other ways to resolve symlinks on Windows actually try to answer
the "final true file location" question in various slightly-incompatible
ways.

The full final-path resolution on Windows is substantially more complex
than readlink and is planned as a follow-up.

## Implementation choice

Implements Windows `expandSymlink` by:

- opening the path with `FILE_FLAG_OPEN_REPARSE_POINT`
- calling `DeviceIoControl(FSCTL_GET_REPARSE_POINT)`
- parsing the reparse payload for `IO_REPARSE_TAG_SYMLINK` and
`IO_REPARSE_TAG_MOUNT_POINT`
- decoding the UTF-16 slice referenced by the payload
- returning the stored target

This is the right primitive for the API:
- does not depend on whole-path finalization
- works for both symlinks and junctions
- matches the existing Linux behaviour

`widestrs` changes allow using WideCString views without temporary
allocations.

Windows prohibits symlink creation without admin rights, so,
unfortunately, the tests are conditionally skipped by default. Manually
running `testament` in an admin console is required.

## Behaviour:

- One hop only
- Relative symlink targets are returned unchanged
- Absolute Windows targets are converted from stored NT-style prefixes
to usable Win32 forms when applicable
- Non-links, malformed payloads, and unsupported reparse tags raise
`OSError`

## Future work

Path canonicalization, i.e. "final true file location". Which is, BTW,
different from `absolutePath`, which works on paths only and doesn't hit
the underlying FS. So this needs to be an API extension.

I'd like to follow-up with this when I sort through the docs, for now
you can resolve symlinks in a loop.

---------

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
This commit is contained in:
Zoom
2026-06-29 20:25:41 +04:00
committed by GitHub
parent 58a5b9e27e
commit 00d8f66311
6 changed files with 379 additions and 123 deletions

View File

@@ -79,6 +79,8 @@ parameter and result types, not just their source-level shape. Use
Modes include `Nim` (default, fully compatible) and two new experimental modes:
`Lax` and `Gnu` for different option parsing behaviors.
- `std/symlinks.expandSymlink` now supports Windows symlinks and junctions with
POSIX-like single-hop `readlink` semantics.
- `std/nre2` is added to replace deprecated NRE.
- `system.typeof` adds a new parameter `modifierMode` to specify how type modifiers are handled.

View File

@@ -11,17 +11,119 @@ when defined(nimPreviewSlimSystem):
when weirdTarget:
discard
elif defined(windows):
import std/[winlean, times]
import std/winlean
from std/strutils import toHex, toLowerAscii
const
reparseHeaderSize = 8
substituteNameOffsetField = 8
substituteNameLengthField = 10
symlinkFlagsField = 16
mountPointPathBufferOffset = 16
symlinkPathBufferOffset = 20
type
ReparseBuffer = array[MAXIMUM_REPARSE_DATA_BUFFER_SIZE, byte]
ReparseLinkInfo = object
tag: int32
flags: int32
pathBufOffset: int
flagsField: int
substituteNameOffset: int
substituteNameLength: int
template readU16(buf: ReparseBuffer; off: int): uint16 =
uint16(buf[off]) or (uint16(buf[off + 1]) shl 8)
template readI32(buf: ReparseBuffer; off: int): int32 =
cast[int32](
uint32(buf[off]) or (uint32(buf[off + 1]) shl 8) or
(uint32(buf[off + 2]) shl 16) or (uint32(buf[off + 3]) shl 24))
func startsWithAsciiIgnoreCase(wide: openArray[Utf16Char]; prefix: openArray[char]): bool =
## Matches an ASCII prefix against UTF-16 code units.
##
## This is only correct for ASCII prefixes.
## It is not a valid general case-insensitive Unicode comparison
## and must not be used for arbitrary UTF-16 text.
if prefix.len > wide.len:
return false
var i = 0
while i < prefix.len:
let rune = ord(wide[i])
if rune > 0x7F or toLowerAscii(char(rune)) != toLowerAscii(prefix[i]):
return false
inc i
true
proc decodeWinTarget(wide: openArray[Utf16Char]): string =
if wide.startsWithAsciiIgnoreCase(r"\??\unc\"):
r"\\" & $(wide.toOpenArray(8, wide.len - 1))
elif wide.startsWithAsciiIgnoreCase(r"\??\"):
$(wide.toOpenArray(4, wide.len - 1))
else:
$wide
template invalidReparseData(path, details: string) =
raise newException(OSError,
"expandSymlink: invalid reparse data for " & path & " (" & details & ")")
proc parseReparseLinkInfo(buf: ReparseBuffer; bytesReturned: int;
symlinkPath: string): ReparseLinkInfo =
if bytesReturned < reparseHeaderSize:
invalidReparseData(symlinkPath, "truncated header")
let
reparseDataLen = int(readU16(buf, 4))
wholeDataLen = reparseHeaderSize + reparseDataLen
if wholeDataLen > bytesReturned:
invalidReparseData(symlinkPath, "payload exceeds returned size")
result.tag = readI32(buf, 0)
case result.tag
of IO_REPARSE_TAG_SYMLINK:
result.pathBufOffset = symlinkPathBufferOffset
result.flagsField = symlinkFlagsField
of IO_REPARSE_TAG_MOUNT_POINT:
result.pathBufOffset = mountPointPathBufferOffset
result.flagsField = -1
else:
raise newException(OSError,
"expandSymlink: unsupported reparse tag for " & symlinkPath &
" (ReparseTag=0x" & toHex(result.tag) & ")")
if result.pathBufOffset > wholeDataLen:
invalidReparseData(symlinkPath, "missing path buffer")
result.substituteNameOffset = int(readU16(buf, substituteNameOffsetField))
result.substituteNameLength = int(readU16(buf, substituteNameLengthField))
if result.substituteNameLength <= 0:
invalidReparseData(symlinkPath, "empty substitute name")
if (result.substituteNameOffset and 1) != 0 or
(result.substituteNameLength and 1) != 0:
invalidReparseData(symlinkPath, "unaligned UTF-16 substitute name")
let startByte = result.pathBufOffset + result.substituteNameOffset
let endByte = startByte + result.substituteNameLength
if startByte < result.pathBufOffset or endByte < startByte or
endByte > wholeDataLen:
invalidReparseData(symlinkPath, "substitute name out of bounds")
result.flags =
if result.flagsField >= 0:
readI32(buf, result.flagsField)
else:
0
elif defined(posix):
import std/posix
when weirdTarget:
{.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
else:
{.pragma: noWeirdTarget.}
when defined(nimscript):
# for procs already defined in scriptconfig.nim
template noNimJs(body): untyped = discard
@@ -56,13 +158,65 @@ proc createSymlink*(src, dest: string) {.noWeirdTarget.} =
raiseOSError(osLastError(), $(src, dest))
proc expandSymlink*(symlinkPath: string): string {.noWeirdTarget.} =
## Returns a string representing the path to which the symbolic link points.
## Returns the stored target of the symbolic link `symlinkPath`.
##
## On Windows this is a noop, `symlinkPath` is simply returned.
## This expands exactly one level of indirection, like POSIX `readlink`.
## If the target is itself a symbolic link, it is returned as-is rather than
## being expanded further.
##
## On POSIX, raises `OSError` if `symlinkPath` is not a symbolic link or if
## the target cannot be read.
##
## On Windows, this supports symbolic links and junctions by reading the
## reparse point payload directly. Unsupported reparse tags raise `OSError`.
##
## On Nintendo Switch this is currently a noop: `symlinkPath` is simply
## returned, without checking whether it is actually a symbolic link.
##
## See also:
## * `createSymlink proc`_
when defined(windows) or defined(nintendoswitch):
when defined(windows):
let handle = createFileW(
newWideCString(symlinkPath),
0'i32,
FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
nil,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT or FILE_FLAG_BACKUP_SEMANTICS,
Handle(0)
)
if handle == INVALID_HANDLE_VALUE:
raiseOSError(osLastError(), "expandSymlink: cannot open " & symlinkPath)
defer:
discard closeHandle(handle)
var buf: ReparseBuffer
var bytesReturned: DWORD
if deviceIoControl(
handle,
FSCTL_GET_REPARSE_POINT,
nil, 0'i32,
addr buf[0], DWORD(buf.len),
bytesReturned,
nil
) == 0:
raiseOSError(osLastError(),
"expandSymlink: DeviceIoControl failed for " & symlinkPath)
let
info = parseReparseLinkInfo(buf, int(bytesReturned), symlinkPath)
startByte = info.pathBufOffset + info.substituteNameOffset
runeLen = info.substituteNameLength shr 1
wideSlicePtr = cast[ptr UncheckedArray[Utf16Char]](addr buf[startByte])
if info.tag == IO_REPARSE_TAG_SYMLINK and
(info.flags and SYMLINK_FLAG_RELATIVE) != 0:
return $(wideSlicePtr.toOpenArray(0, runeLen - 1))
decodeWinTarget(wideSlicePtr.toOpenArray(0, runeLen - 1))
elif defined(nintendoswitch):
result = symlinkPath
else:
var bufLen = 1024

View File

@@ -24,9 +24,20 @@ proc createSymlink*(src, dest: Path) {.inline.} =
createSymlink(src.string, dest.string)
proc expandSymlink*(symlinkPath: Path): Path {.inline.} =
## Returns a string representing the path to which the symbolic link points.
## Returns the stored target of the symbolic link `symlinkPath`.
##
## On Windows this is a noop, `symlinkPath` is simply returned.
## This expands exactly one level of indirection, like POSIX `readlink`.
## If the target is itself a symbolic link, it is returned as-is rather than
## being expanded further.
##
## On POSIX, raises `OSError` if `symlinkPath` is not a symbolic link or if
## the target cannot be read.
##
## On Windows, this supports symbolic links and junctions by reading the
## reparse point payload directly. Unsupported reparse tags raise `OSError`.
##
## On Nintendo Switch this is currently a noop: `symlinkPath` is simply
## returned, without checking whether it is actually a symbolic link.
##
## See also:
## * `createSymlink proc`_

View File

@@ -185,47 +185,88 @@ when not (defined(cpu16) or defined(cpu8)):
proc newWideCString*(s: string): WideCStringObj =
result = newWideCString(cstring s, s.len)
proc `$`*(w: WideCString, estimate: int, replacement: int = 0xFFFD): string =
result = newStringOfCap(estimate + estimate shr 2)
iterator decodeUtf16(w: WideCString; replacement: int): int =
## Looks for a terminating NUL for length
var i = 0
while w[i].int16 != 0'i16:
var ch = ord(w[i])
inc i
if ch >= UNI_SUR_HIGH_START and ch <= UNI_SUR_HIGH_END:
# If the 16 bits following the high surrogate are in the source buffer...
let ch2 = ord(w[i])
# If it's a low surrogate, convert to UTF32:
if ch2 >= UNI_SUR_LOW_START and ch2 <= UNI_SUR_LOW_END:
ch = (((ch and halfMask) shl halfShift) + (ch2 and halfMask)) + halfBase
inc i
# If the 16 bits following the high surrogate are NOT in the source...
if w[i].int16 == 0'i16:
ch = replacement #invalid UTF-16
else:
#invalid UTF-16
ch = replacement
let ch2 = ord(w[i])
# If it's a low surrogate, convert to UTF32:
if ch2 >= UNI_SUR_LOW_START and ch2 <= UNI_SUR_LOW_END:
ch = (((ch and halfMask) shl halfShift) + (ch2 and halfMask)) + halfBase
inc i
else:
ch = replacement #invalid UTF-16
elif ch >= UNI_SUR_LOW_START and ch <= UNI_SUR_LOW_END:
#invalid UTF-16
ch = replacement
ch = replacement #invalid UTF-16
yield ch
if ch < 0x80:
result.add chr(ch)
elif ch < 0x800:
result.add chr((ch shr 6) or 0xc0)
result.add chr((ch and 0x3f) or 0x80)
elif ch < 0x10000:
result.add chr((ch shr 12) or 0xe0)
result.add chr(((ch shr 6) and 0x3f) or 0x80)
result.add chr((ch and 0x3f) or 0x80)
elif ch <= 0x10FFFF:
result.add chr((ch shr 18) or 0xf0)
result.add chr(((ch shr 12) and 0x3f) or 0x80)
result.add chr(((ch shr 6) and 0x3f) or 0x80)
result.add chr((ch and 0x3f) or 0x80)
else:
# replacement char(in case user give very large number):
result.add chr(0xFFFD shr 12 or 0b1110_0000)
result.add chr(0xFFFD shr 6 and ones(6) or 0b10_0000_00)
result.add chr(0xFFFD and ones(6) or 0b10_0000_00)
iterator decodeUtf16(w: openArray[Utf16Char]; replacement: int): int =
## Doesn't look for terminating NUL for length, trusts `w.len`
var i = 0
while i < w.len:
var ch = ord(w[i])
inc i
if ch >= UNI_SUR_HIGH_START and ch <= UNI_SUR_HIGH_END:
# If the 16 bits following the high surrogate are NOT in the source...
if i >= w.len:
ch = replacement #invalid UTF-16
else:
let ch2 = ord(w[i])
# If it's a low surrogate, convert to UTF32:
if ch2 >= UNI_SUR_LOW_START and ch2 <= UNI_SUR_LOW_END:
ch = (((ch and halfMask) shl halfShift) + (ch2 and halfMask)) + halfBase
inc i
else:
ch = replacement #invalid UTF-16
elif ch >= UNI_SUR_LOW_START and ch <= UNI_SUR_LOW_END:
ch = replacement #invalid UTF-16
yield ch
proc addUtf8(dest: var string; rune: int) =
if rune < 0x80:
dest.add chr(rune)
elif rune < 0x800:
dest.add chr((rune shr 6) or 0xc0)
dest.add chr((rune and 0x3f) or 0x80)
elif rune < 0x10000:
dest.add chr((rune shr 12) or 0xe0)
dest.add chr(((rune shr 6) and 0x3f) or 0x80)
dest.add chr((rune and 0x3f) or 0x80)
elif rune <= 0x10FFFF:
dest.add chr((rune shr 18) or 0xf0)
dest.add chr(((rune shr 12) and 0x3f) or 0x80)
dest.add chr(((rune shr 6) and 0x3f) or 0x80)
dest.add chr((rune and 0x3f) or 0x80)
else:
# replacement char (in case user give very large number):
dest.add chr(0xFFFD shr 12 or 0b1110_0000)
dest.add chr(0xFFFD shr 6 and ones(6) or 0b10_0000_00)
dest.add chr(0xFFFD and ones(6) or 0b10_0000_00)
proc `$`*(w: openArray[Utf16Char]; replacement: int = 0xFFFD): string =
## Decodes a length-delimited UTF-16 slice to UTF-8.
##
## Unlike the `WideCString` overloads, this preserves the provided length
## and does not search for a terminating NUL.
if w.len == 0:
result = ""
else:
result = newStringOfCap(w.len + w.len shr 2)
for rune in w.decodeUtf16(replacement):
result.addUtf8(rune)
proc `$`*(w: WideCString; estimate: int; replacement: int = 0xFFFD): string =
result = newStringOfCap(estimate + estimate shr 2)
for rune in w.decodeUtf16(replacement):
result.addUtf8(rune)
proc `$`*(s: WideCString): string =
result = s $ 80

View File

@@ -261,6 +261,11 @@ const
FILE_ATTRIBUTE_OFFLINE* = 0x00001000'i32
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED* = 0x00002000'i32
IO_REPARSE_TAG_MOUNT_POINT* = 0xA0000003'i32
IO_REPARSE_TAG_SYMLINK* = 0xA000000C'i32
MAXIMUM_REPARSE_DATA_BUFFER_SIZE* = 16 * 1024
SYMLINK_FLAG_RELATIVE* = 0x1'i32
FILE_FLAG_FIRST_PIPE_INSTANCE* = 0x00080000'i32
FILE_FLAG_OPEN_NO_RECALL* = 0x00100000'i32
FILE_FLAG_OPEN_REPARSE_POINT* = 0x00200000'i32
@@ -282,6 +287,10 @@ const
MOVEFILE_REPLACE_EXISTING* = 0x1'i32
MOVEFILE_WRITE_THROUGH* = 0x8'i32
# CTL_CODE(FILE_DEVICE_FILE_SYSTEM = 9, func = 42, METHOD_BUFFERED = 0,
# FILE_ANY_ACCESS = 0)
FSCTL_GET_REPARSE_POINT* = 0x000900A8'i32
type
WIN32_FIND_DATA* {.pure.} = object
dwFileAttributes*: int32
@@ -654,6 +663,12 @@ proc createFileW*(lpFileName: WideCString, dwDesiredAccess, dwShareMode: DWORD,
dwCreationDisposition, dwFlagsAndAttributes: DWORD,
hTemplateFile: Handle): Handle {.
stdcall, dynlib: "kernel32", importc: "CreateFileW".}
proc deviceIoControl*(hDevice: Handle, dwIoControlCode: DWORD,
lpInBuffer: pointer, nInBufferSize: DWORD,
lpOutBuffer: pointer, nOutBufferSize: DWORD,
lpBytesReturned: var DWORD,
lpOverlapped: pointer): WINBOOL {.
stdcall, dynlib: "kernel32", importc: "DeviceIoControl".}
proc deleteFileW*(pathName: WideCString): int32 {.
importc: "DeleteFileW", dynlib: "kernel32", stdcall.}
proc createFileA*(lpFileName: cstring, dwDesiredAccess, dwShareMode: DWORD,

View File

@@ -179,7 +179,7 @@ block fileOperations:
# Symlink handling in `copyFile`, `copyFileWithPermissions`, `copyFileToDir`,
# `copyDir`, `copyDirWithPermissions`, `moveFile`, and `moveDir`.
block:
const symlinksAreHandled = not defined(windows)
const symlinkCopiesAreHandled = not defined(windows)
const dname = buildDir/"D20210116T140629"
const subDir = dname/"sub"
const subDir2 = dname/"sub2"
@@ -189,98 +189,131 @@ block fileOperations:
const brokenSymlinkCopy = brokenSymlink & "_COPY"
const brokenSymlinkInSubDir = subDir/brokenSymlinkName
const brokenSymlinkInSubDir2 = subDir2/brokenSymlinkName
const symlinkProbeTarget = dname/"symlink_probe_target"
const symlinkProbeLink = dname/"symlink_probe_link"
createDir(subDir)
createSymlink(brokenSymlinkSrc, brokenSymlink)
proc removePathIfExists(path: string) =
if fileExists(path):
removeFile(path)
elif dirExists(path):
removeDir(path)
# Test copyFile
when symlinksAreHandled:
proc canCreateSymlinks(): bool =
# We need this check for Windows if we want to permit the block to run
# when we have admin privileges
try:
removePathIfExists(dname)
createDir(dname)
writeFile(symlinkProbeTarget, "")
createSymlink(symlinkProbeTarget, symlinkProbeLink)
result = true
except OSError:
result = false
finally:
removePathIfExists(symlinkProbeLink)
removePathIfExists(symlinkProbeTarget)
removePathIfExists(dname)
proc doAssertExpandedSymlink(path, expected: string) =
let actual = expandSymlink(path)
doAssert actual == expected,
"expandSymlink(" & path & ") returned " & actual &
" instead of " & expected
removePathIfExists(dname)
let symlinksAreAvailable = not defined(windows) or canCreateSymlinks()
if symlinksAreAvailable:
defer:
removePathIfExists(dname)
createDir(subDir)
createSymlink(brokenSymlinkSrc, brokenSymlink)
doAssertExpandedSymlink(brokenSymlink, brokenSymlinkSrc)
doAssertRaises(OSError):
copyFile(brokenSymlink, brokenSymlinkCopy)
doAssertRaises(OSError):
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkFollow})
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkCopy)
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkAsIs})
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkCopy) == brokenSymlinkSrc
removeFile(brokenSymlinkCopy)
else:
discard expandSymlink(dname)
# Test copyFile
when symlinkCopiesAreHandled:
doAssertRaises(OSError):
copyFile(brokenSymlink, brokenSymlinkCopy)
doAssertRaises(OSError):
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkFollow})
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkCopy)
doAssertRaises(AssertionDefect):
copyFile(brokenSymlink, brokenSymlinkCopy,
{cfSymlinkAsIs, cfSymlinkFollow})
copyFile(brokenSymlink, brokenSymlinkCopy, {cfSymlinkAsIs})
when symlinkCopiesAreHandled:
doAssertExpandedSymlink(brokenSymlinkCopy, brokenSymlinkSrc)
removeFile(brokenSymlinkCopy)
else:
doAssert not fileExists(brokenSymlinkCopy)
doAssertRaises(AssertionDefect):
copyFile(brokenSymlink, brokenSymlinkCopy,
{cfSymlinkAsIs, cfSymlinkFollow})
# Test copyFileWithPermissions
when symlinksAreHandled:
doAssertRaises(OSError):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy)
doAssertRaises(OSError):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkFollow})
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkCopy)
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkAsIs})
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkCopy) == brokenSymlinkSrc
removeFile(brokenSymlinkCopy)
else:
doAssert not fileExists(brokenSymlinkCopy)
doAssertRaises(AssertionDefect):
# Test copyFileWithPermissions
when symlinkCopiesAreHandled:
doAssertRaises(OSError):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy)
doAssertRaises(OSError):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkFollow})
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkAsIs, cfSymlinkFollow})
options = {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkCopy)
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkAsIs})
when symlinkCopiesAreHandled:
doAssertExpandedSymlink(brokenSymlinkCopy, brokenSymlinkSrc)
removeFile(brokenSymlinkCopy)
else:
doAssert not fileExists(brokenSymlinkCopy)
doAssertRaises(AssertionDefect):
copyFileWithPermissions(brokenSymlink, brokenSymlinkCopy,
options = {cfSymlinkAsIs, cfSymlinkFollow})
# Test copyFileToDir
when symlinksAreHandled:
doAssertRaises(OSError):
copyFileToDir(brokenSymlink, subDir)
doAssertRaises(OSError):
copyFileToDir(brokenSymlink, subDir, {cfSymlinkFollow})
copyFileToDir(brokenSymlink, subDir, {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkInSubDir)
copyFileToDir(brokenSymlink, subDir, {cfSymlinkAsIs})
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkInSubDir) == brokenSymlinkSrc
removeFile(brokenSymlinkInSubDir)
else:
# Test copyFileToDir
when symlinkCopiesAreHandled:
doAssertRaises(OSError):
copyFileToDir(brokenSymlink, subDir)
doAssertRaises(OSError):
copyFileToDir(brokenSymlink, subDir, {cfSymlinkFollow})
copyFileToDir(brokenSymlink, subDir, {cfSymlinkIgnore})
doAssert not fileExists(brokenSymlinkInSubDir)
copyFileToDir(brokenSymlink, subDir, {cfSymlinkAsIs})
when symlinkCopiesAreHandled:
doAssertExpandedSymlink(brokenSymlinkInSubDir, brokenSymlinkSrc)
removeFile(brokenSymlinkInSubDir)
else:
doAssert not fileExists(brokenSymlinkInSubDir)
createSymlink(brokenSymlinkSrc, brokenSymlinkInSubDir)
createSymlink(brokenSymlinkSrc, brokenSymlinkInSubDir)
# Test copyDir
copyDir(subDir, subDir2)
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkInSubDir2) == brokenSymlinkSrc
# Test copyDir
copyDir(subDir, subDir2)
when symlinkCopiesAreHandled:
doAssertExpandedSymlink(brokenSymlinkInSubDir2, brokenSymlinkSrc)
else:
doAssert not fileExists(brokenSymlinkInSubDir2)
removeDir(subDir2)
# Test copyDirWithPermissions
copyDirWithPermissions(subDir, subDir2)
when symlinkCopiesAreHandled:
doAssertExpandedSymlink(brokenSymlinkInSubDir2, brokenSymlinkSrc)
else:
doAssert not fileExists(brokenSymlinkInSubDir2)
removeDir(subDir2)
# Test moveFile
moveFile(brokenSymlink, brokenSymlinkCopy)
doAssertExpandedSymlink(brokenSymlinkCopy, brokenSymlinkSrc)
removeFile(brokenSymlinkCopy)
# Test moveDir
moveDir(subDir, subDir2)
doAssertExpandedSymlink(brokenSymlinkInSubDir2, brokenSymlinkSrc)
else:
doAssert not fileExists(brokenSymlinkInSubDir2)
removeDir(subDir2)
# Test copyDirWithPermissions
copyDirWithPermissions(subDir, subDir2)
when symlinksAreHandled:
doAssert expandSymlink(brokenSymlinkInSubDir2) == brokenSymlinkSrc
else:
doAssert not fileExists(brokenSymlinkInSubDir2)
removeDir(subDir2)
# Test moveFile
moveFile(brokenSymlink, brokenSymlinkCopy)
when not defined(windows):
doAssert expandSymlink(brokenSymlinkCopy) == brokenSymlinkSrc
else:
doAssert symlinkExists(brokenSymlinkCopy)
removeFile(brokenSymlinkCopy)
# Test moveDir
moveDir(subDir, subDir2)
when not defined(windows):
doAssert expandSymlink(brokenSymlinkInSubDir2) == brokenSymlinkSrc
else:
doAssert symlinkExists(brokenSymlinkInSubDir2)
removeDir(dname)
discard "Skipping symlink tests: symlink creation is not permitted in this environment"
block: # moveFile
let tempDir = getTempDir() / "D20210609T151608"