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