AK: Port LexicalPath to Windows

Supported:
* Normal absolute and relative paths: C:\Windows\Fonts, AK\LexicalPath.h
* Forward slashes and multiple separators: C:/Windows///\\\notepad.exe

Not supported:
* Paths starting with two backslashes: \\?\C:\Windows, \\server\share
* Unusual relative paths like C:, C:a\b, \, \a\b

More on Windows path formats: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
This commit is contained in:
stasoid 2024-10-22 22:47:51 +05:00 committed by Andrew Kaster
parent 9ebed7d8d5
commit f026d495cd
Notes: github-actions[bot] 2024-11-09 19:43:37 +00:00
4 changed files with 176 additions and 6 deletions

View file

@ -16,7 +16,6 @@ set(SOURCES
JsonObject.cpp
JsonParser.cpp
JsonValue.cpp
LexicalPath.cpp
MemoryStream.cpp
NumberFormat.cpp
OptionParser.cpp
@ -38,6 +37,12 @@ set(SOURCES
kmalloc.cpp
)
if (WIN32)
list(APPEND SOURCES LexicalPathWindows.cpp)
else()
list(APPEND SOURCES LexicalPath.cpp)
endif()
serenity_lib(AK ak)
serenity_install_headers(AK)

View file

@ -58,6 +58,11 @@ LexicalPath::LexicalPath(ByteString path)
}
}
bool LexicalPath::is_absolute() const
{
return m_string.starts_with('/');
}
Vector<ByteString> LexicalPath::parts() const
{
Vector<ByteString> vector;
@ -163,7 +168,7 @@ ByteString LexicalPath::relative_path(StringView a_path, StringView a_prefix)
if (prefix == "/"sv)
return path.substring_view(1);
// NOTE: This means the prefix is a direct child of the path.
// NOTE: This means the path is a direct child of the prefix.
if (path.starts_with(prefix) && path[prefix.length()] == '/') {
return path.substring_view(prefix.length() + 1);
}

View file

@ -26,11 +26,11 @@ public:
explicit LexicalPath(ByteString);
bool is_absolute() const { return !m_string.is_empty() && m_string[0] == '/'; }
bool is_absolute() const;
ByteString const& string() const { return m_string; }
StringView dirname() const { return m_dirname; }
StringView basename(StripExtension s = StripExtension::No) const { return s == StripExtension::No ? m_basename : m_basename.substring_view(0, m_basename.length() - m_extension.length() - 1); }
StringView basename(StripExtension s = StripExtension::No) const { return s == StripExtension::No ? m_basename : m_title; }
StringView title() const { return m_title; }
StringView extension() const { return m_extension; }
@ -46,13 +46,14 @@ public:
[[nodiscard]] static ByteString canonicalized_path(ByteString);
[[nodiscard]] static ByteString absolute_path(ByteString dir_path, ByteString target);
[[nodiscard]] static ByteString relative_path(StringView absolute_path, StringView prefix);
[[nodiscard]] static ByteString relative_path(StringView absolute_path, StringView absolute_prefix);
template<typename... S>
[[nodiscard]] static LexicalPath join(StringView first, S&&... rest)
{
StringBuilder builder;
builder.append(first);
// NOTE: On Windows slashes will be converted to backslashes in LexicalPath constructor
((builder.append('/'), builder.append(forward<S>(rest))), ...);
return LexicalPath { builder.to_byte_string() };
@ -88,7 +89,7 @@ private:
StringView m_dirname;
StringView m_basename;
StringView m_title;
StringView m_extension;
StringView m_extension; // doesn't include the dot
};
template<>

159
AK/LexicalPathWindows.cpp Normal file
View file

@ -0,0 +1,159 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, Max Wipfli <max.wipfli@serenityos.org>
* Copyright (c) 2024, stasoid <stasoid@yahoo.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
namespace AK {
static bool is_absolute_path(StringView path)
{
return path.length() >= 2 && path[1] == ':';
}
static bool is_root(auto const& parts)
{
return parts.size() == 1 && is_absolute_path(parts[0]);
}
LexicalPath::LexicalPath(ByteString path)
{
m_string = canonicalized_path(path);
m_parts = m_string.split_view('\\');
auto last_slash_index = m_string.find_last('\\');
if (!last_slash_index.has_value())
m_dirname = "."sv;
else
m_dirname = m_string.substring_view(0, *last_slash_index);
// NOTE: For "C:\", both m_dirname and m_basename are "C:", which matches the behavior of dirname/basename in Cygwin/MSYS/git (but not MinGW)
m_basename = m_parts.last();
auto last_dot_index = m_basename.find_last('.');
// NOTE: If the last dot index is 0, it's not an extension: ".foo".
if (last_dot_index.has_value() && *last_dot_index != 0 && m_basename != "..") {
m_title = m_basename.substring_view(0, *last_dot_index);
m_extension = m_basename.substring_view(*last_dot_index + 1);
} else {
m_title = m_basename;
m_extension = {};
}
}
bool LexicalPath::is_absolute() const
{
return is_absolute_path(m_string);
}
Vector<ByteString> LexicalPath::parts() const
{
Vector<ByteString> vector;
for (auto part : m_parts)
vector.append(part);
return vector;
}
bool LexicalPath::has_extension(StringView extension) const
{
if (extension[0] == '.')
extension = extension.substring_view(1);
return m_extension.equals_ignoring_ascii_case(extension);
}
bool LexicalPath::is_child_of(LexicalPath const& possible_parent) const
{
// Any relative path is a child of an absolute path.
if (!this->is_absolute() && possible_parent.is_absolute())
return true;
return m_string.starts_with(possible_parent.string())
&& m_string[possible_parent.string().length()] == '\\';
}
ByteString LexicalPath::canonicalized_path(ByteString path)
{
path = path.replace("/"sv, "\\"sv);
auto parts = path.split_view('\\');
Vector<ByteString> canonical_parts;
for (auto part : parts) {
if (part == ".")
continue;
if (part == ".." && !canonical_parts.is_empty()) {
// At the root, .. does nothing.
if (is_root(canonical_parts))
continue;
// A .. and a previous non-.. part cancel each other.
if (canonical_parts.last() != "..") {
canonical_parts.take_last();
continue;
}
}
canonical_parts.append(part);
}
StringBuilder builder;
builder.join('\\', canonical_parts);
// "X:" -> "X:\"
if (is_root(canonical_parts))
builder.append('\\');
path = builder.to_byte_string();
return path == "" ? "." : path;
}
ByteString LexicalPath::absolute_path(ByteString dir_path, ByteString target)
{
if (is_absolute_path(target))
return canonicalized_path(target);
return join(dir_path, target).string();
}
// Returns relative version of abs_path (relative to abs_prefix), such that join(abs_prefix, rel_path) == abs_path.
ByteString LexicalPath::relative_path(StringView abs_path, StringView abs_prefix)
{
if (!is_absolute_path(abs_path) || !is_absolute_path(abs_prefix)
|| abs_path[0] != abs_prefix[0]) // different drives
return "";
auto path = canonicalized_path(abs_path);
auto prefix = canonicalized_path(abs_prefix);
if (path == prefix)
return ".";
auto path_parts = path.split_view('\\');
auto prefix_parts = prefix.split_view('\\');
size_t first_mismatch = 0;
for (; first_mismatch < min(path_parts.size(), prefix_parts.size()); first_mismatch++) {
if (path_parts[first_mismatch] != prefix_parts[first_mismatch])
break;
}
StringBuilder builder;
builder.append_repeated("..\\"sv, prefix_parts.size() - first_mismatch);
builder.join('\\', path_parts.span().slice(first_mismatch));
return builder.to_byte_string();
}
LexicalPath LexicalPath::append(StringView value) const
{
return join(m_string, value);
}
LexicalPath LexicalPath::prepend(StringView value) const
{
return join(value, m_string);
}
LexicalPath LexicalPath::parent() const
{
return append(".."sv);
}
}