Skip to content

Commit 6a9308e

Browse files
committed
Windows: Add support for paths longer than 260 characters
* For local paths, we use the `\\?\` prefix * For UNC paths, we use the `\\?\UNC\` prefix * Added a `--long-paths` option to the test app to verify behavior
1 parent d428211 commit 6a9308e

File tree

2 files changed

+147
-3
lines changed
  • src/main/cpp
  • test-app/src/main/java/net/rubygrapefruit/platform/test

2 files changed

+147
-3
lines changed

src/main/cpp/win.cpp

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,76 @@ wchar_t* java_to_wchar(JNIEnv *env, jstring string, jobject result) {
5757
return str;
5858
}
5959

60+
//
61+
// Returns 'true' if the path of the form "X:\", where 'X' is a drive letter.
62+
//
63+
bool is_path_absolute_local(wchar_t* path, int path_len) {
64+
if (path_len < 3) {
65+
return false;
66+
}
67+
return (('a' <= path[0] && path[0] <= 'z') || ('A' <= path[0] && path[0] <= 'Z')) &&
68+
path[1] == ':' &&
69+
path[2] == '\\';
70+
}
71+
72+
//
73+
// Returns 'true' if the path is of the form "\\server\share", i.e. is a UNC path.
74+
//
75+
bool is_path_absolute_unc(wchar_t* path, int path_len) {
76+
if (path_len < 3) {
77+
return false;
78+
}
79+
return path[0] == '\\' && path[1] == '\\';
80+
}
81+
82+
//
83+
// Returns a UTF-16 string that is the concatenation of |prefix| and |path|.
84+
//
85+
wchar_t* add_prefix(wchar_t* path, int path_len, wchar_t* prefix) {
86+
int prefix_len = wcslen(prefix);
87+
int str_len = path_len + prefix_len;
88+
wchar_t* str = (wchar_t*)malloc(sizeof(wchar_t) * (str_len + 1));
89+
wcscpy_s(str, str_len + 1, prefix);
90+
wcscat_s(str, str_len + 1, path);
91+
return str;
92+
}
93+
94+
//
95+
// Converts a Java string to a UNICODE path, including the Long Path prefix ("\\?\")
96+
// so that the resulting path supports paths longer than MAX_PATH (260 characters)
97+
//
98+
wchar_t* java_to_wchar_path(JNIEnv *env, jstring string, jobject result) {
99+
// Copy the Java string into a UTF-16 string.
100+
jsize len = env->GetStringLength(string);
101+
wchar_t* str = (wchar_t*)malloc(sizeof(wchar_t) * (len+1));
102+
env->GetStringRegion(string, 0, len, (jchar*)str);
103+
str[len] = L'\0';
104+
105+
// Technically, this should be MAX_PATH (i.e. 260), except some Win32 API related
106+
// to working with directory paths are actually limited to 240. It is just
107+
// safer/simpler to cover both cases in one code path.
108+
if (len <= 240) {
109+
return str;
110+
}
111+
112+
if (is_path_absolute_local(str, len)) {
113+
// Format: C:\... -> \\?\C:\...
114+
wchar_t* str2 = add_prefix(str, len, L"\\\\\?\\");
115+
free(str);
116+
return str2;
117+
} else if (is_path_absolute_unc(str, len)) {
118+
// In this case, we need to skip the first 2 characters:
119+
// Format: \\server\share\... -> \\?\UNC\server\share\...
120+
wchar_t* str2 = add_prefix(&str[2], len - 2, L"\\\\?\\UNC\\");
121+
free(str);
122+
return str2;
123+
}
124+
else {
125+
// It is some sort of unknown format, don't mess with it
126+
return str;
127+
}
128+
}
129+
60130
JNIEXPORT void JNICALL
61131
Java_net_rubygrapefruit_platform_internal_jni_NativeLibraryFunctions_getSystemInfo(JNIEnv *env, jclass target, jobject info, jobject result) {
62132
jclass infoClass = env->GetObjectClass(info);
@@ -280,7 +350,7 @@ typedef struct watch_details {
280350

281351
JNIEXPORT jobject JNICALL
282352
Java_net_rubygrapefruit_platform_internal_jni_FileEventFunctions_createWatch(JNIEnv *env, jclass target, jstring path, jobject result) {
283-
wchar_t* pathStr = java_to_wchar(env, path, result);
353+
wchar_t* pathStr = java_to_wchar_path(env, path, result);
284354
HANDLE h = FindFirstChangeNotificationW(pathStr, FALSE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE);
285355
free(pathStr);
286356
if (h == INVALID_HANDLE_VALUE) {
@@ -331,7 +401,7 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_stat(JNIEnv *
331401
}
332402

333403
WIN32_FILE_ATTRIBUTE_DATA attr;
334-
wchar_t* pathStr = java_to_wchar(env, path, result);
404+
wchar_t* pathStr = java_to_wchar_path(env, path, result);
335405
BOOL ok = GetFileAttributesExW(pathStr, GetFileExInfoStandard, &attr);
336406
free(pathStr);
337407
if (!ok) {
@@ -363,7 +433,7 @@ Java_net_rubygrapefruit_platform_internal_jni_WindowsFileFunctions_readdir(JNIEn
363433
}
364434

365435
WIN32_FIND_DATAW entry;
366-
wchar_t* pathStr = java_to_wchar(env, path, result);
436+
wchar_t* pathStr = java_to_wchar_path(env, path, result);
367437
HANDLE dirHandle = FindFirstFileW(pathStr, &entry);
368438
free(pathStr);
369439
if (dirHandle == INVALID_HANDLE_VALUE) {

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public static void main(String[] args) throws IOException {
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();
4444
optionParser.accepts("ls", "Display contents of the specified directory").withRequiredArg();
45+
optionParser.accepts("long-paths", "Test support for long (i.e. >= 260 characters) paths");
4546
optionParser.accepts("watch", "Watches for changes to the specified file or directory").withRequiredArg();
4647
optionParser.accepts("machine", "Display details about the current machine");
4748
optionParser.accepts("terminal", "Display details about the terminal");
@@ -74,6 +75,11 @@ public static void main(String[] args) throws IOException {
7475
return;
7576
}
7677

78+
if (result.has("long-paths")) {
79+
longPaths();
80+
return;
81+
}
82+
7783
if (result.has("watch")) {
7884
watch((String) result.valueOf("watch"));
7985
return;
@@ -388,6 +394,74 @@ private static void ls(String path) {
388394
}
389395
}
390396

397+
private static void longPaths() {
398+
try {
399+
File rootDir = createTempDirectory();
400+
try {
401+
// Create a temporary directory with a long path
402+
File dir = rootDir;
403+
while (dir.toString().length() < 300) {
404+
dir = new File(dir, "somewhat-long-named-sub-directory");
405+
if (!dir.mkdir()) {
406+
throw new IOException(String.format("Error creating temporary directory \"%s\"", dir));
407+
}
408+
}
409+
System.out.println(String.format("Created directory with long (%d characters) path: \"%s\"", dir.toString().length(), dir));
410+
System.out.println();
411+
412+
// Create a couple of empty files in there
413+
File textFile1 = new File(dir, "foo.txt");
414+
textFile1.createNewFile();
415+
416+
File textFile2 = new File(dir, "foo2.txt");
417+
textFile2.createNewFile();
418+
419+
System.out.println("Created 2 empty files \"foo.txt\" and \"foo2.txt\" in directory");
420+
System.out.println();
421+
422+
// List the contents of the directory
423+
System.out.println("Checking \"ls\" function works as expected:");
424+
ls(dir.getAbsolutePath());
425+
System.out.println();
426+
427+
System.out.println("Checking \"stat\" function works as expected:");
428+
stat(textFile1.getAbsolutePath());
429+
430+
System.out.println("Success!");
431+
} finally {
432+
deleteDirectoryRecursive(rootDir);
433+
}
434+
} catch (IOException e) {
435+
throw new RuntimeException(e);
436+
}
437+
}
438+
439+
private static void deleteDirectoryRecursive(File file) throws IOException {
440+
if (file.isDirectory()) {
441+
File[] entries = file.listFiles();
442+
if (entries != null) {
443+
for (File entry : entries) {
444+
deleteDirectoryRecursive(entry);
445+
}
446+
}
447+
}
448+
if (!file.delete()) {
449+
throw new IOException(String.format("Failed to delete \"%s\"", file));
450+
}
451+
}
452+
453+
private static File createTempDirectory() throws IOException {
454+
File rootDir = File.createTempFile("native-platform-", ".tmp");
455+
if (!rootDir.delete()) {
456+
throw new IOException("Error creating temporary directory");
457+
}
458+
459+
if (!rootDir.mkdir()) {
460+
throw new IOException(String.format("Error creating temporary directory \"%s\"", rootDir));
461+
}
462+
return rootDir;
463+
}
464+
391465
private static void stat(String path) {
392466
File file = new File(path);
393467

0 commit comments

Comments
 (0)