Serenity Operating System
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}