diff --git a/core/src/main/csharp/NativeMethods.txt b/core/src/main/csharp/NativeMethods.txt
index d2376e6eb46..400488b6f16 100644
--- a/core/src/main/csharp/NativeMethods.txt
+++ b/core/src/main/csharp/NativeMethods.txt
@@ -34,10 +34,7 @@ LoadLibrary
LoadString
MESSAGEBOX_RESULT
OpenFileById
-PATHCCH_ALLOW_LONG_PATHS
PATHCCH_MAX_CCH
-PathCchCanonicalizeEx
-PathCchStripPrefix
PathParseIconLocation
PBST_ERROR
PBST_NORMAL
diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs
index 3aa55ee8b8c..8140648e32a 100644
--- a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs
+++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs
@@ -105,18 +105,6 @@ public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Spa
}
}
- ///
- public static unsafe HRESULT PathCchCanonicalizeEx(ref Span pszPathOut, string pszPathIn, PATHCCH_OPTIONS dwFlags)
- {
- fixed (char* ppszPathOut = pszPathOut)
- {
- PWSTR wstrpszPathOut = ppszPathOut;
- HRESULT __result = CorePInvoke.PathCchCanonicalizeEx(wstrpszPathOut, (nuint)pszPathOut.Length, pszPathIn, dwFlags);
- pszPathOut = pszPathOut.Slice(0, wstrpszPathOut.Length);
- return __result;
- }
- }
-
///
public static unsafe HRESULT SHCreateAssociationRegistration(out T ppv) where T : class
{
diff --git a/core/src/main/csharp/ch/cyberduck/core/LocalExtensions.cs b/core/src/main/csharp/ch/cyberduck/core/LocalExtensions.cs
new file mode 100644
index 00000000000..3ab75d3d706
--- /dev/null
+++ b/core/src/main/csharp/ch/cyberduck/core/LocalExtensions.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using Ch.Cyberduck.Core.Local;
+using CoreLocal = ch.cyberduck.core.Local;
+
+namespace Ch.Cyberduck.Core;
+
+public static class LocalExtensions
+{
+ ///
+ public static string NativePath(this CoreLocal local) => SystemLocal.ToNativePath(local);
+
+ ///
+ public static string PlatformPath(this CoreLocal local) => SystemLocal.ToPlatformPath(local);
+}
diff --git a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs
index 37fb4c5a73a..a2db2df232e 100644
--- a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs
+++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs
@@ -36,22 +36,9 @@ public class NTFSFilesystemBookmarkResolver(CoreLocal local) : FilesystemBookmar
public string create(CoreLocal file, bool prompt)
{
- Span finalNameBuffer = new char[CorePInvoke.PATHCCH_MAX_CCH];
- if (CorePInvoke.PathCchCanonicalizeEx(
- ref finalNameBuffer,
- NetPath.GetFullPath(file.getAbsolute()),
- PATHCCH_OPTIONS.PATHCCH_ALLOW_LONG_PATHS | PATHCCH_OPTIONS.PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS) is
- {
- Failed: true,
- Value: { } error
- })
- {
- goto error;
- }
-
FILE_ID_INFO info;
using (var handle = CorePInvoke.CreateFile(
- lpFileName: finalNameBuffer,
+ lpFileName: file.NativePath(),
dwDesiredAccess: 0,
dwShareMode: (FILE_SHARE_MODE)7,
lpSecurityAttributes: null,
@@ -139,28 +126,7 @@ public object resolve(string bookmark)
throw new LocalAccessDeniedException(bookmark);
}
- /*
- * OpenJDK 8 and .NET 8 are implicitely long-path aware,
- * thus we don't need to carry the long path-prefix,
- * which for OpenJDK means long-path prefixed paths fail.
- */
- if (CorePInvoke.PathCchStripPrefix(ref finalNameBuffer, length) is
- {
- Failed: true, /* PathCchStripPrefix is Success (S_OK (0), S_FALSE(1)) or Failed (HRESULT, <0) */
- Value: { } stripPrefixError
- })
- {
- var errorCode = Marshal.GetHRForLastWin32Error();
- Log.warn(
-#if NETCOREAPP
- $"Path Strip Prefix \"{finalNameBuffer}\" ({errorCode:X8})");
-#else
- $"Path Strip Prefix \"{finalNameBuffer.ToString()}\" ({errorCode:X8})");
-#endif
- throw new LocalAccessDeniedException(bookmark);
- }
-
- return LocalFactory.get(finalNameBuffer.ToString()).setBookmark(bookmark);
+ return new SystemLocal(finalNameBuffer.Slice(0, (int)length).ToString()).setBookmark(bookmark);
}
finally
{
@@ -191,7 +157,7 @@ private static bool TryFindRoot(CoreLocal local, out SafeFileHandle handle)
try
{
result = CorePInvoke.CreateFile(
- lpFileName: local.getAbsolute(),
+ lpFileName: local.NativePath(),
dwDesiredAccess: 0,
dwShareMode: (FILE_SHARE_MODE)7,
lpSecurityAttributes: null, dwCreationDisposition: FILE_CREATION_DISPOSITION.OPEN_EXISTING,
diff --git a/core/src/main/csharp/ch/cyberduck/core/local/RecycleLocalTrashFeature.cs b/core/src/main/csharp/ch/cyberduck/core/local/RecycleLocalTrashFeature.cs
index 0ab3aae4612..e4faa42d4d8 100644
--- a/core/src/main/csharp/ch/cyberduck/core/local/RecycleLocalTrashFeature.cs
+++ b/core/src/main/csharp/ch/cyberduck/core/local/RecycleLocalTrashFeature.cs
@@ -31,11 +31,11 @@ public void trash(ch.cyberduck.core.Local file)
try {
if (file.isFile())
{
- FileSystem.DeleteFile(file.getAbsolute(), UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
+ FileSystem.DeleteFile(file.PlatformPath(), UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
}
else if (file.isDirectory())
{
- FileSystem.DeleteDirectory(file.getAbsolute(), UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
+ FileSystem.DeleteDirectory(file.PlatformPath(), UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
}
}
catch(System.Exception e) {
diff --git a/core/src/main/csharp/ch/cyberduck/core/local/ShellApplicationFinder.cs b/core/src/main/csharp/ch/cyberduck/core/local/ShellApplicationFinder.cs
index f95fa0ca23c..d0e39cb49c4 100644
--- a/core/src/main/csharp/ch/cyberduck/core/local/ShellApplicationFinder.cs
+++ b/core/src/main/csharp/ch/cyberduck/core/local/ShellApplicationFinder.cs
@@ -249,7 +249,7 @@ public unsafe void Launch(ch.cyberduck.core.Local local)
lpClass = PCWSTR.DangerousFromString(getIdentifier()),
fMask = SEE_MASK_CLASSNAME | SEE_MASK_NOASYNC,
lpVerb = PCWSTR.DangerousFromString("open"),
- lpFile = PCWSTR.DangerousFromString(local.getAbsolute())
+ lpFile = PCWSTR.DangerousFromString(local.NativePath())
};
ShellExecuteEx(ref info);
}
@@ -284,7 +284,7 @@ public void Launch(ch.cyberduck.core.Local local)
return;
}
- using var pidl = ILCreateFromPathSafe(local.getAbsolute());
+ using var pidl = ILCreateFromPathSafe(local.NativePath());
if (pidl.IsInvalid)
{
return;
@@ -331,7 +331,7 @@ public void Launch(ch.cyberduck.core.Local local)
OPENASINFO info = new()
{
oaifInFlags = OAIF_EXEC,
- pcszFile = PCWSTR.DangerousFromString(local.getAbsolute())
+ pcszFile = PCWSTR.DangerousFromString(local.NativePath())
};
SHOpenWithDialog(default, info);
}
diff --git a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs
index f7ba7d1c8f2..bf6ca7d3870 100644
--- a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs
+++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs
@@ -12,7 +12,9 @@
// GNU General Public License for more details.
using System;
+using System.ComponentModel;
using System.IO;
+using System.Runtime.CompilerServices;
using ch.cyberduck.core;
using ch.cyberduck.core.exception;
using java.io;
@@ -28,12 +30,13 @@ public class SystemLocal : CoreLocal
{
private static readonly Logger Log = LogManager.getLogger(typeof(SystemLocal).FullName);
+ private const string UncPathPrefix = @"\\?\";
private static readonly char[] INVALID_CHARS = Path.GetInvalidFileNameChars();
private static readonly char[] PATH_SEPARATORS = new[]
{ Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar };
public SystemLocal(string parent, string name)
- : this(Join(parent, Sanitize(name, true)))
+ : this(Join(parent, name))
{
}
@@ -43,7 +46,7 @@ public SystemLocal(CoreLocal parent, string name)
}
public SystemLocal(string path)
- : base(Sanitize(path))
+ : base(Canonicalize(path))
{
}
@@ -52,6 +55,62 @@ public SystemLocal(SystemLocal copy)
{
}
+ ///
+ /// Returns a path usable for Win32 IO, e.g. CreateFile.
+ ///
+ /// A path, optionally prefixed with \\?\ when needed.
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public static string ToNativePath(CoreLocal local)
+ {
+ // TODO There is RtlAreLongPathsEnabled, but this is undocumented.
+ // Manifest | LongPathsEnable | RtlAreLongPathsEnabled
+ // True | False | False
+ // False | False | False
+ // False | True | False
+ // True | True | True
+ // Just prefix every long path.
+
+ var path = local.getAbsolute();
+ bool unc = path.Length > 2
+ && IsDirectorySeparator(path[0])
+ && IsDirectorySeparator(path[1]);
+ bool extended = unc && path.Length > 3
+ && path[2] is '?' or '.';
+
+ if (!extended && path.Length > 248 /*MaxShortDirectoryName*/)
+ {
+ if (unc)
+ {
+ // When storing to a UNC drive, make \\?\UNC\X from \\X
+ return path.Insert(2, @"?\UNC\");
+ }
+ else
+ {
+ // Create \\?\X from X
+ return UncPathPrefix + path;
+ }
+ }
+
+ return path;
+ }
+
+ ///
+ /// Returns a path that is usable for .NET IO.
+ ///
+ /// A path, optionally prefixed with \\?\ when needed.
+ [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
+ public static string ToPlatformPath(CoreLocal local)
+ {
+#if NETCOREAPP
+ // .NET Core automatically handles long paths.
+ return local.getAbsolute();
+#else
+ // .NET Framework doesn't automatically prefix long paths.
+ // Assume same semantics as Win32 paths.
+ return ToNativePath(local);
+#endif
+ }
+
public CoreLocal Resolve()
{
if (null == bookmark)
@@ -89,18 +148,21 @@ public override bool exists()
{
var resolved = Resolve();
string path = resolved.getAbsolute();
+#if NETCOREAPP
+ return Path.Exists(path);
+#else
if (File.Exists(path))
{
return true;
}
- bool directory = Directory.Exists(path);
- if (directory)
+ if (Directory.Exists(path))
{
return true;
}
return false;
+#endif
}
public override LocalAttributes attributes()
@@ -127,125 +189,143 @@ public override bool isSymbolicLink()
return false;
}
- private static string Join(string root, string path)
- {
- // Path.Join doesn't exist in .NET Framework, need to replicate
- bool hasDirectorySeparator = IsDirectorySeparator(root[root.Length - 1]) || IsDirectorySeparator(path[0]);
- return hasDirectorySeparator
- ? string.Concat(root, path)
- : string.Concat(root, Path.DirectorySeparatorChar, path);
-
- static bool IsDirectorySeparator(char sep) =>
- sep == Path.DirectorySeparatorChar || sep == Path.AltDirectorySeparatorChar;
- }
-
- private static string Sanitize(string name, bool makeUnc = false)
+ private static string Canonicalize(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return "";
}
+ int start = 0;
+ if (name[0] is '/')
+ {
+ // LocalFactory.get(Path.getAbsolute()) always retains '/' at the beginning.
+ // Need a Path-Local translation, which removes this.
+ // Adjust offset.
+ start = 1;
+ }
+
+ var pathRoot =
+#if NETCOREAPP
+ PathRoot(name.AsSpan(start));
+#else
+ PathRoot(name.Substring(start));
+#endif
+
+ bool deviceSyntax = pathRoot.Length > 3
+ && pathRoot[2] is '?' or '.'
+ && IsDirectorySeparator(pathRoot[0])
+ && IsDirectorySeparator(pathRoot[1])
+ && IsDirectorySeparator(pathRoot[3]);
+ bool deviceUnc = deviceSyntax && pathRoot.Length > 7
+ && pathRoot[4] is 'U'
+ && pathRoot[5] is 'N'
+ && pathRoot[6] is 'C'
+ && IsDirectorySeparator(pathRoot[7]);
+
using StringWriter writer = new();
+ var buffer = writer.GetStringBuilder();
- var namespan = name.AsSpan();
- int? leadingSeparators = 0;
- bool hasUnc = false, nextDriveLetter = true;
- for (int lastSegment = 0, index = 0; index != -1; lastSegment = index + 1)
+ if (deviceUnc)
{
- index = name.IndexOfAny(PATH_SEPARATORS, lastSegment);
- ReadOnlySpan segment = (index switch
- {
- -1 => namespan.Slice(lastSegment),
- _ => namespan.Slice(lastSegment, index - lastSegment)
- }).Trim();
+ // Is \\?\UNC\X or \\.\UNC\X, write \\X
+ buffer.EnsureCapacity(pathRoot.Length - 5);
+ writer.Write(Path.DirectorySeparatorChar);
+ // Include separator after UNC.
+ WriteNormalized(pathRoot.Slice(7), writer);
+ }
+ else if (deviceSyntax && pathRoot[2] is '?')
+ {
+ // Only if we've got a real long-path (\\?\)
+ // do we remove it.
+ WriteNormalized(pathRoot.Slice(4), writer);
+ }
+ else
+ {
+ // Keep \\.\-prefixes (for e.g. Pipes),
+ // and don't bother working out how `\\SHARE`-paths work.
+ WriteNormalized(pathRoot, writer);
+ }
- if (segment.IsEmpty && leadingSeparators is int lead)
+ start += pathRoot.Length;
+
+ // TODO: Replace following when we have ch.cyberduck.core.Path to ch.cyberduck.core.Local translation
+ // e.g. on Windows when converting a Path to Local the leading slash has to be stripped, this
+ // translation would also be responsible for cleaning up bad file and path names (blocked chars).
+ var path = name.AsSpan(start);
+ var skipped = pathRoot.Length > 0 && IsDirectorySeparator(pathRoot[pathRoot.Length - 1]) ? 1 : 0;
+ for (int lastSegment = 0, index = 0; index != -1; lastSegment += index + 1)
+ {
+ var segment = path.Slice(lastSegment);
+ if ((index = segment.IndexOfAny(PATH_SEPARATORS)) != -1)
{
- // handles up to first two leading separators, that is "\" and "\\"
- leadingSeparators = ++lead;
- if (lead == 2)
- {
- // in the case this is "\\" continue with assuming UNC
- // thus a drive letter _must not_ follow
- hasUnc = true;
- nextDriveLetter = false;
- leadingSeparators = null;
- writer.Write(Path.DirectorySeparatorChar);
- writer.Write(Path.DirectorySeparatorChar);
- }
- // hereafter (after "\\" has been read) every empty segment is skipped "\\\" -> "\"
- // or after anything except "\\" has been read, every empty segment is skipped, "\\" -> "\"
+ segment = segment.Slice(0, index);
}
- else if (!segment.IsEmpty)
+
+ if (skipped++ == 0)
{
- var firstChanceDriveLetter = nextDriveLetter;
- nextDriveLetter = false;
- if (hasUnc)
- {
- // ignore UNC, whatever is in as first value segment is passed-through as is
- // there is no need to validate hostnames here, would bail out somewhere else
- // handles all cases of "\\*\"
- // including, but not limited to: wsl$, wsl.localhost, \\?\ (MAX_PATH bypass), any network share
- writer.Write(segment);
+ writer.Write(Path.DirectorySeparatorChar);
+ }
- nextDriveLetter = segment.Length == 1 && (segment[0] == '?' || segment[0] == '.');
- }
- else if (firstChanceDriveLetter && segment.Length == 2 && segment[1] == Path.VolumeSeparatorChar)
+ if (!segment.IsEmpty)
+ {
+ // pass through segment sanitized from path invalid characters
+ foreach (ref readonly var c in segment)
{
- // _only_ if there is a two-letter segment, that is ending in ':' (VolumeSeparatorChar)
- // is this thing here run.
- // If there is _anything_ wrong (that is not "[A-Z]:") return empty value
-
- ///
- var letter = (segment[0] | 0x20) - 'a';
- if (letter < 0 || letter > 25)
+ writer.Write(Array.IndexOf(INVALID_CHARS, c) switch
{
- // letter is not in range A to Z.
- return "";
- }
-
- // check above is simplified only, this passes raw input through
- // check is 'a' but segment is 'A:', then 'A:' is written to output
- writer.Write(segment[0]);
- writer.Write(makeUnc ? '$' : ':');
- // additionally, this strips away all leading separator characters before the drive letter
- // "/C:" becomes "C:".
+ -1 => c,
+ _ => '_'
+ });
}
- else
- {
- if (leadingSeparators > 0)
- {
- // workaround.
- // there may be input that is leading with one separator, but contains no
- writer.Write(Path.DirectorySeparatorChar);
- }
- // pass through segment sanitized from path invalid characters
- foreach (ref readonly var c in segment)
- {
- writer.Write(Array.IndexOf(INVALID_CHARS, c) switch
- {
- -1 => c,
- _ => '_'
- });
- }
- }
+ skipped = 0;
+ }
+ }
- hasUnc = false;
- leadingSeparators = null;
- if (index != -1)
+ return writer.ToString();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+#if NETCOREAPP
+ static ReadOnlySpan PathRoot(scoped in ReadOnlySpan path)
+ {
+ return Path.GetPathRoot(path);
+ }
+#else
+ static ReadOnlySpan PathRoot(string path)
+ {
+ return Path.GetPathRoot(path).AsSpan();
+ }
+#endif
+
+ static void WriteNormalized(in ReadOnlySpan path, StringWriter writer)
+ {
+ writer.GetStringBuilder().EnsureCapacity(path.Length);
+ foreach (ref readonly var c in path)
+ {
+ if (c == Path.AltDirectorySeparatorChar)
{
- // allow for input of "C:\Abc" and "C:\Abc\", preserve trailing separators, where
- // (1) return "C:\Abc"
- // (2) return "C:\Abc\"
writer.Write(Path.DirectorySeparatorChar);
}
+ else
+ {
+ writer.Write(c);
+ }
}
}
+ }
- return writer.ToString();
+ private static bool IsDirectorySeparator(char sep) =>
+ sep == Path.DirectorySeparatorChar || sep == Path.AltDirectorySeparatorChar;
+
+ private static string Join(string root, string path)
+ {
+ // Path.Join doesn't exist in .NET Framework, need to replicate
+ bool hasDirectorySeparator = IsDirectorySeparator(root[root.Length - 1]) || IsDirectorySeparator(path[0]);
+ return hasDirectorySeparator
+ ? string.Concat(root, path)
+ : string.Concat(root, Path.DirectorySeparatorChar, path);
}
}
}
diff --git a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs
index 18558f24026..e0ea3e49275 100644
--- a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs
+++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs
@@ -36,7 +36,7 @@ public override long getSize()
var resolved = local.Resolve();
try
{
- return new FileInfo(resolved.getAbsolute()).Length;
+ return new FileInfo(resolved.PlatformPath()).Length;
}
catch (Exception e)
{
diff --git a/core/src/test/csharp/Cyberduck.Core.Test.csproj b/core/src/test/csharp/Cyberduck.Core.Test.csproj
index 3b01caa9fd3..59a5c5e1af7 100644
--- a/core/src/test/csharp/Cyberduck.Core.Test.csproj
+++ b/core/src/test/csharp/Cyberduck.Core.Test.csproj
@@ -18,6 +18,7 @@
+
diff --git a/core/src/test/csharp/NativeMethods.txt b/core/src/test/csharp/NativeMethods.txt
new file mode 100644
index 00000000000..147640d7b3f
--- /dev/null
+++ b/core/src/test/csharp/NativeMethods.txt
@@ -0,0 +1 @@
+PathCchCanonicalizeEx
diff --git a/core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs b/core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs
index 42fccc20225..4ef9fe0eee5 100644
--- a/core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs
+++ b/core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs
@@ -17,8 +17,8 @@ public void EnsureRoundtrip()
new DefaultLocalTouchFeature().touch(file);
NTFSFilesystemBookmarkResolver resolver = new(file);
var bookmark = resolver.create(file);
- Assert.That(bookmark, new NotConstraint(new NullConstraint()));
+ Assert.That(bookmark, Is.Not.Null);
CoreLocal resolved = (CoreLocal)resolver.resolve(bookmark);
- Assert.That(resolved, new EqualConstraint(file));
+ Assert.That(resolved, Is.EqualTo(file));
}
}
diff --git a/core/src/test/csharp/ch/cyberduck/core/local/SystemLocalTest.cs b/core/src/test/csharp/ch/cyberduck/core/local/SystemLocalTest.cs
index 5eb2e7be262..a86373e1283 100644
--- a/core/src/test/csharp/ch/cyberduck/core/local/SystemLocalTest.cs
+++ b/core/src/test/csharp/ch/cyberduck/core/local/SystemLocalTest.cs
@@ -1,7 +1,12 @@
-using ch.cyberduck.core;
+using System.Runtime.InteropServices;
+using System.Text;
+using ch.cyberduck.core;
using java.nio.file;
using java.util;
using NUnit.Framework;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.Shell;
using CorePath = ch.cyberduck.core.Path;
using Path = System.IO.Path;
@@ -38,14 +43,14 @@ public void PathsToFileGetUsableSpace()
}
[Test]
- public void TestBadDriveLetter([Values(@"#:\", @"\#:\", @"\\.\#:\")] string path)
+ public void TestBadDriveLetter([Values(@"#:\", @"\\.\#:\")] string path)
{
- // Sanitized empty
- Assert.That(new SystemLocal(path).getAbsolute(), Is.Empty);
+ // Bad paths are copied verbatim.
+ Assert.That(new SystemLocal(path).getAbsolute(), Is.EqualTo(path));
}
[Test, Sequential]
- public void TestPathSanitize(
+ public void TestPathCanonicalize(
[Values(
/* 00 */ "C:\\C:"
)] string path,
@@ -56,13 +61,6 @@ public void TestPathSanitize(
Assert.That(new SystemLocal(path).getAbsolute(), Is.EqualTo(expected));
}
- [Test]
- public void TestConvertToDirectorySeparator()
- {
- SystemLocal path = new(PIPE_NAME.Replace('\\', '/'));
- Assert.That(path.getAbsolute(), Is.EqualTo(PIPE_NAME));
- }
-
[Test]
public void TestDirectorySeparators([ValueSource(nameof(TestDirectorySeparatorsValues))] char sep)
{
@@ -77,29 +75,6 @@ public void TestEmptyPath()
Assert.That(new SystemLocal("").getAbsolute(), Is.Empty);
}
- ///
- ///
- ///
- [Test]
- public void TestFileFormats()
- {
- string[] filenames =
- [
- @"c:\temp\test-file.txt",
- @"\\127.0.0.1\c$\temp\test-file.txt",
- @"\\LOCALHOST\c$\temp\test-file.txt",
- @"\\.\c:\temp\test-file.txt",
- @"\\?\c:\temp\test-file.txt",
- @"\\.\UNC\LOCALHOST\c$\temp\test-file.txt",
- @"\\127.0.0.1\c$\temp\test-file.txt"
- ];
- foreach (var item in filenames)
- {
- // Local passes through invalid paths, and logs an error
- Assert.That(new SystemLocal(item).getAbsolute(), Is.EqualTo(item));
- }
- }
-
[Test]
public void TestPathToLocal()
{
@@ -112,7 +87,6 @@ public void TestPathToLocal()
[Test]
public void TestAbsoluteEquality([Values(
PIPE_NAME,
- @"\\?\C:\ÄÖÜßßäöü",
WSL_PATH,
@"\Volumes\System\Test",
@"C:\Directory\File.ext",
@@ -126,5 +100,63 @@ public void TestTildePath()
{
Assert.That(new SystemLocal("~/.ssh/known_hosts").getAbsolute(), Is.Not.Empty);
}
+
+ ///
+ ///
+ ///
+ [Test]
+ public unsafe void TestPathCchCanonicalize([Values(
+ @"c:\temp\test-file.txt",
+ @"\\127.0.0.1\c$\temp\test-file.txt",
+ @"\\LOCALHOST\c$\temp\test-file.txt",
+ @"\\?\UNC\LOCALHOST\c$\temp\test-file.txt",
+ @"\\?\c:\temp\test-file.txt",
+ @"\\127.0.0.1\c$\temp\test-file.txt",
+ @"\\?\C:\Temp",
+ //@"\\?\C:\Temp\", // Paths.get() removes trailing separator
+ @"\\?\C:\ Leading Trailing Whitespace \", // Paths.get() keeps trailing separator
+ @"\Test"
+ )] string path)
+ {
+ SystemLocal local = new(path);
+ var absolute = local.getAbsolute();
+ string canonical;
+ fixed (char* canonicalLocal = new char[CorePInvoke.PATHCCH_MAX_CCH])
+ {
+ fixed (char* pathLocal = path)
+ {
+ if (PInvoke.PathCchCanonicalizeEx(canonicalLocal, CorePInvoke.PATHCCH_MAX_CCH, pathLocal, PATHCCH_OPTIONS.PATHCCH_ALLOW_LONG_PATHS | PATHCCH_OPTIONS.PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS) is
+ {
+ Failed: true,
+ Value: { } canonicalizeError
+ })
+ {
+ throw Marshal.GetExceptionForHR(canonicalizeError);
+ }
+ }
+
+ canonical = ((PCWSTR)canonicalLocal).ToString();
+ }
+
+ Assert.That(absolute, Is.EqualTo(canonical));
+ }
+
+ [Test]
+ public void EnsurePrefixPlatform()
+ {
+ StringBuilder builder = new((int)CorePInvoke.PATHCCH_MAX_CCH);
+ builder.Append("C:");
+ while (builder.Length < 260)
+ {
+ builder.Append('\\');
+ builder.Append(Path.GetTempFileName());
+ }
+ SystemLocal local = new(builder.ToString());
+ Assert.That(local.getAbsolute(), Is.Not.SubPathOf(@"\\?\"));
+ Assert.That(local.NativePath(), Is.SubPathOf(@"\\?\"));
+#if NETFRAMEWORK
+ Assert.That(local.PlatformPath(), Is.SubPathOf(@"\\?\"));
+#endif
+ }
}
}
diff --git a/windows/src/main/csharp/ch/cyberduck/core/CrashReporter.cs b/windows/src/main/csharp/ch/cyberduck/core/CrashReporter.cs
index 83b07f0d1b0..aec1b918466 100644
--- a/windows/src/main/csharp/ch/cyberduck/core/CrashReporter.cs
+++ b/windows/src/main/csharp/ch/cyberduck/core/CrashReporter.cs
@@ -54,7 +54,7 @@ public void Write(Exception e)
ExceptionReportGenerator reportGenerator = new ExceptionReportGenerator(info);
ExceptionReport report = reportGenerator.CreateExceptionReport();
- string crashDir = Path.Combine(SupportDirectoryFinderFactory.get().find().getAbsolute(),
+ string crashDir = Path.Combine(SupportDirectoryFinderFactory.get().find().PlatformPath(),
"CrashReporter");
Directory.CreateDirectory(crashDir);
using (StreamWriter outfile = new StreamWriter(Path.Combine(crashDir, DateTime.Now.Ticks + ".txt")))