libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 3bfd71615f61f17708ab97757c8fde428519eeb6
parent 0c8f9082a1f34bc187425e990bba061c3d938838
Author: MS <ms@taler.net>
Date:   Mon,  2 Oct 2023 12:44:17 +0200

Implementing accounts deletion.

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt | 21++++++++++++++++++++-
Mbank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt | 49++++++++++++++++++++++++++++++++++++++-----------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 30+++++++++++++++++++++++-------
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 5+++++
Mbank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt | 6+++---
Mbank/src/test/kotlin/Common.kt | 1+
Mbank/src/test/kotlin/DatabaseTest.kt | 42+++++++++++++++++++++++++++++++++++-------
Mbank/src/test/kotlin/LibeuFinApiTest.kt | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mbank/src/test/kotlin/TalerApiTest.kt | 13+++++++------
Mdatabase-versioning/libeufin-bank-0001.sql | 1+
Mdatabase-versioning/procedures.sql | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mutil/src/main/kotlin/Encoding.kt | 4++++
12 files changed, 253 insertions(+), 37 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt @@ -19,7 +19,6 @@ package tech.libeufin.bank -import CreditDebitIndicator import io.ktor.http.* import io.ktor.server.application.* import kotlinx.serialization.Serializable @@ -476,6 +475,16 @@ fun ApplicationCall.getResourceName(param: String): ResourceName = this.expectUriComponent(param) /** + * This type communicates the result of deleting an account + * from the database. + */ +enum class CustomerDeletionResult { + SUCCESS, + CUSTOMER_NOT_FOUND, + BALANCE_NOT_ZERO +} + +/** * This type communicates the result of a database operation * to confirm one withdrawal operation. */ @@ -496,6 +505,16 @@ enum class WithdrawalConfirmationResult { CONFLICT } +/** + * Communicates the result of creating a bank transaction in the database. + */ +enum class BankTransactionResult { + NO_CREDITOR, + NO_DEBTOR, + SUCCESS, + CONFLICT // balance insufficient +} + // GET /config response from the Taler Integration API. @Serializable data class TalerIntegrationConfigResponse( diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt @@ -185,13 +185,10 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { transactionDate = Instant.now() ) when (db.bankTransactionCreate(adminPaysBonus)) { - Database.BankTransactionResult.NO_CREDITOR -> throw internalServerError("Bonus impossible: creditor not found, despite its recent creation.") - - Database.BankTransactionResult.NO_DEBTOR -> throw internalServerError("Bonus impossible: admin not found.") - - Database.BankTransactionResult.CONFLICT -> throw internalServerError("Bonus impossible: admin has insufficient balance.") - - Database.BankTransactionResult.SUCCESS -> {/* continue the execution */ + BankTransactionResult.NO_CREDITOR -> throw internalServerError("Bonus impossible: creditor not found, despite its recent creation.") + BankTransactionResult.NO_DEBTOR -> throw internalServerError("Bonus impossible: admin not found.") + BankTransactionResult.CONFLICT -> throw internalServerError("Bonus impossible: admin has insufficient balance.") + BankTransactionResult.SUCCESS -> {/* continue the execution */ } } } @@ -257,6 +254,36 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { ) return@get } + delete("/accounts/{USERNAME}") { + val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized() + val resourceName = call.expectUriComponent("USERNAME") + // Checking rights. + if (c.login != "admin" && ctx.restrictAccountDeletion) + throw forbidden("Only admin allowed.") + if (!resourceName.canI(c, withAdmin = true)) + throw forbidden("Insufficient rights on this account.") + // Not deleting reserved names. + if (resourceName == "bank" || resourceName == "admin") + throw forbidden("Cannot delete reserved accounts.") + val res = db.customerDeleteIfBalanceIsZero(resourceName) + when (res) { + CustomerDeletionResult.CUSTOMER_NOT_FOUND -> + throw notFound( + "Customer '$resourceName' not found", + talerEc = TalerErrorCode.TALER_EC_NONE // FIXME: need EC. + ) + CustomerDeletionResult.BALANCE_NOT_ZERO -> + throw LibeufinBankException( + httpStatus = HttpStatusCode.PreconditionFailed, + talerError = TalerError( + hint = "Balance is not zero.", + code = TalerErrorCode.TALER_EC_NONE.code // FIXME: need EC. + ) + ) + CustomerDeletionResult.SUCCESS -> call.respond(HttpStatusCode.NoContent) + } + return@delete + } post("/accounts/{USERNAME}/withdrawals") { val c = call.authenticateBankRequest(db, TokenScope.readwrite) @@ -435,13 +462,13 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { ) val res = db.bankTransactionCreate(dbInstructions) when (res) { - Database.BankTransactionResult.CONFLICT -> throw conflict( + BankTransactionResult.CONFLICT -> throw conflict( "Insufficient funds", TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT ) - Database.BankTransactionResult.NO_CREDITOR -> throw internalServerError("Creditor not found despite previous checks.") - Database.BankTransactionResult.NO_DEBTOR -> throw internalServerError("Debtor not found despite the request was authenticated.") - Database.BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK) + BankTransactionResult.NO_CREDITOR -> throw internalServerError("Creditor not found despite previous checks.") + BankTransactionResult.NO_DEBTOR -> throw internalServerError("Debtor not found despite the request was authenticated.") + BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK) } return@post } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -21,7 +21,6 @@ package tech.libeufin.bank import org.postgresql.jdbc.PgConnection -import org.postgresql.util.PSQLException import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.util.getJdbcConnectionFromPg @@ -231,6 +230,27 @@ class Database(private val dbConfig: String, private val bankCurrency: String) { } } + /** + * Deletes a customer (including its bank account row) from + * the database. The bank account gets deleted by the cascade. + */ + fun customerDeleteIfBalanceIsZero(login: String): CustomerDeletionResult { + reconnect() + val stmt = prepare(""" + SELECT + out_nx_customer, + out_balance_not_zero + FROM customer_delete(?); + """) + stmt.setString(1, login) + stmt.executeQuery().apply { + if (!this.next()) throw internalServerError("Deletion returned nothing.") + if (this.getBoolean("out_nx_customer")) return CustomerDeletionResult.CUSTOMER_NOT_FOUND + if (this.getBoolean("out_balance_not_zero")) return CustomerDeletionResult.BALANCE_NOT_ZERO + return CustomerDeletionResult.SUCCESS + } + } + // Mostly used to get customers out of bearer tokens. fun customerGetFromRowId(customer_id: Long): Customer? { reconnect() @@ -304,6 +324,7 @@ class Database(private val dbConfig: String, private val bankCurrency: String) { ) } } + // Possibly more "customerGetFrom*()" to come. // BEARER TOKEN @@ -566,12 +587,7 @@ class Database(private val dbConfig: String, private val bankCurrency: String) { } // BANK ACCOUNT TRANSACTIONS - enum class BankTransactionResult { - NO_CREDITOR, - NO_DEBTOR, - SUCCESS, - CONFLICT // balance insufficient - } + fun bankTransactionCreate( tx: BankInternalTransaction ): BankTransactionResult { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -83,6 +83,10 @@ data class BankApplicationContext( */ val restrictRegistration: Boolean, /** + * Restrict account deletion to the administrator. + */ + val restrictAccountDeletion: Boolean, + /** * Cashout currency, if cashouts are supported. */ val cashoutCurrency: String?, @@ -480,6 +484,7 @@ fun readBankApplicationContextFromConfig(cfg: TalerConfig): BankApplicationConte suggestedWithdrawalExchange = cfg.lookupValueString("libeufin-bank", "suggested_withdrawal_exchange"), defaultAdminDebtLimit = cfg.requireValueAmount("libeufin-bank", "default_admin_debt_limit", currency), spaCaptchaURL = cfg.lookupValueString("libeufin-bank", "spa_captcha_url"), + restrictAccountDeletion = cfg.lookupValueBooleanDefault("libeufin-bank", "restrict_account_deletion", true) ) } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt @@ -123,12 +123,12 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) exchangeBankAccountId = exchangeBankAccount.expectRowId(), timestamp = transferTimestamp ) - if (dbRes.txResult == Database.BankTransactionResult.CONFLICT) + if (dbRes.txResult == BankTransactionResult.CONFLICT) throw conflict( "Insufficient balance for exchange", TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT ) - if (dbRes.txResult == Database.BankTransactionResult.NO_CREDITOR) + if (dbRes.txResult == BankTransactionResult.NO_CREDITOR) throw notFound( "Creditor account was not found", TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT @@ -179,7 +179,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) * Other possible errors are highly unlikely, because of the * previous checks on the existence of the involved bank accounts. */ - if (res == Database.BankTransactionResult.CONFLICT) + if (res == BankTransactionResult.CONFLICT) throw conflict( "Insufficient balance", TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT diff --git a/bank/src/test/kotlin/Common.kt b/bank/src/test/kotlin/Common.kt @@ -47,6 +47,7 @@ fun getTestContext( registrationBonus = null, suggestedWithdrawalExchange = suggestedExchange, spaCaptchaURL = null, + restrictAccountDeletion = true ) } diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -18,8 +18,10 @@ */ import org.junit.Test +import org.postgresql.jdbc.PgConnection import tech.libeufin.bank.* import tech.libeufin.util.CryptoUtil +import java.sql.DriverManager import java.time.Instant import java.util.Random import java.util.UUID @@ -125,7 +127,7 @@ class DatabaseTest { exchangeBankAccountId = 1L, timestamp = Instant.now() ) - assert(res.txResult == Database.BankTransactionResult.SUCCESS) + assert(res.txResult == BankTransactionResult.SUCCESS) } @Test @@ -167,13 +169,13 @@ class DatabaseTest { TalerAmount(50, 0, currency) ) val firstSpending = db.bankTransactionCreate(fooPaysBar) // Foo pays Bar and goes debit. - assert(firstSpending == Database.BankTransactionResult.SUCCESS) + assert(firstSpending == BankTransactionResult.SUCCESS) fooAccount = db.bankAccountGetFromOwnerId(fooId) // Foo: credit -> debit assert(fooAccount?.hasDebt == true) // Asserting Foo's debit. // Now checking that more spending doesn't get Foo out of debit. val secondSpending = db.bankTransactionCreate(fooPaysBar) - assert(secondSpending == Database.BankTransactionResult.SUCCESS) + assert(secondSpending == BankTransactionResult.SUCCESS) fooAccount = db.bankAccountGetFromOwnerId(fooId) // Checking that Foo's debit is two times the paid amount // Foo: debit -> debit @@ -200,14 +202,14 @@ class DatabaseTest { transactionDate = Instant.now() ) val barPays = db.bankTransactionCreate(barPaysFoo) - assert(barPays == Database.BankTransactionResult.SUCCESS) + assert(barPays == BankTransactionResult.SUCCESS) barAccount = db.bankAccountGetFromOwnerId(barId) val barBalanceTen: TalerAmount? = barAccount?.balance // Bar: credit -> credit assert(barAccount?.hasDebt == false && barBalanceTen?.value == 10L && barBalanceTen.frac == 0) // Bar pays again to let Foo return in credit. val barPaysAgain = db.bankTransactionCreate(barPaysFoo) - assert(barPaysAgain == Database.BankTransactionResult.SUCCESS) + assert(barPaysAgain == BankTransactionResult.SUCCESS) // Refreshing the two accounts. barAccount = db.bankAccountGetFromOwnerId(barId) fooAccount = db.bankAccountGetFromOwnerId(fooId) @@ -218,7 +220,7 @@ class DatabaseTest { assert(barAccount?.balance?.equals(TalerAmount(0, 0, "KUDOS")) == true) // Bringing Bar to debit. val barPaysMore = db.bankTransactionCreate(barPaysFoo) - assert(barPaysMore == Database.BankTransactionResult.SUCCESS) + assert(barPaysMore == BankTransactionResult.SUCCESS) barAccount = db.bankAccountGetFromOwnerId(barId) fooAccount = db.bankAccountGetFromOwnerId(fooId) // Bar: credit -> debit @@ -226,6 +228,32 @@ class DatabaseTest { assert(fooAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == true) assert(barAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == true) } + + // Testing customer(+bank account) deletion logic. + @Test + fun customerDeletionTest() { + val db = initDb() + // asserting false, as foo doesn't exist yet. + assert(db.customerDeleteIfBalanceIsZero("foo") == CustomerDeletionResult.CUSTOMER_NOT_FOUND) + // Creating foo. + db.customerCreate(customerFoo).apply { + assert(this != null) + assert(db.bankAccountCreate(bankAccountFoo) != null) + } + // foo has zero balance, deletion should succeed. + assert(db.customerDeleteIfBalanceIsZero("foo") == CustomerDeletionResult.SUCCESS) + val db2 = initDb() + // Creating foo again, artificially setting its balance != zero. + db2.customerCreate(customerFoo).apply { + assert(this != null) + db2.bankAccountCreate(bankAccountFoo).apply { + assert(this != null) + val conn = DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java) + conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.frac = 1 WHERE bank_account_id = $this") + } + } + assert(db.customerDeleteIfBalanceIsZero("foo") == CustomerDeletionResult.BALANCE_NOT_ZERO) + } @Test fun customerCreationTest() { val db = initDb() @@ -346,7 +374,7 @@ class DatabaseTest { paymentInformationId = "pmtinfid", transactionDate = Instant.now() ) - ) == Database.BankTransactionResult.SUCCESS) + ) == BankTransactionResult.SUCCESS) // Confirming the cash-out assert(db.cashoutConfirm(op.cashoutUuid, 1L, 1L)) // Checking the confirmation took place. diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt @@ -2,13 +2,16 @@ import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* +import io.ktor.server.engine.* import io.ktor.server.testing.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import net.taler.wallet.crypto.Base32Crockford import org.junit.Test +import org.postgresql.jdbc.PgConnection import tech.libeufin.bank.* import tech.libeufin.util.CryptoUtil +import java.sql.DriverManager import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit @@ -518,6 +521,66 @@ class LibeuFinApiTest { } /** + * Tests DELETE /accounts/foo + */ + @Test + fun deleteAccount() { + val db = initDb() + val ctx = getTestContext() + val adminCustomer = Customer( + "admin", + CryptoUtil.hashpw("pass"), + "CFO" + ) + db.customerCreate(adminCustomer) + testApplication { + application { + corebankWebApp(db, ctx) + } + // account to delete doesn't exist. + client.delete("/accounts/foo") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.NotFound) + } + // account to delete is reserved. + client.delete("/accounts/admin") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.Forbidden) + } + // successful deletion + db.customerCreate(customerFoo).apply { + assert(this != null) + assert(db.bankAccountCreate(genBankAccount(this!!)) != null) + } + client.delete("/accounts/foo") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.NoContent) + } + // fail to delete, due to a non-zero balance. + db.customerCreate(customerBar).apply { + assert(this != null) + db.bankAccountCreate(genBankAccount(this!!)).apply { + assert(this != null) + val conn = DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java) + conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 1 WHERE bank_account_id = $this") + } + } + client.delete("/accounts/bar") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.PreconditionFailed) + } + } + } + + /** * Tests the GET /accounts endpoint. */ @Test @@ -544,12 +607,12 @@ class LibeuFinApiTest { // foo account db.customerCreate(customerFoo).apply { assert(this != null) - db.bankAccountCreate(genBankAccount(this!!)) != null + assert(db.bankAccountCreate(genBankAccount(this!!)) != null) } // bar account db.customerCreate(customerBar).apply { assert(this != null) - db.bankAccountCreate(genBankAccount(this!!)) != null + assert(db.bankAccountCreate(genBankAccount(this!!)) != null) } // Two users registered, requesting all of them. client.get("/accounts") { diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt @@ -4,6 +4,7 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import kotlinx.serialization.json.Json +import net.taler.wallet.crypto.Base32Crockford import org.junit.Test import tech.libeufin.bank.* import tech.libeufin.util.CryptoUtil @@ -151,14 +152,14 @@ class TalerApiTest { // Foo pays Bar (the exchange) twice. val reservePubOne = "5ZFS98S1K4Y083W95GVZK638TSRE44RABVASB3AFA3R95VCW17V0" val reservePubTwo = "TFBT5NEVT8D2GETZ4DRF7C69XZHKHJ15296HRGB1R5ARNK0SP8A0" - assert(db.bankTransactionCreate(genTx(reservePubOne)) == Database.BankTransactionResult.SUCCESS) - assert(db.bankTransactionCreate(genTx(reservePubTwo)) == Database.BankTransactionResult.SUCCESS) + assert(db.bankTransactionCreate(genTx(reservePubOne)) == BankTransactionResult.SUCCESS) + assert(db.bankTransactionCreate(genTx(reservePubTwo)) == BankTransactionResult.SUCCESS) // Should not show up in the taler wire gateway API history - assert(db.bankTransactionCreate(genTx("bogus foobar")) == Database.BankTransactionResult.SUCCESS) + assert(db.bankTransactionCreate(genTx("bogus foobar")) == BankTransactionResult.SUCCESS) // Bar pays Foo once, but that should not appear in the result. assert( db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId = 2)) == - Database.BankTransactionResult.SUCCESS + BankTransactionResult.SUCCESS ) // Bar expects two entries in the incoming history testApplication { @@ -172,9 +173,9 @@ class TalerApiTest { val j: IncomingHistory = Json.decodeFromString(resp.bodyAsText()) assert(j.incoming_transactions.size == 2) // Testing ranges. - val mockReservePub = "X".repeat(52) + val mockReservePub = Base32Crockford.encode(ByteArray(32)) for (i in 1..400) - assert(db.bankTransactionCreate(genTx(mockReservePub)) == Database.BankTransactionResult.SUCCESS) + assert(db.bankTransactionCreate(genTx(mockReservePub)) == BankTransactionResult.SUCCESS) // forward range: val range = client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=10&start=30") { basicAuth("bar", "secret") diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql @@ -91,6 +91,7 @@ CREATE TABLE IF NOT EXISTS bank_accounts ,internal_payto_uri TEXT NOT NULL UNIQUE ,owning_customer_id BIGINT NOT NULL UNIQUE -- UNIQUE enforces 1-1 map with customers REFERENCES customers(customer_id) + ON DELETE CASCADE ,is_public BOOLEAN DEFAULT FALSE NOT NULL -- privacy by default ,is_taler_exchange BOOLEAN DEFAULT FALSE NOT NULL ,last_nexus_fetch_row_id BIGINT diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql @@ -86,6 +86,57 @@ END $$; COMMENT ON PROCEDURE bank_set_config(TEXT, TEXT) IS 'Update or insert configuration values'; +CREATE OR REPLACE FUNCTION customer_delete( + IN in_login TEXT, + OUT out_nx_customer BOOLEAN, + OUT out_balance_not_zero BOOLEAN +) +LANGUAGE plpgsql +AS $$ +DECLARE +my_customer_id BIGINT; +my_balance_val INT8; +my_balance_frac INT4; +BEGIN +-- check if login exists +SELECT customer_id + INTO my_customer_id + FROM customers + WHERE login = in_login; +IF NOT FOUND +THEN + out_nx_customer=TRUE; + RETURN; +END IF; +out_nx_customer=FALSE; + +-- get the balance +SELECT + (balance).val as balance_val, + (balance).frac as balance_frac + INTO + my_balance_val, + my_balance_frac + FROM bank_accounts + WHERE owning_customer_id = my_customer_id; +IF NOT FOUND +THEN + RAISE EXCEPTION 'Invariant failed: customer lacks bank account'; +END IF; +-- check that balance is zero. +IF my_balance_val != 0 OR my_balance_frac != 0 +THEN + out_balance_not_zero=TRUE; + RETURN; +END IF; +out_balance_not_zero=FALSE; + +-- actual deletion +DELETE FROM customers WHERE login = in_login; +END $$; +COMMENT ON FUNCTION customer_delete(TEXT) + IS 'Deletes a customer (and its bank account via cascade) if the balance is zero'; + CREATE OR REPLACE FUNCTION taler_transfer( IN in_request_uid TEXT, IN in_wtid TEXT, diff --git a/util/src/main/kotlin/Encoding.kt b/util/src/main/kotlin/Encoding.kt @@ -53,6 +53,10 @@ object Base32Crockford { return sb.toString() } + /** + * Decodes the input to its binary representation, throws + * net.taler.wallet.crypto.EncodingException on invalid encodings. + */ fun decode(encoded: String, out: ByteArrayOutputStream) { val size = encoded.length var bitpos = 0