LibCore: Add immutable byte storage for mapped ranges
Allow MappedFile to expose a byte range within an fd. It maps the page-aligned region required by mmap internally. Add ImmutableBytes as a small shared holder for either owned bytes or a mapped file. This lets cache code thread file-backed response bodies through without copying response data into anonymous memory. Cover non-page-aligned ranges, empty ranges, invalid ranges, and mapped ImmutableBytes in TestLibCoreMappedFile.
This commit is contained in:
parent
429b7fc809
commit
26504d84bb
6 changed files with 242 additions and 10 deletions
|
|
@ -11,6 +11,7 @@ set(SOURCES
|
|||
EventLoopImplementation.cpp
|
||||
EventReceiver.cpp
|
||||
File.cpp
|
||||
ImmutableBytes.cpp
|
||||
MappedFile.cpp
|
||||
MimeData.cpp
|
||||
Notifier.cpp
|
||||
|
|
|
|||
79
Libraries/LibCore/ImmutableBytes.cpp
Normal file
79
Libraries/LibCore/ImmutableBytes.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibCore/ImmutableBytes.h>
|
||||
|
||||
namespace Core {
|
||||
|
||||
ErrorOr<ImmutableBytes> ImmutableBytes::copy(ReadonlyBytes bytes)
|
||||
{
|
||||
return adopt(TRY(ByteBuffer::copy(bytes)));
|
||||
}
|
||||
|
||||
ImmutableBytes ImmutableBytes::adopt(ByteBuffer bytes)
|
||||
{
|
||||
return ImmutableBytes { adopt_ref(*new Impl(move(bytes))) };
|
||||
}
|
||||
|
||||
ImmutableBytes ImmutableBytes::adopt_mapped_file(NonnullOwnPtr<MappedFile> mapped_file)
|
||||
{
|
||||
return ImmutableBytes { adopt_ref(*new Impl(move(mapped_file))) };
|
||||
}
|
||||
|
||||
ErrorOr<ImmutableBytes> ImmutableBytes::map_from_fd_range_and_close(int fd, StringView path, off_t offset, size_t size)
|
||||
{
|
||||
return adopt_mapped_file(TRY(MappedFile::map_from_fd_range_and_close(fd, path, offset, size)));
|
||||
}
|
||||
|
||||
bool ImmutableBytes::is_file_backed() const
|
||||
{
|
||||
return m_impl && m_impl->is_file_backed();
|
||||
}
|
||||
|
||||
ReadonlyBytes ImmutableBytes::bytes() const
|
||||
{
|
||||
if (!m_impl)
|
||||
return {};
|
||||
return m_impl->bytes();
|
||||
}
|
||||
|
||||
ErrorOr<ByteBuffer> ImmutableBytes::copy_to_byte_buffer() const
|
||||
{
|
||||
return ByteBuffer::copy(bytes());
|
||||
}
|
||||
|
||||
ImmutableBytes::ImmutableBytes(NonnullRefPtr<Impl> impl)
|
||||
: m_impl(move(impl))
|
||||
{
|
||||
}
|
||||
|
||||
ImmutableBytes::Impl::Impl(ByteBuffer bytes)
|
||||
: m_storage(move(bytes))
|
||||
{
|
||||
}
|
||||
|
||||
ImmutableBytes::Impl::Impl(NonnullOwnPtr<MappedFile> mapped_file)
|
||||
: m_storage(move(mapped_file))
|
||||
{
|
||||
}
|
||||
|
||||
bool ImmutableBytes::Impl::is_file_backed() const
|
||||
{
|
||||
return m_storage.has<NonnullOwnPtr<MappedFile>>();
|
||||
}
|
||||
|
||||
ReadonlyBytes ImmutableBytes::Impl::bytes() const
|
||||
{
|
||||
return m_storage.visit(
|
||||
[](ByteBuffer const& bytes) -> ReadonlyBytes {
|
||||
return bytes.bytes();
|
||||
},
|
||||
[](NonnullOwnPtr<MappedFile> const& mapped_file) -> ReadonlyBytes {
|
||||
return mapped_file->bytes();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
54
Libraries/LibCore/ImmutableBytes.h
Normal file
54
Libraries/LibCore/ImmutableBytes.h
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibCore/Export.h>
|
||||
#include <LibCore/MappedFile.h>
|
||||
|
||||
namespace Core {
|
||||
|
||||
class CORE_API ImmutableBytes {
|
||||
public:
|
||||
static ErrorOr<ImmutableBytes> copy(ReadonlyBytes);
|
||||
static ImmutableBytes adopt(ByteBuffer);
|
||||
static ImmutableBytes adopt_mapped_file(NonnullOwnPtr<MappedFile>);
|
||||
static ErrorOr<ImmutableBytes> map_from_fd_range_and_close(int fd, StringView path, off_t offset, size_t size);
|
||||
|
||||
ImmutableBytes() = default;
|
||||
|
||||
[[nodiscard]] bool is_empty() const { return size() == 0; }
|
||||
[[nodiscard]] bool is_valid() const { return m_impl; }
|
||||
[[nodiscard]] bool is_file_backed() const;
|
||||
|
||||
[[nodiscard]] size_t size() const { return bytes().size(); }
|
||||
[[nodiscard]] ReadonlyBytes bytes() const LIFETIME_BOUND;
|
||||
[[nodiscard]] ErrorOr<ByteBuffer> copy_to_byte_buffer() const;
|
||||
|
||||
private:
|
||||
class Impl final : public RefCounted<Impl> {
|
||||
public:
|
||||
explicit Impl(ByteBuffer);
|
||||
explicit Impl(NonnullOwnPtr<MappedFile>);
|
||||
|
||||
[[nodiscard]] bool is_file_backed() const;
|
||||
[[nodiscard]] ReadonlyBytes bytes() const LIFETIME_BOUND;
|
||||
|
||||
private:
|
||||
Variant<ByteBuffer, NonnullOwnPtr<MappedFile>> m_storage;
|
||||
};
|
||||
|
||||
explicit ImmutableBytes(NonnullRefPtr<Impl>);
|
||||
|
||||
RefPtr<Impl> m_impl;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Checked.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibCore/MappedFile.h>
|
||||
|
|
@ -27,12 +28,35 @@ ErrorOr<NonnullOwnPtr<MappedFile>> MappedFile::map_from_file(NonnullOwnPtr<Core:
|
|||
|
||||
ErrorOr<NonnullOwnPtr<MappedFile>> MappedFile::map_from_fd_and_close(int fd, [[maybe_unused]] StringView path, Mode mode)
|
||||
{
|
||||
ScopeGuard fd_close_guard = [fd] {
|
||||
ArmedScopeGuard fd_close_guard = [fd] {
|
||||
(void)System::close(fd);
|
||||
};
|
||||
|
||||
auto stat = TRY(Core::System::fstat(fd));
|
||||
auto size = stat.st_size;
|
||||
if (stat.st_size < 0)
|
||||
return Error::from_errno(EINVAL);
|
||||
|
||||
fd_close_guard.disarm();
|
||||
return map_from_fd_range_and_close(fd, path, 0, static_cast<size_t>(stat.st_size), mode);
|
||||
}
|
||||
|
||||
ErrorOr<NonnullOwnPtr<MappedFile>> MappedFile::map_from_fd_range_and_close(int fd, [[maybe_unused]] StringView path, off_t offset, size_t size, Mode mode)
|
||||
{
|
||||
ScopeGuard fd_close_guard = [fd] {
|
||||
(void)System::close(fd);
|
||||
};
|
||||
|
||||
if (offset < 0 || !AK::is_within_range<size_t>(offset))
|
||||
return Error::from_errno(EINVAL);
|
||||
|
||||
auto stat = TRY(Core::System::fstat(fd));
|
||||
if (stat.st_size < 0 || !AK::is_within_range<size_t>(stat.st_size))
|
||||
return Error::from_errno(EINVAL);
|
||||
|
||||
auto file_size = static_cast<size_t>(stat.st_size);
|
||||
auto requested_offset = static_cast<size_t>(offset);
|
||||
if (requested_offset > file_size || size > file_size - requested_offset)
|
||||
return Error::from_errno(EINVAL);
|
||||
|
||||
int protection;
|
||||
int flags;
|
||||
|
|
@ -48,23 +72,40 @@ ErrorOr<NonnullOwnPtr<MappedFile>> MappedFile::map_from_fd_and_close(int fd, [[m
|
|||
break;
|
||||
}
|
||||
|
||||
auto* ptr = TRY(Core::System::mmap(nullptr, size, protection, flags, fd, 0, 0, path));
|
||||
if (size == 0)
|
||||
return adopt_own(*new MappedFile(nullptr, 0, nullptr, 0, mode));
|
||||
|
||||
return adopt_own(*new MappedFile(ptr, size, mode));
|
||||
auto page_aligned_offset = align_down_to(requested_offset, PAGE_SIZE);
|
||||
auto offset_in_mapping = requested_offset - page_aligned_offset;
|
||||
|
||||
Checked<size_t> mapping_size = offset_in_mapping;
|
||||
mapping_size += size;
|
||||
if (mapping_size.has_overflow())
|
||||
return Error::from_errno(EOVERFLOW);
|
||||
|
||||
auto* mapping = TRY(Core::System::mmap(nullptr, mapping_size.value(), protection, flags, fd, page_aligned_offset, 0, path));
|
||||
auto* data = reinterpret_cast<u8*>(mapping) + offset_in_mapping;
|
||||
|
||||
return adopt_own(*new MappedFile(mapping, mapping_size.value(), data, size, mode));
|
||||
}
|
||||
|
||||
MappedFile::MappedFile(void* ptr, size_t size, Mode mode)
|
||||
: FixedMemoryStream(Bytes { ptr, size }, mode)
|
||||
, m_data(ptr)
|
||||
MappedFile::MappedFile(void* mapping, size_t mapping_size, void* data, size_t size, Mode mode)
|
||||
: FixedMemoryStream(Bytes { data, size }, mode)
|
||||
, m_mapping(mapping)
|
||||
, m_mapping_size(mapping_size)
|
||||
, m_data(data)
|
||||
, m_size(size)
|
||||
{
|
||||
}
|
||||
|
||||
MappedFile::~MappedFile()
|
||||
{
|
||||
auto res = Core::System::munmap(m_data, m_size);
|
||||
if (!m_mapping)
|
||||
return;
|
||||
|
||||
auto res = Core::System::munmap(m_mapping, m_mapping_size);
|
||||
if (res.is_error())
|
||||
dbgln("Failed to unmap MappedFile (@ {:p}): {}", m_data, res.error());
|
||||
dbgln("Failed to unmap MappedFile (@ {:p}): {}", m_mapping, res.error());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public:
|
|||
static ErrorOr<NonnullOwnPtr<MappedFile>> map(StringView path, Mode mode = Mode::ReadOnly);
|
||||
static ErrorOr<NonnullOwnPtr<MappedFile>> map_from_file(NonnullOwnPtr<Core::File>, StringView path);
|
||||
static ErrorOr<NonnullOwnPtr<MappedFile>> map_from_fd_and_close(int fd, StringView path, Mode mode = Mode::ReadOnly);
|
||||
static ErrorOr<NonnullOwnPtr<MappedFile>> map_from_fd_range_and_close(int fd, StringView path, off_t offset, size_t size, Mode mode = Mode::ReadOnly);
|
||||
virtual ~MappedFile();
|
||||
|
||||
// Non-stream APIs for using MappedFile as a simple POSIX API wrapper.
|
||||
|
|
@ -33,8 +34,10 @@ public:
|
|||
ReadonlyBytes bytes() const LIFETIME_BOUND { return { m_data, m_size }; }
|
||||
|
||||
private:
|
||||
explicit MappedFile(void*, size_t, Mode);
|
||||
explicit MappedFile(void* mapping, size_t mapping_size, void* data, size_t size, Mode);
|
||||
|
||||
void* m_mapping { nullptr };
|
||||
size_t m_mapping_size { 0 };
|
||||
void* m_data { nullptr };
|
||||
size_t m_size { 0 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <AK/MaybeOwned.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibCore/ImmutableBytes.h>
|
||||
#include <LibCore/MappedFile.h>
|
||||
#include <LibCore/StandardPaths.h>
|
||||
#include <LibCore/System.h>
|
||||
|
|
@ -128,6 +129,59 @@ TEST_CASE(mapped_file_adopt_fd)
|
|||
// A single seek & read test should be fine for now.
|
||||
}
|
||||
|
||||
TEST_CASE(mapped_file_adopt_fd_range)
|
||||
{
|
||||
int rc = TRY_OR_FAIL(Core::System::open("./long_lines.txt"sv, O_RDONLY));
|
||||
EXPECT(rc >= 0);
|
||||
|
||||
auto file = TRY_OR_FAIL(Core::MappedFile::map_from_fd_range_and_close(rc, "./long_lines.txt"sv, 500, 16));
|
||||
|
||||
EXPECT_EQ(file->size().release_value(), 16ul);
|
||||
EXPECT_EQ(file->bytes(), expected_seek_contents1.bytes());
|
||||
|
||||
auto buffer = TRY_OR_FAIL(ByteBuffer::create_uninitialized(16));
|
||||
TRY_OR_FAIL(file->read_until_filled(buffer));
|
||||
EXPECT_EQ(buffer.bytes(), expected_seek_contents1.bytes());
|
||||
EXPECT(file->is_eof());
|
||||
}
|
||||
|
||||
TEST_CASE(mapped_file_adopt_empty_fd_range)
|
||||
{
|
||||
int rc = TRY_OR_FAIL(Core::System::open("./long_lines.txt"sv, O_RDONLY));
|
||||
EXPECT(rc >= 0);
|
||||
|
||||
auto file = TRY_OR_FAIL(Core::MappedFile::map_from_fd_range_and_close(rc, "./long_lines.txt"sv, 500, 0));
|
||||
|
||||
EXPECT_EQ(file->size().release_value(), 0ul);
|
||||
EXPECT(file->bytes().is_empty());
|
||||
EXPECT(file->is_eof());
|
||||
}
|
||||
|
||||
TEST_CASE(mapped_file_adopt_invalid_fd_range)
|
||||
{
|
||||
int rc = TRY_OR_FAIL(Core::System::open("./long_lines.txt"sv, O_RDONLY));
|
||||
EXPECT(rc >= 0);
|
||||
|
||||
auto maybe_file = Core::MappedFile::map_from_fd_range_and_close(rc, "./long_lines.txt"sv, 8700, 16);
|
||||
EXPECT(maybe_file.is_error());
|
||||
EXPECT_EQ(maybe_file.error().code(), EINVAL);
|
||||
}
|
||||
|
||||
TEST_CASE(immutable_bytes_from_mapped_file_range)
|
||||
{
|
||||
int rc = TRY_OR_FAIL(Core::System::open("./long_lines.txt"sv, O_RDONLY));
|
||||
EXPECT(rc >= 0);
|
||||
|
||||
auto bytes = TRY_OR_FAIL(Core::ImmutableBytes::map_from_fd_range_and_close(rc, "./long_lines.txt"sv, 500, 16));
|
||||
|
||||
EXPECT(bytes.is_file_backed());
|
||||
EXPECT_EQ(bytes.size(), 16ul);
|
||||
EXPECT_EQ(bytes.bytes(), expected_seek_contents1.bytes());
|
||||
|
||||
auto copied_bytes = TRY_OR_FAIL(bytes.copy_to_byte_buffer());
|
||||
EXPECT_EQ(copied_bytes.bytes(), expected_seek_contents1.bytes());
|
||||
}
|
||||
|
||||
TEST_CASE(mapped_file_adopt_invalid_fd)
|
||||
{
|
||||
auto maybe_file = Core::MappedFile::map_from_fd_and_close(-1, "./long_lines.txt"sv);
|
||||
|
|
|
|||
Loading…
Reference in a new issue