From fd65f71fbbd10da3d1818d492ad65471f46870c2 Mon Sep 17 00:00:00 2001 From: Narr the Reg <5944268+german77@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:32:55 -0600 Subject: [PATCH] stream: Implement TextStreamFormat --- include/stream/seadStreamFormat.h | 7 + modules/src/stream/seadStreamFormat.cpp | 451 ++++++++++++++++++++++++ 2 files changed, 458 insertions(+) diff --git a/include/stream/seadStreamFormat.h b/include/stream/seadStreamFormat.h index b360cefd..50deb190 100644 --- a/include/stream/seadStreamFormat.h +++ b/include/stream/seadStreamFormat.h @@ -79,6 +79,8 @@ class BinaryStreamFormat : public StreamFormat class TextStreamFormat : public StreamFormat { public: + TextStreamFormat(); + u8 readU8(StreamSrc* src, Endian::Types endian) override; u16 readU16(StreamSrc* src, Endian::Types endian) override; u32 readU32(StreamSrc* src, Endian::Types endian) override; @@ -108,6 +110,11 @@ class TextStreamFormat : public StreamFormat void skip(StreamSrc* src, u32 offset) override; void flush(StreamSrc* src) override; void rewind(StreamSrc* src) override; + +private: + void getNextData_(sead::StreamSrc* src); + + FixedSafeString<128> mEntryTerminator; }; } // namespace sead diff --git a/modules/src/stream/seadStreamFormat.cpp b/modules/src/stream/seadStreamFormat.cpp index 962156ab..3260b81b 100644 --- a/modules/src/stream/seadStreamFormat.cpp +++ b/modules/src/stream/seadStreamFormat.cpp @@ -1,10 +1,19 @@ #include "stream/seadStreamFormat.h" +#include + +#include "codec/seadBase64.h" #include "math/seadMathCalcCommon.h" +#include "prim/seadScopedLock.h" +#include "prim/seadStringUtil.h" #include "stream/seadStreamSrc.h" +#include "thread/seadMutex.h" namespace sead { +static FixedSafeString<128> sTextData; +static Mutex sMutex; + u8 BinaryStreamFormat::readU8(StreamSrc* src, Endian::Types endian) { u8 rawValue = 0; @@ -220,4 +229,446 @@ void BinaryStreamFormat::rewind(StreamSrc* src) { src->rewind(); } + +TextStreamFormat::TextStreamFormat() : mEntryTerminator(" \t\r\n") {} + +u8 TextStreamFormat::readU8(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + u8 value = 0; + getNextData_(src); + StringUtil::tryParseU8(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +u16 TextStreamFormat::readU16(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + u16 value = 0; + getNextData_(src); + StringUtil::tryParseU16(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +u32 TextStreamFormat::readU32(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + u32 value = 0; + getNextData_(src); + StringUtil::tryParseU32(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +u64 TextStreamFormat::readU64(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + u64 value = 0; + getNextData_(src); + StringUtil::tryParseU64(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +s8 TextStreamFormat::readS8(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + s8 value = 0; + getNextData_(src); + StringUtil::tryParseS8(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +s16 TextStreamFormat::readS16(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + s16 value = 0; + getNextData_(src); + StringUtil::tryParseS16(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +s32 TextStreamFormat::readS32(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + s32 value = 0; + getNextData_(src); + StringUtil::tryParseS32(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +s64 TextStreamFormat::readS64(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + s64 value = 0; + getNextData_(src); + StringUtil::tryParseS64(&value, sTextData.cstr(), StringUtil::CardinalNumber::BaseAuto); + return value; +} + +f32 TextStreamFormat::readF32(StreamSrc* src, [[maybe_unused]] Endian::Types endian) +{ + ScopedLock lock(&sMutex); + f32 value = 0.0f; + getNextData_(src); + + if (sTextData.calcLength() != 0) + sscanf(sTextData.cstr(), "%f", &value); + + return value; +} + +// NON_MATCHING: https://decomp.me/scratch/PKNGF +void TextStreamFormat::readBit(StreamSrc* src, void* data, u32 bits) +{ + ScopedLock lock(&sMutex); + u8* dest = static_cast(data); + getNextData_(src); + + SafeString bitStr = sTextData; + if (!bitStr.comparen("0b", 2)) + bitStr = bitStr.getPart(2); + + u32 bitCount = 0; + u8 currentByte = 0; + u32 length = bitStr.calcLength(); + for (u32 i = 0; bitCount < bits && i < length + 1; i++) + { + currentByte <<= 1; + if (bitStr.at(i) == '1') + currentByte |= 1; + + if ((++bitCount & 7) == 0) + { + dest[bitCount / 8 - 1] = currentByte; + currentByte = 0; + } + } + + u32 remainder = bitCount & 7; + if (remainder != 0) + { + u8 mask = 0xff << remainder; + dest[bitCount / 8] &= mask; + dest[bitCount / 8] |= currentByte; + } +} + +void TextStreamFormat::readString(StreamSrc* src, BufferedSafeString* str, + [[maybe_unused]] u32 size) +{ + ScopedLock lock(&sMutex); + getNextData_(src); + str->copy(sTextData); +} + +u32 TextStreamFormat::readMemBlock(StreamSrc* src, void* buffer, u32 size) +{ + ScopedLock lock(&sMutex); + getNextData_(src); + u32 textSize = sTextData.calcLength(); + + u64 readSize = 0; + u64 tmpReadSize = 0; + Base64::decode(buffer, size, sTextData.cstr(), textSize, &tmpReadSize); + readSize = tmpReadSize; + + return readSize; +} + +void TextStreamFormat::writeU8(StreamSrc* src, [[maybe_unused]] Endian::Types endian, u8 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%u", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeU16(StreamSrc* src, [[maybe_unused]] Endian::Types endian, u16 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%u", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeU32(StreamSrc* src, [[maybe_unused]] Endian::Types endian, u32 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%u", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeU64(StreamSrc* src, [[maybe_unused]] Endian::Types endian, u64 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%llu", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeS8(StreamSrc* src, [[maybe_unused]] Endian::Types endian, s8 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%d", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeS16(StreamSrc* src, [[maybe_unused]] Endian::Types endian, s16 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%d", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeS32(StreamSrc* src, [[maybe_unused]] Endian::Types endian, s32 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%d", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeS64(StreamSrc* src, [[maybe_unused]] Endian::Types endian, s64 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%lld", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeF32(StreamSrc* src, [[maybe_unused]] Endian::Types endian, f32 value) +{ + sead::FixedSafeString<32> tmp; + tmp.format("%.8f", value); + u32 length = tmp.calcLength(); + + src->write(tmp.cstr(), length); + src->write(mEntryTerminator.cstr(), 1); +} + +void TextStreamFormat::writeBit(StreamSrc* src, const void* data, u32 bits) +{ + ScopedLock lock(&sMutex); + const u8* dataU8 = static_cast(data); + sTextData = "0b"; + + for (u32 i = 0; i < (bits + 7) / 8; i++) + { + u32 bitsInByte = bits - i * 8; + if (bitsInByte > 8) + bitsInByte = 8; + + for (s32 j = bitsInByte - 1; j >= 0; j--) + { + if (dataU8[i] & (1 << j)) + sTextData.append('1'); + else + sTextData.append('0'); + } + } + + src->write(sTextData.cstr(), bits + 2); // 0b + bits + src->write(mEntryTerminator.cstr(), 1); +} + +// NOTE: Quotes are sanitized with \" +void TextStreamFormat::writeString(StreamSrc* src, const SafeString& str, u32 size) +{ + u32 length = str.calcLength(); + if (size <= length) + length = size; + + char quotes = '"'; + char backslash = '\\'; + src->write("es, 1); + for (u32 i = 0; i < length; i++) + { + if (str[i] == '"') + { + src->write(&backslash, 1); + } + else + { + if (i != 0) + str.cstr(); + if (str[i] == '"') + src->write(&backslash, 1); + } + + src->write(&str.cstr()[i], 1); + } + src->write("es, 1); +} + +inline s32 toBase64Size(u32 size) +{ + s32 bytes = size / 3; + if (size % 3 != 0) + bytes++; + return bytes * 4; +} + +// NON_MATCHING: Bad toBase64Size calculation https://decomp.me/scratch/Xzhbw +void TextStreamFormat::writeMemBlock(StreamSrc* src, const void* buffer, u32 size) +{ + ScopedLock lock(&sMutex); + sTextData.clear(); + + if (toBase64Size(size) + 1 < sTextData.getBufferSize()) + { + char* textBuffer = sTextData.getBuffer(); + + textBuffer[toBase64Size(size)] = SafeString::cNullChar; + Base64::encode(textBuffer, buffer, size, false); + u32 length = sTextData.calcLength(); + + src->write("\"", 1); + src->write(sTextData.cstr(), length); + src->write("\"", 1); + src->write(mEntryTerminator.cstr(), 1); + } +} + +void TextStreamFormat::writeDecorationText(StreamSrc* src, const SafeString& text) +{ + u32 length = text.calcLength(); + src->write(text.cstr(), length); +} + +void TextStreamFormat::writeNullChar(StreamSrc* src) +{ + char nullchar = '\0'; + src->write(&nullchar, 1); +} + +// NOTE: It will skip the next field regardless of the specified offset +void TextStreamFormat::skip(StreamSrc* src, [[maybe_unused]] u32 offset) +{ + ScopedLock lock(&sMutex); + getNextData_(src); +} + +void TextStreamFormat::flush([[maybe_unused]] StreamSrc* src) {} + +void TextStreamFormat::rewind(StreamSrc* src) +{ + src->rewind(); +} + +// NON_MATCHING: Bad implementation https://decomp.me/scratch/6wcQg +void TextStreamFormat::getNextData_(sead::StreamSrc* src) +{ + sTextData.clear(); + + s32 txtLength = 0; + char commentEndChar = '\0'; + bool inQuotes = false; + + char value; + while (src->read(&value, 1) != 0) + { + // Skip comments + if (commentEndChar != '\0') + { + if (value != commentEndChar) + { + if (commentEndChar == '/') + commentEndChar = '*'; + continue; + } + + if (commentEndChar == '*') + { + commentEndChar = '/'; + continue; + } + + commentEndChar = '\0'; + value = mEntryTerminator[0]; + } + + if (inQuotes) + { + if (value == '"') + { + if (txtLength >= 2 && sTextData[txtLength - 1] == '\\') + { + // Since Quotes are sanitized we need to override the backslash with quotes + sTextData.copyAt(-2, "\"", 1); + continue; + } + return; + } + + sTextData.append(value); + txtLength++; + continue; + } + + if (txtLength == 0 && value == '"') + { + inQuotes = true; + continue; + } + + if (mEntryTerminator.include(value) || value == '\0') + { + if (!sTextData.isEmpty()) + { + return; + } + continue; + } + + sTextData.append(value); + txtLength++; + + // Skip hash comments until new line + if (txtLength >= 1 && sTextData[txtLength - 1] == '#') + { + commentEndChar = '\n'; + txtLength--; + sTextData.trim(txtLength); + } + + if (txtLength >= 2) + { + // Skip comments until new line + if (sTextData[txtLength - 2] == '/' && sTextData[txtLength - 1] == '/') + { + commentEndChar = '\n'; + txtLength -= 2; + sTextData.trim(txtLength); + continue; + } + + // Skip multiline comments until end of comment + if (sTextData[txtLength - 2] == '/' && sTextData[txtLength - 1] == '*') + { + commentEndChar = '*'; + txtLength -= 2; + sTextData.trim(txtLength); + continue; + } + } + } +} } // namespace sead