Skip to content

Commit

Permalink
fix(io): fix canonicalization of C:\missing.txt\..
Browse files Browse the repository at this point in the history
POSIX and Windows resolve '..' paths differently. canonicalize_path
implements POSIX semantics, which is wrong on Windows.

Teach canonicalize_path about Windows's '..' semantics.

Also, implement canonicalization of paths like "C:foo.txt" (untested).
  • Loading branch information
strager committed Feb 10, 2024
1 parent 434d085 commit 6d7bf2d
Show file tree
Hide file tree
Showing 8 changed files with 539 additions and 75 deletions.
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ quick_lint_js_add_library(
quick-lint-js/fe/language-debug.cpp
quick-lint-js/fe/lex-debug.cpp
quick-lint-js/i18n/po-parser-debug.cpp
quick-lint-js/io/file-path-debug.cpp
quick-lint-js/lsp/lsp-location-debug.cpp
quick-lint-js/port/char8-debug.cpp
)
Expand Down
57 changes: 12 additions & 45 deletions src/quick-lint-js/io/file-canonical.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@ class Path_Canonicalizer_Base {
path_to_process_ = path_to_process_.substr(next_component_index);
}

// TODO(strager): Have canonicalize_path accept an allocator.
Monotonic_Allocator allocator_{"Path_Canonicalizer_Base"};

Canonicalize_Observer *observer_;
Path_String_View original_path_;

Expand Down Expand Up @@ -583,51 +586,15 @@ class Windows_Path_Canonicalizer

quick_lint_js::Result<void, Canonicalizing_Path_IO_Error>
process_start_of_path() {
std::wstring temp(path_to_process_);

// The PathCch functions only support '\' as a directory separator. Convert
// all '/'s into '\'s.
for (wchar_t &c : temp) {
if (c == L'/') {
c = L'\\';
}
}

wchar_t *root_end;
HRESULT result = ::PathCchSkipRoot(temp.data(), &root_end);
switch (result) {
case S_OK:
// Path is absolute.
QLJS_ASSERT(root_end != temp.data());

path_to_process_ = path_to_process_.substr(root_end - temp.data());
skip_to_next_component();

// Drop '\' from 'C:\' if present.
if (root_end[-1] == L'\\') {
--root_end;
}
canonical_.assign(temp.data(), root_end);

need_root_slash_ = true;
break;

case HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER): {
// Path is invalid or is relative. Assume that it is relative.
quick_lint_js::Result<void, Windows_File_IO_Error> r = load_cwd();
if (!r.ok()) {
return failed_result(Canonicalizing_Path_IO_Error{
.canonicalizing_path = Path_String(this->path_to_process_),
.io_error = r.error(),
});
}
break;
}

default:
QLJS_UNIMPLEMENTED();
break;
}
// @@@ what about empty path?

// FIXME(strager): Do we need to copy (std::wstring) to add the null
// terminator?
Simplified_Path simplified_path = simplify_path_and_make_absolute(
&this->allocator_, std::wstring(path_to_process_).c_str());
this->canonical_ = simplified_path.root;
this->path_to_process_ = simplified_path.relative;
this->need_root_slash_ = true;

return {};
}
Expand Down
42 changes: 42 additions & 0 deletions src/quick-lint-js/io/file-path-debug.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (C) 2020 Matthew "strager" Glazar
// See end of file for extended copyright information.

#include <ostream>
#include <quick-lint-js/io/file-path.h>
#include <quick-lint-js/util/utf-16.h>
#include <string_view>

namespace quick_lint_js {
#if defined(_WIN32)
std::ostream& operator<<(std::ostream& out, Simplified_Path path) {
auto write_field = [&](const char* name, std::wstring_view s) -> void {
out << " ." << name << " = \"" << wstring_to_mbstring(s).value()
<< "\",\n";
};
out << "Simplified_Path{\n";
write_field("full_path", path.full_path);
write_field("root", path.root);
write_field("relative", path.relative);
out << "}";
return out;
}
#endif
}

// quick-lint-js finds bugs in JavaScript programs.
// Copyright (C) 2020 Matthew "strager" Glazar
//
// This file is part of quick-lint-js.
//
// quick-lint-js is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// quick-lint-js is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with quick-lint-js. If not, see <https://www.gnu.org/licenses/>.
72 changes: 72 additions & 0 deletions src/quick-lint-js/io/file-path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See end of file for extended copyright information.

#include <quick-lint-js/assert.h>
#include <quick-lint-js/container/string-view.h>
#include <quick-lint-js/io/file-path.h>
#include <quick-lint-js/port/have.h>
#include <quick-lint-js/util/ascii.h>
Expand Down Expand Up @@ -160,6 +161,77 @@ std::string_view path_file_name(std::string_view path) {
}
return path.substr(last_slash_index + 1);
}

