// // Automated Testing Framework (atf) // // Copyright (c) 2007 The NetBSD Foundation, Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND // CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. // IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN // IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // #if defined(HAVE_CONFIG_H) #include "config.h" #endif extern "C" { #include #include #include #include #include #include #include #include } #include #include #include #include #include "auto_array.hpp" #include "env.hpp" #include "exceptions.hpp" #include "fs.hpp" #include "process.hpp" #include "text.hpp" #include "user.hpp" namespace impl = tools::fs; #define IMPL_NAME "tools::fs" // ------------------------------------------------------------------------ // Auxiliary functions. // ------------------------------------------------------------------------ static void cleanup_aux(const impl::path&, dev_t, bool); static void cleanup_aux_dir(const impl::path&, const impl::file_info&, bool); static void do_unmount(const impl::path&); static bool safe_access(const impl::path&, int, int); static const int access_f = 1 << 0; static const int access_r = 1 << 1; static const int access_w = 1 << 2; static const int access_x = 1 << 3; //! //! An implementation of access(2) but using the effective user value //! instead of the real one. Also avoids false positives for root when //! asking for execute permissions, which appear in SunOS. //! static void eaccess(const tools::fs::path& p, int mode) { assert(mode & access_f || mode & access_r || mode & access_w || mode & access_x); struct stat st; if (lstat(p.c_str(), &st) == -1) throw tools::system_error(IMPL_NAME "::eaccess", "Cannot get information from file " + p.str(), errno); /* Early return if we are only checking for existence and the file * exists (stat call returned). */ if (mode & access_f) return; bool ok = false; if (tools::user::is_root()) { if (!ok && !(mode & access_x)) { /* Allow root to read/write any file. */ ok = true; } if (!ok && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { /* Allow root to execute the file if any of its execution bits * are set. */ ok = true; } } else { if (!ok && (tools::user::euid() == st.st_uid)) { ok = ((mode & access_r) && (st.st_mode & S_IRUSR)) || ((mode & access_w) && (st.st_mode & S_IWUSR)) || ((mode & access_x) && (st.st_mode & S_IXUSR)); } if (!ok && tools::user::is_member_of_group(st.st_gid)) { ok = ((mode & access_r) && (st.st_mode & S_IRGRP)) || ((mode & access_w) && (st.st_mode & S_IWGRP)) || ((mode & access_x) && (st.st_mode & S_IXGRP)); } if (!ok && ((tools::user::euid() != st.st_uid) && !tools::user::is_member_of_group(st.st_gid))) { ok = ((mode & access_r) && (st.st_mode & S_IROTH)) || ((mode & access_w) && (st.st_mode & S_IWOTH)) || ((mode & access_x) && (st.st_mode & S_IXOTH)); } } if (!ok) throw tools::system_error(IMPL_NAME "::eaccess", "Access check failed", EACCES); } //! //! \brief A controlled version of access(2). //! //! This function reimplements the standard access(2) system call to //! safely control its exit status and raise an exception in case of //! failure. //! static bool safe_access(const impl::path& p, int mode, int experr) { try { eaccess(p, mode); return true; } catch (const tools::system_error& e) { if (e.code() == experr) return false; else throw e; } } // The cleanup routines below are tricky: they are executed immediately after // a test case's death, and after we have forcibly killed any stale processes. // However, even if the processes are dead, this does not mean that the file // system we are scanning is stable. In particular, if the test case has // mounted file systems through fuse/puffs, the fact that the processes died // does not mean that the file system is truly unmounted. // // The code below attempts to cope with this by catching errors and either // ignoring them or retrying the actions on the same file/directory a few times // before giving up. static const int max_retries = 5; static const int retry_delay_in_seconds = 1; // The erase parameter in this routine is to control nested mount points. // We want to descend into a mount point to unmount anything that is // mounted under it, but we do not want to delete any files while doing // this traversal. In other words, we erase files until we cross the // first mount point, and after that point we only scan and unmount. static void cleanup_aux(const impl::path& p, dev_t parent_device, bool erase) { try { impl::file_info fi(p); if (fi.get_type() == impl::file_info::dir_type) cleanup_aux_dir(p, fi, fi.get_device() == parent_device); if (fi.get_device() != parent_device) do_unmount(p); if (erase) { if (fi.get_type() == impl::file_info::dir_type) impl::rmdir(p); else impl::remove(p); } } catch (const tools::system_error& e) { if (e.code() != ENOENT && e.code() != ENOTDIR) throw e; } } static void cleanup_aux_dir(const impl::path& p, const impl::file_info& fi, bool erase) { if (erase && ((fi.get_mode() & S_IRWXU) != S_IRWXU)) { int retries = max_retries; retry_chmod: if (chmod(p.c_str(), fi.get_mode() | S_IRWXU) == -1) { if (retries > 0) { retries--; ::sleep(retry_delay_in_seconds); goto retry_chmod; } else { throw tools::system_error(IMPL_NAME "::cleanup(" + p.str() + ")", "chmod(2) failed", errno); } } } std::set< std::string > subdirs; { bool ok = false; int retries = max_retries; while (!ok) { assert(retries > 0); try { const impl::directory d(p); subdirs = d.names(); ok = true; } catch (const tools::system_error& e) { retries--; if (retries == 0) throw e; ::sleep(retry_delay_in_seconds); } } assert(ok); } for (std::set< std::string >::const_iterator iter = subdirs.begin(); iter != subdirs.end(); iter++) { const std::string& name = *iter; if (name != "." && name != "..") cleanup_aux(p / name, fi.get_device(), erase); } } static void do_unmount(const impl::path& in_path) { // At least, FreeBSD's unmount(2) requires the path to be absolute. // Let's make it absolute in all cases just to be safe that this does // not affect other systems. const impl::path& abs_path = in_path.is_absolute() ? in_path : in_path.to_absolute(); #if defined(HAVE_UNMOUNT) int retries = max_retries; retry_unmount: if (unmount(abs_path.c_str(), 0) == -1) { if (errno == EBUSY && retries > 0) { retries--; ::sleep(retry_delay_in_seconds); goto retry_unmount; } else { throw tools::system_error(IMPL_NAME "::cleanup(" + in_path.str() + ")", "unmount(2) failed", errno); } } #else // We could use umount(2) instead if it was available... but // trying to do so under, e.g. Linux, is a nightmare because we // also have to update /etc/mtab to match what we did. It is // stools::fser to just leave the system-specific umount(8) tool deal // with it, at least for now. const impl::path prog("umount"); tools::process::argv_array argv("umount", abs_path.c_str(), NULL); tools::process::status s = tools::process::exec(prog, argv, tools::process::stream_inherit(), tools::process::stream_inherit()); if (!s.exited() || s.exitstatus() != EXIT_SUCCESS) throw std::runtime_error("Call to unmount failed"); #endif } static std::string normalize(const std::string& in) { assert(!in.empty()); std::string out; std::string::size_type pos = 0; do { const std::string::size_type next_pos = in.find('/', pos); const std::string component = in.substr(pos, next_pos - pos); if (!component.empty()) { if (pos == 0) out += component; else if (component != ".") out += "/" + component; } if (next_pos == std::string::npos) pos = next_pos; else pos = next_pos + 1; } while (pos != std::string::npos); return out.empty() ? "/" : out; } // ------------------------------------------------------------------------ // The "path" class. // ------------------------------------------------------------------------ impl::path::path(const std::string& s) : m_data(normalize(s)) { } impl::path::~path(void) { } const char* impl::path::c_str(void) const { return m_data.c_str(); } std::string impl::path::str(void) const { return m_data; } bool impl::path::is_absolute(void) const { return !m_data.empty() && m_data[0] == '/'; } bool impl::path::is_root(void) const { return m_data == "/"; } impl::path impl::path::branch_path(void) const { const std::string::size_type endpos = m_data.rfind('/'); if (endpos == std::string::npos) return path("."); else if (endpos == 0) return path("/"); else return path(m_data.substr(0, endpos)); } std::string impl::path::leaf_name(void) const { std::string::size_type begpos = m_data.rfind('/'); if (begpos == std::string::npos) begpos = 0; else begpos++; return m_data.substr(begpos); } impl::path impl::path::to_absolute(void) const { assert(!is_absolute()); return get_current_dir() / m_data; } bool impl::path::operator==(const path& p) const { return m_data == p.m_data; } bool impl::path::operator!=(const path& p) const { return m_data != p.m_data; } impl::path impl::path::operator/(const std::string& p) const { return path(m_data + "/" + normalize(p)); } impl::path impl::path::operator/(const path& p) const { return path(m_data) / p.m_data; } bool impl::path::operator<(const path& p) const { return std::strcmp(m_data.c_str(), p.m_data.c_str()) < 0; } // ------------------------------------------------------------------------ // The "file_info" class. // ------------------------------------------------------------------------ const int impl::file_info::blk_type = 1; const int impl::file_info::chr_type = 2; const int impl::file_info::dir_type = 3; const int impl::file_info::fifo_type = 4; const int impl::file_info::lnk_type = 5; const int impl::file_info::reg_type = 6; const int impl::file_info::sock_type = 7; const int impl::file_info::wht_type = 8; impl::file_info::file_info(const path& p) { if (lstat(p.c_str(), &m_sb) == -1) throw system_error(IMPL_NAME "::file_info", "Cannot get information of " + p.str() + "; " + "lstat(2) failed", errno); int type = m_sb.st_mode & S_IFMT; switch (type) { case S_IFBLK: m_type = blk_type; break; case S_IFCHR: m_type = chr_type; break; case S_IFDIR: m_type = dir_type; break; case S_IFIFO: m_type = fifo_type; break; case S_IFLNK: m_type = lnk_type; break; case S_IFREG: m_type = reg_type; break; case S_IFSOCK: m_type = sock_type; break; #if defined(S_IFWHT) case S_IFWHT: m_type = wht_type; break; #endif default: throw system_error(IMPL_NAME "::file_info", "Unknown file type " "error", EINVAL); } } impl::file_info::~file_info(void) { } dev_t impl::file_info::get_device(void) const { return m_sb.st_dev; } ino_t impl::file_info::get_inode(void) const { return m_sb.st_ino; } mode_t impl::file_info::get_mode(void) const { return m_sb.st_mode & ~S_IFMT; } off_t impl::file_info::get_size(void) const { return m_sb.st_size; } int impl::file_info::get_type(void) const { return m_type; } bool impl::file_info::is_owner_readable(void) const { return m_sb.st_mode & S_IRUSR; } bool impl::file_info::is_owner_writable(void) const { return m_sb.st_mode & S_IWUSR; } bool impl::file_info::is_owner_executable(void) const { return m_sb.st_mode & S_IXUSR; } bool impl::file_info::is_group_readable(void) const { return m_sb.st_mode & S_IRGRP; } bool impl::file_info::is_group_writable(void) const { return m_sb.st_mode & S_IWGRP; } bool impl::file_info::is_group_executable(void) const { return m_sb.st_mode & S_IXGRP; } bool impl::file_info::is_other_readable(void) const { return m_sb.st_mode & S_IROTH; } bool impl::file_info::is_other_writable(void) const { return m_sb.st_mode & S_IWOTH; } bool impl::file_info::is_other_executable(void) const { return m_sb.st_mode & S_IXOTH; } // ------------------------------------------------------------------------ // The "directory" class. // ------------------------------------------------------------------------ impl::directory::directory(const path& p) { DIR* dp = ::opendir(p.c_str()); if (dp == NULL) throw system_error(IMPL_NAME "::directory::directory(" + p.str() + ")", "opendir(3) failed", errno); struct dirent* dep; while ((dep = ::readdir(dp)) != NULL) { path entryp = p / dep->d_name; insert(value_type(dep->d_name, file_info(entryp))); } if (::closedir(dp) == -1) throw system_error(IMPL_NAME "::directory::directory(" + p.str() + ")", "closedir(3) failed", errno); } std::set< std::string > impl::directory::names(void) const { std::set< std::string > ns; for (const_iterator iter = begin(); iter != end(); iter++) ns.insert((*iter).first); return ns; } // ------------------------------------------------------------------------ // The "temp_dir" class. // ------------------------------------------------------------------------ impl::temp_dir::temp_dir(const path& p) { tools::auto_array< char > buf(new char[p.str().length() + 1]); std::strcpy(buf.get(), p.c_str()); if (::mkdtemp(buf.get()) == NULL) throw tools::system_error(IMPL_NAME "::temp_dir::temp_dir(" + p.str() + ")", "mkdtemp(3) failed", errno); m_path.reset(new path(buf.get())); } impl::temp_dir::~temp_dir(void) { cleanup(*m_path); } const impl::path& impl::temp_dir::get_path(void) const { return *m_path; } // ------------------------------------------------------------------------ // Free functions. // ------------------------------------------------------------------------ bool impl::exists(const path& p) { try { eaccess(p, access_f); return true; } catch (const system_error& e) { if (e.code() == ENOENT) return false; else throw; } } bool impl::have_prog_in_path(const std::string& prog) { assert(prog.find('/') == std::string::npos); // Do not bother to provide a default value for PATH. If it is not // there something is broken in the user's environment. if (!tools::env::has("PATH")) throw std::runtime_error("PATH not defined in the environment"); std::vector< std::string > dirs = tools::text::split(tools::env::get("PATH"), ":"); bool found = false; for (std::vector< std::string >::const_iterator iter = dirs.begin(); !found && iter != dirs.end(); iter++) { const path& dir = path(*iter); if (is_executable(dir / prog)) found = true; } return found; } bool impl::is_executable(const path& p) { if (!exists(p)) return false; return safe_access(p, access_x, EACCES); } void impl::remove(const path& p) { if (file_info(p).get_type() == file_info::dir_type) throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")", "Is a directory", EPERM); if (::unlink(p.c_str()) == -1) throw tools::system_error(IMPL_NAME "::remove(" + p.str() + ")", "unlink(" + p.str() + ") failed", errno); } void impl::rmdir(const path& p) { if (::rmdir(p.c_str())) { if (errno == EEXIST) { /* Some operating systems (e.g. OpenSolaris 200906) return * EEXIST instead of ENOTEMPTY for non-empty directories. * Homogenize the return value so that callers don't need * to bother about differences in operating systems. */ errno = ENOTEMPTY; } throw system_error(IMPL_NAME "::rmdir", "Cannot remove directory", errno); } } impl::path impl::change_directory(const path& dir) { path olddir = get_current_dir(); if (olddir != dir) { if (::chdir(dir.c_str()) == -1) throw tools::system_error(IMPL_NAME "::chdir(" + dir.str() + ")", "chdir(2) failed", errno); } return olddir; } void impl::cleanup(const path& p) { impl::file_info fi(p); cleanup_aux(p, fi.get_device(), true); } impl::path impl::get_current_dir(void) { std::auto_ptr< char > cwd; #if defined(HAVE_GETCWD_DYN) cwd.reset(getcwd(NULL, 0)); #else cwd.reset(getcwd(NULL, MAXPATHLEN)); #endif if (cwd.get() == NULL) throw tools::system_error(IMPL_NAME "::get_current_dir()", "getcwd() failed", errno); return path(cwd.get()); }