Chaste Commit::fa89f2b838c1edb21a1eaec92ee3a2eacc9255dd
FileFinder.cpp
1/*
2
3Copyright (c) 2005-2026, University of Oxford.
4All rights reserved.
5
6University of Oxford means the Chancellor, Masters and Scholars of the
7University of Oxford, having an administrative office at Wellington
8Square, Oxford OX1 2JD, UK.
9
10This file is part of Chaste.
11
12Redistribution and use in source and binary forms, with or without
13modification, are permitted provided that the following conditions are met:
14 * Redistributions of source code must retain the above copyright notice,
15 this list of conditions and the following disclaimer.
16 * Redistributions in binary form must reproduce the above copyright notice,
17 this list of conditions and the following disclaimer in the documentation
18 and/or other materials provided with the distribution.
19 * Neither the name of the University of Oxford nor the names of its
20 contributors may be used to endorse or promote products derived from this
21 software without specific prior written permission.
22
23THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
27LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
29GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
32OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
34*/
35
36#include <algorithm>
37#include <cassert>
38
39#include "FileFinder.hpp"
40#include "ChasteBuildRoot.hpp"
41#include "Exception.hpp"
42#include "FilesystemPermissions.hpp"
43#include "GetCurrentWorkingDirectory.hpp"
44#include "OutputFileHandler.hpp"
45#include "PosixPathFixer.hpp"
46#include "Warnings.hpp"
47
48bool FileFinder::msFaking = false;
49
51
52std::string FileFinder::msFakePath = "";
53
54#define UNSET_PATH "UNSET!"
55
68#define CONVERT_ERROR(code) \
69 try \
70 { \
71 code; \
72 } \
73 catch (const fs::filesystem_error& e) \
74 { \
75 EXCEPTION(e.what()); \
76 }
77
79 : mAbsPath(UNSET_PATH)
80{
81}
82
83FileFinder::FileFinder(const std::string& rRelativePath, RelativeTo::Value relativeTo)
84{
85 SetPath(rRelativePath, relativeTo);
86}
87
88FileFinder::FileFinder(const std::string& rLeafName, const FileFinder& rParentOrSibling)
89{
90 SetPath(rLeafName, rParentOrSibling);
91}
92
93FileFinder::FileFinder(const fs::path& rPath)
94{
95 SetPath(fs::absolute(rPath).string(), RelativeTo::Absolute);
96}
97
101
102void FileFinder::SetPath(const std::string& rRelativePath, RelativeTo::Value relativeTo)
103{
104 switch (relativeTo)
105 {
107 mAbsPath = ChasteSourceRootDir() + rRelativePath;
108 break;
109
111 mAbsPath = ChasteBuildRootDir() + rRelativePath;
112 break;
113
116 break;
117
118 case RelativeTo::CWD:
119 mAbsPath = GetCurrentWorkingDirectory() + "/" + rRelativePath;
120 break;
121
123 mAbsPath = rRelativePath;
124 break;
125
127 if (FileFinder::IsAbsolutePath(rRelativePath))
128 {
129 mAbsPath = rRelativePath;
130 }
131 else
132 {
133 mAbsPath = GetCurrentWorkingDirectory() + "/" + rRelativePath;
134 }
135 break;
136
137 default:
138 // Getting here is impossible
140 break;
141 }
142
143 if (msFaking && msFakeWhat == relativeTo)
144 {
145 // Fake the resulting path
146 mAbsPath = msFakePath + "/" + rRelativePath;
147 }
148
149 // Remove any trailing /
150 std::string::iterator it = mAbsPath.end();
151 while (it != mAbsPath.begin() && *(--it) == '/')
152 {
153 // Iterator was decremented in the while test
154 }
155 // it now points at the last non-slash character, if any
156 if (it != mAbsPath.end() && (++it) != mAbsPath.end())
157 {
158 mAbsPath.erase(it, mAbsPath.end());
159 }
160}
161
162void FileFinder::SetPath(const std::string& rLeafName, const FileFinder& rParentOrSibling)
163{
164 if (!rParentOrSibling.Exists())
165 {
166 EXCEPTION("Reference path '" << rParentOrSibling.GetAbsolutePath() << "' does not exist.");
167 }
168 if (rParentOrSibling.IsDir())
169 {
170 SetPath(rParentOrSibling.GetAbsolutePath() + rLeafName, RelativeTo::Absolute);
171 }
172 else
173 {
174 SetPath(rParentOrSibling.GetParent().GetAbsolutePath() + rLeafName, RelativeTo::Absolute);
175 }
176}
177
179{
180 return mAbsPath != UNSET_PATH;
181}
182
184{
185 return fs::exists(mAbsPath);
186}
187
189{
190 return fs::is_regular_file(mAbsPath);
191}
192
194{
195 return fs::is_directory(mAbsPath);
196}
197
199{
200 bool empty = true;
201 if (IsFile())
202 {
203 empty = (fs::file_size(mAbsPath) == 0u);
204 }
205 else if (IsDir())
206 {
207 fs::directory_iterator end_iter;
208 for (fs::directory_iterator dir_iter(mAbsPath); dir_iter != end_iter; ++dir_iter)
209 {
210 if ((dir_iter->path().filename().string()).substr(0, 1) != ".")
211 {
212 empty = false;
213 break;
214 }
215 }
216 }
217 else
218 {
219 EXCEPTION("The path '" << mAbsPath << "' does not exist.");
220 }
221 return empty;
222}
223
225{
226 if (IsDir())
227 {
228 return mAbsPath + '/';
229 }
230 return mAbsPath;
231}
232
233bool FileFinder::IsNewerThan(const FileFinder& rOtherEntity) const
234{
235 assert(Exists());
236 assert(rOtherEntity.Exists());
237 return fs::last_write_time(mAbsPath) > fs::last_write_time(rOtherEntity.mAbsPath);
238}
239
240std::string FileFinder::GetLeafName() const
241{
242 return fs::path(mAbsPath).filename().string();
243}
244
246{
247 return fs::path(mAbsPath).stem().string();
248}
249
250std::string FileFinder::GetExtension() const
251{
252 return fs::path(mAbsPath).extension().string();
253}
254
256{
257 fs::path our_path(mAbsPath);
258 // Assertion will only happen if constructed with an empty string and absolute path.
259 // (Arguably a user could do this, but not accidentally.)
260 assert(!our_path.parent_path().empty());
261 return FileFinder(our_path.parent_path().string(),
263}
264
265std::string FileFinder::GetRelativePath(const FileFinder& rBasePath) const
266{
267 const std::string base_path = rBasePath.GetAbsolutePath();
268 const std::string our_path = GetAbsolutePath();
269 if (our_path.substr(0, base_path.length()) != base_path)
270 {
271 EXCEPTION("The path '" << our_path << "' is not relative to '" << base_path << "'.");
272 }
273 return our_path.substr(base_path.length());
274}
275
281void RecursiveCopy(const fs::path& rFromPath, const fs::path& rToPath);
282
283void RecursiveCopy(const fs::path& rFromPath, const fs::path& rToPath)
284{
285 fs::path dest = rToPath;
286 // If rToPath is a folder, then we're copying to the source name *inside* this folder
287 if (fs::is_directory(dest))
288 {
289 dest /= rFromPath.filename();
290 }
291 // If the source is a folder, it's complicated
292 if (fs::is_directory(rFromPath))
293 {
294 // Create the destination folder
295 EXCEPT_IF(fs::exists(dest));
297 // Recursively copy our contents
298 fs::directory_iterator end_iter;
299 for (fs::directory_iterator dir_iter(rFromPath); dir_iter != end_iter; ++dir_iter)
300 {
301 RecursiveCopy(dir_iter->path(), dest);
302 }
303 }
304 else
305 {
307 }
308}
309
311{
312 if (!Exists())
313 {
314 EXCEPTION("Cannot copy '" << mAbsPath << "' as it does not exist.");
315 }
316 fs::path from_path(mAbsPath);
317 fs::path to_path(rDest.mAbsPath);
318 if (rDest.IsDir())
319 {
320 to_path /= from_path.filename();
321 }
322 if (fs::exists(to_path))
323 {
324 if (IsFile())
325 {
326 CONVERT_ERROR(fs::remove(to_path));
327 }
328 else
329 {
330 EXCEPTION("Cannot copy '" << mAbsPath << "' to '" << to_path << "' as it would overwrite an existing file.");
331 }
332 }
333 CONVERT_ERROR(RecursiveCopy(from_path, to_path));
334 return FileFinder(to_path);
335}
336
341void RemoveAll(const fs::path& rPath);
342
343void RemoveAll(const fs::path& rPath)
344{
345 // First recursively remove any children
346 if (fs::is_directory(rPath))
347 {
348 fs::directory_iterator end_iter;
349 for (fs::directory_iterator dir_iter(rPath); dir_iter != end_iter; ++dir_iter)
350 {
351 RemoveAll(dir_iter->path());
352 }
353 }
354 // Now remove the item itself
355 fs::remove(rPath);
356}
357
358void FileFinder::PrivateRemove(bool dangerous) const
359{
360 // Test for bad paths
361 const std::string test_output(OutputFileHandler::GetChasteTestOutputDirectory());
362 const std::string test_output_path(ChastePosixPathFixer::ToPosix(fs::path(test_output)));
363 const std::string absolute_path(ChastePosixPathFixer::ToPosix(fs::path(GetAbsolutePath())));
364 bool in_testoutput = (absolute_path.substr(0, test_output_path.length()) == test_output_path);
365
366 if (!in_testoutput)
367 {
368 if (dangerous)
369 {
370 const std::string source_folder(FileFinder("", RelativeTo::ChasteSourceRoot).GetAbsolutePath());
371 const std::string source_folder_path = ChastePosixPathFixer::ToPosix(fs::path(source_folder));
372 bool in_source = (absolute_path.substr(0, source_folder_path.length()) == source_folder_path);
373
374 const std::string build_folder(FileFinder("", RelativeTo::ChasteBuildRoot).GetAbsolutePath());
375 const std::string build_folder_path = ChastePosixPathFixer::ToPosix(fs::path(build_folder));
376 bool in_build = (absolute_path.substr(0, build_folder_path.length()) == build_folder_path);
377
378 if (!(in_source || in_build))
379 {
380 EXCEPTION("Cannot remove location '" << mAbsPath
381 << "' as it is not located within the Chaste test output folder ("
382 << test_output_path << "), the Chaste source folder ("
383 << source_folder_path << ") or the Chaste build folder ("
384 << build_folder_path << ").");
385 }
386 }
387 else
388 {
389 EXCEPTION("Cannot remove location '" << mAbsPath
390 << "' as it is not located within the Chaste test output folder ("
391 << test_output_path << ").");
392 }
393 }
394
395 if (mAbsPath.find("..") != std::string::npos)
396 {
397 EXCEPTION("Cannot remove location '" << mAbsPath
398 << "' as it contains a dangerous path component.");
399 }
400 if (Exists())
401 {
402 if (!dangerous)
403 {
404 fs::path sig_file(mAbsPath);
405 if (IsFile())
406 {
407 // We need to look for the signature file in the parent folder
408 sig_file.remove_filename();
409 }
411 if (!fs::exists(sig_file))
412 {
413 EXCEPTION("Cannot remove location '" << mAbsPath << "' because the signature file '"
414 << OutputFileHandler::SIG_FILE_NAME << "' is not present.");
415 }
416 }
417 // Do the removal
418 CONVERT_ERROR(RemoveAll(mAbsPath));
419 }
420}
421
423{
425}
426
428{
429 PrivateRemove(true);
430}
431
432std::vector<FileFinder> FileFinder::FindMatches(const std::string& rPattern) const
433{
434 // Check for error/warning cases
435 if (!IsDir())
436 {
437 EXCEPTION("Cannot search for matching files in '" << mAbsPath << "' as it is not a directory.");
438 }
439 size_t len = rPattern.length();
440 size_t inner_star_pos = rPattern.find('*', 1);
441 if (inner_star_pos != std::string::npos && inner_star_pos < len - 1)
442 {
443 WARNING("A '*' only has special meaning at the start or end of a pattern.");
444 }
445
446 // Note initial or trailing *, and use of ?
447 std::string pattern(rPattern);
448 bool star_fini = false;
449 if (!pattern.empty() && *(pattern.rbegin()) == '*')
450 {
451 star_fini = true;
452 pattern = pattern.substr(0, len - 1);
453 len--;
454 }
455 bool star_init = false;
456 if (!pattern.empty() && pattern[0] == '*')
457 {
458 star_init = true;
459 pattern = pattern.substr(1);
460 len--;
461 }
462 bool has_query = (pattern.find('?') != std::string::npos);
463 // Disallow a harder case to match
464 if (star_init && star_fini && has_query)
465 {
466 EXCEPTION("The '*' wildcard may not be used at both the start and end of the pattern if the '?' wildcard is also used.");
467 }
468
469 // Search the folder
470 std::vector<FileFinder> results;
471 if (!rPattern.empty())
472 {
473 fs::directory_iterator end_iter;
474 fs::path our_path(mAbsPath);
475 for (fs::directory_iterator dir_iter(our_path); dir_iter != end_iter; ++dir_iter)
476 {
477 std::string leafname = dir_iter->path().filename().string();
478 size_t leaf_len = leafname.length();
479 if (leafname[0] != '.' // Don't include hidden files
480 && leaf_len >= len) // Ignore stuff that can't match
481 {
482 if (!has_query) // Easier case
483 {
484 size_t pos = leafname.find(pattern);
485 if ((star_init || pos == 0) && (star_fini || pos + len == leaf_len))
486 {
487 results.push_back(FileFinder(our_path / leafname));
488 }
489 }
490 else
491 {
492 std::string match;
493 if (star_init)
494 {
495 // Match against last len chars
496 match = leafname.substr(leaf_len - len);
497 }
498 else
499 {
500 // Match against first len chars
501 match = leafname.substr(0, len);
502 }
503 bool ok = true;
504 for (std::string::const_iterator it_p = pattern.begin(), it_m = match.begin();
505 it_p != pattern.end();
506 ++it_p, ++it_m)
507 {
508 if (*it_p != '?' && *it_p != *it_m)
509 {
510 ok = false;
511 break;
512 }
513 }
514 if (ok)
515 {
516 results.push_back(FileFinder(our_path / leafname));
517 }
518 }
519 }
520 }
521 }
522
523 std::sort(results.begin(), results.end());
524 return results;
525}
526
527bool FileFinder::IsAbsolutePath(const std::string& rPath)
528{
529 return fs::path(rPath).is_absolute();
530}
531
533{
534 for (std::string::iterator it = rPath.begin(); it != rPath.end(); ++it)
535 {
536 if (*it == ' ')
537 {
538 *it = '_';
539 }
540 }
541}
542
544{
545 for (std::string::iterator it = rPath.begin(); it != rPath.end(); ++it)
546 {
547 if (*it == '_')
548 {
549 *it = ' ';
550 }
551 }
552}
553
554bool FileFinder::operator<(const FileFinder& otherFinder) const
555{
556 return (mAbsPath < otherFinder.GetAbsolutePath());
557}
558
559void FileFinder::FakePath(RelativeTo::Value fakeWhat, const std::string& rFakePath)
560{
561 msFakeWhat = fakeWhat;
562 msFakePath = rFakePath;
563 msFaking = true;
564}
565
567{
568 msFaking = false;
569}
const char * ChasteSourceRootDir()
const char * ChasteBuildRootDir()
#define EXCEPTION(message)
#define EXCEPT_IF(test)
#define NEVER_REACHED
static std::string ToPosix(const fs::path path)
void Remove() const
std::string GetLeafNameNoExtension() const
std::string mAbsPath
std::string GetRelativePath(const FileFinder &rBasePath) const
bool IsEmpty() const
bool IsNewerThan(const FileFinder &rOtherEntity) const
static void ReplaceSpacesWithUnderscores(std::string &rPath)
std::string GetAbsolutePath() const
static bool IsAbsolutePath(const std::string &rPath)
static bool msFaking
void PrivateRemove(bool dangerous=false) const
void DangerousRemove() const
std::string GetExtension() const
std::vector< FileFinder > FindMatches(const std::string &rPattern) const
FileFinder GetParent() const
static void FakePath(RelativeTo::Value fakeWhat, const std::string &rFakePath)
static RelativeTo::Value msFakeWhat
bool IsFile() const
static void ReplaceUnderscoresWithSpaces(std::string &rPath)
bool IsDir() const
bool operator<(const FileFinder &otherFinder) const
std::string GetLeafName() const
virtual void SetPath(const std::string &rPath, RelativeTo::Value relativeTo)
bool Exists() const
static std::string msFakePath
virtual ~FileFinder()
FileFinder CopyTo(const FileFinder &rDest) const
bool IsPathSet() const
static void StopFaking()
static void CopyFileWithPermissions(const fs::path &rFromPath, const fs::path &rToPath)
static bool CreateDirectoryWithPermissions(const fs::path &rPath)
static std::string GetChasteTestOutputDirectory()
static const std::string SIG_FILE_NAME