Skip to content

Commit ac86e6b

Browse files
committed
Windows: Add support for returning file IDs from stat and readdir
* 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
1 parent 3324fdd commit ac86e6b

11 files changed

Lines changed: 220 additions & 9 deletions

File tree

src/main/cpp/win.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ typedef struct file_stat {
177177
LONG fileType;
178178
LONGLONG lastModified;
179179
LONGLONG size;
180+
LONG volumeSerialNumber;
181+
LONGLONG fileId;
180182
} file_stat_t;
181183

182184
//
@@ -198,11 +200,15 @@ DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileSta
198200
pFileStat->lastModified = 0;
199201
pFileStat->size = 0;
200202
pFileStat->fileType = FILE_TYPE_MISSING;
203+
pFileStat->volumeSerialNumber = 0;
204+
pFileStat->fileId = 0;
201205
return ERROR_SUCCESS;
202206
}
203207
return error;
204208
}
205209
pFileStat->lastModified = lastModifiedNanos(&attr.ftLastWriteTime);
210+
pFileStat->volumeSerialNumber = 0;
211+
pFileStat->fileId = 0;
206212
if (attr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
207213
pFileStat->size = 0;
208214
pFileStat->fileType = FILE_TYPE_DIRECTORY;
@@ -232,6 +238,8 @@ DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileSta
232238
pFileStat->lastModified = 0;
233239
pFileStat->size = 0;
234240
pFileStat->fileType = FILE_TYPE_MISSING;
241+
pFileStat->volumeSerialNumber = 0;
242+
pFileStat->fileId = 0;
235243
return ERROR_SUCCESS;
236244
}
237245
return error;
@@ -259,6 +267,8 @@ DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileSta
259267

260268
pFileStat->lastModified = lastModifiedNanos(&fileInfo.ftLastWriteTime);
261269
pFileStat->size = 0;
270+
pFileStat->volumeSerialNumber = fileInfo.dwVolumeSerialNumber;
271+
pFileStat->fileId = ((LONGLONG)fileInfo.nFileIndexHigh << 32) | fileInfo.nFileIndexLow;
262272
if (is_file_symlink(fileTagInfo.FileAttributes, fileTagInfo.ReparseTag)) {
263273
pFileStat->fileType = FILE_TYPE_SYMLINK;
264274
} else if (fileTagInfo.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
@@ -533,12 +543,21 @@ Java_net_rubygrapefruit_platform_internal_jni_FileEventFunctions_closeWatch(JNIE
533543

534544
JNIEXPORT void JNICALL
535545
Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_stat(JNIEnv *env, jclass target, jstring path, jboolean followLink, jobject dest, jobject result) {
546+
#ifdef WINDOWS_MIN
536547
jclass destClass = env->GetObjectClass(dest);
537548
jmethodID mid = env->GetMethodID(destClass, "details", "(IJJ)V");
538549
if (mid == NULL) {
539550
mark_failed_with_message(env, "could not find method", result);
540551
return;
541552
}
553+
#else
554+
jclass destClass = env->GetObjectClass(dest);
555+
jmethodID mid = env->GetMethodID(destClass, "details", "(IJJIJ)V");
556+
if (mid == NULL) {
557+
mark_failed_with_message(env, "could not find method", result);
558+
return;
559+
}
560+
#endif
542561

543562
wchar_t* pathStr = java_to_wchar_path(env, path, result);
544563
file_stat_t fileStat;
@@ -548,7 +567,12 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_stat(JNIEnv *
548567
mark_failed_with_code(env, "could not file attributes", errorCode, NULL, result);
549568
return;
550569
}
570+
571+
#ifdef WINDOWS_MIN
551572
env->CallVoidMethod(dest, mid, fileStat.fileType, fileStat.size, fileStat.lastModified);
573+
#else
574+
env->CallVoidMethod(dest, mid, fileStat.fileType, fileStat.size, fileStat.lastModified, fileStat.volumeSerialNumber, fileStat.fileId);
575+
#endif
552576
}
553577

554578
JNIEXPORT void JNICALL
@@ -589,6 +613,8 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEn
589613
fileInfo.fileType = FILE_TYPE_MISSING;
590614
fileInfo.size = 0;
591615
fileInfo.lastModified = 0;
616+
fileInfo.volumeSerialNumber = 0;
617+
fileInfo.fileId = 0;
592618
}
593619
} else {
594620
fileInfo.fileType = isSymLink ?
@@ -598,6 +624,8 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEn
598624
FILE_TYPE_FILE;
599625
fileInfo.lastModified = lastModifiedNanos(&entry.ftLastWriteTime);
600626
fileInfo.size = ((jlong)entry.nFileSizeHigh << 32) | entry.nFileSizeLow;
627+
fileInfo.volumeSerialNumber = 0;
628+
fileInfo.fileId = 0;
601629
}
602630

