[xswiftbus] Fix UTF-8 bug when displaying text messages

std::string is not UTF-8 aware, so it was possible to create
malformed strings by splitting in the middle of a code point.

The splitting code also did read beyond the end of the input string.

Now we use a Unicode-aware iterator adaptor to help find the
correct place to split the string across multiple lines.
This commit is contained in:
Mat Sutcliffe
2020-11-24 22:15:33 +00:00
parent 550f8fd7e6
commit e28df21641
2 changed files with 66 additions and 10 deletions

View File

@@ -17,6 +17,7 @@
#include <cctype> #include <cctype>
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <iterator>
// Strict header only X-Plane model parser utils shared between BlackMisc and XSwiftBus. // Strict header only X-Plane model parser utils shared between BlackMisc and XSwiftBus.
// Header only is necessary to no require XSwiftBus to link against BlackMisc. // Header only is necessary to no require XSwiftBus to link against BlackMisc.
@@ -249,6 +250,59 @@ namespace BlackMisc
acfProperties.modelString = simplifyWhitespace(stringForFlyableModel(acfProperties, filePath)); acfProperties.modelString = simplifyWhitespace(stringForFlyableModel(acfProperties, filePath));
return acfProperties; return acfProperties;
} }
//! Encoding-aware iterator adaptor for std::u8string
template <typename I>
struct Utf8Iterator
{
//! STL compatibility
//! @{
using value_type = typename std::iterator_traits<I>::value_type;
using difference_type = typename std::iterator_traits<I>::difference_type;
using reference = typename std::iterator_traits<I>::reference;
using pointer = typename std::iterator_traits<I>::pointer;
using iterator_category = std::forward_iterator_tag;
//! @}
//! Default constructor
Utf8Iterator() = default;
//! Constructor
Utf8Iterator(I base, I end) : base(base), end(end) {}
//! Equality
//! @{
friend bool operator ==(Utf8Iterator a, Utf8Iterator b) { return a.base == b.base; }
friend bool operator !=(Utf8Iterator a, Utf8Iterator b) { return a.base != b.base; }
friend bool operator ==(Utf8Iterator a, I b) { return a.base == b; }
friend bool operator !=(Utf8Iterator a, I b) { return a.base != b; }
friend bool operator ==(I a, Utf8Iterator b) { return a == b.base; }
friend bool operator !=(I a, Utf8Iterator b) { return a != b.base; }
//! @}
//! Dereference (not encoding-aware)
reference operator *() const { return *base; }
//! Pointer indirection (not encoding-aware)
pointer operator ->() const { return base.operator ->(); }
//! Pre-increment
Utf8Iterator &operator ++()
{
constexpr auto isContinuation = [](auto c)
{
return (c & static_cast<value_type>(0b11000000)) == static_cast<value_type>(0b10000000);
};
do { ++base; } while (base != end && isContinuation(*base));
return *this;
}
//! Post-increment
Utf8Iterator operator ++(int) { auto copy = *this; ++*this; return copy; }
I base; //!< Underlying iterator
I end; //!< Underlying end iterator
};
} }
} // namespace } // namespace
} // namespace } // namespace

View File

@@ -146,21 +146,23 @@ namespace XSwiftBus
{ {
if (text.empty()) { return; } if (text.empty()) { return; }
static const CMessage::string ellipsis = u8"\u2026"; static const CMessage::string ellipsis = u8"\u2026";
const int lineLength = m_messages.maxLineLength() - static_cast<int>(ellipsis.size()); const unsigned lineLength = m_messages.maxLineLength() - 1;
using U8It = Utf8Iterator<typename CMessage::string::const_iterator>;
U8It begin(text.begin(), text.end());
auto characters = std::distance(begin, U8It(text.end(), text.end()));
std::vector<CMessage::string> wrappedLines; std::vector<CMessage::string> wrappedLines;
for (size_t i = 0; i < text.size(); i += static_cast<size_t>(lineLength))
for (; characters > lineLength; characters -= lineLength)
{ {
wrappedLines.emplace_back(text.begin() + i, text.begin() + i + static_cast<size_t>(lineLength)); auto end = std::next(begin, lineLength);
wrappedLines.emplace_back(begin.base, end.base);
wrappedLines.back() += ellipsis; wrappedLines.back() += ellipsis;
begin = end;
} }
wrappedLines.back().erase(wrappedLines.back().size() - ellipsis.size()); if (characters > 0)
if (wrappedLines.back().empty()) { wrappedLines.pop_back(); }
else if (wrappedLines.back().size() == ellipsis.size() && wrappedLines.size() > 1)
{ {
auto secondLastLine = wrappedLines.end() - 2; wrappedLines.emplace_back(begin.base, text.end());
secondLastLine->erase(wrappedLines.back().size() - ellipsis.size());
secondLastLine->append(wrappedLines.back());
wrappedLines.pop_back();
} }
for (const auto &line : wrappedLines) for (const auto &line : wrappedLines)
{ {