Line data Source code
1 :
2 : /** \file fileUtils.hpp
3 : * \brief Declarations of utilities for working with files
4 : *
5 : * \author Jared R. Males (jaredmales@gmail.com)
6 : *
7 : * \ingroup fileutils
8 : *
9 : */
10 :
11 : //***********************************************************************//
12 : // Copyright 2015-2020 Jared R. Males (jaredmales@gmail.com)
13 : //
14 : // This file is part of mxlib.
15 : //
16 : // mxlib is free software: you can redistribute it and/or modify
17 : // it under the terms of the GNU General Public License as published by
18 : // the Free Software Foundation, either version 3 of the License, or
19 : // (at your option) any later version.
20 : //
21 : // mxlib is distributed in the hope that it will be useful,
22 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
23 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 : // GNU General Public License for more details.
25 : //
26 : // You should have received a copy of the GNU General Public License
27 : // along with mxlib. If not, see <http://www.gnu.org/licenses/>.
28 : //***********************************************************************//
29 :
30 : #ifndef ioutils_fileUtils_hpp
31 : #define ioutils_fileUtils_hpp
32 :
33 : #include <string>
34 : #include <vector>
35 : #include <filesystem>
36 : #include <algorithm>
37 :
38 : #include "../mxlib.hpp"
39 :
40 : namespace mx
41 : {
42 : namespace ioutils
43 : {
44 :
45 : #ifndef ioutils_fileUtils_detail_hpp
46 : #define ioutils_fileUtils_detail_hpp
47 : namespace fileUtilsDetail
48 : {
49 :
50 : /** \cond */
51 : /// File-utility stages exposed to deterministic exception tests.
52 : enum class operation
53 : {
54 : stringToPath,
55 : getFileNames,
56 : createDirectories
57 : };
58 :
59 : /// Signature of the resettable file-utility operation hook.
60 : using operationHookT = void ( * )( operation );
61 :
62 : /// Access the process-wide file-utility operation hook.
63 : operationHookT &operationHook();
64 :
65 : /// Signature of the resettable file-utility error-code hook.
66 : using errorCodeHookT = void ( * )( operation, std::error_code & );
67 :
68 : /// Access the process-wide file-utility error-code hook.
69 : errorCodeHookT &errorCodeHook();
70 : /** \endcond */
71 :
72 : } // namespace fileUtilsDetail
73 : #endif // ioutils_fileUtils_detail_hpp
74 :
75 : #ifdef MXLIBTEST_NAMESPACE
76 : namespace MXLIBTEST_NAMESPACE
77 : {
78 : #endif
79 :
80 : /** \addtogroup fileutils
81 : * @{
82 : */
83 :
84 : /// Convert a string to a path, handling exceptions.
85 : /** Wrapper for `path = str` assignment that handles exceptions
86 : * and implements mxlib standard error handling.
87 : *
88 : * \returns error_t::noerror if no exceptions
89 : * \returns error_t::std_bad_alloc if std::bad_alloc is caught
90 : * \returns error_t::std_filesystem_error if std::filesystem::filesystem_error is caught
91 : * \returns error_t::std_exception if any other exceptions are caught
92 : *
93 : * \throws a nested mx::exception for any uncaught exceptions.
94 : */
95 : template <class verboseT>
96 : error_t string2path( std::filesystem::path &path, const std::string &str );
97 :
98 : /// Check if a path exists
99 : /**
100 : * \returns true if the path exists and no errors occur
101 : * \returns false otherwise
102 : */
103 : template <class verboseT = verbose::d>
104 : bool exists( const std::string &path, /**< [in] the path to check for existence */
105 : mx::error_t &errc /**< [out] error code. Typically convereted as errno from std::filesystem*/
106 : );
107 :
108 : /// Check if a path exists and is a directory
109 : /**
110 : * \returns true only if \p dir both exists and is a directory, and no errors occur
111 : * \returns false otherwise
112 : */
113 : template <class verboseT = verbose::d>
114 : bool dir_exists_is( const std::string &dir, /**< [in] the path to check */
115 : mx::error_t &errc /**< [out] error code. Typically convereted as errno from std::filesystem*/
116 : );
117 :
118 : /// Create a directory or directories
119 : /** This will create any directories in path that don't exist. It silently ignores already existing directories.
120 : *
121 : * \returns error_t::noerror on success, indicating the directories were created or already existed.
122 : * \returns other codes, error_t::exxxx (from errno) or error_t::filesystem, on errors.
123 : */
124 : error_t createDirectories( const std::string &path /**< [in] the path of the directory(ies)to create */ );
125 :
126 : /// Get the stem of the filename
127 : /**
128 : * \returns the stem for the filename, that is without the path or extension
129 : */
130 : std::string pathStem( const std::string &fname );
131 :
132 : /// Get the base filename
133 : /**
134 : * \returns the filename, including the extension but without the path
135 : */
136 : std::string pathFilename( const std::string &fname );
137 :
138 : /// Get the parent path from a filename
139 : /**
140 : * \returns the parent path of the file
141 : */
142 : std::string parentPath( const std::string &fname );
143 :
144 : /// Get a list of file names from the specified directory, specifying a prefix, a substring to match, and an extension
145 : /**
146 : * \returns mx::error_t::success on success
147 : * \returns mx::error_t::invalidarg if \p directory is not a directory
148 : * \returns mx::error_t::dirnotfound if \p directory does not exist
149 : * \returns mx::error_t::exception if an exception is thrown from the standard library
150 : *
151 : * \tparam verbose if true then error messages are printed as they occur
152 : *
153 : *
154 : */
155 : template <class verboseT = verbose::d>
156 : error_t getFileNames( std::vector<std::string> &fileNames, /** [out] The populated list of file names.*/
157 : const std::string &directory, /**< [in] The path to the directory to search.
158 : Can not be empty.*/
159 : const std::string &prefix, /**< [in] The file name prefix (the beginning
160 : characters of the file name) to search
161 : for. If "" then not used.*/
162 : const std::string &substr, /**< [in] A substring of the filename to search
163 : for. If "" then not used. Only matches
164 : after the first character.*/
165 : const std::string &extension /**< [in] The file name extension to search for.
166 : If "" then not used. This does not need
167 : to include the ".", as in".ext".*/
168 : );
169 :
170 : /// Prepend and/or append strings to a file name, leaving the directory and extension unaltered.
171 : /**
172 : * \returns the new file name
173 : */
174 : std::string fileNamePrependAppend( const std::string &fname, /**< [in] the original file name, possibly including a
175 : directory and extension*/
176 : const std::string &prepend, /**< [in] is the string to insert at the beginning of the
177 : file name after the path*/
178 : const std::string &append /**< [in] is the string to insert at the end of the file
179 : name, before the extension*/
180 : );
181 :
182 : /// Append a string to a file name, leaving the directory and extension unaltered.
183 : /**
184 : * \returns the new file name
185 : */
186 : std::string fileNameAppend( const std::string &fname, /**< [in] the original file name, possibly including
187 : a directory and extension*/
188 : const std::string &append /**< [in] is the string to insert at the end
189 : of the file name, before the extension*/
190 : );
191 :
192 : /// Prepend strings to a file name, leaving the directory and extension unaltered.
193 : /**
194 : * \returns the new file name
195 : */
196 : std::string fileNamePrepend( const std::string &fname, /**< [in] the original file name, possibly including
197 : a directory and extension*/
198 : const std::string &prepend /**< [in] is the string to insert at the beginning of
199 : the file name after the path*/
200 : );
201 :
202 : /// Get the next file in a numbered sequence
203 : /** Searches for files in the path designated by basename of the form basenameXXXXextension
204 : * where the number of digits in XXXX is set by the \a ndigit parameter.
205 : *
206 : * \warning this does not currently detect missing files in the sequence, e.g. if you have 0,1,3 in the directory this
207 : * will start with 2!
208 : *
209 : * \todo switch to using a regex or something so we can detect the missing file.
210 : *
211 : * \retval std::string containing the next filename.
212 : *
213 : */
214 : std::string getSequentialFilename( const std::string &basename, ///< [in] path and initial name of the file*/
215 : const std::string &extension = "", /**< [in] [optional] extension to append after the
216 : number. Default is empty.*/
217 : const int startat = 0, /**< [in] [optional] number to start the
218 : search from.
219 : Default is 0.*/
220 : int ndigit = 4 /**< [in] [optional] number of digits in string
221 : representation
222 : of the number.Default is 4. */
223 : );
224 :
225 : /// Get the size in bytes of a file
226 : /** Uses fstat.
227 : *
228 : * \returns the file size if fd is valid and no errors occur
229 : * \returns -1 on an error
230 : */
231 : off_t fileSize( int fd /**< [in] an open file descriptor */ );
232 :
233 : /// Get the size in bytes of a file pointed to by a FILE pointer
234 : /** Uses fileno to get the associated descriptor, then uses fstat.
235 : *
236 : * \returns the file size if fd is valid and no errors occur
237 : * \returns -1 on an error
238 : *
239 : * \overload
240 : */
241 : off_t fileSize( FILE *f /**< [in] an open file */ );
242 :
243 : ///@} -fileutils
244 :
245 : /* ===================================================================== */
246 : /* implementations */
247 :
248 : template <class verboseT>
249 29 : error_t string2path( std::filesystem::path &path, const std::string &str )
250 : {
251 : try
252 : {
253 29 : fileUtilsDetail::operationHook()( fileUtilsDetail::operation::stringToPath );
254 17 : path = str;
255 :
256 17 : return error_t::noerror;
257 : }
258 15 : catch( const std::bad_alloc &e )
259 : {
260 : // clang-format off
261 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS )
262 9 : return internal::mxlib_error_report<verboseT>( error_t::std_bad_alloc, e.what() );
263 : #else
264 6 : std::throw_with_nested(mx::exception<verboseT>(error_t::std_bad_alloc));
265 : #endif
266 : // clang-format on
267 : }
268 3 : catch( const std::filesystem::filesystem_error &e )
269 : {
270 : // clang-format off
271 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS ) || defined(MXLIB_CATCH_NONALLOC_EXCEPTIONS)
272 3 : return internal::mxlib_error_report<verboseT>( error_t::std_filesystem_error, e.what() );
273 : #else
274 2 : std::throw_with_nested(mx::exception<verboseT>(error_t::std_filesystem_error));
275 : #endif
276 : // clang-format on
277 : }
278 3 : catch( const std::exception &e )
279 : {
280 : // clang-format off
281 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS ) || defined(MXLIB_CATCH_NONALLOC_EXCEPTIONS)
282 3 : return internal::mxlib_error_report<verboseT>( error_t::std_exception, e.what() );
283 : #else
284 2 : std::throw_with_nested(mx::exception<verboseT>());
285 : #endif
286 : // clang-format on
287 : }
288 3 : catch( ... )
289 : {
290 : // clang-format off
291 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS ) || defined(MXLIB_CATCH_NONALLOC_EXCEPTIONS)
292 3 : return internal::mxlib_error_report<verboseT>( error_t::exception, "unknown exception");
293 : #else
294 2 : std::throw_with_nested(mx::exception<verboseT>());
295 : #endif
296 : // clang-format on
297 : }
298 : }
299 :
300 : template <class verboseT>
301 14 : bool exists( const std::string &strpath, mx::error_t &errc )
302 : {
303 14 : std::error_code ec;
304 :
305 14 : std::filesystem::path path;
306 :
307 : try
308 : {
309 14 : errc = string2path<verboseT>( path, strpath );
310 :
311 13 : if( !!errc )
312 : {
313 2 : internal::mxlib_error_report<verboseT>( errc, "converting path" );
314 1 : return false;
315 : }
316 : }
317 2 : catch( const mx::exception<verboseT> &e )
318 : {
319 2 : std::throw_with_nested( mx::exception<verboseT>( e.code() ) );
320 : }
321 :
322 12 : bool ex = std::filesystem::exists( path, ec );
323 :
324 12 : if( ec.value() != 0 )
325 : {
326 0 : errc = mx::errno2error_t( ec.value() );
327 0 : if( errc == error_t::error )
328 : {
329 0 : errc = error_t::filesystem;
330 : }
331 :
332 0 : internal::mxlib_error_report<verboseT>( errc, ec.message() );
333 :
334 0 : return false;
335 : }
336 :
337 12 : errc = error_t::noerror;
338 12 : return ex;
339 14 : }
340 :
341 : template <class verboseT>
342 7 : bool dir_exists_is( const std::string &dir, mx::error_t &errc )
343 : {
344 7 : std::error_code ec;
345 :
346 7 : std::filesystem::path path;
347 :
348 : try
349 : {
350 7 : errc = string2path<verboseT>( path, dir );
351 :
352 6 : if( !!errc )
353 : {
354 2 : internal::mxlib_error_report<verboseT>( errc, "converting path" );
355 1 : return false;
356 : }
357 : }
358 2 : catch( const mx::exception<verboseT> &e )
359 : {
360 2 : std::throw_with_nested( mx::exception<verboseT>( e.code() ) );
361 : }
362 :
363 5 : bool exists = std::filesystem::exists( path, ec );
364 :
365 : // clang-format off
366 : #ifdef MXLIBTEST_DIREXISTSIS_ISEXISTSERR
367 : ec = std::error_code( EEXIST, std::system_category() ); // LCOV_EXCL_LINE
368 : #endif
369 : // clang-format on
370 :
371 5 : if( ec.value() != 0 )
372 : {
373 1 : errc = mx::errno2error_t( ec.value() );
374 1 : if( errc == error_t::error )
375 : {
376 0 : errc = error_t::filesystem;
377 : }
378 :
379 1 : internal::mxlib_error_report<verboseT>( errc, ec.message() );
380 :
381 1 : return false;
382 : }
383 :
384 4 : if( !exists )
385 : {
386 1 : return false;
387 : }
388 :
389 3 : bool isdir = std::filesystem::is_directory( path, ec );
390 :
391 : // clang-format off
392 : #ifdef MXLIBTEST_DIREXISTSIS_ISDIRERR
393 : ec = std::error_code( EACCES, std::system_category() ); // LCOV_EXCL_LINE
394 : #endif
395 : // clang-format on
396 :
397 3 : if( ec.value() != 0 )
398 : {
399 1 : errc = errno2error_t( ec.value() );
400 1 : if( errc == mx::error_t::error )
401 : {
402 0 : errc = mx::error_t::filesystem;
403 : }
404 :
405 1 : internal::mxlib_error_report<verboseT>( errc, ec.message() );
406 :
407 1 : return false;
408 : }
409 :
410 2 : errc = error_t::noerror;
411 2 : return isdir;
412 7 : }
413 :
414 : template <class verboseT>
415 29 : error_t getFileNames( std::vector<std::string> &fileNames,
416 : const std::string &directory,
417 : const std::string &prefix,
418 : const std::string &substr,
419 : const std::string &extension )
420 : {
421 : try // there are several things that can throw here
422 : {
423 29 : fileUtilsDetail::operationHook()( fileUtilsDetail::operation::getFileNames );
424 21 : fileNames.clear();
425 :
426 21 : if( std::filesystem::exists( directory ) )
427 : {
428 17 : if( std::filesystem::is_directory( directory ) )
429 : {
430 13 : bool hasext = false;
431 13 : std::string _ext;
432 13 : if( extension.size() > 0 )
433 : {
434 5 : if( extension[0] != '.' )
435 : {
436 1 : _ext = '.';
437 : }
438 :
439 5 : _ext += extension;
440 :
441 5 : hasext = true;
442 : }
443 :
444 13 : bool hasprefix = ( prefix.size() > 0 );
445 :
446 13 : bool hassub = ( substr.size() > 0 );
447 :
448 13 : std::filesystem::directory_iterator it{ directory };
449 13 : auto it_end = std::filesystem::directory_iterator{};
450 212 : for( it; it != it_end; ++it )
451 : {
452 112 : if( hasext )
453 : {
454 50 : if( it->path().extension() != _ext )
455 : {
456 68 : continue;
457 : }
458 : }
459 :
460 87 : std::string p = it->path().filename().generic_string();
461 :
462 87 : if( hasprefix )
463 : {
464 41 : if( p.size() < prefix.size() )
465 : {
466 1 : continue;
467 : }
468 : else
469 : {
470 : // This won't throw because:
471 : // - prefix has size > 0
472 : // - p.size() >= prefix.size()
473 : // - therefore prefix.size() > 0
474 : // - so pos1 = 0 will not throw.
475 40 : if( p.compare( 0, prefix.size(), prefix ) != 0 )
476 : {
477 19 : continue;
478 : }
479 : }
480 : }
481 :
482 67 : if( hassub )
483 : {
484 34 : if( p.size() < 2 )
485 : {
486 1 : continue;
487 : }
488 :
489 33 : size_t sspos = p.find( substr, 1 ); // only match if not prefix
490 :
491 33 : if( sspos == std::string::npos )
492 : {
493 22 : continue;
494 : }
495 : }
496 :
497 : // If here then it passed all checks
498 : // this could throw
499 44 : fileNames.push_back( it->path().native() );
500 : }
501 :
502 13 : std::sort( fileNames.begin(), fileNames.end() );
503 13 : }
504 : else
505 : {
506 4 : return internal::mxlib_error_report<verboseT>( error_t::invalidarg, directory + " is not a directory" );
507 : }
508 : }
509 : else
510 : {
511 4 : return internal::mxlib_error_report<verboseT>( error_t::dirnotfound, directory + " was not found" );
512 : }
513 :
514 13 : return error_t::noerror;
515 : }
516 9 : catch( const std::bad_alloc &e )
517 : {
518 : // clang-format off
519 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS )
520 3 : return internal::mxlib_error_report<verboseT>( error_t::std_bad_alloc, e.what() );;
521 : #else
522 2 : std::throw_with_nested(mx::exception<verboseT>(error_t::std_bad_alloc));
523 : #endif
524 : // clang-format on
525 : }
526 3 : catch( const std::filesystem::filesystem_error &e )
527 : {
528 : // clang-format off
529 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS ) || defined( MXLIB_CATCH_NONALLOC_EXCEPTIONS )
530 3 : return internal::mxlib_error_report<verboseT>( error_t::std_filesystem_error, e.what() );
531 : #else
532 2 : std::throw_with_nested(mx::exception(error_t::std_filesystem_error));
533 : #endif
534 : // clang-format on
535 : }
536 3 : catch( const std::exception &e )
537 : {
538 : // clang-format off
539 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS ) || defined( MXLIB_CATCH_NONALLOC_EXCEPTIONS )
540 3 : return internal::mxlib_error_report<verboseT>( error_t::std_exception, e.what() );
541 : #else
542 2 : std::throw_with_nested(mx::exception<verboseT>(error_t::std_exception));
543 : #endif
544 : // clang-format on
545 : }
546 3 : catch( ... )
547 : {
548 : // clang-format off
549 : #if defined( MXLIB_CATCH_ALL_EXCEPTIONS ) || defined( MXLIB_CATCH_NONALLOC_EXCEPTIONS )
550 1 : return internal::mxlib_error_report<verboseT>( error_t::exception );
551 : #else
552 2 : std::throw_with_nested(mx::exception<verboseT>());
553 : #endif
554 : // clang-format on
555 : }
556 : }
557 :
558 : #ifdef MXLIBTEST_NAMESPACE
559 : } // namespace MXLIBTEST_NAMESPACE
560 : #endif
561 :
562 : } // namespace ioutils
563 : } // namespace mx
564 :
565 : #endif // fileUtils_hpp
|