+ * Enable "the linker option used to save portions of the execution state immediately after calling a foreign function associated with a downcall method handle, + * before it can be overwritten by the Java runtime". + * For more details, see https://docs.oracle.com/en/java/javase/25/core/checking-native-errors-using-errno.html + *
+ * @see java.lang.foreign.Linker.Option#captureCallState(String...) + */ + private static MemorySegment FindFirstFileW(MemorySegment lpFileName, MemorySegment lpFindFileData, MemorySegment capturedError) { + try { + return (MemorySegment) FIND_FIRST_FILE__W_HANDLE.invokeExact(capturedError, lpFileName, lpFindFileData); + } catch (Error | RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); //$NON-NLS-1$ + } + } + + private static int GetLastError(MemorySegment capturedError) { + return (int) GET_LAST_ERROR_HANDLE.get(capturedError, 0L); + } + + /** + * Sets the given {@link IFileInfo} to the given file. + * + * @implNote + * This implementation invokes the native Windows API methods {@code GetFileAttributesW} and {@code SetFileAttributesW} to read and write the file attributes. + * It allows to set the updated file attributes only once, after all modifications are applied and only if there is any overall change. + * Through {@link java.nio.file.attribute.DosFileAttributeView} each file attribute can only be set individually, + * which leads to one native file-attributes get and (up to) one set invocation per attribute. + * The NIO API does not provide means for batch updates. + * Since there are currently there file attributes considered, using the Java NIO API would consequently be up to three times slower. + */ @Override public boolean putFileInfo(String fileName, IFileInfo info, int options) { - WString lpFileName = new WString(toLongWindowsPath(fileName)); - long dwFileAttributes = FileAPIh.GetFileAttributesW(lpFileName); - if (dwFileAttributes == FileAPIh.INVALID_FILE_ATTRIBUTES) { - return false; - } - if (dwFileAttributes == WinNT.FILE_ATTRIBUTE_NORMAL) { - // Assume nothing is set, as the documentation of FILE_ATTRIBUTE_NORMAL states: - // "A file that does not have other attributes set. This attribute is valid only when used alone." - dwFileAttributes = 0; - } - long fileAttributes = dwFileAttributes; + String longFilename = toLongWindowsPath(fileName); + try (Arena arena = Arena.ofConfined()) { + MemorySegment lpFileName = allocateWideString(longFilename, arena); + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileattributesw + int dwFileAttributes = FileAPI.GetFileAttributesW(lpFileName); + if (dwFileAttributes == FileAPI.INVALID_FILE_ATTRIBUTES()) { + return false; + } + if (dwFileAttributes == FileAPI.FILE_ATTRIBUTE_NORMAL()) { + // Assume nothing is set, as the documentation of FILE_ATTRIBUTE_NORMAL states: + // "A file that does not have other attributes set. This attribute is valid only when used alone." + dwFileAttributes = 0; + } + int fileAttributes = dwFileAttributes; - boolean archive = info.getAttribute(EFS.ATTRIBUTE_ARCHIVE); - boolean readOnly = info.getAttribute(EFS.ATTRIBUTE_READ_ONLY); - boolean hidden = info.getAttribute(EFS.ATTRIBUTE_HIDDEN); - fileAttributes = set(fileAttributes, WinNT.FILE_ATTRIBUTE_ARCHIVE, archive); - fileAttributes = set(fileAttributes, WinNT.FILE_ATTRIBUTE_READONLY, readOnly); - fileAttributes = set(fileAttributes, WinNT.FILE_ATTRIBUTE_HIDDEN, hidden); + boolean archive = info.getAttribute(EFS.ATTRIBUTE_ARCHIVE); + boolean readOnly = info.getAttribute(EFS.ATTRIBUTE_READ_ONLY); + boolean hidden = info.getAttribute(EFS.ATTRIBUTE_HIDDEN); + fileAttributes = set(fileAttributes, FileAPI.FILE_ATTRIBUTE_ARCHIVE(), archive); + fileAttributes = set(fileAttributes, FileAPI.FILE_ATTRIBUTE_READONLY(), readOnly); + fileAttributes = set(fileAttributes, FileAPI.FILE_ATTRIBUTE_HIDDEN(), hidden); - if (dwFileAttributes == fileAttributes) { - return true; // Everything is already up to date -> nothing to do + if (dwFileAttributes == fileAttributes) { + return true; // Everything is already up to date -> nothing to do + } + // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileattributesw + return FileAPI.SetFileAttributesW(lpFileName, fileAttributes) != 0; } - return FileAPIh.SetFileAttributesW(lpFileName, fileAttributes); } public static String getShortPathName(String longPath) { longPath = toLongWindowsPath(longPath); - char[] buffer = new char[longPath.length()]; // https://learn.microsoft.com/de-de/windows/win32/api/fileapi/nf-fileapi-getshortpathnamew - int newLength = com.sun.jna.platform.win32.Kernel32.INSTANCE.GetShortPathName(longPath, buffer, buffer.length); - if (0 < newLength && newLength < buffer.length) { // zero means error - int offset = longPath.startsWith(WIN32_UNC_RAW_PATH_PREFIX) ? WIN32_UNC_RAW_PATH_PREFIX.length() : WIN32_RAW_PATH_PREFIX.length(); - return new String(buffer, offset, newLength - offset); + try (Arena arena = Arena.ofConfined()) { + MemorySegment lpszLongPath = allocateWideString(longPath, arena); + MemorySegment lpszShortPath = arena.allocate(lpszLongPath.byteSize()); // short name should be shorter -> will fit into array + + int newLength = FileAPI.GetShortPathNameW(lpszLongPath, lpszShortPath, longPath.length()); + if (0 < newLength && newLength < longPath.length()) { // zero means error and if not shorter it's not useful + int offset = longPath.startsWith(WIN32_UNC_RAW_PATH_PREFIX) ? WIN32_UNC_RAW_PATH_PREFIX.length() : WIN32_RAW_PATH_PREFIX.length(); + return getWideString(lpszShortPath).substring(offset); + } + return null; } - return null; } private static String toLongWindowsPath(String fileName) { @@ -135,122 +209,70 @@ private static String toLongWindowsPath(String fileName) { return fileName; } - static class FileAPIh { - static { - Native.register(NativeLibrary.getInstance("Kernel32" /* , W32APIOptions.DEFAULT_OPTIONS */ )); //$NON-NLS-1$ - // Not using W32APIOptions.DEFAULT_OPTIONS requires the usage of a few special types (e.g. WString) but improves performance. - } - private static final long INVALID_HANDLE_VALUE = Pointer.nativeValue(WinBase.INVALID_HANDLE_VALUE.getPointer()); - - // winnt.h HANDLE can be expressed as java long - - // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilew - static native long FindFirstFileW(WString lpFileName, Pointer lpFindFileData); - - // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileattributesw - static native long GetFileAttributesW(WString lpFileName); - - // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileattributesw - static native boolean SetFileAttributesW(WString lpFileName, long dwFileAttributes); - - static final long INVALID_FILE_ATTRIBUTES = new WinBase.DWORD(-1).longValue(); - - static native boolean FindClose(long handle); - } - - private static final int DWORD_SIZE = WinBase.DWORD.SIZE; - private static final int FILETIME_SIZE = 2 * DWORD_SIZE; - private static final int WCHAR_SIZE = 2; - - /** - * Read the of the native memory data-strcuture - * {@code WIN32_FIND_DATAW}. - *
- * typedef struct _WIN32_FIND_DATAW {
- * DWORD dwFileAttributes;
- * FILETIME ftCreationTime;
- * FILETIME ftLastAccessTime;
- * FILETIME ftLastWriteTime;
- * DWORD nFileSizeHigh;
- * DWORD nFileSizeLow;
- * DWORD dwReserved0;
- * DWORD dwReserved1;
- * _Field_z_ WCHAR cFileName[ MAX_PATH ];
- * _Field_z_ WCHAR cAlternateFileName[ 14 ];
- * } WIN32_FIND_DATAW
- *
- */
- private static final int DW_FILE_ATTRIBUTES = 0;
- private static final int FT_CREATION_TIME = DW_FILE_ATTRIBUTES + DWORD_SIZE;
- private static final int FT_LAST_ACCESS_TIME = FT_CREATION_TIME + FILETIME_SIZE;
- private static final int FT_LAST_WRITE_TIME = FT_LAST_ACCESS_TIME + FILETIME_SIZE;
- private static final int N_FILE_SIZE_HIGH = FT_LAST_WRITE_TIME + FILETIME_SIZE;
- private static final int N_FILE_SIZE_LOW = N_FILE_SIZE_HIGH + DWORD_SIZE;
- private static final int DW_RESERVED_0 = N_FILE_SIZE_LOW + DWORD_SIZE;
- private static final int DW_RESERVED_1 = DW_RESERVED_0 + DWORD_SIZE;
- private static final int C_FILE_NAME = DW_RESERVED_1 + DWORD_SIZE;
- private static final int C_ALTERNATE_FILE_NAME = C_FILE_NAME + WinDef.MAX_PATH * WCHAR_SIZE;
-
- private static final int WIN32_FIND_DATA_SIZE = C_ALTERNATE_FILE_NAME + 14 * WCHAR_SIZE;
- static {
- if (WIN32_FIND_DATA_SIZE != WinBase.WIN32_FIND_DATA.sizeOf()) {
- throw new IllegalStateException("Struct 'WIN32_FIND_DATAW' has unexpected size"); //$NON-NLS-1$
- }
- }
- private static final long MAXDWORD = 0xFFFFFFFFL; // unsigned long from winnt.h. On Windows a C long usually has only 32bit
-
- private static void convertFindDataWToFileInfo(Memory mem, FileInfo info, String fileName) throws IOException {
+ @SuppressWarnings("static-access")
+ private static void convertFindDataWToFileInfo(MemorySegment mem, FileInfo info, String fileName) throws IOException {
/**
* For possible values of dwFileAttributes and their descriptions,
* see File Attribute Constants.
*/
- int dwFileAttributes = readDWORDAsSignedInt(mem, DW_FILE_ATTRIBUTES);
- Date ftLastWriteTime = readFILETIME(mem, FT_LAST_WRITE_TIME);
- long nFileSizeHigh = readDWORD(mem, N_FILE_SIZE_HIGH);
- long nFileSizeLow = readDWORD(mem, N_FILE_SIZE_LOW);
- int dwReserved0 = readDWORDAsSignedInt(mem, DW_RESERVED_0);
- String cFileName = mem.getWideString(C_FILE_NAME);
+ int dwFileAttributes = WIN32_FIND_DATAW.dwFileAttributes(mem);
+ Instant ftLastWriteTime = readFILETIME(WIN32_FIND_DATAW.ftLastWriteTime(mem));
- long fileLength = (nFileSizeHigh * (MAXDWORD + 1)) + nFileSizeLow;
+ int nFileSizeHigh = WIN32_FIND_DATAW.nFileSizeHigh(mem);
+ int nFileSizeLow = WIN32_FIND_DATAW.nFileSizeLow(mem);
+ int dwReserved0 = WIN32_FIND_DATAW.dwReserved0(mem);
+ String cFileName = getWideString(WIN32_FIND_DATAW.cFileName(mem));
+
+ long fileLength = toLong(nFileSizeHigh, nFileSizeLow);
info.setName(cFileName);
info.setExists(true);
- info.setLastModified(ftLastWriteTime.getTime());
+ info.setLastModified(ftLastWriteTime.toEpochMilli());
info.setLength(fileLength);
- info.setDirectory(isSet(dwFileAttributes, WinNT.FILE_ATTRIBUTE_DIRECTORY));
- info.setAttribute(EFS.ATTRIBUTE_ARCHIVE, isSet(dwFileAttributes, WinNT.FILE_ATTRIBUTE_ARCHIVE));
- info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, isSet(dwFileAttributes, WinNT.FILE_ATTRIBUTE_READONLY));
- info.setAttribute(EFS.ATTRIBUTE_HIDDEN, isSet(dwFileAttributes, WinNT.FILE_ATTRIBUTE_HIDDEN));
+ info.setDirectory(isSet(dwFileAttributes, FileAPI.FILE_ATTRIBUTE_DIRECTORY()));
+ info.setAttribute(EFS.ATTRIBUTE_ARCHIVE, isSet(dwFileAttributes, FileAPI.FILE_ATTRIBUTE_ARCHIVE()));
+ info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, isSet(dwFileAttributes, FileAPI.FILE_ATTRIBUTE_READONLY()));
+ info.setAttribute(EFS.ATTRIBUTE_HIDDEN, isSet(dwFileAttributes, FileAPI.FILE_ATTRIBUTE_HIDDEN()));
- boolean isReparsePoint = isSet(dwFileAttributes, WinNT.FILE_ATTRIBUTE_REPARSE_POINT);
- if (isReparsePoint && dwReserved0 == WinNT.IO_REPARSE_TAG_SYMLINK) {
+ boolean isReparsePoint = isSet(dwFileAttributes, FileAPI.FILE_ATTRIBUTE_REPARSE_POINT());
+ if (isReparsePoint && dwReserved0 == FileAPI.IO_REPARSE_TAG_SYMLINK()) {
Path linkTarget = Files.readSymbolicLink(Path.of(fileName));
info.setAttribute(EFS.ATTRIBUTE_SYMLINK, true);
info.setStringAttribute(EFS.ATTRIBUTE_LINK_TARGET, linkTarget.toString());
}
}
- private static int readDWORDAsSignedInt(Memory memory, int offset) {
- return memory.getInt(offset); // int is signed
+ private static final Instant WINDOWS_REFERENCE_DATE = LocalDateTime.of(1601, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC);
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
+ @SuppressWarnings("static-access")
+ private static Instant readFILETIME(MemorySegment struct) {
+ int low = FILETIME.dwLowDateTime(struct);
+ int high = FILETIME.dwHighDateTime(struct);
+ final long filetime = toLong(high, low);
+ return WINDOWS_REFERENCE_DATE.plus(filetime / 10, ChronoUnit.MICROS);
}
- private static long readDWORD(Memory memory, int offset) {
- // From https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
- // "DWORD - A 32-bit unsigned integer. The range is 0 through 4294967295 decimal. This type is declared in IntSafe.h as follows: typedef unsigned long DWORD;"
- return readDWORDAsSignedInt(memory, offset) & MAXDWORD;
+ // https://learn.microsoft.com/en-us/windows/win32/learnwin32/working-with-strings
+ private static MemorySegment allocateWideString(String longPath, Arena arena) {
+ return arena.allocateFrom(longPath, StandardCharsets.UTF_16LE);
}
- private static Date readFILETIME(Memory memory, int offset) {
- int low = readDWORDAsSignedInt(memory, offset);
- int high = readDWORDAsSignedInt(memory, offset + DWORD_SIZE);
- return WinBase.FILETIME.filetimeToDate(high, low);
+ private static String getWideString(MemorySegment memory) {
+ return memory.getString(0, StandardCharsets.UTF_16LE);
+ }
+
+ // See also https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
+
+ private static long toLong(int highDWORD, int lowDWORD) {
+ return (long) highDWORD << 32 | lowDWORD & 0xffffffffL;
}
- private static boolean isSet(long field, int bit) {
+ private static boolean isSet(int field, int bit) {
return (field & bit) != 0;
}
- private long set(long field, int bit, boolean isSet) {
+ private int set(int field, int bit, boolean isSet) {
return isSet ? (field | bit) : (field & ~bit);
}