From dc21fae18e06b5f8f70553bdc942f30194152fe9 Mon Sep 17 00:00:00 2001 From: frosch Date: Thu, 3 Apr 2025 20:50:51 +0200 Subject: [PATCH] Codechange: Add InPlaceReplacement to couple StringConsumer and Builder on the same buffer. --- src/core/CMakeLists.txt | 2 + src/core/string_inplace.cpp | 63 ++++++++++++++++++++ src/core/string_inplace.hpp | 105 +++++++++++++++++++++++++++++++++ src/settingsgen/CMakeLists.txt | 1 + src/strgen/CMakeLists.txt | 1 + src/tests/CMakeLists.txt | 1 + src/tests/string_inplace.cpp | 57 ++++++++++++++++++ 7 files changed, 230 insertions(+) create mode 100644 src/core/string_inplace.cpp create mode 100644 src/core/string_inplace.hpp create mode 100644 src/tests/string_inplace.cpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d21ea8da92..448090585d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -27,6 +27,8 @@ add_files( string_builder.hpp string_consumer.cpp string_consumer.hpp + string_inplace.cpp + string_inplace.hpp strong_typedef_type.hpp utf8.cpp utf8.hpp diff --git a/src/core/string_inplace.cpp b/src/core/string_inplace.cpp new file mode 100644 index 0000000000..248fe8d130 --- /dev/null +++ b/src/core/string_inplace.cpp @@ -0,0 +1,63 @@ +/* + * 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_inplace.cpp Inplace-replacement of textual and binary data. + */ + +#include "../stdafx.h" +#include "string_inplace.hpp" +#include "../safeguards.h" + +/** + * Check whether any unused bytes are left between the Builder and Consumer position. + */ +[[nodiscard]] bool InPlaceBuilder::AnyBytesUnused() const noexcept +{ + return this->consumer.GetBytesRead() > this->position; +} + +/** + * Get number of unused bytes left between the Builder and Consumer position. + */ +[[nodiscard]] InPlaceBuilder::size_type InPlaceBuilder::GetBytesUnused() const noexcept +{ + return this->consumer.GetBytesRead() - this->position; +} + +/** + * Append buffer. + */ +void InPlaceBuilder::PutBuffer(const char *str, size_type len) +{ + auto unused = this->GetBytesUnused(); + if (len > unused) NOT_REACHED(); + std::copy(str, str + len, this->dest.data() + this->position); + this->position += len; +} + +/** + * Create coupled Consumer+Builder pair. + * @param buffer Data to consume and replace. + * @note The lifetime of the buffer must exceed the lifetime of both the Consumer and the Builder. + */ +InPlaceReplacement::InPlaceReplacement(std::span buffer) + : consumer(buffer), builder(buffer, consumer) +{ +} + +InPlaceReplacement::InPlaceReplacement(const InPlaceReplacement &src) + : consumer(src.consumer), builder(src.builder, consumer) +{ +} + +InPlaceReplacement& InPlaceReplacement::operator=(const InPlaceReplacement &src) +{ + this->consumer = src.consumer; + this->builder.AssignBuffer(src.builder); + return *this; +} diff --git a/src/core/string_inplace.hpp b/src/core/string_inplace.hpp new file mode 100644 index 0000000000..855ae55896 --- /dev/null +++ b/src/core/string_inplace.hpp @@ -0,0 +1,105 @@ +/* + * 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_inplace.hpp Inplace-replacement of textual and binary data. + */ + +#ifndef STRING_INPLACE_HPP +#define STRING_INPLACE_HPP + +#include "string_builder.hpp" +#include "string_consumer.hpp" + +/** + * Builder implementation for InPlaceReplacement. + */ +class InPlaceBuilder final : public BaseStringBuilder +{ + std::span dest; + size_type position = 0; + const StringConsumer &consumer; + + friend class InPlaceReplacement; + explicit InPlaceBuilder(std::span dest, const StringConsumer &consumer) : dest(dest), consumer(consumer) {} + InPlaceBuilder(const InPlaceBuilder &src, const StringConsumer &consumer) : dest(src.dest), position(src.position), consumer(consumer) {} + void AssignBuffer(const InPlaceBuilder &src) { this->dest = src.dest; this->position = src.position; } +public: + InPlaceBuilder(const InPlaceBuilder &) = delete; + InPlaceBuilder& operator=(const InPlaceBuilder &) = delete; + + /** + * Check whether any bytes have been written. + */ + [[nodiscard]] bool AnyBytesWritten() const noexcept { return this->position != 0; } + /** + * Get number of already written bytes. + */ + [[nodiscard]] size_type GetBytesWritten() const noexcept { return this->position; } + /** + * Get already written data. + */ + [[nodiscard]] std::string_view GetWrittenData() const noexcept { return {this->dest.data(), this->position}; } + + [[nodiscard]] bool AnyBytesUnused() const noexcept; + [[nodiscard]] size_type GetBytesUnused() const noexcept; + + using BaseStringBuilder::PutBuffer; + void PutBuffer(const char *str, size_type len) override; + + /** + * Implementation of std::back_insert_iterator for non-growing destination buffer. + */ + class back_insert_iterator { + InPlaceBuilder *parent = nullptr; + public: + using value_type = void; + using difference_type = void; + using iterator_category = std::output_iterator_tag; + using pointer = void; + using reference = void; + + back_insert_iterator(InPlaceBuilder &parent) : parent(&parent) {} + + back_insert_iterator &operator++() { return *this; } + back_insert_iterator operator++(int) { return *this; } + back_insert_iterator &operator*() { return *this; } + + back_insert_iterator &operator=(char value) + { + this->parent->PutChar(value); + return *this; + } + }; + /** + * Create a back-insert-iterator. + */ + back_insert_iterator back_inserter() + { + return back_insert_iterator(*this); + } +}; + +/** + * Compose data into a fixed size buffer, which is consumed at the same time. + * - The Consumer reads data from a buffer. + * - The Builder writes data to the buffer, replacing already consumed data. + * - The Builder asserts, if it overtakes the consumer. + */ +class InPlaceReplacement +{ +public: + StringConsumer consumer; ///< Consumer from shared buffer + InPlaceBuilder builder; ///< Builder into shared buffer + +public: + InPlaceReplacement(std::span buffer); + InPlaceReplacement(const InPlaceReplacement &src); + InPlaceReplacement& operator=(const InPlaceReplacement &src); +}; + +#endif /* STRING_INPLACE_HPP */ diff --git a/src/settingsgen/CMakeLists.txt b/src/settingsgen/CMakeLists.txt index 220ffb972b..7f7c1854b1 100644 --- a/src/settingsgen/CMakeLists.txt +++ b/src/settingsgen/CMakeLists.txt @@ -12,6 +12,7 @@ if (NOT HOST_BINARY_DIR) ../string.cpp ../core/string_builder.cpp ../core/string_consumer.cpp + ../core/string_inplace.cpp ../core/utf8.cpp ) add_definitions(-DSETTINGSGEN) diff --git a/src/strgen/CMakeLists.txt b/src/strgen/CMakeLists.txt index 40eb11448f..64d1905089 100644 --- a/src/strgen/CMakeLists.txt +++ b/src/strgen/CMakeLists.txt @@ -14,6 +14,7 @@ if (NOT HOST_BINARY_DIR) ../string.cpp ../core/string_builder.cpp ../core/string_consumer.cpp + ../core/string_inplace.cpp ../core/utf8.cpp ) add_definitions(-DSTRGEN) diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 29f97ad205..94579147fa 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -9,6 +9,7 @@ add_test_files( mock_spritecache.h string_builder.cpp string_consumer.cpp + string_inplace.cpp string_func.cpp test_main.cpp test_network_crypto.cpp diff --git a/src/tests/string_inplace.cpp b/src/tests/string_inplace.cpp new file mode 100644 index 0000000000..275c2c1fd3 --- /dev/null +++ b/src/tests/string_inplace.cpp @@ -0,0 +1,57 @@ +/* + * 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_inplace.cpp Test functionality from core/string_inplace. */ + +#include "../stdafx.h" +#include "../3rdparty/catch2/catch.hpp" +#include "../core/string_inplace.hpp" +#include "../safeguards.h" + +using namespace std::literals; + +TEST_CASE("InPlaceReplacement") +{ + std::array buffer{1, 2, 3, 4}; + InPlaceReplacement inplace(buffer); + + CHECK(!inplace.builder.AnyBytesWritten()); + CHECK(inplace.builder.GetBytesWritten() == 0); + CHECK(inplace.builder.GetWrittenData() == ""sv); + CHECK(!inplace.builder.AnyBytesUnused()); + CHECK(inplace.builder.GetBytesUnused() == 0); + CHECK(!inplace.consumer.AnyBytesRead()); + CHECK(inplace.consumer.GetBytesRead() == 0); + CHECK(inplace.consumer.AnyBytesLeft()); + CHECK(inplace.consumer.GetBytesLeft() == 4); + + CHECK(inplace.consumer.ReadUint16LE() == 0x201); + + CHECK(inplace.builder.GetBytesWritten() == 0); + CHECK(inplace.builder.GetBytesUnused() == 2); + CHECK(inplace.consumer.GetBytesRead() == 2); + CHECK(inplace.consumer.GetBytesLeft() == 2); + + inplace.builder.PutUint8(11); + + CHECK(inplace.builder.GetBytesWritten() == 1); + CHECK(inplace.builder.GetBytesUnused() == 1); + CHECK(inplace.consumer.GetBytesRead() == 2); + CHECK(inplace.consumer.GetBytesLeft() == 2); + + inplace.builder.PutUint8(12); + + CHECK(inplace.builder.GetBytesWritten() == 2); + CHECK(inplace.builder.GetBytesUnused() == 0); + CHECK(inplace.consumer.GetBytesRead() == 2); + CHECK(inplace.consumer.GetBytesLeft() == 2); + + CHECK(buffer[0] == 11); + CHECK(buffer[1] == 12); + CHECK(buffer[2] == 3); + CHECK(buffer[3] == 4); +}