Skip to content

Commit

Permalink
Windows: Add support for returning file IDs from stat and readdir
Browse files Browse the repository at this point in the history
* Add `FileInfo.getKey()` that returns a file identifier than can be used to compare
  efficiently files for equality. If the key is not available, the method returns
  `null`.
* This is a similar concept as exposed in the Java 7 nio API
  https://docs.oracle.com/javase/7/docs/api/java/nio/file/attribute/BasicFileAttributes.html#fileKey()
* A WindowsFileKey is a pair of (volume serial number, file id on the volume)
* Update the `stat` and `fastReaddirXxx` jni entry points to provide this information
* Update the test app to display `file key` if it is available
  • Loading branch information
rpaquay committed Aug 7, 2019
1 parent 3230537 commit 3720c9a
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 9 deletions.
4 changes: 4 additions & 0 deletions src/main/cpp/posix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ void unpackStat(struct stat* source, file_stat_t* result) {
#else
result->lastModified = toMillis(source->st_mtimespec);
#endif
result->volumeId = 0;
result->fileId = 0;
}

JNIEXPORT void JNICALL
Expand Down Expand Up @@ -207,6 +209,8 @@ Java_net_rubygrapefruit_platform_internal_jni_PosixFileFunctions_readdir(JNIEnv
fileResult.fileType = FILE_TYPE_MISSING;
fileResult.size = 0;
fileResult.lastModified = 0;
fileResult.volumeId = 0;
fileResult.fileId = 0;
} else {
unpackStat(&fileInfo, &fileResult);
}
Expand Down
51 changes: 51 additions & 0 deletions src/main/cpp/win.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,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->volumeId = 0;
pFileStat->fileId = 0;
return ERROR_SUCCESS;
}
return error;
}
pFileStat->lastModified = lastModifiedNanos(&attr.ftLastWriteTime);
pFileStat->volumeId = 0;
pFileStat->fileId = 0;
if (attr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
pFileStat->size = 0;
pFileStat->fileType = FILE_TYPE_DIRECTORY;
Expand Down Expand Up @@ -223,6 +227,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->volumeId = 0;
pFileStat->fileId = 0;
return ERROR_SUCCESS;
}
return error;
Expand Down Expand Up @@ -250,6 +256,8 @@ DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileSta

pFileStat->lastModified = lastModifiedNanos(&fileInfo.ftLastWriteTime);
pFileStat->size = 0;
pFileStat->volumeId = 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) {
Expand Down Expand Up @@ -524,12 +532,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;
Expand All @@ -539,7 +556,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.volumeId, fileStat.fileId);
#endif
}

JNIEXPORT void JNICALL
Expand Down Expand Up @@ -587,6 +609,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.volumeId = 0;
fileInfo.fileId = 0;
}

// Add entry
Expand Down Expand Up @@ -619,6 +643,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

Expand Down Expand Up @@ -666,6 +691,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);
Expand All @@ -675,6 +711,7 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirOp
}
readdirHandle->handle = handle;
readdirHandle->pathStr = pathStr;
readdirHandle->volumeSerialNumber = fileInfo.dwVolumeSerialNumber;
return (jlong)readdirHandle;
#endif
}
Expand All @@ -694,6 +731,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
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/rubygrapefruit/platform/file/FileInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>See <a href="https://docs.oracle.com/javase/7/docs/api/java/nio/file/attribute/BasicFileAttributes.html#fileKey()">BasicFileAttributes.fileKey()</a>
* for a more in depth explanation.</p>
*/
Object getKey();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ private class FastLister implements DirectoryLister {
private static final int OFFSETOF_FILE_ATTRIBUTES = 56;
private static final int OFFSETOF_FILENAME_LENGTH = 60;
private static final int OFFSETOF_EA_SIZE = 64;
private static final int OFFSETOF_FILE_ID = 72;
private static final int OFFSETOF_FILENAME = 80;

/**
Expand All @@ -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();

Expand All @@ -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) {
Expand Down Expand Up @@ -148,7 +153,7 @@ public List<? extends DirEntry> listDir(File dir, boolean linkTarget) throws Nat
* <p>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.</p>
*/
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
Expand All @@ -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);

Expand All @@ -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);
}
}

Expand Down
18 changes: 15 additions & 3 deletions src/main/java/net/rubygrapefruit/platform/internal/DirList.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ public class DirList {
// Called from native code
@SuppressWarnings("UnusedDeclaration")
public void addFile(String name, int type, long size, long lastModified) {
DefaultDirEntry fileStat = new DefaultDirEntry(name, FileInfo.Type.values()[type], size, lastModified);
files.add(fileStat);
addFile(name, FileInfo.Type.values()[type], size, lastModified);
}

private static class DefaultDirEntry implements DirEntry {
void addFile(String name, FileInfo.Type type, long size, long lastModified) {
DefaultDirEntry fileStat = new DefaultDirEntry(name, type, size, lastModified);
addEntry(fileStat);
}

void addEntry(DirEntry entry) {
files.add(entry);
}

protected static class DefaultDirEntry implements DirEntry {
private final String name;
private final Type type;
private final long size;
Expand Down Expand Up @@ -65,5 +73,9 @@ public long getLastModifiedTime() {
public long getSize() {
return size;
}

public Object getKey() {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,8 @@ public long getBlockSize() {
public long getLastModifiedTime() {
return modificationTime;
}

public Object getKey() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,54 @@

package net.rubygrapefruit.platform.internal;

import net.rubygrapefruit.platform.file.FileInfo;
import net.rubygrapefruit.platform.file.WindowsFileInfo;

public class WindowsDirList extends DirList {
// Called from native code
@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 DefaultDirEntry {
private final int volumeId;
private final long fileId;
// Lazily initialized to avoid extra allocation if not needed
private volatile WindowsFileKey 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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(Long.toHexString(fileId))
.append(')');
return sb.toString();
}
}
Loading

0 comments on commit 3720c9a

Please sign in to comment.