Skip to content

Commit f3ec80d

Browse files
committed
Windows: Add support for SYMLINKS
* Both `stat` and `readdir` entry points support returning FILE_TYPE_SYMLINK * Both `stat` and `readdir` entry points support the "followLink" parameter * Add a `stat-L` option in the test app (`stat` with `followLink` option) * Add a `ls-L` option in test app (`ls` with `followLink` option) * Requires Windows Vista/Windows Server 2008 or later
1 parent 6a9308e commit f3ec80d

File tree

5 files changed

+216
-42
lines changed

5 files changed

+216
-42
lines changed

src/main/cpp/win.cpp

Lines changed: 170 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,19 @@ wchar_t* add_prefix(wchar_t* path, int path_len, wchar_t* prefix) {
8787
int str_len = path_len + prefix_len;
8888
wchar_t* str = (wchar_t*)malloc(sizeof(wchar_t) * (str_len + 1));
8989
wcscpy_s(str, str_len + 1, prefix);
90-
wcscat_s(str, str_len + 1, path);
90+
wcsncat_s(str, str_len + 1, path, path_len);
91+
return str;
92+
}
93+
94+
//
95+
// Returns a UTF-16 string that is the concatenation of |path| and |suffix|.
96+
//
97+
wchar_t* add_suffix(wchar_t* path, int path_len, wchar_t* suffix) {
98+
int suffix_len = wcslen(suffix);
99+
int str_len = path_len + suffix_len;
100+
wchar_t* str = (wchar_t*)malloc(sizeof(wchar_t) * (str_len + 1));
101+
wcsncpy_s(str, str_len + 1, path, path_len);
102+
wcscat_s(str, str_len + 1, suffix);
91103
return str;
92104
}
93105

@@ -127,6 +139,125 @@ wchar_t* java_to_wchar_path(JNIEnv *env, jstring string, jobject result) {
127139
}
128140
}
129141

