diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b56ce544fa..15b48ba6334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,9 +130,14 @@ applications (issue [flutter/flutter#63038][]). * [Abstract Unix Domain Socket][] is supported on Linux/Android now. Using an `InternetAddress` with `address` starting with '@' and type being `InternetAddressType.Unix` will create an abstract Unix Domain Socket. +* On Windows, file APIs can now handle files and directories identified by + long paths (greater than 260 characters). It complies with all restrictions + from [Long Path on Windows][]. Note that `Directory.current` does not work + with long path. [#42006]: https://github.com/dart-lang/sdk/issues/42006 [Abstract Unix Domain Socket]: http://man7.org/linux/man-pages/man7/unix.7.html +[Long Path on Windows]: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation #### `dart:html` diff --git a/runtime/bin/builtin_impl_sources.gni b/runtime/bin/builtin_impl_sources.gni index b8ea4eb808e..58323d0f0cd 100644 --- a/runtime/bin/builtin_impl_sources.gni +++ b/runtime/bin/builtin_impl_sources.gni @@ -45,6 +45,7 @@ builtin_impl_sources = [ "file_macos.cc", "file_support.cc", "file_win.cc", + "file_win.h", "io_buffer.cc", "io_buffer.h", "isolate_data.cc", diff --git a/runtime/bin/directory_win.cc b/runtime/bin/directory_win.cc index ec968633191..16b09846da3 100644 --- a/runtime/bin/directory_win.cc +++ b/runtime/bin/directory_win.cc @@ -13,6 +13,7 @@ #include "bin/crypto.h" #include "bin/dartutils.h" #include "bin/file.h" +#include "bin/file_win.h" #include "bin/namespace.h" #include "bin/utils.h" #include "bin/utils_win.h" @@ -21,8 +22,6 @@ #undef DeleteFile -#define MAX_LONG_PATH 32767 - namespace dart { namespace bin { @@ -278,7 +277,12 @@ static bool DeleteEntry(LPWIN32_FIND_DATAW find_file_data, PathBuffer* path) { } static bool DeleteRecursively(PathBuffer* path) { - DWORD attributes = GetFileAttributesW(path->AsStringW()); + PathBuffer prefixed_path; + if (!prefixed_path.Add(PrefixLongDirectoryPath(path->AsScopedString()))) { + return false; + } + + DWORD attributes = GetFileAttributesW(prefixed_path.AsStringW()); if (attributes == INVALID_FILE_ATTRIBUTES) { return false; } @@ -286,33 +290,34 @@ static bool DeleteRecursively(PathBuffer* path) { // filesystem that we do not want to recurse into. if ((attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { // Just delete the junction itself. - return RemoveDirectoryW(path->AsStringW()) != 0; + return RemoveDirectoryW(prefixed_path.AsStringW()) != 0; } // If it's a file, remove it directly. if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { - return DeleteFile(L"", path); + return DeleteFile(L"", &prefixed_path); } - if (!path->AddW(L"\\*")) { + if (!prefixed_path.AddW(L"\\*")) { return false; } WIN32_FIND_DATAW find_file_data; - HANDLE find_handle = FindFirstFileW(path->AsStringW(), &find_file_data); + HANDLE find_handle = + FindFirstFileW(prefixed_path.AsStringW(), &find_file_data); if (find_handle == INVALID_HANDLE_VALUE) { return false; } // Adjust the path by removing the '*' used for the search. - int path_length = path->length() - 1; - path->Reset(path_length); + int path_length = prefixed_path.length() - 1; + prefixed_path.Reset(path_length); do { - if (!DeleteEntry(&find_file_data, path)) { + if (!DeleteEntry(&find_file_data, &prefixed_path)) { break; } - path->Reset(path_length); // DeleteEntry adds to the path. + prefixed_path.Reset(path_length); // DeleteEntry adds to the path. } while (FindNextFileW(find_handle, &find_file_data) != 0); DWORD last_error = GetLastError(); @@ -324,8 +329,9 @@ static bool DeleteRecursively(PathBuffer* path) { return false; } // All content deleted succesfully, try to delete directory. - path->Reset(path_length - 1); // Drop the "\" from the end of the path. - return RemoveDirectoryW(path->AsStringW()) != 0; + prefixed_path.Reset(path_length - + 1); // Drop the "\" from the end of the path. + return RemoveDirectoryW(prefixed_path.AsStringW()) != 0; } static Directory::ExistsResult ExistsHelper(const wchar_t* dir_name) { @@ -349,7 +355,8 @@ static Directory::ExistsResult ExistsHelper(const wchar_t* dir_name) { Directory::ExistsResult Directory::Exists(Namespace* namespc, const char* dir_name) { - Utf8ToWideScope system_name(dir_name); + const char* prefixed_dir_name = PrefixLongDirectoryPath(dir_name); + Utf8ToWideScope system_name(prefixed_dir_name); return ExistsHelper(system_name.wide()); } @@ -369,7 +376,8 @@ char* Directory::CurrentNoScope() { } bool Directory::Create(Namespace* namespc, const char* dir_name) { - Utf8ToWideScope system_name(dir_name); + const char* prefixed_dir_name = PrefixLongDirectoryPath(dir_name); + Utf8ToWideScope system_name(prefixed_dir_name); int create_status = CreateDirectoryW(system_name.wide(), NULL); // If the directory already existed, treat it as a success. if ((create_status == 0) && (GetLastError() == ERROR_ALREADY_EXISTS) && @@ -475,10 +483,11 @@ const char* Directory::CreateTemp(Namespace* namespc, const char* prefix) { bool Directory::Delete(Namespace* namespc, const char* dir_name, bool recursive) { + const char* prefixed_dir_name = PrefixLongDirectoryPath(dir_name); bool result = false; - Utf8ToWideScope system_dir_name(dir_name); + Utf8ToWideScope system_dir_name(prefixed_dir_name); if (!recursive) { - if (File::GetType(namespc, dir_name, true) == File::kIsDirectory) { + if (File::GetType(namespc, prefixed_dir_name, true) == File::kIsDirectory) { result = (RemoveDirectoryW(system_dir_name.wide()) != 0); } else { SetLastError(ERROR_FILE_NOT_FOUND); @@ -495,18 +504,20 @@ bool Directory::Delete(Namespace* namespc, bool Directory::Rename(Namespace* namespc, const char* path, const char* new_path) { - Utf8ToWideScope system_path(path); - Utf8ToWideScope system_new_path(new_path); + const char* prefixed_dir = PrefixLongDirectoryPath(path); + Utf8ToWideScope system_path(prefixed_dir); ExistsResult exists = ExistsHelper(system_path.wide()); if (exists != EXISTS) { return false; } + const char* prefixed_new_dir = PrefixLongDirectoryPath(new_path); + Utf8ToWideScope system_new_path(prefixed_new_dir); ExistsResult new_exists = ExistsHelper(system_new_path.wide()); // MoveFile does not allow replacing existing directories. Therefore, // if the new_path is currently a directory we need to delete it // first. if (new_exists == EXISTS) { - bool success = Delete(namespc, new_path, true); + bool success = Delete(namespc, prefixed_new_dir, true); if (!success) { return false; } diff --git a/runtime/bin/file_win.cc b/runtime/bin/file_win.cc index 21f8df23f4e..ee282840161 100644 --- a/runtime/bin/file_win.cc +++ b/runtime/bin/file_win.cc @@ -5,13 +5,11 @@ #include "platform/globals.h" #if defined(HOST_OS_WINDOWS) -#include "bin/file.h" - +#include // NOLINT #include // NOLINT #include // NOLINT #include // NOLINT -#include // NOLINT -#undef StrDup // defined in Shlwapi.h as StrDupW +#undef StrDup // defined in Shlwapi.h as StrDupW #include // NOLINT #include // NOLINT #include // NOLINT @@ -20,6 +18,8 @@ #include "bin/builtin.h" #include "bin/crypto.h" #include "bin/directory.h" +#include "bin/file.h" +#include "bin/file_win.h" #include "bin/namespace.h" #include "bin/utils.h" #include "bin/utils_win.h" @@ -299,7 +299,7 @@ File* File::FileOpenW(const wchar_t* system_name, FileOpenMode mode) { } File* File::Open(Namespace* namespc, const char* path, FileOpenMode mode) { - Utf8ToWideScope system_name(path); + Utf8ToWideScope system_name(PrefixLongFilePath(path)); File* file = FileOpenW(system_name.wide(), mode); return file; } @@ -365,7 +365,7 @@ static bool StatHelper(wchar_t* path, struct __stat64* st) { bool File::Exists(Namespace* namespc, const char* name) { struct __stat64 st; - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); return StatHelper(system_name.wide(), &st); } @@ -379,7 +379,7 @@ bool File::ExistsUri(Namespace* namespc, const char* uri) { } bool File::Create(Namespace* namespc, const char* name) { - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); int fd = _wopen(system_name.wide(), O_RDONLY | O_CREAT, 0666); if (fd < 0) { return false; @@ -426,8 +426,8 @@ static const int kMountPointHeaderSize = 4 * sizeof(USHORT); bool File::CreateLink(Namespace* namespc, const char* utf8_name, const char* utf8_target) { - Utf8ToWideScope name(utf8_name); - Utf8ToWideScope target(utf8_target); + Utf8ToWideScope name(PrefixLongFilePath(utf8_name)); + Utf8ToWideScope target(PrefixLongFilePath(utf8_target)); DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; File::Type type = File::GetType(namespc, utf8_target, true); @@ -449,13 +449,13 @@ bool File::CreateLink(Namespace* namespc, } bool File::Delete(Namespace* namespc, const char* name) { - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); int status = _wremove(system_name.wide()); return status != -1; } bool File::DeleteLink(Namespace* namespc, const char* name) { - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); bool result = false; DWORD attributes = GetFileAttributesW(system_name.wide()); if ((attributes == INVALID_FILE_ATTRIBUTES) || @@ -477,13 +477,15 @@ bool File::DeleteLink(Namespace* namespc, const char* name) { bool File::Rename(Namespace* namespc, const char* old_path, const char* new_path) { - File::Type type = GetType(namespc, old_path, false); + const char* prefixed_old_path = PrefixLongFilePath(old_path); + File::Type type = GetType(namespc, prefixed_old_path, false); if (type != kIsFile) { SetLastError(ERROR_FILE_NOT_FOUND); return false; } - Utf8ToWideScope system_old_path(old_path); - Utf8ToWideScope system_new_path(new_path); + const char* prefixed_new_path = PrefixLongFilePath(new_path); + Utf8ToWideScope system_old_path(prefixed_old_path); + Utf8ToWideScope system_new_path(prefixed_new_path); DWORD flags = MOVEFILE_WRITE_THROUGH | MOVEFILE_REPLACE_EXISTING; int move_status = MoveFileExW(system_old_path.wide(), system_new_path.wide(), flags); @@ -493,23 +495,25 @@ bool File::Rename(Namespace* namespc, bool File::RenameLink(Namespace* namespc, const char* old_path, const char* new_path) { - File::Type type = GetType(namespc, old_path, false); + const char* prefixed_old_path = PrefixLongFilePath(old_path); + File::Type type = GetType(namespc, prefixed_old_path, false); if (type != kIsLink) { SetLastError(ERROR_FILE_NOT_FOUND); return false; } - Utf8ToWideScope system_old_path(old_path); - Utf8ToWideScope system_new_path(new_path); + Utf8ToWideScope system_old_path(prefixed_old_path); + const char* prefixed_new_path = PrefixLongFilePath(new_path); + Utf8ToWideScope system_new_path(prefixed_new_path); DWORD flags = MOVEFILE_WRITE_THROUGH | MOVEFILE_REPLACE_EXISTING; // Junction links on Windows appear as special directories. MoveFileExW's // MOVEFILE_REPLACE_EXISTING does not allow for replacement of directories, // so we need to remove it before renaming a link. This step is only // necessary for junctions created by the old Link.create implementation. - if ((Directory::Exists(namespc, new_path) == Directory::EXISTS) && - (GetType(namespc, new_path, false) == kIsLink)) { + if ((Directory::Exists(namespc, prefixed_new_path) == Directory::EXISTS) && + (GetType(namespc, prefixed_new_path, false) == kIsLink)) { // Bail out if the DeleteLink call fails. - if (!DeleteLink(namespc, new_path)) { + if (!DeleteLink(namespc, prefixed_new_path)) { return false; } } @@ -615,24 +619,26 @@ static wchar_t* CopyIntoTempFile(const char* src, const char* dest) { bool File::Copy(Namespace* namespc, const char* old_path, const char* new_path) { - File::Type type = GetType(namespc, old_path, false); + const char* prefixed_old_path = PrefixLongFilePath(old_path); + const char* prefixed_new_path = PrefixLongFilePath(new_path); + File::Type type = GetType(namespc, prefixed_old_path, false); if (type != kIsFile) { SetLastError(ERROR_FILE_NOT_FOUND); return false; } - wchar_t* temp_file = CopyIntoTempFile(old_path, new_path); + wchar_t* temp_file = CopyIntoTempFile(prefixed_old_path, prefixed_new_path); if (temp_file == NULL) { // If temp file creation fails, fall back on doing a direct copy. - Utf8ToWideScope system_old_path(old_path); - Utf8ToWideScope system_new_path(new_path); + Utf8ToWideScope system_old_path(prefixed_old_path); + Utf8ToWideScope system_new_path(prefixed_new_path); return CopyFileExW(system_old_path.wide(), system_new_path.wide(), NULL, NULL, NULL, 0) != 0; } - Utf8ToWideScope system_new_dest(new_path); + Utf8ToWideScope system_new_dest(prefixed_new_path); // Remove the existing file. Otherwise, renaming will fail. - if (Exists(namespc, new_path)) { + if (Exists(namespc, prefixed_new_path)) { DeleteFileW(system_new_dest.wide()); } @@ -647,7 +653,7 @@ bool File::Copy(Namespace* namespc, int64_t File::LengthFromPath(Namespace* namespc, const char* name) { struct __stat64 st; - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); if (!StatHelper(system_name.wide(), &st)) { return -1; } @@ -658,7 +664,8 @@ const char* File::LinkTarget(Namespace* namespc, const char* pathname, char* dest, int dest_size) { - const wchar_t* name = StringUtilsWin::Utf8ToWide(pathname); + const wchar_t* name = + StringUtilsWin::Utf8ToWide(PrefixLongFilePath(pathname)); HANDLE dir_handle = CreateFileW( name, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, @@ -668,8 +675,10 @@ const char* File::LinkTarget(Namespace* namespc, return NULL; } + // Allocate a buffer for regular paths (smaller than MAX_PATH). If buffer is + // too small for a long path, allocate a bigger buffer and try again. int buffer_size = - sizeof(REPARSE_DATA_BUFFER) + 2 * (MAX_PATH + 1) * sizeof(WCHAR); + sizeof(REPARSE_DATA_BUFFER) + (MAX_PATH + 1) * sizeof(WCHAR); REPARSE_DATA_BUFFER* buffer = reinterpret_cast(Dart_ScopeAllocate(buffer_size)); DWORD received_bytes; // Value is not used. @@ -677,9 +686,26 @@ const char* File::LinkTarget(Namespace* namespc, buffer, buffer_size, &received_bytes, NULL); if (result == 0) { DWORD error = GetLastError(); - CloseHandle(dir_handle); - SetLastError(error); - return NULL; + // If ERROR_MORE_DATA is thrown, the target path exceeds the size limit. A + // bigger buffer will be required. + if (error == ERROR_MORE_DATA) { + // Allocate a bigger buffer with MAX_LONG_PATH + buffer_size = + sizeof(REPARSE_DATA_BUFFER) + (MAX_LONG_PATH + 1) * sizeof(WCHAR); + buffer = reinterpret_cast( + Dart_ScopeAllocate(buffer_size)); + result = DeviceIoControl(dir_handle, FSCTL_GET_REPARSE_POINT, NULL, 0, + buffer, buffer_size, &received_bytes, NULL); + if (result == 0) { + // Overwrite the ERROR_MORE_DATA. + error = GetLastError(); + } + } + if (result == 0) { + CloseHandle(dir_handle); + SetLastError(error); + return NULL; + } } if (CloseHandle(dir_handle) == 0) { return NULL; @@ -726,11 +752,12 @@ const char* File::LinkTarget(Namespace* namespc, } void File::Stat(Namespace* namespc, const char* name, int64_t* data) { - File::Type type = GetType(namespc, name, false); + const char* prefixed_name = PrefixLongFilePath(name); + File::Type type = GetType(namespc, prefixed_name, false); data[kType] = type; if (type != kDoesNotExist) { struct _stat64 st; - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(prefixed_name); int stat_status = _wstat64(system_name.wide(), &st); if (stat_status == 0) { data[kCreatedTime] = st.st_ctime * 1000; @@ -746,7 +773,7 @@ void File::Stat(Namespace* namespc, const char* name, int64_t* data) { time_t File::LastAccessed(Namespace* namespc, const char* name) { struct __stat64 st; - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); if (!StatHelper(system_name.wide(), &st)) { return -1; } @@ -755,7 +782,7 @@ time_t File::LastAccessed(Namespace* namespc, const char* name) { time_t File::LastModified(Namespace* namespc, const char* name) { struct __stat64 st; - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); if (!StatHelper(system_name.wide(), &st)) { return -1; } @@ -767,7 +794,7 @@ bool File::SetLastAccessed(Namespace* namespc, int64_t millis) { // First get the current times. struct __stat64 st; - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); if (!StatHelper(system_name.wide(), &st)) { return false; } @@ -784,7 +811,7 @@ bool File::SetLastModified(Namespace* namespc, int64_t millis) { // First get the current times. struct __stat64 st; - Utf8ToWideScope system_name(name); + Utf8ToWideScope system_name(PrefixLongFilePath(name)); if (!StatHelper(system_name.wide(), &st)) { return false; } @@ -801,7 +828,6 @@ bool File::SetLastModified(Namespace* namespc, bool File::IsAbsolutePath(const char* pathname) { if (pathname == NULL) return false; char first = pathname[0]; - if (pathname == 0) return false; char second = pathname[1]; if (first == '\\' && second == '\\') return true; if (second != ':') return false; @@ -810,11 +836,77 @@ bool File::IsAbsolutePath(const char* pathname) { return (first >= 'a') && (first <= 'z') && (third == '\\' || third == '/'); } +const char* PrefixLongFilePath(const char* path) { + return PrefixLongPathIfExceedLimit(path, true); +} + +const char* PrefixLongDirectoryPath(const char* path) { + return PrefixLongPathIfExceedLimit(path, false); +} + +const char* PrefixLongPathIfExceedLimit(const char* path, bool file_name) { + const char* prefix = "\\\\?\\"; + // File name and Directory name has a different size limit. + // Reference: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation + int limit = file_name ? MAX_PATH : MAX_DIRECTORY_SIZE; + int length; + if (!File::IsAbsolutePath(path)) { + // Convert relative paths to absolute paths because relative paths don't + // work when they represent long(>MAX_PATH) absolute paths. + wchar_t* buffer = + reinterpret_cast(malloc(sizeof(wchar_t) * (limit + 1))); + Utf8ToWideScope dir(path); + LPWSTR* file = NULL; + int size = GetFullPathNameW(dir.wide(), limit + 1, buffer, file); + free(buffer); + if (size == 0) { + return path; + } + if (size > limit) { + // The absolute path is a long path. Assign a buffer with right size and + // try again. + // Note that in this case, size represents the size of the path plus the + // null terminator. + wchar_t* buf = + reinterpret_cast(malloc(sizeof(wchar_t) * (size))); + size = GetFullPathNameW(dir.wide(), size, buf, file); + if (size == 0) { + return path; + } + path = StringUtilsWin::WideToUtf8(buf); + free(buf); + } + // length doesn't include null terminator on success return. + length = size; + } else { + length = strlen(path); + } + if (length <= limit) { + return path; + } + int length_prefix = strlen(prefix); + // If a path is already prefixed, return it. + if ((length > length_prefix && strncmp(path, prefix, length_prefix) == 0)) { + return path; + } + // Replace forward slashes with backward slashes. + char* result = reinterpret_cast( + Dart_ScopeAllocate((length_prefix + 1 + length) * sizeof(char))); + + strncpy(result, prefix, length_prefix); + + for (int i = 0; i < length; i++) { + result[length_prefix + i] = path[i] == '/' ? '\\' : path[i]; + } + result[length + length_prefix] = '\0'; + return result; +} + const char* File::GetCanonicalPath(Namespace* namespc, const char* pathname, char* dest, int dest_size) { - Utf8ToWideScope system_name(pathname); + Utf8ToWideScope system_name(PrefixLongFilePath(pathname)); HANDLE file_handle = CreateFileW(system_name.wide(), 0, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); @@ -838,9 +930,8 @@ const char* File::GetCanonicalPath(Namespace* namespc, // Remove leading \\?\ if possible, unless input used it. int offset = 0; - if ((result_size < MAX_PATH - 1 + 4) && (result_size > 4) && - (wcsncmp(path.get(), L"\\\\?\\", 4) == 0) && - (wcsncmp(system_name.wide(), L"\\\\?\\", 4) != 0)) { + if ((result_size > 4) && (wcsncmp(path.get(), L"\\\\?\\", 4) == 0) && + (strncmp(pathname, "\\\\?\\", 4) != 0)) { offset = 4; } int utf8_size = WideCharToMultiByte(CP_UTF8, 0, path.get() + offset, -1, @@ -879,7 +970,7 @@ File::Type File::GetType(Namespace* namespc, const char* pathname, bool follow_links) { // Convert to wchar_t string. - Utf8ToWideScope name(pathname); + Utf8ToWideScope name(PrefixLongFilePath(pathname)); DWORD attributes = GetFileAttributesW(name.wide()); File::Type result = kIsFile; if (attributes == INVALID_FILE_ATTRIBUTES) { @@ -919,7 +1010,8 @@ File::Identical File::AreIdentical(Namespace* namespc_1, USE(namespc_1); USE(namespc_2); BY_HANDLE_FILE_INFORMATION file_info[2]; - const char* file_names[2] = {file_1, file_2}; + const char* file_names[2] = {PrefixLongFilePath(file_1), + PrefixLongFilePath(file_2)}; for (int i = 0; i < 2; ++i) { Utf8ToWideScope wide_name(file_names[i]); HANDLE file_handle = CreateFileW( diff --git a/runtime/bin/file_win.h b/runtime/bin/file_win.h new file mode 100644 index 00000000000..8993438457d --- /dev/null +++ b/runtime/bin/file_win.h @@ -0,0 +1,24 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#ifndef RUNTIME_BIN_FILE_WIN_H_ +#define RUNTIME_BIN_FILE_WIN_H_ + +#include "file.h" + +// The limit for a regular directory is 248. +// Reference: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation +#define MAX_DIRECTORY_SIZE (MAX_PATH - 12) + +namespace dart { +namespace bin { + +const char* PrefixLongFilePath(const char* path); +const char* PrefixLongDirectoryPath(const char* path); +const char* PrefixLongPathIfExceedLimit(const char* path, bool file_name); + +} // namespace bin +} // namespace dart + +#endif // RUNTIME_BIN_FILE_WIN_H_ diff --git a/runtime/bin/namespace_win.cc b/runtime/bin/namespace_win.cc index f31d37a1db0..09a05f5db34 100644 --- a/runtime/bin/namespace_win.cc +++ b/runtime/bin/namespace_win.cc @@ -10,6 +10,8 @@ #include #include +#include "bin/file.h" +#include "bin/file_win.h" #include "bin/utils.h" #include "bin/utils_win.h" @@ -46,6 +48,9 @@ const char* Namespace::GetCurrent(Namespace* namespc) { } bool Namespace::SetCurrent(Namespace* namespc, const char* path) { + // TODO(zichangguo): "\\?\" prepended long path doesn't work. + // https://github.com/dart-lang/sdk/issues/42416 + path = PrefixLongDirectoryPath(path); Utf8ToWideScope system_path(path); bool result = SetCurrentDirectoryW(system_path.wide()) != 0; return result; diff --git a/runtime/bin/utils_win.h b/runtime/bin/utils_win.h index fdb49097489..0e4fae9ab81 100644 --- a/runtime/bin/utils_win.h +++ b/runtime/bin/utils_win.h @@ -9,6 +9,8 @@ #include "platform/utils.h" +#define MAX_LONG_PATH 32767 + namespace dart { namespace bin { diff --git a/tests/standalone/io/file_long_path_test.dart b/tests/standalone/io/file_long_path_test.dart new file mode 100644 index 00000000000..454e633d785 --- /dev/null +++ b/tests/standalone/io/file_long_path_test.dart @@ -0,0 +1,247 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// This test is Windows-only. + +import 'dart:io'; + +import "package:expect/expect.dart"; + +const maxPath = 260; +const maxDirectoryPath = maxPath - 12; +String longName = '${'x' * 248}'; + +Directory createLongPathDir(Directory tmp) { + if (tmp.path.length <= maxDirectoryPath) { + var path = tmp.path; + path += '\\${'t' * 248}'; + var dir = Directory(path); + dir.createSync(recursive: true); + // Test the rename() of directory + dir = dir.renameSync(tmp.path + '\\$longName'); + Expect.isTrue(dir.existsSync()); + Expect.isFalse(Directory(path).existsSync()); + return dir; + } else { + return tmp; + } +} + +void testCreate(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + Expect.isTrue(file.existsSync()); + file.deleteSync(); +} + +void testCopy(String dir) { + final src = '${dir}\\a_long_path_filename_1'; + final dest = '${dir}\\a_long_path_filename_2'; + Expect.isTrue(src.length > maxPath); + final file1 = File(src); + file1.createSync(); + + final file2 = file1.copySync(dest); + Expect.isTrue(file2.existsSync()); + file1.deleteSync(); + file2.deleteSync(); +} + +void testRename(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + Expect.isTrue(file.existsSync()); + + final renamedFile = file.renameSync('${path}_copy'); + + Expect.isFalse(file.existsSync()); + Expect.isTrue(renamedFile.existsSync()); + renamedFile.deleteSync(); +} + +void testReadWrite(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + + final content = "testReadWrite"; + file.writeAsStringSync(content); + Expect.isTrue(file.existsSync()); + + int length = file.lengthSync(); + Expect.equals(content.length, length); + + final string = file.readAsStringSync(); + Expect.equals(content, string); + file.deleteSync(); +} + +void testOpen(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + final access = file.openSync(); + access.closeSync(); +} + +void testFileStat(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + final stat = FileStat.statSync(file.path); + + final dateTime = DateTime.utc(2020); + + file.setLastModifiedSync(dateTime); + Expect.notEquals( + stat.modified.toString(), file.lastModifiedSync().toString()); + + file.setLastAccessedSync(dateTime); + Expect.notEquals( + stat.accessed.toString(), file.lastAccessedSync().toString()); +} + +void testCreateLinkToDir(String dir) { + final path = '${dir}\\a_long_path_linkname'; + Expect.isTrue(path.length > maxPath); + var target = '$dir\\a_long_path_target'; + final link = Link(path)..createSync(target); + + final dest = Directory(target)..createSync(); + Expect.isTrue(dest.existsSync()); + + Expect.isTrue(link.existsSync()); + Expect.isTrue(link.targetSync().contains('a_long_path_target')); + + // Rename link + var renamedLink = link.renameSync('${dir}\\a_renamed_long_path_link'); + Expect.isTrue(renamedLink.existsSync()); + Expect.isFalse(link.existsSync()); + Expect.isTrue(renamedLink.targetSync().contains('a_long_path_target')); + + // Update link target + target = '$dir\\an_updated_target'; + final renamedDest = Directory(target)..createSync(); + renamedLink.updateSync(target); + Expect.isTrue(renamedLink.targetSync().contains('an_updated_target')); + + dest.deleteSync(); + renamedDest.deleteSync(); + renamedLink.deleteSync(); +} + +void testCreateLinkToFile(String dir) { + final path = '${dir}\\a_long_path_linkname'; + Expect.isTrue(path.length > maxPath); + var target = '$dir\\a_long_path_target'; + final link = Link(path)..createSync(target); + + final dest = File(target)..createSync(); + Expect.isTrue(dest.existsSync()); + + Expect.isTrue(link.existsSync()); + Expect.isTrue(link.targetSync().contains('a_long_path_target')); + Expect.isTrue(link.resolveSymbolicLinksSync().contains('a_long_path_target')); + + // Rename link + var renamedLink = link.renameSync('${dir}\\a_renamed_long_path_link'); + Expect.isTrue(renamedLink.existsSync()); + Expect.isFalse(link.existsSync()); + Expect.isTrue(renamedLink.targetSync().contains('a_long_path_target')); + + // Update link target + target = '$dir\\an_updated_target'; + final renamedDest = File(target)..createSync(); + renamedLink.updateSync(target); + Expect.isTrue(renamedLink.targetSync().contains('an_updated_target')); + + dest.deleteSync(); + renamedDest.deleteSync(); + renamedLink.deleteSync(); +} + +testNormalLinkToLongPath(String short, String long) { + var target = File('$long\\file_target')..createSync(); + final link = Link('$short\\link')..createSync(target.path); + Expect.isTrue(target.path.length > maxPath); + Expect.isTrue(link.resolveSymbolicLinksSync().length > maxPath); + Expect.isTrue(link.path.length < maxPath); + Expect.isTrue(link.resolveSymbolicLinksSync().contains('file_target')); + + Expect.isTrue(link.existsSync()); + Expect.equals(target.path, link.targetSync()); + + var targetDir = Directory('$long\\dir_target')..createSync(); + link.updateSync(targetDir.path); + Expect.equals(targetDir.path, link.targetSync()); + + link.deleteSync(); + target.deleteSync(); + targetDir.deleteSync(); +} + +testLongPathLinkToNormal(String short, String long) { + var target = File('$short\\file_target')..createSync(); + final link = Link('$long\\link')..createSync(target.path); + + Expect.isTrue(target.path.length < maxPath); + Expect.isTrue(link.path.length > maxPath); + Expect.isTrue(link.resolveSymbolicLinksSync().contains('file_target')); + + Expect.isTrue(link.existsSync()); + Expect.equals(target.path, link.targetSync()); + + var targetDir = Directory('$short\\dir_target')..createSync(); + link.updateSync(targetDir.path); + Expect.equals(targetDir.path, link.targetSync()); + + link.deleteSync(); + target.deleteSync(); + targetDir.deleteSync(); +} + +testDirectorySetCurrent(String dir) { + // This tests setting a long path directory to current directory. + // This will fail. + Expect.isTrue(dir.length > maxPath); + Expect.throws(() { + Directory.current = dir; + }, (e) => e.toString().contains('extension is too long')); +} + +void main() { + if (!Platform.isWindows) { + return; + } + final tmp = Directory.systemTemp.createTempSync('dart-file-long-path'); + final oldCurrent = Directory.current; + Directory.current = tmp; + try { + String dir = createLongPathDir(tmp).path; + testDirectorySetCurrent(dir); + for (final path in [dir, ".\\$longName", ".\\$longName\\..\\$longName"]) { + testCreate(path); + testCopy(path); + testRename(path); + testReadWrite(path); + testOpen(path); + testFileStat(path); + testCreateLinkToDir(path); + testCreateLinkToFile(path); + } + + testNormalLinkToLongPath(tmp.path, dir); + testLongPathLinkToNormal(tmp.path, dir); + } finally { + // Reset the current Directory. + Directory.current = oldCurrent; + tmp.deleteSync(recursive: true); + } +} diff --git a/tests/standalone/io/file_relative_long_path_test.dart b/tests/standalone/io/file_relative_long_path_test.dart new file mode 100644 index 00000000000..0c53ab37a42 --- /dev/null +++ b/tests/standalone/io/file_relative_long_path_test.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// This test is Windows-only. It tests a short (shorter than 260) relative path +// representing a long absolute path cannot be used by Windows API. Running this +// test without proper support on long path will get an error. + +import 'dart:io'; + +const maxPath = 260; + +void main(args) { + if (!Platform.isWindows) { + return; + } + final dir = Directory.systemTemp.createTempSync('test'); + + if (dir.path.length >= maxPath) { + return; + } + + // Make sure oldpath is shorter than MAX_PATH (260). + int length = (maxPath - dir.path.length) ~/ 2; + final oldpath = Directory('${dir.path}\\${'x' * length}}'); + oldpath.createSync(recursive: true); + final temp = Directory.current; + + Directory.current = oldpath.path; + + // The length of relative path is always shorter than maxPath, but it + // represents a path exceeding the maxPath. + final newpath = Directory('.\\${'y' * 2 * length}'); + newpath.createSync(); + + // Reset current directory before deletion. + Directory.current = temp.path; + dir.deleteSync(recursive: true); +} diff --git a/tests/standalone_2/io/file_long_path_test.dart b/tests/standalone_2/io/file_long_path_test.dart new file mode 100644 index 00000000000..454e633d785 --- /dev/null +++ b/tests/standalone_2/io/file_long_path_test.dart @@ -0,0 +1,247 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// This test is Windows-only. + +import 'dart:io'; + +import "package:expect/expect.dart"; + +const maxPath = 260; +const maxDirectoryPath = maxPath - 12; +String longName = '${'x' * 248}'; + +Directory createLongPathDir(Directory tmp) { + if (tmp.path.length <= maxDirectoryPath) { + var path = tmp.path; + path += '\\${'t' * 248}'; + var dir = Directory(path); + dir.createSync(recursive: true); + // Test the rename() of directory + dir = dir.renameSync(tmp.path + '\\$longName'); + Expect.isTrue(dir.existsSync()); + Expect.isFalse(Directory(path).existsSync()); + return dir; + } else { + return tmp; + } +} + +void testCreate(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + Expect.isTrue(file.existsSync()); + file.deleteSync(); +} + +void testCopy(String dir) { + final src = '${dir}\\a_long_path_filename_1'; + final dest = '${dir}\\a_long_path_filename_2'; + Expect.isTrue(src.length > maxPath); + final file1 = File(src); + file1.createSync(); + + final file2 = file1.copySync(dest); + Expect.isTrue(file2.existsSync()); + file1.deleteSync(); + file2.deleteSync(); +} + +void testRename(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + Expect.isTrue(file.existsSync()); + + final renamedFile = file.renameSync('${path}_copy'); + + Expect.isFalse(file.existsSync()); + Expect.isTrue(renamedFile.existsSync()); + renamedFile.deleteSync(); +} + +void testReadWrite(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + + final content = "testReadWrite"; + file.writeAsStringSync(content); + Expect.isTrue(file.existsSync()); + + int length = file.lengthSync(); + Expect.equals(content.length, length); + + final string = file.readAsStringSync(); + Expect.equals(content, string); + file.deleteSync(); +} + +void testOpen(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + final access = file.openSync(); + access.closeSync(); +} + +void testFileStat(String dir) { + final path = '${dir}\\a_long_path_filename'; + Expect.isTrue(path.length > maxPath); + final file = File(path); + file.createSync(); + final stat = FileStat.statSync(file.path); + + final dateTime = DateTime.utc(2020); + + file.setLastModifiedSync(dateTime); + Expect.notEquals( + stat.modified.toString(), file.lastModifiedSync().toString()); + + file.setLastAccessedSync(dateTime); + Expect.notEquals( + stat.accessed.toString(), file.lastAccessedSync().toString()); +} + +void testCreateLinkToDir(String dir) { + final path = '${dir}\\a_long_path_linkname'; + Expect.isTrue(path.length > maxPath); + var target = '$dir\\a_long_path_target'; + final link = Link(path)..createSync(target); + + final dest = Directory(target)..createSync(); + Expect.isTrue(dest.existsSync()); + + Expect.isTrue(link.existsSync()); + Expect.isTrue(link.targetSync().contains('a_long_path_target')); + + // Rename link + var renamedLink = link.renameSync('${dir}\\a_renamed_long_path_link'); + Expect.isTrue(renamedLink.existsSync()); + Expect.isFalse(link.existsSync()); + Expect.isTrue(renamedLink.targetSync().contains('a_long_path_target')); + + // Update link target + target = '$dir\\an_updated_target'; + final renamedDest = Directory(target)..createSync(); + renamedLink.updateSync(target); + Expect.isTrue(renamedLink.targetSync().contains('an_updated_target')); + + dest.deleteSync(); + renamedDest.deleteSync(); + renamedLink.deleteSync(); +} + +void testCreateLinkToFile(String dir) { + final path = '${dir}\\a_long_path_linkname'; + Expect.isTrue(path.length > maxPath); + var target = '$dir\\a_long_path_target'; + final link = Link(path)..createSync(target); + + final dest = File(target)..createSync(); + Expect.isTrue(dest.existsSync()); + + Expect.isTrue(link.existsSync()); + Expect.isTrue(link.targetSync().contains('a_long_path_target')); + Expect.isTrue(link.resolveSymbolicLinksSync().contains('a_long_path_target')); + + // Rename link + var renamedLink = link.renameSync('${dir}\\a_renamed_long_path_link'); + Expect.isTrue(renamedLink.existsSync()); + Expect.isFalse(link.existsSync()); + Expect.isTrue(renamedLink.targetSync().contains('a_long_path_target')); + + // Update link target + target = '$dir\\an_updated_target'; + final renamedDest = File(target)..createSync(); + renamedLink.updateSync(target); + Expect.isTrue(renamedLink.targetSync().contains('an_updated_target')); + + dest.deleteSync(); + renamedDest.deleteSync(); + renamedLink.deleteSync(); +} + +testNormalLinkToLongPath(String short, String long) { + var target = File('$long\\file_target')..createSync(); + final link = Link('$short\\link')..createSync(target.path); + Expect.isTrue(target.path.length > maxPath); + Expect.isTrue(link.resolveSymbolicLinksSync().length > maxPath); + Expect.isTrue(link.path.length < maxPath); + Expect.isTrue(link.resolveSymbolicLinksSync().contains('file_target')); + + Expect.isTrue(link.existsSync()); + Expect.equals(target.path, link.targetSync()); + + var targetDir = Directory('$long\\dir_target')..createSync(); + link.updateSync(targetDir.path); + Expect.equals(targetDir.path, link.targetSync()); + + link.deleteSync(); + target.deleteSync(); + targetDir.deleteSync(); +} + +testLongPathLinkToNormal(String short, String long) { + var target = File('$short\\file_target')..createSync(); + final link = Link('$long\\link')..createSync(target.path); + + Expect.isTrue(target.path.length < maxPath); + Expect.isTrue(link.path.length > maxPath); + Expect.isTrue(link.resolveSymbolicLinksSync().contains('file_target')); + + Expect.isTrue(link.existsSync()); + Expect.equals(target.path, link.targetSync()); + + var targetDir = Directory('$short\\dir_target')..createSync(); + link.updateSync(targetDir.path); + Expect.equals(targetDir.path, link.targetSync()); + + link.deleteSync(); + target.deleteSync(); + targetDir.deleteSync(); +} + +testDirectorySetCurrent(String dir) { + // This tests setting a long path directory to current directory. + // This will fail. + Expect.isTrue(dir.length > maxPath); + Expect.throws(() { + Directory.current = dir; + }, (e) => e.toString().contains('extension is too long')); +} + +void main() { + if (!Platform.isWindows) { + return; + } + final tmp = Directory.systemTemp.createTempSync('dart-file-long-path'); + final oldCurrent = Directory.current; + Directory.current = tmp; + try { + String dir = createLongPathDir(tmp).path; + testDirectorySetCurrent(dir); + for (final path in [dir, ".\\$longName", ".\\$longName\\..\\$longName"]) { + testCreate(path); + testCopy(path); + testRename(path); + testReadWrite(path); + testOpen(path); + testFileStat(path); + testCreateLinkToDir(path); + testCreateLinkToFile(path); + } + + testNormalLinkToLongPath(tmp.path, dir); + testLongPathLinkToNormal(tmp.path, dir); + } finally { + // Reset the current Directory. + Directory.current = oldCurrent; + tmp.deleteSync(recursive: true); + } +} diff --git a/tests/standalone_2/io/file_relative_long_path_test.dart b/tests/standalone_2/io/file_relative_long_path_test.dart new file mode 100644 index 00000000000..0c53ab37a42 --- /dev/null +++ b/tests/standalone_2/io/file_relative_long_path_test.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// This test is Windows-only. It tests a short (shorter than 260) relative path +// representing a long absolute path cannot be used by Windows API. Running this +// test without proper support on long path will get an error. + +import 'dart:io'; + +const maxPath = 260; + +void main(args) { + if (!Platform.isWindows) { + return; + } + final dir = Directory.systemTemp.createTempSync('test'); + + if (dir.path.length >= maxPath) { + return; + } + + // Make sure oldpath is shorter than MAX_PATH (260). + int length = (maxPath - dir.path.length) ~/ 2; + final oldpath = Directory('${dir.path}\\${'x' * length}}'); + oldpath.createSync(recursive: true); + final temp = Directory.current; + + Directory.current = oldpath.path; + + // The length of relative path is always shorter than maxPath, but it + // represents a path exceeding the maxPath. + final newpath = Directory('.\\${'y' * 2 * length}'); + newpath.createSync(); + + // Reset current directory before deletion. + Directory.current = temp.path; + dir.deleteSync(recursive: true); +}