Skip to content

Commit

Permalink
optimize BBQr
Browse files Browse the repository at this point in the history
add BBQr tests
  • Loading branch information
odudex committed Jun 12, 2024
1 parent 70c7383 commit f6e57cc
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 22 deletions.
13 changes: 12 additions & 1 deletion simulator/kruxsim/mocks/qrcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,25 @@
import sys
from unittest import mock
import pyqrcode
import re


def encode(data):
# Uses string encoded qr as it already cleaned up the frames
# PyQRcode also doesn't offer any binary output

def is_qr_alphanumeric(string):
return bool(re.match('^[A-Z0-9 $%*+\-./:]+$', string))

mode = "binary"
if isinstance(data, str):
if data.isnumeric():
mode = "numeric"
elif is_qr_alphanumeric(data):
mode = "alphanumeric"

try:
code_str = pyqrcode.create(data, error="L", mode="binary").text(quiet_zone=0)
code_str = pyqrcode.create(data, error="L", mode=mode).text(quiet_zone=0)
except:
# pre-decode if binary (SeedQR)
data = data.decode("latin-1")
Expand Down
25 changes: 14 additions & 11 deletions src/krux/bbqr.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
# This code an adaptation of Coinkite's BBQr python implementation for Krux environment
# https://github.com/coinkite/BBQr

import gc

# BBQR
# Human names
FILETYPE_NAMES = {
Expand Down Expand Up @@ -160,8 +162,8 @@ def encode_bbqr(data, encoding="Z", file_type="P"):
data = cmp

data = data.encode("utf-8") if isinstance(data, str) else data
data = base32_encode_stream(data).rstrip("=")
return BBQrCode(data, encoding, file_type)
gc.collect()
return BBQrCode("".join(base32_encode_stream(data)), encoding, file_type)


# Base 32 encoding/decoding, used in BBQR only
Expand Down Expand Up @@ -198,9 +200,8 @@ def base32_decode_stream(encoded_str):
return bytes(decoded_bytes)


def base32_encode_stream(data):
def base32_encode_stream(data, add_padding=False):
"""A streaming base32 encoder"""
encoded = []
buffer = 0
bits_left = 0

Expand All @@ -210,14 +211,16 @@ def base32_encode_stream(data):

while bits_left >= 5:
bits_left -= 5
encoded.append(B32CHARS[(buffer >> bits_left) & 0x1F])
yield B32CHARS[(buffer >> bits_left) & 0x1F]
buffer &= (1 << bits_left) - 1 # Keep only the remaining bits

if bits_left > 0:
buffer <<= 5 - bits_left
encoded.append(B32CHARS[buffer & 0x1F])

while len(encoded) % 8 != 0:
encoded.append("=") # Add padding

return "".join(encoded)
yield B32CHARS[buffer & 0x1F]

# Padding
if add_padding:
padding = 8 - (len(data) * 8 % 5)
if padding != 8:
for _ in range(padding):
yield "="
8 changes: 6 additions & 2 deletions src/krux/pages/home_pages/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,12 @@ def sign_psbt(self):
qr_format = FORMAT_PMOFN if qr_format == FORMAT_NONE else qr_format
from ...psbt import PSBTSigner

# Warns in case of path mismatch
signer = PSBTSigner(self.ctx.wallet, data, qr_format, psbt_filename)

del data
gc.collect()

# Warns in case of path mismatch
path_mismatch = signer.path_mismatch()
if path_mismatch:
self.ctx.display.clear()
Expand Down Expand Up @@ -347,7 +351,7 @@ def sign_psbt(self):
self.ctx.input.wait_for_button()

# memory management
del data, outputs
del outputs
gc.collect()

index = self._sign_menu()
Expand Down
45 changes: 37 additions & 8 deletions src/krux/qr.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
# List of capacities, based on versions
# Version 1(index 0)=21x21px = 17 bytes, version 2=25x25px = 32 bytes ...
# Limited to version 20
QR_CAPACITY = [
QR_CAPACITY_BYTE = [
17,
32,
53,
Expand All @@ -73,6 +73,29 @@
858,
]

QR_CAPACITY_ALPHANUMERIC = [
25,
47,
77,
114,
154,
195,
224,
279,
335,
395,
468,
535,
619,
667,
758,
854,
938,
1046,
1153,
1249,
]


class QRPartParser:
"""Responsible for parsing either a singular or animated series of QR codes
Expand Down Expand Up @@ -245,23 +268,29 @@ def get_size(qr_code):
return int(size)


def max_qr_bytes(max_width):
def max_qr_bytes(max_width, encoding="byte"):
"""Calculates the maximum length, in bytes, a QR code of a given size can store"""
# Given qr_size = 17 + 4 * version + 2 * frame_size
# Given qr_size = 17 + 4 * version + 2 * frame_size
max_width -= 2 # Subtract frame width
qr_version = (max_width - 17) // 4
if encoding == "alphanumeric":
capacity_list = QR_CAPACITY_ALPHANUMERIC
else:
capacity_list = QR_CAPACITY_BYTE

try:
return QR_CAPACITY[qr_version - 1]
return capacity_list[qr_version - 1]
except:
# Limited to version 20
return QR_CAPACITY[-1]
return capacity_list[-1]


def find_min_num_parts(data, max_width, qr_format):
"""Finds the minimum number of QR parts necessary to encode the data in
the specified format within the max_width constraint
"""
qr_capacity = max_qr_bytes(max_width)
encoding = "alphanumeric" if qr_format == FORMAT_BBQR else "byte"
qr_capacity = max_qr_bytes(max_width, encoding)
if qr_format == FORMAT_PMOFN:
data_length = len(data)
part_size = qr_capacity - PMOFN_PREFIX_LENGTH_1D
Expand Down Expand Up @@ -295,8 +324,8 @@ def find_min_num_parts(data, max_width, qr_format):

# Calculate the optimal part size to make it a multiple of 8
part_size = (data_length + num_parts - 1) // num_parts
# Adjust to the nearest higher multiple of 8
part_size += 7 - (part_size - 1) % 8
# Adjust to the nearest lower multiple of 8
part_size = (part_size // 8) * 8

# Recalculate the number of parts with the adjusted part_size
num_parts = (data_length + part_size - 1) // part_size
Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from Crypto.Cipher import AES
import pytest
from .shared_mocks import (
DeflateIO,
board_amigo_tft,
board_dock,
board_m5stickv,
Expand Down Expand Up @@ -54,6 +55,7 @@ def mp_modules(mocker, monkeypatch):
"uos",
mocker.MagicMock(statvfs=statvfs),
)
monkeypatch.setitem(sys.modules, "deflate", mocker.MagicMock(DeflateIO=DeflateIO))


@pytest.fixture
Expand Down
26 changes: 26 additions & 0 deletions tests/shared_mocks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
from unittest import mock
import pyqrcode
import zlib


class DeflateIO:
def __init__(self, stream) -> None:
self.stream = stream
self.data = stream.read()

def read(self):
return zlib.decompress(self.data, wbits=-10)

def write(self, input_data):
compressor = zlib.compressobj(wbits=-10)
compressed_data = compressor.compress(input_data)
compressed_data += compressor.flush()
self.stream.seek(0) # Ensure we overwrite the stream from the beginning
self.stream.write(compressed_data)
self.stream.truncate() # Remove any remaining part of the old data

def __enter__(self):
# Return the instance itself when entering the context
return self

def __exit__(self, exc_type, exc_val, exc_tb):
# Handle cleanup here if necessary
pass


def encode_to_string(data):
Expand Down
Loading

0 comments on commit f6e57cc

Please sign in to comment.