xref: /minix/external/bsd/atf/dist/tools/fs.cpp (revision 0a6a1f1d)
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 defined(__minix)
258     if (minix_umount(abs_path.c_str(), 0) == -1) {
259 #else
260     if (unmount(abs_path.c_str(), 0) == -1) {
261 #endif /* defined(__minix) */
262         if (errno == EBUSY && retries > 0) {
263             retries--;
264             ::sleep(retry_delay_in_seconds);
265             goto retry_unmount;
266         } else {
267             throw tools::system_error(IMPL_NAME "::cleanup(" + in_path.str() +
268                                     ")", "unmount(2) failed", errno);
269         }
270     }
271 }
272 
273 static
274 std::string
275 normalize(const std::string& in)
276 {
277     assert(!in.empty());
278 
279     std::string out;
280 
281     std::string::size_type pos = 0;
282     do {
283         const std::string::size_type next_pos = in.find('/', pos);
284 
285         const std::string component = in.substr(pos, next_pos - pos);
286         if (!component.empty()) {
287             if (pos == 0)
288                 out += component;
289             else if (component != ".")
290                 out += "/" + component;
291         }
292 
293         if (next_pos == std::string::npos)
294             pos = next_pos;
295         else
296             pos = next_pos + 1;
297     } while (pos != std::string::npos);
298 
299     return out.empty() ? "/" : out;
300 }
301 
302 // ------------------------------------------------------------------------
303 // The "path" class.
304 // ------------------------------------------------------------------------
305 
306 impl::path::path(const std::string& s) :
307     m_data(normalize(s))
308 {
309 }
310 
311 impl::path::~path(void)
312 {
313 }
314 
315 const char*
316 impl::path::c_str(void)
317     const
318 {
319     return m_data.c_str();
320 }
321 
322 std::string
323 impl::path::str(void)
324     const
325 {
326     return m_data;
327 }
328 
329 bool
330 impl::path::is_absolute(void)
331     const
332 {
333     return !m_data.empty() && m_data[0] == '/';
334 }
335 
336 bool
337 impl::path::is_root(void)
338     const
339 {
340     return m_data == "/";
341 }
342 
343 impl::path
344 impl::path::branch_path(void)
345     const
346 {
347     const std::string::size_type endpos = m_data.rfind('/');
348     if (endpos == std::string::npos)
349         return path(".");
350     else if (endpos == 0)
351         return path("/");
352     else
353         return path(m_data.substr(0, endpos));
354 }
355 
356 std::string
357 impl::path::leaf_name(void)
358     const
359 {
360     std::string::size_type begpos = m_data.rfind('/');
361     if (begpos == std::string::npos)
362         begpos = 0;
363     else
364         begpos++;
365 
366     return m_data.substr(begpos);
367 }
368 
369 impl::path
370 impl::path::to_absolute(void)
371     const
372 {
373     assert(!is_absolute());
374     return get_current_dir() / m_data;
375 }
376 
377 bool
378 impl::path::operator==(const path& p)
379     const
380 {
381     return m_data == p.m_data;
382 }
383 
384 bool
385 impl::path::operator!=(const path& p)
386     const
387 {
388     return m_data != p.m_data;
389 }
390 
391 impl::path
392 impl::path::operator/(const std::string& p)
393     const
394 {
395     return path(m_data + "/" + normalize(p));
396 }
397 
398 impl::path
399 impl::path::operator/(const path& p)
400     const
401 {
402     return path(m_data) / p.m_data;
403 }
404 
405 bool
406 impl::path::operator<(const path& p)
407     const
408 {
409     return std::strcmp(m_data.c_str(), p.m_data.c_str()) < 0;
410 }
411 
412 // ------------------------------------------------------------------------
413 // The "file_info" class.
414 // ------------------------------------------------------------------------
415 
416 const int impl::file_info::blk_type = 1;
417 const int impl::file_info::chr_type = 2;
418 const int impl::file_info::dir_type = 3;
419 const int impl::file_info::fifo_type = 4;
420 const int impl::file_info::lnk_type = 5;
421 const int impl::file_info::reg_type = 6;
422 const int impl::file_info::sock_type = 7;
423 const int impl::file_info::wht_type = 8;
424 
425 impl::file_info::file_info(const path& p)
426 {
427     if (lstat(p.c_str(), &m_sb) == -1)
428         throw system_error(IMPL_NAME "::file_info",
429                            "Cannot get information of " + p.str() + "; " +
430                            "lstat(2) failed", errno);
431 
432     int type = m_sb.st_mode & S_IFMT;
433     switch (type) {
434     case S_IFBLK:  m_type = blk_type;  break;
435     case S_IFCHR:  m_type = chr_type;  break;
436     case S_IFDIR:  m_type = dir_type;  break;
437     case S_IFIFO:  m_type = fifo_type; break;
438     case S_IFLNK:  m_type = lnk_type;  break;
439     case S_IFREG:  m_type = reg_type;  break;
440     case S_IFSOCK: m_type = sock_type; break;
441     case S_IFWHT:  m_type = wht_type;  break;
442     default:
443         throw system_error(IMPL_NAME "::file_info", "Unknown file type "
444                            "error", EINVAL);
445     }
446 }
447 
448 impl::file_info::~file_info(void)
449 {
450 }
451 
452 dev_t
453 impl::file_info::get_device(void)
454     const
455 {
456     return m_sb.st_dev;
457 }
458 
459 ino_t
460 impl::file_info::get_inode(void)
461     const
462 {
463     return m_sb.st_ino;
464 }
465 
466 mode_t
467 impl::file_info::get_mode(void)
468     const
469 {
470     return m_sb.st_mode & ~S_IFMT;
471 }
472 
473 off_t
474 impl::file_info::get_size(void)
475     const
476 {
477     return m_sb.st_size;
478 }
479 
480 int
481 impl::file_info::get_type(void)
482     const
483 {
484     return m_type;
485 }
486 
487 bool
488 impl::file_info::is_owner_readable(void)
489     const
490 {
491     return m_sb.st_mode & S_IRUSR;
492 }
493 
494 bool
495 impl::file_info::is_owner_writable(void)
496     const
497 {
498     return m_sb.st_mode & S_IWUSR;
499 }
500 
501 bool
502 impl::file_info::is_owner_executable(void)
503     const
504 {
505     return m_sb.st_mode & S_IXUSR;
506 }
507 
508 bool
509 impl::file_info::is_group_readable(void)
510     const
511 {
512     return m_sb.st_mode & S_IRGRP;
513 }
514 
515 bool
516 impl::file_info::is_group_writable(void)
517     const
518 {
519     return m_sb.st_mode & S_IWGRP;
520 }
521 
522 bool
523 impl::file_info::is_group_executable(void)
524     const
525 {
526     return m_sb.st_mode & S_IXGRP;
527 }
528 
529 bool
530 impl::file_info::is_other_readable(void)
531     const
532 {
533     return m_sb.st_mode & S_IROTH;
534 }
535 
536 bool
537 impl::file_info::is_other_writable(void)
538     const
539 {
540     return m_sb.st_mode & S_IWOTH;
541 }
542 
543 bool
544 impl::file_info::is_other_executable(void)
545     const
546 {
547     return m_sb.st_mode & S_IXOTH;
548 }
549 
550 // ------------------------------------------------------------------------
551 // The "directory" class.
552 // ------------------------------------------------------------------------
553 
554 impl::directory::directory(const path& p)
555 {
556     DIR* dp = ::opendir(p.c_str());
557     if (dp == NULL)
558         throw system_error(IMPL_NAME "::directory::directory(" +
559                            p.str() + ")", "opendir(3) failed", errno);
560 
561     struct dirent* dep;
562     while ((dep = ::readdir(dp)) != NULL) {
563         path entryp = p / dep->d_name;
564         insert(value_type(dep->d_name, file_info(entryp)));
565     }
566 
567     if (::closedir(dp) == -1)
568         throw system_error(IMPL_NAME "::directory::directory(" +
569                            p.str() + ")", "closedir(3) failed", errno);
570 }
571 
572 std::set< std::string >
573 impl::directory::names(void)
574     const
575 {
576     std::set< std::string > ns;
577 
578     for (const_iterator iter = begin(); iter != end(); iter++)
579         ns.insert((*iter).first);
580 
581     return ns;
582 }
583 
584 // ------------------------------------------------------------------------
585 // The "temp_dir" class.
586 // ------------------------------------------------------------------------
587 
588 impl::temp_dir::temp_dir(const path& p)
589 {
590     tools::auto_array< char > buf(new char[p.str().length() + 1]);
591     std::strcpy(buf.get(), p.c_str());
592     if (::mkdtemp(buf.get()) == NULL)
593         throw tools::system_error(IMPL_NAME "::temp_dir::temp_dir(" +
594                                 p.str() + ")", "mkdtemp(3) failed",
595                                 errno);
596 
597     m_path.reset(new path(buf.get()));
598 }
599 
600 impl::temp_dir::~temp_dir(void)
601 {
602     cleanup(*m_path);
603 }
604 
605 const impl::path&
606 impl::temp_dir::get_path(void)
607     const
608 {
609     return *m_path;
610 }
611 
612 // ------------------------------------------------------------------------
613 // Free functions.
614 // ------------------------------------------------------------------------
615 
616 bool
617 impl::exists(const path& p)
618 {
619     try {
620         eaccess(p, access_f);
621         return true;
622     } catch (const system_error& e) {
623         if (e.code() == ENOENT)
624             return false;
625         else
626             throw;
627     }
628 }
629 
630 bool
631 impl::have_prog_in_path(const std::string& prog)
632 {
633     assert(prog.find('/') == std::string::npos);
634 
635     // Do not bother to provide a default value for PATH.  If it is not
636     // there something is broken in the user's environment.
637     if (!tools::env::has("PATH"))
638         throw std::runtime_error("PATH not defined in the environment");
639     std::vector< std::string > dirs =
640         tools::text::split(tools::env::get("PATH"), ":");
641 
642     bool found = false;
643     for (std::vector< std::string >::const_iterator iter = dirs.begin();
644          !found && iter != dirs.end(); iter++) {
645         const path& dir = path(*iter);
646 
647         if (is_executable(dir / prog))
648             found = true;
649     }
650     return found;
651 }
652 
653 bool
654 impl::is_executable(const path& p)
655 {
656     if (!exists(p))
657         return false;
658     return safe_access(p, access_x, EACCES);
659 }
660 
661 void
662 impl::remove(const path& p)
663 {
664     if (file_info(p).get_type() == file_info::dir_type)
665         throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")",
666                                   "Is a directory",
667                                   EPERM);
668     if (::unlink(p.c_str()) == -1)
669         throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")",
670                                   "unlink(" + p.str() + ") failed",
671                                   errno);
672 }
673 
674 void
675 impl::rmdir(const path& p)
676 {
677     if (::rmdir(p.c_str())) {
678         if (errno == EEXIST) {
679             /* Some operating systems (e.g. OpenSolaris 200906) return
680              * EEXIST instead of ENOTEMPTY for non-empty directories.
681              * Homogenize the return value so that callers don't need
682              * to bother about differences in operating systems. */
683             errno = ENOTEMPTY;
684         }
685         throw system_error(IMPL_NAME "::rmdir", "Cannot remove directory",
686                            errno);
687     }
688 }
689 
690 impl::path
691 impl::change_directory(const path& dir)
692 {
693     path olddir = get_current_dir();
694 
695     if (olddir != dir) {
696         if (::chdir(dir.c_str()) == -1)
697             throw tools::system_error(IMPL_NAME "::chdir(" + dir.str() + ")",
698                                     "chdir(2) failed", errno);
699     }
700 
701     return olddir;
702 }
703 
704 void
705 impl::cleanup(const path& p)
706 {
707     impl::file_info fi(p);
708     cleanup_aux(p, fi.get_device(), true);
709 }
710 
711 impl::path
712 impl::get_current_dir(void)
713 {
714     std::auto_ptr< char > cwd;
715     cwd.reset(getcwd(NULL, 0));
716     if (cwd.get() == NULL)
717         throw tools::system_error(IMPL_NAME "::get_current_dir()",
718                                 "getcwd() failed", errno);
719 
720     return path(cwd.get());
721 }
722