plptools
Loading...
Searching...
No Matches
pathutils.cc
Go to the documentation of this file.
1/*
2 * This file is part of plptools.
3 *
4 * Copyright (C) 1999 Philip Proudman <philip.proudman@btinternet.com>
5 * Copyright (C) 1999-2002 Fritz Elfert <felfert@to.com>
6 * Copyright (C) 2006-2025 Reuben Thomas <rrt@sc3d.org>
7 * Copyright (C) 2026 Jason Morley <hello@jbmorley.co.uk>
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * along with this program; if not, see <https://www.gnu.org/licenses/>.
21 *
22 */
23
24#include "config.h"
25
26#include "pathutils.h"
27
28#include "xalloc.h"
29#include "xvasprintf.h"
30#include <string>
31#include <unistd.h>
32
33static bool is_drive_component(const std::string &pathComponent) {
34 return (pathComponent.size() == 2 &&
35 std::isalpha(static_cast<unsigned char>(pathComponent[0])) &&
36 pathComponent[1] == ':');
37}
38
39static bool is_separator(const std::string &pathComponent, const pathutils::PathFormat format) {
40 return pathComponent.length() == 1 && pathComponent[0] == pathutils::path_separator(format);
41}
42
43static bool is_absolute(const std::vector<std::string> &components, const pathutils::PathFormat format) {
44 switch (format) {
46 return components.size() >= 1 && components[0] == "/";
48 return components.size() >= 2 && is_drive_component(components[0]) && components[1] == "\\";
49 }
50}
51
52static bool is_rooted(const std::vector<std::string> &components, const pathutils::PathFormat format) {
53 switch (format) {
55 return components.size() >= 1 && components[0] == "/";
57 return ((components.size() >= 2 && is_drive_component(components[0]) && components[1] == "\\") ||
58 (components.size() >= 1 && components[0] == "\\"));
59 }
60}
61
62static std::string drive_component(const std::vector<std::string> components, const pathutils::PathFormat format) {
63 if (format != pathutils::PathFormat::kWindows || components.empty()) {
64 return "";
65 }
66 std::string front = components.front();
67 if (!is_drive_component(front)) {
68 return "";
69 }
70 front[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(front[0])));
71 return front;
72}
73
75 switch (format) {
77 return '/';
79 return '\\';
80 }
81}
82
83std::string pathutils::epoc_basename(std::string path) {
84 size_t end = path.find_last_of("\\");
85 if (end == std::string::npos) {
86 return std::string(path);
87 }
88 return path.substr(end+1);
89}
90
91char *pathutils::epoc_dirname(const char *path) {
92 char *f1 = xstrdup(path);
93 char *p = f1 + strlen(f1);
94
95 // Skip trailing slash.
96 if (p > f1) {
97 *--p = '\0';
98 }
99
100 // Skip backwards to next slash.
101 while ((p > f1) && (*p != '/') && (*p != '\\')) {
102 p--;
103 }
104
105 // If we have just a drive letter, colon and slash, keep exactly that.
106 if (p - f1 < 3) {
107 p = f1 + 3;
108 }
109
110 // Truncate the string at the current point, and return it.
111 *p = '\0';
112
113 return f1;
114}
115
116char *pathutils::resolve_epoc_path(const char *path, const char *relativeToPath) {
117 char *f1;
118
119 // If we have asked for parent dir, get dirname of cwd.
120 if (!strcmp(path, "..")) {
121 f1 = epoc_dirname(relativeToPath);
122 } else {
123 if ((path[0] != '/') && (path[0] != '\\') && (path[1] != ':')) {
124 // If path is relative, append it to cwd.
125 f1 = xasprintf("%s%s", relativeToPath, path);
126 } else {
127 // Otherwise, path is absolute, so duplicate it.
128 f1 = xstrdup(path);
129 }
130 }
131
132 /* Convert forward slashes in new path to backslashes. */
133 for (char *p = f1; *p; p++) {
134 if (*p == '/') {
135 *p = '\\';
136 }
137 }
138
139 return f1;
140}
141
142std::vector<std::string> pathutils::split(const std::string &path, const PathFormat format) {
143 auto separator = path_separator(format);
144 std::vector<std::string> result;
145 size_t pathStartIndex = 0;
146
147 // If we're on windows, we need to consume the drive first so we pattern match on that and update the starting
148 // index accordingly.
149 if (format == PathFormat::kWindows && path.length() >= 2 && is_drive_component(path.substr(0, 2))) {
150 result.push_back(path.substr(0, 2));
151 pathStartIndex = 2;
152 }
153
154 // Once we've handled the drive, it's safe to treat the remaining path consistently across Windows and POSIX---we
155 // want to preserve the leading path separator regardless of path format.
156 size_t previousIndex = pathStartIndex;
157 size_t index = pathStartIndex;
158 bool foundNonRootSeparator = false;
159 while ((index = path.find(separator, previousIndex)) != std::string::npos) {
160 // If the index of the first separator is 0, then we know the path is absolute and we insert the root directory
161 // path component.
162 if (index == pathStartIndex) {
163 result.push_back(path.substr(index, 1));
164 } else {
165 foundNonRootSeparator = true;
166 }
167 size_t length = index - previousIndex;
168 if (length > 0) {
169 result.push_back(path.substr(previousIndex, length));
170 }
171 previousIndex = index + 1;
172 }
173 if (previousIndex < path.length() || foundNonRootSeparator) {
174 result.push_back(path.substr(previousIndex));
175 }
176 return result;
177}
178
179std::string pathutils::join(const std::vector<std::string> &components, const PathFormat format) {
180 std::string drive;
181 auto begin = components.begin();
182
183 // Special-case handling of Windows drives.
184 if (format == PathFormat::kWindows && components.size() >= 1 && is_drive_component(components[0])) {
185 drive += components[0];
186 ++begin;
187 }
188
189 // Handle drive paths consistently, taking care not to make relative paths absolute by over-inserting a separator.
190 std::string path;
191 auto separator = path_separator(format);
192 for (auto iterator = begin; iterator != components.end(); iterator++) {
193 if (iterator != begin && path.back() != separator) {
194 path += separator;
195 }
196
197 // An empty string is a directory marker. We don't need to do anything special if the path is already non-empty
198 // but if the path is empty then we need to set it to '.' followed by the path separator to ensure it can be
199 // correctly resolved by the platform.
200 // Note that this implementation implicitly performs some path normalization. While this does go beyond the
201 // intended responsibility of this function, it also ensures the output paths are clean. We might choose to
202 // move this normalization out into a separate method which walks the components and drops unnecessary directory
203 // markers (and flattens parent markers, etc).
204 if ((*iterator).empty() && path.empty()) {
205 path += ".";
206 path += separator;
207 }
208
209 path += *iterator;
210 }
211
212 return drive + path;
213}
214
215std::string pathutils::appending_components(const std::string &path,
216 const std::vector<std::string> &components,
217 const PathFormat format) {
218 auto pathComponents = split(path, format);
219 pathComponents.insert(pathComponents.end(), components.begin(), components.end());
220 return join(pathComponents, format);
221}
222
223bool pathutils::is_absolute(const std::string &path, const PathFormat format) {
224 auto components = split(path, format);
225 return ::is_absolute(components, format);
226}
227
235static std::vector<std::string>::const_iterator path_find_rootless_components_begin(const std::vector<std::string> &components,
236 const pathutils::PathFormat format) {
237 std::vector<std::string>::const_iterator iterator = components.begin();
238
239 // Check we're not already at the end of the vector.
240 if (iterator == components.end()) {
241 return iterator;
242 }
243
244 // Consume the windows drive component if appropriate and then check we're not at the end of the vector.
245 if (format == pathutils::PathFormat::kWindows) {
246 if (is_drive_component(*iterator)) {
247 ++iterator;
248 }
249 if (iterator == components.end()) {
250 return iterator;
251 }
252 }
253
254 // Consume the root component ('\\' or '/') if present.
255 if (is_separator(*iterator, format)) {
256 ++iterator;
257 }
258
259 return iterator;
260}
261
262std::string pathutils::resolve_path(const std::string &path,
263 const std::string &basePath,
264 const PathFormat format) {
265 std::vector<std::string> pathComponents = split(path, format);
266 std::vector<std::string> basePathComponents = split(basePath, format);
267
268 // Don't do anything if the path is already absolute.
269 if (::is_absolute(pathComponents, format)) {
270 return path;
271 }
272
273 // Windows paths are pretty gnarly as they can cross drive boundaries. If both paths refer to different drives,
274 // we perform some special case checks to ensure we can do something meaningful.
275 std::string pathDrive = drive_component(pathComponents, format);
276 std::string basePathDrive = drive_component(basePathComponents, format);
277 if (!pathDrive.empty() && pathDrive != basePathDrive && !is_rooted(pathComponents, format)) {
278 return path;
279 }
280
281 // Work out where the path starts.
282 auto basePathComponentsPathBegin = path_find_rootless_components_begin(basePathComponents, format);
283 std::vector<std::string> resolvedPathRootComponents(basePathComponents.cbegin(), basePathComponentsPathBegin);
284 std::vector<std::string> resolvedPathRootlessComponents(basePathComponentsPathBegin, basePathComponents.cend());
285
286 if (is_rooted(pathComponents, format)) {
287
288 // If the path is rooted (but not absolute), then we can simply set the resolved components to that of the path.
289 std::string pathSeparator;
290 pathSeparator += path_separator(format);
291 if (resolvedPathRootComponents.empty() || resolvedPathRootComponents.back() != pathSeparator) {
292 resolvedPathRootComponents.push_back(pathSeparator);
293 }
294 resolvedPathRootlessComponents = std::vector<std::string>(++pathComponents.begin(), pathComponents.end());
295
296 } else {
297
298 // Otherwise, we fold the incoming path components into our base path components.
299 for (const auto &pathComponent : pathComponents) {
300
301 // Ensure there aren't any trailing directory markers before processing the next component.
302 while (!resolvedPathRootlessComponents.empty() && resolvedPathRootlessComponents.back().empty()) {
303 resolvedPathRootlessComponents.pop_back();
304 }
305
306 if (pathComponent == "..") {
307 if (resolvedPathRootlessComponents.empty() || resolvedPathRootlessComponents.back() == "..") {
308 resolvedPathRootlessComponents.push_back("..");
309 } else {
310 resolvedPathRootlessComponents.pop_back();
311 }
312 } else {
313 resolvedPathRootlessComponents.push_back(pathComponent);
314 }
315 }
316
317 }
318
319 // Reconstitute the path.
320 std::vector<std::string> resolvedPathComponents = resolvedPathRootComponents;
321 resolvedPathComponents.insert(resolvedPathComponents.end(), resolvedPathRootlessComponents.begin(), resolvedPathRootlessComponents.end());
322 std::string resolvedPath = join(resolvedPathComponents, format);
323
324 // If the resulting path is an empty string, we return '.' to ensure it's valid.
325 if (resolvedPath.empty()) {
326 return ".";
327 }
328
329 return resolvedPath;
330}
char path_separator(const PathFormat format)
Definition: pathutils.cc:74
char * epoc_dirname(const char *path)
Compute parent directory of an EPOC directory.
Definition: pathutils.cc:91
bool is_absolute(const std::string &path, const PathFormat format)
Check if a path is absolute.
Definition: pathutils.cc:223
std::vector< std::string > split(const std::string &path, const PathFormat format)
Split a path, path, into its components, using the path separator, separator.
Definition: pathutils.cc:142
char * resolve_epoc_path(const char *path, const char *initialPath)
Returns a new absolute EPOC path, determined by resolving path relative to initialPath.
Definition: pathutils.cc:116
std::string appending_components(const std::string &path, const std::vector< std::string > &components, const PathFormat format)
Convenience wrapper for join that returns a new path resulting from appending path components,...
Definition: pathutils.cc:215
std::string resolve_path(const std::string &path, const std::string &startingPath, const PathFormat format)
Returns a path by resolving a relative or absolute path against a starting path.
Definition: pathutils.cc:262
std::string epoc_basename(std::string path)
Returns the last path component of an EPOC path.
Definition: pathutils.cc:83
std::string join(const std::vector< std::string > &components, const PathFormat format)
Return a new path by joining the path components, components, with path separator,...
Definition: pathutils.cc:179
static bool is_drive_component(const std::string &pathComponent)
Definition: pathutils.cc:33
static bool is_separator(const std::string &pathComponent, const pathutils::PathFormat format)
Definition: pathutils.cc:39
static std::vector< std::string >::const_iterator path_find_rootless_components_begin(const std::vector< std::string > &components, const pathutils::PathFormat format)
Return the iterator corresponding with the beginning of the path components.
Definition: pathutils.cc:235
static bool is_rooted(const std::vector< std::string > &components, const pathutils::PathFormat format)
Definition: pathutils.cc:52
static bool is_absolute(const std::vector< std::string > &components, const pathutils::PathFormat format)
Definition: pathutils.cc:43
static std::string drive_component(const std::vector< std::string > components, const pathutils::PathFormat format)
Definition: pathutils.cc:62