Skip to content

Commit c422baf

Browse files
committed
FileSystem: readByBlocks
1 parent fd7a910 commit c422baf

3 files changed

Lines changed: 147 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
- [`EventQueue`](modules/Execution/EventQueue.mpp) - Thread-safe event queue running on a dedicated thread for asynchronous event processing (preserves event order)
4444
- [`ScopeGuard`](modules/Execution/ScopeGuard.mpp) - RAII utility to execute a function when leaving a scope, ensuring resource cleanup
4545

46-
### 📁 File System
46+
### 📁 Filesystem
47+
- [`File`](modules/FileSystem/File.mpp) - Binary and text file I/O, including optimized block-by-block reading
4748
- [`Watcher`](modules/FileSystem/Watcher.mpp) - File modification watcher
4849

4950
### 🧠 Functional

modules/FileSystem/File.mpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ import CppUtils.Type;
55

66
export namespace CppUtils::FileSystem
77
{
8+
struct Error
9+
{
10+
enum class Type
11+
{
12+
OpenFailed,
13+
ReadFailed,
14+
CallbackError
15+
} type;
16+
std::string_view message;
17+
};
18+
819
inline auto forFiles(
920
const std::filesystem::path& directoryPath,
1021
auto&& function,
@@ -36,8 +47,56 @@ export namespace CppUtils::FileSystem
3647
}, recursively);
3748
}
3849

