diff --git a/runtime/bin/directory_win.cc b/runtime/bin/directory_win.cc index 0fd6c77aefd..460ae694c90 100644 --- a/runtime/bin/directory_win.cc +++ b/runtime/bin/directory_win.cc @@ -379,7 +379,7 @@ Directory::ExistsResult Directory::Exists(const wchar_t* dir_name) { Directory::ExistsResult Directory::Exists(Namespace* namespc, const char* dir_name) { - const auto path = ToWinAPIDirectoryPath(dir_name); + const auto path = ToWinAPIPath(dir_name); return Exists(path.get()); } @@ -400,7 +400,7 @@ char* Directory::CurrentNoScope() { } bool Directory::Create(Namespace* namespc, const char* dir_name) { - const auto path = ToWinAPIDirectoryPath(dir_name); + const auto path = ToWinAPIPath(dir_name); int create_status = CreateDirectoryW(path.get(), nullptr); // If the directory already existed, treat it as a success. if ((create_status == 0) && (GetLastError() == ERROR_ALREADY_EXISTS) && @@ -506,8 +506,7 @@ const char* Directory::CreateTemp(Namespace* namespc, const char* prefix) { bool Directory::Delete(Namespace* namespc, const char* dir_name, bool recursive) { - const auto path = - ToWinAPIDirectoryPath(dir_name, /*force_long_prefix=*/recursive); + const auto path = ToWinAPIPath(dir_name); bool result = false; if (!recursive) { if (File::GetType(path.get(), /*follow_links=*/true) == @@ -526,13 +525,13 @@ bool Directory::Delete(Namespace* namespc, bool Directory::Rename(Namespace* namespc, const char* old_name, const char* new_name) { - const auto old_path = ToWinAPIDirectoryPath(old_name); + const auto old_path = ToWinAPIPath(old_name); ExistsResult exists = Exists(old_path.get()); if (exists != EXISTS) { SetLastError(ERROR_FILE_NOT_FOUND); return false; } - const auto new_path = ToWinAPIDirectoryPath(new_name); + const auto new_path = ToWinAPIPath(new_name); DWORD flags = MOVEFILE_WRITE_THROUGH; int move_status = MoveFileExW(old_path.get(), new_path.get(), flags); return (move_status != 0); diff --git a/runtime/bin/file_win.cc b/runtime/bin/file_win.cc index 69377695eff..48af4220dd0 100644 --- a/runtime/bin/file_win.cc +++ b/runtime/bin/file_win.cc @@ -379,115 +379,33 @@ static bool IsDeviceNamespacePrefixed(const std::unique_ptr& path) { kDeviceNamespacePrefixLength) == 0; } -// Converts the given UTF8 path to wide char. If resulting path does not -// fit into MAX_PATH / MAX_DIRECTORY_PATH (or if |force_long_prefix| is true) -// then converts the path to the absolute `\\?\`-prefixed form. +// Converts the given UTF8 path to wide char '\\?\'-prefix absolute path. // -// This function does not change paths which are already prefixed with `\\.\` -// prefix. -// -// UNC paths (`\\server\share\...`) are converted to `\\?\UNC\server\share\...` -// if necessary. -// -// Note: -// 1. Some WinAPI functions (like SetCurrentDirectoryW) are always limited -// to MAX_PATH long paths and converting to `\\?\`-prefixed form does not -// remove this limitation. Always check Win API documentation. -// 2. This function might change relative path to an absolute path. -static std::unique_ptr ToWinAPIPath(const char* utf8_path, - bool is_file, - bool force_long_prefix) { +// Note that some WinAPI functions (like SetCurrentDirectoryW) are always +// limited to MAX_PATH long paths and converting to `\\?\`-prefixed form does +// not remove this limitation. Always check Win API documentation. +std::unique_ptr ToWinAPIPath(const char* utf8_path) { auto path = Utf8ToWideChar(utf8_path); - - // File name and Directory name have different size limit. - // Reference: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation - const int path_short_limit = is_file ? MAX_PATH : MAX_DIRECTORY_PATH; - - std::unique_ptr absolute_path; - // Need to convert to absolute path if we want to use long prefix - // to normalize the path - this is to get rid of .., . that can't be - // interpreted in the long-prefixed mode). - if (force_long_prefix || !IsAbsolutePath(path.get())) { - absolute_path = ConvertToAbsolutePath(path); - if (absolute_path == nullptr) { - return path; - } - } else { - absolute_path = std::move(path); + // Among other things ConvertToAbsolutePath replaces '/' with '\', + // which PathAllocCanonicalize won't do. + auto abs_path = ConvertToAbsolutePath(path); + if (abs_path.get() == nullptr) { + return std::unique_ptr(nullptr); } - int path_length = wcslen(absolute_path.get()); - - if (!force_long_prefix && path_length < path_short_limit) { - if (path == nullptr) { - return absolute_path; - } else { - return path; - } + PWSTR canonical_path; + if (PathAllocCanonicalize(abs_path.get(), + PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH, + &canonical_path) != S_OK) { + return std::unique_ptr(nullptr); } - - if (IsLongPathPrefixed(absolute_path) || - IsDeviceNamespacePrefixed(absolute_path)) { - return absolute_path; - } - - // If the path already starts with `\\` but not with `\\?\` or `\\.\` - // then we need to replace `\\` with `\\?\UNC\`. - const bool is_unc = (wcsncmp(absolute_path.get(), L"\\\\", 2) == 0); - const wchar_t* kUNCLongPathPrefix = L"\\\\?\\UNC\\"; - const int kUNCLongPathPrefixLength = 8; - - // Add prefix and replace forward slashes with backward slashes. - // - // If the path is UNC we skip the first two characters of the path `\\` - // hence -2. - const intptr_t result_length = - (is_unc ? kUNCLongPathPrefixLength : kLongPathPrefixLength) + - path_length + (is_unc ? -2 : 0) + 1; - auto result = std::make_unique(result_length); - intptr_t result_pos; - intptr_t path_pos; - if (is_unc) { - wcsncpy(result.get(), kUNCLongPathPrefix, kUNCLongPathPrefixLength); - result_pos = kUNCLongPathPrefixLength; - path_pos = 2; - } else { - wcsncpy(result.get(), kLongPathPrefix, kLongPathPrefixLength); - result_pos = kLongPathPrefixLength; - path_pos = 0; - } - while (path_pos < path_length) { - wchar_t ch = absolute_path[path_pos++]; - result.get()[result_pos++] = ch == L'/' ? L'\\' : ch; - } - result.get()[result_pos++] = L'\0'; - ASSERT(result_pos == result_length); + auto result = std::unique_ptr(wcsdup(canonical_path)); + LocalFree(canonical_path); return result; } -// Converts the given UTF8 path to wide char. If resulting path does not -// fit into MAX_DIRECTORY_PATH (or if |force_long_prefix| is true) then -// converts the path to the absolute `\\?\`-prefixed form. -// -// Note: -// 1. Some WinAPI functions (like SetCurrentDirectoryW) are always limited -// to MAX_PATH long paths and converting to `\\?\`-prefixed form does not -// remove this limitation. Always check Win API documentation. -// 2. This function might change relative path to an absolute path. -static std::unique_ptr ToWinAPIFilePath( - const char* path, - bool force_long_prefix = false) { - return ToWinAPIPath(path, /*is_file=*/true, force_long_prefix); -} - -std::unique_ptr ToWinAPIDirectoryPath( - const char* path, - bool force_long_prefix /* = false */) { - return ToWinAPIPath(path, /*is_file=*/false, force_long_prefix); -} - File* File::Open(Namespace* namespc, const char* name, FileOpenMode mode) { - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); File* file = FileOpenW(path.get(), mode); return file; } @@ -557,7 +475,7 @@ static bool FileExists(const wchar_t* path) { } bool File::Exists(Namespace* namespc, const char* name) { - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); return FileExists(path.get()); } @@ -571,7 +489,7 @@ bool File::ExistsUri(Namespace* namespc, const char* uri) { } bool File::Create(Namespace* namespc, const char* name, bool exclusive) { - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); int flags = O_RDONLY | O_CREAT; if (exclusive) { flags |= O_EXCL; @@ -616,12 +534,12 @@ typedef struct _REPARSE_DATA_BUFFER { bool File::CreateLink(Namespace* namespc, const char* utf8_name, const char* utf8_target) { - const auto name = ToWinAPIFilePath(utf8_name); + const auto name = ToWinAPIPath(utf8_name); std::unique_ptr target; bool target_is_directory; if (File::IsAbsolutePath(utf8_target)) { - target = ToWinAPIFilePath(utf8_target); + target = ToWinAPIPath(utf8_target); target_is_directory = File::GetType(target.get(), /*follow_links=*/true) == kIsDirectory; } else { @@ -706,7 +624,7 @@ bool File::CreatePipe(Namespace* namespc, File** readPipe, File** writePipe) { } bool File::Delete(Namespace* namespc, const char* name) { - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); int status = _wremove(path.get()); return status != -1; } @@ -731,20 +649,20 @@ static bool DeleteLinkHelper(const wchar_t* path) { } bool File::DeleteLink(Namespace* namespc, const char* name) { - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); return DeleteLinkHelper(path.get()); } static bool RenameHelper(File::Type expected, const char* old_name, const char* new_name) { - const auto old_path = ToWinAPIFilePath(old_name); + const auto old_path = ToWinAPIPath(old_name); File::Type type = File::GetType(old_path.get(), /*follow_links=*/false); if (type != expected) { SetLastError(ERROR_FILE_NOT_FOUND); return false; } - const auto new_path = ToWinAPIFilePath(new_name); + const auto new_path = ToWinAPIPath(new_name); DWORD flags = MOVEFILE_WRITE_THROUGH | MOVEFILE_REPLACE_EXISTING; // Symbolic links (e.g. produced by Link.create) to directories on Windows @@ -860,8 +778,8 @@ bool File::Copy(Namespace* namespc, const char* new_name) { // We are going to concatenate new path with temporary file names in // CopyIntoTempFile so we force long prefix no matter what. - const auto old_path = ToWinAPIFilePath(old_name); - const auto new_path = ToWinAPIFilePath(new_name, /*force_long_prefix=*/true); + const auto old_path = ToWinAPIPath(old_name); + const auto new_path = ToWinAPIPath(new_name); File::Type type = GetType(old_path.get(), /*follow_links=*/false); if (type != kIsFile) { @@ -893,7 +811,7 @@ bool File::Copy(Namespace* namespc, int64_t File::LengthFromPath(Namespace* namespc, const char* name) { struct __stat64 st; - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); if (!StatHelper(path.get(), &st)) { return -1; } @@ -904,7 +822,7 @@ const char* File::LinkTarget(Namespace* namespc, const char* pathname, char* dest, int dest_size) { - const auto path = ToWinAPIFilePath(pathname); + const auto path = ToWinAPIPath(pathname); HANDLE dir_handle = CreateFileW( path.get(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, @@ -991,7 +909,7 @@ const char* File::LinkTarget(Namespace* namespc, } void File::Stat(Namespace* namespc, const char* name, int64_t* data) { - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); File::Type type = GetType(path.get(), /*follow_links=*/true); data[kType] = type; if (type != kDoesNotExist) { @@ -1011,7 +929,7 @@ void File::Stat(Namespace* namespc, const char* name, int64_t* data) { time_t File::LastAccessed(Namespace* namespc, const char* name) { struct __stat64 st; - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); if (!StatHelper(path.get(), &st)) { return -1; } @@ -1020,7 +938,7 @@ time_t File::LastAccessed(Namespace* namespc, const char* name) { time_t File::LastModified(Namespace* namespc, const char* name) { struct __stat64 st; - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); if (!StatHelper(path.get(), &st)) { return -1; } @@ -1031,7 +949,7 @@ bool File::SetLastAccessed(Namespace* namespc, const char* name, int64_t millis) { struct __stat64 st; - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); if (!StatHelper(path.get(), &st)) { // Checks that it is a file. return false; } @@ -1060,7 +978,7 @@ bool File::SetLastModified(Namespace* namespc, int64_t millis) { // First get the current times. struct __stat64 st; - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); if (!StatHelper(path.get(), &st)) { return false; } @@ -1089,7 +1007,7 @@ const char* File::GetCanonicalPath(Namespace* namespc, const char* pathname, char* dest, int dest_size) { - const auto path = ToWinAPIFilePath(pathname); + const auto path = ToWinAPIPath(pathname); HANDLE file_handle = CreateFileW(path.get(), 0, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); @@ -1191,7 +1109,7 @@ File::Type File::GetType(const wchar_t* path, bool follow_links) { File::Type File::GetType(Namespace* namespc, const char* name, bool follow_links) { - const auto path = ToWinAPIFilePath(name); + const auto path = ToWinAPIPath(name); return GetType(path.get(), follow_links); } @@ -1202,8 +1120,8 @@ File::Identical File::AreIdentical(Namespace* namespc_1, USE(namespc_1); USE(namespc_2); BY_HANDLE_FILE_INFORMATION file_info[2]; - const std::unique_ptr file_names[2] = {ToWinAPIFilePath(file_1), - ToWinAPIFilePath(file_2)}; + const std::unique_ptr file_names[2] = {ToWinAPIPath(file_1), + ToWinAPIPath(file_2)}; for (int i = 0; i < 2; ++i) { HANDLE file_handle = CreateFileW( file_names[i].get(), 0, diff --git a/runtime/bin/file_win.h b/runtime/bin/file_win.h index 9ba7da02b71..604a9bd121d 100644 --- a/runtime/bin/file_win.h +++ b/runtime/bin/file_win.h @@ -16,18 +16,12 @@ namespace dart { namespace bin { -// Converts the given UTF8 path to wide char. If resulting path does not -// fit into MAX_DIRECTORY_PATH (or if |force_long_prefix| is true) then -// converts the path to the absolute `\\?\`-prefixed form. +// Converts the given UTF8 path to wide char '\\?\'-prefix absolute path. // -// Note: -// 1. Some WinAPI functions (like SetCurrentDirectoryW) are always limited -// to MAX_PATH long paths and converting to `\\?\`-prefixed form does not -// remove this limitation. Always check Win API documentation. -// 2. This function might change relative path to an absolute path. -std::unique_ptr ToWinAPIDirectoryPath( - const char* path, - bool force_long_prefix = false); +// Note that some WinAPI functions (like SetCurrentDirectoryW) are always +// limited to MAX_PATH long paths and converting to `\\?\`-prefixed form does +// not remove this limitation. Always check Win API documentation. +std::unique_ptr ToWinAPIPath(const char* path); } // namespace bin } // namespace dart diff --git a/sdk/lib/io/common.dart b/sdk/lib/io/common.dart index 9816f1f39dc..a336e60a4c9 100644 --- a/sdk/lib/io/common.dart +++ b/sdk/lib/io/common.dart @@ -39,6 +39,7 @@ const _errorNetworkAccessDenied = 65; const _errorBadNetName = 67; const _errorFileExists = 80; const _errorDriveLocked = 108; +const _errorInvalidName = 123; const _errorBadPathName = 161; const _errorAlreadyExists = 183; const _errorFilenameExedRange = 206; diff --git a/sdk/lib/io/file.dart b/sdk/lib/io/file.dart index 040a04b5b8c..dbd8ba2b58e 100644 --- a/sdk/lib/io/file.dart +++ b/sdk/lib/io/file.dart @@ -173,6 +173,13 @@ class FileLock { /// In addition to length, the [exists], [lastModified], [stat], and /// other methods, are asynchronous. /// +/// ## Special 'nul' file +/// +/// On Linux and Mac '/dev/null' and on Windows '\\?\NUL' refer to a special file, +/// such that all writes to it get consumed and disappear, and all reads produce empty +/// output. Note that on Windows 'nul'(without '\\?\'-prefix) refers to a regular file +/// named 'nul' in current directory. +/// /// ## Other resources /// /// * The [Files and directories](https://dart.dev/guides/libraries/library-tour#files-and-directories) @@ -1063,6 +1070,7 @@ class FileSystemException implements IOException { case _errorFileNotFound: case _errorPathNotFound: case _errorInvalidDrive: + case _errorInvalidName: case _errorNoMoreFiles: case _errorBadNetpath: case _errorBadNetName: diff --git a/tests/standalone/io/directory_test.dart b/tests/standalone/io/directory_test.dart index c9befa8c934..d3ff9b58820 100644 --- a/tests/standalone/io/directory_test.dart +++ b/tests/standalone/io/directory_test.dart @@ -188,10 +188,20 @@ class DirectoryTest { buffer.write("/../${subDirName}"); } var long = new Directory("${buffer.toString()}"); - Future.value(long.delete()).catchError((error) { - Expect.isTrue(error is FileSystemException); - asyncEnd(); - }); + // Passes on Windows, fails everywhere else. + // Windows can handle path names up to 32k in length(provided that + // they are '\\?\'-prefixed, which they are internally in dart io), + // which is more than Linux or Mac can handle. + if (Platform.isWindows) { + long.delete().then((_) { + asyncEnd(); + }); + } else { + Future.value(long.delete()).catchError((error) { + Expect.isTrue(error is FileSystemException); + asyncEnd(); + }); + } }); }); } @@ -250,7 +260,12 @@ class DirectoryTest { Directory d = Directory.systemTemp.createTempSync('dart_directory_test'); StringBuffer buffer = createLongDirName(d); var long = new Directory("${buffer.toString()}"); - Expect.throws(long.deleteSync); + if (!Platform.isWindows) { + // Windows can handle path names up to 32k in length(provided that + // they are '\\?\'-prefixed, which they are internally in dart io), + // which is more than Linux or Mac can handle. + Expect.throws(long.deleteSync); + } d.deleteSync(recursive: true); } { diff --git a/tests/standalone/io/file_error_test.dart b/tests/standalone/io/file_error_test.dart index b03265b397e..c5f4f3bbe8e 100644 --- a/tests/standalone/io/file_error_test.dart +++ b/tests/standalone/io/file_error_test.dart @@ -23,7 +23,7 @@ bool checkCannotOpenFileException(e) { } else if (Platform.operatingSystem == "macos") { Expect.equals(2, e.osError.errorCode); } else if (Platform.operatingSystem == "windows") { - Expect.equals(3, e.osError.errorCode); + Expect.equals(123, e.osError.errorCode); } return true; } diff --git a/tests/standalone/io/file_windows_test.dart b/tests/standalone/io/file_windows_test.dart index 450f15f05e4..08b69eda399 100644 --- a/tests/standalone/io/file_windows_test.dart +++ b/tests/standalone/io/file_windows_test.dart @@ -8,12 +8,8 @@ import "package:expect/expect.dart"; /// Regression test for https://github.com/dart-lang/sdk/issues/54386. void testDriveLetterStat() { - // "C:" not acceptable - final cDrive = Directory("C:"); - final cDriveStat = cDrive.statSync(); - Expect.equals(cDriveStat.type, FileSystemEntityType.notFound); // These are acceptable cases - final acceptablePathRootDrives = ["C: ", "C:\\", "C:/"]; + final acceptablePathRootDrives = ["C:", "C: ", "C:\\", "C:/"]; for (final drivePath in acceptablePathRootDrives) { final dir = Directory(drivePath); final dirStat = dir.statSync(); @@ -21,6 +17,35 @@ void testDriveLetterStat() { } } +// Check that "C:abc" refers +// either to a file at current directory if current directory is at "C:" drive, +// or to a file at "C:\\abc", if current directory is not at "C:" drive. +// +// Do this check by looking at system temp directory entries, converting them +// to no-backslash-after-drive-letter form, confirming that original and +// converted refer to the same thing. +void testDriveLetterNoBackslash() { + final current = Directory.current.path; + for (var e in Directory.systemTemp.listSync()) { + final path = e.path; + if (path.length < 3) return; + if (path[1] == ':' && path[2] == "\\") { + final driveletter = path[0]; + var noBackslash = path.substring(0, 2); + if (path.substring(0, 3).compareTo(current.substring(0, 3)) == 0) { + for (int i = 0; i < current.length; i++) { + if (current[i] == '\\') { + noBackslash += "..\\"; + } + } + } + noBackslash += path.substring(3); + Expect.equals("${Directory(noBackslash).statSync()}", + "${Directory(path).statSync()}"); + } + } +} + void testDeleteLongPathPrefix() { var dir = Directory.systemTemp.createTempSync('dart_file_win'); var dirPath = "\\\\?\\${dir.path}"; @@ -41,4 +66,5 @@ void main() { if (!Platform.isWindows) return; testDeleteLongPathPrefix(); testDriveLetterStat(); + testDriveLetterNoBackslash(); } diff --git a/tests/standalone/io/file_write_as_test.dart b/tests/standalone/io/file_write_as_test.dart index 786a9649a59..15f2dd32863 100644 --- a/tests/standalone/io/file_write_as_test.dart +++ b/tests/standalone/io/file_write_as_test.dart @@ -60,7 +60,7 @@ testWriteWithLargeList(dir) { return; } if (Platform.isWindows) { - File('NUL').writeAsBytesSync(bytes); + File('\\\\?\\NUL').writeAsBytesSync(bytes); } else { File('/dev/null').writeAsBytesSync(bytes); }