Serenity Operating System
at master 210 lines 6.8 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021, Max Wipfli <max.wipfli@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <AK/LexicalPath.h> 9#include <AK/StringBuilder.h> 10#include <AK/StringView.h> 11#include <AK/Vector.h> 12 13namespace AK { 14 15char s_single_dot = '.'; 16 17LexicalPath::LexicalPath(DeprecatedString path) 18 : m_string(canonicalized_path(move(path))) 19{ 20 if (m_string.is_empty()) { 21 m_string = "."; 22 m_dirname = m_string; 23 m_basename = {}; 24 m_title = {}; 25 m_extension = {}; 26 m_parts.clear(); 27 return; 28 } 29 30 m_parts = m_string.split_view('/'); 31 32 auto last_slash_index = m_string.view().find_last('/'); 33 if (!last_slash_index.has_value()) { 34 // The path contains a single part and is not absolute. m_dirname = "."sv 35 m_dirname = { &s_single_dot, 1 }; 36 } else if (*last_slash_index == 0) { 37 // The path contains a single part and is absolute. m_dirname = "/"sv 38 m_dirname = m_string.substring_view(0, 1); 39 } else { 40 m_dirname = m_string.substring_view(0, *last_slash_index); 41 } 42 43 if (m_string == "/") 44 m_basename = m_string; 45 else { 46 VERIFY(m_parts.size() > 0); 47 m_basename = m_parts.last(); 48 } 49 50 auto last_dot_index = m_basename.find_last('.'); 51 // NOTE: if the dot index is 0, this means we have ".foo", it's not an extension, as the title would then be "". 52 if (last_dot_index.has_value() && *last_dot_index != 0) { 53 m_title = m_basename.substring_view(0, *last_dot_index); 54 m_extension = m_basename.substring_view(*last_dot_index + 1); 55 } else { 56 m_title = m_basename; 57 m_extension = {}; 58 } 59} 60 61Vector<DeprecatedString> LexicalPath::parts() const 62{ 63 Vector<DeprecatedString> vector; 64 vector.ensure_capacity(m_parts.size()); 65 for (auto& part : m_parts) 66 vector.unchecked_append(part); 67 return vector; 68} 69 70bool LexicalPath::has_extension(StringView extension) const 71{ 72 return m_string.ends_with(extension, CaseSensitivity::CaseInsensitive); 73} 74 75bool LexicalPath::is_child_of(LexicalPath const& possible_parent) const 76{ 77 // Any relative path is a child of an absolute path. 78 if (!this->is_absolute() && possible_parent.is_absolute()) 79 return true; 80 // An absolute path can't meaningfully be a child of a relative path. 81 if (this->is_absolute() && !possible_parent.is_absolute()) 82 return false; 83 84 // Two relative paths and two absolute paths can be meaningfully compared. 85 if (possible_parent.parts_view().size() > this->parts_view().size()) 86 return false; 87 auto common_parts_with_parent = this->parts_view().span().trim(possible_parent.parts_view().size()); 88 return common_parts_with_parent == possible_parent.parts_view().span(); 89} 90 91DeprecatedString LexicalPath::canonicalized_path(DeprecatedString path) 92{ 93 if (path.is_null()) 94 return {}; 95 96 // NOTE: We never allow an empty m_string, if it's empty, we just set it to '.'. 97 if (path.is_empty()) 98 return "."; 99 100 // NOTE: If there are no dots, no '//' and the path doesn't end with a slash, it is already canonical. 101 if (!path.contains("."sv) && !path.contains("//"sv) && !path.ends_with('/')) 102 return path; 103 104 auto is_absolute = path[0] == '/'; 105 auto parts = path.split_view('/'); 106 size_t approximate_canonical_length = 0; 107 Vector<DeprecatedString> canonical_parts; 108 109 for (auto& part : parts) { 110 if (part == ".") 111 continue; 112 if (part == "..") { 113 if (canonical_parts.is_empty()) { 114 if (is_absolute) { 115 // At the root, .. does nothing. 116 continue; 117 } 118 } else { 119 if (canonical_parts.last() != "..") { 120 // A .. and a previous non-.. part cancel each other. 121 canonical_parts.take_last(); 122 continue; 123 } 124 } 125 } 126 approximate_canonical_length += part.length() + 1; 127 canonical_parts.append(part); 128 } 129 130 if (canonical_parts.is_empty() && !is_absolute) 131 canonical_parts.append("."); 132 133 StringBuilder builder(approximate_canonical_length); 134 if (is_absolute) 135 builder.append('/'); 136 builder.join('/', canonical_parts); 137 return builder.to_deprecated_string(); 138} 139 140DeprecatedString LexicalPath::absolute_path(DeprecatedString dir_path, DeprecatedString target) 141{ 142 if (LexicalPath(target).is_absolute()) { 143 return LexicalPath::canonicalized_path(target); 144 } 145 return LexicalPath::canonicalized_path(join(dir_path, target).string()); 146} 147 148DeprecatedString LexicalPath::relative_path(StringView a_path, StringView a_prefix) 149{ 150 if (!a_path.starts_with('/') || !a_prefix.starts_with('/')) { 151 // FIXME: This should probably VERIFY or return an Optional<DeprecatedString>. 152 return ""sv; 153 } 154 155 if (a_path == a_prefix) 156 return "."; 157 158 // NOTE: Strip optional trailing slashes, except if the full path is only "/". 159 auto path = canonicalized_path(a_path); 160 auto prefix = canonicalized_path(a_prefix); 161 162 if (path == prefix) 163 return "."; 164 165 // NOTE: Handle this special case first. 166 if (prefix == "/"sv) 167 return path.substring_view(1); 168 169 // NOTE: This means the prefix is a direct child of the path. 170 if (path.starts_with(prefix) && path[prefix.length()] == '/') { 171 return path.substring_view(prefix.length() + 1); 172 } 173 174 auto path_parts = path.split_view('/'); 175 auto prefix_parts = prefix.split_view('/'); 176 size_t index_of_first_part_that_differs = 0; 177 for (; index_of_first_part_that_differs < path_parts.size() && index_of_first_part_that_differs < prefix_parts.size(); index_of_first_part_that_differs++) { 178 if (path_parts[index_of_first_part_that_differs] != prefix_parts[index_of_first_part_that_differs]) 179 break; 180 } 181 182 StringBuilder builder; 183 for (size_t part_index = index_of_first_part_that_differs; part_index < prefix_parts.size(); part_index++) { 184 builder.append("../"sv); 185 } 186 for (size_t part_index = index_of_first_part_that_differs; part_index < path_parts.size(); part_index++) { 187 builder.append(path_parts[part_index]); 188 if (part_index != path_parts.size() - 1) // We don't need a slash after the file name or the name of the last directory 189 builder.append('/'); 190 } 191 192 return builder.to_deprecated_string(); 193} 194 195LexicalPath LexicalPath::append(StringView value) const 196{ 197 return LexicalPath::join(m_string, value); 198} 199 200LexicalPath LexicalPath::prepend(StringView value) const 201{ 202 return LexicalPath::join(value, m_string); 203} 204 205LexicalPath LexicalPath::parent() const 206{ 207 return append(".."sv); 208} 209 210}