xref: /netbsd-src/external/bsd/atf/dist/tools/fs.cpp (revision 6d3e616a9320317516277c0a83e2129886060cf5)
1 //
2 // Automated Testing Framework (atf)
3 //
4 // Copyright (c) 2007 The NetBSD Foundation, Inc.
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions
9 // are met:
10 // 1. Redistributions of source code must retain the above copyright
11 //    notice, this list of conditions and the following disclaimer.
12 // 2. Redistributions in binary form must reproduce the above copyright
13 //    notice, this list of conditions and the following disclaimer in the
14 //    documentation and/or other materials provided with the distribution.
15 //
16 // THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND
17 // CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
18 // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19 // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 // IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY
21 // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23 // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
25 // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
26 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27 // IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 //
29 
30 extern "C" {
31 #include <sys/param.h>
32 #include <sys/types.h>
33 #include <sys/mount.h>
34 #include <sys/stat.h>
35 #include <sys/wait.h>
36 
37 #include <dirent.h>
38 #include <libgen.h>
39 #include <unistd.h>
40 }
41 
42 #include <cassert>
43 #include <cerrno>
44 #include <cstdlib>
45 #include <cstring>
46 
47 #include "auto_array.hpp"
48 #include "env.hpp"
49 #include "exceptions.hpp"
50 #include "fs.hpp"
51 #include "text.hpp"
52 #include "user.hpp"
53 
54 namespace impl = tools::fs;
55 #define IMPL_NAME "tools::fs"
56 
57 // ------------------------------------------------------------------------
58 // Auxiliary functions.
59 // ------------------------------------------------------------------------
60 
61 static void cleanup_aux(const impl::path&, dev_t, bool);
62 static void cleanup_aux_dir(const impl::path&, const impl::file_info&,
63                             bool);
64 static void do_unmount(const impl::path&);
65 static bool safe_access(const impl::path&, int, int);
66 
67 static const int access_f = 1 << 0;
68 static const int access_r = 1 << 1;
69 static const int access_w = 1 << 2;
70 static const int access_x = 1 << 3;
71 
72 //!
73 //! An implementation of access(2) but using the effective user value
74 //! instead of the real one.  Also avoids false positives for root when
75 //! asking for execute permissions, which appear in SunOS.
76 //!
77 static
78 void
eaccess(const tools::fs::path & p,int mode)79 eaccess(const tools::fs::path& p, int mode)
80 {
81     assert(mode & access_f || mode & access_r ||
82            mode & access_w || mode & access_x);
83 
84     struct stat st;
85     if (lstat(p.c_str(), &st) == -1)
86         throw tools::system_error(IMPL_NAME "::eaccess",
87                                   "Cannot get information from file " +
88                                   p.str(), errno);
89 
90     /* Early return if we are only checking for existence and the file
91      * exists (stat call returned). */
92     if (mode & access_f)
93         return;
94 
95     bool ok = false;
96     if (tools::user::is_root()) {
97         if (!ok && !(mode & access_x)) {
98             /* Allow root to read/write any file. */
99             ok = true;
100         }
101 
102         if (!ok && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
103             /* Allow root to execute the file if any of its execution bits
104              * are set. */
105             ok = true;
106         }
107     } else {
108         if (!ok && (tools::user::euid() == st.st_uid)) {
109             ok = ((mode & access_r) && (st.st_mode & S_IRUSR)) ||
110                  ((mode & access_w) && (st.st_mode & S_IWUSR)) ||
111                  ((mode & access_x) && (st.st_mode & S_IXUSR));
112         }
113         if (!ok && tools::user::is_member_of_group(st.st_gid)) {
114             ok = ((mode & access_r) && (st.st_mode & S_IRGRP)) ||
115                  ((mode & access_w) && (st.st_mode & S_IWGRP)) ||
116                  ((mode & access_x) && (st.st_mode & S_IXGRP));
117         }
118         if (!ok && ((tools::user::euid() != st.st_uid) &&
119                     !tools::user::is_member_of_group(st.st_gid))) {
120             ok = ((mode & access_r) && (st.st_mode & S_IROTH)) ||
121                  ((mode & access_w) && (st.st_mode & S_IWOTH)) ||
122                  ((mode & access_x) && (st.st_mode & S_IXOTH));
123         }
124     }
125 
126     if (!ok)
127         throw tools::system_error(IMPL_NAME "::eaccess", "Access check failed",
128                                   EACCES);
129 }
130 
131 //!
132 //! \brief A controlled version of access(2).
133 //!
134 //! This function reimplements the standard access(2) system call to
135 //! safely control its exit status and raise an exception in case of
136 //! failure.
137 //!
138 static
139 bool
safe_access(const impl::path & p,int mode,int experr)140 safe_access(const impl::path& p, int mode, int experr)
141 {
142     try {
143         eaccess(p, mode);
144         return true;
145     } catch (const tools::system_error& e) {
146         if (e.code() == experr)
147             return false;
148         else
149             throw e;
150     }
151 }
152 
153 // The cleanup routines below are tricky: they are executed immediately after
154 // a test case's death, and after we have forcibly killed any stale processes.
155 // However, even if the processes are dead, this does not mean that the file
156 // system we are scanning is stable.  In particular, if the test case has
157 // mounted file systems through fuse/puffs, the fact that the processes died
158 // does not mean that the file system is truly unmounted.
159 //
160 // The code below attempts to cope with this by catching errors and either
161 // ignoring them or retrying the actions on the same file/directory a few times
162 // before giving up.
163 static const int max_retries = 5;
164 static const int retry_delay_in_seconds = 1;
165 
166 // The erase parameter in this routine is to control nested mount points.
167 // We want to descend into a mount point to unmount anything that is
168 // mounted under it, but we do not want to delete any files while doing
169 // this traversal.  In other words, we erase files until we cross the
170 // first mount point, and after that point we only scan and unmount.
171 static
172 void
cleanup_aux(const impl::path & p,dev_t parent_device,bool erase)173 cleanup_aux(const impl::path& p, dev_t parent_device, bool erase)
174 {
175     try {
176         impl::file_info fi(p);
177 
178         if (fi.get_type() == impl::file_info::dir_type)
179             cleanup_aux_dir(p, fi, fi.get_device() == parent_device);
180 
181         if (fi.get_device() != parent_device)
182             do_unmount(p);
183 
184         if (erase) {
185             if (fi.get_type() == impl::file_info::dir_type)
186                 impl::rmdir(p);
187             else
188                 impl::remove(p);
189         }
190     } catch (const tools::system_error& e) {
191         if (e.code() != ENOENT && e.code() != ENOTDIR)
192             throw e;
193     }
194 }
195 
196 static
197 void
cleanup_aux_dir(const impl::path & p,const impl::file_info & fi,bool erase)198 cleanup_aux_dir(const impl::path& p, const impl::file_info& fi,
199                 bool erase)
200 {
201     if (erase && ((fi.get_mode() & S_IRWXU) != S_IRWXU)) {
202         int retries = max_retries;
203 retry_chmod:
204         if (chmod(p.c_str(), fi.get_mode() | S_IRWXU) == -1) {
205             if (retries > 0) {
206                 retries--;
207                 ::sleep(retry_delay_in_seconds);
208                 goto retry_chmod;
209             } else {
210                 throw tools::system_error(IMPL_NAME "::cleanup(" +
211                                         p.str() + ")", "chmod(2) failed",
212                                         errno);
213             }
214         }
215     }
216 
217     std::set< std::string > subdirs;
218     {
219         bool ok = false;
220         int retries = max_retries;
221         while (!ok) {
222             assert(retries > 0);
223             try {
224                 const impl::directory d(p);
225                 subdirs = d.names();
226                 ok = true;
227             } catch (const tools::system_error& e) {
228                 retries--;
229                 if (retries == 0)
230                     throw e;
231                 ::sleep(retry_delay_in_seconds);
232             }
233         }
234         assert(ok);
235     }
236 
237     for (std::set< std::string >::const_iterator iter = subdirs.begin();
238          iter != subdirs.end(); iter++) {
239         const std::string& name = *iter;
240         if (name != "." && name != "..")
241             cleanup_aux(p / name, fi.get_device(), erase);
242     }
243 }
244 
245 static
246 void
do_unmount(const impl::path & in_path)247 do_unmount(const impl::path& in_path)
248 {
249     // At least, FreeBSD's unmount(2) requires the path to be absolute.
250     // Let's make it absolute in all cases just to be safe that this does
251     // not affect other systems.
252     const impl::path& abs_path = in_path.is_absolute() ?
253         in_path : in_path.to_absolute();
254 
255     int retries = max_retries;
256 retry_unmount:
257     if (unmount(abs_path.c_str(), 0) == -1) {
258         if (errno == EBUSY && retries > 0) {
259             retries--;
260             ::sleep(retry_delay_in_seconds);
261             goto retry_unmount;
262         } else {
263             throw tools::system_error(IMPL_NAME "::cleanup(" + in_path.str() +
264                                     ")", "unmount(2) failed", errno);
265         }
266     }
267 }
268 
269 static
270 std::string
normalize(const std::string & in)271 normalize(const std::string& in)
272 {
273     assert(!in.empty());
274 
275     std::string out;
276 
277     std::string::size_type pos = 0;
278     do {
279         const std::string::size_type next_pos = in.find('/', pos);
280 
281         const std::string component = in.substr(pos, next_pos - pos);
282         if (!component.empty()) {
283             if (pos == 0)
284                 out += component;
285             else if (component != ".")
286                 out += "/" + component;
287         }
288 
289         if (next_pos == std::string::npos)
290             pos = next_pos;
291         else
292             pos = next_pos + 1;
293     } while (pos != std::string::npos);
294 
295     return out.empty() ? "/" : out;
296 }
297 
298 // ------------------------------------------------------------------------
299 // The "path" class.
300 // ------------------------------------------------------------------------
301 
path(const std::string & s)302 impl::path::path(const std::string& s) :
303     m_data(normalize(s))
304 {
305 }
306 
~path(void)307 impl::path::~path(void)
308 {
309 }
310 
311 const char*
c_str(void) const312 impl::path::c_str(void)
313     const
314 {
315     return m_data.c_str();
316 }
317 
318 std::string
str(void) const319 impl::path::str(void)
320     const
321 {
322     return m_data;
323 }
324 
325 bool
is_absolute(void) const326 impl::path::is_absolute(void)
327     const
328 {
329     return !m_data.empty() && m_data[0] == '/';
330 }
331 
332 bool
is_root(void) const333 impl::path::is_root(void)
334     const
335 {
336     return m_data == "/";
337 }
338 
339 impl::path
branch_path(void) const340 impl::path::branch_path(void)
341     const
342 {
343     const std::string::size_type endpos = m_data.rfind('/');
344     if (endpos == std::string::npos)
345         return path(".");
346     else if (endpos == 0)
347         return path("/");
348     else
349         return path(m_data.substr(0, endpos));
350 }
351 
352 std::string
leaf_name(void) const353 impl::path::leaf_name(void)
354     const
355 {
356     std::string::size_type begpos = m_data.rfind('/');
357     if (begpos == std::string::npos)
358         begpos = 0;
359     else
360         begpos++;
361 
362     return m_data.substr(begpos);
363 }
364 
365 impl::path
to_absolute(void) const366 impl::path::to_absolute(void)
367     const
368 {
369     assert(!is_absolute());
370     return get_current_dir() / m_data;
371 }
372 
373 bool
operator ==(const path & p) const374 impl::path::operator==(const path& p)
375     const
376 {
377     return m_data == p.m_data;
378 }
379 
380 bool
operator !=(const path & p) const381 impl::path::operator!=(const path& p)
382     const
383 {
384     return m_data != p.m_data;
385 }
386 
387 impl::path
operator /(const std::string & p) const388 impl::path::operator/(const std::string& p)
389     const
390 {
391     return path(m_data + "/" + normalize(p));
392 }
393 
394 impl::path
operator /(const path & p) const395 impl::path::operator/(const path& p)
396     const
397 {
398     return path(m_data) / p.m_data;
399 }
400 
401 bool
operator <(const path & p) const402 impl::path::operator<(const path& p)
403     const
404 {
405     return std::strcmp(m_data.c_str(), p.m_data.c_str()) < 0;
406 }
407 
408 // ------------------------------------------------------------------------
409 // The "file_info" class.
410 // ------------------------------------------------------------------------
411 
412 const int impl::file_info::blk_type = 1;
413 const int impl::file_info::chr_type = 2;
414 const int impl::file_info::dir_type = 3;
415 const int impl::file_info::fifo_type = 4;
416 const int impl::file_info::lnk_type = 5;
417 const int impl::file_info::reg_type = 6;
418 const int impl::file_info::sock_type = 7;
419 const int impl::file_info::wht_type = 8;
420 
file_info(const path & p)421 impl::file_info::file_info(const path& p)
422 {
423     if (lstat(p.c_str(), &m_sb) == -1)
424         throw system_error(IMPL_NAME "::file_info",
425                            "Cannot get information of " + p.str() + "; " +
426                            "lstat(2) failed", errno);
427 
428     int type = m_sb.st_mode & S_IFMT;
429     switch (type) {
430     case S_IFBLK:  m_type = blk_type;  break;
431     case S_IFCHR:  m_type = chr_type;  break;
432     case S_IFDIR:  m_type = dir_type;  break;
433     case S_IFIFO:  m_type = fifo_type; break;
434     case S_IFLNK:  m_type = lnk_type;  break;
435     case S_IFREG:  m_type = reg_type;  break;
436     case S_IFSOCK: m_type = sock_type; break;
437     case S_IFWHT:  m_type = wht_type;  break;
438     default:
439         throw system_error(IMPL_NAME "::file_info", "Unknown file type "
440                            "error", EINVAL);
441     }
442 }
443 
~file_info(void)444 impl::file_info::~file_info(void)
445 {
446 }
447 
448 dev_t
get_device(void) const449 impl::file_info::get_device(void)
450     const
451 {
452     return m_sb.st_dev;
453 }
454 
455 ino_t
get_inode(void) const456 impl::file_info::get_inode(void)
457     const
458 {
459     return m_sb.st_ino;
460 }
461 
462 mode_t
get_mode(void) const463 impl::file_info::get_mode(void)
464     const
465 {
466     return m_sb.st_mode & ~S_IFMT;
467 }
468 
469 off_t
get_size(void) const470 impl::file_info::get_size(void)
471     const
472 {
473     return m_sb.st_size;
474 }
475 
476 int
get_type(void) const477 impl::file_info::get_type(void)
478     const
479 {
480     return m_type;
481 }
482 
483 bool
is_owner_readable(void) const484 impl::file_info::is_owner_readable(void)
485     const
486 {
487     return m_sb.st_mode & S_IRUSR;
488 }
489 
490 bool
is_owner_writable(void) const491 impl::file_info::is_owner_writable(void)
492     const
493 {
494     return m_sb.st_mode & S_IWUSR;
495 }
496 
497 bool
is_owner_executable(void) const498 impl::file_info::is_owner_executable(void)
499     const
500 {
501     return m_sb.st_mode & S_IXUSR;
502 }
503 
504 bool
is_group_readable(void) const505 impl::file_info::is_group_readable(void)
506     const
507 {
508     return m_sb.st_mode & S_IRGRP;
509 }
510 
511 bool
is_group_writable(void) const512 impl::file_info::is_group_writable(void)
513     const
514 {
515     return m_sb.st_mode & S_IWGRP;
516 }
517 
518 bool
is_group_executable(void) const519 impl::file_info::is_group_executable(void)
520     const
521 {
522     return m_sb.st_mode & S_IXGRP;
523 }
524 
525 bool
is_other_readable(void) const526 impl::file_info::is_other_readable(void)
527     const
528 {
529     return m_sb.st_mode & S_IROTH;
530 }
531 
532 bool
is_other_writable(void) const533 impl::file_info::is_other_writable(void)
534     const
535 {
536     return m_sb.st_mode & S_IWOTH;
537 }
538 
539 bool
is_other_executable(void) const540 impl::file_info::is_other_executable(void)
541     const
542 {
543     return m_sb.st_mode & S_IXOTH;
544 }
545 
546 // ------------------------------------------------------------------------
547 // The "directory" class.
548 // ------------------------------------------------------------------------
549 
directory(const path & p)550 impl::directory::directory(const path& p)
551 {
552     DIR* dp = ::opendir(p.c_str());
553     if (dp == NULL)
554         throw system_error(IMPL_NAME "::directory::directory(" +
555                            p.str() + ")", "opendir(3) failed", errno);
556 
557     struct dirent* dep;
558     while ((dep = ::readdir(dp)) != NULL) {
559         path entryp = p / dep->d_name;
560         insert(value_type(dep->d_name, file_info(entryp)));
561     }
562 
563     if (::closedir(dp) == -1)
564         throw system_error(IMPL_NAME "::directory::directory(" +
565                            p.str() + ")", "closedir(3) failed", errno);
566 }
567 
568 std::set< std::string >
names(void) const569 impl::directory::names(void)
570     const
571 {
572     std::set< std::string > ns;
573 
574     for (const_iterator iter = begin(); iter != end(); iter++)
575         ns.insert((*iter).first);
576 
577     return ns;
578 }
579 
580 // ------------------------------------------------------------------------
581 // The "temp_dir" class.
582 // ------------------------------------------------------------------------
583 
temp_dir(const path & p)584 impl::temp_dir::temp_dir(const path& p)
585 {
586     tools::auto_array< char > buf(new char[p.str().length() + 1]);
587     std::strcpy(buf.get(), p.c_str());
588     if (::mkdtemp(buf.get()) == NULL)
589         throw tools::system_error(IMPL_NAME "::temp_dir::temp_dir(" +
590                                 p.str() + ")", "mkdtemp(3) failed",
591                                 errno);
592 
593     m_path.reset(new path(buf.get()));
594 }
595 
~temp_dir(void)596 impl::temp_dir::~temp_dir(void)
597 {
598     cleanup(*m_path);
599 }
600 
601 const impl::path&
get_path(void) const602 impl::temp_dir::get_path(void)
603     const
604 {
605     return *m_path;
606 }
607 
608 // ------------------------------------------------------------------------
609 // Free functions.
610 // ------------------------------------------------------------------------
611 
612 bool
exists(const path & p)613 impl::exists(const path& p)
614 {
615     try {
616         eaccess(p, access_f);
617         return true;
618     } catch (const system_error& e) {
619         if (e.code() == ENOENT)
620             return false;
621         else
622             throw;
623     }
624 }
625 
626 bool
have_prog_in_path(const std::string & prog)627 impl::have_prog_in_path(const std::string& prog)
628 {
629     assert(prog.find('/') == std::string::npos);
630 
631     // Do not bother to provide a default value for PATH.  If it is not
632     // there something is broken in the user's environment.
633     if (!tools::env::has("PATH"))
634         throw std::runtime_error("PATH not defined in the environment");
635     std::vector< std::string > dirs =
636         tools::text::split(tools::env::get("PATH"), ":");
637 
638     bool found = false;
639     for (std::vector< std::string >::const_iterator iter = dirs.begin();
640          !found && iter != dirs.end(); iter++) {
641         const path& dir = path(*iter);
642 
643         if (is_executable(dir / prog))
644             found = true;
645     }
646     return found;
647 }
648 
649 bool
is_executable(const path & p)650 impl::is_executable(const path& p)
651 {
652     if (!exists(p))
653         return false;
654     return safe_access(p, access_x, EACCES);
655 }
656 
657 void
remove(const path & p)658 impl::remove(const path& p)
659 {
660     if (file_info(p).get_type() == file_info::dir_type)
661         throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")",
662                                   "Is a directory",
663                                   EPERM);
664     if (::unlink(p.c_str()) == -1)
665         throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")",
666                                   "unlink(" + p.str() + ") failed",
667                                   errno);
668 }
669 
670 void
rmdir(const path & p)671 impl::rmdir(const path& p)
672 {
673     if (::rmdir(p.c_str())) {
674         if (errno == EEXIST) {
675             /* Some operating systems (e.g. OpenSolaris 200906) return
676              * EEXIST instead of ENOTEMPTY for non-empty directories.
677              * Homogenize the return value so that callers don't need
678              * to bother about differences in operating systems. */
679             errno = ENOTEMPTY;
680         }
681         throw system_error(IMPL_NAME "::rmdir", "Cannot remove directory",
682                            errno);
683     }
684 }
685 
686 void
change_ownership(const path & p,const std::pair<int,int> & user)687 impl::change_ownership(const path& p, const std::pair < int, int >& user)
688 {
689     if (::chown(p.c_str(), user.first, user.second) == -1) {
690         std::stringstream ss;
691         ss << IMPL_NAME "::chown(" << p.str() << ", " << user.first << ", "
692            << user.second << ")";
693         throw tools::system_error(ss.str(), "chown(2) failed", errno);
694     }
695 }
696 
697 impl::path
change_directory(const path & dir)698 impl::change_directory(const path& dir)
699 {
700     path olddir = get_current_dir();
701 
702     if (olddir != dir) {
703         if (::chdir(dir.c_str()) == -1)
704             throw tools::system_error(IMPL_NAME "::chdir(" + dir.str() + ")",
705                                     "chdir(2) failed", errno);
706     }
707 
708     return olddir;
709 }
710 
711 void
cleanup(const path & p)712 impl::cleanup(const path& p)
713 {
714     impl::file_info fi(p);
715     cleanup_aux(p, fi.get_device(), true);
716 }
717 
718 impl::path
get_current_dir(void)719 impl::get_current_dir(void)
720 {
721     char *cwd = getcwd(NULL, 0);
722     if (cwd == NULL)
723         throw tools::system_error(IMPL_NAME "::get_current_dir()",
724                                 "getcwd() failed", errno);
725 
726     try {
727         impl::path p(cwd);
728         free(cwd);
729         return p;
730     } catch(...) {
731         free(cwd);
732         throw;
733     }
734 }
735