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
30extern "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
54namespace impl = tools::fs;
55#define IMPL_NAME "tools::fs"
56
57// ------------------------------------------------------------------------
58// Auxiliary functions.
59// ------------------------------------------------------------------------
60
61static void cleanup_aux(const impl::path&, dev_t, bool);
62static void cleanup_aux_dir(const impl::path&, const impl::file_info&,
63                            bool);
64static void do_unmount(const impl::path&);
65static bool safe_access(const impl::path&, int, int);
66
67static const int access_f = 1 << 0;
68static const int access_r = 1 << 1;
69static const int access_w = 1 << 2;
70static 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//!
77static
78void
79eaccess(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//!
138static
139bool
140safe_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.
163static const int max_retries = 5;
164static 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.
171static
172void
173cleanup_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
196static
197void
198cleanup_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;
203retry_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
245static
246void
247do_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;
256retry_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
269static
270std::string
271normalize(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
302impl::path::path(const std::string& s) :
303    m_data(normalize(s))
304{
305}
306
307impl::path::~path(void)
308{
309}
310
311const char*
312impl::path::c_str(void)
313    const
314{
315    return m_data.c_str();
316}
317
318std::string
319impl::path::str(void)
320    const
321{
322    return m_data;
323}
324
325bool
326impl::path::is_absolute(void)
327    const
328{
329    return !m_data.empty() && m_data[0] == '/';
330}
331
332bool
333impl::path::is_root(void)
334    const
335{
336    return m_data == "/";
337}
338
339impl::path
340impl::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
352std::string
353impl::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
365impl::path
366impl::path::to_absolute(void)
367    const
368{
369    assert(!is_absolute());
370    return get_current_dir() / m_data;
371}
372
373bool
374impl::path::operator==(const path& p)
375    const
376{
377    return m_data == p.m_data;
378}
379
380bool
381impl::path::operator!=(const path& p)
382    const
383{
384    return m_data != p.m_data;
385}
386
387impl::path
388impl::path::operator/(const std::string& p)
389    const
390{
391    return path(m_data + "/" + normalize(p));
392}
393
394impl::path
395impl::path::operator/(const path& p)
396    const
397{
398    return path(m_data) / p.m_data;
399}
400
401bool
402impl::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
412const int impl::file_info::blk_type = 1;
413const int impl::file_info::chr_type = 2;
414const int impl::file_info::dir_type = 3;
415const int impl::file_info::fifo_type = 4;
416const int impl::file_info::lnk_type = 5;
417const int impl::file_info::reg_type = 6;
418const int impl::file_info::sock_type = 7;
419const int impl::file_info::wht_type = 8;
420
421impl::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
444impl::file_info::~file_info(void)
445{
446}
447
448dev_t
449impl::file_info::get_device(void)
450    const
451{
452    return m_sb.st_dev;
453}
454
455ino_t
456impl::file_info::get_inode(void)
457    const
458{
459    return m_sb.st_ino;
460}
461
462mode_t
463impl::file_info::get_mode(void)
464    const
465{
466    return m_sb.st_mode & ~S_IFMT;
467}
468
469off_t
470impl::file_info::get_size(void)
471    const
472{
473    return m_sb.st_size;
474}
475
476int
477impl::file_info::get_type(void)
478    const
479{
480    return m_type;
481}
482
483bool
484impl::file_info::is_owner_readable(void)
485    const
486{
487    return m_sb.st_mode & S_IRUSR;
488}
489
490bool
491impl::file_info::is_owner_writable(void)
492    const
493{
494    return m_sb.st_mode & S_IWUSR;
495}
496
497bool
498impl::file_info::is_owner_executable(void)
499    const
500{
501    return m_sb.st_mode & S_IXUSR;
502}
503
504bool
505impl::file_info::is_group_readable(void)
506    const
507{
508    return m_sb.st_mode & S_IRGRP;
509}
510
511bool
512impl::file_info::is_group_writable(void)
513    const
514{
515    return m_sb.st_mode & S_IWGRP;
516}
517
518bool
519impl::file_info::is_group_executable(void)
520    const
521{
522    return m_sb.st_mode & S_IXGRP;
523}
524
525bool
526impl::file_info::is_other_readable(void)
527    const
528{
529    return m_sb.st_mode & S_IROTH;
530}
531
532bool
533impl::file_info::is_other_writable(void)
534    const
535{
536    return m_sb.st_mode & S_IWOTH;
537}
538
539bool
540impl::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
550impl::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
568std::set< std::string >
569impl::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
584impl::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
596impl::temp_dir::~temp_dir(void)
597{
598    cleanup(*m_path);
599}
600
601const impl::path&
602impl::temp_dir::get_path(void)
603    const
604{
605    return *m_path;
606}
607
608// ------------------------------------------------------------------------
609// Free functions.
610// ------------------------------------------------------------------------
611
612bool
613impl::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
626bool
627impl::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
649bool
650impl::is_executable(const path& p)
651{
652    if (!exists(p))
653        return false;
654    return safe_access(p, access_x, EACCES);
655}
656
657void
658impl::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
670void
671impl::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
686void
687impl::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
697impl::path
698impl::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
711void
712impl::cleanup(const path& p)
713{
714    impl::file_info fi(p);
715    cleanup_aux(p, fi.get_device(), true);
716}
717
718impl::path
719impl::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