Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/lang/io/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/lang/io/include/sourcemeta/core/io.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
#include <sourcemeta/core/io_export.h>
#endif

// NOLINTBEGIN(misc-include-cleaner)
#include <sourcemeta/core/io_error.h>
#include <sourcemeta/core/io_fileview.h>
// NOLINTEND(misc-include-cleaner)

#include <cassert> // assert
#include <filesystem> // std::filesystem
#include <fstream> // std::basic_ifstream
Expand Down
52 changes: 52 additions & 0 deletions src/lang/io/include/sourcemeta/core/io_error.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#ifndef SOURCEMETA_CORE_IO_ERROR_H_
#define SOURCEMETA_CORE_IO_ERROR_H_

#ifndef SOURCEMETA_CORE_IO_EXPORT
#include <sourcemeta/core/io_export.h>
#endif

#include <exception> // std::exception
#include <filesystem> // std::filesystem::path
#include <string> // std::string
#include <string_view> // std::string_view
#include <utility> // 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
67 changes: 67 additions & 0 deletions src/lang/io/include/sourcemeta/core/io_fileview.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#ifndef SOURCEMETA_CORE_IO_FILEVIEW_H_
#define SOURCEMETA_CORE_IO_FILEVIEW_H_

#ifndef SOURCEMETA_CORE_IO_EXPORT
#include <sourcemeta/core/io_export.h>
#endif

#include <cassert> // assert
#include <cstddef> // std::size_t
#include <cstdint> // std::uint8_t
#include <filesystem> // std::filesystem::path

namespace sourcemeta::core {

/// @ingroup io
/// A read-only memory-mapped file. For example:
///
/// ```cpp
/// #include <sourcemeta/core/io.h>
/// #include <cassert>
///
/// struct Header {
/// std::uint32_t magic;
/// std::uint32_t version;
/// };
///
/// sourcemeta::core::FileView view{"/path/to/file.bin"};
/// const auto *header = view.as<Header>();
/// 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 <typename T>
[[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<const T *>(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
105 changes: 105 additions & 0 deletions src/lang/io/io_fileview.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#include <sourcemeta/core/io_error.h>
#include <sourcemeta/core/io_fileview.h>

#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <fcntl.h> // open, O_RDONLY
#include <sys/mman.h> // mmap, munmap
#include <sys/stat.h> // fstat
#include <unistd.h> // 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<std::size_t>(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<const std::uint8_t *>(
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<std::size_t>(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<const std::uint8_t *>(mapped);
}

FileView::~FileView() {
if (this->data_ != nullptr && this->size_ > 0) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
munmap(const_cast<std::uint8_t *>(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
5 changes: 3 additions & 2 deletions test/io/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
6 changes: 3 additions & 3 deletions test/io/io_canonical_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
47 changes: 47 additions & 0 deletions test/io/io_fileview_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include <gtest/gtest.h>

#include <sourcemeta/core/io.h>

#include <cstdint> // 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<Header>();

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<Data>(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);
}
4 changes: 2 additions & 2 deletions test/io/io_flush_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
#include <sourcemeta/core/io.h>

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);
Expand Down
6 changes: 3 additions & 3 deletions test/io/io_read_file_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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()};
Expand All @@ -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";
}
Expand Down
8 changes: 4 additions & 4 deletions test/io/io_weakly_canonical_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Binary file added test/io/stubs/fileview.bin
Binary file not shown.
File renamed without changes.