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

This commit is contained in:
2025-09-27 11:59:27 -05:00
20 changed files with 1077 additions and 242 deletions

View File

@@ -26,6 +26,7 @@
### Changes from v2.0.7-release
* 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 intermittent client hang on remote mount server disconnect
* Implemented POSIX-compliant `unlink()` with FUSE `hard_remove`

View File

@@ -25,6 +25,7 @@
#include "utils/common.hpp"
namespace repertory::comm {
inline static constexpr std::uint32_t max_packet_bytes{32U * 1024U * 1024U};
inline constexpr const std::uint8_t max_read_attempts{5U};
inline constexpr const std::uint16_t packet_nonce_size{256U};
inline constexpr const std::uint16_t server_handshake_timeout_ms{3000U};
@@ -45,6 +46,8 @@ private:
boost::asio::ip::tcp::socket &sock;
};
void apply_common_socket_properties(boost::asio::ip::tcp::socket &sock);
[[nodiscard]] auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock)
-> bool;

View File

@@ -1,17 +1,13 @@
/*
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
copies of the Software, and to permit persons 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
@@ -24,6 +20,7 @@
#include "comm/packet/packet.hpp"
#include "types/remote.hpp"
#include "utils/atomic.hpp"
using boost::asio::ip::tcp;
@@ -47,24 +44,25 @@ public:
auto operator=(packet_client &&) -> packet_client & = delete;
private:
mutable boost::asio::io_context io_context_;
remote::remote_config cfg_;
std::string unique_id_;
mutable boost::asio::io_context io_context_;
utils::atomic<std::string> unique_id_;
private:
bool allow_connections_{true};
boost::asio::ip::basic_resolver<boost::asio::ip::tcp>::results_type
std::atomic<bool> allow_connections_{true};
utils::atomic<
boost::asio::ip::basic_resolver<boost::asio::ip::tcp>::results_type>
resolve_results_;
std::mutex clients_mutex_;
std::vector<std::shared_ptr<client>> clients_;
std::vector<std::thread> service_threads_;
private:
static void close(client &cli);
static void close(client &cli) noexcept;
void close_all();
void connect(client &cli);
[[nodiscard]] auto connect(client &cli) -> bool;
[[nodiscard]] auto get_client() -> std::shared_ptr<client>;

View File

@@ -176,6 +176,8 @@ public:
const open_file_data &ofd, std::uint64_t &handle,
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 rename_directory(const std::string &from_api_path,

View File

@@ -34,6 +34,11 @@ struct i_http_comm;
struct head_object_result;
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:
static const constexpr auto type{provider_type::s3};
@@ -170,6 +175,11 @@ public:
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,
std::size_t size, std::uint64_t offset,
data_buffer &data,

View File

@@ -77,6 +77,12 @@ auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock) -> bool {
return false;
}
void apply_common_socket_properties(boost::asio::ip::tcp::socket &sock) {
sock.set_option(boost::asio::ip::tcp::no_delay(true));
sock.set_option(boost::asio::socket_base::linger(false, 0));
sock.set_option(boost::asio::socket_base::keep_alive(true));
}
template <class op_t>
void run_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock, op_t &&operation,

View File

@@ -1,17 +1,13 @@
/*
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
copies of the Software, and to permit persons 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
@@ -26,10 +22,10 @@
#include "types/repertory.hpp"
#include "utils/collection.hpp"
#include "utils/common.hpp"
#include "utils/config.hpp"
#include "utils/error_utils.hpp"
#include "utils/utils.hpp"
#include "version.hpp"
#include <utils/config.hpp>
using namespace repertory::comm;
@@ -42,29 +38,30 @@ packet_client::packet_client(remote::remote_config cfg)
}
packet_client::~packet_client() {
allow_connections_ = false;
close_all();
REPERTORY_USES_FUNCTION_NAME();
for (std::size_t idx = 0U; idx < service_threads_.size(); ++idx) {
io_context_.stop();
allow_connections_ = false;
try {
close_all();
} catch (...) {
}
io_context_.stop();
for (auto &thread : service_threads_) {
thread.join();
if (thread.joinable()) {
thread.join();
}
}
}
void packet_client::close(client &cli) {
REPERTORY_USES_FUNCTION_NAME();
void packet_client::close(client &cli) noexcept {
boost::system::error_code err1;
auto res = cli.socket.shutdown(boost::asio::socket_base::shutdown_both, err1);
try {
cli.socket.shutdown(boost::asio::socket_base::shutdown_both);
boost::system::error_code err;
[[maybe_unused]] auto res = cli.socket.close(err);
} catch (const std::exception &e) {
utils::error::raise_error(function_name, e, "connection handshake failed");
}
boost::system::error_code err2;
res = cli.socket.close(err2);
}
void packet_client::close_all() {
@@ -72,10 +69,8 @@ void packet_client::close_all() {
for (auto &cli : clients_) {
close(*cli);
}
clients_.clear();
io_context_.restart();
resolve_results_ = {};
resolve_results_.store({});
unique_id_ = utils::create_uuid_string();
}
@@ -94,19 +89,12 @@ auto packet_client::check_version(std::uint32_t client_version,
connect_with_deadline(ctx, cli.socket, resolve_results,
std::chrono::milliseconds(cfg_.conn_timeout_ms));
cli.socket.set_option(boost::asio::ip::tcp::no_delay(true));
cli.socket.set_option(boost::asio::socket_base::linger(false, 0));
cli.socket.set_option(boost::asio::socket_base::keep_alive(true));
comm::apply_common_socket_properties(cli.socket);
if (not handshake(cli, ctx, min_version)) {
return api_error::comm_error;
}
if ((min_version & 0xFFU) != 0U) {
min_version = 0U;
return api_error::incompatible_version;
}
if (client_version < min_version) {
return api_error::incompatible_version;
}
@@ -119,21 +107,20 @@ auto packet_client::check_version(std::uint32_t client_version,
return api_error::error;
}
void packet_client::connect(client &cli) {
auto packet_client::connect(client &cli) -> bool {
try {
resolve();
connect_with_deadline(io_context_, cli.socket, resolve_results_,
auto cached = resolve_results_.load();
connect_with_deadline(io_context_, cli.socket, cached,
std::chrono::milliseconds(cfg_.conn_timeout_ms));
cli.socket.set_option(boost::asio::ip::tcp::no_delay(true));
cli.socket.set_option(boost::asio::socket_base::linger(false, 0));
cli.socket.set_option(boost::asio::socket_base::keep_alive(true));
comm::apply_common_socket_properties(cli.socket);
std::uint32_t min_version{};
if (not handshake(cli, io_context_, min_version)) {
close(cli);
return;
return false;
}
packet response;
@@ -141,9 +128,10 @@ void packet_client::connect(client &cli) {
if (res != 0) {
throw std::runtime_error(fmt::format("read packet failed|err|{}", res));
}
return true;
} catch (...) {
close(cli);
resolve_results_ = {};
resolve_results_.store({});
throw;
}
}
@@ -151,17 +139,19 @@ void packet_client::connect(client &cli) {
auto packet_client::get_client() -> std::shared_ptr<packet_client::client> {
REPERTORY_USES_FUNCTION_NAME();
unique_mutex_lock clients_lock(clients_mutex_);
if (not allow_connections_) {
return nullptr;
}
try {
unique_mutex_lock clients_lock(clients_mutex_);
if (clients_.empty()) {
clients_lock.unlock();
auto cli = std::make_shared<client>(io_context_);
connect(*cli);
if (not connect(*cli)) {
return nullptr;
}
return cli;
}
@@ -169,7 +159,7 @@ auto packet_client::get_client() -> std::shared_ptr<packet_client::client> {
utils::collection::remove_element(clients_, cli);
return cli;
} catch (const std::exception &e) {
utils::error::raise_error(function_name, e, "connection handshake failed");
utils::error::raise_error(function_name, e, "get_client failed");
}
return nullptr;
@@ -190,7 +180,8 @@ auto packet_client::handshake(client &cli, boost::asio::io_context &ctx,
data_buffer buffer;
{
packet tmp;
tmp.encode(utils::get_version_number(REPERTORY_MIN_REMOTE_VERSION));
tmp.encode(utils::get_version_number(project_get_version()));
tmp.encode(~utils::get_version_number(project_get_version()));
tmp.encode(utils::generate_random_string(packet_nonce_size));
tmp.to_buffer(buffer);
}
@@ -198,11 +189,22 @@ auto packet_client::handshake(client &cli, boost::asio::io_context &ctx,
read_exact_with_deadline(ctx, cli.socket, boost::asio::buffer(buffer),
std::chrono::milliseconds(cfg_.recv_timeout_ms));
packet response(buffer);
auto res = response.decode(min_version);
if (res != 0) {
throw std::runtime_error("failed to decode server version");
}
std::uint32_t min_version_check{};
res = response.decode(min_version_check);
if (res != 0) {
throw std::runtime_error("failed to decode server version");
}
if (min_version_check != ~min_version) {
throw std::runtime_error("failed to decode server version");
}
response.encrypt(cfg_.encryption_token, false);
response.to_buffer(buffer);
@@ -210,7 +212,7 @@ auto packet_client::handshake(client &cli, boost::asio::io_context &ctx,
std::chrono::milliseconds(cfg_.send_timeout_ms));
return true;
} catch (const std::exception &e) {
utils::error::raise_error(function_name, e, "handlshake failed");
utils::error::raise_error(function_name, e, "handshake failed");
}
return false;
@@ -241,6 +243,11 @@ auto packet_client::read_packet(client &cli, boost::asio::io_context &ctx,
std::uint32_t to_read{};
std::memcpy(&to_read, buffer.data(), sizeof(to_read));
boost::endian::big_to_native_inplace(to_read);
if (to_read == 0U || to_read > comm::max_packet_bytes) {
return utils::from_api_error(api_error::comm_error);
}
buffer.resize(to_read);
read_exact_with_deadline(ctx, cli.socket, boost::asio::buffer(buffer),
@@ -255,7 +262,9 @@ auto packet_client::read_packet(client &cli, boost::asio::io_context &ctx,
}
void packet_client::resolve() {
if (not resolve_results_.empty()) {
boost::asio::ip::basic_resolver<boost::asio::ip::tcp>::results_type cached =
resolve_results_;
if (not cached.empty()) {
return;
}
@@ -282,12 +291,14 @@ auto packet_client::send(std::string_view method, packet &request,
REPERTORY_USES_FUNCTION_NAME();
auto success = false;
packet::error_type ret = utils::from_api_error(api_error::error);
request.encode_top(method);
request.encode_top(utils::get_thread_id());
request.encode_top(unique_id_);
request.encode_top(PACKET_SERVICE_FLAGS);
request.encode_top(std::string{project_get_version()});
auto ret = utils::from_api_error(api_error::error);
auto base_request = request;
base_request.encode_top(method);
base_request.encode_top(utils::get_thread_id());
base_request.encode_top(unique_id_.load());
base_request.encode_top(PACKET_SERVICE_FLAGS);
base_request.encode_top(std::string{project_get_version()});
for (std::uint8_t retry = 1U;
allow_connections_ && not success && (retry <= max_read_attempts);
@@ -295,12 +306,16 @@ auto packet_client::send(std::string_view method, packet &request,
auto current_client = get_client();
if (current_client) {
try {
request.encode_top(current_client->nonce);
request.encrypt(cfg_.encryption_token);
auto current_request = base_request;
current_request.encode_top(current_client->nonce);
request = current_request;
current_request.encrypt(cfg_.encryption_token);
write_all_with_deadline(
io_context_, current_client->socket,
boost::asio::buffer(&request[0], request.get_size()),
boost::asio::buffer(&current_request[0],
current_request.get_size()),
std::chrono::milliseconds(cfg_.send_timeout_ms));
ret = read_packet(*current_client, response);
@@ -318,7 +333,6 @@ auto packet_client::send(std::string_view method, packet &request,
}
} catch (const std::exception &e) {
utils::error::raise_error(function_name, e, "send failed");
close(*current_client);
if (allow_connections_ && (retry < max_read_attempts)) {
std::this_thread::sleep_for(1s);

View File

@@ -92,6 +92,7 @@ auto packet_server::handshake(std::shared_ptr<connection> conn) const -> bool {
data_buffer buffer;
packet request;
request.encode(utils::get_version_number(REPERTORY_MIN_REMOTE_VERSION));
request.encode(~utils::get_version_number(REPERTORY_MIN_REMOTE_VERSION));
request.encode(conn->nonce);
request.to_buffer(buffer);
auto to_read{buffer.size() + utils::encryption::encryption_header_size};
@@ -145,17 +146,26 @@ auto packet_server::handshake(std::shared_ptr<connection> conn) const -> bool {
if (response.decrypt(encryption_token_) == 0) {
std::uint32_t client_version{};
if (response.decode(client_version) == 0) {
std::string nonce;
if (response.decode(nonce) == 0) {
if (nonce == conn->nonce) {
conn->generate_nonce();
return true;
std::uint32_t client_version_check{};
if (response.decode(client_version_check) == 0) {
if (~client_version != client_version_check) {
throw std::runtime_error("client version check failed");
}
throw std::runtime_error("nonce mismatch");
std::string nonce;
if (response.decode(nonce) == 0) {
if (nonce == conn->nonce) {
conn->generate_nonce();
return true;
}
throw std::runtime_error("nonce mismatch");
}
throw std::runtime_error("invalid nonce");
}
throw std::runtime_error("invalid nonce");
throw std::runtime_error("invalid client version");
}
throw std::runtime_error("invalid client version");
@@ -222,9 +232,7 @@ void packet_server::on_accept(std::shared_ptr<connection> conn,
return;
}
conn->socket.set_option(boost::asio::ip::tcp::no_delay(true));
conn->socket.set_option(boost::asio::socket_base::linger(false, 0));
conn->socket.set_option(boost::asio::socket_base::keep_alive(true));
comm::apply_common_socket_properties(conn->socket);
boost::asio::dispatch(conn->socket.get_executor(), [this, conn]() {
if (not handshake(conn)) {
@@ -276,6 +284,11 @@ void packet_server::read_packet(std::shared_ptr<connection> conn,
}
};
if (data_size > comm::max_packet_bytes) {
throw std::runtime_error(
fmt::format("packet too large|size|{}", data_size));
}
auto should_send_response = true;
auto response = std::make_shared<packet>();
conn->buffer.resize(data_size);

View File

@@ -926,17 +926,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 {
REPERTORY_USES_FUNCTION_NAME();
utils::error::handle_info(function_name, "called");
auto res = check_parent_access(api_path, W_OK | X_OK);
if (res != api_error::success) {
return res;
}
res = provider_.remove_directory(api_path);
res = fm_->remove_directory(api_path);
if (res != api_error::success) {
return res;
}
auto iter = directory_cache_->remove_directory(api_path);
directory_cache_->remove_directory(api_path);
return api_error::success;
}

View File

@@ -1721,7 +1721,6 @@ auto remote_server::update_to_windows_format(const std::string &root_api_path,
}
auto update_meta{false};
auto default_value = std::to_string(utils::time::get_time_now());
const auto ensure_set = [&item, &update_meta](
const std::string &name,
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);
ensure_set(META_CREATION, default_value, true);
ensure_set(META_MODIFIED, default_value, true);
auto default_time = std::to_string(utils::time::get_time_now());
ensure_set(META_ACCESSED, default_time, 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_WRITTEN, item[JSON_META][META_MODIFIED], true);
ensure_set(META_ATTRIBUTES, "0", false);

View File

@@ -216,15 +216,11 @@ VOID winfsp_drive::Cleanup(PVOID file_node, PVOID file_desc,
FspFileSystemDeleteDirectoryBuffer(&directory_buffer);
}
if (not directory) {
return handle_error(fm_->remove_file(api_path));
if (directory) {
return handle_error(fm_->remove_directory(api_path));
}
if (provider_.get_directory_item_count(api_path) == 0) {
return handle_error(provider_.remove_directory(api_path));
}
return handle_error(api_error::directory_not_empty);
return handle_error(fm_->remove_file(api_path));
}
if (((flags & FspCleanupSetArchiveBit) != 0U) && not directory) {

View File

@@ -101,6 +101,10 @@ void file_manager::close(std::uint64_t handle) {
closeable_file->close();
if (closeable_file->is_directory()) {
return;
}
auto file = utils::file::file{closeable_file->get_source_path()};
if (file.remove()) {
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 {
REPERTORY_USES_FUNCTION_NAME();
unique_recur_mutex_lock open_lock(open_file_mtx_);
filesystem_item fsi{};
auto res = provider_.get_filesystem_item(api_path, false, fsi);
if (res != api_error::success) {
@@ -704,8 +744,6 @@ auto file_manager::remove_file(const std::string &api_path) -> api_error {
return res;
}
unique_recur_mutex_lock open_lock(open_file_mtx_);
unique_mutex_lock upload_lock(upload_mtx_);
remove_upload(api_path, true);
remove_resume(api_path, fsi.source_path, true);

View File

@@ -275,69 +275,34 @@ auto s3_provider::get_directory_item_count(const std::string &api_path) const
try {
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;
if (is_encrypted) {
auto res{get_item_meta(api_path, META_KEY, key)};
if (res != api_error::success) {
if (get_item_meta(api_path, META_KEY, key) != api_error::success) {
return 0U;
}
}
auto object_name{
api_path == "/"
? ""
: utils::path::create_api_path(is_encrypted ? key : api_path),
};
auto object_name =
(api_path == "/")
? std::string{}
: utils::path::create_api_path(is_encrypted ? key : api_path);
std::string response_data{};
long response_code{};
auto prefix{object_name.empty() ? object_name : object_name + "/"};
auto grab_more{true};
std::string token{};
std::uint64_t total_count{};
while (grab_more) {
if (not get_object_list(response_data, response_code, "/", "", token)) {
return total_count;
}
if (response_code == http_error_codes::not_found) {
return total_count;
}
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;
}
auto prefix = object_name.empty() ? std::string{} : object_name + "/";
std::uint64_t total_count{0};
auto res = iterate_prefix(
prefix,
[&total_count](auto &&, auto &&, auto &&) {
++total_count;
return api_error::success;
},
[&total_count](auto &&, auto &&, auto &&) {
++total_count;
return api_error::success;
});
if (res != api_error::success) {
return 0U;
}
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()};
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,
bool directory, auto node) -> api_error {
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 + "/",
};
auto grab_more{true};
std::string token{};
while (grab_more) {
std::string response_data{};
long response_code{};
if (not get_object_list(response_data, response_code, "/", prefix, token)) {
return api_error::comm_error;
}
return iterate_prefix(
prefix,
[&](auto && /* prefix */, auto &&node, auto &&) -> auto {
return add_directory_item(
utils::path::create_api_path(
utils::path::combine("/", {node.text().as_string()})),
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 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;
return add_directory_item(child_api_path, false, node);
});
}
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;
}
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)
-> api_error {
REPERTORY_USES_FUNCTION_NAME();

View File

@@ -113,6 +113,31 @@ TYPED_TEST(fuse_test, directory_can_opendir_after_closedir) {
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);
}
} // namespace repertory
#endif // !defined(_WIN32)

