summaryrefslogtreecommitdiff
path: root/utils/fs
diff options
context:
space:
mode:
authorBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
committerBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
commit08334c51dbb99d9ecd2bb86a2d94ed06da9e167a (patch)
treec43eb24d59bd5c963583a5190caef80fc8387322 /utils/fs
Notes
Diffstat (limited to 'utils/fs')
-rw-r--r--utils/fs/Kyuafile10
-rw-r--r--utils/fs/Makefile.am.inc84
-rw-r--r--utils/fs/auto_cleaners.cpp261
-rw-r--r--utils/fs/auto_cleaners.hpp89
-rw-r--r--utils/fs/auto_cleaners_fwd.hpp46
-rw-r--r--utils/fs/auto_cleaners_test.cpp167
-rw-r--r--utils/fs/directory.cpp360
-rw-r--r--utils/fs/directory.hpp120
-rw-r--r--utils/fs/directory_fwd.hpp55
-rw-r--r--utils/fs/directory_test.cpp190
-rw-r--r--utils/fs/exceptions.cpp162
-rw-r--r--utils/fs/exceptions.hpp110
-rw-r--r--utils/fs/exceptions_test.cpp95
-rw-r--r--utils/fs/lua_module.cpp340
-rw-r--r--utils/fs/lua_module.hpp54
-rw-r--r--utils/fs/lua_module_test.cpp376
-rw-r--r--utils/fs/operations.cpp803
-rw-r--r--utils/fs/operations.hpp72
-rw-r--r--utils/fs/operations_test.cpp826
-rw-r--r--utils/fs/path.cpp303
-rw-r--r--utils/fs/path.hpp87
-rw-r--r--utils/fs/path_fwd.hpp45
-rw-r--r--utils/fs/path_test.cpp277
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);
+}