Bimba.git

commit dc7187a5a02ad5a295abe79ddfe34054eff1f342

Author: Adam Evyčędo <git@apiote.xyz>

save feed infos, not response

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt
index a93eb3aba06721748653ce20bd6b35e591997495..f7f11028cc98d6a6293159a69cd838bef236a0ac 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesViewModel.kt
@@ -72,6 +72,9 @@ 		val intent = (context as Activity).intent
 		var feeds = OfflineRepository().getFeeds(context)
 		if (feeds.isNullOrEmpty()) {
 			feeds = OnlineRepository().getFeeds(context)
+			if (feeds != null) {
+				OfflineRepository().saveFeedCache(context, feeds)
+			}
 		}
 		return when (intent.action) {
 			Intent.ACTION_VIEW -> {




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
index 4c3211555a8fc4219845e9562e824ca6195c25f5..04d7f546e88a6465b0fc535cd15d304d42b478d6 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/FeedInfo.kt
@@ -6,11 +6,24 @@ package xyz.apiote.bimba.czwek.repo
 
 import xyz.apiote.bimba.czwek.api.structs.FeedInfoV1
 import xyz.apiote.bimba.czwek.api.structs.FeedInfoV2
+import xyz.apiote.bimba.czwek.api.structs.QrLocationV1
+import xyz.apiote.fruchtfleisch.Reader
+import xyz.apiote.fruchtfleisch.Writer
+import java.io.InputStream
+import java.io.OutputStream
 import java.time.LocalDate
 import java.time.format.DateTimeFormatter
 import java.time.format.FormatStyle
 import java.util.Locale
 
+class FeedInfoPrev {
+	companion object {
+		fun unmarshal(stream: InputStream): FeedInfo {
+			return FeedInfo(FeedInfoPrev())
+		}
+	}
+}
+
 data class FeedInfo(
 	val id: String,
 	val name: String,
@@ -24,6 +37,41 @@ 	val validSince: LocalDate?,
 	val validTill: LocalDate?,
 	val cached: Boolean
 ) {
+	companion object {
+		const val VERSION = 100u
+		private fun parseDate(dateString: String) =
+			LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE)
+
+
+		fun unmarshal(stream: InputStream): FeedInfo {
+			val reader = Reader(stream)
+			val id = reader.readString()
+			val name = reader.readString()
+			val attribution = reader.readString()
+			val description = reader.readString()
+			val lastUpdate = parseDate(reader.readString())
+			val qrHost = reader.readString()
+			val qrIn = QrLocation.of(QrLocationV1.of(reader.readUInt().toULong().toUInt()))
+			val qrSelector = reader.readString()
+			val validSince = reader.readString()
+			val validTill = reader.readString()
+
+			return FeedInfo(
+				id,
+				name,
+				attribution,
+				description,
+				lastUpdate,
+				qrHost,
+				qrIn,
+				qrSelector,
+				if (validSince != "") parseDate(validSince) else null,
+				if (validTill != "") parseDate(validTill) else null,
+				true
+			)
+		}
+	}
+
 	constructor(f: FeedInfoV2, cached: Boolean = false) : this(
 		f.id,
 		f.name,
@@ -52,6 +100,38 @@ 		null,
 		cached
 	)
 
+	constructor(f: FeedInfoPrev) : this(
+		"",
+		"",
+		"",
+		"",
+		LocalDate.MIN,
+		"",
+		QrLocation.UNKNOWN,
+		"",
+		null,
+		null,
+		false
+	)
+
+	fun marshal(stream: OutputStream) {
+		val writer = Writer(stream)
+		writer.writeString(id)
+		writer.writeString(name)
+		writer.writeString(attribution)
+		writer.writeString(description)
+		writer.writeString(formatDateMarshal(lastUpdate))
+		writer.writeString(qrHost)
+		writer.writeUInt(qrIn.value().toULong())
+		writer.writeString(qrSelector)
+		writer.writeString(if (validSince == null) "" else formatDateMarshal(validSince))
+		writer.writeString(if (validTill == null) "" else formatDateMarshal(validTill))
+	}
+
+	private fun formatDateMarshal(date: LocalDate): String {
+		return date.format(DateTimeFormatter.ISO_LOCAL_DATE)
+	}
+
 	fun formatDate(): String {
 		return lastUpdate.format(
 			DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(Locale.getDefault())
@@ -74,8 +154,16 @@ 		if (lastUpdate.isAfter(other.lastUpdate)) lastUpdate else other.lastUpdate,
 		other.qrHost,
 		other.qrIn,
 		other.qrSelector,
-		if (other.validSince == null || (validSince?:LocalDate.MIN).isAfter(other.validSince)) validSince else other.validSince,
-		if (other.validTill == null || (validTill?:LocalDate.MIN).isAfter(other.validTill)) validTill else other.validTill,
+		if (other.validSince == null || (validSince
+				?: LocalDate.MIN).isAfter(other.validSince)
+		) validSince else other.validSince,
+		if (other.validTill == null || (validTill
+				?: LocalDate.MIN).isAfter(other.validTill)
+		) validTill else other.validTill,
 		this.cached && other.cached
 	)
 }
+
+fun migrateFeeds() {
+
+}
\ No newline at end of file




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
index 060a5e8b409d2253e46f0193a99e0536234e3435..370e3fdc6c72ba886737c4440c0b88e18d4c77cc 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OfflineRepository.kt
@@ -5,19 +5,33 @@
 package xyz.apiote.bimba.czwek.repo
 
 import android.content.Context
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
+import androidx.core.content.edit
 import xyz.apiote.bimba.czwek.api.Server
-import xyz.apiote.bimba.czwek.api.responses.FeedsResponse
-import xyz.apiote.bimba.czwek.api.responses.FeedsResponseDev
-import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV1
-import xyz.apiote.bimba.czwek.api.responses.FeedsResponseV2
+import xyz.apiote.fruchtfleisch.Reader
+import xyz.apiote.fruchtfleisch.Writer
 import java.io.File
-import java.io.FileInputStream
 import java.net.URLEncoder
 import java.time.LocalDate
 
 class OfflineRepository : Repository {
+	fun saveFeedCache(context: Context, feedInfos: Map<String, FeedInfo>) {
+		val file = File(
+			context.filesDir, URLEncoder.encode(Server.get(context).apiPath, "utf-8")
+		)
+		context.getSharedPreferences("offlineFeeds", Context.MODE_PRIVATE).edit {
+			putInt("version", FeedInfo.VERSION.toInt())
+		}
+		val stream = file.outputStream()
+		val writer = Writer(stream)
+		writer.writeUInt(feedInfos.size.toULong())
+		feedInfos.forEach {
+			it.value.marshal(stream)
+		}
+		stream.flush()
+		stream.close()
+	}
+
+	@Suppress("RedundantNullableReturnType")
 	override suspend fun getFeeds(
 		context: Context,
 		server: Server
@@ -28,31 +42,25 @@ 		)
 		if (!file.exists()) {
 			return emptyMap()
 		}
-		return when (val response =
-			withContext(Dispatchers.IO) { FeedsResponse.unmarshal(FileInputStream(file)) }) {
-			is FeedsResponseDev -> response.feeds.associate {
-				Pair(
-					it.id,
-					FeedInfo(it).copy(cached = true)
-				)
-			}
 
-			is FeedsResponseV2 -> response.feeds.associate {
-				Pair(
-					it.id,
-					FeedInfo(it).copy(cached = true)
-				)
-			}
-
-			is FeedsResponseV1 -> response.feeds.associate {
-				Pair(
-					it.id,
-					FeedInfo(it).copy(cached = true)
-				)
+		val version =
+			context.getSharedPreferences("offlineFeeds", Context.MODE_PRIVATE).getInt("version", -1)
+		if (version < 0) {
+			return emptyMap()
+		}
+		val unmarshaller =
+			if (version.toUInt() == FeedInfo.VERSION) FeedInfo::unmarshal else FeedInfoPrev::unmarshal
+		val stream = file.inputStream()
+		val feeds = mutableMapOf<String, FeedInfo>()
+		val n = Reader(stream).readUInt().toULong().toInt()
+		repeat(n) {
+			val feed = unmarshaller(stream)
+			feeds[feed.id] = feed
+			if (version.toUInt() != FeedInfo.VERSION) {
+				saveFeedCache(context, feeds)
 			}
-
-			else -> null
 		}
+		return feeds
 	}
 
 	override suspend fun getDepartures(




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
index 46c1285469207349ce1535a492aef6d7b1fb8a30..41719310917f2dbc331d147cd03fa30d5676bd61 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/OnlineRepository.kt
@@ -52,13 +52,6 @@
 // todo [3.2] in Repository check if responses are BARE or HTML
 
 class OnlineRepository : Repository {
-	private fun saveFeedCache(server: Server, context: Context, rawResponse: ByteArray) {
-		val file = File(
-			context.filesDir, URLEncoder.encode(server.apiPath, "utf-8")
-		)
-		file.writeBytes(rawResponse)
-	}
-
 	override suspend fun getFeeds(
 		context: Context,
 		server: Server
@@ -74,7 +67,6 @@ 				throw TrafficResponseException(result.error.statusCode, "", result.error)
 			}
 		} else {
 			val rawResponse = result.stream!!.readBytes()
-			saveFeedCache(server, context, rawResponse)
 			return when (val response =
 				withContext(Dispatchers.IO) { FeedsResponse.unmarshal(rawResponse.inputStream()) }) {
 				is FeedsResponseDev -> response.feeds.associate { Pair(it.id, FeedInfo(it)) }




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt
index 93d5932ed09463d59ff583a66727df435461f03e..108b0077fdb81df22d1e537ed9e696f3bc6d4f92 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/QrLocation.kt
@@ -20,4 +20,13 @@ 				QrLocationV1.QUERY -> QUERY
 			}
 		}
 	}
+
+	fun value(): UInt {
+		return when (this) {
+			UNKNOWN -> 0u
+			NONE -> 1u
+			PATH -> 2u
+			QUERY -> 3u
+		}
+	}
 }




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt
index 4b651ced88475df9b4e0d3ae1982b9eb49d26500..6d5fb983e6baf603ce0237922088462d9450f328 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedsViewModel.kt
@@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import xyz.apiote.bimba.czwek.api.Error
 import xyz.apiote.bimba.czwek.repo.FeedInfo
 import xyz.apiote.bimba.czwek.repo.OfflineRepository
@@ -44,25 +45,36 @@ 		setSettings(feedID, feedSettings?.copy(enabled = enabled) ?: FeedSettings(enabled, true))
 	}
 
 	fun loadFeeds(context: Context) {
+		var offlineFeeds: Map<String, FeedInfo>? = null
+		var onlineFeeds: Map<String, FeedInfo>? = null
+		var error: Error? = null
 		MainScope().launch {
-			val offlineRepository = OfflineRepository()
-			val offlineFeeds =
-				offlineRepository.getFeeds(context)
-			if (!offlineFeeds.isNullOrEmpty()) {
-				_feeds.value = offlineFeeds!!
+			withContext(coroutineContext) {
+				launch {
+					offlineFeeds =
+						OfflineRepository().getFeeds(context)
+					if (!offlineFeeds.isNullOrEmpty()) {
+						_feeds.value = offlineFeeds!!
+					}
+				}
+				launch {
+					try {
+						val repository = OnlineRepository()
+						onlineFeeds =
+							repository.getFeeds(context)
+					} catch (e: TrafficResponseException) {
+						error = e.error
+						Log.e("Feeds", "$e")
+					}
+				}
 			}
-			try {
-				val repository = OnlineRepository()
-				val onlineFeeds =
-					repository.getFeeds(context)
+			if (offlineFeeds.isNullOrEmpty() && error != null) {
+				_error.value = error!!
+			}  else{
 				joinFeeds(offlineFeeds, onlineFeeds).let { joinedFeeds ->
 					_feeds.value = joinedFeeds
+					OfflineRepository().saveFeedCache(context, joinedFeeds)
 				}
-			} catch (e: TrafficResponseException) {
-				if (offlineFeeds.isNullOrEmpty()) {
-					_error.value = e.error
-				}
-				Log.e("Feeds", "$e")
 			}
 		}
 	}
@@ -82,6 +94,8 @@ 		if (feeds2.isNullOrEmpty()) {
 			return feeds1
 		}
 
-		return feeds1.keys.union(feeds2.keys).associateWith { feeds1[it].join(feeds2[it]) }
+		return feeds1.keys.union(feeds2.keys).associateWith {
+			feeds1[it].join(feeds2[it])
+		}
 	}
 }




diff --git a/fruchtfleisch/build.gradle b/fruchtfleisch/build.gradle
index a510340b4c57eef3e6a539c90d057d9dd62b44f1..85c32f76546645efcd429747057f155ce9b51955 100644
--- a/fruchtfleisch/build.gradle
+++ b/fruchtfleisch/build.gradle
@@ -8,9 +8,15 @@     id 'org.jetbrains.kotlin.jvm'
 }
 
 dependencies {
+    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
+    testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
+
     //implementation 'org.jetbrains.kotlin:kotlin-reflect:1.8.10'
 }
 java {
     sourceCompatibility = JavaVersion.VERSION_17
     targetCompatibility = JavaVersion.VERSION_17
+}
+test {
+    useJUnitPlatform()
 }
\ No newline at end of file




diff --git a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt
index a336c9f136e40ff0896895110071eb81a23f8ba3..3b29727d880991e2d5a8cd58f7e91bd2e34401f4 100644
--- a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt
+++ b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Reader.kt
@@ -9,17 +9,10 @@ import java.io.InputStream
 import java.lang.Double.longBitsToDouble
 import java.lang.Float.intBitsToFloat
 
-data class IntVar(private val v: Long) {
-	fun toLong() = v
-}
-data class UIntVar(private val v: ULong) {
-	fun toULong() = v
-}
-
 @Suppress("MemberVisibilityCanBePrivate", "unused", "BooleanMethodIsAlwaysInverted")
 class Reader(private val stream: InputStream) {
 	fun readUInt(): UIntVar {
-		var result: ULong = 0UL
+		var result = 0UL
 		var i = 0
 		var s = 0
 		while (true) {




diff --git a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/UInt.kt b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/UInt.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d058ee473c7bc0bf6bc200dfc5aca6ac8da2ed84
--- /dev/null
+++ b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/UInt.kt
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package xyz.apiote.fruchtfleisch
+
+data class IntVar(private val v: Long) {
+	fun toLong() = v
+}
+data class UIntVar(private val v: ULong) {
+	fun toULong() = v
+}
\ No newline at end of file




diff --git a/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Writer.kt b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Writer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7fe8ebe1fe76d0dbda1f0af4bb65cf8f69e673fe
--- /dev/null
+++ b/fruchtfleisch/src/main/java/xyz/apiote/fruchtfleisch/Writer.kt
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package xyz.apiote.fruchtfleisch
+
+import java.io.OutputStream
+
+@Suppress("MemberVisibilityCanBePrivate")
+class Writer(private val stream: OutputStream) {
+	@OptIn(ExperimentalUnsignedTypes::class)
+	fun writeUInt(v: ULong) {
+		var value = v
+		val bytes = mutableListOf<UByte>()
+		while (value >= 0x80u) {
+			bytes.add(value.toUByte() or 0x80u)
+			value = value.shr(7)
+		}
+		bytes.add(value.toUByte())
+		stream.write(bytes.toUByteArray().toByteArray())
+	}
+
+	fun writeFixedData(v: ByteArray) {
+		stream.write(v)
+	}
+
+	fun writeData(v: ByteArray) {
+		writeUInt(v.size.toULong())
+		writeFixedData(v)
+	}
+
+	fun writeString(v: String) {
+		writeData(v.encodeToByteArray())
+	}
+}
\ No newline at end of file




diff --git a/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/ReaderTest.kt b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/ReaderTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..210beec41cea3377f8cd050f2091bb4f4c1bb652
--- /dev/null
+++ b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/ReaderTest.kt
@@ -0,0 +1,96 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package xyz.apiote.fruchtfleisch
+
+import org.junit.jupiter.api.Test
+
+
+@OptIn(ExperimentalUnsignedTypes::class)
+class ReaderTest {
+	@Test
+	fun readUInt17() {
+		val stream = byteArrayOf(0x11).inputStream()
+		val reader = Reader(stream)
+		assert(reader.readUInt().toULong().toInt() == 17)
+	}
+	@Test
+	fun readUInt23() {
+		val stream = byteArrayOf(0x17).inputStream()
+		val reader = Reader(stream)
+		assert(reader.readUInt().toULong().toInt() == 23)
+	}
+	@Test
+	fun readUInt999() {
+		val stream = ubyteArrayOf(0xe7u, 0x7u).toByteArray().inputStream()
+		val reader = Reader(stream)
+		assert(reader.readUInt().toULong().toInt() == 999)
+	}
+
+	@Test
+	fun readInt() {
+	}
+
+	@Test
+	fun readU8() {
+	}
+
+	@Test
+	fun readU16() {
+	}
+
+	@Test
+	fun readU32() {
+	}
+
+	@Test
+	fun readU64() {
+	}
+
+	@Test
+	fun readI8() {
+	}
+
+	@Test
+	fun readI16() {
+	}
+
+	@Test
+	fun readI32() {
+	}
+
+	@Test
+	fun readI64() {
+	}
+
+	@Test
+	fun readFloat32() {
+	}
+
+	@Test
+	fun readFloat64() {
+	}
+
+	@Test
+	fun readData() {
+	}
+
+	@Test
+	fun readStringAscii() {
+		val stream = byteArrayOf(0x24, 0x4d, 0x72, 0x2e, 0x20, 0x4a, 0x6f, 0x63, 0x6b, 0x2c, 0x20, 0x54, 0x56, 0x20, 0x71, 0x75, 0x69, 0x7a, 0x20, 0x50, 0x68, 0x44, 0x2c, 0x20, 0x62, 0x61, 0x67, 0x73, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6c, 0x79, 0x6e, 0x78).inputStream()
+		val reader = Reader(stream)
+		assert(reader.readString() == "Mr. Jock, TV quiz PhD, bags few lynx")
+	}
+
+	@Test
+	fun readStringUnicode() {
+		val stream = ubyteArrayOf(0x34u, 0x53u, 0x74u, 0x72u, 0xc3u, 0xb3u, 0xc5u, 0xbcu, 0x20u, 0x70u, 0x63u, 0x68u, 0x6eu, 0xc4u, 0x85u, 0xc5u, 0x82u, 0x20u, 0x6bu, 0x6fu, 0xc5u, 0x9bu, 0xc4u, 0x87u, 0x20u, 0x77u, 0x20u, 0x71u, 0x75u, 0x69u, 0x7au, 0x20u, 0x67u, 0xc4u, 0x99u, 0x64u, 0xc5u, 0xbau, 0x62u, 0x20u, 0x76u, 0x65u, 0x6cu, 0x20u, 0x66u, 0x61u, 0x78u, 0x20u, 0x6du, 0x79u, 0x6au, 0xc5u, 0x84u).toByteArray().inputStream()
+		val reader = Reader(stream)
+		assert(reader.readString() == "Stróż pchnął kość w quiz gędźb vel fax myjń")
+	}
+
+	@Test
+	fun readBoolean() {
+	}
+}
\ No newline at end of file




diff --git a/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/WriterTest.kt b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/WriterTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cad8ee16bbcd6c8082e51e99b0302636c75913c1
--- /dev/null
+++ b/fruchtfleisch/src/test/java/xyz/apiote/fruchtfleisch/WriterTest.kt
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: Adam Evyčędo
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package xyz.apiote.fruchtfleisch
+
+import org.junit.jupiter.api.Test
+import java.io.ByteArrayOutputStream
+
+@OptIn(ExperimentalUnsignedTypes::class)
+class WriterTest {
+
+	@Test
+	fun writeUInt17() {
+		val stream = ByteArrayOutputStream()
+		val writer = Writer(stream)
+		writer.writeUInt(17u)
+		val bytes = stream.toByteArray()
+		assert(bytes.contentEquals(byteArrayOf(0x11)))
+	}
+
+	@Test
+	fun writeUInt23() {
+		val stream = ByteArrayOutputStream()
+		val writer = Writer(stream)
+		writer.writeUInt(23u)
+		val bytes = stream.toByteArray()
+		assert(bytes.contentEquals(byteArrayOf(0x17)))
+	}
+
+	@Test
+	fun writeUInt999() {
+		val stream = ByteArrayOutputStream()
+		val writer = Writer(stream)
+		writer.writeUInt(999u)
+		val bytes = stream.toByteArray().toUByteArray()
+		assert(bytes.contentEquals(ubyteArrayOf(0xe7u, 0x7u)))
+	}
+
+	@Test
+	fun writeStringAscii() {
+		val stream = ByteArrayOutputStream()
+		val writer = Writer(stream)
+		writer.writeString("Mr. Jock, TV quiz PhD, bags few lynx")
+		val bytes = stream.toByteArray()
+		assert(bytes.contentEquals(byteArrayOf(0x24, 0x4d, 0x72, 0x2e, 0x20, 0x4a, 0x6f, 0x63, 0x6b, 0x2c, 0x20, 0x54, 0x56, 0x20, 0x71, 0x75, 0x69, 0x7a, 0x20, 0x50, 0x68, 0x44, 0x2c, 0x20, 0x62, 0x61, 0x67, 0x73, 0x20, 0x66, 0x65, 0x77, 0x20, 0x6c, 0x79, 0x6e, 0x78)))
+	}
+}
\ No newline at end of file