603631
// Add entry
@@ -630,6 +658,7 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirIs
630658
typedef struct fast_readdir_handle {
631659
HANDLE handle;
632660
wchar_t* pathStr;
661+
ULONG volumeSerialNumber;
633662
} readdir_fast_handle_t;
634663
#endif
635664

@@ -676,6 +705,17 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirOp
676705
free(pathStr);
677706
return NULL;
678707
}
708+
709+
// This call allows retrieving the volume ID of this directory (and all its entries)
710+
BY_HANDLE_FILE_INFORMATION fileInfo;
711+
BOOL ok = GetFileInformationByHandle(handle, &fileInfo);
712+
if (!ok) {
713+
mark_failed_with_errno(env, "could not open directory", result);
714+
free(pathStr);
715+
CloseHandle(handle);
716+
return NULL;
717+
}
718+
679719
readdir_fast_handle_t* readdirHandle = (readdir_fast_handle_t*)LocalAlloc(LPTR, sizeof(readdir_fast_handle_t));
680720
if (readdirHandle == NULL) {
681721
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
685725
}
686726
readdirHandle->handle = handle;
687727
readdirHandle->pathStr = pathStr;
728+
readdirHandle->volumeSerialNumber = fileInfo.dwVolumeSerialNumber;
688729
return (jlong)readdirHandle;
689730
#endif
690731
}
@@ -704,6 +745,20 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirCl
704745
#endif
705746
}
706747

748+
//
749+
// Returns the volume id of the directory opened by fastReaddirOpen
750+
//
751+
JNIEXPORT jint JNICALL
752+
Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_fastReaddirGetVolumeId(JNIEnv *env, jclass target, jlong handle, jobject result) {
753+
#ifdef WINDOWS_MIN
754+
mark_failed_with_code(env, "Operation not supported", ERROR_CALL_NOT_IMPLEMENTED, NULL, result);
755+
return 0;
756+
#else
757+
readdir_fast_handle_t* readdirHandle = (readdir_fast_handle_t*)handle;
758+
return readdirHandle->volumeSerialNumber;
759+
#endif
760+
}
761+
707762
//
708763
// Reads the next batch of entries from the directory.
709764
// Returns JNI_TRUE on success and if there are more entries found

src/main/java/net/rubygrapefruit/platform/file/FileInfo.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,12 @@ enum Type {
4444
* Returns the last modification time of this file, in ms since epoch. Returns 0 when this file does not exist.
4545
*/
4646
long getLastModifiedTime();
47+
48+
/**
49+
* Returns an object that uniquely identifies the given file, or null if a file key is not available.
50+
*
51+
* <p>See <a href="https://docs.oracle.com/javase/7/docs/api/java/nio/file/attribute/BasicFileAttributes.html#fileKey()">BasicFileAttributes.fileKey()</a>
52+
* for a more in depth explanation.</p>
53+
*/
54+
Object getKey();
4755
}

src/main/java/net/rubygrapefruit/platform/file/WindowsFileInfo.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,13 @@
2525
*/
2626
@ThreadSafe
2727
public interface WindowsFileInfo extends FileInfo {
28+
/**
29+
* Returns the volume ID (serial number) of the file.
30+
*/
31+
int getVolumeId();
32+
33+
/**
34+
* Returns the file ID of the file, unique within the volume identified by {@link #getVolumeId()}.
35+
*/
36+
long getFileId();
2837
}

