Merge branch 'v2.1.0-rc-develop' of https://git.fifthgrid.com/blockstorage/repertory into v2.1.0-rc-develop

# Conflicts:
#	repertory/repertory_test/src/packet_client_test.cpp
This commit is contained in:
2025-09-29 07:30:36 -05:00
40 changed files with 2048 additions and 684 deletions

View File

@@ -26,6 +26,7 @@
### Changes from v2.0.7-release ### Changes from v2.0.7-release
* Added check version support to remote mounts * Added check version support to remote mounts
* Fixed directory item count bug on S3 provider
* Fixed handling of `FALLOC_FL_KEEP_SIZE` on Linux * Fixed handling of `FALLOC_FL_KEEP_SIZE` on Linux
* Fixed intermittent client hang on remote mount server disconnect * Fixed intermittent client hang on remote mount server disconnect
* Implemented POSIX-compliant `unlink()` with FUSE `hard_remove` * Implemented POSIX-compliant `unlink()` with FUSE `hard_remove`

View File

@@ -22,6 +22,8 @@
#ifndef REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_ #ifndef REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_
#define REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_ #define REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_
#include "events/event_system.hpp"
#include "events/types/packet_client_timeout.hpp"
#include "utils/common.hpp" #include "utils/common.hpp"
namespace repertory::comm { namespace repertory::comm {
@@ -46,6 +48,16 @@ private:
boost::asio::ip::tcp::socket &sock; boost::asio::ip::tcp::socket &sock;
}; };
template <class op_t>
inline void run_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
op_t operation,
std::chrono::milliseconds deadline,
const std::string &event_name,
const std::string &function_name);
void apply_common_socket_properties(boost::asio::ip::tcp::socket &sock);
[[nodiscard]] auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock) [[nodiscard]] auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock)
-> bool; -> bool;
@@ -64,6 +76,59 @@ void write_all_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock, boost::asio::ip::tcp::socket &sock,
boost::asio::mutable_buffer buf, boost::asio::mutable_buffer buf,
std::chrono::milliseconds deadline); std::chrono::milliseconds deadline);
template <class op_t>
inline void run_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
op_t operation,
std::chrono::milliseconds deadline,
const std::string &event_name,
const std::string &function_name) {
deadline = std::max(deadline, std::chrono::milliseconds{250});
struct request_state final {
std::atomic<bool> done{false};
std::atomic<bool> timed_out{false};
boost::system::error_code err;
};
auto state = std::make_shared<request_state>();
boost::asio::steady_timer timer{io_ctx};
timer.expires_after(deadline);
timer.async_wait([state, &sock](auto &&err) {
if (not err && not state->done) {
state->timed_out = true;
boost::system::error_code ignored_ec;
[[maybe_unused]] auto res = sock.cancel(ignored_ec);
}
});
operation([state](auto &&err) {
state->err = err;
state->done = true;
});
io_ctx.restart();
while (not state->done && not state->timed_out) {
io_ctx.run_one();
}
timer.cancel();
io_ctx.poll();
if (state->timed_out) {
repertory::event_system::instance().raise<repertory::packet_client_timeout>(
std::string(event_name), std::string(function_name));
throw std::runtime_error(event_name + " timed-out");
}
if (state->err) {
throw std::runtime_error(event_name + " failed|err|" +
state->err.message());
}
}
} // namespace repertory::comm } // namespace repertory::comm
#endif // REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_ #endif // REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_

View File

@@ -133,6 +133,8 @@ public:
[[nodiscard]] auto check_parent_access(const std::string &api_path, [[nodiscard]] auto check_parent_access(const std::string &api_path,
int mask) const -> api_error override; int mask) const -> api_error override;
[[nodiscard]] static auto validate_timespec(const timespec &spec) -> bool;
}; };
} // namespace repertory } // namespace repertory

View File

