diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 7177ad3224..c14d10bf39 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -3,6 +3,7 @@ add_files( backup_type.hpp base_bitset_type.hpp bitmath_func.hpp + container_func.hpp convertible_through_base.hpp endian_func.hpp enum_type.hpp @@ -22,7 +23,8 @@ add_files( random_func.cpp random_func.hpp smallstack_type.hpp - container_func.hpp + string_builder.cpp + string_builder.hpp strong_typedef_type.hpp utf8.cpp utf8.hpp diff --git a/src/core/string_builder.cpp b/src/core/string_builder.cpp new file mode 100644 index 0000000000..3144656e4c --- /dev/null +++ b/src/core/string_builder.cpp @@ -0,0 +1,125 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file string_builder.cpp Implementation of string composing. */ + +#include "../stdafx.h" +#include "string_builder.hpp" +#include "utf8.hpp" +#include "../safeguards.h" + +/** + * Append binary uint8. + */ +void BaseStringBuilder::PutUint8(uint8_t value) +{ + std::array buf{ + static_cast(value) + }; + this->PutBuffer(buf); +} + +/** + * Append binary int8. + */ +void BaseStringBuilder::PutSint8(int8_t value) +{ + this->PutUint8(static_cast(value)); +} + +/** + * Append binary uint16 using little endian. + */ +void BaseStringBuilder::PutUint16LE(uint16_t value) +{ + std::array buf{ + static_cast(static_cast(value)), + static_cast(static_cast(value >> 8)) + }; + this->PutBuffer(buf); +} + +/** + * Append binary int16 using little endian. + */ +void BaseStringBuilder::PutSint16LE(int16_t value) +{ + this->PutUint16LE(static_cast(value)); +} + +/** + * Append binary uint32 using little endian. + */ +void BaseStringBuilder::PutUint32LE(uint32_t value) +{ + std::array buf{ + static_cast(static_cast(value)), + static_cast(static_cast(value >> 8)), + static_cast(static_cast(value >> 16)), + static_cast(static_cast(value >> 24)) + }; + this->PutBuffer(buf); +} + +/** + * Append binary int32 using little endian. + */ +void BaseStringBuilder::PutSint32LE(int32_t value) +{ + this->PutUint32LE(static_cast(value)); +} + +/** + * Append binary uint64 using little endian. + */ +void BaseStringBuilder::PutUint64LE(uint64_t value) +{ + std::array buf{ + static_cast(static_cast(value)), + static_cast(static_cast(value >> 8)), + static_cast(static_cast(value >> 16)), + static_cast(static_cast(value >> 24)), + static_cast(static_cast(value >> 32)), + static_cast(static_cast(value >> 40)), + static_cast(static_cast(value >> 48)), + static_cast(static_cast(value >> 56)) + }; + this->PutBuffer(buf); +} + +/** + * Append binary int64 using little endian. + */ +void BaseStringBuilder::PutSint64LE(int64_t value) +{ + this->PutUint64LE(static_cast(value)); +} + +/** + * Append 8-bit char. + */ +void BaseStringBuilder::PutChar(char c) +{ + this->PutUint8(static_cast(c)); +} + +/** + * Append UTF.8 char. + */ +void BaseStringBuilder::PutUtf8(char32_t c) +{ + auto [buf, len] = EncodeUtf8(c); + this->PutBuffer(buf, len); +} + +/** + * Append buffer. + */ +void StringBuilder::PutBuffer(const char *str, size_type len) +{ + this->dest->append(str, len); +} diff --git a/src/core/string_builder.hpp b/src/core/string_builder.hpp new file mode 100644 index 0000000000..77fba335fd --- /dev/null +++ b/src/core/string_builder.hpp @@ -0,0 +1,119 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** + * @file string_builder.hpp Compose strings from textual and binary data. + */ + +#ifndef STRING_BUILDER_HPP +#define STRING_BUILDER_HPP + +#include + +/** + * Compose data into a string / buffer. + */ +class BaseStringBuilder { +public: + using size_type = std::string_view::size_type; + + virtual ~BaseStringBuilder() = default; + + /** + * Append buffer. + */ + virtual void PutBuffer(const char *str, size_type len) = 0; + + /** + * Append span. + */ + void PutBuffer(std::span str) { this->PutBuffer(str.data(), str.size()); } + + /** + * Append string. + */ + void Put(std::string_view str) { this->PutBuffer(str.data(), str.size()); } + + void PutUint8(uint8_t value); + void PutSint8(int8_t value); + void PutUint16LE(uint16_t value); + void PutSint16LE(int16_t value); + void PutUint32LE(uint32_t value); + void PutSint32LE(int32_t value); + void PutUint64LE(uint64_t value); + void PutSint64LE(int64_t value); + + void PutChar(char c); + void PutUtf8(char32_t c); + + /** + * Append integer 'value' in given number 'base'. + */ + template + void PutIntegerBase(T value, int base) + { + std::array buf; + auto result = std::to_chars(buf.data(), buf.data() + buf.size(), value, base); + if (result.ec != std::errc{}) return; + size_type len = result.ptr - buf.data(); + this->PutBuffer(buf.data(), len); + } +}; + +/** + * Compose data into a growing std::string. + */ +class StringBuilder final : public BaseStringBuilder +{ + std::string *dest; +public: + /** + * Construct StringBuilder into destination string. + * @note The lifetime of the string must exceed the lifetime of the StringBuilder. + */ + StringBuilder(std::string &dest) : dest(&dest) {} + + /** + * Check whether any bytes have been written. + */ + [[nodiscard]] bool AnyBytesWritten() const noexcept { return !this->dest->empty(); } + /** + * Get number of already written bytes. + */ + [[nodiscard]] size_type GetBytesWritten() const noexcept { return this->dest->size(); } + /** + * Get already written data. + */ + [[nodiscard]] const std::string &GetWrittenData() const noexcept { return *dest; } + /** + * Get mutable already written data. + */ + [[nodiscard]] std::string &GetString() noexcept { return *dest; } + + using BaseStringBuilder::PutBuffer; + void PutBuffer(const char *str, size_type len) override; + + /** + * Append string. + */ + StringBuilder& operator+=(std::string_view str) + { + this->Put(str); + return *this; + } + + using back_insert_iterator = std::back_insert_iterator; + /** + * Create a back-insert-iterator. + */ + back_insert_iterator back_inserter() + { + return back_insert_iterator(*this->dest); + } +}; + +#endif /* STRING_BUILDER_HPP */ diff --git a/src/settingsgen/CMakeLists.txt b/src/settingsgen/CMakeLists.txt index f088a2a642..c69f755035 100644 --- a/src/settingsgen/CMakeLists.txt +++ b/src/settingsgen/CMakeLists.txt @@ -10,6 +10,7 @@ if (NOT HOST_BINARY_DIR) ../error.cpp ../ini_load.cpp ../string.cpp + ../core/string_builder.cpp ../core/utf8.cpp ) add_definitions(-DSETTINGSGEN) diff --git a/src/strgen/CMakeLists.txt b/src/strgen/CMakeLists.txt index 5d67a744e7..425cc1dd1a 100644 --- a/src/strgen/CMakeLists.txt +++ b/src/strgen/CMakeLists.txt @@ -12,6 +12,7 @@ if (NOT HOST_BINARY_DIR) ../misc/getoptdata.cpp ../error.cpp ../string.cpp + ../core/string_builder.cpp ../core/utf8.cpp ) add_definitions(-DSTRGEN) diff --git a/src/strings.cpp b/src/strings.cpp index 2ceaa0e1ec..9f550b374b 100644 --- a/src/strings.cpp +++ b/src/strings.cpp @@ -489,7 +489,7 @@ static void FormatNumber(StringBuilder &builder, int64_t number, const char *sep int thousands_offset = (max_digits - 1) % 3; if (number < 0) { - builder += '-'; + builder.PutChar('-'); number = -number; } @@ -502,7 +502,7 @@ static void FormatNumber(StringBuilder &builder, int64_t number, const char *sep num = num % divisor; } if ((tot |= quot) || i == max_digits - 1) { - builder += '0' + quot; // quot is a single digit + builder.PutChar('0' + quot); // quot is a single digit if ((i % 3) == thousands_offset && i < max_digits - 1) builder += separator; } @@ -519,17 +519,17 @@ static void FormatCommaNumber(StringBuilder &builder, int64_t number) static void FormatNoCommaNumber(StringBuilder &builder, int64_t number) { - fmt::format_to(builder, "{}", number); + fmt::format_to(builder.back_inserter(), "{}", number); } static void FormatZerofillNumber(StringBuilder &builder, int64_t number, int count) { - fmt::format_to(builder, "{:0{}d}", number, count); + fmt::format_to(builder.back_inserter(), "{:0{}d}", number, count); } static void FormatHexNumber(StringBuilder &builder, uint64_t number) { - fmt::format_to(builder, "0x{:X}", number); + fmt::format_to(builder.back_inserter(), "0x{:X}", number); } /** @@ -551,18 +551,18 @@ static void FormatBytes(StringBuilder &builder, int64_t number) if (number < 1024) { id = 0; - fmt::format_to(builder, "{}", number); + fmt::format_to(builder.back_inserter(), "{}", number); } else if (number < 1024 * 10) { - fmt::format_to(builder, "{}{}{:02}", number / 1024, GetDecimalSeparator(), (number % 1024) * 100 / 1024); + fmt::format_to(builder.back_inserter(), "{}{}{:02}", number / 1024, GetDecimalSeparator(), (number % 1024) * 100 / 1024); } else if (number < 1024 * 100) { - fmt::format_to(builder, "{}{}{:01}", number / 1024, GetDecimalSeparator(), (number % 1024) * 10 / 1024); + fmt::format_to(builder.back_inserter(), "{}{}{:01}", number / 1024, GetDecimalSeparator(), (number % 1024) * 10 / 1024); } else { assert(number < 1024 * 1024); - fmt::format_to(builder, "{}", number / 1024); + fmt::format_to(builder.back_inserter(), "{}", number / 1024); } assert(id < lengthof(iec_prefixes)); - fmt::format_to(builder, NBSP "{}B", iec_prefixes[id]); + fmt::format_to(builder.back_inserter(), NBSP "{}B", iec_prefixes[id]); } static void FormatYmdString(StringBuilder &builder, TimerGameCalendar::Date date, uint case_index) @@ -600,9 +600,9 @@ static void FormatGenericCurrency(StringBuilder &builder, const CurrencySpec *sp /* convert from negative */ if (number < 0) { - builder.Utf8Encode(SCC_PUSH_COLOUR); - builder.Utf8Encode(SCC_RED); - builder += '-'; + builder.PutUtf8(SCC_PUSH_COLOUR); + builder.PutUtf8(SCC_RED); + builder.PutChar('-'); number = -number; } @@ -646,7 +646,7 @@ static void FormatGenericCurrency(StringBuilder &builder, const CurrencySpec *sp if (spec->symbol_pos != 0) builder += spec->suffix; if (negative) { - builder.Utf8Encode(SCC_POP_COLOUR); + builder.PutUtf8(SCC_POP_COLOUR); } } @@ -1130,7 +1130,7 @@ static void FormatString(StringBuilder &builder, std::string_view str_arg, Strin } if (b < SCC_CONTROL_START || b > SCC_CONTROL_END) { - builder.Utf8Encode(b); + builder.PutUtf8(b); continue; } @@ -1195,8 +1195,8 @@ static void FormatString(StringBuilder &builder, std::string_view str_arg, Strin * We just ignore this one. It's used in {G 0 Der Die Das} to determine the case. */ case SCC_GENDER_INDEX: // {GENDER 0} if (_scan_for_gender_data) { - builder.Utf8Encode(SCC_GENDER_INDEX); - builder += *str++; + builder.PutUtf8(SCC_GENDER_INDEX); + builder.PutUint8(*str++); } else { str++; } @@ -1317,7 +1317,7 @@ static void FormatString(StringBuilder &builder, std::string_view str_arg, Strin int64_t fractional = number % divisor; number /= divisor; FormatCommaNumber(builder, number); - fmt::format_to(builder, "{}{:0{}d}", GetDecimalSeparator(), fractional, digits); + fmt::format_to(builder.back_inserter(), "{}{:0{}d}", GetDecimalSeparator(), fractional, digits); break; } @@ -1810,12 +1810,12 @@ static void FormatString(StringBuilder &builder, std::string_view str_arg, Strin case SCC_COLOUR: { // {COLOUR} StringControlCode scc = (StringControlCode)(SCC_BLUE + args.GetNextParameter()); - if (IsInsideMM(scc, SCC_BLUE, SCC_COLOUR)) builder.Utf8Encode(scc); + if (IsInsideMM(scc, SCC_BLUE, SCC_COLOUR)) builder.PutUtf8(scc); break; } default: - builder.Utf8Encode(b); + builder.PutUtf8(b); break; } } catch (std::out_of_range &e) { @@ -1828,11 +1828,11 @@ static void FormatString(StringBuilder &builder, std::string_view str_arg, Strin static void StationGetSpecialString(StringBuilder &builder, StationFacilities x) { - if (x.Test(StationFacility::Train)) builder.Utf8Encode(SCC_TRAIN); - if (x.Test(StationFacility::TruckStop)) builder.Utf8Encode(SCC_LORRY); - if (x.Test(StationFacility::BusStop)) builder.Utf8Encode(SCC_BUS); - if (x.Test(StationFacility::Dock)) builder.Utf8Encode(SCC_SHIP); - if (x.Test(StationFacility::Airport)) builder.Utf8Encode(SCC_PLANE); + if (x.Test(StationFacility::Train)) builder.PutUtf8(SCC_TRAIN); + if (x.Test(StationFacility::TruckStop)) builder.PutUtf8(SCC_LORRY); + if (x.Test(StationFacility::BusStop)) builder.PutUtf8(SCC_BUS); + if (x.Test(StationFacility::Dock)) builder.PutUtf8(SCC_SHIP); + if (x.Test(StationFacility::Airport)) builder.PutUtf8(SCC_PLANE); } static const char * const _silly_company_names[] = { @@ -1928,13 +1928,13 @@ static void GenAndCoName(StringBuilder &builder, uint32_t seed) static void GenPresidentName(StringBuilder &builder, uint32_t seed) { - builder += _initial_name_letters[std::size(_initial_name_letters) * GB(seed, 0, 8) >> 8]; + builder.PutChar(_initial_name_letters[std::size(_initial_name_letters) * GB(seed, 0, 8) >> 8]); builder += ". "; /* The second initial is optional. */ size_t index = (std::size(_initial_name_letters) + 35) * GB(seed, 8, 8) >> 8; if (index < std::size(_initial_name_letters)) { - builder += _initial_name_letters[index]; + builder.PutChar(_initial_name_letters[index]); builder += ". "; } diff --git a/src/strings_internal.h b/src/strings_internal.h index 3a17626a4b..47e4329bad 100644 --- a/src/strings_internal.h +++ b/src/strings_internal.h @@ -12,6 +12,7 @@ #include "strings_func.h" #include "string_func.h" +#include "core/string_builder.hpp" class StringParameters { protected: @@ -204,79 +205,6 @@ public: } }; -/** - * Equivalent to the std::back_insert_iterator in function, with some - * convenience helpers for string concatenation. - */ -class StringBuilder { - std::string *string; - -public: - /* Required type for this to be an output_iterator; mimics std::back_insert_iterator. */ - using value_type = void; - using difference_type = void; - using iterator_category = std::output_iterator_tag; - using pointer = void; - using reference = void; - - /** - * Create the builder of an external buffer. - * @param string The string to write to. - */ - StringBuilder(std::string &string) : string(&string) {} - - /* Required operators for this to be an output_iterator; mimics std::back_insert_iterator, which has no-ops. */ - StringBuilder &operator++() { return *this; } - StringBuilder operator++(int) { return *this; } - StringBuilder &operator*() { return *this; } - - /** - * Operator to add a character to the end of the buffer. Like the back - * insert iterators this also increases the position of the end of the - * buffer. - * @param value The character to add. - * @return Reference to this inserter. - */ - StringBuilder &operator=(const char value) - { - return this->operator+=(value); - } - - /** - * Operator to add a character to the end of the buffer. - * @param value The character to add. - * @return Reference to this inserter. - */ - StringBuilder &operator+=(const char value) - { - this->string->push_back(value); - return *this; - } - - /** - * Operator to append the given string to the output buffer. - * @param str The string to add. - * @return Reference to this inserter. - */ - StringBuilder &operator+=(std::string_view str) - { - *this->string += str; - return *this; - } - - /** - * Encode the given Utf8 character into the output buffer. - * @param c The character to encode. - */ - void Utf8Encode(char32_t c) - { - auto iterator = std::back_inserter(*this->string); - ::Utf8Encode(iterator, c); - } - - std::string &GetString() { return *this->string; } -}; - void GetStringWithArgs(StringBuilder &builder, StringID string, StringParameters &args, uint case_index = 0, bool game_script = false); void GetStringWithArgs(StringBuilder &builder, StringID string, std::span params, uint case_index = 0, bool game_script = false); std::string GetStringWithArgs(StringID string, StringParameters &args); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index bd1876297e..7bfc261e4c 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -7,6 +7,7 @@ add_test_files( mock_fontcache.h mock_spritecache.cpp mock_spritecache.h + string_builder.cpp string_func.cpp test_main.cpp test_network_crypto.cpp diff --git a/src/tests/string_builder.cpp b/src/tests/string_builder.cpp new file mode 100644 index 0000000000..b4dcf6bf6c --- /dev/null +++ b/src/tests/string_builder.cpp @@ -0,0 +1,78 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file string_builder.cpp Test functionality from core/string_builder. */ + +#include "../stdafx.h" +#include "../3rdparty/catch2/catch.hpp" +#include "../core/string_builder.hpp" +#include "../safeguards.h" + +using namespace std::literals; + +TEST_CASE("StringBuilder - basic") +{ + std::string buffer; + StringBuilder builder(buffer); + + CHECK(!builder.AnyBytesWritten()); + CHECK(builder.GetBytesWritten() == 0); + CHECK(builder.GetWrittenData() == ""sv); + + builder.Put("ab"); + builder += "cdef"; + + CHECK(builder.AnyBytesWritten()); + CHECK(builder.GetBytesWritten() == 6); + CHECK(builder.GetWrittenData() == "abcdef"sv); + + CHECK(buffer == "abcdef"sv); +} + +TEST_CASE("StringBuilder - binary") +{ + std::string buffer; + StringBuilder builder(buffer); + + builder.PutUint8(1); + builder.PutSint8(-1); + builder.PutUint16LE(0x201); + builder.PutSint16LE(-0x201); + builder.PutUint32LE(0x30201); + builder.PutSint32LE(-0x30201); + builder.PutUint64LE(0x7060504030201); + builder.PutSint64LE(-0x7060504030201); + + CHECK(buffer == "\x01\xFF\x01\x02\xFF\xFD\x01\x02\x03\x00\xFF\xFD\xFC\xFF\x01\x02\x03\04\x05\x06\x07\x00\xFF\xFD\xFC\xFB\xFA\xF9\xF8\xFF"sv); +} + +TEST_CASE("StringBuilder - text") +{ + std::string buffer; + StringBuilder builder(buffer); + + builder.PutChar('a'); + builder.PutUtf8(0x1234); + builder.PutChar(' '); + builder.PutIntegerBase(1234, 10); + builder.PutChar(' '); + builder.PutIntegerBase(0x7FFF, 16); + builder.PutChar(' '); + builder.PutIntegerBase(-1234, 10); + builder.PutChar(' '); + builder.PutIntegerBase(-0x7FFF, 16); + builder.PutChar(' '); + builder.PutIntegerBase(1'234'567'890'123, 10); + builder.PutChar(' '); + builder.PutIntegerBase(0x1234567890, 16); + builder.PutChar(' '); + builder.PutIntegerBase(-1'234'567'890'123, 10); + builder.PutChar(' '); + builder.PutIntegerBase(-0x1234567890, 16); + + CHECK(buffer == "a\u1234 1234 7fff -1234 -7fff 1234567890123 1234567890 -1234567890123 -1234567890"sv); +} diff --git a/src/townname.cpp b/src/townname.cpp index cbdc31a49c..b19bfef79e 100644 --- a/src/townname.cpp +++ b/src/townname.cpp @@ -250,7 +250,7 @@ static void ReplaceEnglishWords(std::string &str, size_t start, bool original) */ static void MakeEnglishOriginalTownName(StringBuilder &builder, uint32_t seed) { - size_t start = builder.GetString().size(); + size_t start = builder.GetBytesWritten(); /* optional first segment */ int i = SeedChanceBias(0, std::size(_name_original_english_1), seed, 50); @@ -696,7 +696,7 @@ static void MakeCzechTownName(StringBuilder &builder, uint32_t seed) builder += _name_czech_adj[prefix].name; builder += _name_czech_patmod[gender][pattern]; - builder += ' '; + builder.PutChar(' '); } if (dynamic_subst) {