Add a posix-native copy_file.
* Add a posix-native copy_file.
This PR is part of the std::filesystem removal-on-POSIX project.
* Reduce syscalls by 1, as suggested by Nicole.
* Add back masked_options.
* add todos
* Fix handling of mode bits and comment about not handing the oddball symlink case.
MODIFY
src/vcpkg-test/files.cpp
src/vcpkg-test/files.cpp
#include <vcpkg-test/util.h>
+#if !defined(_WIN32)
+#include <sys/stat.h>
+#endif // ^^^ !_WIN32
+
using namespace vcpkg;
using Test::base_temporary_directory;
@@ -703,6 +707,86 @@ TEST_CASE ("get_regular_files_non_recursive_symlinks", "[files]")
});
}
+TEST_CASE ("copy_file", "[files]")
+{
+ urbg_t urbg;
+
+ auto& fs = setup();
+
+ auto temp_dir = base_temporary_directory() / get_random_filename(urbg);
+ INFO("temp dir is: " << temp_dir.native());
+
+ fs.create_directory(temp_dir, VCPKG_LINE_INFO);
+ const auto existing_from = temp_dir / "a";
+ constexpr StringLiteral existing_from_contents = "hello there";
+ fs.write_contents(existing_from, existing_from_contents, VCPKG_LINE_INFO);
+
+ const auto existing_to = temp_dir / "already_existing";
+ constexpr StringLiteral existing_to_contents = "already existing file";
+ fs.write_contents(existing_to, existing_to_contents, VCPKG_LINE_INFO);
+
+ std::error_code ec;
+
+ // N4861 [fs.op.copy.file]/4.1:
+ // "report an error [...] if ..."
+ //
+ // is_regular_file(from) is false
+ REQUIRE(!fs.copy_file(temp_dir, temp_dir / "b", CopyOptions::overwrite_existing, ec));
+ REQUIRE(ec);
+ REQUIRE(!fs.copy_file(temp_dir / "nonexistent", temp_dir / "b", CopyOptions::overwrite_existing, ec));
+ REQUIRE(ec);
+
+ // exists(to) is true and is_regular_file(to) is false
+ fs.create_directory(temp_dir / "a_directory", VCPKG_LINE_INFO);
+ REQUIRE(!fs.copy_file(existing_from, temp_dir / "a_directory", CopyOptions::overwrite_existing, ec));
+ REQUIRE(ec);
+
+ // exists(to) is true and equivalent(from, true) is true
+ REQUIRE(!fs.copy_file(existing_from, temp_dir / "a/../a", CopyOptions::overwrite_existing, ec));
+ REQUIRE(ec);
+
+ // exists(to) is true and [neither skip_existing nor overwrite_existing]
+ REQUIRE(!fs.copy_file(existing_from, existing_to, CopyOptions::none, ec));
+ REQUIRE(ec);
+
+ // Otherwise, copy the contents and attributes of the file from resolves to to the file
+ // to resolves to, if
+
+ // exists(to) is false
+ REQUIRE(fs.copy_file(existing_from, temp_dir / "b", CopyOptions::none, ec));
+ REQUIRE(!ec);
+ REQUIRE(fs.read_contents(temp_dir / "b", VCPKG_LINE_INFO) == existing_from_contents);
+
+ // [skip_existing]
+ REQUIRE(!fs.copy_file(existing_from, existing_to, CopyOptions::skip_existing, ec));
+ REQUIRE(!ec);
+ REQUIRE(fs.read_contents(existing_to, VCPKG_LINE_INFO) == existing_to_contents);
+
+ // [overwrite_existing]
+ REQUIRE(fs.copy_file(existing_from, existing_to, CopyOptions::overwrite_existing, ec));
+ REQUIRE(!ec);
+ REQUIRE(fs.read_contents(existing_to, VCPKG_LINE_INFO) == existing_from_contents);
+
+#if !defined(_WIN32)
+ // Also check that mode bits are copied
+ REQUIRE(::chmod(existing_from.c_str(), 0555) == 0); // note: not writable
+ const auto attributes_target = temp_dir / "attributes_target";
+ fs.copy_file(existing_from, attributes_target, CopyOptions::none, VCPKG_LINE_INFO);
+ REQUIRE(fs.read_contents(attributes_target, VCPKG_LINE_INFO) == existing_from_contents);
+ struct stat copied_attributes_stat;
+ REQUIRE(::stat(attributes_target.c_str(), &copied_attributes_stat) == 0);
+ const auto actual_mode = copied_attributes_stat.st_mode & 0777;
+ REQUIRE(actual_mode == 0555);
+#endif // ^^^ !_WIN32
+
+ Path fp;
+ fs.remove_all(temp_dir, ec, fp);
+ CHECK_EC_ON_FILE(fp, ec);
+
+ REQUIRE_FALSE(fs.exists(temp_dir, ec));
+ CHECK_EC_ON_FILE(temp_dir, ec);
+}
+
TEST_CASE ("LinesCollector", "[files]")
{
using Strings::LinesCollector;

MODIFY
src/vcpkg/base/files.cpp
src/vcpkg/base/files.cpp
@@ -616,6 +616,30 @@ namespace
}
}
+ void ftruncate(off_t length, std::error_code& ec) const noexcept
+ {
+ if (::ftruncate(fd, length) == 0)
+ {
+ ec.clear();
+ }
+ else
+ {
+ ec.assign(errno, std::generic_category());
+ }
+ }
+
+ void fchmod(mode_t new_mode, std::error_code& ec) const noexcept
+ {
+ if (::fchmod(fd, new_mode) == 0)
+ {
+ ec.clear();
+ }
+ else
+ {
+ ec.assign(errno, std::generic_category());
+ }
+ }
+
operator bool() const noexcept { return fd >= 0; }
int get() const noexcept { return fd; }
@@ -2290,67 +2314,24 @@ namespace vcpkg
#if !defined(_WIN32)
if (ec)
{
- auto dst = new_path;
- dst += temp_suffix;
-
- PosixFd i_fd{old_path.c_str(), O_RDONLY, ec};
- if (ec) return;
-
- PosixFd o_fd{dst.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0664, ec};
- if (ec) return;
-
-#if defined(__linux__)
- off_t bytes = 0;
- struct stat info = {0};
- i_fd.fstat(&info, ec);
- if (ec) return;
- auto written_bytes = sendfile(o_fd.get(), i_fd.get(), &bytes, info.st_size);
-#elif defined(__APPLE__)
- auto written_bytes = fcopyfile(i_fd.get(), o_fd.get(), 0, COPYFILE_ALL);
-#else // ^^^ defined(__APPLE__) // !(defined(__APPLE__) || defined(__linux__)) vvv
- ssize_t written_bytes = 0;
- {
- constexpr std::size_t buffer_length = 4096;
- unsigned char buffer[buffer_length];
- while (auto read_bytes = i_fd.read(buffer, buffer_length))
- {
- if (read_bytes == -1)
- {
- written_bytes = -1;
- break;
- }
- auto remaining = read_bytes;
- while (remaining > 0)
- {
- auto read_result = o_fd.write(buffer, remaining);
- if (read_result == -1)
+ auto dst = new_path + temp_suffix;
+ this->copy_file(old_path, dst, CopyOptions::overwrite_existing, ec);
+ if (ec)
{
- written_bytes = -1;
- // break two loops
- goto copy_failure;
- }
- remaining -= read_result;
- }
+ return;
}
- copy_failure:;
- }
-#endif // ^^^ !(defined(__APPLE__) || defined(__linux__))
- if (written_bytes == -1)
+ this->rename(dst, new_path, ec);
+ if (ec)
{
- ec.assign(errno, std::generic_category());
return;
}
- i_fd.close();
- o_fd.close();
-
- this->rename(dst, new_path, ec);
- if (ec) return;
this->remove(old_path, ec);
}
#endif // ^^^ !defined(_WIN32)
}
+
virtual bool remove(const Path& target, std::error_code& ec) override
{
#if defined(_WIN32)
@@ -2605,9 +2586,129 @@ namespace vcpkg
CopyOptions options,
std::error_code& ec) override
{
+#if defined(_WIN32)
return stdfs::copy_file(
to_stdfs_path(source), to_stdfs_path(destination), convert_copy_options(options), ec);
+#else // ^^^ _WIN32 // !_WIN32 vvv
+ PosixFd source_fd{source.c_str(), O_RDONLY, ec};
+ if (ec)
+ {
+ return false;
+ }
+
+ struct stat source_stat;
+ source_fd.fstat(&source_stat, ec);
+ if (ec)
+ {
+ return false;
+ }
+
+ if (!S_ISREG(source_stat.st_mode))
+ {
+ // N4861 [fs.op.copy.file]/4: "report an error if"
+ // is_regular_file(from) is false
+ ec = std::make_error_code(std::errc::invalid_argument);
+ return false;
+ }
+
+ auto masked_options =
+ static_cast<CopyOptions>(static_cast<int>(options) & static_cast<int>(CopyOptions::existing_mask));
+ int open_options = O_WRONLY | O_CREAT;
+ if (masked_options != CopyOptions::overwrite_existing)
+ {
+ // the standard wording suggests that we should create a file through a broken symlink which would
+ // forbid use of O_EXCL. However, implementations like boost::copy_file don't do this and doing it
+ // would introduce more potential for TOCTOU bugs
+ open_options |= O_EXCL;
+ }
+
+ const mode_t open_mode = source_stat.st_mode | 0222; // ensure the file is created writable
+ PosixFd destination_fd{destination.c_str(), open_options, open_mode, ec};
+ if (ec)
+ {
+ if (masked_options == CopyOptions::skip_existing && ec == std::errc::file_exists)
+ {
+ ec.clear();
+ }
+
+ return false;
+ }
+
+ struct stat destination_stat;
+ destination_fd.fstat(&destination_stat, ec);
+ if (ec)
+ {
+ return false;
+ }
+ else if (!S_ISREG(destination_stat.st_mode))
+ {
+ ec = std::make_error_code(std::errc::invalid_argument);
+ return false;
+ }
+
+ if (source_stat.st_dev == destination_stat.st_dev && source_stat.st_ino == destination_stat.st_ino)
+ {
+ // /4 exists(to) is true and equivalent(from, to) is true
+ ec = std::make_error_code(std::errc::device_or_resource_busy);
+ return false;
+ }
+
+ if (masked_options == CopyOptions::overwrite_existing)
+ {
+ destination_fd.ftruncate(0, ec);
+ if (ec) return false;
}
+
+#if defined(__linux__)
+ off_t bytes = 0;
+ if (sendfile(destination_fd.get(), source_fd.get(), &bytes, source_stat.st_size) == -1)
+ {
+ ec.assign(errno, std::generic_category());
+ return false;
+ }
+
+ destination_fd.fchmod(source_stat.st_mode, ec);
+ return !ec;
+#elif defined(__APPLE__)
+ if (fcopyfile(source_fd.get(), destination_fd.get(), 0, COPYFILE_ALL) == -1)
+ {
+ ec.assign(errno, std::generic_category());
+ return false;
+ }
+
+ // fcopyfile copies the mode so no need to fchmod here
+ return true;
+#else // ^^^ defined(__APPLE__) // !(defined(__APPLE__) || defined(__linux__)) vvv
+ constexpr std::size_t buffer_length = 4096;
+ unsigned char buffer[buffer_length];
+ while (auto read_bytes = i_fd.read(buffer, buffer_length))
+ {
+ if (read_bytes == -1)
+ {
+ ec.assign(errno, std::generic_category());
+ return false;
+ }
+
+ auto remaining = read_bytes;
+ while (remaining > 0)
+ {
+ auto write_result = destination_fd.write(buffer, remaining);
+ if (write_result == -1)
+ {
+ ec.assign(errno, std::generic_category());
+ return false;
+ }
+
+ remaining -= write_result;
+ }
+ }
+
+ destination_fd.fchmod(source_stat.st_mode, ec);
+ return !ec;
+#endif // ^^^ !(defined(__APPLE__) || defined(__linux__))
+#endif // ^^^ !_WIN32
+ }
+
virtual void copy_symlink(const Path& source, const Path& destination, std::error_code& ec) override
{
return stdfs::copy_symlink(to_stdfs_path(source), to_stdfs_path(destination), ec);

Add a posix-native copy_file. (#205)

* Add a posix-native copy_file.

This PR is part of the std::filesystem removal-on-POSIX project.

* Reduce syscalls by 1, as suggested by Nicole.

* Add back masked_options.

* add todos

* Fix handling of mode bits and comment about not handing the oddball symlink case.