1 #include "filesystem.h"
2 
3 #include <cstddef>
4 // IWYU pragma: no_include <sys/dirent.h>
5 // IWYU pragma: no_include <sys/errno.h>
6 // FILE I/O
7 #include <sys/stat.h>
8 #include <algorithm>
9 #include <cstdio>
10 #include <cstdlib>
11 #include <cstring>
12 #include <deque>
13 #include <fstream>
14 #include <functional>
15 #include <iterator>
16 #include <string>
17 #include <type_traits>
18 #include <vector>
19 
20 #include "cata_utility.h"
21 #include "debug.h"
22 
23 #if defined(_MSC_VER)
24 #   include <direct.h>
25 
26 #   include "wdirent.h"
27 #else
28 #   include <dirent.h>
29 #   include <unistd.h>
30 #endif
31 
32 #if defined(_WIN32)
33 #   include "platform_win.h"
34 #endif
35 
36 namespace
37 {
38 
39 #if defined(_WIN32)
do_mkdir(const std::string & path,const int)40 bool do_mkdir( const std::string &path, const int /*mode*/ )
41 {
42 #if defined(_MSC_VER)
43     return _mkdir( path.c_str() ) == 0;
44 #else
45     return mkdir( path.c_str() ) == 0;
46 #endif
47 }
48 #else
49 bool do_mkdir( const std::string &path, const int mode )
50 {
51     return mkdir( path.c_str(), mode ) == 0;
52 }
53 #endif
54 
55 } //anonymous namespace
56 
assure_dir_exist(const std::string & path)57 bool assure_dir_exist( const std::string &path )
58 {
59     return do_mkdir( path, 0777 ) || ( errno == EEXIST && dir_exist( path ) );
60 }
61 
dir_exist(const std::string & path)62 bool dir_exist( const std::string &path )
63 {
64     DIR *dir = opendir( path.c_str() );
65     if( dir != nullptr ) {
66         closedir( dir );
67         return true;
68     }
69     return false;
70 }
71 
file_exist(const std::string & path)72 bool file_exist( const std::string &path )
73 {
74     struct stat buffer;
75     return ( stat( path.c_str(), &buffer ) == 0 );
76 }
77 
78 #if defined(_WIN32)
remove_file(const std::string & path)79 bool remove_file( const std::string &path )
80 {
81     return DeleteFile( path.c_str() ) != 0;
82 }
83 #else
remove_file(const std::string & path)84 bool remove_file( const std::string &path )
85 {
86     return unlink( path.c_str() ) == 0;
87 }
88 #endif
89 
90 #if defined(_WIN32)
rename_file(const std::string & old_path,const std::string & new_path)91 bool rename_file( const std::string &old_path, const std::string &new_path )
92 {
93     // Windows rename function does not override existing targets, so we
94     // have to remove the target to make it compatible with the Linux rename
95     if( file_exist( new_path ) ) {
96         if( !remove_file( new_path ) ) {
97             return false;
98         }
99     }
100 
101     return rename( old_path.c_str(), new_path.c_str() ) == 0;
102 }
103 #else
rename_file(const std::string & old_path,const std::string & new_path)104 bool rename_file( const std::string &old_path, const std::string &new_path )
105 {
106     return rename( old_path.c_str(), new_path.c_str() ) == 0;
107 }
108 #endif
109 
remove_directory(const std::string & path)110 bool remove_directory( const std::string &path )
111 {
112 #if defined(_WIN32)
113     return RemoveDirectory( path.c_str() );
114 #else
115     return remove( path.c_str() ) == 0;
116 #endif
117 }
118 
eol()119 const char *cata_files::eol()
120 {
121 #if defined(_WIN32)
122     // NOLINTNEXTLINE(cata-text-style): carriage return is necessary here
123     static const char local_eol[] = "\r\n";
124 #else
125     static const char local_eol[] = "\n";
126 #endif
127     return local_eol;
128 }
129 
read_entire_file(const std::string & path)130 std::string read_entire_file( const std::string &path )
131 {
132     std::ifstream infile( path, std::ifstream::in | std::ifstream::binary );
133     return std::string( std::istreambuf_iterator<char>( infile ),
134                         std::istreambuf_iterator<char>() );
135 }
136 
137 namespace
138 {
139 //--------------------------------------------------------------------------------------------------
140 // For non-empty path, call function for each file at path.
141 //--------------------------------------------------------------------------------------------------
142 template <typename Function>
for_each_dir_entry(const std::string & path,Function function)143 void for_each_dir_entry( const std::string &path, Function function )
144 {
145     using dir_ptr = DIR*;
146 
147     if( path.empty() ) {
148         return;
149     }
150 
151     const dir_ptr root = opendir( path.c_str() );
152     if( !root ) {
153         const char *e_str = strerror( errno );
154         DebugLog( D_WARNING, D_MAIN ) << "opendir [" << path << "] failed with \"" << e_str << "\".";
155         return;
156     }
157 
158     while( const dirent *entry = readdir( root ) ) {
159         function( *entry );
160     }
161     closedir( root );
162 }
163 
164 //--------------------------------------------------------------------------------------------------
165 #if !defined(_WIN32)
resolve_path(const std::string & full_path)166 std::string resolve_path( const std::string &full_path )
167 {
168     char *const result_str = realpath( full_path.c_str(), nullptr );
169     if( !result_str ) {
170         char *const e_str = strerror( errno );
171         DebugLog( D_WARNING, D_MAIN ) << "realpath [" << full_path << "] failed with \"" << e_str << "\".";
172         return {};
173     }
174 
175     std::string result( result_str );
176     free( result_str );
177     return result;
178 }
179 #endif
180 
181 //--------------------------------------------------------------------------------------------------
is_directory_stat(const std::string & full_path)182 bool is_directory_stat( const std::string &full_path )
183 {
184     if( full_path.empty() ) {
185         return false;
186     }
187 
188     struct stat result;
189     if( stat( full_path.c_str(), &result ) != 0 ) {
190         const char *e_str = strerror( errno );
191         DebugLog( D_WARNING, D_MAIN ) << "stat [" << full_path << "] failed with \"" << e_str << "\".";
192         return false;
193     }
194 
195     if( S_ISDIR( result.st_mode ) ) {
196         // NOLINTNEXTLINE(readability-simplify-boolean-expr)
197         return true;
198     }
199 
200 #if !defined(_WIN32)
201     if( S_ISLNK( result.st_mode ) ) {
202         return is_directory_stat( resolve_path( full_path ) );
203     }
204 #endif
205 
206     return false;
207 }
208 
209 //--------------------------------------------------------------------------------------------------
210 // Returns true if entry is a directory, false otherwise.
211 //--------------------------------------------------------------------------------------------------
212 #if defined(__MINGW32__)
is_directory(const dirent &,const std::string & full_path)213 bool is_directory( const dirent &/*entry*/, const std::string &full_path )
214 {
215     // no dirent::d_type
216     return is_directory_stat( full_path );
217 }
218 #else
is_directory(const dirent & entry,const std::string & full_path)219 bool is_directory( const dirent &entry, const std::string &full_path )
220 {
221     if( entry.d_type == DT_DIR ) {
222         return true;
223     }
224 
225 #if !defined(_WIN32)
226     if( entry.d_type == DT_LNK ) {
227         return is_directory_stat( resolve_path( full_path ) );
228     }
229 #endif
230 
231     if( entry.d_type == DT_UNKNOWN ) {
232         return is_directory_stat( full_path );
233     }
234 
235     return false;
236 }
237 #endif
238 
239 //--------------------------------------------------------------------------------------------------
240 // Returns true if the name of entry matches "." or "..".
241 //--------------------------------------------------------------------------------------------------
is_special_dir(const dirent & entry)242 bool is_special_dir( const dirent &entry )
243 {
244     return !strncmp( entry.d_name, ".",  sizeof( entry.d_name ) - 1 ) ||
245            !strncmp( entry.d_name, "..", sizeof( entry.d_name ) - 1 );
246 }
247 
248 //--------------------------------------------------------------------------------------------------
249 // If at_end is true, returns whether entry's name ends in match.
250 // Otherwise, returns whether entry's name contains match.
251 //--------------------------------------------------------------------------------------------------
name_contains(const dirent & entry,const std::string & match,const bool at_end)252 bool name_contains( const dirent &entry, const std::string &match, const bool at_end )
253 {
254     const size_t len_fname = strlen( entry.d_name );
255     const size_t len_match = match.length();
256 
257     if( len_match > len_fname ) {
258         return false;
259     }
260 
261     const size_t offset = at_end ? ( len_fname - len_match ) : 0;
262     return strstr( entry.d_name + offset, match.c_str() ) != nullptr;
263 }
264 
265 //--------------------------------------------------------------------------------------------------
266 // Return every file at root_path matching predicate.
267 //
268 // If root_path is empty, search the current working directory.
269 // If recursive_search is true, search breadth-first into the directory hierarchy.
270 //
271 // Results are ordered depth-first with directories searched in lexically order. Furthermore,
272 // regular files at each level are also ordered lexically by file name.
273 //
274 // Files ending in ~ are excluded.
275 //--------------------------------------------------------------------------------------------------
276 template <typename Predicate>
find_file_if_bfs(const std::string & root_path,const bool recursive_search,Predicate predicate)277 std::vector<std::string> find_file_if_bfs( const std::string &root_path,
278         const bool recursive_search,
279         Predicate predicate )
280 {
281     std::deque<std::string>  directories {!root_path.empty() ? root_path : "."};
282     std::vector<std::string> results;
283 
284     while( !directories.empty() ) {
285         const auto path = std::move( directories.front() );
286         directories.pop_front();
287 
288         const std::ptrdiff_t n_dirs    = static_cast<std::ptrdiff_t>( directories.size() );
289         const std::ptrdiff_t n_results = static_cast<std::ptrdiff_t>( results.size() );
290 
291         for_each_dir_entry( path, [&]( const dirent & entry ) {
292             // exclude special directories.
293             if( is_special_dir( entry ) ) {
294                 return;
295             }
296 
297             const auto full_path = path + "/" + entry.d_name;
298 
299             // don't add files ending in '~'.
300             if( full_path.back() == '~' ) {
301                 return;
302             }
303 
304             // add sub directories to recursive_search if requested
305             const bool is_dir = is_directory( entry, full_path );
306             if( recursive_search && is_dir ) {
307                 directories.emplace_back( full_path );
308             }
309 
310             // check the file
311             if( !predicate( entry, is_dir ) ) {
312                 return;
313             }
314 
315             results.emplace_back( full_path );
316         } );
317 
318         // Keep files and directories to recurse ordered consistently
319         // by sorting from the old end to the new end.
320         // NOLINTNEXTLINE(cata-use-localized-sorting)
321         std::sort( std::begin( directories ) + n_dirs,    std::end( directories ) );
322         // NOLINTNEXTLINE(cata-use-localized-sorting)
323         std::sort( std::begin( results )     + n_results, std::end( results ) );
324     }
325 
326     return results;
327 }
328 
329 } //anonymous namespace
330 
331 //--------------------------------------------------------------------------------------------------
get_files_from_path(const std::string & pattern,const std::string & root_path,const bool recursive_search,const bool match_extension)332 std::vector<std::string> get_files_from_path( const std::string &pattern,
333         const std::string &root_path, const bool recursive_search, const bool match_extension )
334 {
335     return find_file_if_bfs( root_path, recursive_search, [&]( const dirent & entry, bool ) {
336         return name_contains( entry, pattern, match_extension );
337     } );
338 }
339 
340 /**
341  *  Find directories which containing pattern.
342  *  @param pattern Search pattern.
343  *  @param root_path Search root.
344  *  @param recursive_search Be recurse or not.
345  *  @return vector or directories without pattern filename at end.
346  */
get_directories_with(const std::string & pattern,const std::string & root_path,const bool recursive_search)347 std::vector<std::string> get_directories_with( const std::string &pattern,
348         const std::string &root_path, const bool recursive_search )
349 {
350     if( pattern.empty() ) {
351         return std::vector<std::string>();
352     }
353 
354     auto files = find_file_if_bfs( root_path, recursive_search, [&]( const dirent & entry, bool ) {
355         return name_contains( entry, pattern, true );
356     } );
357 
358     // Chop off the file names. Dir path MUST be splitted by '/'
359     for( auto &file : files ) {
360         file.erase( file.rfind( '/' ), std::string::npos );
361     }
362 
363     files.erase( std::unique( std::begin( files ), std::end( files ) ), std::end( files ) );
364 
365     return files;
366 }
367 
368 /**
369  *  Find directories which containing pattern.
370  *  @param patterns Search patterns.
371  *  @param root_path Search root.
372  *  @param recursive_search Be recurse or not.
373  *  @return vector or directories without pattern filename at end.
374  */
get_directories_with(const std::vector<std::string> & patterns,const std::string & root_path,const bool recursive_search)375 std::vector<std::string> get_directories_with( const std::vector<std::string> &patterns,
376         const std::string &root_path, const bool recursive_search )
377 {
378     if( patterns.empty() ) {
379         return std::vector<std::string>();
380     }
381 
382     const auto ext_beg = std::begin( patterns );
383     const auto ext_end = std::end( patterns );
384 
385     auto files = find_file_if_bfs( root_path, recursive_search, [&]( const dirent & entry, bool ) {
386         return std::any_of( ext_beg, ext_end, [&]( const std::string & ext ) {
387             return name_contains( entry, ext, true );
388         } );
389     } );
390 
391     //chop off the file names
392     for( auto &file : files ) {
393         file.erase( file.rfind( '/' ), std::string::npos );
394     }
395 
396     //remove resulting duplicates
397     files.erase( std::unique( std::begin( files ), std::end( files ) ), std::end( files ) );
398 
399     return files;
400 }
401 
copy_file(const std::string & source_path,const std::string & dest_path)402 bool copy_file( const std::string &source_path, const std::string &dest_path )
403 {
404     std::ifstream source_stream( source_path.c_str(), std::ifstream::in | std::ifstream::binary );
405     if( !source_stream ) {
406         return false;
407     }
408     return write_to_file( dest_path, [&]( std::ostream & dest_stream ) {
409         dest_stream << source_stream.rdbuf();
410     }, nullptr ) &&source_stream;
411 }
412 
ensure_valid_file_name(const std::string & file_name)413 std::string ensure_valid_file_name( const std::string &file_name )
414 {
415     const char replacement_char = ' ';
416     const std::string invalid_chars = "\\/:?\"<>|";
417 
418     // do any replacement in the file name, if needed.
419     std::string new_file_name = file_name;
420     std::transform( new_file_name.begin(), new_file_name.end(),
421     new_file_name.begin(), [&]( const char c ) {
422         if( invalid_chars.find( c ) != std::string::npos ) {
423             return replacement_char;
424         }
425         return c;
426     } );
427 
428     return new_file_name;
429 }
430