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));
+ }
+}