src/main/java/net/rubygrapefruit/platform/internal/DefaultWindowsFiles.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ private class FastLister implements DirectoryLister {
8686
static final int OFFSETOF_FILE_ATTRIBUTES = 56;
8787
static final int OFFSETOF_FILENAME_LENGTH = 60;
8888
static final int OFFSETOF_EA_SIZE = 64;
89+
static final int OFFSETOF_FILE_ID = 72;
8990
static final int OFFSETOF_FILENAME = 80;
9091

9192
/**
@@ -109,6 +110,10 @@ public List<? extends DirEntry> listDir(File dir, boolean linkTarget) throws Nat
109110
if (result.isFailed()) {
110111
throw listDirFailure(dir, result);
111112
}
113+
int volumeId = WindowsFileFunctions.fastReaddirGetVolumeId(handle, result);
114+
if (result.isFailed()) {
115+
throw listDirFailure(dir, result);
116+
}
112117
try {
113118
NtQueryDirectoryFileContext context = getNtQueryDirectoryFileContext();
114119

@@ -120,7 +125,7 @@ public List<? extends DirEntry> listDir(File dir, boolean linkTarget) throws Nat
120125
int entryOffset = 0;
121126
while (true) {
122127
// Read entry from buffer
123-
entryOffset = addFullDirEntry(context, dir, linkTarget, entryOffset, dirList);
128+
entryOffset = addFullDirEntry(context, dir, linkTarget, volumeId, entryOffset, dirList);
124129

125130
// If we reached end of buffer, fetch next set of entries
126131
if (entryOffset == 0) {
@@ -148,7 +153,7 @@ public List<? extends DirEntry> listDir(File dir, boolean linkTarget) throws Nat
148153
* <p>Returns the byte offset of the next entry in {@link NtQueryDirectoryFileContext#buffer} if there is one,
149154
* or {@code 0} if there is no next entry.</p>
150155
*/
151-
private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boolean followLink, int entryOffset, WindowsDirList dirList) {
156+
private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boolean followLink, int volumeId, int entryOffset, WindowsDirList dirList) {
152157
// typedef struct _FILE_ID_FULL_DIR_INFORMATION {
153158
// ULONG NextEntryOffset; // offset = 0
154159
// ULONG FileIndex; // offset = 4
@@ -175,6 +180,7 @@ private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boole
175180
//
176181
int fileAttributes = context.buffer.getInt(entryOffset + OFFSETOF_FILE_ATTRIBUTES);
177182
int reparseTagData = context.buffer.getInt(entryOffset + OFFSETOF_EA_SIZE);
183+
long fileId = context.buffer.getLong(entryOffset + OFFSETOF_FILE_ID);
178184

179185
FileInfo.Type type = getFileType(fileAttributes, reparseTagData);
180186

@@ -189,7 +195,7 @@ private int addFullDirEntry(NtQueryDirectoryFileContext context, File dir, boole
189195
WindowsFileInfo targetInfo = stat(new File(dir, fileName), true);
190196
dirList.addFile(fileName, targetInfo);
191197
} else {
192-
dirList.addFile(fileName, type.ordinal(), fileSize, lastModified);
198+
dirList.addFile(fileName, type, fileSize, WindowsFileTime.toJavaTime(lastModified), volumeId, fileId);
193199
}
194200
}
195201

