Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
ResourcePaths.cpp
Go to the documentation of this file.
1#include "ResourcePaths.hpp"
2
3#include <algorithm>
4#include <string>
5#include <system_error>
6#include <utility>
7
8namespace monopoly::data
9{
10 namespace
11 {
12 [[nodiscard]] DataError pathError(
13 DataErrorCode code,
14 const std::filesystem::path& path,
15 std::string detail)
16 {
17 return { code, path, std::nullopt, std::move(detail) };
18 }
19
20
21 [[nodiscard]] char8_t foldedAscii(char8_t value) noexcept
22 {
23 return value >= u8'A' && value <= u8'Z'
24 ? static_cast<char8_t>(value + (u8'a' - u8'A'))
25 : value;
26 }
27
28
30 std::u8string_view left,
31 std::u8string_view right) noexcept
32 {
33 return left.size() == right.size() && std::equal(
34 left.begin(), left.end(), right.begin(),
35 [] (char8_t a, char8_t b)
36 {
37 return foldedAscii(a) == foldedAscii(b);
38 });
39 }
40
41
42 [[nodiscard]] bool absent(const std::error_code& error) noexcept
43 {
44 return error == std::errc::no_such_file_or_directory ||
45 error == std::errc::not_a_directory;
46 }
47
48
49 [[nodiscard]] std::expected<std::vector<std::u8string>, DataError>
50 splitRelativePath(std::string_view input)
51 {
52 if (input.empty() || input.find('\0') != std::string_view::npos ||
53 input.front() == '/' || input.front() == '\\' ||
54 input.find(':') != std::string_view::npos)
55 {
56 return std::unexpected(pathError(
58 "resource path must be a nonempty relative UTF-8 path"));
59 }
60
61 std::vector<std::u8string> components;
62 std::size_t offset = 0;
63
64 while (offset < input.size())
65 {
66 const auto separator = input.find_first_of("/\\", offset);
67 const auto end = separator == std::string_view::npos
68 ? input.size() : separator;
69 const auto component = input.substr(offset, end - offset);
70
71 if (component == ".." ||
72 (component == "." && end == input.size()))
73 {
74 return std::unexpected(pathError(
76 "parent traversal or a terminal dot is not a resource file"));
77 }
78
79 if (!component.empty() && component != ".")
80 {
81 // Byte-preserving UTF-8 construction also selects the
82 // UTF-8 conversion overload of filesystem::path on Windows.
83 std::u8string utf8;
84 utf8.reserve(component.size());
85 for (const auto byte : component)
86 {
87 utf8.push_back(static_cast<char8_t>(byte));
88 }
89 components.push_back(std::move(utf8));
90 }
91
92 offset = end + 1;
93 }
94
95 if (components.empty() || input.back() == '/' ||
96 input.back() == '\\')
97 {
98 return std::unexpected(pathError(
100 "resource path must name a file"));
101 }
102
103 return components;
104 }
105
106
107 [[nodiscard]] std::expected<std::filesystem::path, DataError>
109 const std::filesystem::path& directory,
110 std::u8string_view wanted)
111 {
112 std::error_code error;
113 std::filesystem::directory_iterator iterator(directory, error);
114 if (error)
115 {
116 return std::unexpected(pathError(
119 directory, "cannot enumerate resource directory: " +
120 error.message()));
121 }
122
123 std::filesystem::path match;
124 const std::filesystem::directory_iterator end;
125 while (iterator != end)
126 {
127 if (sameLegacyName(iterator->path().filename().u8string(), wanted))
128 {
129 if (!match.empty())
130 {
131 return std::unexpected(pathError(
133 "multiple directory entries match the legacy ASCII case"));
134 }
135 match = iterator->path();
136 }
137
138 iterator.increment(error);
139 if (error)
140 {
141 return std::unexpected(pathError(
143 "cannot finish enumerating resource directory: " +
144 error.message()));
145 }
146 }
147
148 if (match.empty())
149 {
150 return std::unexpected(pathError(
152 "resource path component was not found"));
153 }
154
155 return match;
156 }
157 }
158
159
160 std::expected<ResourcePaths, DataError> ResourcePaths::create(
161 std::span<const std::filesystem::path> roots)
162 {
163 if (roots.empty())
164 {
165 return std::unexpected(pathError(
167 "at least one explicit absolute resource root is required"));
168 }
169
170 ResourcePaths result;
171 for (const auto& root : roots)
172 {
173 if (root.empty() || !root.is_absolute() ||
174 root.native().find(std::filesystem::path::value_type{}) !=
175 std::filesystem::path::string_type::npos)
176 {
177 return std::unexpected(pathError(
179 "resource roots must be explicit absolute paths without NUL"));
180 }
181
182 auto normalized = root.lexically_normal();
183 if (normalized != normalized.root_path() &&
184 normalized.filename().empty())
185 {
186 normalized = normalized.parent_path();
187 }
188
189 std::error_code error;
190 const auto status = std::filesystem::status(normalized, error);
191 if (error && !absent(error))
192 {
193 return std::unexpected(pathError(
195 "cannot inspect resource root: " + error.message()));
196 }
197 if (!error && std::filesystem::exists(status) &&
198 !std::filesystem::is_directory(status))
199 {
200 return std::unexpected(pathError(
202 "resource root exists but is not a directory"));
203 }
204
205 if (std::find(result.roots_.begin(), result.roots_.end(), normalized) ==
206 result.roots_.end())
207 {
208 result.roots_.push_back(std::move(normalized));
209 }
210 }
211
212 return result;
213 }
214
215
216 std::expected<std::filesystem::path, DataError> ResourcePaths::resolve(
217 std::string_view relativePath) const
218 {
219 const auto components = splitRelativePath(relativePath);
220 if (!components)
221 {
222 return std::unexpected(components.error());
223 }
224
225 try
226 {
227 for (const auto& root : roots_)
228 {
229 auto candidate = root;
230 bool missing = false;
231 for (const auto& component : *components)
232 {
234 if (!match)
235 {
236 if (match.error().code != DataErrorCode::ResourceNotFound)
237 {
238 return std::unexpected(match.error());
239 }
240 missing = true;
241 break;
242 }
243 candidate = std::move(*match);
244 }
245
246 if (missing)
247 {
248 continue;
249 }
250
251 std::error_code error;
252 const auto status = std::filesystem::status(candidate, error);
253 if (error && !absent(error))
254 {
255 return std::unexpected(pathError(
257 "cannot inspect resource file: " + error.message()));
258 }
259 if (!error && std::filesystem::is_regular_file(status))
260 {
261 return candidate;
262 }
263 }
264 }
265 catch (const std::filesystem::filesystem_error& error)
266 {
267 return std::unexpected(pathError(
269 }
270
271 return std::unexpected(pathError(
273 "resource file was not found in the explicit roots: " +
274 std::string(relativePath)));
275 }
276
277
278 std::span<const std::filesystem::path> ResourcePaths::roots() const noexcept
279 {
280 return roots_;
281 }
282}
std::size_t offset
std::size_t root
std::atomic< float > left
std::atomic< float > right
unsigned index
std::array< data::DataTag, 3 > roots
std::filesystem::path directory
std::string path
std::string error
Bytes input
std::uint16_t value
Definition Timers.cpp:16
std::expected< std::filesystem::path, DataError > resolve(std::string_view relativePath) const
std::span< const std::filesystem::path > roots() const noexcept
static std::expected< ResourcePaths, DataError > create(std::span< const std::filesystem::path > roots)