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 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 { diff --git a/core/src/main/csharp/NativeMethods.txt b/core/src/main/csharp/NativeMethods.txt index 4725549a25c..d2376e6eb46 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,11 @@ CredRead CredWrite DefWindowProc ExtractIconEx +FILE_ID_INFO FOLDERID_Downloads GetCurrentPackageFullName +GetFileInformationByHandleEx +GetFinalPathNameByHandle GetTokenInformation GetWindowLong GetWindowLongPtr @@ -29,6 +33,11 @@ KNOWN_FOLDER_FLAG 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 013a7531f43..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,36 @@ 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) + { + fixed (char* lpszFilePathLocal = lpszFilePath) + { + return GetFinalPathNameByHandle(hFile, lpszFilePathLocal, (uint)lpszFilePath.Length, dwFlags); + } + } + + /// + 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/Windows/Win32/CorePInvoke.net472.cs b/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs new file mode 100644 index 00000000000..46125f08e59 --- /dev/null +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.net472.cs @@ -0,0 +1,10 @@ +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..7a319f8869d --- /dev/null +++ b/core/src/main/csharp/Windows/Win32/CorePInvoke.net8.0.cs @@ -0,0 +1,12 @@ +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); +} 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..37fb4c5a73a --- /dev/null +++ b/core/src/main/csharp/ch/cyberduck/core/local/NTFSFilesystemBookmarkResolver.cs @@ -0,0 +1,219 @@ +// 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.CompilerServices; +using System.Runtime.InteropServices; +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 Windows.Win32.UI.Shell; +using CoreLocal = ch.cyberduck.core.Local; +using NetPath = System.IO.Path; + +namespace Ch.Cyberduck.Core.Local +{ + public class NTFSFilesystemBookmarkResolver(CoreLocal local) : FilesystemBookmarkResolver + { + private static readonly Logger Log = LogManager.getLogger(typeof(NTFSFilesystemBookmarkResolver).FullName); + + public string create(CoreLocal file) => FilesystemBookmarkResolver.__DefaultMethods.create(this, file); + + 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, + 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 (!ToFileId(bookmark, out var fileId)) + { + throw new LocalAccessDeniedException(bookmark); + } + + SafeFileHandle fileHandle = null; + try + { + SafeFileHandle rootHandle = null; + try + { + if (!TryFindRoot(local, out rootHandle)) + { + throw new LocalAccessDeniedException($"Cannot find root for \"{local}\""); + } + + 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 {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]; + 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} originally {local.getAbsolute()} ({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( +#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); + } + 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; + } + } + + 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; + } + } +} 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..f7ba7d1c8f2 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,36 @@ -// -// 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 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 +52,62 @@ public SystemLocal(SystemLocal copy) { } + public CoreLocal Resolve() + { + if (null == bookmark) + { + return this; + } + + try + { + return (CoreLocal)new NTFSFilesystemBookmarkResolver(this).resolve(bookmark); + } + 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 +135,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 +145,7 @@ private static string Sanitize(string name, bool makeUnc = false) { return ""; } + using StringWriter writer = new(); var namespan = name.AsSpan(); @@ -160,6 +204,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 +231,7 @@ private static string Sanitize(string name, bool makeUnc = false) }); } } + hasUnc = false; leadingSeparators = null; @@ -198,6 +244,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..18558f24026 --- /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; + } + } + } +} 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)); + } +}