View File

@@ -0,0 +1,123 @@
/*
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.
*/
#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) {
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);
}
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
#endif // !defined(_WIN32)

View File

@@ -54,7 +54,6 @@ TYPED_TEST(fuse_test, unlink_open_file_leaves_handle_intact) {
ASSERT_EQ(0, ::unlink(path.c_str()));
auto res = ::lseek(desc, 0, SEEK_END);
fmt::println("lseek|{}|{}", res, errno);
ASSERT_NE(-1, res);
this->write_all(desc, " WORLD");

View File

@@ -0,0 +1,122 @@
/*
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 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"
#include "comm/packet/packet.hpp"
#include "comm/packet/packet_client.hpp"
#include "comm/packet/packet_server.hpp"
#include "types/remote.hpp"
#include "utils/common.hpp"
#include "utils/utils.hpp"
#include "version.hpp"
using namespace repertory;
using namespace repertory::comm;
namespace {
class test_packet_server final {
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});
}
})) {}
private:
std::unique_ptr<packet_server> server_;
};
inline auto make_cfg(std::uint16_t port, const std::string &token)
-> remote::remote_config {
remote::remote_config cfg{};
cfg.host_name_or_ip = "127.0.0.1";
cfg.api_port = port;
cfg.max_connections = 2U;
cfg.conn_timeout_ms = 1500U;
cfg.recv_timeout_ms = 1500U;
cfg.send_timeout_ms = 1500U;
cfg.encryption_token = token;
return cfg;
}
} // namespace
TEST(packet_client_test, can_check_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(project_get_version()), min_version);
EXPECT_EQ(api, api_error::success);
EXPECT_NE(min_version, 0U);
}
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) {
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 service_flags{};
packet request;
packet response;
auto ret = client.send("ping", request, response, service_flags);
EXPECT_EQ(ret, 0);
}

View File

@@ -1,27 +1,24 @@
/*
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.
/* Copyright <2018-2025>
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/packet.hpp"
#include "types/remote.hpp"
namespace repertory {
TEST(packet_test, encrypt_and_decrypt) {
@@ -35,7 +32,435 @@ TEST(packet_test, encrypt_and_decrypt) {
std::string data;
EXPECT_EQ(0, test_packet.decode(data));
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

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"));
}
TYPED_TEST(providers_test, get_directory_item_count) {
if (this->provider->get_provider_type() == provider_type::encrypt) {
EXPECT_EQ(std::size_t(2U), this->provider->get_directory_item_count("/"));