From 27a01e1424cdd3d0c37001b39d5e581d073b49ca Mon Sep 17 00:00:00 2001 From: Yves Langisch Date: Fri, 3 Oct 2025 16:56:07 +0200 Subject: [PATCH 01/13] Add missing implementation. --- .../core/local/FinderLocalAttributes.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/dylib/src/main/java/ch/cyberduck/core/local/FinderLocalAttributes.java b/core/dylib/src/main/java/ch/cyberduck/core/local/FinderLocalAttributes.java index f3df0e2264b..1f0449a5eac 100644 --- a/core/dylib/src/main/java/ch/cyberduck/core/local/FinderLocalAttributes.java +++ b/core/dylib/src/main/java/ch/cyberduck/core/local/FinderLocalAttributes.java @@ -123,6 +123,20 @@ public long getCreationDate() { } } + @Override + public long getModificationDate() { + try { + final NSObject object = this.getNativeAttribute(NSFileManager.NSFileModificationDate); + if(object.isKindOfClass(Rococoa.createClass("NSDate", NSDate._Class.class))) { + return (long) (Rococoa.cast(object, NSDate.class).timeIntervalSince1970() * 1000); + } + return -1; + } + catch(AccessDeniedException | NotfoundException e) { + return -1; + } + } + @Override public String getOwner() { try { From 18e9d3506fc479c7d403215938299d3f63a7c16c Mon Sep 17 00:00:00 2001 From: Yves Langisch Date: Fri, 3 Oct 2025 16:57:00 +0200 Subject: [PATCH 02/13] Add NTFSFilesystemBookmarkResolver. --- core/src/main/csharp/NativeMethods.txt | 5 + .../local/NTFSFilesystemBookmarkResolver.cs | 160 ++++++++++++++++++ .../ch/cyberduck/core/local/SystemLocal.cs | 80 +++++++-- .../core/local/SystemLocalAttributes.cs | 94 ++++++++++ 4 files changed, 324 insertions(+), 15 deletions(-) create mode 100644 core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs create mode 100644 core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs diff --git a/core/src/main/csharp/NativeMethods.txt b/core/src/main/csharp/NativeMethods.txt index 4725549a25c..b4145691ad5 100644 --- a/core/src/main/csharp/NativeMethods.txt +++ b/core/src/main/csharp/NativeMethods.txt @@ -2,6 +2,7 @@ BHID_DataObject CLSID_QueryAssociations CoTaskMemFree +CreateFile CRED_FLAGS CRED_PERSIST CRED_TYPE @@ -11,8 +12,10 @@ CredRead CredWrite DefWindowProc ExtractIconEx +FILE_ID_INFO FOLDERID_Downloads GetCurrentPackageFullName +GetFinalPathNameByHandle GetTokenInformation GetWindowLong GetWindowLongPtr @@ -29,6 +32,8 @@ KNOWN_FOLDER_FLAG LoadLibrary LoadString MESSAGEBOX_RESULT +OpenFileById +PathCchStripPrefix PathParseIconLocation PBST_ERROR PBST_NORMAL diff --git a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs new file mode 100644 index 00000000000..ce6efcec83e --- /dev/null +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -0,0 +1,160 @@ +// Copyright (c) 2010-2025 iterate GmbH. All rights reserved. +// https://cyberduck.io/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +using System; +using System.Globalization; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Storage.FileSystem; +using ch.cyberduck.core; +using ch.cyberduck.core.exception; +using ch.cyberduck.core.local; +using Microsoft.Win32.SafeHandles; +using org.apache.logging.log4j; + +namespace Ch.Cyberduck.Core.Local +{ + public class NTFSFilesystemBookmarkResolver(ch.cyberduck.core.Local root) : FilesystemBookmarkResolver + { + private static readonly Logger Log = LogManager.getLogger(typeof(NTFSFilesystemBookmarkResolver).FullName); + + public string create(ch.cyberduck.core.Local file) + { + throw new NotImplementedException(); + } + + public string create(ch.cyberduck.core.Local l, bool prompt) + { + throw new NotImplementedException(); + } + + public object resolve(string bookmark) + { + if (!ToFileId(bookmark, out var fileId)) + { + throw new LocalAccessDeniedException(bookmark); + } + + var rootPath = root.getAbsolute(); + SafeFileHandle fileHandle = null; + try + { + using (var rootHandle = CorePInvoke.CreateFile( + lpFileName: rootPath, + dwDesiredAccess: 0, + dwShareMode: (FILE_SHARE_MODE)7, + lpSecurityAttributes: null, + dwCreationDisposition: FILE_CREATION_DISPOSITION.OPEN_EXISTING, + dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS, + hTemplateFile: null)) + { + if (rootHandle.IsInvalid) + { + if (Log.isDebugEnabled()) + { + var errorCode = Marshal.GetHRForLastWin32Error(); + Log.debug( + $"Opening root {rootPath} error ({errorCode:X8})"); + } + + throw new LocalAccessDeniedException(bookmark); + } + + FILE_ID_DESCRIPTOR fileDescriptor = new() + { + dwSize = (uint)Marshal.SizeOf(), + Type = FILE_ID_TYPE.FileIdType, + Anonymous = + { + FileId = fileId + } + }; + + fileHandle = CorePInvoke.OpenFileById( + hVolumeHint: rootHandle, + lpFileId: fileDescriptor, + dwDesiredAccess: 0, + dwShareMode: (FILE_SHARE_MODE)7, + lpSecurityAttributes: null, + dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS); + if (fileHandle.IsInvalid) + { + var errorCode = Marshal.GetHRForLastWin32Error(); + Log.warn( + $"Opening file id {bookmark} on {rootPath} ({errorCode:X8})"); + throw new LocalAccessDeniedException(bookmark); + } + } + + // Allocate enough space to store 32768-wchars. + Span finalNameBuffer = new char[32 * 1024 + 1]; + var length = CorePInvoke.GetFinalPathNameByHandle( + hFile: fileHandle, + lpszFilePath: finalNameBuffer, + dwFlags: GETFINALPATHNAMEBYHANDLE_FLAGS.VOLUME_NAME_DOS | + GETFINALPATHNAMEBYHANDLE_FLAGS.FILE_NAME_NORMALIZED); + if (length == 0) + { + var errorCode = Marshal.GetHRForLastWin32Error(); + Log.warn( + $"Get final path name for {fileId} in {rootPath} ({errorCode:X8})"); + 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( + $"Path Strip Prefix \"{finalNameBuffer}\" ({errorCode:X8})"); + throw new LocalAccessDeniedException(bookmark); + } + + var finalName = LocalFactory.get(finalNameBuffer.ToString()); + if (!finalName.equals(root) && !finalName.isChild(root)) + { + Log.warn($"Mismatched root: \"{finalNameBuffer}\", expected \"{rootPath}\""); + throw new LocalAccessDeniedException(bookmark); + } + + return finalName.setBookmark(bookmark); + } + finally + { + fileHandle?.Dispose(); + } + } + + public static bool ToFileId(string bookmark, out long fileId) + { + long fileIdResult = 0; + try + { + return bookmark?.Length == 16 && + long.TryParse(bookmark, NumberStyles.HexNumber, null, out fileIdResult); + } + finally + { + fileId = fileIdResult; + } + } + } +} 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 7c37f370412..4e91c7118a7 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs @@ -1,35 +1,37 @@ -// -// Copyright (c) 2010-2018 Yves Langisch. All rights reserved. -// http://cyberduck.io/ -// +// Copyright (c) 2010-2025 iterate GmbH. All rights reserved. +// https://cyberduck.io/ +// // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// -// Bug fixes, suggestions and comments should be sent to: -// feedback@cyberduck.io -// -using ch.cyberduck.core; -using org.apache.logging.log4j; using System; using System.IO; +using ch.cyberduck.core; +using ch.cyberduck.core.exception; +using ch.cyberduck.core.local; +using java.io; +using org.apache.logging.log4j; using CoreLocal = ch.cyberduck.core.Local; +using File = System.IO.File; using Path = System.IO.Path; +using StringWriter = System.IO.StringWriter; namespace Ch.Cyberduck.Core.Local { public class SystemLocal : CoreLocal { - private static readonly char[] INVALID_CHARS = Path.GetInvalidFileNameChars(); private static readonly Logger Log = LogManager.getLogger(typeof(SystemLocal).FullName); - private static readonly char[] PATH_SEPARATORS = new[] { Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar }; + + 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))) @@ -51,21 +53,64 @@ public SystemLocal(SystemLocal copy) { } + public CoreLocal Resolve() + { + if (null == bookmark) + { + return this; + } + + try + { + var local = FilesystemBookmarkResolverFactory.get().resolve(bookmark) as CoreLocal; + return local; + } + catch (LocalAccessDeniedException e) + + { + Log.warn($"Failure resolving bookmark for {this}", e); + return this; + } + } + + public override AttributedList list(Filter filter) + { + return base.list(Resolve().getAbsolute(), filter); + } + + public override OutputStream getOutputStream(bool append) + { + return base.getOutputStream(Resolve().getAbsolute(), append); + } + + public override InputStream getInputStream() + { + return base.getInputStream(Resolve().getAbsolute()); + } + public override bool exists() { - string path = getAbsolute(); + var resolved = Resolve(); + string path = resolved.getAbsolute(); if (File.Exists(path)) { return true; } + bool directory = Directory.Exists(path); if (directory) { return true; } + return false; } + public override LocalAttributes attributes() + { + return new SystemLocalAttributes(this); + } + public override String getAbbreviatedPath() { return getAbsolute(); @@ -93,7 +138,8 @@ private static string Join(string root, string path) ? string.Concat(root, path) : string.Concat(root, Path.DirectorySeparatorChar, path); - static bool IsDirectorySeparator(char sep) => sep == Path.DirectorySeparatorChar || sep == Path.AltDirectorySeparatorChar; + static bool IsDirectorySeparator(char sep) => + sep == Path.DirectorySeparatorChar || sep == Path.AltDirectorySeparatorChar; } private static string Sanitize(string name, bool makeUnc = false) @@ -102,6 +148,7 @@ private static string Sanitize(string name, bool makeUnc = false) { return ""; } + using StringWriter writer = new(); var namespan = name.AsSpan(); @@ -160,6 +207,7 @@ private static string Sanitize(string name, bool makeUnc = false) // 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]); @@ -186,6 +234,7 @@ private static string Sanitize(string name, bool makeUnc = false) }); } } + hasUnc = false; leadingSeparators = null; @@ -198,6 +247,7 @@ private static string Sanitize(string name, bool makeUnc = false) } } } + return writer.ToString(); } } diff --git a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs new file mode 100644 index 00000000000..af5a01eba34 --- /dev/null +++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2010-2025 iterate GmbH. All rights reserved. +// https://cyberduck.io/ +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +using System; +using System.IO; +using ch.cyberduck.core; +using java.nio.file; +using java.nio.file.attribute; +using org.apache.logging.log4j; + +namespace Ch.Cyberduck.Core.Local +{ + public class SystemLocalAttributes : LocalAttributes + { + private static readonly Logger Log = LogManager.getLogger(typeof(SystemLocalAttributes).FullName); + + private readonly SystemLocal local; + + public SystemLocalAttributes(SystemLocal local) : base(local.getAbsolute()) + { + this.local = local; + } + + public override long getSize() + { + var resolved = local.resolve(); + try + { + return new FileInfo(resolved.getAbsolute()).Length; + } + catch (Exception e) + { + Log.warn($"Failure getting size of {resolved}. {e.Message}"); + return -1L; + } + } + + public override long getModificationDate() + { + var resolved = local.resolve(); + try + { + return Files.getLastModifiedTime(Paths.get(resolved.getAbsolute())).toMillis(); + } + catch (Exception e) + { + Log.warn($"Failure getting timestamp of {resolved}. {e.Message}"); + return -1L; + } + } + + public override long getCreationDate() + { + var resolved = local.resolve(); + try + { + return Files + .readAttributes(Paths.get(resolved.getAbsolute()), typeof(BasicFileAttributes)).creationTime() + .toMillis(); + } + catch (Exception e) + { + Log.warn($"Failure getting timestamp of {resolved}. {e.Message}"); + return -1L; + } + } + + public override long getAccessedDate() + { + var resolved = local.resolve(); + try + { + return Files + .readAttributes(Paths.get(resolved.getAbsolute()), typeof(BasicFileAttributes)).lastAccessTime() + .toMillis(); + } + catch (Exception e) + { + Log.warn($"Failure getting timestamp of {resolved}. {e.Message}"); + return -1L; + } + } + } +} From bd97e0cec0baf8b7bf2a079bb006fa973971d373 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Mon, 6 Oct 2025 10:19:32 +0200 Subject: [PATCH 03/13] Add implementation --- core/src/main/csharp/Windows/Win32/CorePInvoke.cs | 8 ++++++++ .../main/csharp/Windows/Win32/CorePInvoke.net472.cs | 11 +++++++++++ .../main/csharp/Windows/Win32/CorePInvoke.net8.0.cs | 13 +++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs create mode 100644 core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs index 013a7531f43..97b9ba5a2f0 100644 --- a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs @@ -43,6 +43,14 @@ public static unsafe SafeCredentialHandle CredRead(string TargetName, CRED_TYPE return new((nint)credential, true); } + public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Span lpszFilePath, GETFINALPATHNAMEBYHANDLE_FLAGS dwFlags) + { + fixed (char* lpszFilePathLocal = lpszFilePath) + { + return GetFinalPathNameByHandle(hFile, lpszFilePathLocal, (uint)lpszFilePath.Length, dwFlags); + } + } + /// public static unsafe HRESULT SHCreateAssociationRegistration(out T ppv) where T : class { diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs new file mode 100644 index 00000000000..b9e74204943 --- /dev/null +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs @@ -0,0 +1,11 @@ +using System; +using System.Runtime.InteropServices; +using Windows.Win32.Storage.FileSystem; + +namespace Windows.Win32; + +partial class CorePInvoke +{ + /// + public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Span lpszFilePath, GETFINALPATHNAMEBYHANDLE_FLAGS dwFlags); +} diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs new file mode 100644 index 00000000000..5f845f88ca4 --- /dev/null +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Windows.Win32.Storage.FileSystem; + +namespace Windows.Win32; + +partial class CorePInvoke +{ + /// + [SupportedOSPlatform("windows6.0.6000")] + public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Span lpszFilePath, GETFINALPATHNAMEBYHANDLE_FLAGS dwFlags); +} From de0683a7c12c13bd0c36121e1c0e374739b0cd2b Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Mon, 6 Oct 2025 10:19:42 +0200 Subject: [PATCH 04/13] Fix method call --- .../ch/cyberduck/core/local/SystemLocalAttributes.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 af5a01eba34..18558f24026 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocalAttributes.cs @@ -33,7 +33,7 @@ public SystemLocalAttributes(SystemLocal local) : base(local.getAbsolute()) public override long getSize() { - var resolved = local.resolve(); + var resolved = local.Resolve(); try { return new FileInfo(resolved.getAbsolute()).Length; @@ -47,7 +47,7 @@ public override long getSize() public override long getModificationDate() { - var resolved = local.resolve(); + var resolved = local.Resolve(); try { return Files.getLastModifiedTime(Paths.get(resolved.getAbsolute())).toMillis(); @@ -61,7 +61,7 @@ public override long getModificationDate() public override long getCreationDate() { - var resolved = local.resolve(); + var resolved = local.Resolve(); try { return Files @@ -77,7 +77,7 @@ public override long getCreationDate() public override long getAccessedDate() { - var resolved = local.resolve(); + var resolved = local.Resolve(); try { return Files From 4e3647d5b7b1824afcef0f37a92d7da59e62587d Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Mon, 6 Oct 2025 10:45:32 +0200 Subject: [PATCH 05/13] Default implementation --- .../core/local/NTFSFilesystemBookmarkResolver.cs | 10 ++-------- 1 file changed, 2 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 ce6efcec83e..c59201f6e23 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -28,15 +28,9 @@ public class NTFSFilesystemBookmarkResolver(ch.cyberduck.core.Local root) : File { private static readonly Logger Log = LogManager.getLogger(typeof(NTFSFilesystemBookmarkResolver).FullName); - public string create(ch.cyberduck.core.Local file) - { - throw new NotImplementedException(); - } + public string create(ch.cyberduck.core.Local file) => FilesystemBookmarkResolver.__DefaultMethods.create(this, file); - public string create(ch.cyberduck.core.Local l, bool prompt) - { - throw new NotImplementedException(); - } + public string create(ch.cyberduck.core.Local file, bool prompt) => null; public object resolve(string bookmark) { From a6dabe2d8a2cc6da288694eb65d10bb48e06bef1 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Mon, 6 Oct 2025 11:17:57 +0200 Subject: [PATCH 06/13] Resolve logging for Span in Framework --- .../cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 c59201f6e23..c353da5e239 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -118,14 +118,18 @@ public object resolve(string bookmark) { 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); } var finalName = LocalFactory.get(finalNameBuffer.ToString()); if (!finalName.equals(root) && !finalName.isChild(root)) { - Log.warn($"Mismatched root: \"{finalNameBuffer}\", expected \"{rootPath}\""); + Log.warn($"Mismatched root: \"{finalName.getAbsolute()}\", expected \"{rootPath}\""); throw new LocalAccessDeniedException(bookmark); } From 0649f1dc0c269e4f9916a92904ff6a0f7db1e0da Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Mon, 6 Oct 2025 12:28:28 +0200 Subject: [PATCH 07/13] Formatting --- core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs | 1 - 1 file changed, 1 deletion(-) 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 4e91c7118a7..726b11f6898 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs @@ -66,7 +66,6 @@ public CoreLocal Resolve() return local; } catch (LocalAccessDeniedException e) - { Log.warn($"Failure resolving bookmark for {this}", e); return this; From 35351e5714552f243de95db6f49dfb5a1c2c9d56 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 7 Oct 2025 11:05:37 +0200 Subject: [PATCH 08/13] Move documentation --- core/src/main/csharp/Windows/Win32/CorePInvoke.cs | 1 + core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs | 1 - core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs index 97b9ba5a2f0..bc47bcd947b 100644 --- a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs @@ -43,6 +43,7 @@ public static unsafe SafeCredentialHandle CredRead(string TargetName, CRED_TYPE return new((nint)credential, true); } + /// public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Span lpszFilePath, GETFINALPATHNAMEBYHANDLE_FLAGS dwFlags) { fixed (char* lpszFilePathLocal = lpszFilePath) diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs index b9e74204943..46125f08e59 100644 --- a/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs @@ -6,6 +6,5 @@ namespace Windows.Win32; partial class CorePInvoke { - /// public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Span lpszFilePath, GETFINALPATHNAMEBYHANDLE_FLAGS dwFlags); } diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs index 5f845f88ca4..7a319f8869d 100644 --- a/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs @@ -7,7 +7,6 @@ namespace Windows.Win32; partial class CorePInvoke { - /// [SupportedOSPlatform("windows6.0.6000")] public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Span lpszFilePath, GETFINALPATHNAMEBYHANDLE_FLAGS dwFlags); } From 51227c3eb5b0e37da23663967a56d8a4571a9d7f Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 7 Oct 2025 11:06:09 +0200 Subject: [PATCH 09/13] Pass argument to NTFSFilesystemBookmarkResolver --- core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 726b11f6898..f7ba7d1c8f2 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs @@ -15,7 +15,6 @@ using System.IO; using ch.cyberduck.core; using ch.cyberduck.core.exception; -using ch.cyberduck.core.local; using java.io; using org.apache.logging.log4j; using CoreLocal = ch.cyberduck.core.Local; @@ -62,8 +61,7 @@ public CoreLocal Resolve() try { - var local = FilesystemBookmarkResolverFactory.get().resolve(bookmark) as CoreLocal; - return local; + return (CoreLocal)new NTFSFilesystemBookmarkResolver(this).resolve(bookmark); } catch (LocalAccessDeniedException e) { From 31764f1994495083d059471822ecca796ced8dab Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 7 Oct 2025 11:07:45 +0200 Subject: [PATCH 10/13] Add formatting rule --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index b01d449846a..0eba263f1c9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,6 +22,9 @@ indent_size = 2 [*.yml] indent_size = 2 +[*.{cs,vb}] +dotnet_sort_system_directives_first = true + [*.java] ij_java_align_consecutive_assignments = false ij_java_align_consecutive_variable_declarations = false From 58979958bbe864d8e64cc5f54d8f2916808316df Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 7 Oct 2025 11:10:07 +0200 Subject: [PATCH 11/13] Try find existing parent for volume hint of local --- .../local/NTFSFilesystemBookmarkResolver.cs | 101 ++++++++++++------ 1 file changed, 68 insertions(+), 33 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 c353da5e239..db823384fef 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -12,56 +12,60 @@ // GNU General Public License for more details. using System; +using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; -using Windows.Win32; -using Windows.Win32.Storage.FileSystem; using ch.cyberduck.core; using ch.cyberduck.core.exception; using ch.cyberduck.core.local; using Microsoft.Win32.SafeHandles; using org.apache.logging.log4j; +using Windows.Win32; +using Windows.Win32.Storage.FileSystem; +using CoreLocal = ch.cyberduck.core.Local; namespace Ch.Cyberduck.Core.Local { - public class NTFSFilesystemBookmarkResolver(ch.cyberduck.core.Local root) : FilesystemBookmarkResolver + public class NTFSFilesystemBookmarkResolver(CoreLocal local) : FilesystemBookmarkResolver { private static readonly Logger Log = LogManager.getLogger(typeof(NTFSFilesystemBookmarkResolver).FullName); - public string create(ch.cyberduck.core.Local file) => FilesystemBookmarkResolver.__DefaultMethods.create(this, file); + public NTFSFilesystemBookmarkResolver() : this(null) + { + } + + public string create(CoreLocal file) => FilesystemBookmarkResolver.__DefaultMethods.create(this, file); + + public string create(CoreLocal file, bool prompt) + { + Debug.Assert(local is null, "Unecessary usage of Local-constructor."); + + // ToDo: Backport - public string create(ch.cyberduck.core.Local file, bool prompt) => null; + return null; + } public object resolve(string bookmark) { + if (local is null) + { + throw new LocalAccessDeniedException("Unsupported Interface usage"); + } + if (!ToFileId(bookmark, out var fileId)) { throw new LocalAccessDeniedException(bookmark); } - var rootPath = root.getAbsolute(); SafeFileHandle fileHandle = null; try { - using (var rootHandle = CorePInvoke.CreateFile( - lpFileName: rootPath, - dwDesiredAccess: 0, - dwShareMode: (FILE_SHARE_MODE)7, - lpSecurityAttributes: null, - dwCreationDisposition: FILE_CREATION_DISPOSITION.OPEN_EXISTING, - dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS, - hTemplateFile: null)) + SafeFileHandle rootHandle = null; + try { - if (rootHandle.IsInvalid) + if (!TryFindRoot(local, out rootHandle)) { - if (Log.isDebugEnabled()) - { - var errorCode = Marshal.GetHRForLastWin32Error(); - Log.debug( - $"Opening root {rootPath} error ({errorCode:X8})"); - } - - throw new LocalAccessDeniedException(bookmark); + throw new LocalAccessDeniedException($"Cannot find root for \"{local}\""); } FILE_ID_DESCRIPTOR fileDescriptor = new() @@ -85,10 +89,14 @@ public object resolve(string bookmark) { var errorCode = Marshal.GetHRForLastWin32Error(); Log.warn( - $"Opening file id {bookmark} on {rootPath} ({errorCode:X8})"); + $"Opening file {local.getAbsolute()} with id {bookmark} ({errorCode:X8})"); throw new LocalAccessDeniedException(bookmark); } } + finally + { + rootHandle?.Dispose(); + } // Allocate enough space to store 32768-wchars. Span finalNameBuffer = new char[32 * 1024 + 1]; @@ -101,7 +109,7 @@ public object resolve(string bookmark) { var errorCode = Marshal.GetHRForLastWin32Error(); Log.warn( - $"Get final path name for {fileId} in {rootPath} ({errorCode:X8})"); + $"Get final path name for {fileId} originally {local.getAbsolute()} ({errorCode:X8})"); throw new LocalAccessDeniedException(bookmark); } @@ -126,14 +134,7 @@ public object resolve(string bookmark) throw new LocalAccessDeniedException(bookmark); } - var finalName = LocalFactory.get(finalNameBuffer.ToString()); - if (!finalName.equals(root) && !finalName.isChild(root)) - { - Log.warn($"Mismatched root: \"{finalName.getAbsolute()}\", expected \"{rootPath}\""); - throw new LocalAccessDeniedException(bookmark); - } - - return finalName.setBookmark(bookmark); + return LocalFactory.get(finalNameBuffer.ToString()).setBookmark(bookmark); } finally { @@ -154,5 +155,39 @@ public static bool ToFileId(string bookmark, out long fileId) fileId = fileIdResult; } } + + private static bool TryFindRoot(CoreLocal local, out SafeFileHandle handle) + { + while (!local.isRoot()) + { + local = local.getParent(); + SafeFileHandle result = null; + try + { + result = CorePInvoke.CreateFile( + lpFileName: local.getAbsolute(), + dwDesiredAccess: 0, + dwShareMode: (FILE_SHARE_MODE)7, + lpSecurityAttributes: null, dwCreationDisposition: FILE_CREATION_DISPOSITION.OPEN_EXISTING, + dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS, + hTemplateFile: null); + if (result.IsInvalid) + { + continue; + } + + handle = result; + result = null; + return true; + } + finally + { + result?.Dispose(); + } + } + + handle = null; + return false; + } } } From 11057840cf05a9a145c696e028d6f23e16a73649 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 7 Oct 2025 11:16:27 +0200 Subject: [PATCH 12/13] Typo --- .../ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 db823384fef..4ff0066aa63 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -38,7 +38,7 @@ public NTFSFilesystemBookmarkResolver() : this(null) public string create(CoreLocal file, bool prompt) { - Debug.Assert(local is null, "Unecessary usage of Local-constructor."); + Debug.Assert(local is null, "Unnecessary usage of Local-constructor."); // ToDo: Backport From 4b92d2d969149c420f76884a9be3a4327ce50305 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 7 Oct 2025 12:17:29 +0200 Subject: [PATCH 13/13] Implement remaining --- core/src/main/csharp/NativeMethods.txt | 4 ++ .../main/csharp/Windows/Win32/CorePInvoke.cs | 65 +++++++++++++++++++ .../local/NTFSFilesystemBookmarkResolver.cs | 50 ++++++++++---- .../NTFSFilesystemBookmarkResolverTest.cs | 24 +++++++ 4 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs diff --git a/core/src/main/csharp/NativeMethods.txt b/core/src/main/csharp/NativeMethods.txt index b4145691ad5..d2376e6eb46 100644 --- a/core/src/main/csharp/NativeMethods.txt +++ b/core/src/main/csharp/NativeMethods.txt @@ -15,6 +15,7 @@ ExtractIconEx FILE_ID_INFO FOLDERID_Downloads GetCurrentPackageFullName +GetFileInformationByHandleEx GetFinalPathNameByHandle GetTokenInformation GetWindowLong @@ -33,6 +34,9 @@ LoadLibrary LoadString MESSAGEBOX_RESULT OpenFileById +PATHCCH_ALLOW_LONG_PATHS +PATHCCH_MAX_CCH +PathCchCanonicalizeEx PathCchStripPrefix PathParseIconLocation PBST_ERROR diff --git a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs index bc47bcd947b..3aa55ee8b8c 100644 --- a/core/src/main/csharp/Windows/Win32/CorePInvoke.cs +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.cs @@ -1,6 +1,8 @@ using System; using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; using Windows.Win32.Foundation; +using Windows.Win32.Security; using Windows.Win32.Security.Credentials; using Windows.Win32.Storage.FileSystem; using Windows.Win32.UI.Shell; @@ -22,6 +24,48 @@ public static unsafe int LoadString(SafeHandle hInstance, uint uID, out PCWSTR l } } + /// + public static unsafe SafeFileHandle CreateFile( + in ReadOnlySpan lpFileName, + uint dwDesiredAccess, + FILE_SHARE_MODE dwShareMode, + SECURITY_ATTRIBUTES? lpSecurityAttributes, + FILE_CREATION_DISPOSITION dwCreationDisposition, + FILE_FLAGS_AND_ATTRIBUTES dwFlagsAndAttributes, + SafeHandle hTemplateFile) + { + bool hTemplateFileAddRef = false; + try + { + fixed (char* lpFileNameLocal = lpFileName) + { + SECURITY_ATTRIBUTES lpSecurityAttributesLocal = lpSecurityAttributes ?? default(SECURITY_ATTRIBUTES); + HANDLE hTemplateFileLocal; + if (hTemplateFile is object) + { + hTemplateFile.DangerousAddRef(ref hTemplateFileAddRef); + hTemplateFileLocal = (HANDLE)hTemplateFile.DangerousGetHandle(); + } + else + hTemplateFileLocal = (HANDLE)new IntPtr(0L); + HANDLE __result = CorePInvoke.CreateFile( + lpFileName: lpFileNameLocal, + dwDesiredAccess: dwDesiredAccess, + dwShareMode: dwShareMode, + lpSecurityAttributes: lpSecurityAttributes.HasValue ? &lpSecurityAttributesLocal : null, + dwCreationDisposition: dwCreationDisposition, + dwFlagsAndAttributes: dwFlagsAndAttributes, + hTemplateFile: hTemplateFileLocal); + return new SafeFileHandle(__result, ownsHandle: true); + } + } + finally + { + if (hTemplateFileAddRef) + hTemplateFile.DangerousRelease(); + } + } + /// public static unsafe bool CredDelete(string TargetName, CRED_TYPE type, CRED_FLAGS flags) { @@ -43,6 +87,15 @@ public static unsafe SafeCredentialHandle CredRead(string TargetName, CRED_TYPE return new((nint)credential, true); } + /// + public static unsafe BOOL GetFileInformationByHandleEx(SafeHandle hFile, FILE_INFO_BY_HANDLE_CLASS FileInformationClass, out T value) where T : unmanaged + { + fixed (T* valueLocal = &value) + { + return GetFileInformationByHandleEx(hFile, FileInformationClass, valueLocal, (uint)Marshal.SizeOf()); + } + } + /// public static unsafe partial uint GetFinalPathNameByHandle(SafeHandle hFile, Span lpszFilePath, GETFINALPATHNAMEBYHANDLE_FLAGS dwFlags) { @@ -52,6 +105,18 @@ 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/local/NTFSFilesystemBookmarkResolver.cs b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs index 4ff0066aa63..37fb4c5a73a 100644 --- a/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -12,8 +12,8 @@ // GNU General Public License for more details. using System; -using System.Diagnostics; using System.Globalization; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using ch.cyberduck.core; using ch.cyberduck.core.exception; @@ -22,7 +22,9 @@ using org.apache.logging.log4j; using Windows.Win32; using Windows.Win32.Storage.FileSystem; +using Windows.Win32.UI.Shell; using CoreLocal = ch.cyberduck.core.Local; +using NetPath = System.IO.Path; namespace Ch.Cyberduck.Core.Local { @@ -30,28 +32,52 @@ public class NTFSFilesystemBookmarkResolver(CoreLocal local) : FilesystemBookmar { private static readonly Logger Log = LogManager.getLogger(typeof(NTFSFilesystemBookmarkResolver).FullName); - public NTFSFilesystemBookmarkResolver() : this(null) - { - } - public string create(CoreLocal file) => FilesystemBookmarkResolver.__DefaultMethods.create(this, file); public string create(CoreLocal file, bool prompt) { - Debug.Assert(local is null, "Unnecessary usage of Local-constructor."); + 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; + } - // ToDo: Backport + FILE_ID_INFO info; + using (var handle = CorePInvoke.CreateFile( + lpFileName: finalNameBuffer, + dwDesiredAccess: 0, + dwShareMode: (FILE_SHARE_MODE)7, + lpSecurityAttributes: null, + dwCreationDisposition: FILE_CREATION_DISPOSITION.OPEN_EXISTING, + dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS, + hTemplateFile: null)) + { + if (handle.IsInvalid) + { + goto error; + } + if (!CorePInvoke.GetFileInformationByHandleEx(handle, FILE_INFO_BY_HANDLE_CLASS.FileIdInfo, out info)) + { + goto error; + } + } + + return Unsafe.As(ref info.FileId).ToString("X16"); + + error: return null; } public object resolve(string bookmark) { - if (local is null) - { - throw new LocalAccessDeniedException("Unsupported Interface usage"); - } - if (!ToFileId(bookmark, out var fileId)) { throw new LocalAccessDeniedException(bookmark); diff --git a/core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs b/core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs new file mode 100644 index 00000000000..42fccc20225 --- /dev/null +++ b/core/src/test/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolverTest.cs @@ -0,0 +1,24 @@ +using ch.cyberduck.core.local; +using NUnit.Framework; +using NUnit.Framework.Constraints; +using CoreLocal = ch.cyberduck.core.Local; +using NetPath = System.IO.Path; + +namespace Ch.Cyberduck.Core.Local; + +[TestFixture] +public class NTFSFilesystemBookmarkResolverTest +{ + [Test] + public void EnsureRoundtrip() + { + SystemLocal temp = new(NetPath.GetTempPath()); + SystemLocal file = new(temp, NetPath.GetRandomFileName()); + new DefaultLocalTouchFeature().touch(file); + NTFSFilesystemBookmarkResolver resolver = new(file); + var bookmark = resolver.create(file); + Assert.That(bookmark, new NotConstraint(new NullConstraint())); + CoreLocal resolved = (CoreLocal)resolver.resolve(bookmark); + Assert.That(resolved, new EqualConstraint(file)); + } +}