diff options
author | Brooks Davis <brooks@FreeBSD.org> | 2020-03-17 16:56:50 +0000 |
---|---|---|
committer | Brooks Davis <brooks@FreeBSD.org> | 2020-03-17 16:56:50 +0000 |
commit | 08334c51dbb99d9ecd2bb86a2d94ed06da9e167a (patch) | |
tree | c43eb24d59bd5c963583a5190caef80fc8387322 /utils/fs |
Notes
Diffstat (limited to 'utils/fs')
-rw-r--r-- | utils/fs/Kyuafile | 10 | ||||
-rw-r--r-- | utils/fs/Makefile.am.inc | 84 | ||||
-rw-r--r-- | utils/fs/auto_cleaners.cpp | 261 | ||||
-rw-r--r-- | utils/fs/auto_cleaners.hpp | 89 | ||||
-rw-r--r-- | utils/fs/auto_cleaners_fwd.hpp | 46 | ||||
-rw-r--r-- | utils/fs/auto_cleaners_test.cpp | 167 | ||||
-rw-r--r-- | utils/fs/directory.cpp | 360 | ||||
-rw-r--r-- | utils/fs/directory.hpp | 120 | ||||
-rw-r--r-- | utils/fs/directory_fwd.hpp | 55 | ||||
-rw-r--r-- | utils/fs/directory_test.cpp | 190 | ||||
-rw-r--r-- | utils/fs/exceptions.cpp | 162 | ||||
-rw-r--r-- | utils/fs/exceptions.hpp | 110 | ||||
-rw-r--r-- | utils/fs/exceptions_test.cpp | 95 | ||||
-rw-r--r-- | utils/fs/lua_module.cpp | 340 | ||||
-rw-r--r-- | utils/fs/lua_module.hpp | 54 | ||||
-rw-r--r-- | utils/fs/lua_module_test.cpp | 376 | ||||
-rw-r--r-- | utils/fs/operations.cpp | 803 | ||||
-rw-r--r-- | utils/fs/operations.hpp | 72 | ||||
-rw-r--r-- | utils/fs/operations_test.cpp | 826 | ||||
-rw-r--r-- | utils/fs/path.cpp | 303 | ||||
-rw-r--r-- | utils/fs/path.hpp | 87 | ||||
-rw-r--r-- | utils/fs/path_fwd.hpp | 45 | ||||
-rw-r--r-- | utils/fs/path_test.cpp | 277 |
23 files changed, 4932 insertions, 0 deletions
diff --git a/utils/fs/Kyuafile b/utils/fs/Kyuafile new file mode 100644 index 000000000000..66cb918fca92 --- /dev/null +++ b/utils/fs/Kyuafile @@ -0,0 +1,10 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="auto_cleaners_test"} +atf_test_program{name="directory_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="lua_module_test"} +atf_test_program{name="operations_test"} +atf_test_program{name="path_test"} diff --git a/utils/fs/Makefile.am.inc b/utils/fs/Makefile.am.inc new file mode 100644 index 000000000000..2acdadafa79b --- /dev/null +++ b/utils/fs/Makefile.am.inc @@ -0,0 +1,84 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +# OWNER 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. + +UTILS_CFLAGS += $(LUTOK_CFLAGS) +UTILS_LIBS += $(LUTOK_LIBS) + +libutils_a_CPPFLAGS += $(LUTOK_CFLAGS) +libutils_a_SOURCES += utils/fs/auto_cleaners.cpp +libutils_a_SOURCES += utils/fs/auto_cleaners.hpp +libutils_a_SOURCES += utils/fs/auto_cleaners_fwd.hpp +libutils_a_SOURCES += utils/fs/directory.cpp +libutils_a_SOURCES += utils/fs/directory.hpp +libutils_a_SOURCES += utils/fs/directory_fwd.hpp +libutils_a_SOURCES += utils/fs/exceptions.cpp +libutils_a_SOURCES += utils/fs/exceptions.hpp +libutils_a_SOURCES += utils/fs/lua_module.cpp +libutils_a_SOURCES += utils/fs/lua_module.hpp +libutils_a_SOURCES += utils/fs/operations.cpp +libutils_a_SOURCES += utils/fs/operations.hpp +libutils_a_SOURCES += utils/fs/path.cpp +libutils_a_SOURCES += utils/fs/path.hpp +libutils_a_SOURCES += utils/fs/path_fwd.hpp + +if WITH_ATF +tests_utils_fsdir = $(pkgtestsdir)/utils/fs + +tests_utils_fs_DATA = utils/fs/Kyuafile +EXTRA_DIST += $(tests_utils_fs_DATA) + +tests_utils_fs_PROGRAMS = utils/fs/auto_cleaners_test +utils_fs_auto_cleaners_test_SOURCES = utils/fs/auto_cleaners_test.cpp +utils_fs_auto_cleaners_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_auto_cleaners_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/directory_test +utils_fs_directory_test_SOURCES = utils/fs/directory_test.cpp +utils_fs_directory_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_directory_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/exceptions_test +utils_fs_exceptions_test_SOURCES = utils/fs/exceptions_test.cpp +utils_fs_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/lua_module_test +utils_fs_lua_module_test_SOURCES = utils/fs/lua_module_test.cpp +utils_fs_lua_module_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_lua_module_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/operations_test +utils_fs_operations_test_SOURCES = utils/fs/operations_test.cpp +utils_fs_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/path_test +utils_fs_path_test_SOURCES = utils/fs/path_test.cpp +utils_fs_path_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_path_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/fs/auto_cleaners.cpp b/utils/fs/auto_cleaners.cpp new file mode 100644 index 000000000000..94ef94465e57 --- /dev/null +++ b/utils/fs/auto_cleaners.cpp @@ -0,0 +1,261 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/auto_cleaners.hpp" + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" + +namespace fs = utils::fs; +namespace signals = utils::signals; + + +/// Shared implementation of the auto_directory. +struct utils::fs::auto_directory::impl : utils::noncopyable { + /// The path to the directory being managed. + fs::path _directory; + + /// Whether cleanup() has been already executed or not. + bool _cleaned; + + /// Constructor. + /// + /// \param directory_ The directory to grab the ownership of. + impl(const path& directory_) : + _directory(directory_), + _cleaned(false) + { + } + + /// Destructor. + ~impl(void) + { + try { + this->cleanup(); + } catch (const fs::error& e) { + LW(F("Failed to auto-cleanup directory '%s': %s") % _directory % + e.what()); + } + } + + /// Removes the directory. + /// + /// See the cleanup() method of the auto_directory class for details. + void + cleanup(void) + { + if (!_cleaned) { + // Mark this as cleaned first so that, in case of failure, we don't + // reraise the error from the destructor. + _cleaned = true; + + fs::rmdir(_directory); + } + } +}; + + +/// Constructs a new auto_directory and grabs ownership of a directory. +/// +/// \param directory_ The directory to grab the ownership of. +fs::auto_directory::auto_directory(const path& directory_) : + _pimpl(new impl(directory_)) +{ +} + + +/// Deletes the managed directory; must be empty. +/// +/// This should not be relied on because it cannot provide proper error +/// reporting. Instead, the caller should use the cleanup() method. +fs::auto_directory::~auto_directory(void) +{ +} + + +/// Creates a self-destructing temporary directory. +/// +/// See the notes for fs::mkdtemp_public() for details on the permissions +/// given to the temporary directory, which are looser than what the standard +/// mkdtemp would grant. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The self-destructing directory. +/// +/// \throw fs::error If the creation fails. +fs::auto_directory +fs::auto_directory::mkdtemp_public(const std::string& path_template) +{ + signals::interrupts_inhibiter inhibiter; + const fs::path directory_ = fs::mkdtemp_public(path_template); + try { + return auto_directory(directory_); + } catch (...) { + fs::rmdir(directory_); + throw; + } +} + + +/// Gets the directory managed by this auto_directory. +/// +/// \return The path to the managed directory. +const fs::path& +fs::auto_directory::directory(void) const +{ + return _pimpl->_directory; +} + + +/// Deletes the managed directory; must be empty. +/// +/// This operation is idempotent. +/// +/// \throw fs::error If there is a problem removing any directory or file. +void +fs::auto_directory::cleanup(void) +{ + _pimpl->cleanup(); +} + + +/// Shared implementation of the auto_file. +struct utils::fs::auto_file::impl : utils::noncopyable { + /// The path to the file being managed. + fs::path _file; + + /// Whether removed() has been already executed or not. + bool _removed; + + /// Constructor. + /// + /// \param file_ The file to grab the ownership of. + impl(const path& file_) : + _file(file_), + _removed(false) + { + } + + /// Destructor. + ~impl(void) + { + try { + this->remove(); + } catch (const fs::error& e) { + LW(F("Failed to auto-cleanup file '%s': %s") % _file % + e.what()); + } + } + + /// Removes the file. + /// + /// See the remove() method of the auto_file class for details. + void + remove(void) + { + if (!_removed) { + // Mark this as cleaned first so that, in case of failure, we don't + // reraise the error from the destructor. + _removed = true; + + fs::unlink(_file); + } + } +}; + + +/// Constructs a new auto_file and grabs ownership of a file. +/// +/// \param file_ The file to grab the ownership of. +fs::auto_file::auto_file(const path& file_) : + _pimpl(new impl(file_)) +{ +} + + +/// Deletes the managed file. +/// +/// This should not be relied on because it cannot provide proper error +/// reporting. Instead, the caller should use the remove() method. +fs::auto_file::~auto_file(void) +{ +} + + +/// Creates a self-destructing temporary file. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The self-destructing file. +/// +/// \throw fs::error If the creation fails. +fs::auto_file +fs::auto_file::mkstemp(const std::string& path_template) +{ + signals::interrupts_inhibiter inhibiter; + const fs::path file_ = fs::mkstemp(path_template); + try { + return auto_file(file_); + } catch (...) { + fs::unlink(file_); + throw; + } +} + + +/// Gets the file managed by this auto_file. +/// +/// \return The path to the managed file. +const fs::path& +fs::auto_file::file(void) const +{ + return _pimpl->_file; +} + + +/// Deletes the managed file. +/// +/// This operation is idempotent. +/// +/// \throw fs::error If there is a problem removing the file. +void +fs::auto_file::remove(void) +{ + _pimpl->remove(); +} diff --git a/utils/fs/auto_cleaners.hpp b/utils/fs/auto_cleaners.hpp new file mode 100644 index 000000000000..f3e6937e3cea --- /dev/null +++ b/utils/fs/auto_cleaners.hpp @@ -0,0 +1,89 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/auto_cleaners.hpp +/// RAII wrappers to automatically remove file system entries. + +#if !defined(UTILS_FS_AUTO_CLEANERS_HPP) +#define UTILS_FS_AUTO_CLEANERS_HPP + +#include "utils/fs/auto_cleaners_fwd.hpp" + +#include <memory> +#include <string> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace fs { + + +/// Grabs ownership of a directory and removes it upon destruction. +/// +/// This class is reference-counted and therefore only the destruction of the +/// last instance will cause the removal of the directory. +class auto_directory { + struct impl; + /// Reference-counted, shared implementation. + std::shared_ptr< impl > _pimpl; + +public: + explicit auto_directory(const path&); + ~auto_directory(void); + + static auto_directory mkdtemp_public(const std::string&); + + const path& directory(void) const; + void cleanup(void); +}; + + +/// Grabs ownership of a file and removes it upon destruction. +/// +/// This class is reference-counted and therefore only the destruction of the +/// last instance will cause the removal of the file. +class auto_file { + struct impl; + /// Reference-counted, shared implementation. + std::shared_ptr< impl > _pimpl; + +public: + explicit auto_file(const path&); + ~auto_file(void); + + static auto_file mkstemp(const std::string&); + + const path& file(void) const; + void remove(void); +}; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_AUTO_CLEANERS_HPP) diff --git a/utils/fs/auto_cleaners_fwd.hpp b/utils/fs/auto_cleaners_fwd.hpp new file mode 100644 index 000000000000..c0cfa6333a1a --- /dev/null +++ b/utils/fs/auto_cleaners_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/auto_cleaners_fwd.hpp +/// Forward declarations for utils/fs/auto_cleaners.hpp + +#if !defined(UTILS_FS_AUTO_CLEANERS_FWD_HPP) +#define UTILS_FS_AUTO_CLEANERS_FWD_HPP + +namespace utils { +namespace fs { + + +class auto_directory; +class auto_file; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_AUTO_CLEANERS_FWD_HPP) diff --git a/utils/fs/auto_cleaners_test.cpp b/utils/fs/auto_cleaners_test.cpp new file mode 100644 index 000000000000..da4bbeb2da68 --- /dev/null +++ b/utils/fs/auto_cleaners_test.cpp @@ -0,0 +1,167 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/auto_cleaners.hpp" + +extern "C" { +#include <unistd.h> +} + +#include <atf-c++.hpp> + +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__automatic); +ATF_TEST_CASE_BODY(auto_directory__automatic) +{ + const fs::path root("root"); + fs::mkdir(root, 0755); + + { + fs::auto_directory dir(root); + ATF_REQUIRE_EQ(root, dir.directory()); + + ATF_REQUIRE(::access("root", X_OK) == 0); + + { + fs::auto_directory dir_copy(dir); + } + // Should still exist after a copy is destructed. + ATF_REQUIRE(::access("root", X_OK) == 0); + } + ATF_REQUIRE(::access("root", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__explicit); +ATF_TEST_CASE_BODY(auto_directory__explicit) +{ + const fs::path root("root"); + fs::mkdir(root, 0755); + + fs::auto_directory dir(root); + ATF_REQUIRE_EQ(root, dir.directory()); + + ATF_REQUIRE(::access("root", X_OK) == 0); + dir.cleanup(); + dir.cleanup(); + ATF_REQUIRE(::access("root", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__mkdtemp_public); +ATF_TEST_CASE_BODY(auto_directory__mkdtemp_public) +{ + utils::setenv("TMPDIR", (fs::current_path() / "tmp").str()); + fs::mkdir(fs::path("tmp"), 0755); + + const std::string path_template("test.XXXXXX"); + { + fs::auto_directory auto_directory = fs::auto_directory::mkdtemp_public( + path_template); + ATF_REQUIRE(::access((fs::path("tmp") / path_template).c_str(), + X_OK) == -1); + ATF_REQUIRE(::rmdir("tmp") == -1); + + ATF_REQUIRE(::access(auto_directory.directory().c_str(), X_OK) == 0); + } + ATF_REQUIRE(::rmdir("tmp") != -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__automatic); +ATF_TEST_CASE_BODY(auto_file__automatic) +{ + const fs::path file("foo"); + atf::utils::create_file(file.str(), ""); + { + fs::auto_file auto_file(file); + ATF_REQUIRE_EQ(file, auto_file.file()); + + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + + { + fs::auto_file auto_file_copy(auto_file); + } + // Should still exist after a copy is destructed. + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + } + ATF_REQUIRE(::access(file.c_str(), R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__explicit); +ATF_TEST_CASE_BODY(auto_file__explicit) +{ + const fs::path file("bar"); + atf::utils::create_file(file.str(), ""); + + fs::auto_file auto_file(file); + ATF_REQUIRE_EQ(file, auto_file.file()); + + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + auto_file.remove(); + auto_file.remove(); + ATF_REQUIRE(::access(file.c_str(), R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__mkstemp); +ATF_TEST_CASE_BODY(auto_file__mkstemp) +{ + utils::setenv("TMPDIR", (fs::current_path() / "tmp").str()); + fs::mkdir(fs::path("tmp"), 0755); + + const std::string path_template("test.XXXXXX"); + { + fs::auto_file auto_file = fs::auto_file::mkstemp(path_template); + ATF_REQUIRE(::access((fs::path("tmp") / path_template).c_str(), + X_OK) == -1); + ATF_REQUIRE(::rmdir("tmp") == -1); + + ATF_REQUIRE(::access(auto_file.file().c_str(), R_OK) == 0); + } + ATF_REQUIRE(::rmdir("tmp") != -1); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, auto_directory__automatic); + ATF_ADD_TEST_CASE(tcs, auto_directory__explicit); + ATF_ADD_TEST_CASE(tcs, auto_directory__mkdtemp_public); + + ATF_ADD_TEST_CASE(tcs, auto_file__automatic); + ATF_ADD_TEST_CASE(tcs, auto_file__explicit); + ATF_ADD_TEST_CASE(tcs, auto_file__mkstemp); +} diff --git a/utils/fs/directory.cpp b/utils/fs/directory.cpp new file mode 100644 index 000000000000..ff7ad5e34357 --- /dev/null +++ b/utils/fs/directory.cpp @@ -0,0 +1,360 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/directory.hpp" + +extern "C" { +#include <sys/types.h> + +#include <dirent.h> +} + +#include <cerrno> +#include <memory> + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace detail = utils::fs::detail; +namespace fs = utils::fs; +namespace text = utils::text; + + +/// Constructs a new directory entry. +/// +/// \param name_ Name of the directory entry. +fs::directory_entry::directory_entry(const std::string& name_) : name(name_) +{ +} + + +/// Checks if two directory entries are equal. +/// +/// \param other The entry to compare to. +/// +/// \return True if the two entries are equal; false otherwise. +bool +fs::directory_entry::operator==(const directory_entry& other) const +{ + return name == other.name; +} + + +/// Checks if two directory entries are different. +/// +/// \param other The entry to compare to. +/// +/// \return True if the two entries are different; false otherwise. +bool +fs::directory_entry::operator!=(const directory_entry& other) const +{ + return !(*this == other); +} + + +/// Checks if this entry sorts before another entry. +/// +/// \param other The entry to compare to. +/// +/// \return True if this entry sorts before the other entry; false otherwise. +bool +fs::directory_entry::operator<(const directory_entry& other) const +{ + return name < other.name; +} + + +/// Formats a directory entry. +/// +/// \param output Stream into which to inject the formatted entry. +/// \param entry The entry to format. +/// +/// \return A reference to output. +std::ostream& +fs::operator<<(std::ostream& output, const directory_entry& entry) +{ + output << F("directory_entry{name=%s}") % text::quote(entry.name, '\''); + return output; +} + + +/// Internal implementation details for the directory_iterator. +/// +/// In order to support multiple concurrent iterators over the same directory +/// object, this class is the one that performs all directory-level accesses. +/// In particular, even if it may seem surprising, this is the class that +/// handles the DIR object for the directory. +/// +/// Note that iterators implemented by this class do not rely on the container +/// directory class at all. This should not be relied on for object lifecycle +/// purposes. +struct utils::fs::detail::directory_iterator::impl : utils::noncopyable { + /// Path of the directory accessed by this iterator. + const fs::path _path; + + /// Raw pointer to the system representation of the directory. + /// + /// We also use this to determine if the iterator is valid (at the end) or + /// not. A null pointer means an invalid iterator. + ::DIR* _dirp; + + /// Raw representation of the system directory entry. + /// + /// We need to keep this at the class level so that we can use the + /// readdir_r(3) function. + ::dirent _dirent; + + /// Custom representation of the directory entry. + /// + /// This is separate from _dirent because this is the type we return to the + /// user. We must keep this as a pointer so that we can support the common + /// operators (* and ->) over iterators. + std::auto_ptr< directory_entry > _entry; + + /// Constructs an iterator pointing to the "end" of the directory. + impl(void) : _path("invalid-directory-entry"), _dirp(NULL) + { + } + + /// Constructs a new iterator to start scanning a directory. + /// + /// \param path The directory that will be scanned. + /// + /// \throw system_error If there is a problem opening the directory. + explicit impl(const path& path) : _path(path) + { + DIR* dirp = ::opendir(_path.c_str()); + if (dirp == NULL) { + const int original_errno = errno; + throw fs::system_error(F("opendir(%s) failed") % _path, + original_errno); + } + _dirp = dirp; + + // Initialize our first directory entry. Note that this may actually + // close the directory we just opened if the directory happens to be + // empty -- but directories are never empty because they at least have + // '.' and '..' entries. + next(); + } + + /// Destructor. + /// + /// This closes the directory if still open. + ~impl(void) + { + if (_dirp != NULL) + close(); + } + + /// Closes the directory and invalidates the iterator. + void + close(void) + { + PRE(_dirp != NULL); + if (::closedir(_dirp) == -1) { + UNREACHABLE_MSG("Invalid dirp provided to closedir(3)"); + } + _dirp = NULL; + } + + /// Advances the directory entry to the next one. + /// + /// It is possible to use this function on a new directory_entry object to + /// initialize the first entry. + /// + /// \throw system_error If the call to readdir_r fails. + void + next(void) + { + ::dirent* result; + + if (::readdir_r(_dirp, &_dirent, &result) == -1) { + const int original_errno = errno; + throw fs::system_error(F("readdir_r(%s) failed") % _path, + original_errno); + } + if (result == NULL) { + _entry.reset(NULL); + close(); + } else { + _entry.reset(new directory_entry(_dirent.d_name)); + } + } +}; + + +/// Constructs a new directory iterator. +/// +/// \param pimpl The constructed internal implementation structure to use. +detail::directory_iterator::directory_iterator(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +detail::directory_iterator::~directory_iterator(void) +{ +} + + +/// Creates a new directory iterator for a directory. +/// +/// \return The directory iterator. Note that the result may be invalid. +/// +/// \throw system_error If opening the directory or reading its first entry +/// fails. +detail::directory_iterator +detail::directory_iterator::new_begin(const path& path) +{ + return directory_iterator(std::shared_ptr< impl >(new impl(path))); +} + + +/// Creates a new invalid directory iterator. +/// +/// \return The invalid directory iterator. +detail::directory_iterator +detail::directory_iterator::new_end(void) +{ + return directory_iterator(std::shared_ptr< impl >(new impl())); +} + + +/// Checks if two iterators are equal. +/// +/// We consider two iterators to be equal if both of them are invalid or, +/// otherwise, if they have the exact same internal representation (as given by +/// equality of the pimpl pointers). +/// +/// \param other The object to compare to. +/// +/// \return True if the two iterators are equal; false otherwise. +bool +detail::directory_iterator::operator==(const directory_iterator& other) const +{ + return (_pimpl->_dirp == NULL && other._pimpl->_dirp == NULL) || + _pimpl == other._pimpl; +} + + +/// Checks if two iterators are different. +/// +/// \param other The object to compare to. +/// +/// \return True if the two iterators are different; false otherwise. +bool +detail::directory_iterator::operator!=(const directory_iterator& other) const +{ + return !(*this == other); +} + + +/// Moves the iterator one element forward. +/// +/// \return A reference to the iterator. +/// +/// \throw system_error If advancing the iterator fails. +detail::directory_iterator& +detail::directory_iterator::operator++(void) +{ + _pimpl->next(); + return *this; +} + + +/// Dereferences the iterator to its contents. +/// +/// \return A reference to the directory entry pointed to by the iterator. +const fs::directory_entry& +detail::directory_iterator::operator*(void) const +{ + PRE(_pimpl->_entry.get() != NULL); + return *_pimpl->_entry; +} + + +/// Dereferences the iterator to its contents. +/// +/// \return A pointer to the directory entry pointed to by the iterator. +const fs::directory_entry* +detail::directory_iterator::operator->(void) const +{ + PRE(_pimpl->_entry.get() != NULL); + return _pimpl->_entry.get(); +} + + +/// Internal implementation details for the directory. +struct utils::fs::directory::impl : utils::noncopyable { + /// Path to the directory to scan. + fs::path _path; + + /// Constructs a new directory. + /// + /// \param path_ Path to the directory to scan. + impl(const fs::path& path_) : _path(path_) + { + } +}; + + +/// Constructs a new directory. +/// +/// \param path_ Path to the directory to scan. +fs::directory::directory(const path& path_) : _pimpl(new impl(path_)) +{ +} + + +/// Returns an iterator to start scanning the directory. +/// +/// \return An iterator on the directory. +/// +/// \throw system_error If the directory cannot be opened to obtain its first +/// entry. +fs::directory::const_iterator +fs::directory::begin(void) const +{ + return const_iterator::new_begin(_pimpl->_path); +} + + +/// Returns an invalid iterator to check for the end of an scan. +/// +/// \return An invalid iterator. +fs::directory::const_iterator +fs::directory::end(void) const +{ + return const_iterator::new_end(); +} diff --git a/utils/fs/directory.hpp b/utils/fs/directory.hpp new file mode 100644 index 000000000000..53c37ec86450 --- /dev/null +++ b/utils/fs/directory.hpp @@ -0,0 +1,120 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/directory.hpp +/// Provides the utils::fs::directory class. + +#if !defined(UTILS_FS_DIRECTORY_HPP) +#define UTILS_FS_DIRECTORY_HPP + +#include "utils/fs/directory_fwd.hpp" + +#include <memory> +#include <ostream> +#include <string> + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace fs { + + +/// Representation of a single directory entry. +struct directory_entry { + /// Name of the directory entry. + std::string name; + + explicit directory_entry(const std::string&); + + bool operator==(const directory_entry&) const; + bool operator!=(const directory_entry&) const; + bool operator<(const directory_entry&) const; +}; + + +std::ostream& operator<<(std::ostream&, const directory_entry&); + + +namespace detail { + + +/// Forward directory iterator. +class directory_iterator { + struct impl; + + /// Internal implementation details. + std::shared_ptr< impl > _pimpl; + + directory_iterator(std::shared_ptr< impl >); + + friend class fs::directory; + static directory_iterator new_begin(const path&); + static directory_iterator new_end(void); + +public: + ~directory_iterator(); + + bool operator==(const directory_iterator&) const; + bool operator!=(const directory_iterator&) const; + directory_iterator& operator++(void); + + const directory_entry& operator*(void) const; + const directory_entry* operator->(void) const; +}; + + +} // namespace detail + + +/// Representation of a local filesystem directory. +/// +/// This class is pretty much stateless. All the directory manipulation +/// operations happen within the iterator. +class directory { +public: + /// Public type for a constant forward directory iterator. + typedef detail::directory_iterator const_iterator; + +private: + struct impl; + + /// Internal implementation details. + std::shared_ptr< impl > _pimpl; + +public: + explicit directory(const path&); + + const_iterator begin(void) const; + const_iterator end(void) const; +}; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_DIRECTORY_HPP) diff --git a/utils/fs/directory_fwd.hpp b/utils/fs/directory_fwd.hpp new file mode 100644 index 000000000000..50886551ca88 --- /dev/null +++ b/utils/fs/directory_fwd.hpp @@ -0,0 +1,55 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/directory_fwd.hpp +/// Forward declarations for utils/fs/directory.hpp + +#if !defined(UTILS_FS_DIRECTORY_FWD_HPP) +#define UTILS_FS_DIRECTORY_FWD_HPP + +namespace utils { +namespace fs { + + +namespace detail { + + +class directory_iterator; + + +} // namespace detail + + +struct directory_entry; +class directory; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_DIRECTORY_FWD_HPP) diff --git a/utils/fs/directory_test.cpp b/utils/fs/directory_test.cpp new file mode 100644 index 000000000000..4c1aa2d010f4 --- /dev/null +++ b/utils/fs/directory_test.cpp @@ -0,0 +1,190 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/directory.hpp" + +#include <sstream> + +#include <atf-c++.hpp> + +#include "utils/format/containers.ipp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__public_fields); +ATF_TEST_CASE_BODY(directory_entry__public_fields) +{ + const fs::directory_entry entry("name"); + ATF_REQUIRE_EQ("name", entry.name); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__equality); +ATF_TEST_CASE_BODY(directory_entry__equality) +{ + const fs::directory_entry entry1("name"); + const fs::directory_entry entry2("other-name"); + + ATF_REQUIRE( entry1 == entry1); + ATF_REQUIRE(!(entry1 != entry1)); + + ATF_REQUIRE(!(entry1 == entry2)); + ATF_REQUIRE( entry1 != entry2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__sorting); +ATF_TEST_CASE_BODY(directory_entry__sorting) +{ + const fs::directory_entry entry1("name"); + const fs::directory_entry entry2("other-name"); + + ATF_REQUIRE(!(entry1 < entry1)); + ATF_REQUIRE(!(entry2 < entry2)); + ATF_REQUIRE( entry1 < entry2); + ATF_REQUIRE(!(entry2 < entry1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__format); +ATF_TEST_CASE_BODY(directory_entry__format) +{ + const fs::directory_entry entry("this is the name"); + std::ostringstream output; + output << entry; + ATF_REQUIRE_EQ("directory_entry{name='this is the name'}", output.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__empty); +ATF_TEST_CASE_BODY(integration__empty) +{ + fs::mkdir(fs::path("empty"), 0755); + + std::set< fs::directory_entry > contents; + const fs::directory dir(fs::path("empty")); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + // While we are here, make sure both * and -> represent the same. + ATF_REQUIRE((*iter).name == iter->name); + } + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__some_contents); +ATF_TEST_CASE_BODY(integration__some_contents) +{ + fs::mkdir(fs::path("full"), 0755); + atf::utils::create_file("full/a file", ""); + atf::utils::create_file("full/something-else", ""); + atf::utils::create_file("full/.hidden", ""); + fs::mkdir(fs::path("full/subdir"), 0755); + atf::utils::create_file("full/subdir/not-listed", ""); + + std::set< fs::directory_entry > contents; + const fs::directory dir(fs::path("full")); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + // While we are here, make sure both * and -> represent the same. + ATF_REQUIRE((*iter).name == iter->name); + } + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + exp_contents.insert(fs::directory_entry(".hidden")); + exp_contents.insert(fs::directory_entry("a file")); + exp_contents.insert(fs::directory_entry("something-else")); + exp_contents.insert(fs::directory_entry("subdir")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__open_failure); +ATF_TEST_CASE_BODY(integration__open_failure) +{ + const fs::directory directory(fs::path("non-existent")); + ATF_REQUIRE_THROW_RE(fs::system_error, "opendir(.*non-existent.*) failed", + directory.begin()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__iterators_equality); +ATF_TEST_CASE_BODY(integration__iterators_equality) +{ + const fs::directory directory(fs::path(".")); + + fs::directory::const_iterator iter_ok1 = directory.begin(); + fs::directory::const_iterator iter_ok2 = directory.begin(); + fs::directory::const_iterator iter_end = directory.end(); + + ATF_REQUIRE( iter_ok1 == iter_ok1); + ATF_REQUIRE(!(iter_ok1 != iter_ok1)); + + ATF_REQUIRE( iter_ok2 == iter_ok2); + ATF_REQUIRE(!(iter_ok2 != iter_ok2)); + + ATF_REQUIRE(!(iter_ok1 == iter_ok2)); + ATF_REQUIRE( iter_ok1 != iter_ok2); + + ATF_REQUIRE(!(iter_ok1 == iter_end)); + ATF_REQUIRE( iter_ok1 != iter_end); + + ATF_REQUIRE(!(iter_ok2 == iter_end)); + ATF_REQUIRE( iter_ok2 != iter_end); + + ATF_REQUIRE( iter_end == iter_end); + ATF_REQUIRE(!(iter_end != iter_end)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, directory_entry__public_fields); + ATF_ADD_TEST_CASE(tcs, directory_entry__equality); + ATF_ADD_TEST_CASE(tcs, directory_entry__sorting); + ATF_ADD_TEST_CASE(tcs, directory_entry__format); + + ATF_ADD_TEST_CASE(tcs, integration__empty); + ATF_ADD_TEST_CASE(tcs, integration__some_contents); + ATF_ADD_TEST_CASE(tcs, integration__open_failure); + ATF_ADD_TEST_CASE(tcs, integration__iterators_equality); +} diff --git a/utils/fs/exceptions.cpp b/utils/fs/exceptions.cpp new file mode 100644 index 000000000000..102e9069ee3c --- /dev/null +++ b/utils/fs/exceptions.cpp @@ -0,0 +1,162 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/exceptions.hpp" + +#include <cstring> + +#include "utils/format/macros.hpp" + +namespace fs = utils::fs; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +fs::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +fs::error::~error(void) throw() +{ +} + + +/// Constructs a new invalid_path_error. +/// +/// \param textual_path Textual representation of the invalid path. +/// \param reason Description of the error in the path. +fs::invalid_path_error::invalid_path_error(const std::string& textual_path, + const std::string& reason) : + error(F("Invalid path '%s': %s") % textual_path % reason), + _textual_path(textual_path) +{ +} + + +/// Destructor for the error. +fs::invalid_path_error::~invalid_path_error(void) throw() +{ +} + + +/// Returns the invalid path related to the exception. +/// +/// \return The textual representation of the invalid path. +const std::string& +fs::invalid_path_error::invalid_path(void) const +{ + return _textual_path; +} + + +/// Constructs a new join_error. +/// +/// \param textual_path1_ Textual representation of the first path. +/// \param textual_path2_ Textual representation of the second path. +/// \param reason Description of the error in the join operation. +fs::join_error::join_error(const std::string& textual_path1_, + const std::string& textual_path2_, + const std::string& reason) : + error(F("Cannot join paths '%s' and '%s': %s") % textual_path1_ % + textual_path2_ % reason), + _textual_path1(textual_path1_), + _textual_path2(textual_path2_) +{ +} + + +/// Destructor for the error. +fs::join_error::~join_error(void) throw() +{ +} + + +/// Gets the first path that caused the error in a join operation. +/// +/// \return The textual representation of the path. +const std::string& +fs::join_error::textual_path1(void) const +{ + return _textual_path1; +} + + +/// Gets the second path that caused the error in a join operation. +/// +/// \return The textual representation of the path. +const std::string& +fs::join_error::textual_path2(void) const +{ + return _textual_path2; +} + + +/// Constructs a new error based on an errno code. +/// +/// \param message_ The message describing what caused the error. +/// \param errno_ The error code. +fs::system_error::system_error(const std::string& message_, const int errno_) : + error(F("%s: %s") % message_ % std::strerror(errno_)), + _original_errno(errno_) +{ +} + + +/// Destructor for the error. +fs::system_error::~system_error(void) throw() +{ +} + + + +/// \return The original errno code. +int +fs::system_error::original_errno(void) const throw() +{ + return _original_errno; +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +fs::unsupported_operation_error::unsupported_operation_error( + const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +fs::unsupported_operation_error::~unsupported_operation_error(void) throw() +{ +} diff --git a/utils/fs/exceptions.hpp b/utils/fs/exceptions.hpp new file mode 100644 index 000000000000..32b4af2ce463 --- /dev/null +++ b/utils/fs/exceptions.hpp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/exceptions.hpp +/// Exception types raised by the fs module. + +#if !defined(UTILS_FS_EXCEPTIONS_HPP) +#define UTILS_FS_EXCEPTIONS_HPP + +#include <stdexcept> +#include <string> + +namespace utils { +namespace fs { + + +/// Base exception for fs errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error denoting an invalid path while constructing a fs::path object. +class invalid_path_error : public error { + /// Raw value of the invalid path. + std::string _textual_path; + +public: + explicit invalid_path_error(const std::string&, const std::string&); + virtual ~invalid_path_error(void) throw(); + + const std::string& invalid_path(void) const; +}; + + +/// Paths cannot be joined. +class join_error : public error { + /// Raw value of the first path in the join operation. + std::string _textual_path1; + + /// Raw value of the second path in the join operation. + std::string _textual_path2; + +public: + explicit join_error(const std::string&, const std::string&, + const std::string&); + virtual ~join_error(void) throw(); + + const std::string& textual_path1(void) const; + const std::string& textual_path2(void) const; +}; + + +/// Exceptions for errno-based errors. +/// +/// TODO(jmmv): This code is duplicated in, at least, utils::process. Figure +/// out a way to reuse this exception while maintaining the correct inheritance +/// (i.e. be able to keep it as a child of fs::error). +class system_error : public error { + /// Error number describing this libc error condition. + int _original_errno; + +public: + explicit system_error(const std::string&, const int); + ~system_error(void) throw(); + + int original_errno(void) const throw(); +}; + + +/// Exception to denote an unsupported operation. +class unsupported_operation_error : public error { +public: + explicit unsupported_operation_error(const std::string&); + virtual ~unsupported_operation_error(void) throw(); +}; + + +} // namespace fs +} // namespace utils + + +#endif // !defined(UTILS_FS_EXCEPTIONS_HPP) diff --git a/utils/fs/exceptions_test.cpp b/utils/fs/exceptions_test.cpp new file mode 100644 index 000000000000..e67a846506cc --- /dev/null +++ b/utils/fs/exceptions_test.cpp @@ -0,0 +1,95 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/exceptions.hpp" + +#include <cerrno> +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const fs::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_path_error); +ATF_TEST_CASE_BODY(invalid_path_error) +{ + const fs::invalid_path_error e("some/invalid/path", "The reason"); + ATF_REQUIRE(std::strcmp("Invalid path 'some/invalid/path': The reason", + e.what()) == 0); + ATF_REQUIRE_EQ("some/invalid/path", e.invalid_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join_error); +ATF_TEST_CASE_BODY(join_error) +{ + const fs::join_error e("dir1/file1", "/dir2/file2", "The reason"); + ATF_REQUIRE(std::strcmp("Cannot join paths 'dir1/file1' and '/dir2/file2': " + "The reason", e.what()) == 0); + ATF_REQUIRE_EQ("dir1/file1", e.textual_path1()); + ATF_REQUIRE_EQ("/dir2/file2", e.textual_path2()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(system_error); +ATF_TEST_CASE_BODY(system_error) +{ + const fs::system_error e("Call failed", ENOENT); + const std::string expected = F("Call failed: %s") % std::strerror(ENOENT); + ATF_REQUIRE_EQ(expected, e.what()); + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unsupported_operation_error); +ATF_TEST_CASE_BODY(unsupported_operation_error) +{ + const fs::unsupported_operation_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, invalid_path_error); + ATF_ADD_TEST_CASE(tcs, join_error); + ATF_ADD_TEST_CASE(tcs, system_error); + ATF_ADD_TEST_CASE(tcs, unsupported_operation_error); +} diff --git a/utils/fs/lua_module.cpp b/utils/fs/lua_module.cpp new file mode 100644 index 000000000000..dec410927e1a --- /dev/null +++ b/utils/fs/lua_module.cpp @@ -0,0 +1,340 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/lua_module.hpp" + +extern "C" { +#include <dirent.h> +} + +#include <cerrno> +#include <cstring> +#include <stdexcept> +#include <string> + +#include <lutok/operations.hpp> +#include <lutok/stack_cleaner.hpp> +#include <lutok/state.ipp> + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Given a path, qualifies it with the module's start directory if necessary. +/// +/// \param state The Lua state. +/// \param path The path to qualify. +/// +/// \return The original path if it was absolute; otherwise the original path +/// appended to the module's start directory. +/// +/// \throw std::runtime_error If the module's state has been corrupted. +static fs::path +qualify_path(lutok::state& state, const fs::path& path) +{ + lutok::stack_cleaner cleaner(state); + + if (path.is_absolute()) { + return path; + } else { + state.get_global("_fs_start_dir"); + if (!state.is_string(-1)) + throw std::runtime_error("Missing _fs_start_dir global variable; " + "state corrupted?"); + return fs::path(state.to_string(-1)) / path; + } +} + + +/// Safely gets a path from the Lua state. +/// +/// \param state The Lua state. +/// \param index The position in the Lua stack that contains the path to query. +/// +/// \return The queried path. +/// +/// \throw fs::error If the value is not a valid path. +/// \throw std::runtime_error If the value on the Lua stack is not convertible +/// to a path. +static fs::path +to_path(lutok::state& state, const int index) +{ + if (!state.is_string(index)) + throw std::runtime_error("Need a string parameter"); + return fs::path(state.to_string(index)); +} + + +/// Lua binding for fs::path::basename. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The basename of the input path. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_basename(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + state.push_string(path.leaf_name().c_str()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::dirname. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The directory part of the input path. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_dirname(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + state.push_string(path.branch_path().c_str()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::exists. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) Whether the input path exists or not. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_exists(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = qualify_path(state, to_path(state, -1)); + state.push_boolean(fs::exists(path)); + cleaner.forget(); + return 1; +} + + +/// Lua binding for the files iterator. +/// +/// This function takes an open directory from the closure of the iterator and +/// returns the next entry. See lua_fs_files() for the iterator generator +/// function. +/// +/// \pre upvalue(1) The userdata containing an open DIR* object. +/// +/// \param state The lua state. +/// +/// \return The number of result values, i.e. 0 if there are no more entries or +/// 1 if an entry has been read. +static int +files_iterator(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + DIR** dirp = state.to_userdata< DIR* >(state.upvalue_index(1)); + const struct dirent* entry = ::readdir(*dirp); + if (entry == NULL) + return 0; + else { + state.push_string(entry->d_name); + cleaner.forget(); + return 1; + } +} + + +/// Lua binding for the destruction of the files iterator. +/// +/// This function takes an open directory and closes it. See lua_fs_files() for +/// the iterator generator function. +/// +/// \pre stack(-1) The userdata containing an open DIR* object. +/// \post The DIR* object is closed. +/// +/// \param state The lua state. +/// +/// \return The number of result values, i.e. 0. +static int +files_gc(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + PRE(state.is_userdata(-1)); + + DIR** dirp = state.to_userdata< DIR* >(-1); + // For some reason, this may be called more than once. I don't know why + // this happens, but we must protect against it. + if (*dirp != NULL) { + ::closedir(*dirp); + *dirp = NULL; + } + + return 0; +} + + +/// Lua binding to create an iterator to scan the contents of a directory. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The iterator function. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_files(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = qualify_path(state, to_path(state, -1)); + + DIR** dirp = state.new_userdata< DIR* >(); + + state.new_table(); + state.push_string("__gc"); + state.push_cxx_function(files_gc); + state.set_table(-3); + + state.set_metatable(-2); + + *dirp = ::opendir(path.c_str()); + if (*dirp == NULL) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to open directory: %s") % + std::strerror(original_errno)); + } + + state.push_cxx_closure(files_iterator, 1); + + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::is_absolute. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) Whether the input path is absolute or not. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_is_absolute(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + + state.push_boolean(path.is_absolute()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::operator/. +/// +/// \pre stack(-2) The first input path. +/// \pre stack(-1) The second input path. +/// \post stack(-1) The concatenation of the two paths. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_join(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path1 = to_path(state, -2); + const fs::path path2 = to_path(state, -1); + state.push_string((path1 / path2).c_str()); + cleaner.forget(); + return 1; +} + + +} // anonymous namespace + + +/// Creates a Lua 'fs' module with a default start directory of ".". +/// +/// \post The global 'fs' symbol is set to a table that contains functions to a +/// variety of utilites from the fs C++ module. +/// +/// \param s The Lua state. +void +fs::open_fs(lutok::state& s) +{ + open_fs(s, fs::current_path()); +} + + +/// Creates a Lua 'fs' module with an explicit start directory. +/// +/// \post The global 'fs' symbol is set to a table that contains functions to a +/// variety of utilites from the fs C++ module. +/// +/// \param s The Lua state. +/// \param start_dir The start directory to use in all operations that reference +/// the underlying file sytem. +void +fs::open_fs(lutok::state& s, const fs::path& start_dir) +{ + lutok::stack_cleaner cleaner(s); + + s.push_string(start_dir.str()); + s.set_global("_fs_start_dir"); + + std::map< std::string, lutok::cxx_function > members; + members["basename"] = lua_fs_basename; + members["dirname"] = lua_fs_dirname; + members["exists"] = lua_fs_exists; + members["files"] = lua_fs_files; + members["is_absolute"] = lua_fs_is_absolute; + members["join"] = lua_fs_join; + lutok::create_module(s, "fs", members); +} diff --git a/utils/fs/lua_module.hpp b/utils/fs/lua_module.hpp new file mode 100644 index 000000000000..c9c303b15eb7 --- /dev/null +++ b/utils/fs/lua_module.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/lua_module.hpp +/// Lua bindings for the utils::fs module. +/// +/// When the fs module is bound to Lua, the module has the concept of a "start +/// directory". The start directory is the directory used to qualify all +/// relative paths, and is provided at module binding time. + +#if !defined(UTILS_FS_LUA_MODULE_HPP) +#define UTILS_FS_LUA_MODULE_HPP + +#include <lutok/state.hpp> + +#include "utils/fs/path.hpp" + +namespace utils { +namespace fs { + + +void open_fs(lutok::state&); +void open_fs(lutok::state&, const fs::path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_LUA_MODULE_HPP) diff --git a/utils/fs/lua_module_test.cpp b/utils/fs/lua_module_test.cpp new file mode 100644 index 000000000000..263632ded13f --- /dev/null +++ b/utils/fs/lua_module_test.cpp @@ -0,0 +1,376 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/lua_module.hpp" + +#include <atf-c++.hpp> +#include <lutok/operations.hpp> +#include <lutok/state.hpp> +#include <lutok/test_utils.hpp> + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(open_fs); +ATF_TEST_CASE_BODY(open_fs) +{ + lutok::state state; + stack_balance_checker checker(state); + fs::open_fs(state); + lutok::do_string(state, "return fs.basename", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + lutok::do_string(state, "return fs.dirname", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + lutok::do_string(state, "return fs.join", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + state.pop(3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(basename__ok); +ATF_TEST_CASE_BODY(basename__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.basename('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE_EQ("file_foobar", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(basename__fail); +ATF_TEST_CASE_BODY(basename__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.basename({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.basename('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dirname__ok); +ATF_TEST_CASE_BODY(dirname__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.dirname('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE_EQ("/my/test", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dirname__fail); +ATF_TEST_CASE_BODY(dirname__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.dirname({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.dirname('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__ok); +ATF_TEST_CASE_BODY(exists__ok) +{ + lutok::state state; + fs::open_fs(state); + + atf::utils::create_file("foo", ""); + + lutok::do_string(state, "return fs.exists('foo')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('bar')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, + F("return fs.exists('%s')") % fs::current_path(), 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__fail); +ATF_TEST_CASE_BODY(exists__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.exists({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.exists('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__custom_start_dir); +ATF_TEST_CASE_BODY(exists__custom_start_dir) +{ + lutok::state state; + fs::open_fs(state, fs::path("subdir")); + + fs::mkdir(fs::path("subdir"), 0755); + atf::utils::create_file("subdir/foo", ""); + atf::utils::create_file("bar", ""); + + lutok::do_string(state, "return fs.exists('foo')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('subdir/foo')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('bar')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('../bar')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, + F("return fs.exists('%s')") % (fs::current_path() / "bar"), + 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__none); +ATF_TEST_CASE_BODY(files__none) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state); + + fs::mkdir(fs::path("root"), 0755); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('root') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". ..", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__some); +ATF_TEST_CASE_BODY(files__some) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/file1", ""); + atf::utils::create_file("root/file2", ""); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('root') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". .. file1 file2", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__some_with_custom_start_dir); +ATF_TEST_CASE_BODY(files__some_with_custom_start_dir) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state, fs::current_path() / "root"); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/file1", ""); + atf::utils::create_file("root/file2", ""); + atf::utils::create_file("file3", ""); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('.') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". .. file1 file2", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__fail_arg); +ATF_TEST_CASE_BODY(files__fail_arg) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string parameter", + lutok::do_string(state, "fs.files({})", 0, 0, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "fs.files('')", 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__fail_opendir); +ATF_TEST_CASE_BODY(files__fail_opendir) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Failed to open directory", + lutok::do_string(state, "fs.files('root')", 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute__ok); +ATF_TEST_CASE_BODY(is_absolute__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.is_absolute('my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + lutok::do_string(state, "return fs.is_absolute('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute__fail); +ATF_TEST_CASE_BODY(is_absolute__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.is_absolute({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.is_absolute('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__ok); +ATF_TEST_CASE_BODY(join__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.join('/a/b///', 'c/d')", 0, 1, 0); + ATF_REQUIRE_EQ("/a/b/c/d", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__fail); +ATF_TEST_CASE_BODY(join__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.join({}, 'a')", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.join('a', {})", + 0, 1, 0)); + + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.join('', 'a')", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.join('a', '')", + 0, 1, 0)); + + ATF_REQUIRE_THROW_RE(lutok::error, "Cannot join.*'a/b'.*'/c'", + lutok::do_string(state, "fs.join('a/b', '/c')", + 0, 0, 0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, open_fs); + + ATF_ADD_TEST_CASE(tcs, basename__ok); + ATF_ADD_TEST_CASE(tcs, basename__fail); + + ATF_ADD_TEST_CASE(tcs, dirname__ok); + ATF_ADD_TEST_CASE(tcs, dirname__fail); + + ATF_ADD_TEST_CASE(tcs, exists__ok); + ATF_ADD_TEST_CASE(tcs, exists__fail); + ATF_ADD_TEST_CASE(tcs, exists__custom_start_dir); + + ATF_ADD_TEST_CASE(tcs, files__none); + ATF_ADD_TEST_CASE(tcs, files__some); + ATF_ADD_TEST_CASE(tcs, files__some_with_custom_start_dir); + ATF_ADD_TEST_CASE(tcs, files__fail_arg); + ATF_ADD_TEST_CASE(tcs, files__fail_opendir); + + ATF_ADD_TEST_CASE(tcs, is_absolute__ok); + ATF_ADD_TEST_CASE(tcs, is_absolute__fail); + + ATF_ADD_TEST_CASE(tcs, join__ok); + ATF_ADD_TEST_CASE(tcs, join__fail); +} diff --git a/utils/fs/operations.cpp b/utils/fs/operations.cpp new file mode 100644 index 000000000000..7a96d0b2058a --- /dev/null +++ b/utils/fs/operations.cpp @@ -0,0 +1,803 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/operations.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include <sys/param.h> +#if defined(HAVE_SYS_MOUNT_H) +# include <sys/mount.h> +#endif +#include <sys/stat.h> +#if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS) +# include <sys/statvfs.h> +#endif +#if defined(HAVE_SYS_VFS_H) +# include <sys/vfs.h> +#endif +#include <sys/wait.h> + +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <fstream> +#include <iostream> +#include <sstream> +#include <string> + +#include "utils/auto_array.ipp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/units.hpp" + +namespace fs = utils::fs; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Operating systems recognized by the code below. +enum os_type { + os_unsupported = 0, + os_freebsd, + os_linux, + os_netbsd, + os_sunos, +}; + + +/// The current operating system. +static enum os_type current_os = +#if defined(__FreeBSD__) + os_freebsd +#elif defined(__linux__) + os_linux +#elif defined(__NetBSD__) + os_netbsd +#elif defined(__SunOS__) + os_sunos +#else + os_unsupported +#endif + ; + + +/// Specifies if a real unmount(2) is available. +/// +/// We use this as a constant instead of a macro so that we can compile both +/// versions of the unmount code unconditionally. This is a way to prevent +/// compilation bugs going unnoticed for long. +static const bool have_unmount2 = +#if defined(HAVE_UNMOUNT) + true; +#else + false; +#endif + + +#if !defined(UMOUNT) +/// Fake replacement value to the path to umount(8). +# define UMOUNT "do-not-use-this-value" +#else +# if defined(HAVE_UNMOUNT) +# error "umount(8) detected when unmount(2) is also available" +# endif +#endif + + +#if !defined(HAVE_UNMOUNT) +/// Fake unmount(2) function for systems without it. +/// +/// This is only provided to allow our code to compile in all platforms +/// regardless of whether they actually have an unmount(2) or not. +/// +/// \return -1 to indicate error, although this should never happen. +static int +unmount(const char* /* path */, + const int /* flags */) +{ + PRE(false); + return -1; +} +#endif + + +/// Error code returned by subprocess to indicate a controlled failure. +const int exit_known_error = 123; + + +static void run_mount_tmpfs(const fs::path&, const uint64_t) UTILS_NORETURN; + + +/// Executes 'mount -t tmpfs' (or a similar variant). +/// +/// This function must be called from a subprocess as it never returns. +/// +/// \param mount_point Location on which to mount a tmpfs. +/// \param size The size of the tmpfs to mount. If 0, use unlimited. +static void +run_mount_tmpfs(const fs::path& mount_point, const uint64_t size) +{ + const char* mount_args[16]; + std::string size_arg; + + std::size_t last = 0; + switch (current_os) { + case os_freebsd: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-osize=%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_linux: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-osize=%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_netbsd: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-o-s%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_sunos: + mount_args[last++] = "mount"; + mount_args[last++] = "-Ftmpfs"; + if (size > 0) { + size_arg = F("-o-s%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + default: + std::cerr << "Don't know how to mount a temporary file system in this " + "host operating system\n"; + std::exit(exit_known_error); + } + mount_args[last] = NULL; + + const char** arg; + std::cout << "Mounting tmpfs onto " << mount_point << " with:"; + for (arg = &mount_args[0]; *arg != NULL; arg++) + std::cout << " " << *arg; + std::cout << "\n"; + + const int ret = ::execvp(mount_args[0], + UTILS_UNCONST(char* const, mount_args)); + INV(ret == -1); + std::cerr << "Failed to exec " << mount_args[0] << "\n"; + std::exit(EXIT_FAILURE); +} + + +/// Unmounts a file system using unmount(2). +/// +/// \pre unmount(2) must be available; i.e. have_unmount2 must be true. +/// +/// \param mount_point The file system to unmount. +/// +/// \throw fs::system_error If the call to unmount(2) fails. +static void +unmount_with_unmount2(const fs::path& mount_point) +{ + PRE(have_unmount2); + + if (::unmount(mount_point.c_str(), 0) == -1) { + const int original_errno = errno; + throw fs::system_error(F("unmount(%s) failed") % mount_point, + original_errno); + } +} + + +/// Unmounts a file system using umount(8). +/// +/// \pre umount(2) must not be available; i.e. have_unmount2 must be false. +/// +/// \param mount_point The file system to unmount. +/// +/// \throw fs::error If the execution of umount(8) fails. +static void +unmount_with_umount8(const fs::path& mount_point) +{ + PRE(!have_unmount2); + + const pid_t pid = ::fork(); + if (pid == -1) { + const int original_errno = errno; + throw fs::system_error("Cannot fork to execute unmount tool", + original_errno); + } else if (pid == 0) { + const int ret = ::execlp(UMOUNT, "umount", mount_point.c_str(), NULL); + INV(ret == -1); + std::cerr << "Failed to exec " UMOUNT "\n"; + std::exit(EXIT_FAILURE); + } + + int status; +retry: + if (::waitpid(pid, &status, 0) == -1) { + const int original_errno = errno; + if (errno == EINTR) + goto retry; + throw fs::system_error("Failed to wait for unmount subprocess", + original_errno); + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == EXIT_SUCCESS) + return; + else + throw fs::error(F("Failed to unmount %s; returned exit code %s") + % mount_point % WEXITSTATUS(status)); + } else + throw fs::error(F("Failed to unmount %s; unmount tool received signal") + % mount_point); +} + + +/// Stats a file, without following links. +/// +/// \param path The file to stat. +/// +/// \return The stat structure on success. +/// +/// \throw system_error An error on failure. +static struct ::stat +safe_stat(const fs::path& path) +{ + struct ::stat sb; + if (::lstat(path.c_str(), &sb) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Cannot get information about %s") % path, + original_errno); + } + return sb; +} + + +} // anonymous namespace + + +/// Copies a file. +/// +/// \param source The file to copy. +/// \param target The destination of the new copy; must be a file name, not a +/// directory. +/// +/// \throw error If there is a problem copying the file. +void +fs::copy(const fs::path& source, const fs::path& target) +{ + std::ifstream input(source.c_str()); + if (!input) + throw error(F("Cannot open copy source %s") % source); + + std::ofstream output(target.c_str()); + if (!output) + throw error(F("Cannot create copy target %s") % target); + + char buffer[1024]; + while (input.good()) { + input.read(buffer, sizeof(buffer)); + if (input.good() || input.eof()) + output.write(buffer, input.gcount()); + } + if (!input.good() && !input.eof()) + throw error(F("Error while reading input file %s") % source); +} + + +/// Queries the path to the current directory. +/// +/// \return The path to the current directory. +/// +/// \throw fs::error If there is a problem querying the current directory. +fs::path +fs::current_path(void) +{ + char* cwd; +#if defined(HAVE_GETCWD_DYN) + cwd = ::getcwd(NULL, 0); +#else + cwd = ::getcwd(NULL, MAXPATHLEN); +#endif + if (cwd == NULL) { + const int original_errno = errno; + throw fs::system_error(F("Failed to get current working directory"), + original_errno); + } + + try { + const fs::path result(cwd); + std::free(cwd); + return result; + } catch (...) { + std::free(cwd); + throw; + } +} + + +/// Checks if a file exists. +/// +/// Be aware that this is racy in the same way as access(2) is. +/// +/// \param path The file to check the existance of. +/// +/// \return True if the file exists; false otherwise. +bool +fs::exists(const fs::path& path) +{ + return ::access(path.c_str(), F_OK) == 0; +} + + +/// Locates a file in the PATH. +/// +/// \param name The file to locate. +/// +/// \return The path to the located file or none if it was not found. The +/// returned path is always absolute. +optional< fs::path > +fs::find_in_path(const char* name) +{ + const optional< std::string > current_path = utils::getenv("PATH"); + if (!current_path || current_path.get().empty()) + return none; + + std::istringstream path_input(current_path.get() + ":"); + std::string path_component; + while (std::getline(path_input, path_component, ':').good()) { + const fs::path candidate = path_component.empty() ? + fs::path(name) : (fs::path(path_component) / name); + if (exists(candidate)) { + if (candidate.is_absolute()) + return utils::make_optional(candidate); + else + return utils::make_optional(candidate.to_absolute()); + } + } + return none; +} + + +/// Calculates the free space in a given file system. +/// +/// \param path Path to a file in the file system for which to check the free +/// disk space. +/// +/// \return The amount of free space usable by a non-root user. +/// +/// \throw system_error If the call to statfs(2) fails. +utils::units::bytes +fs::free_disk_space(const fs::path& path) +{ +#if defined(HAVE_STATVFS) + struct ::statvfs buf; + if (::statvfs(path.c_str(), &buf) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to stat file system for %s") % path, + original_errno); + } + return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); +#elif defined(HAVE_STATFS) + struct ::statfs buf; + if (::statfs(path.c_str(), &buf) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to stat file system for %s") % path, + original_errno); + } + return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); +#else +# error "Don't know how to query free disk space" +#endif +} + + +/// Checks if the given path is a directory or not. +/// +/// \return True if the path is a directory; false otherwise. +bool +fs::is_directory(const fs::path& path) +{ + const struct ::stat sb = safe_stat(path); + return S_ISDIR(sb.st_mode); +} + + +/// Creates a directory. +/// +/// \param dir The path to the directory to create. +/// \param mode The permissions for the new directory. +/// +/// \throw system_error If the call to mkdir(2) fails. +void +fs::mkdir(const fs::path& dir, const int mode) +{ + if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to create directory %s") % dir, + original_errno); + } +} + + +/// Creates a directory and any missing parents. +/// +/// This is separate from the fs::mkdir function to clearly differentiate the +/// libc wrapper from the more complex algorithm implemented here. +/// +/// \param dir The path to the directory to create. +/// \param mode The permissions for the new directories. +/// +/// \throw system_error If any call to mkdir(2) fails. +void +fs::mkdir_p(const fs::path& dir, const int mode) +{ + try { + fs::mkdir(dir, mode); + } catch (const fs::system_error& e) { + if (e.original_errno() == ENOENT) { + fs::mkdir_p(dir.branch_path(), mode); + fs::mkdir(dir, mode); + } else if (e.original_errno() != EEXIST) + throw e; + } +} + + +/// Creates a temporary directory that is world readable/accessible. +/// +/// The temporary directory is created using mkdtemp(3) using the provided +/// template. This should be most likely used in conjunction with +/// fs::auto_directory. +/// +/// The temporary directory is given read and execute permissions to everyone +/// and thus should not be used to protect data that may be subject to snooping. +/// This goes together with the assumption that this function is used to create +/// temporary directories for test cases, and that those test cases may +/// sometimes be executed as an unprivileged user. In those cases, we need to +/// support two different things: +/// +/// - Allow the unprivileged code to write to files in the work directory by +/// name (e.g. to write the results file, whose name is provided by the +/// monitor code running as root). This requires us to grant search +/// permissions. +/// +/// - Allow the test cases themselves to call getcwd(3) at any point. At least +/// on NetBSD 7.x, getcwd(3) requires both read and search permissions on all +/// path components leading to the current directory. This requires us to +/// grant both read and search permissions. +/// +/// TODO(jmmv): A cleaner way to support this would be for the test executor to +/// create two work directory hierarchies directly rooted under TMPDIR: one for +/// root and one for the unprivileged user. However, that requires more +/// bookkeeping for no real gain, because we are not really trying to protect +/// the data within our temporary directories against attacks. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The generated path for the temporary directory. +/// +/// \throw fs::system_error If the call to mkdtemp(3) fails. +fs::path +fs::mkdtemp_public(const std::string& path_template) +{ + PRE(path_template.find("XXXXXX") != std::string::npos); + + const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); + const fs::path full_template = tmpdir / path_template; + + utils::auto_array< char > buf(new char[full_template.str().length() + 1]); + std::strcpy(buf.get(), full_template.c_str()); + if (::mkdtemp(buf.get()) == NULL) { + const int original_errno = errno; + throw fs::system_error(F("Cannot create temporary directory using " + "template %s") % full_template, + original_errno); + } + const fs::path path(buf.get()); + + if (::chmod(path.c_str(), 0755) == -1) { + const int original_errno = errno; + + try { + rmdir(path); + } catch (const fs::system_error& e) { + // This really should not fail. We just created the directory and + // have not written anything to it so there is no reason for this to + // fail. But better handle the failure just in case. + LW(F("Failed to delete just-created temporary directory %s") + % path); + } + + throw fs::system_error(F("Failed to grant search permissions on " + "temporary directory %s") % path, + original_errno); + } + + return path; +} + + +/// Creates a temporary file. +/// +/// The temporary file is created using mkstemp(3) using the provided template. +/// This should be most likely used in conjunction with fs::auto_file. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The generated path for the temporary directory. +/// +/// \throw fs::system_error If the call to mkstemp(3) fails. +fs::path +fs::mkstemp(const std::string& path_template) +{ + PRE(path_template.find("XXXXXX") != std::string::npos); + + const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); + const fs::path full_template = tmpdir / path_template; + + utils::auto_array< char > buf(new char[full_template.str().length() + 1]); + std::strcpy(buf.get(), full_template.c_str()); + if (::mkstemp(buf.get()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Cannot create temporary file using template " + "%s") % full_template, original_errno); + } + return fs::path(buf.get()); +} + + +/// Mounts a temporary file system with unlimited size. +/// +/// \param in_mount_point The path on which the file system will be mounted. +/// +/// \throw fs::system_error If the attempt to mount process fails. +/// \throw fs::unsupported_operation_error If the code does not know how to +/// mount a temporary file system in the current operating system. +void +fs::mount_tmpfs(const fs::path& in_mount_point) +{ + mount_tmpfs(in_mount_point, units::bytes()); +} + + +/// Mounts a temporary file system. +/// +/// \param in_mount_point The path on which the file system will be mounted. +/// \param size The size of the tmpfs to mount. If 0, use unlimited. +/// +/// \throw fs::system_error If the attempt to mount process fails. +/// \throw fs::unsupported_operation_error If the code does not know how to +/// mount a temporary file system in the current operating system. +void +fs::mount_tmpfs(const fs::path& in_mount_point, const units::bytes& size) +{ + // SunOS's mount(8) requires paths to be absolute. To err on the side of + // caution, let's make the mount point absolute in all cases. + const fs::path mount_point = in_mount_point.is_absolute() ? + in_mount_point : in_mount_point.to_absolute(); + + const pid_t pid = ::fork(); + if (pid == -1) { + const int original_errno = errno; + throw fs::system_error("Cannot fork to execute mount tool", + original_errno); + } + if (pid == 0) + run_mount_tmpfs(mount_point, size); + + int status; +retry: + if (::waitpid(pid, &status, 0) == -1) { + const int original_errno = errno; + if (errno == EINTR) + goto retry; + throw fs::system_error("Failed to wait for mount subprocess", + original_errno); + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == exit_known_error) + throw fs::unsupported_operation_error( + "Don't know how to mount a tmpfs on this operating system"); + else if (WEXITSTATUS(status) == EXIT_SUCCESS) + return; + else + throw fs::error(F("Failed to mount tmpfs on %s; returned exit " + "code %s") % mount_point % WEXITSTATUS(status)); + } else { + throw fs::error(F("Failed to mount tmpfs on %s; mount tool " + "received signal") % mount_point); + } +} + + +/// Recursively removes a directory. +/// +/// This operation simulates a "rm -r". No effort is made to forcibly delete +/// files and no attention is paid to mount points. +/// +/// \param directory The directory to remove. +/// +/// \throw fs::error If there is a problem removing any directory or file. +void +fs::rm_r(const fs::path& directory) +{ + const fs::directory dir(directory); + + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + if (iter->name == "." || iter->name == "..") + continue; + + const fs::path entry = directory / iter->name; + + if (fs::is_directory(entry)) { + LD(F("Descending into %s") % entry); + fs::rm_r(entry); + } else { + LD(F("Removing file %s") % entry); + fs::unlink(entry); + } + } + + LD(F("Removing empty directory %s") % directory); + fs::rmdir(directory); +} + + +/// Removes an empty directory. +/// +/// \param file The directory to remove. +/// +/// \throw fs::system_error If the call to rmdir(2) fails. +void +fs::rmdir(const path& file) +{ + if (::rmdir(file.c_str()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Removal of %s failed") % file, + original_errno); + } +} + + +/// Obtains all the entries in a directory. +/// +/// \param path The directory to scan. +/// +/// \return The set of all directory entries in the given directory. +/// +/// \throw fs::system_error If reading the directory fails for any reason. +std::set< fs::directory_entry > +fs::scan_directory(const fs::path& path) +{ + std::set< fs::directory_entry > contents; + + fs::directory dir(path); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + } + + return contents; +} + + +/// Removes a file. +/// +/// \param file The file to remove. +/// +/// \throw fs::system_error If the call to unlink(2) fails. +void +fs::unlink(const path& file) +{ + if (::unlink(file.c_str()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Removal of %s failed") % file, + original_errno); + } +} + + +/// Unmounts a file system. +/// +/// \param in_mount_point The file system to unmount. +/// +/// \throw fs::error If the unmount fails. +void +fs::unmount(const fs::path& in_mount_point) +{ + // FreeBSD's unmount(2) requires paths to be absolute. To err on the side + // of caution, let's make it absolute in all cases. + const fs::path mount_point = in_mount_point.is_absolute() ? + in_mount_point : in_mount_point.to_absolute(); + + static const int unmount_retries = 3; + static const int unmount_retry_delay_seconds = 1; + + int retries = unmount_retries; +retry: + try { + if (have_unmount2) { + unmount_with_unmount2(mount_point); + } else { + unmount_with_umount8(mount_point); + } + } catch (const fs::system_error& error) { + if (error.original_errno() == EBUSY && retries > 0) { + LW(F("%s busy; unmount retries left %s") % mount_point % retries); + retries--; + ::sleep(unmount_retry_delay_seconds); + goto retry; + } + throw; + } +} diff --git a/utils/fs/operations.hpp b/utils/fs/operations.hpp new file mode 100644 index 000000000000..bd7560ffc048 --- /dev/null +++ b/utils/fs/operations.hpp @@ -0,0 +1,72 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/operations.hpp +/// File system algorithms and access functions. +/// +/// The functions in this module are exception-based, type-improved wrappers +/// over the functions provided by libc. + +#if !defined(UTILS_FS_OPERATIONS_HPP) +#define UTILS_FS_OPERATIONS_HPP + +#include <set> +#include <string> + +#include "utils/fs/directory_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/units_fwd.hpp" + +namespace utils { +namespace fs { + + +void copy(const fs::path&, const fs::path&); +path current_path(void); +bool exists(const fs::path&); +utils::optional< path > find_in_path(const char*); +utils::units::bytes free_disk_space(const fs::path&); +bool is_directory(const fs::path&); +void mkdir(const path&, const int); +void mkdir_p(const path&, const int); +fs::path mkdtemp_public(const std::string&); +fs::path mkstemp(const std::string&); +void mount_tmpfs(const path&); +void mount_tmpfs(const path&, const units::bytes&); +void rm_r(const path&); +void rmdir(const path&); +std::set< directory_entry > scan_directory(const path&); +void unlink(const path&); +void unmount(const path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_OPERATIONS_HPP) diff --git a/utils/fs/operations_test.cpp b/utils/fs/operations_test.cpp new file mode 100644 index 000000000000..f1349351166e --- /dev/null +++ b/utils/fs/operations_test.cpp @@ -0,0 +1,826 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/operations.hpp" + +extern "C" { +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include <dirent.h> +#include <signal.h> +#include <unistd.h> +} + +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <stdexcept> +#include <string> +#include <vector> + +#include <atf-c++.hpp> + +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/stream.hpp" +#include "utils/units.hpp" + +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Checks if a directory entry exists and matches a specific type. +/// +/// \param dir The directory in which to look for the entry. +/// \param name The name of the entry to look up. +/// \param expected_type The expected type of the file as given by dir(5). +/// +/// \return True if the entry exists and matches the given type; false +/// otherwise. +static bool +lookup(const char* dir, const char* name, const unsigned int expected_type) +{ + DIR* dirp = ::opendir(dir); + ATF_REQUIRE(dirp != NULL); + + bool found = false; + struct dirent* dp; + while (!found && (dp = readdir(dirp)) != NULL) { + if (std::strcmp(dp->d_name, name) == 0) { + struct ::stat s; + const fs::path lookup_path = fs::path(dir) / name; + ATF_REQUIRE(::stat(lookup_path.c_str(), &s) != -1); + if ((s.st_mode & S_IFMT) == expected_type) { + found = true; + } + } + } + ::closedir(dirp); + return found; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(copy__ok); +ATF_TEST_CASE_BODY(copy__ok) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + atf::utils::create_file(source.str(), "This is the input"); + fs::copy(source, target); + ATF_REQUIRE(atf::utils::compare_file(target.str(), "This is the input")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(copy__fail_open); +ATF_TEST_CASE_BODY(copy__fail_open) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + ATF_REQUIRE_THROW_RE(fs::error, "Cannot open copy source f1.txt", + fs::copy(source, target)); +} + + +ATF_TEST_CASE(copy__fail_create); +ATF_TEST_CASE_HEAD(copy__fail_create) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(copy__fail_create) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + atf::utils::create_file(target.str(), "Do not override"); + ATF_REQUIRE(::chmod(target.c_str(), 0444) != -1); + + atf::utils::create_file(source.str(), "This is the input"); + ATF_REQUIRE_THROW_RE(fs::error, "Cannot create copy target f2.txt", + fs::copy(source, target)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_path__ok); +ATF_TEST_CASE_BODY(current_path__ok) +{ + const fs::path previous = fs::current_path(); + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(::chdir("root") != -1); + const fs::path cwd = fs::current_path(); + ATF_REQUIRE_EQ(cwd.str().length() - 5, cwd.str().find("/root")); + ATF_REQUIRE_EQ(previous / "root", cwd); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_path__enoent); +ATF_TEST_CASE_BODY(current_path__enoent) +{ + const fs::path previous = fs::current_path(); + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(::chdir("root") != -1); + ATF_REQUIRE(::rmdir("../root") != -1); + try { + (void)fs::current_path(); + fail("system_errpr not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists); +ATF_TEST_CASE_BODY(exists) +{ + const fs::path dir("dir"); + ATF_REQUIRE(!fs::exists(dir)); + fs::mkdir(dir, 0755); + ATF_REQUIRE(fs::exists(dir)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__no_path); +ATF_TEST_CASE_BODY(find_in_path__no_path) +{ + utils::unsetenv("PATH"); + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file("ls", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__empty_path); +ATF_TEST_CASE_BODY(find_in_path__empty_path) +{ + utils::setenv("PATH", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file("ls", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__one_component); +ATF_TEST_CASE_BODY(find_in_path__one_component) +{ + const fs::path dir = fs::current_path() / "bin"; + fs::mkdir(dir, 0755); + utils::setenv("PATH", dir.str()); + + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file((dir / "ls").str(), ""); + ATF_REQUIRE_EQ(dir / "ls", fs::find_in_path("ls").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__many_components); +ATF_TEST_CASE_BODY(find_in_path__many_components) +{ + const fs::path dir1 = fs::current_path() / "dir1"; + const fs::path dir2 = fs::current_path() / "dir2"; + fs::mkdir(dir1, 0755); + fs::mkdir(dir2, 0755); + utils::setenv("PATH", dir1.str() + ":" + dir2.str()); + + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file((dir2 / "ls").str(), ""); + ATF_REQUIRE_EQ(dir2 / "ls", fs::find_in_path("ls").get()); + atf::utils::create_file((dir1 / "ls").str(), ""); + ATF_REQUIRE_EQ(dir1 / "ls", fs::find_in_path("ls").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__current_directory); +ATF_TEST_CASE_BODY(find_in_path__current_directory) +{ + utils::setenv("PATH", "bin:"); + + ATF_REQUIRE(!fs::find_in_path("foo-bar")); + atf::utils::create_file("foo-bar", ""); + ATF_REQUIRE_EQ(fs::path("foo-bar").to_absolute(), + fs::find_in_path("foo-bar").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__always_absolute); +ATF_TEST_CASE_BODY(find_in_path__always_absolute) +{ + fs::mkdir(fs::path("my-bin"), 0755); + utils::setenv("PATH", "my-bin"); + + ATF_REQUIRE(!fs::find_in_path("abcd")); + atf::utils::create_file("my-bin/abcd", ""); + ATF_REQUIRE_EQ(fs::path("my-bin/abcd").to_absolute(), + fs::find_in_path("abcd").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(free_disk_space__ok__smoke); +ATF_TEST_CASE_BODY(free_disk_space__ok__smoke) +{ + const units::bytes space = fs::free_disk_space(fs::path(".")); + ATF_REQUIRE(space > units::MB); // Simple test that should always pass. +} + + +/// Unmounts a directory without raising errors. +/// +/// \param cookie Name of a file that exists while the mount point is still +/// mounted. Used to prevent a double-unmount, which would print a +/// misleading error message. +/// \param mount_point Path to the mount point to unmount. +static void +cleanup_mount_point(const fs::path& cookie, const fs::path& mount_point) +{ + try { + if (fs::exists(cookie)) { + fs::unmount(mount_point); + } + } catch (const std::runtime_error& e) { + std::cerr << "Failed trying to unmount " + mount_point.str() + + " during cleanup: " << e.what() << '\n'; + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(free_disk_space__ok__real); +ATF_TEST_CASE_HEAD(free_disk_space__ok__real) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(free_disk_space__ok__real) +{ + try { + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + fs::mount_tmpfs(mount_point, units::bytes(32 * units::MB)); + atf::utils::create_file("mounted", ""); + const units::bytes space = fs::free_disk_space(fs::path(mount_point)); + fs::unmount(mount_point); + fs::unlink(fs::path("mounted")); + ATF_REQUIRE(space < 35 * units::MB); + ATF_REQUIRE(space > 28 * units::MB); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } +} +ATF_TEST_CASE_CLEANUP(free_disk_space__ok__real) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(free_disk_space__fail); +ATF_TEST_CASE_BODY(free_disk_space__fail) +{ + ATF_REQUIRE_THROW_RE(fs::error, "Failed to stat file system for missing", + fs::free_disk_space(fs::path("missing"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_directory__ok); +ATF_TEST_CASE_BODY(is_directory__ok) +{ + const fs::path file("file"); + atf::utils::create_file(file.str(), ""); + ATF_REQUIRE(!fs::is_directory(file)); + + const fs::path dir("dir"); + fs::mkdir(dir, 0755); + ATF_REQUIRE(fs::is_directory(dir)); +} + + +ATF_TEST_CASE_WITH_CLEANUP(is_directory__fail); +ATF_TEST_CASE_HEAD(is_directory__fail) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(is_directory__fail) +{ + fs::mkdir(fs::path("dir"), 0000); + ATF_REQUIRE_THROW(fs::error, fs::is_directory(fs::path("dir/foo"))); +} +ATF_TEST_CASE_CLEANUP(is_directory__fail) +{ + if (::chmod("dir", 0755) == -1) { + // If we cannot restore the original permissions, we cannot do much + // more. However, leaving an unwritable directory behind will cause the + // runtime engine to report us as broken. + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir__ok); +ATF_TEST_CASE_BODY(mkdir__ok) +{ + fs::mkdir(fs::path("dir"), 0755); + ATF_REQUIRE(lookup(".", "dir", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir__enoent); +ATF_TEST_CASE_BODY(mkdir__enoent) +{ + try { + fs::mkdir(fs::path("dir1/dir2"), 0755); + fail("system_error not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); + } + ATF_REQUIRE(!lookup(".", "dir1", S_IFDIR)); + ATF_REQUIRE(!lookup(".", "dir2", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__one_component); +ATF_TEST_CASE_BODY(mkdir_p__one_component) +{ + ATF_REQUIRE(!lookup(".", "new-dir", S_IFDIR)); + fs::mkdir_p(fs::path("new-dir"), 0755); + ATF_REQUIRE(lookup(".", "new-dir", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__many_components); +ATF_TEST_CASE_BODY(mkdir_p__many_components) +{ + ATF_REQUIRE(!lookup(".", "a", S_IFDIR)); + fs::mkdir_p(fs::path("a/b/c"), 0755); + ATF_REQUIRE(lookup(".", "a", S_IFDIR)); + ATF_REQUIRE(lookup("a", "b", S_IFDIR)); + ATF_REQUIRE(lookup("a/b", "c", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__already_exists); +ATF_TEST_CASE_BODY(mkdir_p__already_exists) +{ + fs::mkdir(fs::path("a"), 0755); + fs::mkdir(fs::path("a/b"), 0755); + fs::mkdir_p(fs::path("a/b"), 0755); +} + + +ATF_TEST_CASE(mkdir_p__eacces) +ATF_TEST_CASE_HEAD(mkdir_p__eacces) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(mkdir_p__eacces) +{ + fs::mkdir(fs::path("a"), 0755); + fs::mkdir(fs::path("a/b"), 0755); + ATF_REQUIRE(::chmod("a/b", 0555) != -1); + try { + fs::mkdir_p(fs::path("a/b/c/d"), 0755); + fail("system_error not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(EACCES, e.original_errno()); + } + ATF_REQUIRE(lookup(".", "a", S_IFDIR)); + ATF_REQUIRE(lookup("a", "b", S_IFDIR)); + ATF_REQUIRE(!lookup(".", "c", S_IFDIR)); + ATF_REQUIRE(!lookup("a", "c", S_IFDIR)); + ATF_REQUIRE(!lookup("a/b", "c", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdtemp_public) +ATF_TEST_CASE_BODY(mkdtemp_public) +{ + const fs::path tmpdir = fs::current_path() / "tmp"; + utils::setenv("TMPDIR", tmpdir.str()); + fs::mkdir(tmpdir, 0755); + + const std::string dir_template("tempdir.XXXXXX"); + const fs::path tempdir = fs::mkdtemp_public(dir_template); + ATF_REQUIRE(!lookup("tmp", dir_template.c_str(), S_IFDIR)); + ATF_REQUIRE(lookup("tmp", tempdir.leaf_name().c_str(), S_IFDIR)); +} + + +ATF_TEST_CASE(mkdtemp_public__getcwd_as_non_root) +ATF_TEST_CASE_HEAD(mkdtemp_public__getcwd_as_non_root) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mkdtemp_public__getcwd_as_non_root) +{ + const std::string dir_template("dir.XXXXXX"); + const fs::path dir = fs::mkdtemp_public(dir_template); + const fs::path subdir = dir / "subdir"; + fs::mkdir(subdir, 0755); + + const uid_t old_euid = ::geteuid(); + const gid_t old_egid = ::getegid(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + ATF_REQUIRE(::setegid(unprivileged_user.gid) != -1); + ATF_REQUIRE(::seteuid(unprivileged_user.uid) != -1); + + // The next code block runs as non-root. We cannot use any ATF macros nor + // functions in it because a failure would cause the test to attempt to + // write to the ATF result file which may not be writable as non-root. + bool failed = false; + { + try { + if (::chdir(subdir.c_str()) == -1) { + std::cerr << "Cannot enter directory\n"; + failed |= true; + } else { + fs::current_path(); + } + } catch (const fs::error& e) { + failed |= true; + std::cerr << "Failed to query current path in: " << subdir << '\n'; + } + + if (::seteuid(old_euid) == -1) { + std::cerr << "Failed to restore euid; cannot continue\n"; + std::abort(); + } + if (::setegid(old_egid) == -1) { + std::cerr << "Failed to restore egid; cannot continue\n"; + std::abort(); + } + } + + if (failed) + fail("Test failed; see stdout for details"); +} + + +ATF_TEST_CASE(mkdtemp_public__search_permissions_as_non_root) +ATF_TEST_CASE_HEAD(mkdtemp_public__search_permissions_as_non_root) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mkdtemp_public__search_permissions_as_non_root) +{ + const std::string dir_template("dir.XXXXXX"); + const fs::path dir = fs::mkdtemp_public(dir_template); + const fs::path cookie = dir / "not-secret"; + atf::utils::create_file(cookie.str(), "this is readable"); + + // We are running as root so there is no reason to assume that our current + // work directory is accessible by non-root. Weaken the permissions so that + // our code below works. + ATF_REQUIRE(::chmod(".", 0755) != -1); + + const uid_t old_euid = ::geteuid(); + const gid_t old_egid = ::getegid(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + ATF_REQUIRE(::setegid(unprivileged_user.gid) != -1); + ATF_REQUIRE(::seteuid(unprivileged_user.uid) != -1); + + // The next code block runs as non-root. We cannot use any ATF macros nor + // functions in it because a failure would cause the test to attempt to + // write to the ATF result file which may not be writable as non-root. + bool failed = false; + { + try { + const std::string contents = utils::read_file(cookie); + std::cerr << "Read contents: " << contents << '\n'; + failed |= (contents != "this is readable"); + } catch (const std::runtime_error& e) { + failed |= true; + std::cerr << "Failed to read " << cookie << '\n'; + } + + if (::seteuid(old_euid) == -1) { + std::cerr << "Failed to restore euid; cannot continue\n"; + std::abort(); + } + if (::setegid(old_egid) == -1) { + std::cerr << "Failed to restore egid; cannot continue\n"; + std::abort(); + } + } + + if (failed) + fail("Test failed; see stdout for details"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkstemp) +ATF_TEST_CASE_BODY(mkstemp) +{ + const fs::path tmpdir = fs::current_path() / "tmp"; + utils::setenv("TMPDIR", tmpdir.str()); + fs::mkdir(tmpdir, 0755); + + const std::string file_template("tempfile.XXXXXX"); + const fs::path tempfile = fs::mkstemp(file_template); + ATF_REQUIRE(!lookup("tmp", file_template.c_str(), S_IFREG)); + ATF_REQUIRE(lookup("tmp", tempfile.leaf_name().c_str(), S_IFREG)); +} + + +static void +test_mount_tmpfs_ok(const units::bytes& size) +{ + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + + try { + atf::utils::create_file("outside", ""); + fs::mount_tmpfs(mount_point, size); + atf::utils::create_file("mounted", ""); + atf::utils::create_file((mount_point / "inside").str(), ""); + + struct ::stat outside, inside; + ATF_REQUIRE(::stat("outside", &outside) != -1); + ATF_REQUIRE(::stat((mount_point / "inside").c_str(), &inside) != -1); + ATF_REQUIRE(outside.st_dev != inside.st_dev); + fs::unmount(mount_point); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(mount_tmpfs__ok__default_size) +ATF_TEST_CASE_HEAD(mount_tmpfs__ok__default_size) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__ok__default_size) +{ + test_mount_tmpfs_ok(units::bytes()); +} +ATF_TEST_CASE_CLEANUP(mount_tmpfs__ok__default_size) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE_WITH_CLEANUP(mount_tmpfs__ok__explicit_size) +ATF_TEST_CASE_HEAD(mount_tmpfs__ok__explicit_size) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__ok__explicit_size) +{ + test_mount_tmpfs_ok(units::bytes(10 * units::MB)); +} +ATF_TEST_CASE_CLEANUP(mount_tmpfs__ok__explicit_size) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE(mount_tmpfs__fail) +ATF_TEST_CASE_HEAD(mount_tmpfs__fail) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__fail) +{ + try { + fs::mount_tmpfs(fs::path("non-existent")); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } catch (const fs::error& e) { + // Expected. + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rm_r__empty); +ATF_TEST_CASE_BODY(rm_r__empty) +{ + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(lookup(".", "root", S_IFDIR)); + fs::rm_r(fs::path("root")); + ATF_REQUIRE(!lookup(".", "root", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rm_r__files_and_directories); +ATF_TEST_CASE_BODY(rm_r__files_and_directories) +{ + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/.hidden_file", ""); + fs::mkdir(fs::path("root/.hidden_dir"), 0755); + atf::utils::create_file("root/.hidden_dir/a", ""); + atf::utils::create_file("root/file", ""); + atf::utils::create_file("root/with spaces", ""); + fs::mkdir(fs::path("root/dir1"), 0755); + fs::mkdir(fs::path("root/dir1/dir2"), 0755); + atf::utils::create_file("root/dir1/dir2/file", ""); + fs::mkdir(fs::path("root/dir1/dir3"), 0755); + ATF_REQUIRE(lookup(".", "root", S_IFDIR)); + fs::rm_r(fs::path("root")); + ATF_REQUIRE(!lookup(".", "root", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rmdir__ok) +ATF_TEST_CASE_BODY(rmdir__ok) +{ + ATF_REQUIRE(::mkdir("foo", 0755) != -1); + ATF_REQUIRE(::access("foo", X_OK) == 0); + fs::rmdir(fs::path("foo")); + ATF_REQUIRE(::access("foo", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rmdir__fail) +ATF_TEST_CASE_BODY(rmdir__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "Removal of foo failed", + fs::rmdir(fs::path("foo"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scan_directory__ok) +ATF_TEST_CASE_BODY(scan_directory__ok) +{ + fs::mkdir(fs::path("dir"), 0755); + atf::utils::create_file("dir/foo", ""); + atf::utils::create_file("dir/.hidden", ""); + + const std::set< fs::directory_entry > contents = fs::scan_directory( + fs::path("dir")); + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + exp_contents.insert(fs::directory_entry(".hidden")); + exp_contents.insert(fs::directory_entry("foo")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scan_directory__fail) +ATF_TEST_CASE_BODY(scan_directory__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "opendir(.*missing.*) failed", + fs::scan_directory(fs::path("missing"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlink__ok) +ATF_TEST_CASE_BODY(unlink__ok) +{ + atf::utils::create_file("foo", ""); + ATF_REQUIRE(::access("foo", R_OK) == 0); + fs::unlink(fs::path("foo")); + ATF_REQUIRE(::access("foo", R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlink__fail) +ATF_TEST_CASE_BODY(unlink__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "Removal of foo failed", + fs::unlink(fs::path("foo"))); +} + + +ATF_TEST_CASE(unmount__ok) +ATF_TEST_CASE_HEAD(unmount__ok) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(unmount__ok) +{ + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + + atf::utils::create_file((mount_point / "test1").str(), ""); + try { + fs::mount_tmpfs(mount_point); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } + + atf::utils::create_file((mount_point / "test2").str(), ""); + + ATF_REQUIRE(!fs::exists(mount_point / "test1")); + ATF_REQUIRE( fs::exists(mount_point / "test2")); + fs::unmount(mount_point); + ATF_REQUIRE( fs::exists(mount_point / "test1")); + ATF_REQUIRE(!fs::exists(mount_point / "test2")); +} + + +ATF_TEST_CASE(unmount__fail) +ATF_TEST_CASE_HEAD(unmount__fail) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(unmount__fail) +{ + ATF_REQUIRE_THROW(fs::error, fs::unmount(fs::path("non-existent"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, copy__ok); + ATF_ADD_TEST_CASE(tcs, copy__fail_open); + ATF_ADD_TEST_CASE(tcs, copy__fail_create); + + ATF_ADD_TEST_CASE(tcs, current_path__ok); + ATF_ADD_TEST_CASE(tcs, current_path__enoent); + + ATF_ADD_TEST_CASE(tcs, exists); + + ATF_ADD_TEST_CASE(tcs, find_in_path__no_path); + ATF_ADD_TEST_CASE(tcs, find_in_path__empty_path); + ATF_ADD_TEST_CASE(tcs, find_in_path__one_component); + ATF_ADD_TEST_CASE(tcs, find_in_path__many_components); + ATF_ADD_TEST_CASE(tcs, find_in_path__current_directory); + ATF_ADD_TEST_CASE(tcs, find_in_path__always_absolute); + + ATF_ADD_TEST_CASE(tcs, free_disk_space__ok__smoke); + ATF_ADD_TEST_CASE(tcs, free_disk_space__ok__real); + ATF_ADD_TEST_CASE(tcs, free_disk_space__fail); + + ATF_ADD_TEST_CASE(tcs, is_directory__ok); + ATF_ADD_TEST_CASE(tcs, is_directory__fail); + + ATF_ADD_TEST_CASE(tcs, mkdir__ok); + ATF_ADD_TEST_CASE(tcs, mkdir__enoent); + + ATF_ADD_TEST_CASE(tcs, mkdir_p__one_component); + ATF_ADD_TEST_CASE(tcs, mkdir_p__many_components); + ATF_ADD_TEST_CASE(tcs, mkdir_p__already_exists); + ATF_ADD_TEST_CASE(tcs, mkdir_p__eacces); + + ATF_ADD_TEST_CASE(tcs, mkdtemp_public); + ATF_ADD_TEST_CASE(tcs, mkdtemp_public__getcwd_as_non_root); + ATF_ADD_TEST_CASE(tcs, mkdtemp_public__search_permissions_as_non_root); + + ATF_ADD_TEST_CASE(tcs, mkstemp); + + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__ok__default_size); + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__ok__explicit_size); + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__fail); + + ATF_ADD_TEST_CASE(tcs, rm_r__empty); + ATF_ADD_TEST_CASE(tcs, rm_r__files_and_directories); + + ATF_ADD_TEST_CASE(tcs, rmdir__ok); + ATF_ADD_TEST_CASE(tcs, rmdir__fail); + + ATF_ADD_TEST_CASE(tcs, scan_directory__ok); + ATF_ADD_TEST_CASE(tcs, scan_directory__fail); + + ATF_ADD_TEST_CASE(tcs, unlink__ok); + ATF_ADD_TEST_CASE(tcs, unlink__fail); + + ATF_ADD_TEST_CASE(tcs, unmount__ok); + ATF_ADD_TEST_CASE(tcs, unmount__fail); +} diff --git a/utils/fs/path.cpp b/utils/fs/path.cpp new file mode 100644 index 000000000000..465ed49c4c2a --- /dev/null +++ b/utils/fs/path.cpp @@ -0,0 +1,303 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/path.hpp" + +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Normalizes an input string to a valid path. +/// +/// A normalized path cannot have empty components; i.e. there can be at most +/// one consecutive separator (/). +/// +/// \param in The string to normalize. +/// +/// \return The normalized string, representing a path. +/// +/// \throw utils::fs::invalid_path_error If the path is empty. +static std::string +normalize(const std::string& in) +{ + if (in.empty()) + throw fs::invalid_path_error(in, "Cannot be 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; +} + + +} // anonymous namespace + + +/// Creates a new path object from a textual representation of a path. +/// +/// \param text A valid representation of a path in textual form. +/// +/// \throw utils::fs::invalid_path_error If the input text does not represent a +/// valid path. +fs::path::path(const std::string& text) : + _repr(normalize(text)) +{ +} + + +/// Gets a view of the path as an array of characters. +/// +/// \return A \code const char* \endcode representation for the object. +const char* +fs::path::c_str(void) const +{ + return _repr.c_str(); +} + + +/// Gets a view of the path as a std::string. +/// +/// \return A \code std::string& \endcode representation for the object. +const std::string& +fs::path::str(void) const +{ + return _repr; +} + + +/// Gets the branch path (directory name) of the path. +/// +/// The branch path of a path with just one component (no separators) is ".". +/// +/// \return A new path representing the branch path. +fs::path +fs::path::branch_path(void) const +{ + const std::string::size_type end_pos = _repr.rfind('/'); + if (end_pos == std::string::npos) + return fs::path("."); + else if (end_pos == 0) + return fs::path("/"); + else + return fs::path(_repr.substr(0, end_pos)); +} + + +/// Gets the leaf name (base name) of the path. +/// +/// \return A new string representing the leaf name. +std::string +fs::path::leaf_name(void) const +{ + const std::string::size_type beg_pos = _repr.rfind('/'); + + if (beg_pos == std::string::npos) + return _repr; + else + return _repr.substr(beg_pos + 1); +} + + +/// Converts a relative path in the current directory to an absolute path. +/// +/// \pre The path is relative. +/// +/// \return The absolute representation of the relative path. +fs::path +fs::path::to_absolute(void) const +{ + PRE(!is_absolute()); + return fs::current_path() / *this; +} + + +/// \return True if the representation of the path is absolute. +bool +fs::path::is_absolute(void) const +{ + return _repr[0] == '/'; +} + + +/// Checks whether the path is a parent of another path. +/// +/// A path is considered to be a parent of itself. +/// +/// \return True if this path is a parent of p. +bool +fs::path::is_parent_of(path p) const +{ + do { + if ((*this) == p) + return true; + p = p.branch_path(); + } while (p != fs::path(".") && p != fs::path("/")); + return false; +} + + +/// Counts the number of components in the path. +/// +/// \return The number of components. +int +fs::path::ncomponents(void) const +{ + int count = 0; + if (_repr == "/") + return 1; + else { + for (std::string::const_iterator iter = _repr.begin(); + iter != _repr.end(); ++iter) { + if (*iter == '/') + count++; + } + return count + 1; + } +} + + +/// Less-than comparator for paths. +/// +/// This is provided to make identifiers useful as map keys. +/// +/// \param p The path to compare to. +/// +/// \return True if this identifier sorts before the other identifier; false +/// otherwise. +bool +fs::path::operator<(const fs::path& p) const +{ + return _repr < p._repr; +} + + +/// Compares two paths for equality. +/// +/// Given that the paths are internally normalized, input paths such as +/// ///foo/bar and /foo///bar are exactly the same. However, this does NOT +/// check for true equality: i.e. this does not access the file system to check +/// if the paths actually point to the same object my means of links. +/// +/// \param p The path to compare to. +/// +/// \returns A boolean indicating whether the paths are equal. +bool +fs::path::operator==(const fs::path& p) const +{ + return _repr == p._repr; +} + + +/// Compares two paths for inequality. +/// +/// See the description of operator==() for more details on the comparison +/// performed. +/// +/// \param p The path to compare to. +/// +/// \returns A boolean indicating whether the paths are different. +bool +fs::path::operator!=(const fs::path& p) const +{ + return _repr != p._repr; +} + + +/// Concatenates this path with one or more components. +/// +/// \param components The new components to concatenate to the path. These are +/// normalized because, in general, they may come from user input. These +/// components cannot represent an absolute path. +/// +/// \return A new path containing the concatenation of this path and the +/// provided components. +/// +/// \throw utils::fs::invalid_path_error If components does not represent a +/// valid path. +/// \throw utils::fs::join_error If the join operation is invalid because the +/// two paths are incompatible. +fs::path +fs::path::operator/(const std::string& components) const +{ + return (*this) / fs::path(components); +} + + +/// Concatenates this path with another path. +/// +/// \param rest The path to concatenate to this one. Cannot be absolute. +/// +/// \return A new path containing the concatenation of this path and the other +/// path. +/// +/// \throw utils::fs::join_error If the join operation is invalid because the +/// two paths are incompatible. +fs::path +fs::path::operator/(const fs::path& rest) const +{ + if (rest.is_absolute()) + throw fs::join_error(_repr, rest._repr, + "Cannot concatenate a path to an absolute path"); + return fs::path(_repr + '/' + rest._repr); +} + + +/// Formats a path for insertion on a stream. +/// +/// \param os The output stream. +/// \param p The path to inject to the stream. +/// +/// \return The output stream os. +std::ostream& +fs::operator<<(std::ostream& os, const fs::path& p) +{ + return (os << p.str()); +} diff --git a/utils/fs/path.hpp b/utils/fs/path.hpp new file mode 100644 index 000000000000..fe55fd55f234 --- /dev/null +++ b/utils/fs/path.hpp @@ -0,0 +1,87 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/path.hpp +/// Provides the utils::fs::path class. +/// +/// This is a poor man's reimplementation of the path class provided by +/// Boost.Filesystem, in the sense that it tries to follow the same API but is +/// much simplified. + +#if !defined(UTILS_FS_PATH_HPP) +#define UTILS_FS_PATH_HPP + +#include "utils/fs/path_fwd.hpp" + +#include <string> +#include <ostream> + +namespace utils { +namespace fs { + + +/// Representation and manipulation of a file system path. +/// +/// Application code should always use this class to represent a path instead of +/// std::string, because this class is more semantically representative, ensures +/// that the values are valid and provides some useful manipulation functions. +/// +/// Conversions to and from strings are always explicit. +class path { + /// Internal representation of the path. + std::string _repr; + +public: + explicit path(const std::string&); + + const char* c_str(void) const; + const std::string& str(void) const; + + path branch_path(void) const; + std::string leaf_name(void) const; + path to_absolute(void) const; + + bool is_absolute(void) const; + bool is_parent_of(path) const; + int ncomponents(void) const; + + bool operator<(const path&) const; + bool operator==(const path&) const; + bool operator!=(const path&) const; + path operator/(const std::string&) const; + path operator/(const path&) const; +}; + + +std::ostream& operator<<(std::ostream&, const path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_PATH_HPP) diff --git a/utils/fs/path_fwd.hpp b/utils/fs/path_fwd.hpp new file mode 100644 index 000000000000..4e6856073553 --- /dev/null +++ b/utils/fs/path_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +/// \file utils/fs/path_fwd.hpp +/// Forward declarations for utils/fs/path.hpp + +#if !defined(UTILS_FS_PATH_FWD_HPP) +#define UTILS_FS_PATH_FWD_HPP + +namespace utils { +namespace fs { + + +class path; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_PATH_FWD_HPP) diff --git a/utils/fs/path_test.cpp b/utils/fs/path_test.cpp new file mode 100644 index 000000000000..30ad3110de31 --- /dev/null +++ b/utils/fs/path_test.cpp @@ -0,0 +1,277 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT +// OWNER 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. + +#include "utils/fs/path.hpp" + +extern "C" { +#include <unistd.h> +} + +#include <set> + +#include <atf-c++.hpp> + +#include "utils/fs/exceptions.hpp" + +using utils::fs::invalid_path_error; +using utils::fs::join_error; +using utils::fs::path; + + +#define REQUIRE_JOIN_ERROR(path1, path2, expr) \ + try { \ + expr; \ + ATF_FAIL("Expecting join_error but no error raised"); \ + } catch (const join_error& e) { \ + ATF_REQUIRE_EQ(path1, e.textual_path1()); \ + ATF_REQUIRE_EQ(path2, e.textual_path2()); \ + } + + +ATF_TEST_CASE_WITHOUT_HEAD(normalize__ok); +ATF_TEST_CASE_BODY(normalize__ok) +{ + ATF_REQUIRE_EQ(".", path(".").str()); + ATF_REQUIRE_EQ("..", path("..").str()); + ATF_REQUIRE_EQ("/", path("/").str()); + ATF_REQUIRE_EQ("/", path("///").str()); + + ATF_REQUIRE_EQ("foo", path("foo").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/bar").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/bar/").str()); + + ATF_REQUIRE_EQ("/foo", path("/foo").str()); + ATF_REQUIRE_EQ("/foo/bar", path("/foo/bar").str()); + ATF_REQUIRE_EQ("/foo/bar", path("/foo/bar/").str()); + + ATF_REQUIRE_EQ("/foo", path("///foo").str()); + ATF_REQUIRE_EQ("/foo/bar", path("///foo///bar").str()); + ATF_REQUIRE_EQ("/foo/bar", path("///foo///bar///").str()); + + ATF_REQUIRE_EQ("./foo/bar", path("./foo/bar").str()); + ATF_REQUIRE_EQ("./foo/bar", path("./foo/./bar").str()); + ATF_REQUIRE_EQ("./foo/bar", path("././foo/./bar").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/././bar").str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(normalize__invalid); +ATF_TEST_CASE_BODY(normalize__invalid) +{ + try { + path(""); + fail("invalid_path_error not raised"); + } catch (const invalid_path_error& e) { + ATF_REQUIRE(e.invalid_path().empty()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute); +ATF_TEST_CASE_BODY(is_absolute) +{ + ATF_REQUIRE( path("/").is_absolute()); + ATF_REQUIRE( path("////").is_absolute()); + ATF_REQUIRE( path("////a").is_absolute()); + ATF_REQUIRE( path("//a//").is_absolute()); + ATF_REQUIRE(!path("a////").is_absolute()); + ATF_REQUIRE(!path("../foo").is_absolute()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_parent_of); +ATF_TEST_CASE_BODY(is_parent_of) +{ + ATF_REQUIRE( path("/").is_parent_of(path("/"))); + ATF_REQUIRE( path(".").is_parent_of(path("."))); + ATF_REQUIRE( path("/a").is_parent_of(path("/a"))); + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE( path("a").is_parent_of(path("a"))); + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c"))); + + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c/d"))); + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c/d/e"))); + ATF_REQUIRE(!path("/a/b/c").is_parent_of(path("a/b/c"))); + ATF_REQUIRE(!path("/a/b/c").is_parent_of(path("a/b/c/d/e"))); + + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c/d"))); + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c/d/e"))); + ATF_REQUIRE(!path("a/b/c").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("a/b/c").is_parent_of(path("/a/b/c/d/e"))); + + ATF_REQUIRE(!path("/a/b/c/d/e").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("/a/b/c/d/e").is_parent_of(path("a/b/c"))); + ATF_REQUIRE(!path("a/b/c/d/e").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("a/b/c/d/e").is_parent_of(path("a/b/c"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ncomponents); +ATF_TEST_CASE_BODY(ncomponents) +{ + ATF_REQUIRE_EQ(1, path(".").ncomponents()); + ATF_REQUIRE_EQ(1, path("/").ncomponents()); + + ATF_REQUIRE_EQ(1, path("abc").ncomponents()); + ATF_REQUIRE_EQ(1, path("abc/").ncomponents()); + + ATF_REQUIRE_EQ(2, path("/abc").ncomponents()); + ATF_REQUIRE_EQ(3, path("/abc/def").ncomponents()); + + ATF_REQUIRE_EQ(2, path("abc/def").ncomponents()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(branch_path); +ATF_TEST_CASE_BODY(branch_path) +{ + ATF_REQUIRE_EQ(".", path(".").branch_path().str()); + ATF_REQUIRE_EQ(".", path("foo").branch_path().str()); + ATF_REQUIRE_EQ("foo", path("foo/bar").branch_path().str()); + ATF_REQUIRE_EQ("/", path("/foo").branch_path().str()); + ATF_REQUIRE_EQ("/foo", path("/foo/bar").branch_path().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(leaf_name); +ATF_TEST_CASE_BODY(leaf_name) +{ + ATF_REQUIRE_EQ(".", path(".").leaf_name()); + ATF_REQUIRE_EQ("foo", path("foo").leaf_name()); + ATF_REQUIRE_EQ("bar", path("foo/bar").leaf_name()); + ATF_REQUIRE_EQ("foo", path("/foo").leaf_name()); + ATF_REQUIRE_EQ("bar", path("/foo/bar").leaf_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_absolute); +ATF_TEST_CASE_BODY(to_absolute) +{ + ATF_REQUIRE(::chdir("/bin") != -1); + const std::string absolute = path("ls").to_absolute().str(); + // In some systems (e.g. in Fedora 17), /bin is really a symlink to + // /usr/bin. Doing an explicit match of 'absolute' to /bin/ls fails in such + // case. Instead, attempt doing a search in the generated path just for a + // substring containing '/bin/ls'. Note that this can still fail if /bin is + // linked to something arbitrary like /a/b... but let's just assume this + // does not happen. + ATF_REQUIRE(absolute.find("/bin/ls") != std::string::npos); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_less_than); +ATF_TEST_CASE_BODY(compare_less_than) +{ + ATF_REQUIRE(!(path("/") < path("/"))); + ATF_REQUIRE(!(path("/") < path("///"))); + + ATF_REQUIRE(!(path("/a/b/c") < path("/a/b/c"))); + + ATF_REQUIRE( path("/a") < path("/b")); + ATF_REQUIRE(!(path("/b") < path("/a"))); + + ATF_REQUIRE( path("/a") < path("/aa")); + ATF_REQUIRE(!(path("/aa") < path("/a"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_equal); +ATF_TEST_CASE_BODY(compare_equal) +{ + ATF_REQUIRE(path("/") == path("///")); + ATF_REQUIRE(path("/a") == path("///a")); + ATF_REQUIRE(path("/a") == path("///a///")); + + ATF_REQUIRE(path("a/b/c") == path("a//b//c")); + ATF_REQUIRE(path("a/b/c") == path("a//b//c///")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_different); +ATF_TEST_CASE_BODY(compare_different) +{ + ATF_REQUIRE(path("/") != path("//a/")); + ATF_REQUIRE(path("/a") != path("a///")); + + ATF_REQUIRE(path("a/b/c") != path("a/b")); + ATF_REQUIRE(path("a/b/c") != path("a//b")); + ATF_REQUIRE(path("a/b/c") != path("/a/b/c")); + ATF_REQUIRE(path("a/b/c") != path("/a//b//c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(concat__to_string); +ATF_TEST_CASE_BODY(concat__to_string) +{ + ATF_REQUIRE_EQ("foo/bar", (path("foo") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar", (path("foo/") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar/baz", (path("foo/") / "bar//baz///").str()); + + ATF_REQUIRE_THROW(invalid_path_error, path("foo") / ""); + REQUIRE_JOIN_ERROR("foo", "/a/b", path("foo") / "/a/b"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(concat__to_path); +ATF_TEST_CASE_BODY(concat__to_path) +{ + ATF_REQUIRE_EQ("foo/bar", (path("foo") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar", (path("foo/") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar/baz", (path("foo/") / "bar//baz///").str()); + + REQUIRE_JOIN_ERROR("foo", "/a/b", path("foo") / path("/a/b")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(use_as_key); +ATF_TEST_CASE_BODY(use_as_key) +{ + std::set< path > paths; + paths.insert(path("/a")); + ATF_REQUIRE(paths.find(path("//a")) != paths.end()); + ATF_REQUIRE(paths.find(path("a")) == paths.end()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, normalize__ok); + ATF_ADD_TEST_CASE(tcs, normalize__invalid); + ATF_ADD_TEST_CASE(tcs, is_absolute); + ATF_ADD_TEST_CASE(tcs, is_parent_of); + ATF_ADD_TEST_CASE(tcs, ncomponents); + ATF_ADD_TEST_CASE(tcs, branch_path); + ATF_ADD_TEST_CASE(tcs, leaf_name); + ATF_ADD_TEST_CASE(tcs, to_absolute); + ATF_ADD_TEST_CASE(tcs, compare_less_than); + ATF_ADD_TEST_CASE(tcs, compare_equal); + ATF_ADD_TEST_CASE(tcs, compare_different); + ATF_ADD_TEST_CASE(tcs, concat__to_string); + ATF_ADD_TEST_CASE(tcs, concat__to_path); + ATF_ADD_TEST_CASE(tcs, use_as_key); +} |