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"