#if defined(_WIN32)
Simplified_Path simplify_path_and_make_absolute(Monotonic_Allocator* allocator,
const wchar_t* path) {
Span<wchar_t> absolute_path_buffer;
if (path[0] == L'\\' && path[1] == L'\\' && path[2] == L'?' &&
path[3] == L'\\') {
// ::GetFullPathNameW mangles \\?\ paths, but we want \\?\ paths to be
// untouched. Also, ::PathCchSkipRoot treats \ and / the same, but they
// differ for \\?\ paths. Handle \\?\ paths specially.
absolute_path_buffer = allocator->new_objects_copy(
Span<const wchar_t>(path, std::wcslen(path) + 1));

const wchar_t* root_end = std::find(absolute_path_buffer.begin() + 4,
absolute_path_buffer.end() - 1, L'\\');

const wchar_t* relative_start =
*root_end == L'\\' ? root_end + 1 : root_end;

return Simplified_Path{
.full_path = absolute_path_buffer.data(),
.root = make_string_view(absolute_path_buffer.data(), root_end),
.relative =
make_string_view(relative_start, absolute_path_buffer.end() - 1),
};
}

if (path[0] == L'\0') {
// ::GetFullPathNameW returns 0 if path is empty, causing us to
// underallocate. ::PathCchSkipRoot also fails if path is empty. Avoid
// problems by special-casing empty inputs.
Span<wchar_t> full_path = allocator->new_objects_copy(Span<const wchar_t>({L'\0'}));
return Simplified_Path{
.full_path = full_path.data(),
.root = std::wstring_view(),
.relative = std::wstring_view(),
};
}

::DWORD absolute_path_buffer_size =
::GetFullPathNameW(path, 0, nullptr, nullptr);
QLJS_ALWAYS_ASSERT(absolute_path_buffer_size > 0);
absolute_path_buffer = allocator->allocate_uninitialized_span<wchar_t>(
absolute_path_buffer_size);
::DWORD absolute_path_length = ::GetFullPathNameW(
path, absolute_path_buffer_size, absolute_path_buffer.data(), nullptr);
QLJS_ALWAYS_ASSERT(absolute_path_length < absolute_path_buffer_size);
QLJS_ALWAYS_ASSERT(absolute_path_buffer[absolute_path_length] == L'\0');
absolute_path_buffer =
absolute_path_buffer.subspan(0, absolute_path_length + 1);

const wchar_t* relative_start;
::HRESULT result =
::PathCchSkipRoot(absolute_path_buffer.data(), &relative_start);
if (result != S_OK) {
QLJS_UNIMPLEMENTED();
}
const wchar_t* root_end = relative_start;
if (root_end != path && root_end[-1] == L'\\') {
// Don't include the trailing '\'.
root_end -= 1;
}

return Simplified_Path{
.full_path = absolute_path_buffer.data(),
.root = make_string_view(absolute_path_buffer.data(), root_end),
.relative =
make_string_view(relative_start, absolute_path_buffer.end() - 1),
};
}
#endif
}

// quick-lint-js finds bugs in JavaScript programs.
Expand Down
39 changes: 39 additions & 0 deletions src/quick-lint-js/io/file-path.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

#pragma once

#include <ostream>
#include <quick-lint-js/container/monotonic-allocator.h>
#include <quick-lint-js/port/have.h>
#include <quick-lint-js/util/cpp.h>
#include <string>
#include <string_view>

#if defined(_WIN32)
#define QLJS_PREFERRED_PATH_DIRECTORY_SEPARATOR "\\"
Expand All @@ -22,6 +25,42 @@ namespace quick_lint_js {
std::string parent_path(std::string&&);

std::string_view path_file_name(std::string_view);

#if defined(_WIN32)
struct Simplified_Path {
// Null-terminated absolute path.
wchar_t* full_path;

// Root portion of the path. Substring of full_path. Does not contain a
// trailing '\'.
std::wstring_view root;

// Relative portion of the path. Substring of full_path. Does not start with a
// leading '\'.
std::wstring_view relative;

friend std::ostream& operator<<(std::ostream&, Simplified_Path);
};

// Simplify (resolve '.' and '..') and make the path absolute (based on the
// current working directories).
//
// This function should not change the path according to ::CreateFileW and other
// Win32 APIs.
//
// * Preserves at most one trailing '\'.
// * Combines redundant '\' characters.
// * Expands relative paths into absolute paths using the process's current
// working directory and the process's per-drive working directories.
// * Does not resolve symlinks, junctions, shortcuts, etc.
// * Does not check validity of the path.
// * Does not check for existence of directories and files in the path.
// * Does not convert 8.3 names into long names.
//
// Returns pointers into memory allocated by 'allocator'.
Simplified_Path simplify_path_and_make_absolute(Monotonic_Allocator* allocator,
const wchar_t* path);
#endif
}

// quick-lint-js finds bugs in JavaScript programs.
Expand Down
9 changes: 9 additions & 0 deletions src/quick-lint-js/port/span.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ class Span {

bool empty() const { return this->size() == 0; }

Span subspan(Span_Size offset, Span_Size count) {
// TODO(strager): Be lax with offset and count.
QLJS_ASSERT(offset >= 0);
QLJS_ASSERT(offset <= this->size());
QLJS_ASSERT(count >= 0);
QLJS_ASSERT(count + offset <= this->size());
return Span(this->begin() + offset, this->begin() + offset + count);
}

friend bool operator==(Span lhs, Span rhs) {
return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
}
Expand Down
Loading

0 comments on commit 6d7bf2d

Please sign in to comment.