Commit dd734c75 authored by Kenton Varda's avatar Kenton Varda

Add Path::parseWin32Api() and Path::forWin32Api() helpers.

There are two purposes:

1. Convert all the way to/from wchar_t, to make interfacing with Win32 filesystem APIs easier.
2. Use and handle '\\?\'-style paths. This prefix apparently opts into support for longer filenames. This seems like a good idea for any new programs! (It also opts out of support for using forward slashes as path separators and handling of '..', but kj::Path already handles these, so great.)
parent 5725bfc3
......@@ -21,6 +21,7 @@
#include "filesystem.h"
#include "test.h"
#include <wchar.h>
namespace kj {
namespace {
......@@ -108,6 +109,14 @@ KJ_TEST("Path exceptions") {
KJ_EXPECT_THROW_MESSAGE("root path has no parent", Path(nullptr).parent());
}
static inline bool operator==(const Array<wchar_t>& arr, const wchar_t* expected) {
return wcscmp(arr.begin(), expected) == 0;
}
constexpr kj::ArrayPtr<const wchar_t> operator "" _a(const wchar_t* str, size_t n) {
return { str, n };
}
KJ_TEST("Win32 Path") {
KJ_EXPECT(Path({"foo", "bar"}).toWin32String() == "foo\\bar");
KJ_EXPECT(Path({"foo", "bar"}).toWin32String(true) == "\\\\foo\\bar");
......@@ -147,6 +156,16 @@ KJ_TEST("Win32 Path") {
.toWin32String(true) == "d:\\qux");
KJ_EXPECT(Path({"foo", "bar", "baz"}).evalWin32("\\qux")
.toWin32String(true) == "\\\\foo\\bar\\qux");
KJ_EXPECT(Path({"foo", "bar"}).forWin32Api(false) == L"foo\\bar");
KJ_EXPECT(Path({"foo", "bar"}).forWin32Api(true) == L"\\\\?\\UNC\\foo\\bar");
KJ_EXPECT(Path({"c:", "foo", "bar"}).forWin32Api(true) == L"\\\\?\\c:\\foo\\bar");
KJ_EXPECT(Path({"A:", "foo", "bar"}).forWin32Api(true) == L"\\\\?\\A:\\foo\\bar");
KJ_EXPECT(Path::parseWin32Api(L"\\\\?\\c:\\foo\\bar"_a).toString() == "c:/foo/bar");
KJ_EXPECT(Path::parseWin32Api(L"\\\\?\\UNC\\foo\\bar"_a).toString() == "foo/bar");
KJ_EXPECT(Path::parseWin32Api(L"c:\\foo\\bar"_a).toString() == "c:/foo/bar");
KJ_EXPECT(Path::parseWin32Api(L"\\\\foo\\bar"_a).toString() == "foo/bar");
}
KJ_TEST("Win32 Path exceptions") {
......
......@@ -23,6 +23,7 @@
#include "vector.h"
#include "debug.h"
#include "one-of.h"
#include "encoding.h"
#include "refcount.h"
#include <map>
......@@ -55,6 +56,11 @@ Path Path::parse(StringPtr path) {
return evalImpl(Vector<String>(countParts(path)), path);
}
Path Path::parseWin32Api(ArrayPtr<const wchar_t> text) {
auto utf8 = decodeWideString(text);
return evalWin32Impl(Vector<String>(countPartsWin32(utf8)), utf8, true);
}
Path PathPtr::append(Path&& suffix) const {
auto newParts = kj::heapArrayBuilder<String>(parts.size() + suffix.parts.size());
for (auto& p: parts) newParts.add(heapString(p));
......@@ -160,7 +166,7 @@ Path Path::evalWin32(StringPtr pathText) && {
return evalWin32Impl(kj::mv(newParts), pathText);
}
String PathPtr::toWin32String(bool absolute) const {
String PathPtr::toWin32StringImpl(bool absolute, bool forApi) const {
if (parts.size() == 0) {
// Special-case empty path.
KJ_REQUIRE(!absolute, "absolute path is missing disk designator") {
......@@ -179,18 +185,36 @@ String PathPtr::toWin32String(bool absolute) const {
KJ_FAIL_REQUIRE("absolute win32 path must start with drive letter or netbios host name",
parts[0]);
}
} else {
// Currently we do nothing differently in the forApi case for relative paths.
forApi = false;
}
size_t size = (isUncPath ? 2 : 0) + (parts.size() - 1);
size_t size = forApi
? (isUncPath ? 8 : 4) + (parts.size() - 1)
: (isUncPath ? 2 : 0) + (parts.size() - 1);
for (auto& p: parts) size += p.size();
String result = heapString(size);
char* ptr = result.begin();
if (isUncPath) {
if (forApi) {
*ptr++ = '\\';
*ptr++ = '\\';
*ptr++ = '?';
*ptr++ = '\\';
if (isUncPath) {
*ptr++ = 'U';
*ptr++ = 'N';
*ptr++ = 'C';
*ptr++ = '\\';
}
} else {
if (isUncPath) {
*ptr++ = '\\';
*ptr++ = '\\';
}
}
bool leadingSlash = false;
......@@ -218,7 +242,7 @@ String PathPtr::toWin32String(bool absolute) const {
// appearing to start with a drive letter.
for (size_t i: kj::indices(result)) {
if (result[i] == ':') {
if (absolute && i == 1) {
if (absolute && i == (forApi ? 5 : 1)) {
// False alarm: this is the drive letter.
} else {
KJ_FAIL_REQUIRE(
......@@ -234,6 +258,10 @@ String PathPtr::toWin32String(bool absolute) const {
return result;
}
Array<wchar_t> PathPtr::forWin32Api(bool absolute) const {
return encodeWideString(toWin32StringImpl(absolute, true), true);
}
// -----------------------------------------------------------------------------
String Path::stripNul(String input) {
......@@ -291,10 +319,10 @@ Path Path::evalImpl(Vector<String>&& parts, StringPtr path) {
return Path(parts.releaseAsArray(), Path::ALREADY_CHECKED);
}
Path Path::evalWin32Impl(Vector<String>&& parts, StringPtr path) {
Path Path::evalWin32Impl(Vector<String>&& parts, StringPtr path, bool fromApi) {
// Convert all forward slashes to backslashes.
String ownPath;
if (path.findFirst('/') != nullptr) {
if (!fromApi && path.findFirst('/') != nullptr) {
ownPath = heapString(path);
for (char& c: ownPath) {
if (c == '/') c = '\\';
......@@ -303,13 +331,23 @@ Path Path::evalWin32Impl(Vector<String>&& parts, StringPtr path) {
}
// Interpret various forms of absolute paths.
if (path.startsWith("\\\\")) {
if (fromApi && path.startsWith("\\\\?\\")) {
path = path.slice(4);
if (path.startsWith("UNC\\")) {
path = path.slice(4);
}
// The path is absolute.
parts.clear();
} else if (path.startsWith("\\\\")) {
// UNC path.
path = path.slice(2);
// This path is absolute. The first component is a server name.
parts.clear();
} else if (path.startsWith("\\")) {
KJ_REQUIRE(!fromApi, "parseWin32Api() requires absolute path");
// Path is relative to the current drive / network share.
if (parts.size() >= 1 && isWin32Drive(parts[0])) {
// Leading \ interpreted as root of current drive.
......@@ -330,6 +368,8 @@ Path Path::evalWin32Impl(Vector<String>&& parts, StringPtr path) {
isWin32Drive(path.slice(0, 2))) {
// Starts with a drive letter.
parts.clear();
} else {
KJ_REQUIRE(!fromApi, "parseWin32Api() requires absolute path");
}
size_t partStart = 0;
......
......@@ -151,7 +151,8 @@ public:
Path evalWin32(StringPtr pathText) const&;
Path evalWin32(StringPtr pathText) &&;
// Evaluates a Win32-style path. Differences from `eval()` include:
// Evaluates a Win32-style path, as might be written by a user. Differences from `eval()`
// include:
//
// - Backslashes can be used as path separators.
// - Absolute paths begin with a drive letter followed by a colon. The drive letter, including
......@@ -159,9 +160,9 @@ public:
// - A network path like "\\host\share\path" is parsed as {"host", "share", "path"}.
String toWin32String(bool absolute = false) const;
// Converts the path to a Win32 path string.
// Converts the path to a Win32 path string, as you might display to a user.
//
// (In most cases you'll want to further convert the returned string from UTF-8 to UTF-16.)
// This is meant for display. For making Win32 system calls, consider `toWin32Api()` instead.
//
// If `absolute` is true, the path is expected to be an absolute path, meaning the first
// component is a drive letter, namespace, or network host name. These are converted to their
......@@ -171,6 +172,24 @@ public:
// Windows, such as if it contains backslashes (within a path component), colons, or special
// names like "con".
Array<wchar_t> forWin32Api(bool absolute) const;
// Like toWin32String, but additionally:
// - Converts the path to UTF-16, with a NUL terminator included.
// - For absolute paths, adds the "\\?\" prefix which opts into permitting paths longer than
// MAX_PATH, and turns off relative path processing (which KJ paths already handle in userspace
// anyway).
//
// This method is good to use when making a Win32 API call, e.g.:
//
// DeleteFileW(path.forWin32Api(true).begin());
static Path parseWin32Api(ArrayPtr<const wchar_t> text);
// Parses an absolute path as returned by a Win32 API call like GetFinalPathNameByHandle() or
// GetCurrentDirectory(). A "\\?\" prefix is optional but understood if present.
//
// Since such Win32 API calls generally return a length, this function inputs an array slice.
// The slice should not include any NUL terminator.
private:
Array<String> parts;
......@@ -186,7 +205,7 @@ private:
static void validatePart(StringPtr part);
static void evalPart(Vector<String>& parts, ArrayPtr<const char> part);
static Path evalImpl(Vector<String>&& parts, StringPtr path);
static Path evalWin32Impl(Vector<String>&& parts, StringPtr path);
static Path evalWin32Impl(Vector<String>&& parts, StringPtr path, bool fromApi = false);
static size_t countParts(StringPtr path);
static size_t countPartsWin32(StringPtr path);
static bool isWin32Drive(ArrayPtr<const char> part);
......@@ -219,6 +238,7 @@ public:
PathPtr slice(size_t start, size_t end) const;
Path evalWin32(StringPtr pathText) const;
String toWin32String(bool absolute = false) const;
Array<wchar_t> forWin32Api(bool absolute) const;
// Equivalent to the corresponding methods of `Path`.
private:
......@@ -226,6 +246,8 @@ private:
explicit PathPtr(ArrayPtr<const String> parts);
String toWin32StringImpl(bool absolute, bool forApi) const;
friend class Path;
};
......@@ -880,6 +902,9 @@ inline Path Path::evalWin32(StringPtr pathText) const& {
inline String Path::toWin32String(bool absolute) const {
return PathPtr(*this).toWin32String(absolute);
}
inline Array<wchar_t> Path::forWin32Api(bool absolute) const {
return PathPtr(*this).forWin32Api(absolute);
}
inline PathPtr::PathPtr(decltype(nullptr)): parts(nullptr) {}
inline PathPtr::PathPtr(const Path& path): parts(path.parts) {}
......@@ -893,6 +918,9 @@ inline const String* PathPtr::end() const { return parts.end(); }
inline PathPtr PathPtr::slice(size_t start, size_t end) const {
return PathPtr(parts.slice(start, end));
}
inline String PathPtr::toWin32String(bool absolute) const {
return toWin32StringImpl(absolute, false);
}
inline Own<FsNode> FsNode::clone() { return cloneFsNode().downcast<FsNode>(); }
inline Own<ReadableFile> ReadableFile::clone() { return cloneFsNode().downcast<ReadableFile>(); }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment