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:
Andreas Kling 2026-05-15 14:27:18 +02:00 committed by Andreas Kling
parent 429b7fc809
commit 26504d84bb
6 changed files with 242 additions and 10 deletions

View file

@ -11,6 +11,7 @@ set(SOURCES
EventLoopImplementation.cpp
EventReceiver.cpp
File.cpp
ImmutableBytes.cpp
MappedFile.cpp
MimeData.cpp
Notifier.cpp

View 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();
});
}
}

View 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;
};
}

View file

@ -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());
}
}

View file

@ -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 };
};

View file

@ -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);