diff --git a/src/main/cpp/win.cpp b/src/main/cpp/win.cpp index 349e049d..d0c92942 100755 --- a/src/main/cpp/win.cpp +++ b/src/main/cpp/win.cpp @@ -177,6 +177,8 @@ typedef struct file_stat { LONG fileType; LONGLONG lastModified; LONGLONG size; + LONG volumeSerialNumber; + LONGLONG fileId; } file_stat_t; // @@ -198,11 +200,15 @@ DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileSta pFileStat->lastModified = 0; pFileStat->size = 0; pFileStat->fileType = FILE_TYPE_MISSING; + pFileStat->volumeSerialNumber = 0; + pFileStat->fileId = 0; return ERROR_SUCCESS; } return error; } pFileStat->lastModified = lastModifiedNanos(&attr.ftLastWriteTime); + pFileStat->volumeSerialNumber = 0; + pFileStat->fileId = 0; if (attr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { pFileStat->size = 0; pFileStat->fileType = FILE_TYPE_DIRECTORY; @@ -232,6 +238,8 @@ DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileSta pFileStat->lastModified = 0; pFileStat->size = 0; pFileStat->fileType = FILE_TYPE_MISSING; + pFileStat->volumeSerialNumber = 0; + pFileStat->fileId = 0; return ERROR_SUCCESS; } return error; @@ -259,6 +267,8 @@ DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileSta pFileStat->lastModified = lastModifiedNanos(&fileInfo.ftLastWriteTime); pFileStat->size = 0; + pFileStat->volumeSerialNumber = fileInfo.dwVolumeSerialNumber; + pFileStat->fileId = ((LONGLONG)fileInfo.nFileIndexHigh << 32) | fileInfo.nFileIndexLow; if (is_file_symlink(fileTagInfo.FileAttributes, fileTagInfo.ReparseTag)) { pFileStat->fileType = FILE_TYPE_SYMLINK; } else if (fileTagInfo.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) { @@ -533,12 +543,21 @@ Java_net_rubygrapefruit_platform_internal_jni_FileEventFunctions_closeWatch(JNIE JNIEXPORT void JNICALL Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_stat(JNIEnv *env, jclass target, jstring path, jboolean followLink, jobject dest, jobject result) { +#ifdef WINDOWS_MIN jclass destClass = env->GetObjectClass(dest); jmethodID mid = env->GetMethodID(destClass, "details", "(IJJ)V"); if (mid == NULL) { mark_failed_with_message(env, "could not find method", result); return; } +#else + jclass destClass = env->GetObjectClass(dest); + jmethodID mid = env->GetMethodID(destClass, "details", "(IJJIJ)V"); + if (mid == NULL) { + mark_failed_with_message(env, "could not find method", result); + return; + } +#endif wchar_t* pathStr = java_to_wchar_path(env, path, result); file_stat_t fileStat; @@ -548,7 +567,12 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_stat(JNIEnv * mark_failed_with_code(env, "could not file attributes", errorCode, NULL, result); return; } + +#ifdef WINDOWS_MIN env->CallVoidMethod(dest, mid, fileStat.fileType, fileStat.size, fileStat.lastModified); +#else + env->CallVoidMethod(dest, mid, fileStat.fileType, fileStat.size, fileStat.lastModified, fileStat.volumeSerialNumber, fileStat.fileId); +#endif } JNIEXPORT void JNICALL @@ -589,6 +613,8 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEn fileInfo.fileType = FILE_TYPE_MISSING; fileInfo.size = 0; fileInfo.lastModified = 0; + fileInfo.volumeSerialNumber = 0; + fileInfo.fileId = 0; } } else { fileInfo.fileType = isSymLink ? @@ -598,6 +624,8 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEn FILE_TYPE_FILE; fileInfo.lastModified = lastModifiedNanos(&entry.ftLastWriteTime); fileInfo.size = ((jlong)entry.nFileSizeHigh << 32) | entry.nFileSizeLow; + fileInfo.volumeSerialNumber = 0; + fileInfo.fileId = 0; } // Add entry @@ -630,6 +658,7 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirIs typedef struct fast_readdir_handle { HANDLE handle; wchar_t* pathStr; + ULONG volumeSerialNumber; } readdir_fast_handle_t; #endif @@ -676,6 +705,17 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirOp free(pathStr); return NULL; } + + // This call allows retrieving the volume ID of this directory (and all its entries) + BY_HANDLE_FILE_INFORMATION fileInfo; + BOOL ok = GetFileInformationByHandle(handle, &fileInfo); + if (!ok) { + mark_failed_with_errno(env, "could not open directory", result); + free(pathStr); + CloseHandle(handle); + return NULL; + } + readdir_fast_handle_t* readdirHandle = (readdir_fast_handle_t*)LocalAlloc(LPTR, sizeof(readdir_fast_handle_t)); if (readdirHandle == NULL) { mark_failed_with_code(env, "Out of native memory", ERROR_OUTOFMEMORY, NULL, result); @@ -685,6 +725,7 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirOp } readdirHandle->handle = handle; readdirHandle->pathStr = pathStr; + readdirHandle->volumeSerialNumber = fileInfo.dwVolumeSerialNumber; return (jlong)readdirHandle; #endif } @@ -704,6 +745,20 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirCl #endif } +// +// Returns the volume id of the directory opened by fastReaddirOpen +// +JNIEXPORT jint JNICALL +Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirGetVolumeId(JNIEnv *env, jclass target, jlong handle, jobject result) { +#ifdef WINDOWS_MIN + mark_failed_with_code(env, "Operation not supported", ERROR_CALL_NOT_IMPLEMENTED, NULL, result); + return 0; +#else + readdir_fast_handle_t* readdirHandle = (readdir_fast_handle_t*)handle; + return readdirHandle->volumeSerialNumber; +#endif +} + // // Reads the next batch of entries from the directory. // Returns JNI_TRUE on success and if there are more entries found diff --git a/src/main/java/net/rubygrapefruit/platform/file/FileInfo.java b/src/main/java/net/rubygrapefruit/platform/file/FileInfo.java index 115fe750..f30048e1 100644 --- a/src/main/java/net/rubygrapefruit/platform/file/FileInfo.java +++ b/src/main/java/net/rubygrapefruit/platform/file/FileInfo.java @@ -44,4 +44,12 @@ enum Type { * Returns the last modification time of this file, in ms since epoch. Returns 0 when this file does not exist. */ long getLastModifiedTime(); + + /** + * Returns an object that uniquely identifies the given file, or null if a file key is not available. + * + *
See BasicFileAttributes.fileKey() + * for a more in depth explanation.
+ */ + Object getKey(); } diff --git a/src/main/java/net/rubygrapefruit/platform/file/WindowsFileInfo.java b/src/main/java/net/rubygrapefruit/platform/file/WindowsFileInfo.java index fcbedfae..1c75b432 100644 --- a/src/main/java/net/rubygrapefruit/platform/file/WindowsFileInfo.java +++ b/src/main/java/net/rubygrapefruit/platform/file/WindowsFileInfo.java @@ -25,4 +25,13 @@ */ @ThreadSafe public interface WindowsFileInfo extends FileInfo { + /** + * Returns the volume ID (serial number) of the file. + */ + int getVolumeId(); + + /** + * Returns the file ID of the file, unique within the volume identified by {@link #getVolumeId()}. + */ + long getFileId(); } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/DefaultWindowsFiles.java b/src/main/java/net/rubygrapefruit/platform/internal/DefaultWindowsFiles.java index 6eaa3958..a396c26b 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/DefaultWindowsFiles.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/DefaultWindowsFiles.java @@ -86,6 +86,7 @@ private class FastLister implements DirectoryLister { static final int OFFSETOF_FILE_ATTRIBUTES = 56; static final int OFFSETOF_FILENAME_LENGTH = 60; static final int OFFSETOF_EA_SIZE = 64; + static final int OFFSETOF_FILE_ID = 72; static final int OFFSETOF_FILENAME = 80; /** @@ -109,6 +110,10 @@ public List extends DirEntry> listDir(File dir, boolean linkTarget) throws Nat if (result.isFailed()) { throw listDirFailure(dir, result); } + int volumeId = WindowsFileFunctions.fastReaddirGetVolumeId(handle, result); + if (result.isFailed()) { + throw listDirFailure(dir, result); + } try { NtQueryDirectoryFileContext context = getNtQueryDirectoryFileContext(); @@ -120,7 +125,7 @@ public List extends DirEntry> listDir(File dir, boolean linkTarget) throws Nat int entryOffset = 0; while (true) { // Read entry from buffer - entryOffset = addFullDirEntry(context, dir, linkTarget, entryOffset, dirList); + entryOffset = addFullDirEntry(context, dir, linkTarget, volumeId, entryOffset, dirList); // If we reached end of buffer, fetch next set of entries if (entryOffset == 0) { @@ -148,7 +153,7 @@ public List extends DirEntry> listDir(File dir, boolean linkTarget) throws Nat *Returns the byte offset of the next entry in {@link NtQueryDirectoryFileContext#buffer} if there is one, * or {@code 0} if there is no next entry.
*/ - private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boolean followLink, int entryOffset, WindowsDirList dirList) { + private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boolean followLink, int volumeId, int entryOffset, WindowsDirList dirList) { // typedef struct _FILE_ID_FULL_DIR_INFORMATION { // ULONG NextEntryOffset; // offset = 0 // ULONG FileIndex; // offset = 4 @@ -175,6 +180,7 @@ private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boole // int fileAttributes = context.buffer.getInt(entryOffset + OFFSETOF_FILE_ATTRIBUTES); int reparseTagData = context.buffer.getInt(entryOffset + OFFSETOF_EA_SIZE); + long fileId = context.buffer.getLong(entryOffset + OFFSETOF_FILE_ID); FileInfo.Type type = getFileType(fileAttributes, reparseTagData); @@ -189,7 +195,7 @@ private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boole WindowsFileInfo targetInfo = stat(new File(dir, fileName), true); dirList.addFile(fileName, targetInfo); } else { - dirList.addFile(fileName, type.ordinal(), fileSize, lastModified); + dirList.addFile(fileName, type, fileSize, WindowsFileTime.toJavaTime(lastModified), volumeId, fileId); } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/DirList.java b/src/main/java/net/rubygrapefruit/platform/internal/DirList.java index db48d80e..207bb19f 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/DirList.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/DirList.java @@ -28,11 +28,19 @@ public class DirList { // Called from native code @SuppressWarnings("UnusedDeclaration") public void addFile(String name, int type, long size, long lastModified) { - PosixDirEntry fileStat = new PosixDirEntry(name, FileInfo.Type.values()[type], size, lastModified); - files.add(fileStat); + addFile(name, FileInfo.Type.values()[type], size, lastModified); } - private class PosixDirEntry implements DirEntry { + void addFile(String name, FileInfo.Type type, long size, long lastModified) { + PosixDirEntry fileStat = new PosixDirEntry(name, type, size, lastModified); + addEntry(fileStat); + } + + void addEntry(DirEntry entry) { + files.add(entry); + } + + protected static class PosixDirEntry implements DirEntry { private final String name; private final Type type; private final long size; @@ -65,5 +73,9 @@ public long getLastModifiedTime() { public long getSize() { return size; } + + public Object getKey() { + return null; + } } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/FileStat.java b/src/main/java/net/rubygrapefruit/platform/internal/FileStat.java index 52ea3025..92fa1e1c 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/FileStat.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/FileStat.java @@ -74,4 +74,8 @@ public long getBlockSize() { public long getLastModifiedTime() { return modificationTime; } + + public Object getKey() { + return null; + } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/WindowsDirList.java b/src/main/java/net/rubygrapefruit/platform/internal/WindowsDirList.java index 0054c925..1727f537 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/WindowsDirList.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/WindowsDirList.java @@ -16,6 +16,7 @@ package net.rubygrapefruit.platform.internal; +import net.rubygrapefruit.platform.file.FileInfo; import net.rubygrapefruit.platform.file.WindowsFileInfo; public class WindowsDirList extends DirList { @@ -23,10 +24,46 @@ public class WindowsDirList extends DirList { @SuppressWarnings("UnusedDeclaration") @Override public void addFile(String name, int type, long size, long lastModified) { - super.addFile(name, type, size, WindowsFileTime.toJavaTime(lastModified)); + addFile(name, FileInfo.Type.values()[type], size, WindowsFileTime.toJavaTime(lastModified), 0, 0); } - public void addFile(String name, WindowsFileInfo fileInfo) { - super.addFile(name, fileInfo.getType().ordinal(), fileInfo.getSize(), fileInfo.getLastModifiedTime()); + void addFile(String name, WindowsFileInfo fileInfo) { + addFile(name, fileInfo.getType(), fileInfo.getSize(), fileInfo.getLastModifiedTime(), fileInfo.getVolumeId(), fileInfo.getFileId()); + } + + void addFile(String name, FileInfo.Type type, long size, long lastModified, int volumeId, long fileId) { + if (volumeId == 0 && fileId == 0) { + super.addFile(name, type, size, lastModified); + } else { + WindowsDirListEntry entry = new WindowsDirListEntry(name, type, size, lastModified, volumeId, fileId); + addEntry(entry); + } + } + + protected static class WindowsDirListEntry extends PosixDirEntry { + private final int volumeId; + private final long fileId; + // Lazily initialized to avoid extra allocation if not needed + private Object key; + + WindowsDirListEntry(String name, Type type, long size, long lastModified, int volumeId, long fileId) { + super(name, type, size, lastModified); + this.volumeId = volumeId; + this.fileId = fileId; + } + + public Object getKey() { + if (volumeId == 0 && fileId == 0) { + return null; + } + if (key == null) { + synchronized (this) { + if (key == null) { + key = new WindowsFileKey(volumeId, fileId); + } + } + } + return key; + } } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/WindowsFileKey.java b/src/main/java/net/rubygrapefruit/platform/internal/WindowsFileKey.java new file mode 100644 index 00000000..bce0d5c9 --- /dev/null +++ b/src/main/java/net/rubygrapefruit/platform/internal/WindowsFileKey.java @@ -0,0 +1,39 @@ +package net.rubygrapefruit.platform.internal; + +final class WindowsFileKey { + private final int volumeId; + private final long fileId; + + WindowsFileKey(int volumeId, long fileId) { + this.volumeId = volumeId; + this.fileId = fileId; + } + + @Override + public int hashCode() { + return (int)(volumeId * 31 + fileId); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (!(obj instanceof WindowsFileKey)) + return false; + + WindowsFileKey other = (WindowsFileKey) obj; + return (this.volumeId == other.volumeId) && + (this.fileId == other.fileId); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("(volumeId=") + .append(Integer.toHexString(volumeId)) + .append(",fileId=") + .append(fileId) + .append(')'); + return sb.toString(); + } +} diff --git a/src/main/java/net/rubygrapefruit/platform/internal/WindowsFileStat.java b/src/main/java/net/rubygrapefruit/platform/internal/WindowsFileStat.java index 1f83cc03..1434d8d6 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/WindowsFileStat.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/WindowsFileStat.java @@ -23,6 +23,10 @@ public class WindowsFileStat implements WindowsFileInfo { private Type type; private long size; private long lastModified; + private int volumeId; + private long fileId; + // Lazily initialized to avoid extra allocation if not needed + private Object key; public WindowsFileStat(String path) { this.path = path; @@ -34,6 +38,14 @@ public void details(int type, long size, long lastModifiedWinTime) { this.lastModified = this.type == Type.Missing ? 0 : WindowsFileTime.toJavaTime(lastModifiedWinTime); } + public void details(int type, long size, long lastModifiedWinTime, int volumeId, long fileId) { + this.type = Type.values()[type]; + this.size = size; + this.lastModified = this.type == Type.Missing ? 0 : WindowsFileTime.toJavaTime(lastModifiedWinTime); + this.volumeId = volumeId; + this.fileId = fileId; + } + @Override public String toString() { return path; @@ -50,4 +62,26 @@ public long getSize() { public long getLastModifiedTime() { return lastModified; } + + public int getVolumeId() { + return volumeId; + } + + public long getFileId() { + return fileId; + } + + public Object getKey() { + if (volumeId == 0 && fileId == 0) { + return null; + } + if (key == null) { + synchronized (this) { + if (key == null) { + key = new WindowsFileKey(volumeId, fileId); + } + } + } + return key; + } } diff --git a/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileFunctions.java b/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileFunctions.java index 1e75e99d..ea142ce5 100644 --- a/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileFunctions.java +++ b/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileFunctions.java @@ -31,5 +31,6 @@ public class WindowsFileFunctions { public static native boolean fastReaddirIsSupported(); public static native long fastReaddirOpen(String path, FunctionResult result); public static native void fastReaddirClose(long handle); + public static native int fastReaddirGetVolumeId(long handle, FunctionResult result); public static native boolean fastReaddirNext(long handle, ByteBuffer buffer, FunctionResult result); } diff --git a/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java b/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java index 991d8f1b..3a57ffcf 100755 --- a/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java +++ b/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java @@ -410,6 +410,9 @@ private static void ls(String path, boolean followLinks) { System.out.println(); System.out.println("* Name: " + entry.getName()); System.out.println("* Type: " + entry.getType()); + if (entry.getKey() != null) { + System.out.println("* Key: " + entry.getKey()); + } stat(new File(dir, entry.getName()), entry); } } @@ -498,6 +501,9 @@ private static void stat(String path, boolean linkTarget) { System.out.println(); System.out.println("* File: " + file); System.out.println("* Type: " + stat.getType()); + if (stat.getKey() != null) { + System.out.println("* Key: " + stat.getKey()); + } if (stat.getType() != FileInfo.Type.Missing) { if (stat instanceof PosixFileInfo) { stat(file, (PosixFileInfo) stat);