diff --git a/AK/CMakeLists.txt b/AK/CMakeLists.txt index fadfaa1bad9..3fbdc65b90a 100644 --- a/AK/CMakeLists.txt +++ b/AK/CMakeLists.txt @@ -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) diff --git a/AK/LexicalPath.cpp b/AK/LexicalPath.cpp index 74bb7ece478..2f5e8b3e250 100644 --- a/AK/LexicalPath.cpp +++ b/AK/LexicalPath.cpp @@ -58,6 +58,11 @@ LexicalPath::LexicalPath(ByteString path) } } +bool LexicalPath::is_absolute() const +{ + return m_string.starts_with('/'); +} + Vector LexicalPath::parts() const { Vector 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); } diff --git a/AK/LexicalPath.h b/AK/LexicalPath.h index f8caec22b3b..4f70df6f1b2 100644 --- a/AK/LexicalPath.h +++ b/AK/LexicalPath.h @@ -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 [[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(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<> diff --git a/AK/LexicalPathWindows.cpp b/AK/LexicalPathWindows.cpp new file mode 100644 index 00000000000..710ed306519 --- /dev/null +++ b/AK/LexicalPathWindows.cpp @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, Max Wipfli + * Copyright (c) 2024, stasoid + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +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 LexicalPath::parts() const +{ + Vector 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 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); +} + +}