Enable long path on Windows

File APIs on Windows can now handle files and directories identified by
long paths (greater than 260 characters). For directory, the limit is
248.

Some restrictions from Windows:
1. The size limit for long path is 32,767 characters.
2. Each component separated by backslashes should not be more than 255
characters.

Reference: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation.

Note that `Directory.current` does not work with long path.

Bug: https://github.com/dart-lang/sdk/issues/42416
Change-Id: Ia1b4608d393fb36f1d843858c6f076f3c825dc83
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/152736
Reviewed-by: Alexander Aprelev <aam@google.com>
Commit-Queue: Zichang Guo <zichangguo@google.com>
This commit is contained in:
Zichang Guo 2020-09-08 20:12:05 +00:00 committed by commit-bot@chromium.org
parent 173a958d37
commit 08662f0575
11 changed files with 777 additions and 65 deletions

View file

@ -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`

View file

@ -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",

View file

@ -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;
}

View file

@ -5,13 +5,11 @@
#include "platform/globals.h"
#if defined(HOST_OS_WINDOWS)
#include "bin/file.h"
#include <Shlwapi.h> // NOLINT
#include <WinIoCtl.h> // NOLINT
#include <fcntl.h> // NOLINT
#include <io.h> // NOLINT
#include <Shlwapi.h> // NOLINT
#undef StrDup // defined in Shlwapi.h as StrDupW
#undef StrDup // defined in Shlwapi.h as StrDupW
#include <stdio.h> // NOLINT
#include <string.h> // NOLINT
#include <sys/stat.h> // 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<REPARSE_DATA_BUFFER*>(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<REPARSE_DATA_BUFFER*>(
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<wchar_t*>(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<wchar_t*>(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<char*>(
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(

24
runtime/bin/file_win.h Normal file
View file

@ -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_

View file

@ -10,6 +10,8 @@
#include <errno.h>
#include <sys/stat.h>
#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;

View file

@ -9,6 +9,8 @@
#include "platform/utils.h"
#define MAX_LONG_PATH 32767
namespace dart {
namespace bin {

View file

@ -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<FileSystemException>(() {
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);
}
}

View file

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

View file

@ -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<FileSystemException>(() {
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);
}
}

View file

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