50+
template<class T = std::span<const std::byte>>
51+
inline auto readByBlocks(
52+
const std::filesystem::path& filePath,
53+
auto&& function,
54+
std::size_t blockSize = 4'096) -> std::expected<void, Error>
55+
{
56+
auto file = std::ifstream{filePath, std::ios::binary};
57+
if (not file.is_open())
58+
return std::unexpected{Error{Error::Type::OpenFailed, "Failed to open file"}};
59+
60+
for (auto buffer = std::vector<char>(blockSize); file.good();)
61+
{
62+
file.read(std::data(buffer), static_cast<std::streamsize>(blockSize));
63+
64+
if (file.bad())
65+
return std::unexpected{Error{Error::Type::ReadFailed, "Hardware read error"}};
66+
67+
const auto bytesRead = static_cast<std::size_t>(file.gcount());
68+
if (bytesRead == 0)
69+
break;
70+
71+
auto block = [&]() {
72+
if constexpr (std::same_as<T, std::string_view>)
73+
return std::string_view{std::data(buffer), bytesRead};
74+
else
75+
return std::as_bytes(std::span{std::data(buffer), bytesRead});
76+
}();
77+
78+
using ResultType = std::invoke_result_t<decltype(function), decltype(block)>;
79+
if constexpr (Type::Specializes<ResultType, std::expected>)
80+
{
81+
if (auto result = function(block); not result)
82+
return std::unexpected{Error{Error::Type::CallbackError, result.error()}};
83+
}
84+
else
85+
function(block);
86+
87+
if (bytesRead < blockSize)
88+
break;
89+
}
90+
return {};
91+
}
92+
3993
namespace Binary
4094
{
95+
[[nodiscard]] inline auto readByBlocks(const std::filesystem::path& filePath, auto&& function, std::size_t blockSize = 4'096)
96+
{
97+
return FileSystem::readByBlocks<std::span<const std::byte>>(filePath, std::forward<decltype(function)>(function), blockSize);
98+
}
99+
41100
template<Type::TriviallyCopyable T>
42101
inline auto write(const std::filesystem::path& filePath, const T& buffer) -> void
43102
{
@@ -83,6 +142,11 @@ export namespace CppUtils::FileSystem
83142

84143
namespace String
85144
{
145+
[[nodiscard]] inline auto readByBlocks(const std::filesystem::path& filePath, auto&& function, std::size_t blockSize = 4'096)
146+
{
147+
return FileSystem::readByBlocks<std::string_view>(filePath, std::forward<decltype(function)>(function), blockSize);
148+
}
149+
86150
inline auto write(const std::filesystem::path& filePath, std::string_view content) -> void
87151
{
88152
auto file = std::ofstream{filePath};

tests/FileSystem/File.mpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import CppUtils;
66
namespace CppUtils::UnitTest::FileSystem::File
77
{
88
auto _ = TestSuite{"FileSystem/File", {"Logger", "FileSystem/Directory"}, [](TestSuite& suite) {
9+
using namespace std::literals;
910
using Logger = TestSuite::Logger;
1011

1112
suite.addTest("forFiles", [&] {
@@ -33,6 +34,86 @@ namespace CppUtils::UnitTest::FileSystem::File
3334
suite.expectEqual(filesCounter, 1u);
3435
}};
3536
});
37+
38+
suite.addTest("Binary/readByBlocks", [&] {
39+
CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void {
40+
const auto filePath = directory / "test.tmp";
41+
auto originalData = std::vector<std::byte>{std::byte{0xDE}, std::byte{0xAD}, std::byte{0xBE}, std::byte{0xEF}};
42+
CppUtils::FileSystem::Binary::writeVector(filePath, originalData);
43+
44+
auto resultData = std::vector<std::byte>{};
45+
auto nbBlocks = 0u;
46+
const auto blockSize = 2uz;
47+
const auto result = CppUtils::FileSystem::Binary::readByBlocks(filePath, [&](auto block) {
48+
resultData.insert(std::end(resultData), std::begin(block), std::end(block));
49+
++nbBlocks;
50+
}, blockSize);
51+
52+
suite.expect(result.has_value());
53+
suite.expectEqual(resultData, originalData);
54+
suite.expectEqual(nbBlocks, (std::size(originalData) + blockSize - 1) / blockSize);
55+
}};
56+
});
57+
58+
suite.addTest("String/readByBlocks", [&] {
59+
CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void {
60+
const auto filePath = directory / "test.tmp";
61+
constexpr auto originalString = "Hello World!"sv;
62+
CppUtils::FileSystem::String::write(filePath, originalString);
63+
64+
auto resultString = ""s;
65+
auto nbBlocks = 0u;
66+
const auto blockSize = 5uz;
67+
const auto result = CppUtils::FileSystem::String::readByBlocks(filePath, [&](std::string_view block) {
68+
resultString += block;
69+
++nbBlocks;
70+
}, blockSize);
71+
72+
suite.expect(result.has_value());
73+
suite.expectEqual(resultString, originalString);
74+
suite.expectEqual(nbBlocks, (std::size(originalString) + blockSize - 1) / blockSize);
75+
}};
76+
});
77+
78+
suite.addTest("readByBlocks (error)", [&] {
79+
const auto result = CppUtils::FileSystem::Binary::readByBlocks("non_existent_file.tmp", [](auto) {});
80+
suite.expect(not result.has_value());
81+
suite.expect(result.error().type == CppUtils::FileSystem::Error::Type::OpenFailed);
82+
suite.expectEqual(result.error().message, "Failed to open file"sv);
83+
});
84+
85+
suite.addTest("readByBlocks (callback error propagation)", [&] {
86+
CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void {
87+
const auto filePath = directory / "test.tmp";
88+
CppUtils::FileSystem::String::write(filePath, "Some content");
89+
90+
const auto result = CppUtils::FileSystem::Binary::readByBlocks(filePath, [](auto) -> std::expected<void, std::string_view> {
91+
return std::unexpected{"Error from callback"};
92+
});
93+
94+
suite.expect(not result.has_value());
95+
suite.expect(result.error().type == CppUtils::FileSystem::Error::Type::CallbackError);
96+
suite.expectEqual(result.error().message, "Error from callback"sv);
97+
}};
98+
});
99+
100+
suite.addTest("IncrementalHash", [&] {
101+
CppUtils::FileSystem::TemporaryDirectory{[&suite](const auto& directory) -> void {
102+
const auto filePath = directory / "test.tmp";
103+
constexpr auto originalString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."sv;
104+
CppUtils::FileSystem::String::write(filePath, originalString);
105+
106+
auto finalHash = CppUtils::Type::HashOffset;
107+
const auto result = CppUtils::FileSystem::Binary::readByBlocks(filePath, [&finalHash](auto block) {
108+
finalHash = CppUtils::Type::hash(block, finalHash);
109+
}, 8uz);
110+
111+
suite.expect(result.has_value());
112+
auto expectedHash = CppUtils::Type::hash(originalString);
113+
114+
suite.expectEqual(finalHash, expectedHash);
115+
}};
116+
});
36117
}};
37118

38119
namespace Binary

0 commit comments

Comments
 (0)