mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-11 05:25:32 +03:00
DeprecatedString (formerly String) has been with us since the start, and it has served us well. However, it has a number of shortcomings that I'd like to address. Some of these issues are hard if not impossible to solve incrementally inside of DeprecatedString, so instead of doing that, let's build a new String class and then incrementally move over to it instead. Problems in DeprecatedString: - It assumes string allocation never fails. This makes it impossible to use in allocation-sensitive contexts, and is the reason we had to ban DeprecatedString from the kernel entirely. - The awkward null state. DeprecatedString can be null. It's different from the empty state, although null strings are considered empty. All code is immediately nicer when using Optional<DeprecatedString> but DeprecatedString came before Optional, which is how we ended up like this. - The encoding of the underlying data is ambiguous. For the most part, we use it as if it's always UTF-8, but there have been cases where we pass around strings in other encodings (e.g ISO8859-1) - operator[] and length() are used to iterate over DeprecatedString one byte at a time. This is done all over the codebase, and will *not* give the right results unless the string is all ASCII. How we solve these issues in the new String: - Functions that may allocate now return ErrorOr<String> so that ENOMEM errors can be passed to the caller. - String has no null state. Use Optional<String> when needed. - String is always UTF-8. This is validated when constructing a String. We may need to add a bypass for this in the future, for cases where you have a known-good string, but for now: validate all the things! - There is no operator[] or length(). You can get the underlying data with bytes(), but for iterating over code points, you should be using an UTF-8 iterator. Furthermore, it has two nifty new features: - String implements a small string optimization (SSO) for strings that can fit entirely within a pointer. This means up to 3 bytes on 32-bit platforms, and 7 bytes on 64-bit platforms. Such small strings will not be heap-allocated. - String can create substrings without making a deep copy of the substring. Instead, the superstring gets +1 refcount from the substring, and it acts like a view into the superstring. To make substrings like this, use the substring_with_shared_superstring() API. One caveat: - String does not guarantee that the underlying data is null-terminated like DeprecatedString does today. While this was nifty in a handful of places where we were calling C functions, it did stand in the way of shared-superstring substrings.
338 lines
8.9 KiB
C++
338 lines
8.9 KiB
C++
/*
|
|
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/Checked.h>
|
|
#include <AK/Format.h>
|
|
#include <AK/Memory.h>
|
|
#include <AK/String.h>
|
|
#include <AK/StringBuilder.h>
|
|
#include <AK/Utf8View.h>
|
|
#include <stdlib.h>
|
|
|
|
namespace AK {
|
|
|
|
namespace Detail {
|
|
|
|
class StringData final : public RefCounted<StringData> {
|
|
public:
|
|
static ErrorOr<NonnullRefPtr<StringData>> create_uninitialized(size_t, u8*& buffer);
|
|
static ErrorOr<NonnullRefPtr<StringData>> create_substring(StringData const& superstring, size_t start, size_t byte_count);
|
|
static ErrorOr<NonnullRefPtr<StringData>> from_utf8(char const* utf8_bytes, size_t);
|
|
|
|
struct SubstringData {
|
|
StringData const* superstring { nullptr };
|
|
u32 start_offset { 0 };
|
|
};
|
|
|
|
void operator delete(void* ptr);
|
|
|
|
~StringData();
|
|
|
|
SubstringData const& substring_data() const
|
|
{
|
|
return *reinterpret_cast<SubstringData const*>(m_bytes_or_substring_data);
|
|
}
|
|
|
|
// NOTE: There is no guarantee about null-termination.
|
|
ReadonlyBytes bytes() const
|
|
{
|
|
if (m_substring) {
|
|
auto const& data = substring_data();
|
|
return data.superstring->bytes().slice(data.start_offset, m_byte_count);
|
|
}
|
|
return { &m_bytes_or_substring_data[0], m_byte_count };
|
|
}
|
|
|
|
StringView bytes_as_string_view() const { return { bytes() }; }
|
|
|
|
bool operator==(StringData const& other) const
|
|
{
|
|
return bytes_as_string_view() == other.bytes_as_string_view();
|
|
}
|
|
|
|
unsigned hash() const
|
|
{
|
|
if (!m_has_hash)
|
|
compute_hash();
|
|
return m_hash;
|
|
}
|
|
|
|
private:
|
|
explicit StringData(size_t byte_count);
|
|
StringData(StringData const& superstring, size_t start, size_t byte_count);
|
|
|
|
void compute_hash() const;
|
|
|
|
u32 m_byte_count { 0 };
|
|
mutable unsigned m_hash { 0 };
|
|
mutable bool m_has_hash { false };
|
|
bool m_substring { false };
|
|
|
|
u8 m_bytes_or_substring_data[0];
|
|
};
|
|
|
|
void StringData::operator delete(void* ptr)
|
|
{
|
|
free(ptr);
|
|
}
|
|
|
|
StringData::StringData(size_t byte_count)
|
|
: m_byte_count(byte_count)
|
|
{
|
|
}
|
|
|
|
StringData::StringData(StringData const& superstring, size_t start, size_t byte_count)
|
|
: m_byte_count(byte_count)
|
|
, m_substring(true)
|
|
{
|
|
auto& data = const_cast<SubstringData&>(substring_data());
|
|
data.start_offset = start;
|
|
data.superstring = &superstring;
|
|
superstring.ref();
|
|
}
|
|
|
|
StringData::~StringData()
|
|
{
|
|
if (m_substring)
|
|
substring_data().superstring->unref();
|
|
}
|
|
|
|
constexpr size_t allocation_size_for_string_data(size_t length)
|
|
{
|
|
return sizeof(StringData) + (sizeof(char) * length) + sizeof(char);
|
|
}
|
|
|
|
ErrorOr<NonnullRefPtr<StringData>> StringData::create_uninitialized(size_t byte_count, u8*& buffer)
|
|
{
|
|
VERIFY(byte_count);
|
|
void* slot = malloc(allocation_size_for_string_data(byte_count));
|
|
if (!slot) {
|
|
return Error::from_errno(ENOMEM);
|
|
}
|
|
auto new_string_data = adopt_ref(*new (slot) StringData(byte_count));
|
|
buffer = const_cast<u8*>(new_string_data->bytes().data());
|
|
return new_string_data;
|
|
}
|
|
|
|
ErrorOr<NonnullRefPtr<StringData>> StringData::from_utf8(char const* utf8_data, size_t byte_count)
|
|
{
|
|
// Strings of MAX_SHORT_STRING_BYTE_COUNT bytes or less should be handled by the String short string optimization.
|
|
VERIFY(byte_count > String::MAX_SHORT_STRING_BYTE_COUNT);
|
|
|
|
Utf8View view(StringView(utf8_data, byte_count));
|
|
if (!view.validate())
|
|
return Error::from_string_literal("StringData::from_utf8: Input was not valid UTF-8");
|
|
|
|
VERIFY(utf8_data);
|
|
u8* buffer = nullptr;
|
|
auto new_string_data = TRY(create_uninitialized(byte_count, buffer));
|
|
memcpy(buffer, utf8_data, byte_count * sizeof(char));
|
|
return new_string_data;
|
|
}
|
|
|
|
ErrorOr<NonnullRefPtr<StringData>> StringData::create_substring(StringData const& superstring, size_t start, size_t byte_count)
|
|
{
|
|
// Strings of MAX_SHORT_STRING_BYTE_COUNT bytes or less should be handled by the String short string optimization.
|
|
VERIFY(byte_count > String::MAX_SHORT_STRING_BYTE_COUNT);
|
|
|
|
void* slot = malloc(sizeof(StringData) + sizeof(StringData::SubstringData));
|
|
if (!slot) {
|
|
return Error::from_errno(ENOMEM);
|
|
}
|
|
return adopt_ref(*new (slot) StringData(superstring, start, byte_count));
|
|
}
|
|
|
|
void StringData::compute_hash() const
|
|
{
|
|
auto bytes = this->bytes();
|
|
if (bytes.size() == 0)
|
|
m_hash = 0;
|
|
else
|
|
m_hash = string_hash(reinterpret_cast<char const*>(bytes.data()), bytes.size());
|
|
m_has_hash = true;
|
|
}
|
|
|
|
}
|
|
|
|
String::String(NonnullRefPtr<Detail::StringData> data)
|
|
: m_data(&data.leak_ref())
|
|
{
|
|
}
|
|
|
|
String::String(ShortString short_string)
|
|
: m_short_string(short_string)
|
|
{
|
|
}
|
|
|
|
String::String(String const& other)
|
|
: m_data(other.m_data)
|
|
{
|
|
if (!is_short_string())
|
|
m_data->ref();
|
|
}
|
|
|
|
String::String(String&& other)
|
|
: m_data(exchange(other.m_data, nullptr))
|
|
{
|
|
}
|
|
|
|
String& String::operator=(String&& other)
|
|
{
|
|
m_data = exchange(other.m_data, nullptr);
|
|
return *this;
|
|
}
|
|
|
|
String& String::operator=(String const& other)
|
|
{
|
|
if (&other != this) {
|
|
m_data = other.m_data;
|
|
if (!is_short_string())
|
|
m_data->ref();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
String::~String()
|
|
{
|
|
if (!is_short_string() && m_data)
|
|
m_data->unref();
|
|
}
|
|
|
|
String::String()
|
|
{
|
|
// This is an empty string, it's always short and zero-length.
|
|
m_short_string.byte_count_and_short_string_flag = SHORT_STRING_FLAG;
|
|
}
|
|
|
|
ErrorOr<String> String::from_utf8(StringView view)
|
|
{
|
|
if (view.length() <= MAX_SHORT_STRING_BYTE_COUNT) {
|
|
ShortString short_string;
|
|
if (!view.is_empty())
|
|
memcpy(short_string.storage, view.characters_without_null_termination(), view.length());
|
|
short_string.byte_count_and_short_string_flag = (view.length() << 1) | SHORT_STRING_FLAG;
|
|
return String { short_string };
|
|
}
|
|
auto data = TRY(Detail::StringData::from_utf8(view.characters_without_null_termination(), view.length()));
|
|
return String { move(data) };
|
|
}
|
|
|
|
StringView String::bytes_as_string_view() const
|
|
{
|
|
return StringView(bytes());
|
|
}
|
|
|
|
ReadonlyBytes String::bytes() const
|
|
{
|
|
if (is_short_string())
|
|
return m_short_string.bytes();
|
|
return m_data->bytes();
|
|
}
|
|
|
|
bool String::is_empty() const
|
|
{
|
|
return bytes().size() == 0;
|
|
}
|
|
|
|
ErrorOr<String> String::vformatted(StringView fmtstr, TypeErasedFormatParams& params)
|
|
{
|
|
StringBuilder builder;
|
|
TRY(vformat(builder, fmtstr, params));
|
|
return builder.to_string();
|
|
}
|
|
|
|
bool String::operator==(String const& other) const
|
|
{
|
|
if (is_short_string())
|
|
return m_data == other.m_data;
|
|
return bytes_as_string_view() == other.bytes_as_string_view();
|
|
}
|
|
|
|
bool String::operator==(StringView other) const
|
|
{
|
|
return bytes_as_string_view() == other;
|
|
}
|
|
|
|
ErrorOr<String> String::substring_from_byte_offset(size_t start, size_t byte_count) const
|
|
{
|
|
if (!byte_count)
|
|
return String {};
|
|
return String::from_utf8(bytes_as_string_view().substring_view(start, byte_count));
|
|
}
|
|
|
|
ErrorOr<String> String::substring_from_byte_offset_with_shared_superstring(size_t start, size_t byte_count) const
|
|
{
|
|
if (!byte_count)
|
|
return String {};
|
|
if (byte_count <= MAX_SHORT_STRING_BYTE_COUNT)
|
|
return String::from_utf8(bytes_as_string_view().substring_view(start, byte_count));
|
|
return String { TRY(Detail::StringData::create_substring(*m_data, start, byte_count)) };
|
|
}
|
|
|
|
bool String::operator==(char const* c_string) const
|
|
{
|
|
return bytes_as_string_view() == c_string;
|
|
}
|
|
|
|
u32 String::hash() const
|
|
{
|
|
if (is_short_string()) {
|
|
auto bytes = this->bytes();
|
|
return string_hash(reinterpret_cast<char const*>(bytes.data()), bytes.size());
|
|
}
|
|
return m_data->hash();
|
|
}
|
|
|
|
Utf8View String::code_points() const
|
|
{
|
|
return Utf8View(bytes_as_string_view());
|
|
}
|
|
|
|
ErrorOr<void> Formatter<String>::format(FormatBuilder& builder, String const& utf8_string)
|
|
{
|
|
return Formatter<StringView>::format(builder, utf8_string.bytes_as_string_view());
|
|
}
|
|
|
|
ErrorOr<String> String::replace(StringView needle, StringView replacement, ReplaceMode replace_mode) const
|
|
{
|
|
return StringUtils::replace(*this, needle, replacement, replace_mode);
|
|
}
|
|
|
|
bool String::is_short_string() const
|
|
{
|
|
return reinterpret_cast<uintptr_t>(m_data) & SHORT_STRING_FLAG;
|
|
}
|
|
|
|
ReadonlyBytes String::ShortString::bytes() const
|
|
{
|
|
return { storage, byte_count() };
|
|
}
|
|
|
|
size_t String::ShortString::byte_count() const
|
|
{
|
|
return byte_count_and_short_string_flag >> 1;
|
|
}
|
|
|
|
unsigned Traits<String>::hash(String const& string)
|
|
{
|
|
return string.hash();
|
|
}
|
|
|
|
DeprecatedString String::to_deprecated_string() const
|
|
{
|
|
return DeprecatedString(bytes_as_string_view());
|
|
}
|
|
|
|
ErrorOr<String> String::from_deprecated_string(DeprecatedString const& deprecated_string)
|
|
{
|
|
Utf8View view(deprecated_string);
|
|
if (!view.validate())
|
|
return Error::from_string_literal("String::from_deprecated_string: Input was not valid UTF-8");
|
|
return String::from_utf8(deprecated_string.view());
|
|
}
|
|
|
|
}
|