From 793cf356e8f18dca9dc556850e38b06a1fe845a1 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 25 Nov 2025 15:33:03 +0100 Subject: [PATCH 1/3] Make SystemLocal accept long paths --- .../ch/cyberduck/core/local/SystemLocal.cs | 216 ++++++++++-------- .../test/csharp/Cyberduck.Core.Test.csproj | 1 + core/src/test/csharp/NativeMethods.txt | 1 + .../NTFSFilesystemBookmarkResolverTest.cs | 4 +- .../cyberduck/core/local/SystemLocalTest.cs | 85 ++++--- 5 files changed, 172 insertions(+), 135 deletions(-) create mode 100644 core/src/test/csharp/NativeMethods.txt 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..3ff6eb0058c 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs @@ -28,12 +28,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 +44,7 @@ public SystemLocal(CoreLocal parent, string name) } public SystemLocal(string path) - : base(Sanitize(path)) + : base(Canonicalize(path)) { } @@ -89,18 +90,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 +131,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/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..19134a0a60d 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,11 @@ -using ch.cyberduck.core; +using System.Runtime.InteropServices; +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 +42,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 +60,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 +74,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 +86,6 @@ public void TestPathToLocal() [Test] public void TestAbsoluteEquality([Values( PIPE_NAME, - @"\\?\C:\ÄÖÜßßäöü", WSL_PATH, @"\Volumes\System\Test", @"C:\Directory\File.ext", @@ -126,5 +99,45 @@ 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)); + } } } From 73909c432c476ef90451afde584f1b1832f97c34 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 25 Nov 2025 15:33:50 +0100 Subject: [PATCH 2/3] Remove unused PathCch --- core/src/main/csharp/NativeMethods.txt | 3 - .../main/csharp/Windows/Win32/CorePInvoke.cs | 12 ---- .../ch/cyberduck/core/LocalExtensions.cs | 14 +++++ .../local/NTFSFilesystemBookmarkResolver.cs | 38 +----------- .../ch/cyberduck/core/local/SystemLocal.cs | 58 +++++++++++++++++++ .../cyberduck/core/local/SystemLocalTest.cs | 19 ++++++ 6 files changed, 93 insertions(+), 51 deletions(-) create mode 100644 core/src/main/csharp/ch/cyberduck/core/LocalExtensions.cs 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..639a86cae6a 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 { 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 3ff6eb0058c..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; @@ -53,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) 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 19134a0a60d..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,4 +1,5 @@ using System.Runtime.InteropServices; +using System.Text; using ch.cyberduck.core; using java.nio.file; using java.util; @@ -139,5 +140,23 @@ public unsafe void TestPathCchCanonicalize([Values( 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 + } } } From 315ea02d09d36d46c7b4886c4a9fb8532fa92647 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 25 Nov 2025 15:34:40 +0100 Subject: [PATCH 3/3] Use new PlatformPath/NativePath --- .../cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs | 2 +- .../ch/cyberduck/core/local/RecycleLocalTrashFeature.cs | 4 ++-- .../ch/cyberduck/core/local/ShellApplicationFinder.cs | 6 +++--- .../csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs | 2 +- windows/src/main/csharp/ch/cyberduck/core/CrashReporter.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) 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 639a86cae6a..a2db2df232e 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -157,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/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/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")))