From e3e7813e5f652a95e6510c2e13734fae40647b03 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Mon, 22 May 2017 15:24:35 -0700 Subject: [PATCH] Add (partial) support for Windows long paths * Update android::base::utf8::open/unlink to support Windows long paths * Add android::base::utf8::fopen, also with support for Windows long paths * Upcoming CLs will add additional APIs to support additional use cases Test: Added tests to utf8_test Bug: 38268753 Change-Id: If72af327f3487766f5370a2f43ee9cabd4a8a810 --- base/include/android-base/utf8.h | 19 ++++++++ base/utf8.cpp | 52 +++++++++++++++++++++- base/utf8_test.cpp | 76 ++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/base/include/android-base/utf8.h b/base/include/android-base/utf8.h index 2d5a6f6d7..c9cc1ab0c 100755 --- a/base/include/android-base/utf8.h +++ b/base/include/android-base/utf8.h @@ -22,6 +22,8 @@ #else // Bring in prototypes for standard APIs so that we can import them into the utf8 namespace. #include // open +#include // fopen +#include // mkdir #include // unlink #endif @@ -53,6 +55,19 @@ bool UTF8ToWide(const char* utf8, std::wstring* utf16); // Convert a UTF-8 std::string (including any embedded NULL characters) to // UTF-16. Returns whether the conversion was done successfully. bool UTF8ToWide(const std::string& utf8, std::wstring* utf16); + +// Convert a file system path, represented as a NULL-terminated string of +// UTF-8 characters, to a UTF-16 string representing the same file system +// path using the Windows extended-lengh path representation. +// +// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#MAXPATH: +// ```The Windows API has many functions that also have Unicode versions to +// permit an extended-length path for a maximum total path length of 32,767 +// characters. To specify an extended-length path, use the "\\?\" prefix. +// For example, "\\?\D:\very long path".``` +// +// Returns whether the conversion was done successfully. +bool UTF8PathToWindowsLongPath(const char* utf8, std::wstring* utf16); #endif // The functions in the utf8 namespace take UTF-8 strings. For Windows, these @@ -73,9 +88,13 @@ bool UTF8ToWide(const std::string& utf8, std::wstring* utf16); namespace utf8 { #ifdef _WIN32 +FILE* fopen(const char* name, const char* mode); +int mkdir(const char* name, mode_t mode); int open(const char* name, int flags, ...); int unlink(const char* name); #else +using ::fopen; +using ::mkdir; using ::open; using ::unlink; #endif diff --git a/base/utf8.cpp b/base/utf8.cpp index 3cca70026..5984fb06c 100644 --- a/base/utf8.cpp +++ b/base/utf8.cpp @@ -19,7 +19,9 @@ #include "android-base/utf8.h" #include +#include +#include #include #include "android-base/logging.h" @@ -153,12 +155,58 @@ bool UTF8ToWide(const std::string& utf8, std::wstring* utf16) { return UTF8ToWide(utf8.c_str(), utf8.length(), utf16); } +static bool isDriveLetter(wchar_t c) { + return (c >= L'a' && c <= L'z') || (c >= L'A' && c <= L'Z'); +} + +bool UTF8PathToWindowsLongPath(const char* utf8, std::wstring* utf16) { + if (!UTF8ToWide(utf8, utf16)) { + return false; + } + // Note: Although most Win32 File I/O API are limited to MAX_PATH (260 + // characters), the CreateDirectory API is limited to 248 characters. + if (utf16->length() >= 248) { + // If path is of the form "x:\" or "x:/" + if (isDriveLetter((*utf16)[0]) && (*utf16)[1] == L':' && + ((*utf16)[2] == L'\\' || (*utf16)[2] == L'/')) { + // Append long path prefix, and make sure there are no unix-style + // separators to ensure a fully compliant Win32 long path string. + utf16->insert(0, LR"(\\?\)"); + std::replace(utf16->begin(), utf16->end(), L'/', L'\\'); + } + } + return true; +} + // Versions of standard library APIs that support UTF-8 strings. namespace utf8 { +FILE* fopen(const char* name, const char* mode) { + std::wstring name_utf16; + if (!UTF8PathToWindowsLongPath(name, &name_utf16)) { + return nullptr; + } + + std::wstring mode_utf16; + if (!UTF8ToWide(mode, &mode_utf16)) { + return nullptr; + } + + return _wfopen(name_utf16.c_str(), mode_utf16.c_str()); +} + +int mkdir(const char* name, mode_t mode) { + std::wstring name_utf16; + if (!UTF8PathToWindowsLongPath(name, &name_utf16)) { + return -1; + } + + return _wmkdir(name_utf16.c_str()); +} + int open(const char* name, int flags, ...) { std::wstring name_utf16; - if (!UTF8ToWide(name, &name_utf16)) { + if (!UTF8PathToWindowsLongPath(name, &name_utf16)) { return -1; } @@ -175,7 +223,7 @@ int open(const char* name, int flags, ...) { int unlink(const char* name) { std::wstring name_utf16; - if (!UTF8ToWide(name, &name_utf16)) { + if (!UTF8PathToWindowsLongPath(name, &name_utf16)) { return -1; } diff --git a/base/utf8_test.cpp b/base/utf8_test.cpp index ae8fc8c79..fcb25c350 100644 --- a/base/utf8_test.cpp +++ b/base/utf8_test.cpp @@ -18,7 +18,12 @@ #include +#include +#include + #include "android-base/macros.h" +#include "android-base/test_utils.h" +#include "android-base/unique_fd.h" namespace android { namespace base { @@ -408,5 +413,76 @@ TEST(SysStrings, SysUTF8ToWide) { EXPECT_EQ(expected_null, SysUTF8ToWide(utf8_null)); } +TEST(UTF8PathToWindowsLongPathTest, DontAddPrefixIfShorterThanMaxPath) { + std::string utf8 = "c:\\mypath\\myfile.txt"; + + std::wstring wide; + EXPECT_TRUE(UTF8PathToWindowsLongPath(utf8.c_str(), &wide)); + + EXPECT_EQ(std::string::npos, wide.find(LR"(\\?\)")); +} + +TEST(UTF8PathToWindowsLongPathTest, AddPrefixIfLongerThanMaxPath) { + std::string utf8 = "c:\\mypath"; + while (utf8.length() < 300 /* MAX_PATH is 260 */) { + utf8 += "\\mypathsegment"; + } + + std::wstring wide; + EXPECT_TRUE(UTF8PathToWindowsLongPath(utf8.c_str(), &wide)); + + EXPECT_EQ(0U, wide.find(LR"(\\?\)")); + EXPECT_EQ(std::string::npos, wide.find(L"/")); +} + +TEST(UTF8PathToWindowsLongPathTest, AddPrefixAndFixSeparatorsIfLongerThanMaxPath) { + std::string utf8 = "c:/mypath"; + while (utf8.length() < 300 /* MAX_PATH is 260 */) { + utf8 += "/mypathsegment"; + } + + std::wstring wide; + EXPECT_TRUE(UTF8PathToWindowsLongPath(utf8.c_str(), &wide)); + + EXPECT_EQ(0U, wide.find(LR"(\\?\)")); + EXPECT_EQ(std::string::npos, wide.find(L"/")); +} + +namespace utf8 { + +TEST(Utf8FilesTest, CanCreateOpenAndDeleteFileWithLongPath) { + TemporaryDir td; + + // Create long directory path + std::string utf8 = td.path; + while (utf8.length() < 300 /* MAX_PATH is 260 */) { + utf8 += "\\mypathsegment"; + EXPECT_EQ(0, mkdir(utf8.c_str(), 0)); + } + + // Create file + utf8 += "\\test-file.bin"; + int flags = O_WRONLY | O_CREAT | O_TRUNC | O_BINARY; + int mode = 0666; + android::base::unique_fd fd(open(utf8.c_str(), flags, mode)); + EXPECT_NE(-1, fd.get()); + + // Close file + fd.reset(); + EXPECT_EQ(-1, fd.get()); + + // Open file with fopen + FILE* file = fopen(utf8.c_str(), "rb"); + EXPECT_NE(nullptr, file); + + if (file) { + fclose(file); + } + + // Delete file + EXPECT_EQ(0, unlink(utf8.c_str())); +} + +} // namespace utf8 } // namespace base } // namespace android