src/main/java/net/rubygrapefruit/platform/internal/DirList.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,19 @@ public class DirList {
2828
// Called from native code
2929
@SuppressWarnings("UnusedDeclaration")
3030
public void addFile(String name, int type, long size, long lastModified) {
31-
PosixDirEntry fileStat = new PosixDirEntry(name, FileInfo.Type.values()[type], size, lastModified);
32-
files.add(fileStat);
31+
addFile(name, FileInfo.Type.values()[type], size, lastModified);
3332
}
3433

35-
private class PosixDirEntry implements DirEntry {
34+
void addFile(String name, FileInfo.Type type, long size, long lastModified) {
35+
PosixDirEntry fileStat = new PosixDirEntry(name, type, size, lastModified);
36+
addEntry(fileStat);
37+
}
38+
39+
void addEntry(DirEntry entry) {
40+
files.add(entry);
41+
}
42+
43+
protected static class PosixDirEntry implements DirEntry {
3644
private final String name;
3745
private final Type type;
3846
private final long size;
@@ -65,5 +73,9 @@ public long getLastModifiedTime() {
6573
public long getSize() {
6674
return size;
6775
}
76+
77+
public Object getKey() {
78+
return null;
79+
}
6880
}
6981
}

src/main/java/net/rubygrapefruit/platform/internal/FileStat.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,8 @@ public long getBlockSize() {
7474
public long getLastModifiedTime() {
7575
return modificationTime;
7676
}
77+
78+
public Object getKey() {
79+
return null;
80+
}
7781
}

src/main/java/net/rubygrapefruit/platform/internal/WindowsDirList.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,54 @@
1616

1717
package net.rubygrapefruit.platform.internal;
1818

19+
import net.rubygrapefruit.platform.file.FileInfo;
1920
import net.rubygrapefruit.platform.file.WindowsFileInfo;
2021

2122
public class WindowsDirList extends DirList {
2223
// Called from native code
2324
@SuppressWarnings("UnusedDeclaration")
2425
@Override
2526
public void addFile(String name, int type, long size, long lastModified) {
26-
super.addFile(name, type, size, WindowsFileTime.toJavaTime(lastModified));
27+
addFile(name, FileInfo.Type.values()[type], size, WindowsFileTime.toJavaTime(lastModified), 0, 0);
2728
}
2829

29-
public void addFile(String name, WindowsFileInfo fileInfo) {
30-
super.addFile(name, fileInfo.getType().ordinal(), fileInfo.getSize(), fileInfo.getLastModifiedTime());
30+
void addFile(String name, WindowsFileInfo fileInfo) {
31+
addFile(name, fileInfo.getType(), fileInfo.getSize(), fileInfo.getLastModifiedTime(), fileInfo.getVolumeId(), fileInfo.getFileId());
32+
}
33+
34+
void addFile(String name, FileInfo.Type type, long size, long lastModified, int volumeId, long fileId) {
35+
if (volumeId == 0 && fileId == 0) {
36+
super.addFile(name, type, size, lastModified);
37+
} else {
38+
WindowsDirListEntry entry = new WindowsDirListEntry(name, type, size, lastModified, volumeId, fileId);
39+
addEntry(entry);
40+
}
41+
}
42+
43+
protected static class WindowsDirListEntry extends PosixDirEntry {
44+
private final int volumeId;
45+
private final long fileId;
46+
// Lazily initialized to avoid extra allocation if not needed
47+
private Object key;
48+
49+
WindowsDirListEntry(String name, Type type, long size, long lastModified, int volumeId, long fileId) {
50+
super(name, type, size, lastModified);
51+
this.volumeId = volumeId;
52+
this.fileId = fileId;
53+
}
54+
55+
public Object getKey() {
56+
if (volumeId == 0 && fileId == 0) {
57+
return null;
58+
}
59+
if (key == null) {
60+
synchronized (this) {
61+
if (key == null) {
62+
key = new WindowsFileKey(volumeId, fileId);
63+
}
64+
}
65+
}
66+
return key;
67+
}
3168
}
3269
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package net.rubygrapefruit.platform.internal;
2+
3+
final class WindowsFileKey {
4+
private final int volumeId;
5+
private final long fileId;
6+
7+
WindowsFileKey(int volumeId, long fileId) {
8+
this.volumeId = volumeId;
9+
this.fileId = fileId;
10+
}
11+
12+
@Override
13+
public int hashCode() {
14+
return (int)(volumeId * 31 + fileId);
15+
}
16+
17+
@Override
18+
public boolean equals(Object obj) {
19+
if (obj == this)
20+
return true;
21+
if (!(obj instanceof WindowsFileKey))
22+
return false;
23+
24+
WindowsFileKey other = (WindowsFileKey) obj;
25+
return (this.volumeId == other.volumeId) &&
26+
(this.fileId == other.fileId);
27+
}
28+
29+
@Override
30+
public String toString() {
31+
StringBuilder sb = new StringBuilder();
32+
sb.append("(volumeId=")
33+
.append(Integer.toHexString(volumeId))
34+
.append(",fileId=")
35+
.append(fileId)
36+
.append(')');
37+
return sb.toString();
38+
}
39+
}

0 commit comments

Comments
 (0)