142+
//
143+
// Returns 'true' if a file, given its attributes, is a Windows Symbolic Link.
144+
//
145+
bool is_file_symlink(DWORD dwFileAttributes, DWORD reparseTagData) {
146+
//
147+
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/reparse-point-tags
148+
// IO_REPARSE_TAG_SYMLINK (0xA000000C)
149+
//
150+
return
151+
((dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT) &&
152+
(reparseTagData == IO_REPARSE_TAG_SYMLINK);
153+
}
154+
155+
jlong lastModifiedNanos(FILETIME* time) {
156+
return ((jlong)time->dwHighDateTime << 32) | time->dwLowDateTime;
157+
}
158+
159+
jlong lastModifiedNanos(LARGE_INTEGER* time) {
160+
return ((jlong)time->HighPart << 32) | time->LowPart;
161+
}
162+
163+
typedef struct file_stat {
164+
int fileType;
165+
LONG64 lastModified;
166+
LONG64 size;
167+
} file_stat_t;
168+
169+
//
170+
// Retrieves the file attributes for the file specified by |pathStr|.
171+
// If |followLink| is true, symbolic link targets are resolved.
172+
//
173+
// * Returns ERROR_SUCCESS if the file exists and file attributes can be retrieved,
174+
// * Returns ERROR_SUCCESS with a FILE_TYPE_MISSING if the file does not exist,
175+
// * Returns a Win32 error code in all other cases.
176+
//
177+
DWORD get_file_stat(wchar_t* pathStr, jboolean followLink, file_stat_t* pFileStat) {
178+
#ifdef WINDOWS_MIN
179+
WIN32_FILE_ATTRIBUTE_DATA attr;
180+
BOOL ok = GetFileAttributesExW(pathStr, GetFileExInfoStandard, &attr);
181+
if (!ok) {
182+
DWORD error = GetLastError();
183+
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND || error == ERROR_NOT_READY) {
184+
// Treat device with no media as missing
185+
pFileStat->lastModified = 0;
186+
pFileStat->size = 0;
187+
pFileStat->fileType = FILE_TYPE_MISSING;
188+
return ERROR_SUCCESS;
189+
}
190+
return error;
191+
}
192+
pFileStat->lastModified = lastModifiedNanos(&attr.ftLastWriteTime);
193+
if (attr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
194+
pFileStat->size = 0;
195+
pFileStat->fileType = FILE_TYPE_DIRECTORY;
196+
} else {
197+
pFileStat->size = ((LONG64)attr.nFileSizeHigh << 32) | attr.nFileSizeLow;
198+
pFileStat->fileType = FILE_TYPE_FILE;
199+
}
200+
return ERROR_SUCCESS;
201+
#else //WINDOWS_MIN: Windows Vista+ support for symlinks
202+
DWORD dwFlagsAndAttributes = FILE_FLAG_BACKUP_SEMANTICS;
203+
if (!followLink) {
204+
dwFlagsAndAttributes |= FILE_FLAG_OPEN_REPARSE_POINT;
205+
}
206+
HANDLE fileHandle = CreateFileW(
207+
pathStr, // lpFileName
208+
GENERIC_READ, // dwDesiredAccess
209+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // dwShareMode
210+
NULL, // lpSecurityAttributes
211+
OPEN_EXISTING, // dwCreationDisposition
212+
dwFlagsAndAttributes, // dwFlagsAndAttributes
213+
NULL // hTemplateFile
214+
);
215+
if (fileHandle == INVALID_HANDLE_VALUE) {
216+
DWORD error = GetLastError();
217+
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND || error == ERROR_NOT_READY) {
218+
// Treat device with no media as missing
219+
pFileStat->lastModified = 0;
220+
pFileStat->size = 0;
221+
pFileStat->fileType = FILE_TYPE_MISSING;
222+
return ERROR_SUCCESS;
223+
}
224+
return error;
225+
}
226+
227+
// This call allows retrieving almost everything except for the reparseTag
228+
BY_HANDLE_FILE_INFORMATION fileInfo;
229+
BOOL ok = GetFileInformationByHandle(fileHandle, &fileInfo);
230+
if (!ok) {
231+
DWORD error = GetLastError();
232+
CloseHandle(fileHandle);
233+
return error;
234+
}
235+
236+
// This call allows retrieving the reparse tag
237+
FILE_ATTRIBUTE_TAG_INFO fileTagInfo;
238+
ok = GetFileInformationByHandleEx(fileHandle, FileAttributeTagInfo, &fileTagInfo, sizeof(fileTagInfo));
239+
if (!ok) {
240+
DWORD error = GetLastError();
241+
CloseHandle(fileHandle);
242+
return error;
243+
}
244+
245+
CloseHandle(fileHandle);
246+
247+
pFileStat->lastModified = lastModifiedNanos(&fileInfo.ftLastWriteTime);
248+
pFileStat->size = 0;
249+
if (is_file_symlink(fileTagInfo.FileAttributes, fileTagInfo.ReparseTag)) {
250+
pFileStat->fileType = FILE_TYPE_SYMLINK;
251+
} else if (fileTagInfo.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
252+
pFileStat->fileType = FILE_TYPE_DIRECTORY;
253+
} else {
254+
pFileStat->size = ((LONG64)fileInfo.nFileSizeHigh << 32) | fileInfo.nFileSizeLow;
255+
pFileStat->fileType = FILE_TYPE_FILE;
256+
}
257+
return ERROR_SUCCESS;
258+
#endif
259+
}
260+
130261
JNIEXPORT void JNICALL
131262
Java_net_rubygrapefruit_platform_internal_jni_NativeLibraryFunctions_getSystemInfo(JNIEnv *env, jclass target, jobject info, jobject result) {
132263
jclass infoClass = env->GetObjectClass(info);
@@ -387,44 +518,28 @@ Java_net_rubygrapefruit_platform_internal_jni_FileEventFunctions_closeWatch(JNIE
387518
free(details);
388519
}
389520

390-
jlong lastModifiedNanos(FILETIME* time) {
391-
return ((jlong)time->dwHighDateTime << 32) | time->dwLowDateTime;
392-
}
393-
394521
JNIEXPORT void JNICALL
395-
Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_stat(JNIEnv *env, jclass target, jstring path, jobject dest, jobject result) {
522+
Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_stat(JNIEnv *env, jclass target, jstring path, jboolean followLink, jobject dest, jobject result) {
396523
jclass destClass = env->GetObjectClass(dest);
397524
jmethodID mid = env->GetMethodID(destClass, "details", "(IJJ)V");
398525
if (mid == NULL) {
399526
mark_failed_with_message(env, "could not find method", result);
400527
return;
401528
}
402529

403-
WIN32_FILE_ATTRIBUTE_DATA attr;
404530
wchar_t* pathStr = java_to_wchar_path(env, path, result);
405-
BOOL ok = GetFileAttributesExW(pathStr, GetFileExInfoStandard, &attr);
531+
file_stat_t fileStat;
532+
DWORD errorCode = get_file_stat(pathStr, followLink, &fileStat);
406533
free(pathStr);
407-
if (!ok) {
408-
DWORD error = GetLastError();
409-
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND || error == ERROR_NOT_READY) {
410-
// Treat device with no media as missing
411-
env->CallVoidMethod(dest, mid, (jint)FILE_TYPE_MISSING, (jlong)0, (jlong)0);
412-
return;
413-
}
414-
mark_failed_with_errno(env, "could not file attributes", result);
534+
if (errorCode != ERROR_SUCCESS) {
535+
mark_failed_with_code(env, "could not file attributes", errorCode, NULL, result);
415536
return;
416537
}
417-
jlong lastModified = lastModifiedNanos(&attr.ftLastWriteTime);
418-
if (attr.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
419-
env->CallVoidMethod(dest, mid, (jint)FILE_TYPE_DIRECTORY, (jlong)0, lastModified);
420-
} else {
421-
jlong size = ((jlong)attr.nFileSizeHigh << 32) | attr.nFileSizeLow;
422-
env->CallVoidMethod(dest, mid, (jint)FILE_TYPE_FILE, size, lastModified);
423-
}
538+
env->CallVoidMethod(dest, mid, fileStat.fileType, fileStat.size, fileStat.lastModified);
424539
}
425540

426541
JNIEXPORT void JNICALL
427-
Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEnv *env, jclass target, jstring path, jobject contents, jobject result) {
542+
Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEnv *env, jclass target, jstring path, jboolean followLink, jobject contents, jobject result) {
428543
jclass contentsClass = env->GetObjectClass(contents);
429544
jmethodID mid = env->GetMethodID(contentsClass, "addFile", "(Ljava/lang/String;IJJ)V");
430545
if (mid == NULL) {
@@ -434,29 +549,55 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEn
434549

435550
WIN32_FIND_DATAW entry;
436551
wchar_t* pathStr = java_to_wchar_path(env, path, result);
437-
HANDLE dirHandle = FindFirstFileW(pathStr, &entry);
552+
wchar_t* patternStr = add_suffix(pathStr, wcslen(pathStr), L"\\*");
438553
free(pathStr);
554+
HANDLE dirHandle = FindFirstFileW(patternStr, &entry);
439555
if (dirHandle == INVALID_HANDLE_VALUE) {
440556
mark_failed_with_errno(env, "could not open directory", result);
557+
free(patternStr);
441558
return;
442559
}
443560

444561
do {
445562
if (wcscmp(L".", entry.cFileName) == 0 || wcscmp(L"..", entry.cFileName) == 0) {
446563
continue;
447564
}
565+
566+
// If entry is a symbolic link, we may have to get the attributes of the link target
567+
bool isSymLink = is_file_symlink(entry.dwFileAttributes, entry.dwReserved0);
568+
file_stat_t fileInfo;
569+
if (isSymLink && followLink) {
570+
// We use patternStr minus the last character ("*") to create the absolute path of the child entry
571+
wchar_t* childPathStr = add_suffix(patternStr, wcslen(patternStr) - 1, entry.cFileName);
572+
DWORD errorCode = get_file_stat(childPathStr, true, &fileInfo);
573+
free(childPathStr);
574+
if (errorCode != ERROR_SUCCESS) {
575+
// If we can't dereference the symbolic link, create a "missing file" entry
576+
fileInfo.fileType = FILE_TYPE_MISSING;
577+
fileInfo.size = 0;
578+
fileInfo.lastModified = 0;
579+
}
580+
} else {
581+
fileInfo.fileType = isSymLink ?
582+
FILE_TYPE_SYMLINK :
583+
(entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ?
584+
FILE_TYPE_DIRECTORY :
585+
FILE_TYPE_FILE;
586+
fileInfo.lastModified = lastModifiedNanos(&entry.ftLastWriteTime);
587+
fileInfo.size = ((jlong)entry.nFileSizeHigh << 32) | entry.nFileSizeLow;
588+
}
589+
590+
// Add entry
448591
jstring childName = wchar_to_java(env, entry.cFileName, wcslen(entry.cFileName), result);
449-
jint type = (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_TYPE_DIRECTORY : FILE_TYPE_FILE;
450-
jlong lastModified = lastModifiedNanos(&entry.ftLastWriteTime);
451-
jlong size = ((jlong)entry.nFileSizeHigh << 32) | entry.nFileSizeLow;
452-
env->CallVoidMethod(contents, mid, childName, type, size, lastModified);
592+
env->CallVoidMethod(contents, mid, childName, fileInfo.fileType, fileInfo.size, fileInfo.lastModified);
453593
} while (FindNextFileW(dirHandle, &entry) != 0);
454594

455595
DWORD error = GetLastError();
456596
if (error != ERROR_NO_MORE_FILES ) {
457597
mark_failed_with_errno(env, "could not read next directory entry", result);
458598
}
459599

600+
free(patternStr);
460601
FindClose(dirHandle);
461602
}
462603

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ public interface WindowsFiles extends Files, NativeIntegration {
2929
* {@inheritDoc}
3030
*/
3131
WindowsFileInfo stat(File file) throws NativeException;
32+
33+
/**
34+
* {@inheritDoc}
35+
*/
36+
WindowsFileInfo stat(File file, boolean linkTarget) throws NativeException;
3237
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,30 @@
2828

2929
public class DefaultWindowsFiles extends AbstractFiles implements WindowsFiles {
3030
public WindowsFileInfo stat(File file) throws NativeException {
31+
return stat(file, false);
32+
}
33+
34+
public WindowsFileInfo stat(File file, boolean linkTarget) throws NativeException {
3135
FunctionResult result = new FunctionResult();
3236
WindowsFileStat stat = new WindowsFileStat(file.getPath());
33-
WindowsFileFunctions.stat(file.getPath(), stat, result);
37+
WindowsFileFunctions.stat(file.getPath(), linkTarget, stat, result);
3438
if (result.isFailed()) {
3539
throw new NativeException(String.format("Could not get file details of %s: %s", file, result.getMessage()));
3640
}
3741
return stat;
3842
}
3943

40-
public FileInfo stat(File file, boolean linkTarget) throws NativeException {
41-
return stat(file);
42-
}
43-
44-
public List<? extends DirEntry> listDir(File dir) throws NativeException {
44+
public List<? extends DirEntry> listDir(File dir, boolean linkTarget) throws NativeException {
4545
FunctionResult result = new FunctionResult();
4646
WindowsDirList dirList = new WindowsDirList();
47-
WindowsFileFunctions.readdir(dir.getPath() + "\\*", dirList, result);
47+
WindowsFileFunctions.readdir(dir.getPath(), linkTarget, dirList, result);
4848
if (result.isFailed()) {
4949
throw listDirFailure(dir, result);
5050
}
5151
return dirList.files;
5252
}
5353

54-
public List<? extends DirEntry> listDir(File dir, boolean linkTarget) throws NativeException {
55-
return listDir(dir);
54+
public List<? extends DirEntry> listDir(File dir) throws NativeException {
55+
return listDir(dir, false);
5656
}
5757
}

src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileFunctions.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import net.rubygrapefruit.platform.internal.WindowsFileStat;
2222

2323
public class WindowsFileFunctions {
24-
public static native void stat(String file, WindowsFileStat stat, FunctionResult result);
24+
public static native void stat(String file, boolean followLink, WindowsFileStat stat, FunctionResult result);
2525

26-
public static native void readdir(String path, DirList dirList, FunctionResult result);
26+
public static native void readdir(String path, boolean followLink, DirList dirList, FunctionResult result);
2727
}

test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public static void main(String[] args) throws IOException {
4141
optionParser.accepts("cache-dir", "The directory to cache native libraries in").withRequiredArg();
4242
optionParser.accepts("ansi", "Force the use of ANSI escape sequences for terminal output");
4343
optionParser.accepts("stat", "Display details about the specified file or directory").withRequiredArg();
44+
optionParser.accepts("stat-L", "Display details about the specified file or directory, following symbolic links").withRequiredArg();
4445
optionParser.accepts("ls", "Display contents of the specified directory").withRequiredArg();
46+
optionParser.accepts("ls-L", "Display contents of the specified directory, following symbolic links").withRequiredArg();
4547
optionParser.accepts("long-paths", "Test support for long (i.e. >= 260 characters) paths");
4648
optionParser.accepts("watch", "Watches for changes to the specified file or directory").withRequiredArg();
4749
optionParser.accepts("machine", "Display details about the current machine");
@@ -70,11 +72,21 @@ public static void main(String[] args) throws IOException {
7072
return;
7173
}
7274

75+
if (result.has("stat-L")) {
76+
statFollowLinks((String) result.valueOf("stat-L"));
77+
return;
78+
}
79+
7380
if (result.has("ls")) {
7481
ls((String) result.valueOf("ls"));
7582
return;
7683
}
7784

85+
if (result.has("ls-L")) {
86+
lsFollowLinks((String) result.valueOf("ls-L"));
87+
return;
88+
}
89+
7890
if (result.has("long-paths")) {
7991
longPaths();
8092
return;
@@ -382,10 +394,18 @@ public void run() {
382394
}
383395

384396
private static void ls(String path) {
397+
ls(path, false);
398+
}
399+
400+
private static void lsFollowLinks(String path) {
401+
ls(path, true);
402+
}
403+
404+
private static void ls(String path, boolean followLinks) {
385405
File dir = new File(path);
386406

387407
Files files = Native.get(Files.class);
388-
List<? extends DirEntry> entries = files.listDir(dir);
408+
List<? extends DirEntry> entries = files.listDir(dir, followLinks);
389409
for (DirEntry entry : entries) {
390410
System.out.println();
391411
System.out.println("* Name: " + entry.getName());
@@ -463,10 +483,18 @@ private static File createTempDirectory() throws IOException {
463483
}
464484

465485
private static void stat(String path) {
486+
stat(path, false);
487+
}
488+
489+
private static void statFollowLinks(String path) {
490+
stat(path, true);
491+
}
492+
493+
private static void stat(String path, boolean linkTarget) {
466494
File file = new File(path);
467495

468496
Files files = Native.get(Files.class);
469-
FileInfo stat = files.stat(file);
497+
FileInfo stat = files.stat(file, linkTarget);
470498
System.out.println();
471499
System.out.println("* File: " + file);
472500
System.out.println("* Type: " + stat.getType());

0 commit comments

Comments
 (0)