diff --git a/src/lang/io/CMakeLists.txt b/src/lang/io/CMakeLists.txt index 1911c6205..a890265e0 100644 --- a/src/lang/io/CMakeLists.txt +++ b/src/lang/io/CMakeLists.txt @@ -1,4 +1,6 @@ -sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME io SOURCES io.cc) +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME io + PRIVATE_HEADERS error.h fileview.h + SOURCES io.cc io_fileview.cc) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME io) diff --git a/src/lang/io/include/sourcemeta/core/io.h b/src/lang/io/include/sourcemeta/core/io.h index 821a09f35..ae03a2338 100644 --- a/src/lang/io/include/sourcemeta/core/io.h +++ b/src/lang/io/include/sourcemeta/core/io.h @@ -5,6 +5,11 @@ #include #endif +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + #include // assert #include // std::filesystem #include // std::basic_ifstream diff --git a/src/lang/io/include/sourcemeta/core/io_error.h b/src/lang/io/include/sourcemeta/core/io_error.h new file mode 100644 index 000000000..2d4fe5d15 --- /dev/null +++ b/src/lang/io/include/sourcemeta/core/io_error.h @@ -0,0 +1,52 @@ +#ifndef SOURCEMETA_CORE_IO_ERROR_H_ +#define SOURCEMETA_CORE_IO_ERROR_H_ + +#ifndef SOURCEMETA_CORE_IO_EXPORT +#include +#endif + +#include // std::exception +#include // std::filesystem::path +#include // std::string +#include // std::string_view +#include // std::move + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup io +/// An error that represents a failure to memory-map a file +class SOURCEMETA_CORE_IO_EXPORT FileViewError : public std::exception { +public: + FileViewError(std::filesystem::path path, const char *message) + : path_{std::move(path)}, message_{message} {} + FileViewError(std::filesystem::path path, std::string message) = delete; + FileViewError(std::filesystem::path path, std::string &&message) = delete; + FileViewError(std::filesystem::path path, std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto path() const noexcept -> const std::filesystem::path & { + return this->path_; + } + +private: + std::filesystem::path path_; + const char *message_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/src/lang/io/include/sourcemeta/core/io_fileview.h b/src/lang/io/include/sourcemeta/core/io_fileview.h new file mode 100644 index 000000000..4cddf041c --- /dev/null +++ b/src/lang/io/include/sourcemeta/core/io_fileview.h @@ -0,0 +1,67 @@ +#ifndef SOURCEMETA_CORE_IO_FILEVIEW_H_ +#define SOURCEMETA_CORE_IO_FILEVIEW_H_ + +#ifndef SOURCEMETA_CORE_IO_EXPORT +#include +#endif + +#include // assert +#include // std::size_t +#include // std::uint8_t +#include // std::filesystem::path + +namespace sourcemeta::core { + +/// @ingroup io +/// A read-only memory-mapped file. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// struct Header { +/// std::uint32_t magic; +/// std::uint32_t version; +/// }; +/// +/// sourcemeta::core::FileView view{"/path/to/file.bin"}; +/// const auto *header = view.as
(); +/// assert(header->magic == 0x12345678); +/// ``` +class SOURCEMETA_CORE_IO_EXPORT FileView { +public: + FileView(const std::filesystem::path &path); + ~FileView(); + + // Disable copying and moving + FileView(const FileView &) = delete; + FileView(FileView &&) = delete; + auto operator=(const FileView &) -> FileView & = delete; + auto operator=(FileView &&) -> FileView & = delete; + + /// The size of the memory-mapped data in bytes + [[nodiscard]] auto size() const noexcept -> std::size_t; + + /// Interpret the memory-mapped data as a pointer to T at the given offset. + template + [[nodiscard]] auto as(const std::size_t offset = 0) const noexcept + -> const T * { + assert(offset + sizeof(T) <= this->size_); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(this->data_ + offset); + } + +private: + const std::uint8_t *data_{nullptr}; + std::size_t size_{0}; +#if defined(_WIN32) + void *file_handle_{nullptr}; + void *mapping_handle_{nullptr}; +#else + int file_descriptor_{-1}; +#endif +}; + +} // namespace sourcemeta::core + +#endif diff --git a/src/lang/io/io_fileview.cc b/src/lang/io/io_fileview.cc new file mode 100644 index 000000000..209057214 --- /dev/null +++ b/src/lang/io/io_fileview.cc @@ -0,0 +1,105 @@ +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#else +#include // open, O_RDONLY +#include // mmap, munmap +#include // fstat +#include // close +#endif + +namespace sourcemeta::core { + +#if defined(_WIN32) + +FileView::FileView(const std::filesystem::path &path) { + this->file_handle_ = + CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (this->file_handle_ == INVALID_HANDLE_VALUE) { + throw FileViewError(path, "Could not open the file"); + } + + LARGE_INTEGER file_size; + if (GetFileSizeEx(this->file_handle_, &file_size) == 0) { + CloseHandle(this->file_handle_); + throw FileViewError(path, "Could not determine the file size"); + } + this->size_ = static_cast(file_size.QuadPart); + + this->mapping_handle_ = CreateFileMappingW(this->file_handle_, nullptr, + PAGE_READONLY, 0, 0, nullptr); + if (this->mapping_handle_ == nullptr) { + CloseHandle(this->file_handle_); + throw FileViewError(path, "Could not create a file mapping"); + } + + this->data_ = static_cast( + MapViewOfFile(this->mapping_handle_, FILE_MAP_READ, 0, 0, 0)); + if (this->data_ == nullptr) { + CloseHandle(this->mapping_handle_); + CloseHandle(this->file_handle_); + throw FileViewError(path, "Could not map the file into memory"); + } +} + +FileView::~FileView() { + if (this->data_ != nullptr) { + UnmapViewOfFile(this->data_); + } + + if (this->mapping_handle_ != nullptr) { + CloseHandle(this->mapping_handle_); + } + + if (this->file_handle_ != nullptr && + this->file_handle_ != INVALID_HANDLE_VALUE) { + CloseHandle(this->file_handle_); + } +} + +#else + +FileView::FileView(const std::filesystem::path &path) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + this->file_descriptor_ = open(path.c_str(), O_RDONLY); + if (this->file_descriptor_ == -1) { + throw FileViewError(path, "Could not open the file"); + } + + struct stat file_stat; + if (fstat(this->file_descriptor_, &file_stat) != 0) { + close(this->file_descriptor_); + throw FileViewError(path, "Could not determine the file size"); + } + this->size_ = static_cast(file_stat.st_size); + + void *mapped = mmap(nullptr, this->size_, PROT_READ, MAP_PRIVATE, + this->file_descriptor_, 0); + if (mapped == MAP_FAILED) { + close(this->file_descriptor_); + throw FileViewError(path, "Could not map the file into memory"); + } + + this->data_ = static_cast(mapped); +} + +FileView::~FileView() { + if (this->data_ != nullptr && this->size_ > 0) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + munmap(const_cast(this->data_), this->size_); + } + + if (this->file_descriptor_ != -1) { + close(this->file_descriptor_); + } +} + +#endif + +auto FileView::size() const noexcept -> std::size_t { return this->size_; } + +} // namespace sourcemeta::core diff --git a/test/io/CMakeLists.txt b/test/io/CMakeLists.txt index 623c66501..07baae803 100644 --- a/test/io/CMakeLists.txt +++ b/test/io/CMakeLists.txt @@ -4,9 +4,10 @@ sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME io io_flush_test.cc io_weakly_canonical_test.cc io_starts_with_test.cc - io_read_file_test.cc) + io_read_file_test.cc + io_fileview_test.cc) target_link_libraries(sourcemeta_core_io_unit PRIVATE sourcemeta::core::io) target_compile_definitions(sourcemeta_core_io_unit - PRIVATE TEST_DIRECTORY="${CMAKE_CURRENT_SOURCE_DIR}") + PRIVATE STUBS_DIRECTORY="${CMAKE_CURRENT_SOURCE_DIR}/stubs") diff --git a/test/io/io_canonical_test.cc b/test/io/io_canonical_test.cc index a9adb0563..2e6183ddf 100644 --- a/test/io/io_canonical_test.cc +++ b/test/io/io_canonical_test.cc @@ -4,12 +4,12 @@ TEST(IO_canonical, test_txt) { const auto path{sourcemeta::core::canonical( - std::filesystem::path{TEST_DIRECTORY} / ".." / "io" / "test.txt")}; - EXPECT_EQ(path, std::filesystem::path{TEST_DIRECTORY} / "test.txt"); + std::filesystem::path{STUBS_DIRECTORY} / "test.txt")}; + EXPECT_EQ(path, std::filesystem::path{STUBS_DIRECTORY} / "test.txt"); } TEST(IO_canonical, not_exists) { EXPECT_THROW(sourcemeta::core::canonical( - std::filesystem::path{TEST_DIRECTORY} / "foo.txt"), + std::filesystem::path{STUBS_DIRECTORY} / "foo.txt"), std::filesystem::filesystem_error); } diff --git a/test/io/io_fileview_test.cc b/test/io/io_fileview_test.cc new file mode 100644 index 000000000..097250ee6 --- /dev/null +++ b/test/io/io_fileview_test.cc @@ -0,0 +1,47 @@ +#include + +#include + +#include // std::uint32_t + +TEST(IO_FileView, size) { + const sourcemeta::core::FileView view{std::filesystem::path{STUBS_DIRECTORY} / + "fileview.bin"}; + EXPECT_EQ(view.size(), 20); +} + +TEST(IO_FileView, as_header) { + struct Header { + std::uint32_t magic; + std::uint32_t version; + std::uint32_t count; + }; + + const sourcemeta::core::FileView view{std::filesystem::path{STUBS_DIRECTORY} / + "fileview.bin"}; + const auto *header = view.as
(); + + EXPECT_EQ(header->magic, 0x56574946); + EXPECT_EQ(header->version, 1); + EXPECT_EQ(header->count, 42); +} + +TEST(IO_FileView, as_with_offset) { + struct Data { + std::uint32_t value1; + std::uint32_t value2; + }; + + const sourcemeta::core::FileView view{std::filesystem::path{STUBS_DIRECTORY} / + "fileview.bin"}; + const auto *data = view.as(12); + + EXPECT_EQ(data->value1, 0xDEADBEEF); + EXPECT_EQ(data->value2, 0xCAFEBABE); +} + +TEST(IO_FileView, file_not_found) { + EXPECT_THROW(sourcemeta::core::FileView( + std::filesystem::path{STUBS_DIRECTORY} / "nonexistent.bin"), + sourcemeta::core::FileViewError); +} diff --git a/test/io/io_flush_test.cc b/test/io/io_flush_test.cc index 97038322c..2220c2d08 100644 --- a/test/io/io_flush_test.cc +++ b/test/io/io_flush_test.cc @@ -3,13 +3,13 @@ #include TEST(IO_flush, test_txt) { - const auto path{std::filesystem::path{TEST_DIRECTORY} / "test.txt"}; + const auto path{std::filesystem::path{STUBS_DIRECTORY} / "test.txt"}; sourcemeta::core::flush(path); SUCCEED(); } TEST(IO_flush, not_exists) { - const auto path{std::filesystem::path{TEST_DIRECTORY} / "foo.txt"}; + const auto path{std::filesystem::path{STUBS_DIRECTORY} / "foo.txt"}; try { sourcemeta::core::flush(path); diff --git a/test/io/io_read_file_test.cc b/test/io/io_read_file_test.cc index 9359a6b67..96d74296f 100644 --- a/test/io/io_read_file_test.cc +++ b/test/io/io_read_file_test.cc @@ -7,7 +7,7 @@ TEST(IO_read_file, text_file) { auto stream{sourcemeta::core::read_file( - std::filesystem::path{TEST_DIRECTORY} / "test.txt")}; + std::filesystem::path{STUBS_DIRECTORY} / "test.txt")}; std::ostringstream contents; contents << stream.rdbuf(); auto result{contents.str()}; @@ -19,10 +19,10 @@ TEST(IO_read_file, text_file) { TEST(IO_read_file, directory) { try { - sourcemeta::core::read_file(std::filesystem::path{TEST_DIRECTORY}); + sourcemeta::core::read_file(std::filesystem::path{STUBS_DIRECTORY}); } catch (const std::filesystem::filesystem_error &error) { EXPECT_EQ(error.code(), std::errc::is_a_directory); - EXPECT_EQ(error.path1(), std::filesystem::path{TEST_DIRECTORY}); + EXPECT_EQ(error.path1(), std::filesystem::path{STUBS_DIRECTORY}); } catch (...) { FAIL() << "The parse function was expected to throw a filesystem error"; } diff --git a/test/io/io_weakly_canonical_test.cc b/test/io/io_weakly_canonical_test.cc index faffd67db..56f408f91 100644 --- a/test/io/io_weakly_canonical_test.cc +++ b/test/io/io_weakly_canonical_test.cc @@ -4,12 +4,12 @@ TEST(IO_weakly_canonical, test_txt) { const auto path{sourcemeta::core::weakly_canonical( - std::filesystem::path{TEST_DIRECTORY} / ".." / "io" / "test.txt")}; - EXPECT_EQ(path, std::filesystem::path{TEST_DIRECTORY} / "test.txt"); + std::filesystem::path{STUBS_DIRECTORY} / "test.txt")}; + EXPECT_EQ(path, std::filesystem::path{STUBS_DIRECTORY} / "test.txt"); } TEST(IO_weakly_canonical, not_exists) { const auto path{sourcemeta::core::weakly_canonical( - std::filesystem::path{TEST_DIRECTORY} / "foo.txt")}; - EXPECT_EQ(path, std::filesystem::path{TEST_DIRECTORY} / "foo.txt"); + std::filesystem::path{STUBS_DIRECTORY} / "foo.txt")}; + EXPECT_EQ(path, std::filesystem::path{STUBS_DIRECTORY} / "foo.txt"); } diff --git a/test/io/stubs/fileview.bin b/test/io/stubs/fileview.bin new file mode 100644 index 000000000..c940f1720 Binary files /dev/null and b/test/io/stubs/fileview.bin differ diff --git a/test/io/test.txt b/test/io/stubs/test.txt similarity index 100% rename from test/io/test.txt rename to test/io/stubs/test.txt