libi2ncommon: (tomj) merged common code from connd
[libi2ncommon] / src / filefunc.cpp
1 /***************************************************************************
2                           escape.cpp  -  escaping of strings
3                              -------------------
4     begin                : Sun Nov 14 1999
5     copyright            : (C) 1999 by Intra2net AG
6     email                : info@intra2net.com
7  ***************************************************************************/
8
9 #include <list>
10 #include <string>
11 #include <fstream>
12 #include <sstream>
13 #include <iostream>
14 #include <stdexcept>
15
16 #include <sys/types.h>
17 #include <sys/stat.h>
18 #include <dirent.h>
19 #include <pwd.h>
20 #include <grp.h>
21 #include <unistd.h>
22 #include <errno.h>
23
24 #include <boost/scoped_array.hpp>
25 #include "filefunc.hxx"
26 #include "stringfunc.hxx"
27
28 namespace i2n
29 {
30
31 using namespace std;
32
33 /*
34 ** implementation of Stat
35 */
36
37 Stat::Stat()
38 : m_valid(false)
39 {
40     clear();
41 } // eo Stat::Stat()
42
43
44 Stat::Stat(const std::string& path, bool follow_links)
45 {
46     stat(path,follow_links);
47 } // eo Stat::Stat(const std::string&,bool)
48
49
50 Stat::~Stat()
51 {
52 } // eo Stat::~Stat()
53
54
55 /**
56  * @brief updates the internal data.
57  *
58  * In other words: stat()'s the file again.
59  */
60 void Stat::recheck()
61 {
62     if (! m_path.empty())
63     {
64         stat(m_path, m_follow_links);
65     }
66 } // eo Stat::recheck()
67
68
69 /**
70  * @brief calls stat() or lstat() to get the information for the given path
71  *  and stores that information.
72  * @param path the path which should be checked
73  * @param follow_links determine if (symbalic) links should be followed.
74  */
75 void Stat::stat(const std::string& path, bool follow_links)
76 {
77     clear();
78     m_path= path;
79     m_follow_links= follow_links;
80     struct stat stat_info;
81     int res;
82     res = ( follow_links ? ::stat(path.c_str(), &stat_info) : ::lstat(path.c_str(), &stat_info) );
83     if ( 0 == res )
84     {
85         m_device = stat_info.st_dev;
86         m_inode  = stat_info.st_ino;
87         m_mode   = (stat_info.st_mode & ~(S_IFMT));
88         m_num_links = stat_info.st_nlink;
89         m_uid    = stat_info.st_uid;
90         m_gid    = stat_info.st_gid;
91         m_device_type = stat_info.st_rdev;
92         m_size   = stat_info.st_size;
93         m_atime  = stat_info.st_atime;
94         m_mtime  = stat_info.st_mtime;
95         m_ctime  = stat_info.st_atime;
96         
97         m_is_link=          S_ISLNK( stat_info.st_mode );
98         m_is_regular=       S_ISREG( stat_info.st_mode );
99         m_is_directory=     S_ISDIR( stat_info.st_mode );
100         m_is_character_device= S_ISCHR( stat_info.st_mode );
101         m_is_block_device=  S_ISBLK( stat_info.st_mode );
102         m_is_fifo=          S_ISFIFO( stat_info.st_mode );
103         m_is_socket=        S_ISSOCK( stat_info.st_mode );
104     }
105     m_valid = (0 == res);
106 } // eo Stat::stat(const std::string&,bool)
107
108
109 /**
110  * @brief clears the internal data.
111  */
112 void Stat::clear()
113 {
114     m_path.clear();
115     m_valid= false;
116     
117     m_device = 0;
118     m_inode  = 0;
119     m_mode   = 0;
120     m_num_links = 0;
121     m_uid    = 0;
122     m_gid    = 0;
123     m_device_type = 0;
124     m_size   = 0;
125     m_atime  = 0;
126     m_mtime  = 0;
127     m_ctime  = 0;
128     
129     m_is_link= false;
130     m_is_regular= false;
131     m_is_directory= false;
132     m_is_character_device= false;
133     m_is_block_device= false;
134     m_is_fifo= false;
135     m_is_socket= false;
136 } // eo Stat::clear()
137
138
139 /**
140  * @brief checks if another instance describes the same file.
141  * @param rhs the other instance.
142  * @return @a true iff the other instance describes the same file.
143  * @note
144  * The "same file" means that the files are located on the same device and use the same inode.
145  * They might still have two different directory entries (different paths)!
146  */
147 bool Stat::isSameAs(const Stat& rhs)
148 {
149     return m_valid and rhs.m_valid
150         and ( m_device == rhs.m_device)
151         and ( m_inode == rhs.m_inode);
152 } // eo Stat::isSameAs(const Stat& rhs);
153
154
155 /**
156  * @brief checks if this and the other instance describe the same device.
157  * @param rhs the other instance.
158  * @return @a true if we and the other instance describe a device and the same device.
159  *
160  * "Same device" means that the devices have the same type and the same major and minor id.
161  */
162 bool Stat::isSameDeviceAs(const Stat& rhs)
163 {
164     return isDevice() and rhs.isDevice()
165         and ( m_is_block_device == rhs.m_is_block_device )
166         and ( m_is_character_device == rhs.m_is_character_device )
167         and ( m_device_type == rhs.m_device_type);
168 } // eo Stat::isSameDeviceAs(const Stat&)
169
170 /**
171  * @brief check existence of a path.
172  * @param path path which should be tested.
173  * @return @a true iff path exists.
174  */
175 bool path_exists(const std::string& path)
176 {
177     struct stat stat_info;
178     int res = ::stat(path.c_str(), &stat_info);
179     if (res) return false;
180     return true;
181 } // eo path_exists(const std::string&)
182
183 /**
184  * @brief check existence of a regular file.
185  * @param path path which should be tested.
186  * @return @a true if path exists and is a regular file (or a link pointing to a regular file).
187  * @note this checks for regular files; not for the pure exitsnace of a path; use pathExists() for that.
188  * @see pathExists
189  */
190 bool file_exists(const std::string& path)
191 {
192     struct stat stat_info;
193     int res = ::stat(path.c_str(), &stat_info);
194     if (res) return false;
195     return S_ISREG(stat_info.st_mode);
196 } // eo file_exists(const std::string&)
197
198 // TODO: Use Stat class
199 /**
200  * Get size of file
201  * @param name filename to get size for
202  * @return file size or -1 if file does not exist
203  */
204 long file_size (const string &name)
205 {
206     long iReturn = -1;
207
208     struct stat statbuff;
209
210     if (lstat(name.c_str(), &statbuff) < 0)
211         return -1;
212
213     if (!S_ISREG(statbuff.st_mode))
214         return -1;
215
216     iReturn=statbuff.st_size;
217
218     return iReturn;
219 }
220
221 /**
222  * @brief tests the last modification time stamp of a path.
223  * @param path path which should be tested.
224  * @return the last modification time or 0 if the path doen't exist.
225  */
226 time_t fileMTime(const std::string& path)
227 {
228     struct stat stat_info;
229     int res = ::stat(path.c_str(), &stat_info);
230     if (res) return 0;
231     return stat_info.st_mtime;
232 } // eo fileMTime(const std::string&)
233
234
235 /**
236  * @brief reads the contents of a directory.
237  * @param path the path to the directory whose contents should be read.
238  * @param[out] result the resulting list of names.
239  * @param include_dot_names determines if dot-files should be included in the list.
240  * @return @a true if reading the directory was succesful, @a false on error.
241  */
242 bool getDir(
243     const std::string& path,
244     std::vector< std::string >& result,
245     bool include_dot_names )
246 {
247     DIR* dir = ::opendir( path.c_str());
248     if (!dir)
249     {
250         return false;
251     }
252     struct dirent *entry;
253     while ( NULL != (entry = ::readdir(dir)) )
254     {
255         std::string name( entry->d_name );
256         if (! include_dot_names && (name[0] == '.') )
257         {
258             continue;
259         }
260         result.push_back( name );
261     }
262     ::closedir(dir);
263     return true;
264 } // eo getDir(const std::string&,std::vector< std::string >&,bool)
265
266
267 /**
268  * @brief reads the contents of a directory
269  * @param path the path to the directory whose contents should be read.
270  * @param include_dot_names determines if dot-files should be included in the list.
271  * @return the list of names (empty on error).
272  */
273 std::vector< std::string > getDir(const std::string& path, bool include_dot_names )
274 {
275     std::vector< std::string > result;
276     getDir(path,result,include_dot_names);
277     return result;
278 } // eo getDir(const std::string&,bool)
279
280
281
282 /**
283  * @brief removes a file from a filesystem.
284  * @param path path to the file.
285  * @return @a true iff the unlink was successful.
286  */
287 bool unlink( const std::string& path )
288 {
289     int res = ::unlink( path.c_str() );
290     return (res == 0);
291 } // eo unlink(const std::string&)
292
293
294
295 /**
296  * @brief creates a symbolic link named @a link_name to @a target.
297  * @param target the target the link should point to.
298  * @param link_name the name of the link.
299  * @param force if @a true, the (file or link) @a link_name is removed if it exists.
300  * @return @a true iff the symlink was successfully created.
301  */
302 bool symlink(const std::string& target, const std::string& link_name, bool force)
303 {
304     int res= -1;
305     if (target.empty() or link_name.empty() or target == link_name)
306     {
307         // no, we don't do this!
308         return false;
309     }
310     std::string n_target;
311     if (target[0] == '/') // absolute target?
312     {
313         n_target= target;
314     }
315     else // relative target
316     {
317         // for stat'ing: prepend dir of link_name:
318         n_target= dirname(link_name)+"/"+ target;
319     }
320     Stat target_stat(n_target, false);
321     Stat link_name_stat(link_name, false);
322     if (target_stat.exists() && link_name_stat.exists())
323     {
324         if (link_name_stat.isSameAs(target_stat)
325             or link_name_stat.isSameDeviceAs(target_stat) )
326         {
327             return false;
328         }
329         //TODO: more consistency checks?!
330     }
331     if (link_name_stat.exists())
332     {
333         // the link name already exists.
334         if (force)
335         {
336             // "force" as given, so try to remove the link_name:
337             unlink(link_name);
338             // update the stat:
339             link_name_stat.recheck();
340         }
341     }
342     if (link_name_stat.exists())
343     {
344         // well, if the link_name still exists; we cannot create that link:
345         errno = EEXIST;
346         return false;
347     }
348     res= ::symlink(target.c_str(), link_name.c_str());
349     return (0 == res);
350 } // eo symlink(const std::string&,const std::string&,bool)
351
352
353
354 /**
355  * @brief reads the target of a symbolic link.
356  * @param path path to the symbolic link
357  * @return the target of the link or an empty string on error.
358  */
359 std::string readLink(const std::string& path)
360 {
361     errno= 0;
362     Stat stat(path,false);
363     if (!stat || !stat.isLink())
364     {
365         return std::string();
366     }
367     int buffer_size= PATH_MAX+1 + 128;
368     boost::scoped_array<char> buffer_ptr( new char[buffer_size] );
369     int res= ::readlink( path.c_str(), buffer_ptr.get(), buffer_size-1 );
370     if (res >= 0)
371     {
372         return std::string( buffer_ptr.get(), res );
373     }
374     return std::string();
375 } // eo readLink(const std::string&)
376
377
378
379 /**
380  * @brief returns content of a file as string.
381  *
382  * A simple (q'n'd) function for retrieving content of a file as string.<br>
383  * Also able to read special (but regular) files which don't provide a size when stat'ed
384  * (like files in the /proc filesystem).
385  *
386  * @param path path to the file.
387  * @return the content of the file as string (empty if file could be opened).
388  */
389 std::string readFile(const std::string& path)
390 {
391     Stat stat(path);
392     if (!stat.isReg())
393     {
394         return std::string();
395     }
396     std::ifstream f( path.c_str(), std::ios::in | std::ios::binary );
397     std::string result;
398     if (f.is_open())
399     {
400         // NOTE: there are cases where we don't know the correct size (/proc files...)
401         // therefore we only use the size for reserving space if we know it, but don't
402         // use it when reading the file!
403         if (stat.size() > 0)
404         {
405             // if we know the size, we reserve enough space.
406             result.reserve( stat.size() );
407         }
408         char buffer[2048];
409         while (f.good())
410         {
411             f.read(buffer, sizeof(buffer));
412             result.append(buffer, f.gcount());
413         }
414     }
415     return result;
416 } // eo readFile
417
418
419 /**
420  * @brief writes a string to a file.
421  * @param path path to the file
422  * @param data the data which should be written into the file
423  * @return @a true if the data was written to the file.
424  *
425  * A simple (q'n'd) function for writing a string to a file.
426  */
427 bool writeFile(const std::string& path, const std::string& data)
428 {
429     std::ofstream f( path.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
430     if (f.good())
431     {
432         f.write( data.data(), data.size() );
433         return f.good();
434     }
435     else
436     {
437         return false;
438     }
439 } // eo writeFile
440
441
442 /**
443  * @brief returns the filename part of a path (last component)
444  * @param path the path.
445  * @return the last component of the path.
446  */
447 std::string basename(const std::string& path)
448 {
449     std::string::size_type pos= path.rfind('/');
450     if (pos != std::string::npos)
451     {
452         return path.substr(pos+1);
453     }
454     return path;
455 } // eo basename(const std::string&)
456
457
458 /**
459  * @brief returns the directory part of a path.
460  * @param path the path.
461  * @return the directory part of the path.
462  */
463 std::string dirname(const std::string& path)
464 {
465     std::string::size_type pos= path.rfind('/');
466     if (pos != std::string::npos)
467     {
468         std::string result(path,0,pos);
469         if (result.empty())
470         {
471             return ".";
472         }
473         return result;
474     }
475     return ".";
476 } // eo dirname(const std::string&)
477
478
479 /**
480  * @brief normalizes a path.
481  *
482  * This method removes empty and "." elements.
483  * It also resolves ".." parts by removing previous path elements if possible.
484  * Leading ".." elements are preserved when a relative path was given; else they are removed.
485  * Trailing slashes are removed.
486  *
487  * @param path the path which should be normalized.
488  * @return the normalized path.
489  */
490
491 std::string normalizePath(const std::string& path)
492 {
493     if (path.empty())
494     {
495         return std::string();
496     }
497     // remember if the given path was absolute since this information vanishes when
498     // we split the path (since we split with omitting empty parts...)
499     bool is_absolute= (path[0]=='/');
500     std::list< std::string >  parts;
501     std::list< std::string >  result_parts;
502     
503     splitString(path,parts,"/",true);
504     
505     for(std::list< std::string >::const_iterator it_parts= parts.begin();
506         it_parts != parts.end();
507         ++it_parts)
508     {
509         std::string part(*it_parts); //convenience..
510         if (part == std::string(".") )
511         {
512             // single dot is "current path"; ignore!
513             continue;
514         }
515         if (part == std::string("..") )
516         {
517             // double dot is "one part back"
518             if (result_parts.empty())
519             {
520                 if (is_absolute)
521                 {
522                     // ignore since we cannot move behind / on absolute paths...
523                 }
524                 else
525                 {
526                     // on relative path, we need to store the "..":
527                     result_parts.push_back(part);
528                 }
529             }
530             else if (result_parts.back() == std::string("..") )
531             {
532                 // if last element was already "..", we need to store the new one again...
533                 // (PS: no need for "absolute" check; this can only be the case on relative path)
534                 result_parts.push_back(part);
535             }
536             else
537             {
538                 // remove last element.
539                 result_parts.pop_back();
540             }
541             continue;
542         }
543         result_parts.push_back(part);
544     }
545     std::string result;
546     if (is_absolute)
547     {
548         result= "/";
549     }
550     result+= joinString(result_parts,"/");
551     return result;
552 } // eo normalizePath(const std::string&)
553
554
555 /**
556  * @brief changes the file(/path) mode.
557  * @param path the path to change the mode for.
558  * @param mode the new file mode.
559  * @return @a true iff the file mode was sucessfully changed.
560  */
561 bool chmod(const std::string& path, int mode)
562 {
563     int res= ::chmod(path.c_str(), mode);
564     return (res==0);
565 } // eo chmod(const std::string&,int)
566
567
568 /**
569  * @brief changed the owner of a file(/path)
570  * @param path the path to change the owner for.
571  * @param user the new file owner.
572  * @param group the new file group.
573  * @return @a true iff the file owner was succesfully changed.
574  *
575  * @note
576  * the validity of user and group within the system is not checked.
577  * This is intentional since this way we can use id's which are not assigned.
578  */
579 bool chown(const std::string& path, const i2n::User& user, const i2n::Group& group)
580 {
581     uid_t uid= user.uid;
582     if (uid<0) return false;
583     gid_t gid= group.gid;
584     if (gid<0) gid= user.gid;
585     if (gid<0) return false;
586     int res= ::chown( path.c_str(), uid, gid);
587     return (res==0);
588 } // eo chown(const std::string&,const User&,const Group&)
589
590 /**
591  * Recursive delete of files and directories
592  * @param path File or directory to delete
593  * @param error Will contain the error if the return value is false
594  * @return true on success, false otherwise
595  */
596 bool recursive_delete(const std::string &path, std::string *error)
597 {
598     bool rtn = true;
599
600     try {
601         struct stat my_stat;
602         if (stat(path.c_str(), &my_stat) != 0) {
603             throw runtime_error("can't stat " + path);
604         }
605
606         if (S_ISDIR(my_stat.st_mode)) {
607             DIR *dir = opendir(path.c_str());
608             if (!dir) {
609                 throw runtime_error("can't open directory " + path);
610             }
611
612             struct dirent *entry;
613             while ((entry = readdir(dir))) {
614                 string filename = entry->d_name;
615                 if (filename == "." || filename == "..") {
616                     continue;
617                 }
618
619                 // Delete subdir or file.
620                 rtn = recursive_delete(path + "/" + filename, error);
621                 if (rtn == false) {
622                     break;
623                 }
624             }
625
626             closedir(dir);
627             if (rmdir(path.c_str()) != 0) {
628                 throw runtime_error("can't remove directory " + path);
629             }
630         } else {
631             if (unlink(path.c_str()) != 0) {
632                 throw runtime_error("can't unlink " + path);
633             }
634         }
635     } catch (exception &e) {
636         if (error) {
637             ostringstream out;
638             out << e.what() << " (" << strerror(errno) << ")";
639             *error = out.str();
640         }
641         rtn = false;
642     } catch (...) {
643         if (error) {
644             ostringstream out;
645             out << "unknown error (" << strerror(errno) << ")";
646             *error = out.str();
647         }
648         rtn = false;
649     }
650
651     return rtn;
652 }
653
654 }