Work to optimize DatumTool: don't die on contents >30K (#731)
What changed, and why it matters
This commit improves a file-viewing tool in Krux (a Bitcoin hardware-wallet firmware) so it no longer crashes when opening large files. It adds memory cleanup, caps expensive encoding checks to small samples, and shows a user-friendly error if a format conversion fails. There is no direct evidence this fixes a security vulnerability; it appears to be a robustness and user-experience improvement.
Treat as a routine robustness improvement. Reviewers may want to verify that the new sample-based encoding detection does not misclassify crafted inputs (e.g., a large file with a valid bech32 prefix but invalid body), and that the conversion-failure path does not leave stale sensitive data in self.history.
Security signals we found
Memory pressure / watchdog reset mitigation on resource-constrained device
User-visible error handling added for conversion failures
Input sampling limits applied to expensive parsing operations
No explicit security claim in commit message or changelog
Evidence from the diff
The patch refactors src/krux/pages/datum_tool.py to handle large (>30 KB, up to ~100 KB) contents in the DatumTool. Key changes: (1) detect_encodings now samples only the first 512 bytes for min/max character checks and skips slow bech32/base43/base58 verification for inputs larger than 16 KB; (2) identify_datum accepts a precomputed encodings list to avoid re-running detection; (3) _show_contents renders content in page-sized slices instead of converting the entire bytes object to a hex string up front; (4) convert_encoding failures are now surfaced with a new ‘Failed to convert’ message instead of silently setting contents to None; (5) gc.collect() and wdt.feed() calls are added to reduce memory pressure and prevent watchdog resets. The remaining diff is translation strings and tests.
Changed components
src/krux/pages/datum_tool.pytests/pages/test_datum_tool.pyi18n translation filesInspect captured patch +166 / −39
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9834443..34fc00a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@ The Maix Bit device has long been discouraged due to its poor-quality camera. St
- Touchscreen test added in Tools for detection check
- wbits for deflate-decompress window set to 10 bits to match KEF spec.
- Remove "Reboot" option and status bar, when empty, from Login menu
-- Optimized Datum show: total pages are no longer visible, and navigation no longer wraps from the first page to the last or vice versa
+- Optimized Datum show: total pages are no longer visible, and navigation no longer wraps from the first page to the last or vice versa; better memory management and handling for large (~100K) files.
- Enabled swipe up/down gestures on keypads, menus, and QR transcribe
# Changelog 25.09.0 - September 2025
diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json
index eabecb1..52b000f 100644
--- a/i18n/translations/de-DE.json
+++ b/i18n/translations/de-DE.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Öffentlicher Schlüssel",
"Factory Settings": "Werkeinstellungen",
"Failed gathering camera entropy": "Fehler beim Sammeln der Kameraentropie",
+ "Failed to convert": "Daten konnten nicht umgewandelt werden.",
"Failed to decrypt": "Entschlüsselung fehlgeschlagen",
"Failed to load": "Laden fehlgeschlagen.",
"Failed to store mnemonic": "Mnemonic konnte nicht gespeichert werden",
diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json
index dc3488e..c74700c 100644
--- a/i18n/translations/es-MX.json
+++ b/i18n/translations/es-MX.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Clave Pública Extendida",
"Factory Settings": "Ajustes de Fábrica",
"Failed gathering camera entropy": "Error al recopilar la entropía de la cámara",
+ "Failed to convert": "Error al convertir:",
"Failed to decrypt": "Error al descifrar",
"Failed to load": "Error al cargar",
"Failed to store mnemonic": "No pudo almacenar mnemónico",
diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json
index 320aa6c..d51dcf0 100644
--- a/i18n/translations/fr-FR.json
+++ b/i18n/translations/fr-FR.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Clé publique",
"Factory Settings": "Paramètres d'usine",
"Failed gathering camera entropy": "Échec de la collecte de l'entropie de la caméra",
+ "Failed to convert": "Échec de la conversion",
"Failed to decrypt": "Échec du déchiffrement",
"Failed to load": "Échec lors du chargement",
"Failed to store mnemonic": "Échec du stockage mnémonique",
diff --git a/i18n/translations/ja-JP.json b/i18n/translations/ja-JP.json
index 8079272..fe8dc2a 100644
--- a/i18n/translations/ja-JP.json
+++ b/i18n/translations/ja-JP.json
@@ -99,6 +99,7 @@
"Extended Public Key": "拡張公開キー",
"Factory Settings": "初期設定",
"Failed gathering camera entropy": "カメラエントロピーの収集に失敗しました",
+ "Failed to convert": "\"CRL変換に失敗\"",
"Failed to decrypt": "復号化に失敗しました",
"Failed to load": "ロードに失敗しました",
"Failed to store mnemonic": "mnemonicの保存に失敗しました",
diff --git a/i18n/translations/ko-KR.json b/i18n/translations/ko-KR.json
index 919298e..49fd789 100644
--- a/i18n/translations/ko-KR.json
+++ b/i18n/translations/ko-KR.json
@@ -99,6 +99,7 @@
"Extended Public Key": "XPUB 내보내기",
"Factory Settings": "공장 초기 설정",
"Failed gathering camera entropy": "카메라 엔트로피 수집 실패",
+ "Failed to convert": "변환 실패",
"Failed to decrypt": "복호화에 실패했습니다",
"Failed to load": "로드 실패",
"Failed to store mnemonic": "니모닉을 저장하지 못했습니다",
diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json
index ef8a6d6..9252f38 100644
--- a/i18n/translations/nl-NL.json
+++ b/i18n/translations/nl-NL.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Uitgebreide publieke sleutel",
"Factory Settings": "Fabrieksinstellingen",
"Failed gathering camera entropy": "Verzamelen van camera entropie mislukt",
+ "Failed to convert": "Kon niet geconverteerd worden",
"Failed to decrypt": "Ontsleutelen is niet gelukt",
"Failed to load": "Laden mislukt",
"Failed to store mnemonic": "Geheugensteun opslaan is niet gelukt",
diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json
index 069ea4f..28880cc 100644
--- a/i18n/translations/pt-BR.json
+++ b/i18n/translations/pt-BR.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Chave Pública Estendida",
"Factory Settings": "Configurações de Fábrica",
"Failed gathering camera entropy": "Falha ao capturar entropia da câmera",
+ "Failed to convert": "Falha ao converter",
"Failed to decrypt": "Falha ao descriptografar",
"Failed to load": "Falha ao carregar",
"Failed to store mnemonic": "Falha ao armazenar mnemônico",
diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json
index 34723a5..58ef802 100644
--- a/i18n/translations/ru-RU.json
+++ b/i18n/translations/ru-RU.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Расширенный Публичный Ключ",
"Factory Settings": "Заводские Настройки",
"Failed gathering camera entropy": "Не удалось собрать энтропию камеры",
+ "Failed to convert": "Преобразование не выполнено.",
"Failed to decrypt": "Не удалось расшифровать",
"Failed to load": "Не удалось ничего загрузить",
"Failed to store mnemonic": "Не удалось сохранить мнемонику",
diff --git a/i18n/translations/tr-TR.json b/i18n/translations/tr-TR.json
index 715dfef..c0bf4cd 100644
--- a/i18n/translations/tr-TR.json
+++ b/i18n/translations/tr-TR.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Genişletilmiş Public Key",
"Factory Settings": "Fabrika Ayarları",
"Failed gathering camera entropy": "Kamera entropisi toplanamadı",
+ "Failed to convert": "Veri dönüştürme başarısız.",
"Failed to decrypt": "Şifre çözme başarısız",
"Failed to load": "Yüklenemedi",
"Failed to store mnemonic": "Mnemonic depolama başarısız",
diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json
index da7e97b..9ee3a80 100644
--- a/i18n/translations/vi-VN.json
+++ b/i18n/translations/vi-VN.json
@@ -99,6 +99,7 @@
"Extended Public Key": "Khóa công cộng",
"Factory Settings": "Cài đặt Gốc",
"Failed gathering camera entropy": "Không thể thu thập entropy của máy ảnh",
+ "Failed to convert": "Không thể chuyển đổi",
"Failed to decrypt": "Không giải mã được",
"Failed to load": "Tải thất bại",
"Failed to store mnemonic": "Không lưu trữ Mnemonic được",
diff --git a/i18n/translations/zh-CN.json b/i18n/translations/zh-CN.json
index 2eb4f33..924fb1b 100644
--- a/i18n/translations/zh-CN.json
+++ b/i18n/translations/zh-CN.json
@@ -99,6 +99,7 @@
"Extended Public Key": "扩展公钥",
"Factory Settings": "出厂设置",
"Failed gathering camera entropy": "收集摄像头熵失败",
+ "Failed to convert": "转换失败.",
"Failed to decrypt": "解密失败",
"Failed to load": "加载失败",
"Failed to store mnemonic": "存储助记词失败",
diff --git a/src/krux/pages/datum_tool.py b/src/krux/pages/datum_tool.py
index 97a1d27..95663db 100644
--- a/src/krux/pages/datum_tool.py
+++ b/src/krux/pages/datum_tool.py
@@ -20,6 +20,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+import gc
+from krux.wdt import wdt
from . import (
Page,
Menu,
@@ -70,6 +72,8 @@ DATUM_BBQR_TYPES = {
}
STATIC_QR_MAX_SIZE = 4 # version 5 - 37x37
+SUFFICIENT_SAMPLE_SIZE = 512 # to truncate large contents for sampling
+SLOW_ENCODING_MAX_SIZE = 2**14 # base43,base58,bech32 not offered above this size
def urobj_to_data(ur_obj):
@@ -129,17 +133,17 @@ def convert_encoding(contents, conversion):
return None
-def identify_datum(data):
+def identify_datum(data, encodings=None):
"""Determine which "datum" type this is; ie: PSBT, XPUB, DESC, ADDR"""
# TODO: more samples and fewer false-positives
-
datum = None
if isinstance(data, bytes):
if data[:5] == b"psbt\xff":
datum = DATUM_PSBT
elif len(data) > 33:
- encodings = detect_encodings(data)
+ if encodings is None:
+ encodings = detect_encodings(data)
if data[:1] in "xyzYZtuvUV" and data[1:4] == "pub" and 58 in encodings:
datum = DATUM_XPUB
@@ -153,7 +157,7 @@ def identify_datum(data):
datum = DATUM_DESCRIPTOR
elif (data[:1] in ("1", "3", "n", "2", "m") and 58 in encodings) or (
data[:4].lower() in ("bc1p", "bc1q", "tb1p", "tb1q")
- and "bech32" in [x.lower()[:6] for x in encodings]
+ and "bech32" in [x.lower()[:6] for x in encodings if isinstance(x, str)]
):
datum = DATUM_ADDRESS
@@ -177,14 +181,15 @@ def detect_encodings(str_data, verify=True):
if not isinstance(str_data, str):
raise TypeError("detect_encodings() expected str")
- # get min and max characters (sorted by ordinal value),
- # check most restrictive encodings first
+ str_len = len(str_data)
- min_chr = min(str_data)
- max_chr = max(str_data)
+ # get min and max characters (sorted by ordinal value)
+ # then check most restrictive encodings first
+ min_chr = min(str_data[:SUFFICIENT_SAMPLE_SIZE])
+ max_chr = max(str_data[:SUFFICIENT_SAMPLE_SIZE])
# might it be hex
- if len(str_data) % 2 == 0 and "0" <= min_chr:
+ if str_len % 2 == 0 and "0" <= min_chr:
if max_chr <= "F":
if verify:
try:
@@ -216,11 +221,12 @@ def detect_encodings(str_data, verify=True):
encodings.append(32)
# might it be bech32
- if "0" <= min_chr:
+ if str_len <= SLOW_ENCODING_MAX_SIZE and "0" <= min_chr:
encoding = None
if max_chr <= "Z":
if verify:
encoding, _, _ = bech32_decode(str_data)
+ wdt.feed()
if encoding == Encoding.BECH32:
encodings.append("BECH32")
elif encoding == Encoding.BECH32M:
@@ -234,10 +240,11 @@ def detect_encodings(str_data, verify=True):
encodings.append("bech32m")
# might it be base43
- if "$" <= min_chr and max_chr <= "Z":
+ if str_len <= SLOW_ENCODING_MAX_SIZE and "$" <= min_chr and max_chr <= "Z":
if verify:
try:
base_decode(str_data, 43)
+ wdt.feed()
encodings.append(43)
except:
pass
@@ -245,10 +252,11 @@ def detect_encodings(str_data, verify=True):
encodings.append(43)
# might it be base58
- if "1" <= min_chr and max_chr <= "z":
+ if str_len <= SLOW_ENCODING_MAX_SIZE and "1" <= min_chr and max_chr <= "z":
if verify:
try:
base_decode(str_data, 58)
+ wdt.feed()
encodings.append(58)
except:
pass
@@ -263,6 +271,7 @@ def detect_encodings(str_data, verify=True):
as_bytes = base_decode(str_data, 64)
if base_encode(as_bytes, 64) == str_data:
encodings.append(64)
+ del as_bytes
except:
pass
else:
@@ -279,6 +288,8 @@ def detect_encodings(str_data, verify=True):
# assume utf8
encodings.append("utf8")
+ gc.collect()
+
return encodings
@@ -353,7 +364,6 @@ class DatumToolMenu(Page):
from .utils import Utils
if not self.has_sd_card():
- self.ctx.display.clear()
self.flash_error(t("SD card not detected."))
return MENU_CONTINUE
@@ -366,6 +376,9 @@ class DatumToolMenu(Page):
if not contents:
return MENU_CONTINUE
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(t("Processing…"))
+
# utils.load_file() always returns binary
try:
contents = contents.decode()
@@ -411,9 +424,9 @@ class DatumTool(Page):
seedqrview_thresh = QR_CAPACITY_BYTE[STATIC_QR_MAX_SIZE]
if not isinstance(self.contents, bytes):
- if all(c.isdigit() for c in self.contents):
+ if all(c.isdigit() for c in self.contents[:SUFFICIENT_SAMPLE_SIZE]):
seedqrview_thresh = QR_CAPACITY_NUMERIC[STATIC_QR_MAX_SIZE]
- elif all(is_alnum(c) for c in self.contents):
+ elif all(is_alnum(c) for c in self.contents[:SUFFICIENT_SAMPLE_SIZE]):
seedqrview_thresh = QR_CAPACITY_ALPHANUMERIC[STATIC_QR_MAX_SIZE]
if len(self.contents) <= seedqrview_thresh:
@@ -451,10 +464,10 @@ class DatumTool(Page):
else:
from ..qr import FORMAT_NONE, FORMAT_PMOFN, FORMAT_BBQR, FORMAT_UR
- menu_opts = [
- (t("Static"), (FORMAT_NONE,)),
- (t("Part M of N"), (FORMAT_PMOFN,)),
- ]
+ menu_opts = []
+ if len(self.contents) <= seedqrview_thresh * 4:
+ menu_opts.append((t("Static"), (FORMAT_NONE,)))
+ menu_opts.append((t("Part M of N"), (FORMAT_PMOFN,)))
if self.datum in DATUM_BBQR_TYPES:
menu_opts.extend(
@@ -464,7 +477,7 @@ class DatumTool(Page):
]
)
- curr_datum = identify_datum(self.contents)
+ curr_datum = identify_datum(self.contents, self.encodings)
if curr_datum:
if curr_datum in DATUM_UR_TYPES:
menu_opts.extend(
@@ -553,12 +566,13 @@ class DatumTool(Page):
),
]
if preview:
+ preview_contents = self.contents[:SUFFICIENT_SAMPLE_SIZE]
parts.append(
self.fit_to_line(
(
- '"' + self.contents + '"'
- if isinstance(self.contents, str)
- else "0x" + hexlify(self.contents).decode()
+ '"' + preview_contents + '"'
+ if isinstance(preview_contents, str)
+ else "0x" + hexlify(preview_contents).decode()
),
crop_middle=False,
)
@@ -574,6 +588,7 @@ class DatumTool(Page):
def _show_contents(self):
"""Displays infobox and contents"""
from binascii import hexlify
+ from ..settings import ELLIPSIS
from ..kboard import kboard
page_indicator = "p.%d"
@@ -583,12 +598,7 @@ class DatumTool(Page):
if not kboard.is_m5stickv
else (self.ctx.display.width() % FONT_WIDTH) // 2
)
- contents = (
- self.contents
- if isinstance(self.contents, str)
- else hexlify(self.contents).decode()
- )
- content_len = len(contents)
+ content_len = len(self.contents)
def _update_infobox(curr_page):
info_len = self._info_box(
@@ -603,9 +613,17 @@ class DatumTool(Page):
while True:
info_len, max_lines = _update_infobox(curr_page + 1)
- lines, endpos = self.ctx.display.to_lines_endpos(
- contents[start_index:], max_lines
- )
+ chars_per_page = self.ctx.display.width() // FONT_WIDTH * max_lines
+ contents = self.contents[start_index : start_index + chars_per_page + 1]
+ if isinstance(contents, bytes):
+ contents = hexlify(contents).decode()[: chars_per_page + 1]
+ lines, endpos = self.ctx.display.to_lines_endpos(contents, max_lines)
+ if isinstance(self.contents, bytes):
+ if endpos % 2:
+ endpos -= 1
+ if lines[-1][-1] == ELLIPSIS:
+ lines[-1] = lines[-1][:-2] + ELLIPSIS * 2
+ endpos = endpos // 2
endpos += start_index
if pages[-1] < endpos < content_len:
pages.append(endpos)
@@ -642,6 +660,8 @@ class DatumTool(Page):
try:
as_str = self.contents.decode()
suggestion = str(detect_encodings(as_str, False)[0])
+ del as_str
+ gc.collect()
if suggestion != "utf8":
suggestion = suggestion + "_via_utf8"
self.encodings = [suggestion + "?"]
@@ -664,12 +684,13 @@ class DatumTool(Page):
self.encodings = detect_encodings(self.contents)
# does it look like a 12 or 24 word mnemonic / Mnemonic QR?
- if len(self.contents.split()) in (12, 24):
+ if len(self.contents[:256].split()) in (12, 24):
self.sensitive = True
# does it look like a 12 or 24 word decimal mnemonic / StandadardSeedQR?
- elif len(set((x in "0123456789" for x in self.contents))) == 1 and (
+ elif (
len(self.contents) in (12 * 4, 24 * 4)
+ and len(set((x in "0123456789" for x in self.contents))) == 1
):
self.sensitive = True
@@ -681,7 +702,7 @@ class DatumTool(Page):
# datum
if not self.datum:
- self.datum = identify_datum(self.contents)
+ self.datum = identify_datum(self.contents, self.encodings)
def _decrypt_as_kef_envelope(self):
"""Assuming self.contents are encrypted, offer to decrypt"""
@@ -726,7 +747,9 @@ class DatumTool(Page):
else:
menu.append((t("to hex"), lambda: "hex"))
menu.append((t("to base32"), lambda: 32))
- menu.append((t("to base43"), lambda: 43))
+ if len(self.contents) <= SLOW_ENCODING_MAX_SIZE * 5.42 / 8:
+ # 5.42 slightly less than log2(43); adjusts for base43 bloat
+ menu.append((t("to base43"), lambda: 43))
menu.append((t("to base64"), lambda: 64))
try:
self.contents.decode()
@@ -770,6 +793,8 @@ class DatumTool(Page):
"""allows to view, convert, encrypt/decrypt, and export short str/bytes contents"""
from .encryption_ui import KEFEnvelope
+ gc.collect()
+
kvargs = {
"try_decrypt": try_decrypt,
"offer_convert": offer_convert,
@@ -806,6 +831,9 @@ class DatumTool(Page):
# if user chose to exit
return MENU_CONTINUE
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(t("Processing…"))
+
if status == "show":
# if user wants to view data
self._show_contents()
@@ -817,11 +845,21 @@ class DatumTool(Page):
kvargs["offer_convert"] = False
elif status in ("undo", "hex", "HEX", 32, 43, 64, "utf8", "shift_case"):
# if user chose a particular conversion
+ undo = False
if status == "undo":
+ undo = True
status = self.history.pop()
+ new_contents = convert_encoding(self.contents, status)
+ if new_contents is not None:
+ self.contents = new_contents
+ if not undo:
+ self.history.append(status)
+ del new_contents
else:
- self.history.append(status)
- self.contents = convert_encoding(self.contents, status)
+ if undo:
+ self.history.append(status)
+ self.flash_error(t("Failed to convert"))
+
elif status == "encrypt":
# if user chose to encrypt
kef = KEFEnvelope(self.ctx)
diff --git a/src/krux/translations/__init__.py b/src/krux/translations/__init__.py
index ff5987c..0b38b7c 100644
--- a/src/krux/translations/__init__.py
+++ b/src/krux/translations/__init__.py
@@ -133,6 +133,7 @@ ref_array = [
1711312434,
3981762528,
44497516,
+ 572371442,
383371114,
3215045701,
2946146830,
diff --git a/src/krux/translations/de.py b/src/krux/translations/de.py
index 9c131ad..9970e8a 100644
--- a/src/krux/translations/de.py
+++ b/src/krux/translations/de.py
@@ -121,6 +121,7 @@ translation_array = [
"Öffentlicher Schlüssel",
"Werkeinstellungen",
"Fehler beim Sammeln der Kameraentropie",
+ "Daten konnten nicht umgewandelt werden.",
"Entschlüsselung fehlgeschlagen",
"Laden fehlgeschlagen.",
"Mnemonic konnte nicht gespeichert werden",
diff --git a/src/krux/translations/es.py b/src/krux/translations/es.py
index 63cf180..114f3ba 100644
--- a/src/krux/translations/es.py
+++ b/src/krux/translations/es.py
@@ -121,6 +121,7 @@ translation_array = [
"Clave Pública Extendida",
"Ajustes de Fábrica",
"Error al recopilar la entropía de la cámara",
+ "Error al convertir:",
"Error al descifrar",
"Error al cargar",
"No pudo almacenar mnemónico",
diff --git a/src/krux/translations/fr.py b/src/krux/translations/fr.py
index 1c4f858..1358e45 100644
--- a/src/krux/translations/fr.py
+++ b/src/krux/translations/fr.py
@@ -121,6 +121,7 @@ translation_array = [
"Clé publique",
"Paramètres d'usine",
"Échec de la collecte de l'entropie de la caméra",
+ "Échec de la conversion",
"Échec du déchiffrement",
"Échec lors du chargement",
"Échec du stockage mnémonique",
diff --git a/src/krux/translations/ja.py b/src/krux/translations/ja.py
index 7b0bb7e..0b01479 100644
--- a/src/krux/translations/ja.py
+++ b/src/krux/translations/ja.py
@@ -121,6 +121,7 @@ translation_array = [
"拡張公開キー",
"初期設定",
"カメラエントロピーの収集に失敗しました",
+ '"CRL変換に失敗"',
"復号化に失敗しました",
"ロードに失敗しました",
"mnemonicの保存に失敗しました",
diff --git a/src/krux/translations/ko.py b/src/krux/translations/ko.py
index cacfb18..e08303a 100644
--- a/src/krux/translations/ko.py
+++ b/src/krux/translations/ko.py
@@ -121,6 +121,7 @@ translation_array = [
"XPUB 내보내기",
"공장 초기 설정",
"카메라 엔트로피 수집 실패",
+ "변환 실패",
"복호화에 실패했습니다",
"로드 실패",
"니모닉을 저장하지 못했습니다",
diff --git a/src/krux/translations/nl.py b/src/krux/translations/nl.py
index adec580..2f6ce75 100644
--- a/src/krux/translations/nl.py
+++ b/src/krux/translations/nl.py
@@ -121,6 +121,7 @@ translation_array = [
"Uitgebreide publieke sleutel",
"Fabrieksinstellingen",
"Verzamelen van camera entropie mislukt",
+ "Kon niet geconverteerd worden",
"Ontsleutelen is niet gelukt",
"Laden mislukt",
"Geheugensteun opslaan is niet gelukt",
diff --git a/src/krux/translations/pt.py b/src/krux/translations/pt.py
index 106e96f..5bd8ed4 100644
--- a/src/krux/translations/pt.py
+++ b/src/krux/translations/pt.py
@@ -121,6 +121,7 @@ translation_array = [
"Chave Pública Estendida",
"Configurações de Fábrica",
"Falha ao capturar entropia da câmera",
+ "Falha ao converter",
"Falha ao descriptografar",
"Falha ao carregar",
"Falha ao armazenar mnemônico",
diff --git a/src/krux/translations/ru.py b/src/krux/translations/ru.py
index 78167ef..b90a392 100644
--- a/src/krux/translations/ru.py
+++ b/src/krux/translations/ru.py
@@ -121,6 +121,7 @@ translation_array = [
"Расширенный Публичный Ключ",
"Заводские Настройки",
"Не удалось собрать энтропию камеры",
+ "Преобразование не выполнено.",
"Не удалось расшифровать",
"Не удалось ничего загрузить",
"Не удалось сохранить мнемонику",
diff --git a/src/krux/translations/tr.py b/src/krux/translations/tr.py
index 21c1f69..d485662 100644
--- a/src/krux/translations/tr.py
+++ b/src/krux/translations/tr.py
@@ -121,6 +121,7 @@ translation_array = [
"Genişletilmiş Public Key",
"Fabrika Ayarları",
"Kamera entropisi toplanamadı",
+ "Veri dönüştürme başarısız.",
"Şifre çözme başarısız",
"Yüklenemedi",
"Mnemonic depolama başarısız",
diff --git a/src/krux/translations/vi.py b/src/krux/translations/vi.py
index bb098ab..2eb51c6 100644
--- a/src/krux/translations/vi.py
+++ b/src/krux/translations/vi.py
@@ -121,6 +121,7 @@ translation_array = [
"Khóa công cộng",
"Cài đặt Gốc",
"Không thể thu thập entropy của máy ảnh",
+ "Không thể chuyển đổi",
"Không giải mã được",
"Tải thất bại",
"Không lưu trữ Mnemonic được",
diff --git a/src/krux/translations/zh.py b/src/krux/translations/zh.py
index 993f874..babb8bd 100644
--- a/src/krux/translations/zh.py
+++ b/src/krux/translations/zh.py
@@ -121,6 +121,7 @@ translation_array = [
"扩展公钥",
"出厂设置",
"收集摄像头熵失败",
+ "转换失败.",
"解密失败",
"加载失败",
"存储助记词失败",
diff --git a/tests/pages/test_datum_tool.py b/tests/pages/test_datum_tool.py
index 298972d..0f9912c 100644
--- a/tests/pages/test_datum_tool.py
+++ b/tests/pages/test_datum_tool.py
@@ -1066,6 +1066,7 @@ def test_datumtool_view_contents(m5stickv, mocker, mock_file_operations):
def test_datumtool_view_contents_multi_page(m5stickv, mocker):
"""simply to cover building of `pages` index, moving to `next page`, and `prev page`"""
+ from binascii import hexlify
from krux.pages.datum_tool import DatumTool
from krux.input import PRESSED, BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
@@ -1092,6 +1093,71 @@ def test_datumtool_view_contents_multi_page(m5stickv, mocker):
page.view_contents()
assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ # call with bytes that will span more than one page
+ BTN_SEQUENCE = [
+ BUTTON_ENTER, # go Show Datum
+ BUTTON_ENTER, # escape Show Datum
+ BUTTON_PAGE_PREV, # to Back
+ BUTTON_ENTER, # go Back
+ ]
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ ctx.display.to_lines_endpos = mocker.MagicMock(
+ side_effect=[
+ (
+ [hexlify(b"8 bytes.").decode() for _ in range(15)]
+ + ["382062797465732…"],
+ 127,
+ ),
+ ([hexlify(b".8 bytes").decode() for _ in range(4)] + ["2e"], 33),
+ ]
+ )
+ page = DatumTool(ctx)
+ page.contents = b"8 bytes." * 20
+ page.title = "title"
+ page.view_contents()
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+
+
+def test_datumtool_view_contents_convert_failure(mocker, m5stickv):
+ """Contents cannot always be converted (ie: mem-alloc-err); flash_error"""
+ from krux.pages.datum_tool import DatumTool
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ # mock failure in convert_encoding so it returns None
+ mocker.patch(
+ "binascii.unhexlify",
+ new=mocker.MagicMock(
+ side_effect=[
+ b"\xde\xad\xbe\xef", # first call to unhexlify works, during detect_encoding()
+ Exception(
+ "mocked failure"
+ ), # second call fails, during convert_encoding()
+ ]
+ ),
+ )
+ from binascii import unhexlify
+
+ # call with bytes to be hexlified that will fail to convert via mocked unhexlify
+ BTN_SEQUENCE = (
+ BUTTON_ENTER, # go Convert
+ BUTTON_ENTER, # go "to hex"
+ BUTTON_ENTER, # go "from hex", mocked failure
+ BUTTON_PAGE_PREV, # to Done Converting
+ BUTTON_ENTER, # go Done Converting
+ BUTTON_PAGE_PREV, # to Back
+ BUTTON_ENTER, # go Back
+ )
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ page = DatumTool(ctx)
+ page.contents = b"\xde\xad\xbe\xef"
+ page.title = "title"
+ page.view_contents()
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert ctx.display.flash_text.call_count == 1
+ ctx.display.flash_text.assert_called_with(
+ "Failed to convert", 248, 2000, highlight_prefix=""
+ )
+
def test_datumtool_show_contents_button_turbo(mocker, m5stickv):
from krux.pages.datum_tool import DatumTool
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.