From 00d8f6631192da6b5aebf3c6fd55f7c582471ca3 Mon Sep 17 00:00:00 2001 From: Zoom Date: Mon, 29 Jun 2026 20:25:41 +0400 Subject: [PATCH] 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 --- changelog.md | 2 + lib/std/private/ossymlinks.nim | 166 ++++++++++++++++++++++++++- lib/std/symlinks.nim | 15 ++- lib/std/widestrs.nim | 107 ++++++++++++------ lib/windows/winlean.nim | 15 +++ tests/stdlib/tos.nim | 197 +++++++++++++++++++-------------- 6 files changed, 379 insertions(+), 123 deletions(-) diff --git a/changelog.md b/changelog.md index 5f1ff21778..172769b820 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/lib/std/private/ossymlinks.nim b/lib/std/private/ossymlinks.nim index 9e915a1e4d..7277b8398c 100644 --- a/lib/std/private/ossymlinks.nim +++ b/lib/std/private/ossymlinks.nim @@ -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 diff --git a/lib/std/symlinks.nim b/lib/std/symlinks.nim index dbe9086121..ef5d848487 100644 --- a/lib/std/symlinks.nim +++ b/lib/std/symlinks.nim @@ -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`_ diff --git a/lib/std/widestrs.nim b/lib/std/widestrs.nim index 7cc905cb49..af9be00108 100644 --- a/lib/std/widestrs.nim +++ b/lib/std/widestrs.nim @@ -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 diff --git a/lib/windows/winlean.nim b/lib/windows/winlean.nim index 39ee582ee4..2704a6111e 100644 --- a/lib/windows/winlean.nim +++ b/lib/windows/winlean.nim @@ -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, diff --git a/tests/stdlib/tos.nim b/tests/stdlib/tos.nim index 3b77ec3c0a..be6c096260 100644 --- a/tests/stdlib/tos.nim +++ b/tests/stdlib/tos.nim @@ -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"