From ee4f669a2d4d56629c8c3d6d2c344ee8def4d915 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 28 Jul 2021 19:04:45 +0700 Subject: [PATCH] Add `AvariceStreamReader` --- sources/Avarice/AvariceStreamReader.uc | 149 ++++++++++++++++++ .../Avarice/Tests/TEST_AvariceStreamReader.uc | Bin 0 -> 6148 bytes sources/Manifest.uc | 1 + 3 files changed, 150 insertions(+) create mode 100644 sources/Avarice/AvariceStreamReader.uc create mode 100644 sources/Avarice/Tests/TEST_AvariceStreamReader.uc diff --git a/sources/Avarice/AvariceStreamReader.uc b/sources/Avarice/AvariceStreamReader.uc new file mode 100644 index 0000000..5ec474e --- /dev/null +++ b/sources/Avarice/AvariceStreamReader.uc @@ -0,0 +1,149 @@ +/** + * Helper class meant for reading byte stream sent to us by the Avarice + * application. + * Avarice sends us utf8-encoded JSONs one-by-one, prepending each of them + * with 4 bytes (big endian) that encode the length of the following message. + * Copyright 2021 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia 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 3 of the License, or + * (at your option) any later version. + * + * Acedia 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 Acedia. If not, see . + */ +class AvariceStreamReader extends AcediaObject; + +// Are we currently reading length of the message (`true`) or +// the message itself (`false`)? +var private bool readingLength; +// How many byte we have read so far. +// Resets to zero when we finish reading either length of the message or +// the message itself. +var private int readBytes; +// Expected length of the next message +var private int nextMessageLength; +// Message read so far +var private ByteArrayRef nextMessage; +// All the messages we have fully read, but did not yet return +var private array outputQueue; +// For converting read messages into `MutableText` +var private Utf8Decoder decoder; +// Set to `true` if Avarice input was somehow unacceptable. +// Cannot be recovered from. +var private bool hasFailed; + +// Maximum allowed size of JSON message sent from avarice; +// Anything more than that is treated as a mistake. +// TODO: make this configurable +var private const int MAX_MESSAGE_LENGTH; + +protected function Constructor() +{ + readingLength = true; + nextMessage = ByteArrayRef(_.memory.Allocate(class'ByteArrayRef')); + decoder = Utf8Decoder(_.memory.Allocate(class'Utf8Decoder')); +} + +protected function Finalizer() +{ + _.memory.FreeMany(outputQueue); + _.memory.Free(nextMessage); + _.memory.Free(decoder); + outputQueue.length = 0; + nextMessage = none; + decoder = none; + hasFailed = false; +} + +/** + * Adds next `byte` from the input Avarice stream to the reader. + * + * If input stream signals that message we have to read is too long + * (longer then `MAX_MESSAGE_LENGTH`) - enters a failed state and will + * no longer accept any input. Failed status can be checked with + * `Failed()` method. + * Otherwise cannot fail. + * + * @param nextByte Next byte from the Avarice input stream. + * @return `false` if caller `AvariceStreamReader` is in a failed state + * (including if it entered one after pushing this byte) + * and `true` otherwise. + */ +public final function bool PushByte(byte nextByte) +{ + if (hasFailed) { + return false; + } + if (readingLength) + { + // Make space for the next 8 bits by shifting previously recorded ones + nextMessageLength = nextMessageLength << 8; + nextMessageLength += nextByte; + readBytes += 1; + if (readBytes >= 4) + { + readingLength = false; + readBytes = 0; + } + // Message either too long or so long it overfilled `MaxInt` + if ( nextMessageLength > MAX_MESSAGE_LENGTH + || nextMessageLength < 0) + { + hasFailed = true; + return false; + } + return true; + } + nextMessage.AddItem(nextByte); + readBytes += 1; + if (readBytes >= nextMessageLength) + { + outputQueue[outputQueue.length] = decoder.Decode(nextMessage); + nextMessage.Empty(); + readingLength = true; + readBytes = 0; + nextMessageLength = 0; + } + return true; +} + +/** + * Returns all complete messages read so far. + * + * Even if caller `AvariceStreamReader` entered a failed state - this method + * will return all the messages read before it has failed. + * + * @return aAl complete messages read from Avarice stream so far + */ +public final function array PopMessages() +{ + local array result; + result = outputQueue; + outputQueue.length = 0; + return result; +} + +/** + * Is caller `AvariceStreamReader` in a failed state? + * See `PushByte()` method for details. + * + * @return `true` iff caller `AvariceStreamReader` has failed. + */ +public final function bool Failed() +{ + return hasFailed; +} + +defaultproperties +{ + MAX_MESSAGE_LENGTH = 26214400 // 25 * 1024 * 1024 = 25MB +} \ No newline at end of file diff --git a/sources/Avarice/Tests/TEST_AvariceStreamReader.uc b/sources/Avarice/Tests/TEST_AvariceStreamReader.uc new file mode 100644 index 0000000000000000000000000000000000000000..1adf053797efda9942849fcb05fb9980a942b0f6 GIT binary patch literal 6148 zcmd^@?N1v=5XSd&rT!0BzQpv!1gc6^nx=J7AV@$EHj1LEA_m*osxg*dLaZqNdfMkV z!*aLBkvJqDkgAn^cl$Ck`^?OyZX6S~Qt{0)F|Cyeq`k(54 zqWfWJTVKbl$r@k$NulpM{obK~`7K|EM?G{ZqKVE!v3vePINWJza;|MRxT(ww>juHw?*+ zM%x9{a!JMUD5GcoLFUwaHw%|0wHqF(Z=t@quBYk)OMHoZe2eE!G>Wg0h8@i6C^{}Q znuT*+ncb6|+zOvLx0!gGXz7~_Bu%u6y~a55k?pV_j?}xY86%t377|%Fkp2_h5kpzn zwJS65HP#?$d_8AsdWgZ3#G?%P>tu7dVW(M zve5_5;L)``7VR1+HpbF4&=^*Gq)rT2b$N^s=DSm|3$>Q1sm=bxxFjf$RJ`e(|+OC zw&7po#C*4}3_0@HF&r93_>Wle+=Xce_6&Y){Z6$C9xX@hUF^It4rcY@ln@`iSE}d8j$KaKUL3@0{5+% zC-SodHpS1aa4c(z=TB4GK)S6MJG85xMa1D-jiI4qeA3cA{I{vO*z3FYQ>iUxac*pD zuQSCpqx@VKYy@xe$_x6coP1HZ;vFa7k(9n$bj9Q!F~7XKZxaX+{QSgD05jqu+}}~ zT2qO|ku3b4>>z4gPE$>>aco%Bg?Cp!Vr`+QY6yd9IT+WpzgVR_STdd-2;utJ9*UC!isXmX~)I!9O4=HN5%h^)wa%#;@!m@JSTLh80Uy@VD%F}cgMYc$>@qDjh zpmgU&3Dtg$ag|@oSRv!~EXRNNZkaYH#d6FK*n;Yw(|I92-?MmtAE{u8DOmN%wT`$) zkD){JI#osF&}d6h{!>XNQXAKLSjYRv%|^w?+#<>;gH+A%TujlcC+t^K9YB^mmuzPT zV)N2CgA@2Wi}7-F#@!OjbxEL(>Z>|&8*6%xxsNN>Ro#~Dij_)DIp%0Rv}%s0$NE*F zTDLlWrg{ow&*YcB@hw##>wYi>T$iq_6s6IxpTh5BT6ZiC&ZTi64dwlK-Y=`?e){RA zm#XksKDMbW|EP@P|A+T)a*|_LjzTKwJ;gNVRaJ~&7WfC^bGhCo7O+WraYs* zZfxLvu<&=go*~0|%iA6vk8d4qMFgiIUlLMJ`g=wmT`QH9^q*R{BB{vZThqttxT1Hd z#fGwkxQ}}M42)G8w&J~*HD9X^i}GU?Sj@)zREOQ289K@a@;;OQc?WdQj&(n=Cvb)Z zCN?KB_l128n8+&&d9jE}%v1L@b(h!wWp=6Ze(Kws_02~3MJ@U`YP$ccmgR?KW14p< zveYYGf75_#@fcsZrsk1%iC=gZCxf=ln{#oq;|I&GlunY=qjBZFB;dt}d)3O@&LEfg F{y(PeuWJAR literal 0 HcmV?d00001 diff --git a/sources/Manifest.uc b/sources/Manifest.uc index f0629aa..b1697b2 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -57,4 +57,5 @@ defaultproperties testCases(21) = class'TEST_LogMessage' testCases(22) = class'TEST_LocalDatabase' testCases(23) = class'TEST_UTF8EncoderDecoder' + testCases(24) = class'TEST_AvariceStreamReader' } \ No newline at end of file