Skip to content

Commit 3dc15ef

Browse files
committed
Refactor canonical_path: simplify canonical_path::parent
I want canonical_path to work with both POSIX paths and Windows paths. Make this easier by determining path components ahead of time rather than when canonical_path::parent is called. This commit should not change behavior, except on Windows for non-ASCII paths. Non-ASCII paths are already broken though; see the TODO comment in windows_path_canonicalizer::result.
1 parent 664c98e commit 3dc15ef

File tree

3 files changed

+108
-30
lines changed

3 files changed

+108
-30
lines changed

src/file-canonical.cpp

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,48 @@ struct canonicalizing_path_io_error {
9898

9999
canonical_path::canonical_path(std::string &&path) : path_(std::move(path)) {
100100
QLJS_ASSERT(!this->path_.empty());
101+
102+
#if QLJS_PATHS_POSIX
103+
std::string_view p(this->path_);
104+
while (!ends_with(p, "/")) {
105+
std::size_t slash_index = p.find_last_of("/");
106+
QLJS_ASSERT(slash_index != std::string::npos);
107+
if (slash_index == 0 || slash_index == 1) {
108+
// Preserve the slash for resulting root paths.
109+
this->path_lengths_.push_back(slash_index + 1);
110+
p = p.substr(0, slash_index + 1);
111+
} else {
112+
this->path_lengths_.push_back(slash_index);
113+
p = p.substr(0, slash_index);
114+
}
115+
}
116+
std::reverse(this->path_lengths_.begin(), this->path_lengths_.end());
117+
#elif QLJS_PATHS_WIN32
118+
// TODO(strager): path_lengths_ is in UTF-16 code units, but it's interpreted
119+
// as UTF-8 code units! Fix by storing a std::wstring in canonical_path
120+
// instead of a std::string.
121+
// TODO(strager): Avoid string conversions and copying.
122+
std::optional<std::wstring> wpath = mbstring_to_wstring(this->path_.c_str());
123+
QLJS_ASSERT(wpath.has_value());
124+
for (;;) {
125+
HRESULT result = ::PathCchRemoveFileSpec(wpath->data(), wpath->size() + 1);
126+
switch (result) {
127+
case S_OK:
128+
this->path_lengths_.push_back(std::wcslen(wpath->data()));
129+
break;
130+
case S_FALSE:
131+
// Path is a root path already.
132+
goto done;
133+
default:
134+
QLJS_UNIMPLEMENTED();
135+
break;
136+
}
137+
}
138+
done:
139+
std::reverse(this->path_lengths_.begin(), this->path_lengths_.end());
140+
#else
141+
#error "Unsupported platform"
142+
#endif
101143
}
102144

103145
std::string_view canonical_path::path() const &noexcept { return this->path_; }
@@ -135,6 +177,7 @@ bool operator!=(const canonical_path &lhs, std::string_view rhs) noexcept {
135177
}
136178

137179
void canonical_path::append_component(std::string_view component) {
180+
this->path_lengths_.push_back(this->path_.size());
138181
if (this->path_[this->path_.size() - 1] !=
139182
preferred_component_separator_char) {
140183
this->path_.push_back(preferred_component_separator_char);
@@ -143,39 +186,13 @@ void canonical_path::append_component(std::string_view component) {
143186
}
144187

145188
bool canonical_path::parent() {
146-
#if QLJS_PATHS_POSIX
147-
if (this->path_[this->path_.size() - 1] == '/') {
189+
if (this->path_lengths_.empty()) {
148190
return false;
149191
}
150-
std::size_t slash_index = this->path_.find_last_of("/");
151-
QLJS_ASSERT(slash_index != std::string::npos);
152-
if (slash_index == 0 || slash_index == 1) {
153-
// Preserve the slash for resulting root paths.
154-
this->path_.resize(slash_index + 1);
155-
} else {
156-
this->path_.resize(slash_index);
157-
}
192+
std::size_t path_length = this->path_lengths_.back();
193+
this->path_lengths_.pop_back();
194+
this->path_.resize(path_length);
158195
return true;
159-
#elif QLJS_PATHS_WIN32
160-
// TODO(strager): Avoid string conversions and copying.
161-
std::optional<std::wstring> wpath = mbstring_to_wstring(this->path_.c_str());
162-
QLJS_ASSERT(wpath.has_value());
163-
HRESULT result = ::PathCchRemoveFileSpec(wpath->data(), wpath->size() + 1);
164-
switch (result) {
165-
case S_OK:
166-
wpath->resize(std::wcslen(wpath->data()));
167-
this->path_ = std::filesystem::path(*wpath).string();
168-
return true;
169-
case S_FALSE:
170-
// Path is a root path already.
171-
return false;
172-
default:
173-
QLJS_UNIMPLEMENTED();
174-
break;
175-
}
176-
#else
177-
#error "Unsupported platform"
178-
#endif
179196
}
180197

181198
canonical_path_result::canonical_path_result(std::string &&path,
@@ -207,6 +224,10 @@ bool canonical_path_result::have_missing_components() const noexcept {
207224
}
208225

209226
void canonical_path_result::drop_missing_components() {
227+
while (!this->path_.path_lengths_.empty() &&
228+
this->path_.path_lengths_.back() >= this->existing_path_length_) {
229+
this->path_.path_lengths_.pop_back();
230+
}
210231
this->path_.path_.resize(this->existing_path_length_);
211232
}
212233

src/quick-lint-js/file-canonical.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <quick-lint-js/result.h>
1616
#include <string>
1717
#include <string_view>
18+
#include <vector>
1819

1920
namespace quick_lint_js {
2021
class canonical_path_result;
@@ -72,6 +73,15 @@ class canonical_path {
7273
private:
7374
std::string path_;
7475

76+
// On POSIX, if path_ is "/hello/world/x", then path_lengths_ is {1, 6, 12}:
77+
//
78+
// /hello/world/x
79+
// ^ ^ ^
80+
// 1 6 12
81+
//
82+
// If path_ is a root path, then path_lengths_ is empty.
83+
std::vector<std::size_t> path_lengths_;
84+
7585
friend canonical_path_result;
7686
};
7787

test/test-file-canonical.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@ TEST_F(test_file_canonical, canonical_path_to_non_existing_file_succeeds) {
145145
EXPECT_EQ(canonical->path(), temp_dir_canonical->path());
146146
}
147147

148+
TEST_F(test_file_canonical,
149+
parent_of_present_canonical_components_of_non_existing_file) {
150+
std::string temp_dir = this->make_temporary_directory();
151+
result<canonical_path_result, canonicalize_path_io_error> temp_dir_canonical =
152+
canonicalize_path(temp_dir);
153+
ASSERT_TRUE(temp_dir_canonical.ok())
154+
<< temp_dir_canonical.error().to_string();
155+
156+
create_directory(temp_dir + "/dir");
157+
result<canonical_path_result, canonicalize_path_io_error> canonicalized =
158+
canonicalize_path(temp_dir + "/dir/does-not-exist.txt");
159+
ASSERT_TRUE(canonicalized.ok()) << canonicalized.error().to_string();
160+
canonicalized->drop_missing_components();
161+
162+
canonical_path canonical = std::move(*canonicalized).canonical();
163+
canonical.parent();
164+
EXPECT_EQ(canonical.path(), temp_dir_canonical->path());
165+
}
166+
148167
TEST_F(test_file_canonical,
149168
canonical_path_to_non_existing_parent_directory_succeeds) {
150169
std::string temp_dir = this->make_temporary_directory();
@@ -987,6 +1006,20 @@ TEST_F(test_file_canonical, append_component_posix) {
9871006
<< "before = " << tc.before << "\ncomponent = " << tc.component;
9881007
}
9891008
}
1009+
1010+
TEST_F(test_file_canonical, remove_appended_component_posix) {
1011+
canonical_path p("/dir/subdir");
1012+
p.append_component("file.txt");
1013+
p.parent();
1014+
EXPECT_EQ(p.path(), "/dir/subdir");
1015+
}
1016+
1017+
TEST_F(test_file_canonical, remove_component_appended_to_root_posix) {
1018+
canonical_path p("/");
1019+
p.append_component("file.txt");
1020+
p.parent();
1021+
EXPECT_EQ(p.path(), "/");
1022+
}
9901023
#endif
9911024

9921025
#if defined(_WIN32)
@@ -1039,6 +1072,20 @@ TEST_F(test_file_canonical, append_component_win32) {
10391072
<< "before = " << tc.before << "\ncomponent = " << tc.component;
10401073
}
10411074
}
1075+
1076+
TEST_F(test_file_canonical, remove_appended_component_win32) {
1077+
canonical_path p(R"(X:\dir\subdir)");
1078+
p.append_component("file.txt");
1079+
p.parent();
1080+
EXPECT_EQ(p.path(), R"(X:\dir\subdir)");
1081+
}
1082+
1083+
TEST_F(test_file_canonical, remove_component_appended_to_root_win32) {
1084+
canonical_path p(R"(X:\)");
1085+
p.append_component("file.txt");
1086+
p.parent();
1087+
EXPECT_EQ(p.path(), R"(X:\)");
1088+
}
10421089
#endif
10431090
}
10441091
}

0 commit comments

Comments
 (0)