@@ -90,8 +90,7 @@ public:
remote::file_handle handle) remote::file_handle handle)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_ftruncate(const char *path, [[nodiscard]] auto fuse_ftruncate(const char *path, remote::file_offset size,
remote::file_offset size,
remote::file_handle handle) remote::file_handle handle)
-> packet::error_type override; -> packet::error_type override;
@@ -119,17 +118,17 @@ public:
[[nodiscard]] auto fuse_mkdir(const char *path, remote::file_mode mode) [[nodiscard]] auto fuse_mkdir(const char *path, remote::file_mode mode)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_opendir(const char *path, remote::file_handle handle) [[nodiscard]] auto fuse_opendir(const char *path, remote::file_handle &handle)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto [[nodiscard]] auto fuse_create(const char *path, remote::file_mode mode,
fuse_create(const char *path, remote::file_mode mode, const remote::open_flags &flags,
const remote::open_flags &flags, remote::file_handle handle) remote::file_handle &handle)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_open(const char *path, [[nodiscard]] auto fuse_open(const char *path,
const remote::open_flags &flags, const remote::open_flags &flags,
remote::file_handle handle) remote::file_handle &handle)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto [[nodiscard]] auto
@@ -140,10 +139,9 @@ public:
[[nodiscard]] auto fuse_rename(const char *from, const char *to) [[nodiscard]] auto fuse_rename(const char *from, const char *to)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_write(const char *path, const char *buffer, [[nodiscard]] auto
remote::file_size write_size, fuse_write(const char *path, const char *buffer, remote::file_size write_size,
remote::file_offset write_offset, remote::file_offset write_offset, remote::file_handle handle)
remote::file_handle handle)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_write_base64(const char *path, const char *buffer, [[nodiscard]] auto fuse_write_base64(const char *path, const char *buffer,
@@ -152,9 +150,9 @@ public:
remote::file_handle handle) remote::file_handle handle)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto [[nodiscard]] auto fuse_readdir(const char *path, remote::file_offset offset,
fuse_readdir(const char *path, remote::file_offset offset, remote::file_handle handle,
remote::file_handle handle, std::string &item_path) std::string &item_path)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_release(const char *path, remote::file_handle handle) [[nodiscard]] auto fuse_release(const char *path, remote::file_handle handle)
@@ -181,8 +179,7 @@ public:
remote::file_time chgtime) remote::file_time chgtime)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_setcrtime(const char *path, [[nodiscard]] auto fuse_setcrtime(const char *path, remote::file_time crtime)
remote::file_time crtime)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_setvolname(const char *volname) [[nodiscard]] auto fuse_setvolname(const char *volname)
@@ -204,8 +201,7 @@ public:
remote::statfs_x &r_stat) remote::statfs_x &r_stat)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_truncate(const char *path, [[nodiscard]] auto fuse_truncate(const char *path, remote::file_offset size)
remote::file_offset size)
-> packet::error_type override; -> packet::error_type override;
[[nodiscard]] auto fuse_unlink(const char *path) [[nodiscard]] auto fuse_unlink(const char *path)
@@ -215,8 +211,8 @@ public:
std::uint64_t op0, std::uint64_t op1) std::uint64_t op0, std::uint64_t op1)
-> packet::error_type override; -> packet::error_type override;
void set_fuse_uid_gid(remote::user_id /* uid */, void set_fuse_uid_gid(remote::user_id /* uid */,
remote::group_id /* gid */) override {} remote::group_id /* gid */) override {}
// JSON Layer // JSON Layer
[[nodiscard]] auto json_create_directory_snapshot(const std::string &path, [[nodiscard]] auto json_create_directory_snapshot(const std::string &path,

View File

@@ -176,6 +176,8 @@ public:
const open_file_data &ofd, std::uint64_t &handle, const open_file_data &ofd, std::uint64_t &handle,
std::shared_ptr<i_open_file> &file) -> api_error; std::shared_ptr<i_open_file> &file) -> api_error;
[[nodiscard]] auto remove_directory(const std::string &api_path) -> api_error;
[[nodiscard]] auto remove_file(const std::string &api_path) -> api_error; [[nodiscard]] auto remove_file(const std::string &api_path) -> api_error;
[[nodiscard]] auto rename_directory(const std::string &from_api_path, [[nodiscard]] auto rename_directory(const std::string &from_api_path,

View File

@@ -34,6 +34,11 @@ struct i_http_comm;
struct head_object_result; struct head_object_result;
class s3_provider final : public base_provider { class s3_provider final : public base_provider {
private:
using interate_callback_t = std::function<api_error(
const std::string &prefix, const pugi::xml_node &node,
const std::string &api_prefix)>;
public: public:
static const constexpr auto type{provider_type::s3}; static const constexpr auto type{provider_type::s3};
@@ -170,6 +175,11 @@ public:
return false; return false;
}; };
[[nodiscard]] auto iterate_prefix(const std::string &prefix,
interate_callback_t prefix_action,
interate_callback_t key_action) const
-> api_error;
[[nodiscard]] auto read_file_bytes(const std::string &api_path, [[nodiscard]] auto read_file_bytes(const std::string &api_path,
std::size_t size, std::uint64_t offset, std::size_t size, std::uint64_t offset,
data_buffer &data, data_buffer &data,

View File

@@ -206,7 +206,8 @@ enum class exit_code : std::int32_t {
ui_mount_failed = -19, ui_mount_failed = -19,
exception = -20, exception = -20,
provider_offline = -21, provider_offline = -21,
ui_failed = -22 ui_failed = -22,
remove_failed = -23
}; };
enum http_error_codes : std::int32_t { enum http_error_codes : std::int32_t {

View File

@@ -21,9 +21,6 @@
*/ */
#include "comm/packet/common.hpp" #include "comm/packet/common.hpp"
#include "events/event_system.hpp"
#include "events/types/packet_client_timeout.hpp"
namespace repertory::comm { namespace repertory::comm {
non_blocking_guard::non_blocking_guard(boost::asio::ip::tcp::socket &sock_) non_blocking_guard::non_blocking_guard(boost::asio::ip::tcp::socket &sock_)
: non_blocking(sock_.non_blocking()), sock(sock_) { : non_blocking(sock_.non_blocking()), sock(sock_) {
@@ -69,48 +66,10 @@ auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock) -> bool {
return not err; return not err;
} }
template <class op_t> void apply_common_socket_properties(boost::asio::ip::tcp::socket &sock) {
void run_with_deadline(boost::asio::io_context &io_ctx, sock.set_option(boost::asio::ip::tcp::no_delay(true));
boost::asio::ip::tcp::socket &sock, op_t &&operation, sock.set_option(boost::asio::socket_base::linger(false, 0));
std::chrono::milliseconds deadline, sock.set_option(boost::asio::socket_base::keep_alive(true));
std::string_view event_name,
std::string_view function_name) {
deadline = std::max(deadline, std::chrono::milliseconds{250});
bool done = false;
bool timed_out = false;
boost::asio::steady_timer timer{io_ctx};
timer.expires_after(deadline);
timer.async_wait([&done, &sock, &timed_out](auto &&err_) {
if (not err_ && not done) {
timed_out = true;
sock.cancel();
}
});
boost::system::error_code err{};
operation([&done, &err](auto &&err_) {
err = err_;
done = true;
});
io_ctx.restart();
while (not done) {
io_ctx.run_one();
}
timer.cancel();
if (timed_out) {
repertory::event_system::instance().raise<repertory::packet_client_timeout>(
std::string(event_name), std::string(function_name));
throw std::runtime_error(std::string(event_name) + " timed-out");
}
if (err) {
throw std::runtime_error(std::string(event_name) + " failed|err|" +
err.message());
}
} }
void connect_with_deadline( void connect_with_deadline(
@@ -126,7 +85,7 @@ void connect_with_deadline(
boost::asio::async_connect( boost::asio::async_connect(
sock, endpoints, [handler](auto &&err, auto &&) { handler(err); }); sock, endpoints, [handler](auto &&err, auto &&) { handler(err); });
}, },
deadline, "connect", function_name); deadline, "connect", std::string{function_name});
} }
void read_exact_with_deadline(boost::asio::io_context &io_ctx, void read_exact_with_deadline(boost::asio::io_context &io_ctx,
@@ -153,7 +112,7 @@ void read_exact_with_deadline(boost::asio::io_context &io_ctx,
handler(err); handler(err);
}); });
}, },
deadline, "read", function_name); deadline, "read", std::string{function_name});
if (bytes_read == 0U) { if (bytes_read == 0U) {
throw std::runtime_error("0 bytes read"); throw std::runtime_error("0 bytes read");
@@ -186,7 +145,7 @@ void write_all_with_deadline(boost::asio::io_context &io_ctx,
handler(err); handler(err);
}); });
}, },
deadline, "write", function_name); deadline, "write", std::string{function_name});
if (bytes_written == 0U) { if (bytes_written == 0U) {
throw std::runtime_error("0 bytes written"); throw std::runtime_error("0 bytes written");

View File

@@ -58,10 +58,11 @@ packet_client::~packet_client() {
void packet_client::close(client &cli) noexcept { void packet_client::close(client &cli) noexcept {
boost::system::error_code err1; boost::system::error_code err1;
auto res = cli.socket.shutdown(boost::asio::socket_base::shutdown_both, err1); [[maybe_unused]] auto res =
cli.socket.shutdown(boost::asio::socket_base::shutdown_both, err1);
boost::system::error_code err2; boost::system::error_code err2;
res = cli.socket.close(err2); [[maybe_unused]] auto res2 = cli.socket.close(err2);
} }
void packet_client::close_all() { void packet_client::close_all() {
@@ -89,9 +90,7 @@ auto packet_client::check_version(std::uint32_t client_version,
connect_with_deadline(ctx, cli.socket, resolve_results, connect_with_deadline(ctx, cli.socket, resolve_results,
std::chrono::milliseconds(cfg_.conn_timeout_ms)); std::chrono::milliseconds(cfg_.conn_timeout_ms));
cli.socket.set_option(boost::asio::ip::tcp::no_delay(true)); comm::apply_common_socket_properties(cli.socket);
cli.socket.set_option(boost::asio::socket_base::linger(false, 0));
cli.socket.set_option(boost::asio::socket_base::keep_alive(true));
if (not handshake(cli, ctx, min_version)) { if (not handshake(cli, ctx, min_version)) {
return api_error::comm_error; return api_error::comm_error;
@@ -117,9 +116,7 @@ auto packet_client::connect(client &cli) -> bool {
connect_with_deadline(io_context_, cli.socket, cached, connect_with_deadline(io_context_, cli.socket, cached,
std::chrono::milliseconds(cfg_.conn_timeout_ms)); std::chrono::milliseconds(cfg_.conn_timeout_ms));
cli.socket.set_option(boost::asio::ip::tcp::no_delay(true)); comm::apply_common_socket_properties(cli.socket);
cli.socket.set_option(boost::asio::socket_base::linger(false, 0));
cli.socket.set_option(boost::asio::socket_base::keep_alive(true));
std::uint32_t min_version{}; std::uint32_t min_version{};
if (not handshake(cli, io_context_, min_version)) { if (not handshake(cli, io_context_, min_version)) {
@@ -248,8 +245,12 @@ auto packet_client::read_packet(client &cli, boost::asio::io_context &ctx,
std::memcpy(&to_read, buffer.data(), sizeof(to_read)); std::memcpy(&to_read, buffer.data(), sizeof(to_read));
boost::endian::big_to_native_inplace(to_read); boost::endian::big_to_native_inplace(to_read);
if (to_read == 0U || to_read > comm::max_packet_bytes) { if (to_read > comm::max_packet_bytes) {
return utils::from_api_error(api_error::comm_error); throw std::runtime_error(fmt::format("packet too large|size|{}", to_read));
}
if (to_read < utils::encryption::encryption_header_size) {
throw std::runtime_error(fmt::format("packet too small|size|{}", to_read));
} }
buffer.resize(to_read); buffer.resize(to_read);

View File

@@ -232,9 +232,7 @@ void packet_server::on_accept(std::shared_ptr<connection> conn,
return; return;
} }
conn->socket.set_option(boost::asio::ip::tcp::no_delay(true)); comm::apply_common_socket_properties(conn->socket);
conn->socket.set_option(boost::asio::socket_base::linger(false, 0));
conn->socket.set_option(boost::asio::socket_base::keep_alive(true));
boost::asio::dispatch(conn->socket.get_executor(), [this, conn]() { boost::asio::dispatch(conn->socket.get_executor(), [this, conn]() {
if (not handshake(conn)) { if (not handshake(conn)) {
@@ -291,6 +289,11 @@ void packet_server::read_packet(std::shared_ptr<connection> conn,
fmt::format("packet too large|size|{}", data_size)); fmt::format("packet too large|size|{}", data_size));
} }
if (data_size < utils::encryption::encryption_header_size) {
throw std::runtime_error(
fmt::format("packet too small|size|{}", data_size));
}
auto should_send_response = true; auto should_send_response = true;
auto response = std::make_shared<packet>(); auto response = std::make_shared<packet>();
conn->buffer.resize(data_size); conn->buffer.resize(data_size);

View File

@@ -51,6 +51,9 @@ fuse_base::fuse_base(app_config &config) : config_(config) {
fuse_ops_.fallocate = fuse_base::fallocate_; fuse_ops_.fallocate = fuse_base::fallocate_;
fuse_ops_.fsync = fuse_base::fsync_; fuse_ops_.fsync = fuse_base::fsync_;
fuse_ops_.getattr = fuse_base::getattr_; fuse_ops_.getattr = fuse_base::getattr_;
#if FUSE_USE_VERSION < 30
fuse_ops_.fgetattr = fuse_base::fgetattr_;
#endif // FUSE_USE_VERSION < 30
fuse_ops_.init = fuse_base::init_; fuse_ops_.init = fuse_base::init_;
fuse_ops_.ioctl = fuse_base::ioctl_; fuse_ops_.ioctl = fuse_base::ioctl_;
fuse_ops_.mkdir = fuse_base::mkdir_; fuse_ops_.mkdir = fuse_base::mkdir_;

View File

@@ -594,7 +594,8 @@ auto fuse_drive::getxtimes_impl(std::string api_path, struct timespec *bkuptime,
} }
api_meta_map meta{}; api_meta_map meta{};
if ((res = provider_.get_item_meta(api_path, meta)) != api_error::success) { res = provider_.get_item_meta(api_path, meta);
if (res != api_error::success) {
return res; return res;
} }
@@ -926,17 +927,20 @@ auto fuse_drive::rename_impl(std::string from_api_path, std::string to_api_path)
} }
auto fuse_drive::rmdir_impl(std::string api_path) -> api_error { auto fuse_drive::rmdir_impl(std::string api_path) -> api_error {
REPERTORY_USES_FUNCTION_NAME();
utils::error::handle_info(function_name, "called");
auto res = check_parent_access(api_path, W_OK | X_OK); auto res = check_parent_access(api_path, W_OK | X_OK);
if (res != api_error::success) { if (res != api_error::success) {
return res; return res;
} }
res = provider_.remove_directory(api_path); res = fm_->remove_directory(api_path);
if (res != api_error::success) { if (res != api_error::success) {
return res; return res;
} }
auto iter = directory_cache_->remove_directory(api_path); directory_cache_->remove_directory(api_path);
return api_error::success; return api_error::success;
} }
@@ -1218,11 +1222,19 @@ auto fuse_drive::setattr_x_impl(std::string api_path, struct setattr_x *attr)
if (SETATTR_WANTS_MODTIME(attr)) { if (SETATTR_WANTS_MODTIME(attr)) {
struct timespec ts[2]; struct timespec ts[2];
if (SETATTR_WANTS_ACCTIME(attr)) { if (SETATTR_WANTS_ACCTIME(attr)) {
if (not fuse_drive_base::validate_timespec(attr->acctime)) {
return api_error::invalid_operation;
}
ts[0].tv_sec = attr->acctime.tv_sec; ts[0].tv_sec = attr->acctime.tv_sec;
ts[0].tv_nsec = attr->acctime.tv_nsec; ts[0].tv_nsec = attr->acctime.tv_nsec;
} else { } else {
if (not fuse_drive_base::validate_timespec(attr->modtime)) {
return api_error::invalid_operation;
}
struct timeval tv{}; struct timeval tv{};
gettimeofday(&tv, NULL); gettimeofday(&tv, nullptr);
ts[0].tv_sec = tv.tv_sec; ts[0].tv_sec = tv.tv_sec;
ts[0].tv_nsec = tv.tv_usec * 1000; ts[0].tv_nsec = tv.tv_usec * 1000;
} }
@@ -1416,6 +1428,10 @@ auto fuse_drive::utimens_impl(std::string api_path, const struct timespec tv[2],
auto fuse_drive::utimens_impl(std::string api_path, const struct timespec tv[2]) auto fuse_drive::utimens_impl(std::string api_path, const struct timespec tv[2])
-> api_error { -> api_error {
#endif #endif
if (not validate_timespec(tv[0]) || not validate_timespec(tv[1])) {
return api_error::invalid_operation;
}
api_meta_map meta; api_meta_map meta;
auto res = provider_.get_item_meta(api_path, meta); auto res = provider_.get_item_meta(api_path, meta);
if (res != api_error::success) { if (res != api_error::success) {

View File

@@ -364,6 +364,20 @@ void fuse_drive_base::set_timespec_from_meta(const api_meta_map &meta,
ts.tv_sec = ts.tv_sec =
static_cast<std::int64_t>(meta_time / utils::time::NANOS_PER_SECOND); static_cast<std::int64_t>(meta_time / utils::time::NANOS_PER_SECOND);
} }
auto fuse_drive_base::validate_timespec(const timespec &spec) -> bool {
if (spec.tv_nsec == UTIME_NOW || spec.tv_nsec == UTIME_OMIT) {
return true;
}
if (spec.tv_nsec < 0 || spec.tv_nsec >= static_cast<std::int64_t>(
utils::time::NANOS_PER_SECOND)) {
return false;
}
return true;
};
} // namespace repertory } // namespace repertory
#endif // !defined(_WIN32) #endif // !defined(_WIN32)

View File

@@ -24,6 +24,7 @@
#include "drives/fuse/remotefuse/remote_fuse_drive.hpp" #include "drives/fuse/remotefuse/remote_fuse_drive.hpp"
#include "app_config.hpp" #include "app_config.hpp"
#include "drives/fuse/fuse_drive_base.hpp"
#include "events/consumers/console_consumer.hpp" #include "events/consumers/console_consumer.hpp"
#include "events/consumers/logging_consumer.hpp" #include "events/consumers/logging_consumer.hpp"
#include "events/event_system.hpp" #include "events/event_system.hpp"
@@ -50,8 +51,8 @@ auto remote_fuse_drive::access_impl(std::string api_path, int mask)
} }
#if defined(__APPLE__) #if defined(__APPLE__)
api_error remote_fuse_drive::chflags_impl(std::string api_path, auto remote_fuse_drive::chflags_impl(std::string api_path, uint32_t flags)
uint32_t flags) { -> api_error {
return utils::to_api_error( return utils::to_api_error(
remote_instance_->fuse_chflags(api_path.c_str(), flags)); remote_instance_->fuse_chflags(api_path.c_str(), flags));
} }
@@ -141,6 +142,18 @@ auto remote_fuse_drive::fsetattr_x_impl(std::string api_path,
struct setattr_x *attr, struct setattr_x *attr,
struct fuse_file_info *f_info) struct fuse_file_info *f_info)
-> api_error { -> api_error {
if (SETATTR_WANTS_MODTIME(attr)) {
if (not fuse_drive_base::validate_timespec(attr->modtime)) {
return api_error::invalid_operation;
}
}
if (SETATTR_WANTS_ACCTIME(attr)) {
if (not fuse_drive_base::validate_timespec(attr->acctime)) {
return api_error::invalid_operation;
}
}
remote::setattr_x attributes{}; remote::setattr_x attributes{};
attributes.valid = attr->valid; attributes.valid = attr->valid;
attributes.mode = attr->mode; attributes.mode = attr->mode;
@@ -209,9 +222,9 @@ auto remote_fuse_drive::getattr_impl(std::string api_path, struct stat *u_stat)
} }
#if defined(__APPLE__) #if defined(__APPLE__)
api_error remote_fuse_drive::getxtimes_impl(std::string api_path, auto remote_fuse_drive::getxtimes_impl(std::string api_path,
struct timespec *bkuptime, struct timespec *bkuptime,
struct timespec *crtime) { struct timespec *crtime) -> api_error {
if (not(bkuptime && crtime)) { if (not(bkuptime && crtime)) {
return utils::to_api_error(-EFAULT); return utils::to_api_error(-EFAULT);
} }
@@ -488,8 +501,20 @@ auto remote_fuse_drive::rmdir_impl(std::string api_path) -> api_error {
} }
#if defined(__APPLE__) #if defined(__APPLE__)
api_error remote_fuse_drive::setattr_x_impl(std::string api_path, auto remote_fuse_drive::setattr_x_impl(std::string api_path,
struct setattr_x *attr) { struct setattr_x *attr) -> api_error {
if (SETATTR_WANTS_MODTIME(attr)) {
if (not fuse_drive_base::validate_timespec(attr->modtime)) {
return api_error::invalid_operation;
}
}
if (SETATTR_WANTS_ACCTIME(attr)) {
if (not fuse_drive_base::validate_timespec(attr->acctime)) {
return api_error::invalid_operation;
}
}
remote::setattr_x attributes{}; remote::setattr_x attributes{};
attributes.valid = attr->valid; attributes.valid = attr->valid;
attributes.mode = attr->mode; attributes.mode = attr->mode;
@@ -517,8 +542,9 @@ api_error remote_fuse_drive::setattr_x_impl(std::string api_path,
remote_instance_->fuse_setattr_x(api_path.c_str(), attributes)); remote_instance_->fuse_setattr_x(api_path.c_str(), attributes));
} }
api_error remote_fuse_drive::setbkuptime_impl(std::string api_path, auto remote_fuse_drive::setbkuptime_impl(std::string api_path,
const struct timespec *bkuptime) { const struct timespec *bkuptime)
-> api_error {
remote::file_time repertory_bkuptime = remote::file_time repertory_bkuptime =
((static_cast<remote::file_time>(bkuptime->tv_sec) * ((static_cast<remote::file_time>(bkuptime->tv_sec) *
utils::time::NANOS_PER_SECOND) + utils::time::NANOS_PER_SECOND) +
@@ -527,8 +553,9 @@ api_error remote_fuse_drive::setbkuptime_impl(std::string api_path,
remote_instance_->fuse_setbkuptime(api_path.c_str(), repertory_bkuptime)); remote_instance_->fuse_setbkuptime(api_path.c_str(), repertory_bkuptime));
} }
api_error remote_fuse_drive::setchgtime_impl(std::string api_path, auto remote_fuse_drive::setchgtime_impl(std::string api_path,
const struct timespec *chgtime) { const struct timespec *chgtime)
-> api_error {
remote::file_time repertory_chgtime = remote::file_time repertory_chgtime =
((static_cast<remote::file_time>(chgtime->tv_sec) * ((static_cast<remote::file_time>(chgtime->tv_sec) *
utils::time::NANOS_PER_SECOND) + utils::time::NANOS_PER_SECOND) +
@@ -537,8 +564,9 @@ api_error remote_fuse_drive::setchgtime_impl(std::string api_path,
remote_instance_->fuse_setchgtime(api_path.c_str(), repertory_chgtime)); remote_instance_->fuse_setchgtime(api_path.c_str(), repertory_chgtime));
} }
api_error remote_fuse_drive::setcrtime_impl(std::string api_path, auto remote_fuse_drive::setcrtime_impl(std::string api_path,
const struct timespec *crtime) { const struct timespec *crtime)
-> api_error {
remote::file_time repertory_crtime = remote::file_time repertory_crtime =
((static_cast<remote::file_time>(crtime->tv_sec) * ((static_cast<remote::file_time>(crtime->tv_sec) *
utils::time::NANOS_PER_SECOND) + utils::time::NANOS_PER_SECOND) +
@@ -547,12 +575,12 @@ api_error remote_fuse_drive::setcrtime_impl(std::string api_path,
remote_instance_->fuse_setcrtime(api_path.c_str(), repertory_crtime)); remote_instance_->fuse_setcrtime(api_path.c_str(), repertory_crtime));
} }
api_error remote_fuse_drive::setvolname_impl(const char *volname) { auto remote_fuse_drive::setvolname_impl(const char *volname) -> api_error {
return utils::to_api_error(remote_instance_->fuse_setvolname(volname)); return utils::to_api_error(remote_instance_->fuse_setvolname(volname));
} }
api_error remote_fuse_drive::statfs_x_impl(std::string api_path, auto remote_fuse_drive::statfs_x_impl(std::string api_path,
struct statfs *stbuf) { struct statfs *stbuf) -> api_error {
auto res = statfs(config_.get_data_directory().c_str(), stbuf); auto res = statfs(config_.get_data_directory().c_str(), stbuf);
if (res == 0) { if (res == 0) {
remote::statfs_x r_stat{}; remote::statfs_x r_stat{};
@@ -623,6 +651,16 @@ auto remote_fuse_drive::utimens_impl(std::string api_path,
auto remote_fuse_drive::utimens_impl(std::string api_path, auto remote_fuse_drive::utimens_impl(std::string api_path,
const struct timespec tv[2]) -> api_error { const struct timespec tv[2]) -> api_error {
#endif // FUSE_USE_VERSION >= 30 #endif // FUSE_USE_VERSION >= 30
REPERTORY_USES_FUNCTION_NAME();
if (not fuse_drive_base::validate_timespec(tv[0]) ||
not fuse_drive_base::validate_timespec(tv[1])) {
utils::error::handle_info(
function_name,
fmt::format("failed|{}|{}", tv[0].tv_nsec, tv[1].tv_nsec));
return api_error::invalid_operation;
}
remote::file_time rtv[2U] = {0}; remote::file_time rtv[2U] = {0};
if (tv != nullptr) { if (tv != nullptr) {
const auto update_timespec = [](auto &dst, const auto &src) { const auto update_timespec = [](auto &dst, const auto &src) {

View File

@@ -1721,7 +1721,6 @@ auto remote_server::update_to_windows_format(const std::string &root_api_path,
} }
auto update_meta{false}; auto update_meta{false};
auto default_value = std::to_string(utils::time::get_time_now());
const auto ensure_set = [&item, &update_meta]( const auto ensure_set = [&item, &update_meta](
const std::string &name, const std::string &name,
const std::string &default_value, bool as_time) { const std::string &default_value, bool as_time) {
@@ -1734,9 +1733,10 @@ auto remote_server::update_to_windows_format(const std::string &root_api_path,
} }
}; };
ensure_set(META_ACCESSED, default_value, true); auto default_time = std::to_string(utils::time::get_time_now());
ensure_set(META_CREATION, default_value, true); ensure_set(META_ACCESSED, default_time, true);
ensure_set(META_MODIFIED, default_value, true); ensure_set(META_CREATION, default_time, true);
ensure_set(META_MODIFIED, default_time, true);
ensure_set(META_CHANGED, item[JSON_META][META_MODIFIED], true); ensure_set(META_CHANGED, item[JSON_META][META_MODIFIED], true);
ensure_set(META_WRITTEN, item[JSON_META][META_MODIFIED], true); ensure_set(META_WRITTEN, item[JSON_META][META_MODIFIED], true);
ensure_set(META_ATTRIBUTES, "0", false); ensure_set(META_ATTRIBUTES, "0", false);

View File

@@ -131,8 +131,7 @@ auto remote_server::fuse_chflags(const char *path, std::uint32_t /*flags*/)
return ret; return ret;
} }
auto remote_server::fuse_chmod(const char *path, auto remote_server::fuse_chmod(const char *path, remote::file_mode /*mode*/)
remote::file_mode /*mode*/)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -142,10 +141,8 @@ auto remote_server::fuse_chmod(const char *path,
return ret; return ret;
} }
auto remote_server::fuse_chown(const char *path, auto remote_server::fuse_chown(const char *path, remote::user_id /*uid*/,
remote::user_id /*uid*/, remote::group_id /*gid*/) -> packet::error_type {
remote::group_id /*gid*/)
-> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
auto file_path = construct_path(path); auto file_path = construct_path(path);
@@ -211,8 +208,7 @@ auto remote_server::fuse_fsetattr_x(const char *path,
return ret; return ret;
} }
auto remote_server::fuse_fsync(const char *path, auto remote_server::fuse_fsync(const char *path, std::int32_t /*datasync*/,
std::int32_t /*datasync*/,
remote::file_handle handle) remote::file_handle handle)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -241,8 +237,7 @@ auto remote_server::fuse_fsync(const char *path,
return ret; return ret;
} }
auto remote_server::fuse_ftruncate(const char *path, auto remote_server::fuse_ftruncate(const char *path, remote::file_offset size,
remote::file_offset size,
remote::file_handle handle) remote::file_handle handle)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -336,8 +331,7 @@ construct_path(path); auto ret = STATUS_NOT_IMPLEMENTED;
return ret; return ret;
}*/ }*/
auto remote_server::fuse_mkdir(const char *path, auto remote_server::fuse_mkdir(const char *path, remote::file_mode /*mode*/)
remote::file_mode /*mode*/)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -350,7 +344,7 @@ auto remote_server::fuse_mkdir(const char *path,
return ret; return ret;
} }
auto remote_server::fuse_opendir(const char *path, remote::file_handle handle) auto remote_server::fuse_opendir(const char *path, remote::file_handle &handle)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -377,7 +371,7 @@ auto remote_server::fuse_opendir(const char *path, remote::file_handle handle)
auto remote_server::fuse_create(const char *path, remote::file_mode mode, auto remote_server::fuse_create(const char *path, remote::file_mode mode,
const remote::open_flags &flags, const remote::open_flags &flags,
remote::file_handle handle) remote::file_handle &handle)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -414,7 +408,7 @@ auto remote_server::fuse_create(const char *path, remote::file_mode mode,
} }
auto remote_server::fuse_open(const char *path, const remote::open_flags &flags, auto remote_server::fuse_open(const char *path, const remote::open_flags &flags,
remote::file_handle handle) remote::file_handle &handle)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -531,17 +525,17 @@ auto remote_server::fuse_write(const char *path, const char *buffer,
return ret; return ret;
} }
auto remote_server::fuse_write_base64( auto remote_server::fuse_write_base64(const char * /*path*/,
const char * /*path*/, const char * /*buffer*/, const char * /*buffer*/,
remote::file_size /*write_size*/, remote::file_size /*write_size*/,
remote::file_offset /*write_offset*/, remote::file_offset /*write_offset*/,
remote::file_handle /*handle*/) -> packet::error_type { remote::file_handle /*handle*/)
-> packet::error_type {
// DOES NOTHING // DOES NOTHING
return 0; return 0;
} }
auto remote_server::fuse_readdir(const char *path, auto remote_server::fuse_readdir(const char *path, remote::file_offset offset,
remote::file_offset offset,
remote::file_handle handle, remote::file_handle handle,
std::string &item_path) -> packet::error_type { std::string &item_path) -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -623,7 +617,7 @@ auto remote_server::fuse_setattr_x(const char *path,
} }
auto remote_server::fuse_setbkuptime(const char *path, auto remote_server::fuse_setbkuptime(const char *path,
remote::file_time /*bkuptime*/) remote::file_time /*bkuptime*/)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -634,7 +628,7 @@ auto remote_server::fuse_setbkuptime(const char *path,
} }
auto remote_server::fuse_setchgtime(const char *path, auto remote_server::fuse_setchgtime(const char *path,
remote::file_time /*chgtime*/) remote::file_time /*chgtime*/)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -645,7 +639,7 @@ auto remote_server::fuse_setchgtime(const char *path,
} }
auto remote_server::fuse_setcrtime(const char *path, auto remote_server::fuse_setcrtime(const char *path,
remote::file_time /*crtime*/) remote::file_time /*crtime*/)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
@@ -725,8 +719,7 @@ auto remote_server::fuse_statfs_x(const char *path, std::uint64_t bsize,
return 0; return 0;
} }
auto remote_server::fuse_truncate(const char *path, auto remote_server::fuse_truncate(const char *path, remote::file_offset size)
remote::file_offset size)
-> packet::error_type { -> packet::error_type {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();

View File

@@ -216,15 +216,11 @@ VOID winfsp_drive::Cleanup(PVOID file_node, PVOID file_desc,
FspFileSystemDeleteDirectoryBuffer(&directory_buffer); FspFileSystemDeleteDirectoryBuffer(&directory_buffer);
} }
if (not directory) { if (directory) {
return handle_error(fm_->remove_file(api_path)); return handle_error(fm_->remove_directory(api_path));
} }
if (provider_.get_directory_item_count(api_path) == 0) { return handle_error(fm_->remove_file(api_path));
return handle_error(provider_.remove_directory(api_path));
}
return handle_error(api_error::directory_not_empty);
} }
if (((flags & FspCleanupSetArchiveBit) != 0U) && not directory) { if (((flags & FspCleanupSetArchiveBit) != 0U) && not directory) {
@@ -334,7 +330,7 @@ auto winfsp_drive::Create(PWSTR file_name, UINT32 create_options,
auto now = utils::time::get_time_now(); auto now = utils::time::get_time_now();
auto meta = create_meta_attributes( auto meta = create_meta_attributes(
now, attributes, now, now, (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0U, now, attributes, now, now, (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0U,
0U, "", 0U, now, 0U, 0U, 0U, 0U, "", 0U, now, 0U, 0U,
(attributes & FILE_ATTRIBUTE_DIRECTORY) != 0U (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0U
? "" ? ""
: utils::path::combine(config_.get_cache_directory(), : utils::path::combine(config_.get_cache_directory(),

View File

@@ -101,6 +101,10 @@ void file_manager::close(std::uint64_t handle) {
closeable_file->close(); closeable_file->close();
if (closeable_file->is_directory()) {
return;
}
auto file = utils::file::file{closeable_file->get_source_path()}; auto file = utils::file::file{closeable_file->get_source_path()};
if (file.remove()) { if (file.remove()) {
return; return;
@@ -689,9 +693,45 @@ void file_manager::queue_upload(const std::string &api_path,
} }
} }
auto file_manager::remove_directory(const std::string &api_path) -> api_error {
REPERTORY_USES_FUNCTION_NAME();
if (api_path == "/") {
return api_error::permission_denied;
}
unique_recur_mutex_lock open_lock(open_file_mtx_);
if (provider_.get_directory_item_count(api_path) != 0) {
return api_error::directory_not_empty;
}
auto res = provider_.remove_directory(api_path);
if (res != api_error::success) {
return res;
}
auto file_iter = open_file_lookup_.find(api_path);
if (file_iter == open_file_lookup_.end()) {
return api_error::success;
}
auto closed_file = file_iter->second;
open_file_lookup_.erase(api_path);
closed_file->set_unlinked(true);
for (const auto &[handle, ofd] : closed_file->get_open_data()) {
unlinked_file_lookup_[handle] = closed_file;
}
open_lock.unlock();
return api_error::success;
}
auto file_manager::remove_file(const std::string &api_path) -> api_error { auto file_manager::remove_file(const std::string &api_path) -> api_error {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();
unique_recur_mutex_lock open_lock(open_file_mtx_);
filesystem_item fsi{}; filesystem_item fsi{};
auto res = provider_.get_filesystem_item(api_path, false, fsi); auto res = provider_.get_filesystem_item(api_path, false, fsi);
if (res != api_error::success) { if (res != api_error::success) {
@@ -704,8 +744,6 @@ auto file_manager::remove_file(const std::string &api_path) -> api_error {
return res; return res;
} }
unique_recur_mutex_lock open_lock(open_file_mtx_);
unique_mutex_lock upload_lock(upload_mtx_); unique_mutex_lock upload_lock(upload_mtx_);
remove_upload(api_path, true); remove_upload(api_path, true);
remove_resume(api_path, fsi.source_path, true); remove_resume(api_path, fsi.source_path, true);

View File

@@ -350,7 +350,33 @@ void ring_buffer_base::set_api_path(const std::string &api_path) {
} }
void ring_buffer_base::update_position(std::size_t count, bool is_forward) { void ring_buffer_base::update_position(std::size_t count, bool is_forward) {
if (count == 0U) {
return;
}
mutex_lock chunk_lock(chunk_mtx_); mutex_lock chunk_lock(chunk_mtx_);
const auto center_ring = [this]() -> void {
auto ring_size = static_cast<std::size_t>(read_state_.size());
auto half_ring_size = ring_size / 2U;
auto offset = ring_pos_ - ring_begin_;
if (offset > half_ring_size) {
auto steps = offset - half_ring_size;
auto max_steps = (total_chunks_ - 1U) - ring_end_;
if (steps > max_steps) {
steps = max_steps;
}
while (steps-- != 0U) {
++ring_begin_;
++ring_end_;
read_state_[ring_end_ % ring_size] = false;
}
}
chunk_notify_.notify_all();
};
if (is_forward) { if (is_forward) {
if ((ring_pos_ + count) > (total_chunks_ - 1U)) { if ((ring_pos_ + count) > (total_chunks_ - 1U)) {
@@ -363,7 +389,7 @@ void ring_buffer_base::update_position(std::size_t count, bool is_forward) {
if (is_forward ? (ring_pos_ + count) <= ring_end_ if (is_forward ? (ring_pos_ + count) <= ring_end_
: (ring_pos_ - count) >= ring_begin_) { : (ring_pos_ - count) >= ring_begin_) {
ring_pos_ += is_forward ? count : -count; ring_pos_ += is_forward ? count : -count;
chunk_notify_.notify_all(); center_ring();
return; return;
} }
@@ -387,7 +413,6 @@ void ring_buffer_base::update_position(std::size_t count, bool is_forward) {
ring_end_ = ring_end_ =
std::min(total_chunks_ - 1U, ring_begin_ + read_state_.size() - 1U); std::min(total_chunks_ - 1U, ring_begin_ + read_state_.size() - 1U);
center_ring();
chunk_notify_.notify_all();
} }
} // namespace repertory } // namespace repertory

View File

@@ -275,69 +275,34 @@ auto s3_provider::get_directory_item_count(const std::string &api_path) const
try { try {
const auto &cfg{get_s3_config()}; const auto &cfg{get_s3_config()};
auto is_encrypted{not cfg.encryption_token.empty()}; const auto is_encrypted{not cfg.encryption_token.empty()};
std::string key; std::string key;
if (is_encrypted) { if (is_encrypted) {
auto res{get_item_meta(api_path, META_KEY, key)}; if (get_item_meta(api_path, META_KEY, key) != api_error::success) {
if (res != api_error::success) {
return 0U; return 0U;
} }
} }
auto object_name{ auto object_name =
api_path == "/" (api_path == "/")
? "" ? std::string{}
: utils::path::create_api_path(is_encrypted ? key : api_path), : utils::path::create_api_path(is_encrypted ? key : api_path);
};
std::string response_data{}; auto prefix = object_name.empty() ? std::string{} : object_name + "/";
long response_code{}; std::uint64_t total_count{0};
auto prefix{object_name.empty() ? object_name : object_name + "/"}; auto res = iterate_prefix(
prefix,
auto grab_more{true}; [&total_count](auto &&, auto &&, auto &&) {
std::string token{}; ++total_count;
std::uint64_t total_count{}; return api_error::success;
while (grab_more) { },
if (not get_object_list(response_data, response_code, "/", "", token)) { [&total_count](auto &&, auto &&, auto &&) {
return total_count; ++total_count;
} return api_error::success;
});
if (response_code == http_error_codes::not_found) { if (res != api_error::success) {
return total_count; return 0U;
}
if (response_code != http_error_codes::ok) {
return total_count;
}
pugi::xml_document doc;
auto res{doc.load_string(response_data.c_str())};
if (res.status != pugi::xml_parse_status::status_ok) {
return total_count;
}
grab_more = doc.select_node("/ListBucketResult/IsTruncated")
.node()
.text()
.as_bool();
if (grab_more) {
token = doc.select_node("/ListBucketResult/NextContinuationToken")
.node()
.text()
.as_string();
}
auto node_list{
doc.select_nodes("/ListBucketResult/CommonPrefixes/Prefix"),
};
total_count += node_list.size();
node_list = doc.select_nodes("/ListBucketResult/Contents");
total_count += node_list.size();
if (not prefix.empty()) {
--total_count;
}
} }
return total_count; return total_count;
@@ -356,7 +321,7 @@ auto s3_provider::get_directory_items_impl(const std::string &api_path,
const auto &cfg{get_s3_config()}; const auto &cfg{get_s3_config()};
auto is_encrypted{not cfg.encryption_token.empty()}; auto is_encrypted{not cfg.encryption_token.empty()};
const auto add_diretory_item = const auto add_directory_item =
[this, &is_encrypted, &list](const std::string &child_object_name, [this, &is_encrypted, &list](const std::string &child_object_name,
bool directory, auto node) -> api_error { bool directory, auto node) -> api_error {
auto res{api_error::success}; auto res{api_error::success};
@@ -440,74 +405,25 @@ auto s3_provider::get_directory_items_impl(const std::string &api_path,
object_name.empty() ? object_name : object_name + "/", object_name.empty() ? object_name : object_name + "/",
}; };
auto grab_more{true}; return iterate_prefix(
std::string token{}; prefix,
while (grab_more) { [&](auto && /* prefix */, auto &&node, auto &&) -> auto {
std::string response_data{}; return add_directory_item(
long response_code{}; utils::path::create_api_path(
if (not get_object_list(response_data, response_code, "/", prefix, token)) { utils::path::combine("/", {node.text().as_string()})),
return api_error::comm_error; true, node);
} },
[&](auto && /* key */, auto &&node, auto &&api_prefix) -> auto {
auto child_api_path{
utils::path::create_api_path(
node.select_node("Key").node().text().as_string()),
};
if (child_api_path == api_prefix) {
return api_error::success;
}
if (response_code == http_error_codes::not_found) { return add_directory_item(child_api_path, false, node);
return api_error::directory_not_found; });
}
if (response_code != http_error_codes::ok) {
utils::error::raise_api_path_error(function_name, api_path, response_code,
"failed to get directory items");
return api_error::comm_error;
}
pugi::xml_document doc;
auto parse_res{doc.load_string(response_data.c_str())};
if (parse_res.status != pugi::xml_parse_status::status_ok) {
return api_error::error;
}
grab_more = doc.select_node("/ListBucketResult/IsTruncated")
.node()
.text()
.as_bool();
if (grab_more) {
token = doc.select_node("/ListBucketResult/NextContinuationToken")
.node()
.text()
.as_string();
}
auto node_list{
doc.select_nodes("/ListBucketResult/CommonPrefixes/Prefix"),
};
for (const auto &node : node_list) {
auto child_object_name{
utils::path::create_api_path(
utils::path::combine("/", {node.node().text().as_string()})),
};
auto res{add_diretory_item(child_object_name, true, node.node())};
if (res != api_error::success) {
return res;
}
}
node_list = doc.select_nodes("/ListBucketResult/Contents");
for (const auto &node : node_list) {
auto child_object_name{
utils::path::create_api_path(
node.node().select_node("Key").node().text().as_string()),
};
if (child_object_name == utils::path::create_api_path(prefix)) {
continue;
}
auto res{add_diretory_item(child_object_name, false, node.node())};
if (res != api_error::success) {
return res;
}
}
}
return api_error::success;
} }
auto s3_provider::get_file(const std::string &api_path, api_file &file) const auto s3_provider::get_file(const std::string &api_path, api_file &file) const
@@ -918,6 +834,114 @@ auto s3_provider::is_online() const -> bool {
return false; return false;
} }
auto s3_provider::iterate_prefix(const std::string &prefix,
interate_callback_t prefix_action,
interate_callback_t key_action) const
-> api_error {
auto api_prefix = utils::path::create_api_path(prefix);
auto api_prefix_parent = api_prefix;
if (api_prefix != "/") {
api_prefix_parent += '/';
}
std::unordered_set<std::string> seen_prefixes;
std::unordered_set<std::string> seen_keys;
bool grab_more{true};
std::string token{};
while (grab_more) {
long response_code{};
std::string response_data{};
if (not get_object_list(response_data, response_code, "/", prefix, token)) {
return api_error::comm_error;
}
if (response_code == http_error_codes::not_found) {
return api_error::item_not_found;
}
if (response_code != http_error_codes::ok) {
return api_error::comm_error;
}
pugi::xml_document doc;
auto parsed = doc.load_string(response_data.c_str());
if (parsed.status != pugi::xml_parse_status::status_ok) {
return api_error::error;
}
grab_more = doc.select_node("/ListBucketResult/IsTruncated")
.node()
.text()
.as_bool();
if (grab_more) {
token = doc.select_node("/ListBucketResult/NextContinuationToken")
.node()
.text()
.as_string();
}
for (auto const &node :
doc.select_nodes("/ListBucketResult/CommonPrefixes/Prefix")) {
std::string cur_prefix = node.node().text().as_string();
auto cur_api_path = utils::path::create_api_path(cur_prefix);
if (cur_prefix.empty()) {
continue;
}
if (not prefix.empty() &&
(cur_api_path == api_prefix ||
not cur_api_path.starts_with(api_prefix_parent))) {
continue;
}
if (not seen_prefixes.contains(cur_prefix)) {
seen_prefixes.insert(cur_prefix);
auto res = prefix_action(cur_prefix, node.node(), api_prefix);
if (res != api_error::success) {
return res;
}
}
}
for (auto const &node : doc.select_nodes("/ListBucketResult/Contents")) {
std::string cur_key = node.node().child("Key").text().as_string();
auto cur_api_path = utils::path::create_api_path(cur_key);
if (cur_key.empty()) {
continue;
}
if (not prefix.empty() &&
(cur_api_path == api_prefix ||
not cur_api_path.starts_with(api_prefix_parent))) {
continue;
}
if (cur_key.back() == '/') {
continue;
}
if (seen_prefixes.contains(cur_key + "/")) {
continue;
}
if (seen_keys.contains(cur_key)) {
continue;
}
seen_keys.insert(cur_key);
auto res = key_action(cur_key, node.node(), api_prefix);
if (res != api_error::success) {
return res;
}
}
}
return api_error::success;
}
auto s3_provider::remove_directory_impl(const std::string &api_path) auto s3_provider::remove_directory_impl(const std::string &api_path)
-> api_error { -> api_error {
REPERTORY_USES_FUNCTION_NAME(); REPERTORY_USES_FUNCTION_NAME();

View File

@@ -0,0 +1,65 @@
/*
Copyright <2018-2025> <scott.e.graves@protonmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef REPERTORY_INCLUDE_CLI_REMOVE_HPP_
#define REPERTORY_INCLUDE_CLI_REMOVE_HPP_
#include "cli/common.hpp"
#include "utils/time.hpp"
namespace repertory::cli::actions {
[[nodiscard]] inline auto
remove(std::vector<const char *> /* args */, const std::string &data_directory,
provider_type prov, const std::string &unique_id, std::string /* user */,
std::string /* password */) -> exit_code {
lock_data lock(prov, unique_id);
auto res = lock.grab_lock(1);
if (res != lock_result::success) {
std::cout << static_cast<std::int32_t>(exit_code::lock_failed) << std::endl;
std::cerr << "FATAL: Unable to get provider lock" << std::endl;
return exit_code::lock_failed;
}
auto trash_path = utils::path::combine(
app_config::get_root_data_directory(),
{
"trash",
app_config::get_provider_name(prov),
unique_id,
fmt::format("{}_{}", utils::time::get_current_time_utc(),
utils::collection::to_hex_string(
utils::generate_secure_random<data_buffer>(4U))),
});
if (utils::file::directory{data_directory}.move_to(trash_path)) {
fmt::println("0");
fmt::println("successfully removed provider|type|{}|id|{}",
app_config::get_provider_name(prov), unique_id);
return exit_code::success;
}
std::cout << static_cast<std::int32_t>(exit_code::remove_failed) << std::endl;
std::cerr << "FATAL: Failed to remove|" << data_directory << std::endl;
return exit_code::remove_failed;
}
} // namespace repertory::cli::actions
#endif // REPERTORY_INCLUDE_CLI_REMOVE_HPP_

View File

@@ -88,6 +88,9 @@ private:
static void handle_get_available_locations(httplib::Response &res); static void handle_get_available_locations(httplib::Response &res);
void handle_del_remove_mount(const httplib::Request &req,
httplib::Response &res);
void handle_get_mount(const httplib::Request &req, void handle_get_mount(const httplib::Request &req,
httplib::Response &res) const; httplib::Response &res) const;

View File

@@ -216,6 +216,10 @@ ui_server::ui_server(mgmt_app_config *config)
: http_error_codes::internal_error; : http_error_codes::internal_error;
}); });
server_.Delete("/api/v1/remove_mount", [this](auto &&req, auto &&res) {
handle_del_remove_mount(req, res);
});
server_.Get("/api/v1/locations", [](auto && /* req */, auto &&res) { server_.Get("/api/v1/locations", [](auto && /* req */, auto &&res) {
handle_get_available_locations(res); handle_get_available_locations(res);
}); });
@@ -399,6 +403,21 @@ void ui_server::generate_config(provider_type prov, std::string_view name,
} }
} }
void ui_server::handle_del_remove_mount(const httplib::Request &req,
httplib::Response &res) {
auto prov = provider_type_from_string(req.get_param_value("type"));
auto name = req.get_param_value("name");
if (not data_directory_exists(prov, name)) {
res.status = http_error_codes::not_found;
return;
}
auto lines = launch_process(prov, name, {"-rp", "-f"}, false);
res.status = lines.at(0U) == "0" ? http_error_codes::ok
: http_error_codes::internal_error;
}
void ui_server::handle_put_mount_auto_start(const httplib::Request &req, void ui_server::handle_put_mount_auto_start(const httplib::Request &req,
httplib::Response &res) const { httplib::Response &res) const {
auto prov = provider_type_from_string(req.get_param_value("type")); auto prov = provider_type_from_string(req.get_param_value("type"));
@@ -431,25 +450,8 @@ void ui_server::handle_put_mount_location(const httplib::Request &req,
void ui_server::handle_get_available_locations(httplib::Response &res) { void ui_server::handle_get_available_locations(httplib::Response &res) {
#if defined(_WIN32) #if defined(_WIN32)
constexpr std::array<std::string_view, 26U> letters{ res.set_content(nlohmann::json(utils::get_available_drive_letters()).dump(),
"a:", "b:", "c:", "d:", "e:", "f:", "g:", "h:", "i:", "application/json");
"j:", "k:", "l:", "m:", "n:", "o:", "p:", "q:", "r:",
"s:", "t:", "u:", "v:", "w:", "x:", "y:", "z:",
};
auto available = std::accumulate(
letters.begin(), letters.end(), std::vector<std::string_view>(),
[](auto &&vec, auto &&letter) -> std::vector<std::string_view> {
if (utils::file::directory{utils::path::combine(letter, {"\\"})}
.exists()) {
return vec;
}
vec.emplace_back(letter);
return vec;
});
res.set_content(nlohmann::json(available).dump(), "application/json");
#else // !defined(_WIN32) #else // !defined(_WIN32)
res.set_content(nlohmann::json(std::vector<std::string_view>()).dump(), res.set_content(nlohmann::json(std::vector<std::string_view>()).dump(),
"application/json"); "application/json");

View File

@@ -332,14 +332,21 @@ protected:
mount_location2 = mount_location; mount_location2 = mount_location;
#if defined(_WIN32) #if defined(_WIN32)
mount_location = utils::string::to_lower(std::string{"V:"}); auto letter = utils::get_available_drive_letter('d');
ASSERT_TRUE(letter.has_value());
mount_location = utils::string::to_lower(std::string{letter.value()});
#else // !defined(_WIN32) #else // !defined(_WIN32)
mount_location = utils::path::combine(test_dir, {"mount"}); mount_location = utils::path::combine(test_dir, {"mount"});
ASSERT_TRUE(utils::file::directory(mount_location).create_directory()); ASSERT_TRUE(utils::file::directory(mount_location).create_directory());
#endif // defined(_WIN32) #endif // defined(_WIN32)
auto cfg_dir2 = make_cfg_dir(test_dir, "cfg2"); auto cfg_dir2 = make_cfg_dir(test_dir, "cfg2");
#if defined(_WIN32)
auto config2 =
std::make_unique<app_config>(provider_type::remote, cfg_dir2);
#else // !defined(_WIN32)
config2 = std::make_unique<app_config>(provider_type::remote, cfg_dir2); config2 = std::make_unique<app_config>(provider_type::remote, cfg_dir2);
#endif // defined(_WIN32)
config2->set_enable_drive_events(true); config2->set_enable_drive_events(true);
config2->set_event_level(event_level::trace); config2->set_event_level(event_level::trace);
#if !defined(_WIN32) #if !defined(_WIN32)

View File

@@ -1,23 +1,6 @@
/* /*
Copyright <2018-2025> <scott.e.graves@protonmail.com> Copyright <2018-2025> <scott.e.graves@protonmail.com>
...
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/ */
#include "test_common.hpp" #include "test_common.hpp"
@@ -36,13 +19,20 @@ public:
protected: protected:
void SetUp() override { event_system::instance().start(); } void SetUp() override { event_system::instance().start(); }
void TearDown() override { event_system::instance().stop(); } void TearDown() override { event_system::instance().stop(); }
}; };
namespace {
[[nodiscard]] auto build_test_buffer(repertory::utils::file::i_file &file)
-> repertory::data_buffer {
repertory::data_buffer buf;
EXPECT_TRUE(file.read_all(buf, 0U));
return buf;
}
} // namespace
TEST_F(direct_open_file_test, read_full_file) { TEST_F(direct_open_file_test, read_full_file) {
auto &source_file = test::create_random_file(test_chunk_size * 32U); auto &source_file = test::create_random_file(test_chunk_size * 32U);
auto dest_path = test::generate_test_file_name("direct_open_file"); auto dest_path = test::generate_test_file_name("direct_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false)); EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
@@ -52,23 +42,29 @@ TEST_F(direct_open_file_test, read_full_file) {
fsi.directory = false; fsi.directory = false;
fsi.size = test_chunk_size * 32U; fsi.size = test_chunk_size * 32U;
std::mutex read_mtx; auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([&read_mtx, &source_file](
const std::string & /* api_path */, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
mutex_lock lock(read_mtx);
EXPECT_FALSE(stop_requested); EXPECT_CALL(provider, read_file_bytes)
std::size_t bytes_read{}; .WillRepeatedly([test_buffer](const std::string &, std::size_t size,
data.resize(size); std::uint64_t offset, data_buffer &data,
auto ret = source_file.read(data, offset, &bytes_read) stop_type &stop_requested) -> api_error {
? api_error::success if (stop_requested) {
: api_error::os_error; return api_error::download_stopped;
EXPECT_EQ(bytes_read, data.size()); }
return ret; std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
}); });
{ {
direct_open_file file(test_chunk_size, 30U, fsi, provider); direct_open_file file(test_chunk_size, 30U, fsi, provider);
@@ -88,23 +84,23 @@ TEST_F(direct_open_file_test, read_full_file) {
++chunk; ++chunk;
to_read -= data.size(); to_read -= data.size();
} }
dest_file->close(); dest_file->close();
source_file.close();
auto hash1 = utils::file::file(source_file.get_path()).sha256(); auto hash1 = utils::file::file(source_file.get_path()).sha256();
auto hash2 = utils::file::file(dest_path).sha256(); auto hash2 = utils::file::file(dest_path).sha256();
EXPECT_TRUE(hash1.has_value()); EXPECT_TRUE(hash1.has_value());
EXPECT_TRUE(hash2.has_value()); EXPECT_TRUE(hash2.has_value());
if (hash1.has_value() && hash2.has_value()) { if (hash1.has_value() && hash2.has_value()) {
EXPECT_STREQ(hash1.value().c_str(), hash2.value().c_str()); EXPECT_STREQ(hash1.value().c_str(), hash2.value().c_str());
} }
} }
source_file.close();
} }
TEST_F(direct_open_file_test, read_full_file_in_reverse) { TEST_F(direct_open_file_test, read_full_file_in_reverse) {
auto &source_file = test::create_random_file(test_chunk_size * 32U); auto &source_file = test::create_random_file(test_chunk_size * 32U);
auto dest_path = test::generate_test_file_name("direct_open_file"); auto dest_path = test::generate_test_file_name("direct_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false)); EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
@@ -114,23 +110,29 @@ TEST_F(direct_open_file_test, read_full_file_in_reverse) {
fsi.directory = false; fsi.directory = false;
fsi.size = test_chunk_size * 32U; fsi.size = test_chunk_size * 32U;
std::mutex read_mtx; auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([&read_mtx, &source_file](
const std::string & /* api_path */, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
mutex_lock lock(read_mtx);
EXPECT_FALSE(stop_requested); EXPECT_CALL(provider, read_file_bytes)
std::size_t bytes_read{}; .WillRepeatedly([test_buffer](const std::string &, std::size_t size,
data.resize(size); std::uint64_t offset, data_buffer &data,
auto ret = source_file.read(data, offset, &bytes_read) stop_type &stop_requested) -> api_error {
? api_error::success if (stop_requested) {
: api_error::os_error; return api_error::download_stopped;
EXPECT_EQ(bytes_read, data.size()); }
return ret; std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
}); });
{ {
direct_open_file file(test_chunk_size, 30U, fsi, provider); direct_open_file file(test_chunk_size, 30U, fsi, provider);
@@ -155,7 +157,6 @@ TEST_F(direct_open_file_test, read_full_file_in_reverse) {
auto hash1 = utils::file::file(source_file.get_path()).sha256(); auto hash1 = utils::file::file(source_file.get_path()).sha256();
auto hash2 = utils::file::file(dest_path).sha256(); auto hash2 = utils::file::file(dest_path).sha256();
EXPECT_TRUE(hash1.has_value()); EXPECT_TRUE(hash1.has_value());
EXPECT_TRUE(hash2.has_value()); EXPECT_TRUE(hash2.has_value());
if (hash1.has_value() && hash2.has_value()) { if (hash1.has_value() && hash2.has_value()) {
@@ -166,7 +167,6 @@ TEST_F(direct_open_file_test, read_full_file_in_reverse) {
TEST_F(direct_open_file_test, read_full_file_in_partial_chunks) { TEST_F(direct_open_file_test, read_full_file_in_partial_chunks) {
auto &source_file = test::create_random_file(test_chunk_size * 32U); auto &source_file = test::create_random_file(test_chunk_size * 32U);
auto dest_path = test::generate_test_file_name("test"); auto dest_path = test::generate_test_file_name("test");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false)); EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
@@ -176,23 +176,29 @@ TEST_F(direct_open_file_test, read_full_file_in_partial_chunks) {
fsi.api_path = "/test.txt"; fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 32U; fsi.size = test_chunk_size * 32U;
std::mutex read_mtx; auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([&read_mtx, &source_file](
const std::string & /* api_path */, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
mutex_lock lock(read_mtx);
EXPECT_FALSE(stop_requested); EXPECT_CALL(provider, read_file_bytes)
std::size_t bytes_read{}; .WillRepeatedly([test_buffer](const std::string &, std::size_t size,
data.resize(size); std::uint64_t offset, data_buffer &data,
auto ret = source_file.read(data, offset, &bytes_read) stop_type &stop_requested) -> api_error {
? api_error::success if (stop_requested) {
: api_error::os_error; return api_error::download_stopped;
EXPECT_EQ(bytes_read, data.size()); }
return ret; std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
}); });
{ {
direct_open_file file(test_chunk_size, 30U, fsi, provider); direct_open_file file(test_chunk_size, 30U, fsi, provider);
@@ -214,7 +220,6 @@ TEST_F(direct_open_file_test, read_full_file_in_partial_chunks) {
auto hash1 = utils::file::file(source_file.get_path()).sha256(); auto hash1 = utils::file::file(source_file.get_path()).sha256();
auto hash2 = utils::file::file(dest_path).sha256(); auto hash2 = utils::file::file(dest_path).sha256();
EXPECT_TRUE(hash1.has_value()); EXPECT_TRUE(hash1.has_value());
EXPECT_TRUE(hash2.has_value()); EXPECT_TRUE(hash2.has_value());
if (hash1.has_value() && hash2.has_value()) { if (hash1.has_value() && hash2.has_value()) {
@@ -225,7 +230,6 @@ TEST_F(direct_open_file_test, read_full_file_in_partial_chunks) {
TEST_F(direct_open_file_test, read_full_file_in_partial_chunks_in_reverse) { TEST_F(direct_open_file_test, read_full_file_in_partial_chunks_in_reverse) {
auto &source_file = test::create_random_file(test_chunk_size * 32U); auto &source_file = test::create_random_file(test_chunk_size * 32U);
auto dest_path = test::generate_test_file_name("direct_open_file"); auto dest_path = test::generate_test_file_name("direct_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false)); EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
@@ -235,23 +239,29 @@ TEST_F(direct_open_file_test, read_full_file_in_partial_chunks_in_reverse) {
fsi.api_path = "/test.txt"; fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 32U; fsi.size = test_chunk_size * 32U;
std::mutex read_mtx; auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([&read_mtx, &source_file](
const std::string & /* api_path */, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
mutex_lock lock(read_mtx);
EXPECT_FALSE(stop_requested); EXPECT_CALL(provider, read_file_bytes)
std::size_t bytes_read{}; .WillRepeatedly([test_buffer](const std::string &, std::size_t size,
data.resize(size); std::uint64_t offset, data_buffer &data,
auto ret = source_file.read(data, offset, &bytes_read) stop_type &stop_requested) -> api_error {
? api_error::success if (stop_requested) {
: api_error::os_error; return api_error::download_stopped;
EXPECT_EQ(bytes_read, data.size()); }
return ret; std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
}); });
{ {
direct_open_file file(test_chunk_size, 30U, fsi, provider); direct_open_file file(test_chunk_size, 30U, fsi, provider);
@@ -281,7 +291,6 @@ TEST_F(direct_open_file_test, read_full_file_in_partial_chunks_in_reverse) {
auto hash1 = utils::file::file(source_file.get_path()).sha256(); auto hash1 = utils::file::file(source_file.get_path()).sha256();
auto hash2 = utils::file::file(dest_path).sha256(); auto hash2 = utils::file::file(dest_path).sha256();
EXPECT_TRUE(hash1.has_value()); EXPECT_TRUE(hash1.has_value());
EXPECT_TRUE(hash2.has_value()); EXPECT_TRUE(hash2.has_value());
if (hash1.has_value() && hash2.has_value()) { if (hash1.has_value() && hash2.has_value()) {
@@ -289,4 +298,282 @@ TEST_F(direct_open_file_test, read_full_file_in_partial_chunks_in_reverse) {
} }
} }
} }
TEST_F(direct_open_file_test, clamp_read_past_eof_returns_remaining_only) {
auto &source_file = test::create_random_file(test_chunk_size * 32U + 11U);
auto dest_path = test::generate_test_file_name("direct_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.api_path = "/test.txt";
fsi.directory = false;
fsi.size = test_chunk_size * 32U + 11U;
auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([test_buffer](const std::string &, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
if (stop_requested) {
return api_error::download_stopped;
}
std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
});
{
direct_open_file file(test_chunk_size, 30U, fsi, provider);
auto out = utils::file::file::open_or_create_file(dest_path);
EXPECT_TRUE(out);
std::uint64_t total_read{0U};
for (std::size_t i = 0U; i < 32U; ++i) {
data_buffer data{};
EXPECT_EQ(api_error::success,
file.read(test_chunk_size, total_read, data));
std::size_t bytes_written{};
EXPECT_TRUE(out->write(data, total_read, &bytes_written));
total_read += data.size();
}
{
data_buffer data{};
EXPECT_EQ(api_error::success,
file.read(test_chunk_size, total_read, data));
EXPECT_EQ(std::size_t(11U), data.size());
std::size_t bytes_written{};
EXPECT_TRUE(out->write(data, total_read, &bytes_written));
total_read += data.size();
}
out->close();
source_file.close();
auto h1 = utils::file::file(source_file.get_path()).sha256();
auto h2 = utils::file::file(dest_path).sha256();
ASSERT_TRUE(h1.has_value());
ASSERT_TRUE(h2.has_value());
EXPECT_STREQ(h1->c_str(), h2->c_str());
}
}
TEST_F(direct_open_file_test, cross_boundary_small_read_is_correct) {
auto &source_file = test::create_random_file(test_chunk_size * 4U);
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.api_path = "/test.txt";
fsi.directory = false;
fsi.size = test_chunk_size * 4U;
auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([test_buffer](const std::string &, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
if (stop_requested) {
return api_error::download_stopped;
}
std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
});
{
direct_open_file file(test_chunk_size, 30U, fsi, provider);
std::size_t read_size{7U};
std::uint64_t offset{test_chunk_size - 3U};
data_buffer data{};
EXPECT_EQ(api_error::success, file.read(read_size, offset, data));
EXPECT_EQ(read_size, data.size());
EXPECT_EQ(0,
std::memcmp(test_buffer.data() + offset, data.data(), read_size));
}
source_file.close();
}
TEST_F(direct_open_file_test, random_seek_pattern_reads_match_source) {
auto &source_file = test::create_random_file(test_chunk_size * 16U);
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.api_path = "/test.txt";
fsi.directory = false;
fsi.size = test_chunk_size * 16U;
auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([test_buffer](const std::string &, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
if (stop_requested) {
return api_error::download_stopped;
}
std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
});
{
direct_open_file file(test_chunk_size, 30U, fsi, provider);
struct req {
std::size_t size;
std::uint64_t off;
};
std::vector<req> pattern_list{
{3U, 0U},
{64U, test_chunk_size - 1U},
{11U, test_chunk_size * 2U + 5U},
{512U, test_chunk_size * 7U + 13U},
{5U, test_chunk_size * 16U - 5U},
};
for (const auto &pattern : pattern_list) {
data_buffer data{};
EXPECT_EQ(api_error::success, file.read(pattern.size, pattern.off, data));
std::size_t avail =
(pattern.off < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(pattern.off))
: 0U;
std::size_t expected_size = std::min(pattern.size, avail);
ASSERT_EQ(expected_size, data.size());
EXPECT_EQ(0, std::memcmp(test_buffer.data() + pattern.off, data.data(),
expected_size));
}
}
source_file.close();
}
TEST_F(direct_open_file_test, provider_error_is_propagated) {
auto &source_file = test::create_random_file(test_chunk_size * 4U);
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.api_path = "/test.txt";
fsi.directory = false;
fsi.size = test_chunk_size * 4U;
auto test_buffer = build_test_buffer(source_file);
bool fail_once{true};
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly(
[test_buffer, &fail_once](const std::string &, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
if (stop_requested) {
return api_error::download_stopped;
}
if (fail_once) {
fail_once = false;
return api_error::os_error;
}
std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
});
{
direct_open_file file(test_chunk_size, 30U, fsi, provider);
data_buffer data{};
EXPECT_EQ(api_error::os_error, file.read(17U, 0U, data));
data_buffer data2{};
EXPECT_EQ(api_error::success, file.read(17U, 0U, data2));
EXPECT_EQ(std::size_t(17U), data2.size());
}
source_file.close();
}
TEST_F(direct_open_file_test, tiny_file_smaller_than_chunk) {
auto &source_file = test::create_random_file(17U);
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.api_path = "/test.txt";
fsi.directory = false;
fsi.size = 17U;
auto test_buffer = build_test_buffer(source_file);
EXPECT_CALL(provider, read_file_bytes)
.WillRepeatedly([test_buffer](const std::string &, std::size_t size,
std::uint64_t offset, data_buffer &data,
stop_type &stop_requested) -> api_error {
if (stop_requested) {
return api_error::download_stopped;
}
std::size_t avail =
(offset < test_buffer.size())
? (test_buffer.size() - static_cast<std::size_t>(offset))
: 0U;
std::size_t to_copy = std::min(size, avail);
data.resize(to_copy);
if (to_copy != 0U) {
std::memcpy(data.data(),
test_buffer.data() + static_cast<std::size_t>(offset),
to_copy);
}
return api_error::success;
});
{
direct_open_file file(test_chunk_size, 30U, fsi, provider);
data_buffer data{};
EXPECT_EQ(api_error::success, file.read(test_chunk_size, 0U, data));
EXPECT_EQ(std::size_t(17U), data.size());
}
source_file.close();
}
} // namespace repertory } // namespace repertory

View File

@@ -113,6 +113,114 @@ TYPED_TEST(fuse_test, directory_can_opendir_after_closedir) {
this->rmdir_and_test(dir); this->rmdir_and_test(dir);
} }
TYPED_TEST(fuse_test, directory_rmdir_on_non_empty_directory_should_fail) {
if (this->current_provider == provider_type::encrypt) {
GTEST_SKIP();
return;
}
std::string dir_name{"non_empty"};
auto dir = this->create_directory_and_test(dir_name);
std::string dir_name2{"non_empty_2"};
auto dir2 = this->create_directory_and_test(dir_name2);
std::string name{dir_name + "/child"};
auto file = this->create_file_and_test(name, 0644);
this->overwrite_text(file, "X");
errno = 0;
EXPECT_EQ(-1, ::rmdir(dir.c_str()));
EXPECT_EQ(ENOTEMPTY, errno);
this->unlink_file_and_test(file);
this->rmdir_and_test(dir);
this->rmdir_and_test(dir2);
}
TYPED_TEST(fuse_test,
directory_rmdir_open_directory_handle_then_readdir_and_closedir) {
std::string dname{"rm_opendir"};
auto dir = this->create_directory_and_test(dname);
auto *dir_ptr = ::opendir(dir.c_str());
ASSERT_NE(dir_ptr, nullptr);
errno = 0;
auto res = ::rmdir(dir.c_str());
if (res == -1 && errno == EBUSY) {
::closedir(dir_ptr);
GTEST_SKIP();
}
ASSERT_EQ(0, res);
struct stat u_stat{};
EXPECT_EQ(-1, ::stat(dir.c_str(), &u_stat));
EXPECT_EQ(ENOENT, errno);
::rewinddir(dir_ptr);
errno = 0;
auto *dir_entry = ::readdir(dir_ptr);
EXPECT_EQ(nullptr, dir_entry);
::closedir(dir_ptr);
}
TYPED_TEST(fuse_test,
directory_rmdir_open_directory_handle_non_empty_ENOTEMPTY) {
std::string dname{"rm_opendir_ne"};
auto dir = this->create_directory_and_test(dname);
std::string child{dname + "/child"};
auto child_path = this->create_file_and_test(child, 0644);
this->overwrite_text(child_path, "x");
auto *dir_ptr = ::opendir(dir.c_str());
ASSERT_NE(dir_ptr, nullptr);
errno = 0;
EXPECT_EQ(-1, ::rmdir(dir.c_str()));
EXPECT_EQ(ENOTEMPTY, errno);
::rewinddir(dir_ptr);
[[maybe_unused]] auto *dir_ptr2 = ::readdir(dir_ptr);
::closedir(dir_ptr);
this->unlink_file_and_test(child_path);
this->rmdir_and_test(dir);
}
// TODO implement POSIX-compliant rmdir when handle is open
// TYPED_TEST(fuse_test, directory_rmdir_open_directory_handle_fstat_dirfd_ok) {
// std::string dname{"rm_opendir_fstat"};
// auto dir = this->create_directory_and_test(dname);
//
// auto *dir_ptr = ::opendir(dir.c_str());
// ASSERT_NE(dir_ptr, nullptr);
// auto dfd = ::dirfd(dir_ptr);
// ASSERT_NE(dfd, -1);
//
// struct stat before{};
// ASSERT_EQ(0, ::fstat(dfd, &before));
//
// errno = 0;
// auto res = ::rmdir(dir.c_str());
// if (res == -1 && errno == EBUSY) {
// ::closedir(dir_ptr);
// GTEST_SKIP();
// }
// ASSERT_EQ(0, res);
//
// struct stat after{};
// ASSERT_EQ(0, ::fstat(dfd, &after));
//
// ::closedir(dir_ptr);
//
// struct stat u_stat{};
// EXPECT_EQ(-1, ::stat(dir.c_str(), &u_stat));
// EXPECT_EQ(ENOENT, errno);
// }
} // namespace repertory } // namespace repertory
#endif // !defined(_WIN32) #endif // !defined(_WIN32)

View File

@@ -19,6 +19,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
*/ */
#include <gtest/gtest.h>
#if !defined(_WIN32) #if !defined(_WIN32)
#include "fixtures/drive_fixture.hpp" #include "fixtures/drive_fixture.hpp"
@@ -26,14 +27,13 @@
namespace repertory { namespace repertory {
TYPED_TEST_SUITE(fuse_test, platform_provider_types); TYPED_TEST_SUITE(fuse_test, platform_provider_types);
TYPED_TEST(fuse_test, fallocate_basic_preallocation_platform_semantics) { TYPED_TEST(fuse_test, fallocate_can_handle_preallocate) {
std::string name{"fallocate"}; std::string name{"fallocate"};
auto src = this->create_file_and_test(name); auto src = this->create_file_and_test(name);
auto desc = ::open(src.c_str(), O_RDWR); auto desc = ::open(src.c_str(), O_RDWR);
ASSERT_NE(desc, -1); ASSERT_NE(desc, -1);
constexpr off_t off = 0;
constexpr off_t len = 64 * 1024; constexpr off_t len = 64 * 1024;
#if defined(__APPLE__) #if defined(__APPLE__)
@@ -45,17 +45,15 @@ TYPED_TEST(fuse_test, fallocate_basic_preallocation_platform_semantics) {
store.fst_bytesalloc = 0; store.fst_bytesalloc = 0;
auto res = ::fcntl(desc, F_PREALLOCATE, &store); auto res = ::fcntl(desc, F_PREALLOCATE, &store);
if (res == -1) { EXPECT_EQ(res, -1);
store.fst_flags = F_ALLOCATEALL;
res = ::fcntl(desc, F_PREALLOCATE, &store);
}
EXPECT_EQ(0, res);
struct stat st_unix{}; struct stat st_unix{};
EXPECT_EQ(0, ::fstat(desc, &st_unix)); EXPECT_EQ(0, ::fstat(desc, &st_unix));
EXPECT_TRUE(S_ISREG(st_unix.st_mode)); EXPECT_TRUE(S_ISREG(st_unix.st_mode));
EXPECT_EQ(0, st_unix.st_size); EXPECT_EQ(0, st_unix.st_size);
#else // !defined(__APPLE__) #else // !defined(__APPLE__)
constexpr off_t off = 0;
auto res = ::posix_fallocate(desc, off, len); auto res = ::posix_fallocate(desc, off, len);
if (res == EOPNOTSUPP) { if (res == EOPNOTSUPP) {
::close(desc); ::close(desc);
@@ -91,17 +89,8 @@ TYPED_TEST(fuse_test, fallocate_then_ftruncate_makes_size_visible) {
store.fst_length = len; store.fst_length = len;
store.fst_bytesalloc = 0; store.fst_bytesalloc = 0;
auto res = ::fcntl(desc, F_PREALLOCATE, &store); auto res = ::fcntl(desc, F_PREALLOCATE, &store);
if (res == -1) { EXPECT_EQ(res, -1);
store.fst_flags = F_ALLOCATEALL;
res = ::fcntl(desc, F_PREALLOCATE, &store);
}
if (res == EOPNOTSUPP) {
::close(desc);
this->unlink_file_and_test(src);
return;
}
EXPECT_EQ(0, res);
EXPECT_EQ(0, ::ftruncate(desc, len)); EXPECT_EQ(0, ::ftruncate(desc, len));
#else // !defined(__APPLE__) #else // !defined(__APPLE__)
auto res = ::posix_fallocate(desc, 0, len); auto res = ::posix_fallocate(desc, 0, len);
@@ -205,14 +194,7 @@ TYPED_TEST(fuse_test, fallocate_can_handle_invalid_arguments) {
store.fst_length = 0; store.fst_length = 0;
errno = 0; errno = 0;
auto res = ::fcntl(desc, F_PREALLOCATE, &store); auto res = ::fcntl(desc, F_PREALLOCATE, &store);
if (res == 0) { EXPECT_EQ(res, -1);
::close(desc);
this->unlink_file_and_test(src);
return;
}
EXPECT_EQ(-1, res);
EXPECT_TRUE(errno == EINVAL || errno == EOPNOTSUPP || errno == ENOSYS);
#else // !defined(__APPLE__) #else // !defined(__APPLE__)
auto ret1 = ::posix_fallocate(desc, -1, 4096); auto ret1 = ::posix_fallocate(desc, -1, 4096);
EXPECT_EQ(EINVAL, ret1); EXPECT_EQ(EINVAL, ret1);
@@ -241,19 +223,16 @@ TYPED_TEST(fuse_test, fallocate_fails_on_directory) {
errno = 0; errno = 0;
auto res = ::fcntl(desc, F_PREALLOCATE, &store); auto res = ::fcntl(desc, F_PREALLOCATE, &store);
EXPECT_EQ(-1, res); EXPECT_EQ(res, -1);
EXPECT_TRUE(errno == EISDIR || errno == EBADF || errno == EOPNOTSUPP ||
errno == ENOTTY || errno == ENOSYS);
::close(desc);
#else // !defined(__APPLE__) #else // !defined(__APPLE__)
auto desc = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY); auto desc = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY);
EXPECT_NE(desc, -1); EXPECT_NE(desc, -1);
auto ret = ::posix_fallocate(desc, 0, 4096); auto ret = ::posix_fallocate(desc, 0, 4096);
EXPECT_NE(0, ret); EXPECT_NE(0, ret);
::close(desc);
#endif // defined(__APPLE__) #endif // defined(__APPLE__)
::close(desc);
this->rmdir_and_test(dir); this->rmdir_and_test(dir);
} }
} // namespace repertory } // namespace repertory

View File

@@ -0,0 +1,129 @@
/*
Copyright <2018-2025> <scott.e.graves@protonmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <gtest/gtest.h>
#if !defined(_WIN32)
#include "fixtures/drive_fixture.hpp"
namespace repertory {
TYPED_TEST_SUITE(fuse_test, platform_provider_types);
TYPED_TEST(fuse_test, fsync_basic_succeeds_on_dirty_desc) {
std::string name{"fsync_dirty"};
auto path = this->create_file_and_test(name, 0644);
auto desc = ::open(path.c_str(), O_RDWR);
ASSERT_NE(desc, -1);
this->write_all(desc, "ABC");
errno = 0;
ASSERT_EQ(0, ::fsync(desc));
::close(desc);
EXPECT_EQ("ABC", this->slurp(path));
this->unlink_file_and_test(path);
}
TYPED_TEST(fuse_test, fsync_noop_on_clean_desc) {
std::string name{"fsync_clean"};
auto path = this->create_file_and_test(name, 0644);
auto desc = ::open(path.c_str(), O_RDONLY);
ASSERT_NE(desc, -1);
errno = 0;
EXPECT_EQ(0, ::fsync(desc));
::close(desc);
this->unlink_file_and_test(path);
}
TYPED_TEST(fuse_test, fsync_on_unlinked_file) {
#if defined(__APPLE__)
GTEST_SKIP();
return;
#endif // defined(__APPLE__)
std::string name{"fsync_unlinked"};
auto path = this->create_file_and_test(name, 0644);
auto desc = ::open(path.c_str(), O_RDWR);
ASSERT_NE(desc, -1);
ASSERT_EQ(0, ::unlink(path.c_str()));
this->write_all(desc, "XYZ");
errno = 0;
EXPECT_EQ(0, ::fsync(desc));
::close(desc);
}
TYPED_TEST(fuse_test, fsync_after_rename) {
if (this->current_provider != provider_type::sia) {
// TODO finish test
GTEST_SKIP();
return;
}
std::string src_name{"fsync_ren_src"};
auto src = this->create_file_and_test(src_name, 0644);
auto desc = ::open(src.c_str(), O_RDWR);
ASSERT_NE(desc, -1);
this->write_all(desc, "AAA");
std::string dst_name{"fsync_ren_dst"};
auto dst =
utils::path::combine(utils::path::get_parent_path(src), {dst_name});
errno = 0;
ASSERT_EQ(0, ::rename(src.c_str(), dst.c_str()));
this->write_all(desc, "_BBB");
errno = 0;
ASSERT_EQ(0, ::fsync(desc));
::close(desc);
EXPECT_EQ("AAA_BBB", this->slurp(dst));
this->unlink_file_and_test(dst);
}
#if defined(__linux__)
TYPED_TEST(fuse_test, fsync_fdatasync_behaves_like_fsync_on_linux) {
std::string name{"descatasync_linux"};
auto path = this->create_file_and_test(name, 0644);
auto desc = ::open(path.c_str(), O_RDWR);
ASSERT_NE(desc, -1);
this->write_all(desc, "DATA");
errno = 0;
ASSERT_EQ(0, ::fdatasync(desc));
::close(desc);
EXPECT_EQ("DATA", this->slurp(path));
this->unlink_file_and_test(path);
}
#endif // defined(__linux__)
} // namespace repertory
#endif // !defined(_WIN32)

View File

@@ -205,6 +205,38 @@ TYPED_TEST(fuse_test, rdrw_can_append_to_file) {
this->unlink_file_and_test(file_path); this->unlink_file_and_test(file_path);
} }
TYPED_TEST(fuse_test, rdrw_open_with_o_trunc_resets_size) {
std::string name{"trunc_test"};
auto path = this->create_file_and_test(name, 0644);
this->overwrite_text(path, "ABCDEFG");
EXPECT_GT(this->stat_size(path), 0);
auto desc = ::open(path.c_str(), O_WRONLY | O_TRUNC);
ASSERT_NE(desc, -1);
::close(desc);
EXPECT_EQ(0, this->stat_size(path));
this->unlink_file_and_test(path);
}
TYPED_TEST(fuse_test, rdrw_o_append_writes_at_eof) {
std::string name{"append_test"};
auto path = this->create_file_and_test(name, 0644);
this->overwrite_text(path, "HEAD");
auto desc = ::open(path.c_str(), O_WRONLY | O_APPEND);
ASSERT_NE(desc, -1);
ASSERT_NE(-1, ::lseek(desc, 0, SEEK_SET));
this->write_all(desc, "TAIL");
::close(desc);
EXPECT_EQ("HEADTAIL", this->slurp(path));
this->unlink_file_and_test(path);
}
} // namespace repertory } // namespace repertory
#endif // !defined(_WIN32) #endif // !defined(_WIN32)

View File

@@ -38,6 +38,12 @@ TYPED_TEST(fuse_test, unlink_can_remove_file) {
} }
TYPED_TEST(fuse_test, unlink_open_file_leaves_handle_intact) { TYPED_TEST(fuse_test, unlink_open_file_leaves_handle_intact) {
#if defined(__APPLE__)
// TODO fgetattr() not supported
GTEST_SKIP();
return;
#endif // defined(__APPLE__)
std::string name{"unlink"}; std::string name{"unlink"};
auto path = this->create_file_and_test(name); auto path = this->create_file_and_test(name);
@@ -63,11 +69,12 @@ TYPED_TEST(fuse_test, unlink_open_file_leaves_handle_intact) {
std::string out; std::string out;
char buf[4096]; char buf[4096];
for (;;) { for (;;) {
ssize_t r = ::read(desc, buf, sizeof(buf)); res = ::read(desc, buf, sizeof(buf));
ASSERT_NE(r, -1); ASSERT_NE(res, -1);
if (r == 0) if (res == 0) {
break; break;
out.append(buf, buf + r); }
out.append(buf, buf + res);
} }
::close(desc); ::close(desc);
@@ -89,7 +96,11 @@ TYPED_TEST(fuse_test, unlink_directory_fails) {
errno = 0; errno = 0;
EXPECT_EQ(-1, ::unlink(dir.c_str())); EXPECT_EQ(-1, ::unlink(dir.c_str()));
#if defined(__APPLE__)
EXPECT_EQ(EPERM, errno);
#else // !defined(__APPLE__)
EXPECT_EQ(EISDIR, errno); EXPECT_EQ(EISDIR, errno);
#endif // defined(__APPLE__)
this->rmdir_and_test(dir); this->rmdir_and_test(dir);
} }

View File

@@ -74,8 +74,9 @@ void get_times_ns(const std::string &path, long long &at_ns, long long &mt_ns) {
} }
constexpr long long GRANULAR_TOL_NS = constexpr long long GRANULAR_TOL_NS =
1LL * static_cast<long long>(NANOS_PER_SECOND); 12LL * static_cast<long long>(NANOS_PER_SECOND);
constexpr long long NOW_TOL_NS = 5LL * static_cast<long long>(NANOS_PER_SECOND); constexpr long long NOW_TOL_NS =
15LL * static_cast<long long>(NANOS_PER_SECOND);
} // namespace } // namespace
namespace repertory { namespace repertory {

View File

@@ -20,275 +20,103 @@
#include "comm/packet/common.hpp" #include "comm/packet/common.hpp"
#include "comm/packet/packet.hpp" #include "comm/packet/packet.hpp"
#include "comm/packet/packet_client.hpp" #include "comm/packet/packet_client.hpp"
#include "comm/packet/packet_server.hpp"
#include "types/remote.hpp" #include "types/remote.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
#include "version.hpp" #include "version.hpp"
using namespace repertory; using namespace repertory;
using namespace repertory::comm; using namespace repertory::comm;
using boost::asio::ip::tcp;
namespace { namespace {
void write_all(tcp::socket &sock, const void *data, std::size_t size) { class test_packet_server final {
boost::asio::write(sock, boost::asio::buffer(data, size)); public:
} test_packet_server(std::uint16_t port, std::string token,
std::uint8_t pool_size = 2)
: server_(std::make_unique<packet_server>(
port, std::move(token), pool_size,
[](const std::string & /*client_id*/) {},
[](std::uint32_t /*service_flags_in*/,
const std::string & /*client_id*/, std::uint64_t /*thread_id*/,
const std::string &method, packet * /*request*/,
packet & /*response*/,
packet_server::message_complete_callback done) {
if (method == "ping") {
done(packet::error_type{0});
} else {
done(packet::error_type{-1});
}
})) {}
void read_exact(tcp::socket &sock, void *data, std::size_t size) { private:
boost::asio::read(sock, boost::asio::buffer(data, size)); std::unique_ptr<packet_server> server_;
}
struct test_server final {
std::string encryption_token;
std::atomic<std::uint16_t> port{0};
std::thread server_thread;
bool send_initial_nonce{false};
bool respond_to_send{false};
std::uint32_t response_service_flags{0};
explicit test_server(std::string token, bool send_nonce,
bool do_send_response, std::uint32_t svc_flags = 0U)
: encryption_token(std::move(token)),
send_initial_nonce(send_nonce),
respond_to_send(do_send_response),
response_service_flags(svc_flags) {}
void start() {
std::promise<void> ready;
server_thread = std::thread([this, &ready]() {
try {
boost::asio::io_context io_ctx;
tcp::acceptor acceptor(io_ctx, tcp::endpoint(tcp::v4(), 0));
port.store(acceptor.local_endpoint().port(), std::memory_order_relaxed);
ready.set_value();
tcp::socket sock(io_ctx);
acceptor.accept(sock);
packet handshake_pkt;
auto min_version = utils::get_version_number(project_get_version());
handshake_pkt.encode(static_cast<std::uint32_t>(min_version));
handshake_pkt.encode(static_cast<std::uint32_t>(~min_version));
handshake_pkt.encode(utils::generate_random_string(packet_nonce_size));
data_buffer out;
handshake_pkt.to_buffer(out);
write_all(sock, out.data(), out.size());
std::vector<unsigned char> echo(out.size());
if (not echo.empty()) {
read_exact(sock, echo.data(), echo.size());
}
std::string last_nonce{};
const auto generate_response = [&]() -> auto {
last_nonce = utils::generate_random_string(packet_nonce_size);
packet resp;
resp.encode(server_nonce);
resp.encode(response_service_flags);
resp.encode(packet::error_type{});
resp.encrypt(encryption_token);
return resp;
};
if (send_initial_nonce) {
auto resp = generate_response();
write_all(sock, &resp[0], resp.get_size());
}
if (respond_to_send) {
std::uint32_t req_net_len{};
if (read_for_size(sock, req_net_len)) {
boost::endian::big_to_native_inplace(req_net_len);
EXPECT_GT(req_net_len, 0);
if (req_net_len > 0) {
data_buffer buffer(req_net_len);
read_exact(sock, buffer.data(), buffer.size());
packet response(buffer);
EXPECT_EQ(0, response.decrypt(token));
std::string nonce;
EXPECT_EQ(0, response.decode(nonce));
EXPECT_STREQ(last_nonce.c_str(), nonce.c_str());
std::string version;
EXPECT_EQ(0, response.decode(version));
std::uint32_t service_flags{};
EXPECT_EQ(0, response.decode(service_flags));
std::string client_id;
EXPECT_EQ(0, response.decode(client_id));
std::string thread_id;
EXPECT_EQ(0, response.decode(thread_id));
std::string method;
EXPECT_EQ(0, response.decode(method));
EXPECT_STREQ("ping", method.c_str());
}
}
auto resp = generate_response();
write_all(sock, &resp[0], resp.get_size());
}
sock.close();
} catch (...) {
}
});
ready.get_future().wait();
}
static bool read_for_size(tcp::socket &sock, std::uint32_t &net_len) {
boost::system::error_code err;
auto count = boost::asio::read(
sock, boost::asio::buffer(&net_len, sizeof(net_len)), err);
return not err && count == sizeof(net_len);
}
void stop() {
if (server_thread.joinable()) {
server_thread.join();
}
}
}; };
remote::remote_config make_cfg(std::uint16_t port, const std::string &token) { inline auto make_cfg(std::uint16_t port, const std::string &token)
return remote::remote_config{ -> remote::remote_config {
.host_name_or_ip = "127.0.0.1", remote::remote_config cfg{};
.api_port = p, cfg.host_name_or_ip = "127.0.0.1";
.max_connections = 2U, cfg.api_port = port;
.conn_timeout_ms = 1500U, cfg.max_connections = 2U;
.recv_timeout_ms = 1500U, cfg.conn_timeout_ms = 1500U;
.send_timeout_ms = 1500U, cfg.recv_timeout_ms = 1500U;
.encryption_token = token, cfg.send_timeout_ms = 1500U;
}; cfg.encryption_token = token;
return cfg;
} }
} // namespace
TEST(packet_client_test, can_check_version) { TEST(packet_client_test, can_check_version) {
std::string token = "cow_moose_doge_chicken"; std::string token = "cow_moose_doge_chicken";
test_server srv(token, false, false); std::uint16_t port{};
srv.start(); ASSERT_TRUE(utils::get_next_available_port(50000U, port));
packet_client client(make_cfg(srv.port.load(), token)); test_packet_server server(port, token);
packet_client client(make_cfg(port, token));
std::uint32_t min_version{}; std::uint32_t min_version{};
auto res = client.check_version( auto api = client.check_version(
utils::get_version_number(project_get_version()), min_version); utils::get_version_number(project_get_version()), min_version);
EXPECT_EQ(res, api_error::success); EXPECT_EQ(api, api_error::success);
EXPECT_NE(min_version, 0U); EXPECT_NE(min_version, 0U);
}
srv.stop(); TEST(packet_client_test, can_detect_incompatible_version) {
std::string token = "cow_moose_doge_chicken";
std::uint16_t port{};
ASSERT_TRUE(utils::get_next_available_port(50000U, port));
test_packet_server server(port, token);
packet_client client(make_cfg(port, token));
std::uint32_t min_version{};
auto api =
client.check_version(utils::get_version_number("1.0.0-rc"), min_version);
EXPECT_EQ(api, api_error::incompatible_version);
EXPECT_NE(min_version, 0U);
} }
TEST(packet_client_test, can_send_request_and_receive_response) { TEST(packet_client_test, can_send_request_and_receive_response) {
std::string token = "cow_moose_doge_chicken"; std::string token = "cow_moose_doge_chicken";
std::uint32_t svc_flags_server = 0xA5A5A5A5U;
test_server srv(token, true, true, svc_flags_server); std::uint16_t port{};
srv.start(); ASSERT_TRUE(utils::get_next_available_port(50000U, port));
packet_client client(make_cfg(srv.port.load(), token)); test_packet_server server(port, token);
packet_client client(make_cfg(port, token));
std::uint32_t service_flags{}; std::uint32_t service_flags{};
packet req; packet request;
packet resp; packet response;
auto ret = client.send("ping", request, response, service_flags);
auto ret = client.send("ping", req, resp, service_flags);
EXPECT_EQ(ret, 0); EXPECT_EQ(ret, 0);
EXPECT_EQ(service_flags, svc_flags_server);
srv.stop();
} }
TEST(packet_client_test, pooled_connection_reused_on_second_send) {
std::string token{"test_token"};
std::uint16_t port{};
ASSERT_TRUE(utils::get_next_available_port(50000U, port));
std::atomic<std::uint32_t> close_count{0U};
packet_server server{
port, token, 2U,
[&close_count](const std::string & /*client_id*/) { ++close_count; },
[](std::uint32_t /*service_flags_in*/, const std::string & /*client_id*/,
std::uint64_t /*thread_id*/, const std::string &method,
packet * /*request*/, packet & /*response*/,
packet_server::message_complete_callback done) {
if (method == "ping") {
done(packet::error_type{0});
} else {
done(packet::error_type{-1});
}
}};
packet_client client(::make_cfg(port, token));
std::uint32_t service_flags{};
packet req_one;
packet resp_one;
auto ret_one = client.send("ping", req_one, resp_one, service_flags);
EXPECT_EQ(ret_one, 0);
packet req_two;
packet resp_two;
auto ret_two = client.send("ping", req_two, resp_two, service_flags);
EXPECT_EQ(ret_two, 0);
EXPECT_EQ(close_count.load(), 0U);
}
TEST(packet_client_test, reconnects_when_server_closes_socket) {
std::string token{"test_token"};
std::uint16_t port{};
ASSERT_TRUE(utils::get_next_available_port(50000U, port));
std::atomic<std::uint32_t> close_count{0U};
std::shared_ptr<connection> last_conn;
packet_server server{
port, token, 2U,
[&close_count](const std::string & /*client_id*/) { ++close_count; },
[&last_conn](std::uint32_t /*service_flags_in*/,
const std::string & /*client_id*/,
std::uint64_t /*thread_id*/, const std::string &method,
packet * /*request*/, packet & /*response*/,
packet_server::message_complete_callback done) {
if (method == "ping") {
done(packet::error_type{0});
} else {
done(packet::error_type{-1});
}
}};
packet_client client(::make_cfg(port, token));
std::uint32_t service_flags{};
packet req_one;
packet resp_one;
auto ret_one = client.send("ping", req_one, resp_one, service_flags);
EXPECT_EQ(ret_one, 0);
{
std::lock_guard<std::mutex> guard(server.conn_mutex_);
if (not server.connections_.empty()) {
auto conn = *server.connections_.begin();
boost::system::error_code ec;
conn->socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
conn->socket().close(ec);
}
}
packet req_two;
packet resp_two;
auto ret_two = client.send("ping", req_two, resp_two, service_flags);
EXPECT_EQ(ret_two, 0);
EXPECT_EQ(close_count.load(), 1U);
}
} // namespace

View File

@@ -0,0 +1,74 @@
/*
Copyright <2018-2025> <scott.e.graves@protonmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: The above copyright
notice and this permission notice shall be included in all copies or
substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS",
WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "test_common.hpp"
#include "comm/packet/common.hpp"
using namespace repertory;
using namespace repertory::comm;
using boost::asio::ip::tcp;
TEST(packet_comm_common_test, operation_completes_prior_to_timeout) {
boost::asio::io_context io_ctx;
tcp::socket sock(io_ctx);
std::atomic<bool> completed{false};
auto operation = [&](auto &&handler) {
boost::asio::post(io_ctx, [&completed, handler]() {
completed = true;
handler(boost::system::error_code{});
});
};
EXPECT_NO_THROW(run_with_deadline(io_ctx, sock, operation,
std::chrono::milliseconds(300), "read",
"packet_deadline_test"));
EXPECT_TRUE(completed);
io_ctx.poll();
}
TEST(packet_comm_common_test, timeout_completes_prior_to_operation) {
boost::asio::io_context io_ctx;
tcp::socket sock(io_ctx);
std::atomic<bool> completed{false};
auto operation = [&](auto &&handler) {
auto delayed = std::make_shared<boost::asio::steady_timer>(io_ctx);
delayed->expires_after(std::chrono::milliseconds(500));
delayed->async_wait([&completed, delayed, handler](auto &&) {
completed = true;
handler(boost::system::error_code{});
});
};
EXPECT_THROW(run_with_deadline(io_ctx, sock, operation,
std::chrono::milliseconds(300), "read",
"packet_deadline_test"),
std::runtime_error);
for (std::uint8_t idx = 0; idx < 80U && not completed; ++idx) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
io_ctx.poll();
}
EXPECT_TRUE(completed);
}

View File

@@ -1,27 +1,24 @@
/* /* Copyright <2018-2025>
Copyright <2018-2025> <scott.e.graves@protonmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Permission is hereby granted, free of charge, to any person obtaining a copy in the Software without restriction, including without limitation the rights
of this software and associated documentation files (the "Software"), to deal to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
in the Software without restriction, including without limitation the rights copies of the Software, and to permit persons to whom the Software is
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell furnished to do so, subject to the following conditions:
copies of the Software, and to permit persons to whom the Software is The above copyright notice and this permission notice shall be included in
furnished to do so, subject to the following conditions: all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
The above copyright notice and this permission notice shall be included in all IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
copies or substantial portions of the Software. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE SOFTWARE.
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/ */
#include "test_common.hpp" #include "test_common.hpp"
#include "comm/packet/packet.hpp" #include "comm/packet/packet.hpp"
#include "types/remote.hpp"
namespace repertory { namespace repertory {
TEST(packet_test, encrypt_and_decrypt) { TEST(packet_test, encrypt_and_decrypt) {
@@ -35,7 +32,435 @@ TEST(packet_test, encrypt_and_decrypt) {
std::string data; std::string data;
EXPECT_EQ(0, test_packet.decode(data)); EXPECT_EQ(0, test_packet.decode(data));
EXPECT_STREQ("test", data.c_str()); EXPECT_STREQ("test", data.c_str());
} }
TEST(packet_test, encode_decode_primitives_and_strings) {
packet pkt;
const std::int8_t i8{-12};
const std::uint8_t u8{250};
const std::int16_t i16{-12345};
const std::uint16_t u16{54321};
const std::int32_t i32{-123456789};
const std::uint32_t u32{3141592653U};
const std::int64_t i64{-1234567890123456789LL};
const std::uint64_t u64{12345678901234567890ULL};
const std::string s{"hello world"};
const std::wstring ws{L"wide 🌟"};
pkt.encode(i8);
pkt.encode(u8);
pkt.encode(i16);
pkt.encode(u16);
pkt.encode(i32);
pkt.encode(u32);
pkt.encode(i64);
pkt.encode(u64);
pkt.encode(s);
pkt.encode(ws);
std::int8_t i8_r{};
std::uint8_t u8_r{};
std::int16_t i16_r{};
std::uint16_t u16_r{};
std::int32_t i32_r{};
std::uint32_t u32_r{};
std::int64_t i64_r{};
std::uint64_t u64_r{};
std::string s_r;
std::wstring ws_r;
EXPECT_EQ(0, pkt.decode(i8_r));
EXPECT_EQ(0, pkt.decode(u8_r));
EXPECT_EQ(0, pkt.decode(i16_r));
EXPECT_EQ(0, pkt.decode(u16_r));
EXPECT_EQ(0, pkt.decode(i32_r));
EXPECT_EQ(0, pkt.decode(u32_r));
EXPECT_EQ(0, pkt.decode(i64_r));
EXPECT_EQ(0, pkt.decode(u64_r));
EXPECT_EQ(0, pkt.decode(s_r));
EXPECT_EQ(0, pkt.decode(ws_r));
EXPECT_EQ(i8, i8_r);
EXPECT_EQ(u8, u8_r);
EXPECT_EQ(i16, i16_r);
EXPECT_EQ(u16, u16_r);
EXPECT_EQ(i32, i32_r);
EXPECT_EQ(u32, u32_r);
EXPECT_EQ(i64, i64_r);
EXPECT_EQ(u64, u64_r);
EXPECT_EQ(s, s_r);
EXPECT_EQ(ws, ws_r);
}
TEST(packet_test, encode_decode_null_c_string_is_empty) {
packet pkt;
const char *p_null{nullptr};
pkt.encode(p_null);
std::string out;
EXPECT_EQ(0, pkt.decode(out));
EXPECT_TRUE(out.empty());
}
TEST(packet_test, encode_decode_raw_buffer) {
packet pkt;
std::array<unsigned char, 32> src{};
for (std::size_t i = 0; i < src.size(); ++i) {
src[i] = static_cast<unsigned char>(i);
}
pkt.encode(src.data(), src.size());
std::array<unsigned char, 32> dst{};
EXPECT_EQ(0, pkt.decode(dst.data(), dst.size()));
EXPECT_EQ(0, std::memcmp(src.data(), dst.data(), dst.size()));
}
TEST(packet_test, encode_decode_pointer_round_trip) {
packet pkt;
int val{42};
void *in_ptr = &val;
pkt.encode(in_ptr);
void *out_ptr = nullptr;
EXPECT_EQ(0, pkt.decode(out_ptr));
EXPECT_EQ(in_ptr, out_ptr);
}
TEST(packet_test, encode_top_affects_decode_order) {
packet pkt;
pkt.encode(static_cast<std::uint32_t>(1));
pkt.encode(static_cast<std::uint32_t>(2));
pkt.encode_top(static_cast<std::uint32_t>(99));
std::uint32_t a{}, b{}, c{};
EXPECT_EQ(0, pkt.decode(a));
EXPECT_EQ(0, pkt.decode(b));
EXPECT_EQ(0, pkt.decode(c));
EXPECT_EQ(99U, a);
EXPECT_EQ(1U, b);
EXPECT_EQ(2U, c);
}
TEST(packet_test, copy_ctor_preserves_decode_offset) {
packet pkt;
pkt.encode(static_cast<std::uint32_t>(10));
pkt.encode(static_cast<std::uint32_t>(20));
pkt.encode(static_cast<std::uint32_t>(30));
std::uint32_t first{};
ASSERT_EQ(0, pkt.decode(first));
ASSERT_EQ(10U, first);
packet pkt_copy{pkt};
std::uint32_t from_copy{};
EXPECT_EQ(0, pkt_copy.decode(from_copy));
EXPECT_EQ(20U, from_copy);
std::uint32_t from_src{};
EXPECT_EQ(0, pkt.decode(from_src));
EXPECT_EQ(20U, from_src);
}
TEST(packet_test, copy_assign_preserves_decode_offset) {
packet src;
src.encode(static_cast<std::uint32_t>(1));
src.encode(static_cast<std::uint32_t>(2));
src.encode(static_cast<std::uint32_t>(3));
std::uint32_t first{};
ASSERT_EQ(0, src.decode(first));
ASSERT_EQ(1U, first);
packet dst;
dst = src;
std::uint32_t from_dst{};
EXPECT_EQ(0, dst.decode(from_dst));
EXPECT_EQ(2U, from_dst);
std::uint32_t from_src{};
EXPECT_EQ(0, src.decode(from_src));
EXPECT_EQ(2U, from_src);
}
TEST(packet_test, move_ctor_preserves_decode_offset) {
packet pkt;
pkt.encode(static_cast<std::uint32_t>(100));
pkt.encode(static_cast<std::uint32_t>(200));
std::uint32_t x{};
ASSERT_EQ(0, pkt.decode(x));
ASSERT_EQ(100U, x);
packet moved{std::move(pkt)};
std::uint32_t y{};
EXPECT_EQ(0, moved.decode(y));
EXPECT_EQ(200U, y);
}
TEST(packet_test, move_assign_preserves_decode_offset) {
packet src;
src.encode(static_cast<std::uint32_t>(7));
src.encode(static_cast<std::uint32_t>(8));
src.encode(static_cast<std::uint32_t>(9));
std::uint32_t first{};
ASSERT_EQ(0, src.decode(first));
ASSERT_EQ(7U, first);
packet dst;
dst.encode(
static_cast<std::uint32_t>(999)); // give dst some state to overwrite
dst = std::move(src);
std::uint32_t next{};
EXPECT_EQ(0, dst.decode(next));
EXPECT_EQ(8U, next);
}
TEST(packet_test, operator_index_read_write_byte) {
packet pkt;
pkt.encode(static_cast<std::uint8_t>(7));
EXPECT_EQ(pkt[0], static_cast<unsigned char>(7));
pkt[0] = static_cast<unsigned char>(8);
std::uint8_t out{};
EXPECT_EQ(0, pkt.decode(out));
EXPECT_EQ(8U, out);
}
TEST(packet_test, current_pointer_tracks_decode_offset) {
packet pkt;
pkt.encode(static_cast<std::uint8_t>(1));
pkt.encode(static_cast<std::uint8_t>(2));
std::uint8_t first{};
EXPECT_EQ(0, pkt.decode(first));
EXPECT_EQ(1U, first);
auto *ptr = pkt.current_pointer();
ASSERT_NE(ptr, nullptr);
EXPECT_EQ(*ptr, static_cast<unsigned char>(2));
const auto &const_pkt = pkt;
const auto *cptr = const_pkt.current_pointer();
ASSERT_NE(cptr, nullptr);
EXPECT_EQ(*cptr, static_cast<unsigned char>(2));
}
TEST(packet_test, open_flags_round_trip) {
packet pkt;
remote::open_flags flags = remote::open_flags::read_only |
remote::open_flags::create |
remote::open_flags::truncate;
pkt.encode(flags);
remote::open_flags decoded{};
EXPECT_EQ(0, pkt.decode(decoded));
EXPECT_EQ(static_cast<std::uint32_t>(flags),
static_cast<std::uint32_t>(decoded));
}
TEST(packet_test, encrypt_and_decrypt_without_size_prefix) {
packet pkt;
pkt.encode("opaque");
pkt.encrypt("moose", false);
EXPECT_EQ(0, pkt.decrypt("moose"));
std::string out;
EXPECT_EQ(0, pkt.decode(out));
EXPECT_EQ("opaque", out);
}
TEST(packet_test, decode_fails_when_empty) {
packet pkt;
std::uint32_t val{};
EXPECT_NE(0, pkt.decode(val));
}
TEST(packet_test, decode_json_round_trip) {
packet pkt;
const json src = {{"x", 1}, {"y", "z"}, {"ok", true}};
pkt.encode(src.dump());
json got{};
EXPECT_EQ(0, packet::decode_json(pkt, got));
EXPECT_EQ(src, got);
}
TEST(packet_test, remote_setattr_x_round_trip) {
packet pkt;
remote::setattr_x sa{};
sa.valid = 0x7;
sa.mode = static_cast<remote::file_mode>(0644);
sa.uid = 1001U;
sa.gid = 1002U;
sa.size = 1234567ULL;
sa.acctime = 111ULL;
sa.modtime = 222ULL;
sa.crtime = 333ULL;
sa.chgtime = 444ULL;
sa.bkuptime = 555ULL;
sa.flags = 0xA5A5A5A5U;
pkt.encode(sa);
remote::setattr_x out{};
EXPECT_EQ(0, pkt.decode(out));
EXPECT_EQ(sa.valid, out.valid);
EXPECT_EQ(sa.mode, out.mode);
EXPECT_EQ(sa.uid, out.uid);
EXPECT_EQ(sa.gid, out.gid);
EXPECT_EQ(sa.size, out.size);
EXPECT_EQ(sa.acctime, out.acctime);
EXPECT_EQ(sa.modtime, out.modtime);
EXPECT_EQ(sa.crtime, out.crtime);
EXPECT_EQ(sa.chgtime, out.chgtime);
EXPECT_EQ(sa.bkuptime, out.bkuptime);
EXPECT_EQ(sa.flags, out.flags);
}
TEST(packet_test, remote_stat_round_trip) {
packet pkt;
remote::stat st{};
st.st_mode = static_cast<remote::file_mode>(0755);
st.st_nlink = static_cast<remote::file_nlink>(2);
st.st_uid = 2001U;
st.st_gid = 2002U;
st.st_atimespec = 101ULL;
st.st_mtimespec = 202ULL;
st.st_ctimespec = 303ULL;
st.st_birthtimespec = 404ULL;
st.st_size = 987654321ULL;
st.st_blocks = 4096ULL;
st.st_blksize = 8192U;
st.st_flags = 0xDEADBEEFU;
pkt.encode(st);
remote::stat out{};
EXPECT_EQ(0, pkt.decode(out));
EXPECT_EQ(st.st_mode, out.st_mode);
EXPECT_EQ(st.st_nlink, out.st_nlink);
EXPECT_EQ(st.st_uid, out.st_uid);
EXPECT_EQ(st.st_gid, out.st_gid);
EXPECT_EQ(st.st_atimespec, out.st_atimespec);
EXPECT_EQ(st.st_mtimespec, out.st_mtimespec);
EXPECT_EQ(st.st_ctimespec, out.st_ctimespec);
EXPECT_EQ(st.st_birthtimespec, out.st_birthtimespec);
EXPECT_EQ(st.st_size, out.st_size);
EXPECT_EQ(st.st_blocks, out.st_blocks);
EXPECT_EQ(st.st_blksize, out.st_blksize);
EXPECT_EQ(st.st_flags, out.st_flags);
}
TEST(packet_test, remote_statfs_round_trip) {
packet pkt;
remote::statfs sfs{};
sfs.f_bavail = 1'000'000ULL;
sfs.f_bfree = 2'000'000ULL;
sfs.f_blocks = 3'000'000ULL;
sfs.f_favail = 4'000'000ULL;
sfs.f_ffree = 5'000'000ULL;
sfs.f_files = 6'000'000ULL;
pkt.encode(sfs);
remote::statfs out{};
EXPECT_EQ(0, pkt.decode(out));
EXPECT_EQ(sfs.f_bavail, out.f_bavail);
EXPECT_EQ(sfs.f_bfree, out.f_bfree);
EXPECT_EQ(sfs.f_blocks, out.f_blocks);
EXPECT_EQ(sfs.f_favail, out.f_favail);
EXPECT_EQ(sfs.f_ffree, out.f_ffree);
EXPECT_EQ(sfs.f_files, out.f_files);
}
TEST(packet_test, remote_statfs_x_round_trip) {
packet pkt;
remote::statfs_x sfsx{};
sfsx.f_bavail = 7'000'000ULL;
sfsx.f_bfree = 8'000'000ULL;
sfsx.f_blocks = 9'000'000ULL;
sfsx.f_favail = 10'000'000ULL;
sfsx.f_ffree = 11'000'000ULL;
sfsx.f_files = 12'000'000ULL;
const char *mnt = "test_mnt";
std::memcpy(sfsx.f_mntfromname.data(), mnt, std::strlen(mnt));
pkt.encode(sfsx);
remote::statfs_x out{};
EXPECT_EQ(0, pkt.decode(out));
EXPECT_EQ(sfsx.f_bavail, out.f_bavail);
EXPECT_EQ(sfsx.f_bfree, out.f_bfree);
EXPECT_EQ(sfsx.f_blocks, out.f_blocks);
EXPECT_EQ(sfsx.f_favail, out.f_favail);
EXPECT_EQ(sfsx.f_ffree, out.f_ffree);
EXPECT_EQ(sfsx.f_files, out.f_files);
EXPECT_EQ(0, std::memcmp(sfsx.f_mntfromname.data(), out.f_mntfromname.data(),
sfsx.f_mntfromname.size()));
}
TEST(packet_test, remote_file_info_round_trip) {
packet pkt;
remote::file_info fi{};
fi.FileAttributes = 0x1234U;
fi.ReparseTag = 0x5678U;
fi.AllocationSize = 1111ULL;
fi.FileSize = 2222ULL;
fi.CreationTime = 3333ULL;
fi.LastAccessTime = 4444ULL;
fi.LastWriteTime = 5555ULL;
fi.ChangeTime = 6666ULL;
fi.IndexNumber = 7777ULL;
fi.HardLinks = 3U;
fi.EaSize = 0U;
pkt.encode(fi);
remote::file_info out{};
EXPECT_EQ(0, pkt.decode(out));
EXPECT_EQ(fi.FileAttributes, fi.FileAttributes);
EXPECT_EQ(fi.ReparseTag, out.ReparseTag);
EXPECT_EQ(fi.AllocationSize, out.AllocationSize);
EXPECT_EQ(fi.FileSize, out.FileSize);
EXPECT_EQ(fi.CreationTime, out.CreationTime);
EXPECT_EQ(fi.LastAccessTime, out.LastAccessTime);
EXPECT_EQ(fi.LastWriteTime, out.LastWriteTime);
EXPECT_EQ(fi.ChangeTime, out.ChangeTime);
EXPECT_EQ(fi.IndexNumber, out.IndexNumber);
EXPECT_EQ(fi.HardLinks, out.HardLinks);
EXPECT_EQ(fi.EaSize, out.EaSize);
}
} // namespace repertory } // namespace repertory

View File

@@ -544,6 +544,7 @@ TYPED_TEST(providers_test, get_directory_items_fails_if_item_is_file) {
EXPECT_EQ(api_error::success, this->provider->remove_file("/pt01.txt")); EXPECT_EQ(api_error::success, this->provider->remove_file("/pt01.txt"));
} }
TYPED_TEST(providers_test, get_directory_item_count) { TYPED_TEST(providers_test, get_directory_item_count) {
if (this->provider->get_provider_type() == provider_type::encrypt) { if (this->provider->get_provider_type() == provider_type::encrypt) {
EXPECT_EQ(std::size_t(2U), this->provider->get_directory_item_count("/")); EXPECT_EQ(std::size_t(2U), this->provider->get_directory_item_count("/"));

View File

@@ -64,15 +64,21 @@ TEST_F(ring_buffer_open_file_test, can_forward_to_last_chunk) {
{ {
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi, ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U); provider, 8U);
file.set(0U, 3U); file.set(0U, 3U);
file.forward(4U); file.forward(4U);
EXPECT_EQ(std::size_t(7U), file.get_current_chunk()); EXPECT_EQ(std::size_t(7U), file.get_current_chunk());
EXPECT_EQ(std::size_t(0U), file.get_first_chunk()); EXPECT_EQ(std::size_t(3U), file.get_first_chunk());
EXPECT_EQ(std::size_t(7U), file.get_last_chunk()); EXPECT_EQ(std::size_t(10U), file.get_last_chunk());
for (std::size_t chunk = 0U; chunk < 8U; chunk++) {
for (std::size_t chunk = 3U; chunk <= 7U; ++chunk) {
EXPECT_TRUE(file.get_read_state(chunk)); EXPECT_TRUE(file.get_read_state(chunk));
} }
for (std::size_t chunk = 8U; chunk <= 10U; ++chunk) {
EXPECT_FALSE(file.get_read_state(chunk));
}
} }
} }
@@ -117,16 +123,22 @@ TEST_F(ring_buffer_open_file_test, can_forward_after_last_chunk) {
{ {
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi, ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U); provider, 8U);
file.set(0U, 3U); file.set(0U, 3U);
file.forward(5U); file.forward(5U);
EXPECT_EQ(std::size_t(8U), file.get_current_chunk()); EXPECT_EQ(std::size_t(8U), file.get_current_chunk());
EXPECT_EQ(std::size_t(1U), file.get_first_chunk()); EXPECT_EQ(std::size_t(4U), file.get_first_chunk());
EXPECT_EQ(std::size_t(8U), file.get_last_chunk()); EXPECT_EQ(std::size_t(11U), file.get_last_chunk());
EXPECT_TRUE(file.get_read_state(4U));
EXPECT_TRUE(file.get_read_state(5U));
EXPECT_TRUE(file.get_read_state(6U));
EXPECT_TRUE(file.get_read_state(7U));
EXPECT_FALSE(file.get_read_state(8U)); EXPECT_FALSE(file.get_read_state(8U));
for (std::size_t chunk = 1U; chunk < 8U; chunk++) { EXPECT_FALSE(file.get_read_state(9U));
EXPECT_TRUE(file.get_read_state(chunk)); EXPECT_FALSE(file.get_read_state(10U));
} EXPECT_FALSE(file.get_read_state(11U));
} }
} }
@@ -144,12 +156,13 @@ TEST_F(ring_buffer_open_file_test, can_forward_and_rollover_after_last_chunk) {
{ {
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi, ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U); provider, 8U);
file.set(16U, 20U); file.set(16U, 20U);
file.forward(8U); file.forward(8U);
EXPECT_EQ(std::size_t(28U), file.get_current_chunk()); EXPECT_EQ(std::size_t(28U), file.get_current_chunk());
EXPECT_EQ(std::size_t(21U), file.get_first_chunk()); EXPECT_EQ(std::size_t(24U), file.get_first_chunk());
EXPECT_EQ(std::size_t(28U), file.get_last_chunk()); EXPECT_EQ(std::size_t(31U), file.get_last_chunk());
} }
} }
@@ -563,4 +576,162 @@ TEST_F(ring_buffer_open_file_test,
} }
} }
} }
TEST_F(ring_buffer_open_file_test, forward_center_noop_when_within_half) {
auto source_path = test::generate_test_file_name("ring_buffer_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.directory = false;
fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 16U;
fsi.source_path = source_path;
{
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U);
file.set(0U, 3U);
file.forward(1U);
EXPECT_EQ(std::size_t(4U), file.get_current_chunk());
EXPECT_EQ(std::size_t(0U), file.get_first_chunk());
EXPECT_EQ(std::size_t(7U), file.get_last_chunk());
for (std::size_t chunk = 0U; chunk <= 7U; ++chunk) {
EXPECT_TRUE(file.get_read_state(chunk)) << "chunk " << chunk;
}
}
}
TEST_F(ring_buffer_open_file_test, forward_center_caps_at_file_end) {
auto source_path = test::generate_test_file_name("ring_buffer_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.directory = false;
fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 16U;
fsi.source_path = source_path;
{
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U);
file.set(6U, 9U);
file.forward(100U);
EXPECT_EQ(std::size_t(15U), file.get_current_chunk());
EXPECT_EQ(std::size_t(8U), file.get_first_chunk());
EXPECT_EQ(std::size_t(15U), file.get_last_chunk());
}
}
TEST_F(ring_buffer_open_file_test,
forward_delta_ge_ring_triggers_full_invalidation_path) {
auto source_path = test::generate_test_file_name("ring_buffer_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.directory = false;
fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 16U;
fsi.source_path = source_path;
{
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U);
file.set(0U, 0U);
file.forward(100U);
EXPECT_EQ(std::size_t(15U), file.get_current_chunk());
EXPECT_EQ(std::size_t(8U), file.get_first_chunk());
EXPECT_EQ(std::size_t(15U), file.get_last_chunk());
for (std::size_t chunk = 8U; chunk <= 15U; ++chunk) {
EXPECT_FALSE(file.get_read_state(chunk)) << "chunk " << chunk;
}
}
}
TEST_F(ring_buffer_open_file_test,
forward_center_marks_only_tail_entrants_unread) {
auto source_path = test::generate_test_file_name("ring_buffer_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.directory = false;
fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 16U;
fsi.source_path = source_path;
{
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U);
file.set(0U, 3U);
file.forward(2U);
EXPECT_EQ(std::size_t(5U), file.get_current_chunk());
EXPECT_EQ(std::size_t(1U), file.get_first_chunk());
EXPECT_EQ(std::size_t(8U), file.get_last_chunk());
for (std::size_t chunk = 1U; chunk <= 7U; ++chunk) {
EXPECT_TRUE(file.get_read_state(chunk)) << "chunk " << chunk;
}
EXPECT_FALSE(file.get_read_state(8U));
}
}
TEST_F(ring_buffer_open_file_test, reverse_does_not_trigger_forward_centering) {
auto source_path = test::generate_test_file_name("ring_buffer_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.directory = false;
fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 16U;
fsi.source_path = source_path;
{
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U);
file.set(8U, 12U);
file.reverse(1U);
EXPECT_EQ(std::size_t(11U), file.get_current_chunk());
EXPECT_EQ(std::size_t(8U), file.get_first_chunk());
EXPECT_EQ(std::size_t(15U), file.get_last_chunk());
}
}
TEST_F(ring_buffer_open_file_test,
forward_minimal_slide_then_center_multi_step) {
auto source_path = test::generate_test_file_name("ring_buffer_open_file");
EXPECT_CALL(provider, is_read_only()).WillRepeatedly(Return(false));
filesystem_item fsi;
fsi.directory = false;
fsi.api_path = "/test.txt";
fsi.size = test_chunk_size * 32U;
fsi.source_path = source_path;
{
ring_buffer_open_file file(ring_buffer_dir, test_chunk_size, 30U, fsi,
provider, 8U);
file.set(0U, 3U);
file.forward(7U);
EXPECT_EQ(std::size_t(10U), file.get_current_chunk());
EXPECT_EQ(std::size_t(6U), file.get_first_chunk());
EXPECT_EQ(std::size_t(13U), file.get_last_chunk());
EXPECT_FALSE(file.get_read_state(11U));
EXPECT_FALSE(file.get_read_state(12U));
EXPECT_FALSE(file.get_read_state(13U));
}
}
} // namespace repertory } // namespace repertory

0
support/3rd_party/icu_configure.sh vendored Normal file → Executable file
View File

View File

@@ -30,6 +30,12 @@ void create_console();
void free_console(); void free_console();
[[nodiscard]] auto get_available_drive_letter(char first = 'a')
-> std::optional<std::string_view>;
[[nodiscard]] auto get_available_drive_letters(char first = 'a')
-> std::vector<std::string_view>;
[[nodiscard]] auto get_local_app_data_directory() -> const std::string &; [[nodiscard]] auto get_local_app_data_directory() -> const std::string &;
[[nodiscard]] auto get_last_error_code() -> DWORD; [[nodiscard]] auto get_last_error_code() -> DWORD;

View File

@@ -210,8 +210,7 @@ auto get_times(std::string_view path) -> std::optional<file_times> {
ret.accessed = ret.accessed =
utils::time::windows_file_time_to_unix_time(times.at(1U)); utils::time::windows_file_time_to_unix_time(times.at(1U));
ret.changed = utils::time::windows_file_time_to_unix_time(times.at(2U)); ret.changed = utils::time::windows_file_time_to_unix_time(times.at(2U));
ret.created = = ret.created = utils::time::windows_file_time_to_unix_time(times.at(0U));
utils::time::windows_file_time_to_unix_time(times.at(0U));
ret.modified = ret.modified =
utils::time::windows_file_time_to_unix_time(times.at(2U)); utils::time::windows_file_time_to_unix_time(times.at(2U));
ret.written = utils::time::windows_file_time_to_unix_time(times.at(2U)); ret.written = utils::time::windows_file_time_to_unix_time(times.at(2U));

View File

@@ -29,6 +29,14 @@
#include "utils/path.hpp" #include "utils/path.hpp"
#include "utils/string.hpp" #include "utils/string.hpp"
namespace {
constexpr std::array<std::string_view, 26U> drive_letters{
"a:", "b:", "c:", "d:", "e:", "f:", "g:", "h:", "i:",
"j:", "k:", "l:", "m:", "n:", "o:", "p:", "q:", "r:",
"s:", "t:", "u:", "v:", "w:", "x:", "y:", "z:",
};
}
namespace repertory::utils { namespace repertory::utils {
void create_console() { void create_console() {
if (::AllocConsole() == 0) { if (::AllocConsole() == 0) {
@@ -61,6 +69,47 @@ void create_console() {
void free_console() { ::FreeConsole(); } void free_console() { ::FreeConsole(); }
auto get_available_drive_letter(char first) -> std::optional<std::string_view> {
const auto *begin = std::ranges::find_if(
drive_letters, [first](auto &&val) { return val.at(0U) == first; });
if (begin == drive_letters.end()) {
begin = drive_letters.begin();
}
auto available =
std::ranges::find_if(begin, drive_letters.end(), [](auto &&val) -> bool {
return not utils::file::directory{utils::path::combine(val, {"\\"})}
.exists();
});
if (available == drive_letters.end()) {
return std::nullopt;
}
return *available;
}
auto get_available_drive_letters(char first) -> std::vector<std::string_view> {
const auto *begin =
std::ranges::find_if(drive_letters, [first](auto &&val) -> bool {
return val.at(0U) == first;
});
if (begin == drive_letters.end()) {
begin = drive_letters.begin();
}
return std::accumulate(
begin, drive_letters.end(), std::vector<std::string_view>(),
[](auto &&vec, auto &&letter) -> auto {
if (utils::file::directory{utils::path::combine(letter, {"\\"})}
.exists()) {
return vec;
}
vec.emplace_back(letter);
return vec;
});
}
auto get_last_error_code() -> DWORD { return ::GetLastError(); } auto get_last_error_code() -> DWORD { return ::GetLastError(); }
auto get_local_app_data_directory() -> const std::string & { auto get_local_app_data_directory() -> const std::string & {