1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 15:38:11 +00:00

feat(core/rust/ui): on-the-fly conversion of bytes to hexadecimal representation

[no changelog]
This commit is contained in:
Martin Milata 2022-10-19 18:06:13 +02:00
parent 4c7060186a
commit 3b92923caa
8 changed files with 349 additions and 123 deletions

View File

@ -168,6 +168,9 @@ fn generate_micropython_bindings() {
.allowlist_var("MP_BUFFER_WRITE")
.allowlist_var("MP_BUFFER_RW")
.allowlist_var("mp_type_str")
.allowlist_var("mp_type_bytes")
.allowlist_var("mp_type_bytearray")
.allowlist_var("mp_type_memoryview")
// dict
.allowlist_type("mp_obj_dict_t")
.allowlist_function("mp_obj_new_dict")

View File

@ -66,7 +66,7 @@ impl TryFrom<Obj> for StrBuffer {
type Error = Error;
fn try_from(obj: Obj) -> Result<Self, Self::Error> {
if obj.is_qstr() || unsafe { ffi::mp_type_str.is_type_of(obj) } {
if obj.is_str() {
let bufinfo = get_buffer_info(obj, ffi::MP_BUFFER_READ)?;
let new = Self {
ptr: bufinfo.buf as _,
@ -189,3 +189,25 @@ impl crate::trace::Trace for StrBuffer {
self.as_ref().trace(t)
}
}
/// Version of `get_buffer` for strings that ties the string lifetime with
/// another object that is expected to be placed on stack or in micropython heap
/// and be the beginning of a chain of references that lead to the `obj`.
///
/// SAFETY:
/// The caller must ensure that:
/// (a) the `_owner` is an object visible to the micropython GC,
/// (b) the `_owner` contains a reference that leads to `obj`, possibly through
/// other objects,
/// (c) that path is not broken as long as the returned reference lives.
pub unsafe fn get_str_owner<T: ?Sized>(_owner: &T, obj: Obj) -> Result<&str, Error> {
if !obj.is_str() {
return Err(Error::TypeError);
}
// SAFETY:
// (a) only immutable references are taken.
// (b) micropython guarantees immutability and the buffer is not freed/moved as
// long as _owner satisfies precondition.
let buffer = unsafe { get_buffer(obj)? };
str::from_utf8(buffer).map_err(|_| Error::TypeError)
}

View File

@ -410,3 +410,18 @@ impl Obj {
}
}
}
impl Obj {
pub fn is_bytes(self) -> bool {
unsafe {
ffi::mp_type_bytes.is_type_of(self)
|| ffi::mp_type_bytearray.is_type_of(self)
|| ffi::mp_type_memoryview.is_type_of(self)
}
}
pub fn is_str(self) -> bool {
let is_type_str = unsafe { ffi::mp_type_str.is_type_of(self) };
is_type_str || self.is_qstr()
}
}

View File

@ -24,8 +24,23 @@ pub const PARAGRAPH_BOTTOM_SPACE: i16 = 5;
pub type ParagraphVecLong<T> = Vec<Paragraph<T>, 32>;
pub type ParagraphVecShort<T> = Vec<Paragraph<T>, 8>;
/// Maximum number of characters that can be displayed on screen at once. Used
/// for on-the-fly conversion of binary data to hexadecimal representation.
/// NOTE: can be fine-tuned for particular model screen to decrease memory
/// consumption and conversion time.
pub const SCRATCH_BUFFER_LEN: usize = 256;
pub trait ParagraphSource {
fn at(&self, i: usize) -> Paragraph<&str>;
/// Return text and associated style for given paragraph index and character
/// offset within the paragraph.
///
/// Implementations can use the provided buffer to perform some kind of
/// conversion (currently used for displaying binary data in
/// hexadecimal) and return a reference to this buffer. Caller needs to
/// make sure the buffer is large enough to fit screenfull of characters.
fn at<'a>(&'a self, index: usize, offset: usize, buffer: &'a mut [u8]) -> Paragraph<&'a str>;
/// Number of paragraphs.
fn size(&self) -> usize;
fn into_paragraphs(self) -> Paragraphs<Self>
@ -40,8 +55,8 @@ impl<T, const N: usize> ParagraphSource for Vec<Paragraph<T>, N>
where
T: AsRef<str>,
{
fn at(&self, i: usize) -> Paragraph<&str> {
self[i].to_ref()
fn at<'a>(&'a self, index: usize, offset: usize, _buffer: &'a mut [u8]) -> Paragraph<&'a str> {
self[index].offset_as_ref(offset)
}
fn size(&self) -> usize {
@ -53,8 +68,8 @@ impl<T, const N: usize> ParagraphSource for [Paragraph<T>; N]
where
T: AsRef<str>,
{
fn at(&self, i: usize) -> Paragraph<&str> {
self[i].to_ref()
fn at<'a>(&'a self, index: usize, offset: usize, _buffer: &'a mut [u8]) -> Paragraph<&'a str> {
self[index].offset_as_ref(offset)
}
fn size(&self) -> usize {
@ -66,9 +81,9 @@ impl<T> ParagraphSource for Paragraph<T>
where
T: AsRef<str>,
{
fn at(&self, i: usize) -> Paragraph<&str> {
assert_eq!(i, 0);
self.to_ref()
fn at<'a>(&'a self, index: usize, offset: usize, _buffer: &'a mut [u8]) -> Paragraph<&'a str> {
assert_eq!(index, 0);
self.offset_as_ref(offset)
}
fn size(&self) -> usize {
@ -157,26 +172,31 @@ where
}
}
/// Returns iterator over visible layouts (bounding box, style) together
/// Iterate over visible layouts (bounding box, style) together
/// with corresponding string content. Should not get monomorphized.
fn visible_content<'a>(
content: &'a dyn ParagraphSource,
fn foreach_visible<'a, 'b>(
source: &'a dyn ParagraphSource,
visible: &'a [TextLayout],
offset: PageOffset,
) -> impl Iterator<Item = (&'a TextLayout, &'a str)> {
visible.iter().zip(
(offset.par..content.size())
.map(|i| content.at(i))
.filter(|p| !p.content.is_empty())
.enumerate()
.map(move |(i, p): (usize, Paragraph<&str>)| {
if i == 0 {
&p.content[offset.chr..]
} else {
p.content
}
}),
)
func: &'b mut dyn FnMut(&TextLayout, &str),
) {
let mut buffer = [0; SCRATCH_BUFFER_LEN];
let mut vis_iter = visible.iter();
let mut chr = offset.chr;
for par in offset.par..source.size() {
let s = source.at(par, chr, &mut buffer).content;
if s.is_empty() {
chr = 0;
continue;
}
if let Some(layout) = vis_iter.next() {
func(layout, s);
} else {
break;
}
chr = 0;
}
}
}
@ -197,9 +217,14 @@ where
}
fn paint(&mut self) {
for (layout, content) in Self::visible_content(&self.source, &self.visible, self.offset) {
layout.render_text(content);
}
Self::foreach_visible(
&self.source,
&self.visible,
self.offset,
&mut |layout, content| {
layout.render_text(content);
},
)
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
@ -239,11 +264,15 @@ pub mod trace {
impl<T: ParagraphSource> crate::trace::Trace for Paragraphs<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Paragraphs");
for (layout, content) in Self::visible_content(&self.source, &self.visible, self.offset)
{
layout.layout_text(content, &mut layout.initial_cursor(), &mut TraceSink(t));
t.string("\n");
}
Self::foreach_visible(
&self.source,
&self.visible,
self.offset,
&mut |layout, content| {
layout.layout_text(content, &mut layout.initial_cursor(), &mut TraceSink(t));
t.string("\n");
},
);
t.close();
}
}
@ -290,16 +319,18 @@ impl<T> Paragraph<T> {
self
}
pub fn content(&self) -> &T {
&self.content
}
pub fn update(&mut self, content: T) {
self.content = content
}
fn to_ref(&self) -> Paragraph<&str>
where
T: AsRef<str>,
{
/// Copy style and replace content.
pub fn with_content<U>(&self, content: U) -> Paragraph<U> {
Paragraph {
content: self.content.as_ref(),
content,
style: self.style,
align: self.align,
break_after: self.break_after,
@ -307,6 +338,13 @@ impl<T> Paragraph<T> {
}
}
pub fn offset_as_ref(&self, offset: usize) -> Paragraph<&str>
where
T: AsRef<str>,
{
self.with_content(&self.content.as_ref()[offset..])
}
fn layout(&self, area: Rect) -> TextLayout {
TextLayout {
padding_top: PARAGRAPH_TOP_SPACE,
@ -344,7 +382,8 @@ impl PageOffset {
source: &dyn ParagraphSource,
full_height: i16,
) -> (PageOffset, Option<Rect>, Option<TextLayout>) {
let paragraph = source.at(self.par);
let mut buffer = [0; SCRATCH_BUFFER_LEN];
let paragraph = source.at(self.par, self.chr, &mut buffer);
// Skip empty paragraphs.
if paragraph.content.is_empty() {
@ -355,8 +394,9 @@ impl PageOffset {
// Handle the `no_break` flag used to keep key-value pair on the same page.
if paragraph.no_break && self.chr == 0 {
let mut next_buffer = [0; SCRATCH_BUFFER_LEN];
if let Some(next_paragraph) =
(self.par + 1 < source.size()).then(|| source.at(self.par + 1))
(self.par + 1 < source.size()).then(|| source.at(self.par + 1, 0, &mut next_buffer))
{
if Self::should_place_pair_on_next_page(
&paragraph,
@ -371,7 +411,7 @@ impl PageOffset {
// Find out the dimensions of the paragraph at given char offset.
let mut layout = paragraph.layout(area);
let fit = layout.fit_text(&paragraph.content[self.chr..]);
let fit = layout.fit_text(paragraph.content);
let (used, remaining_area) = area.split_top(fit.height());
layout.bounds = used;

View File

@ -1,10 +1,18 @@
use crate::{
error::Error,
micropython::{
buffer::{get_buffer, get_str_owner, StrBuffer},
gc::Gc,
iter::{Iter, IterBuf},
list::List,
obj::Obj,
},
ui::component::text::{
paragraphs::{Paragraph, ParagraphSource},
TextStyle,
},
};
use core::str;
use cstr_core::cstr;
use heapless::Vec;
@ -32,3 +40,177 @@ where
// Returns error if array.len() != N
vec.into_array().map_err(|_| err)
}
fn hexlify<'a>(data: &[u8], buffer: &'a mut [u8]) -> &'a str {
const HEX_LOWER: [u8; 16] = *b"0123456789abcdef";
let mut i: usize = 0;
for b in data.iter().take(buffer.len() / 2) {
let hi: usize = ((b & 0xf0) >> 4).into();
let lo: usize = (b & 0x0f).into();
buffer[i] = HEX_LOWER[hi];
buffer[i + 1] = HEX_LOWER[lo];
i += 2;
}
// SAFETY: only <0x7f bytes are used to construct the string
unsafe { str::from_utf8_unchecked(&buffer[0..i]) }
}
pub enum StrOrBytes {
Str(StrBuffer),
Bytes(Obj),
}
impl StrOrBytes {
pub fn as_str_offset<'a>(&'a self, offset: usize, buffer: &'a mut [u8]) -> &'a str {
match self {
StrOrBytes::Str(x) => &x.as_ref()[offset..],
StrOrBytes::Bytes(x) => Self::hexlify(*x, offset, buffer),
}
}
fn hexlify(obj: Obj, offset: usize, buffer: &mut [u8]) -> &str {
if !obj.is_bytes() {
return "ERROR";
}
// Convert offset to byte representation, handle case where it points in the
// middle of a byte.
let bin_off = offset / 2;
let hex_off = offset % 2;
// SAFETY:
// (a) only immutable references are taken
// (b) reference is discarded before returning to micropython
let bin_slice = if let Ok(buf) = unsafe { get_buffer(obj) } {
&buf[bin_off..]
} else {
return "ERROR";
};
let hexadecimal = hexlify(bin_slice, buffer);
&hexadecimal[hex_off..]
}
}
impl TryFrom<Obj> for StrOrBytes {
type Error = Error;
fn try_from(obj: Obj) -> Result<Self, Error> {
if obj.is_str() {
Ok(StrOrBytes::Str(obj.try_into()?))
} else if obj.is_bytes() {
Ok(StrOrBytes::Bytes(obj))
} else {
Err(Error::TypeError)
}
}
}
pub struct ConfirmBlob {
pub description: StrBuffer,
pub extra: StrBuffer,
pub data: StrOrBytes,
pub description_font: &'static TextStyle,
pub extra_font: &'static TextStyle,
pub data_font: &'static TextStyle,
}
impl ParagraphSource for ConfirmBlob {
fn at<'a>(&'a self, index: usize, offset: usize, buffer: &'a mut [u8]) -> Paragraph<&'a str> {
match index {
0 => Paragraph::new(self.description_font, &self.description.as_ref()[offset..]),
1 => Paragraph::new(self.extra_font, &self.extra.as_ref()[offset..]),
2 => Paragraph::new(self.data_font, self.data.as_str_offset(offset, buffer)),
_ => unreachable!(),
}
}
fn size(&self) -> usize {
3
}
}
pub struct PropsList {
items: Gc<List>,
key_font: &'static TextStyle,
value_font: &'static TextStyle,
value_mono_font: &'static TextStyle,
}
impl PropsList {
pub fn new(
obj: Obj,
key_font: &'static TextStyle,
value_font: &'static TextStyle,
value_mono_font: &'static TextStyle,
) -> Result<Self, Error> {
Ok(Self {
items: obj.try_into()?,
key_font,
value_font,
value_mono_font,
})
}
}
impl ParagraphSource for PropsList {
fn at<'a>(&'a self, index: usize, offset: usize, buffer: &'a mut [u8]) -> Paragraph<&'a str> {
let block = move |buffer| {
let entry = self.items.get(index / 2)?;
let [key, value, value_is_mono]: [Obj; 3] = iter_into_objs(entry)?;
let value_is_mono: bool = bool::try_from(value_is_mono)?;
let obj: Obj;
let style: &TextStyle;
if index % 2 == 0 {
if !key.is_str() && key != Obj::const_none() {
return Err(Error::TypeError);
}
style = self.key_font;
obj = key;
} else {
if value_is_mono {
style = self.value_mono_font;
} else {
if value.is_bytes() {
return Err(Error::TypeError);
}
style = self.value_font;
}
obj = value;
};
if obj == Obj::const_none() {
return Ok(Paragraph::new(style, ""));
}
let para = if obj.is_str() {
// SAFETY:
// As long as self is visible to GC, the string will be also. The paragraph
// rendering does not keep the returned references for long so we can reasonably
// expect for the chain of references from self to the buffer not to be broken.
let s = unsafe { get_str_owner(self, obj)? };
Paragraph::new(style, &s[offset..])
} else if obj.is_bytes() {
let s = StrOrBytes::hexlify(obj, offset, buffer);
Paragraph::new(style, s)
} else {
return Err(Error::TypeError);
};
if obj == key && value != Obj::const_none() {
Ok(para.no_break())
} else {
Ok(para)
}
};
match block(buffer) {
Ok(p) => p,
Err(_) => Paragraph::new(self.value_font, "ERROR"),
}
}
fn size(&self) -> usize {
2 * self.items.len()
}
}

View File

@ -1,7 +1,5 @@
use core::{cmp::Ordering, convert::TryInto, ops::Deref};
use heapless::Vec;
use crate::{
error::Error,
micropython::{
@ -31,7 +29,7 @@ use crate::{
layout::{
obj::{ComponentMsgObj, LayoutObj},
result::{CANCELLED, CONFIRMED, INFO},
util::{iter_into_array, iter_into_objs},
util::{iter_into_array, ConfirmBlob, PropsList},
},
},
};
@ -331,26 +329,24 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
fn _confirm_blob(
fn confirm_blob(
title: StrBuffer,
data: Option<StrBuffer>,
data: Obj,
description: Option<StrBuffer>,
extra: Option<StrBuffer>,
verb: Option<StrBuffer>,
verb_cancel: Option<StrBuffer>,
hold: bool,
) -> Result<Obj, Error> {
let mut par_source: Vec<Paragraph<StrBuffer>, 3> = Vec::new();
if let Some(description) = description {
unwrap!(par_source.push(Paragraph::new(&theme::TEXT_NORMAL, description)));
let paragraphs = ConfirmBlob {
description: description.unwrap_or_else(StrBuffer::empty),
extra: extra.unwrap_or_else(StrBuffer::empty),
data: data.try_into()?,
description_font: &theme::TEXT_NORMAL,
extra_font: &theme::TEXT_BOLD,
data_font: &theme::TEXT_MONO,
}
if let Some(extra) = extra {
unwrap!(par_source.push(Paragraph::new(&theme::TEXT_BOLD, extra)));
}
if let Some(data) = data {
unwrap!(par_source.push(Paragraph::new(&theme::TEXT_MONO, data)));
}
let paragraphs = Paragraphs::new(par_source);
.into_paragraphs();
let obj = if hold {
LayoutObj::new(Frame::new(title, SwipeHoldPage::new(paragraphs, theme::BG)))?
@ -369,10 +365,10 @@ fn _confirm_blob(
extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let data: StrBuffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?;
let description: StrBuffer =
kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let extra: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_extra, StrBuffer::empty())?;
let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?;
let description: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let extra: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?;
let verb_cancel: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
@ -382,11 +378,11 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map
let verb: StrBuffer = "CONFIRM".into();
_confirm_blob(
confirm_blob(
title,
Some(data),
Some(description),
Some(extra),
data,
description,
extra,
Some(verb),
verb_cancel,
hold,
@ -401,31 +397,12 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let mut paragraphs = ParagraphVecLong::new();
let mut iter_buf = IterBuf::new();
let iter = Iter::try_from_obj_with_buf(items, &mut iter_buf)?;
for para in iter {
let [key, value, is_mono]: [Obj; 3] = iter_into_objs(para)?;
let key = key.try_into_option::<StrBuffer>()?;
let value = value.try_into_option::<StrBuffer>()?;
if let Some(key) = key {
if value.is_some() {
paragraphs.add(Paragraph::new(&theme::TEXT_BOLD, key).no_break());
} else {
paragraphs.add(Paragraph::new(&theme::TEXT_BOLD, key));
}
}
if let Some(value) = value {
if is_mono.try_into()? {
paragraphs.add(Paragraph::new(&theme::TEXT_MONO, value));
} else {
paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, value));
}
}
}
let paragraphs = PropsList::new(
items,
&theme::TEXT_BOLD,
&theme::TEXT_NORMAL,
&theme::TEXT_MONO,
)?;
let obj = if hold {
LayoutObj::new(Frame::new(
title,
@ -497,8 +474,9 @@ extern "C" fn new_show_qr(n_args: usize, args: *const Obj, kwargs: *mut Map) ->
extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let value: StrBuffer = kwargs.get(Qstr::MP_QSTR_value)?.try_into()?;
let description: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let value: Obj = kwargs.get(Qstr::MP_QSTR_value)?;
let verb: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb)
@ -506,15 +484,7 @@ extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Ma
.try_into_option()?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
_confirm_blob(
title,
Some(value),
Some(description),
None,
verb,
None,
hold,
)
confirm_blob(title, value, description, None, verb, None, hold)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
@ -1142,9 +1112,9 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def confirm_blob(
/// *,
/// title: str,
/// data: str,
/// description: str = "",
/// extra: str = "",
/// data: str | bytes,
/// description: str | None,
/// extra: str | None,
/// verb_cancel: str | None = None,
/// ask_pagination: bool = False,
/// hold: bool = False,
@ -1155,12 +1125,11 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def confirm_properties(
/// *,
/// title: str,
/// items: Iterable[Tuple[str | None, str | None, bool]],
/// items: list[tuple[str | None, str | bytes | None, bool]],
/// hold: bool = False,
/// ) -> object:
/// """Confirm list of key-value pairs. The third component in the tuple should be True if
/// the value is to be rendered as binary with monospace font, False otherwise.
/// This only concerns the text style, you need to decode the value to UTF-8 in python."""
/// the value is to be rendered as binary with monospace font, False otherwise."""
Qstr::MP_QSTR_confirm_properties => obj_fn_kw!(0, new_confirm_properties).as_obj(),
/// def confirm_reset_device(

View File

@ -75,9 +75,9 @@ def confirm_action(
def confirm_blob(
*,
title: str,
data: str,
description: str = "",
extra: str = "",
data: str | bytes,
description: str | None,
extra: str | None,
verb_cancel: str | None = None,
ask_pagination: bool = False,
hold: bool = False,
@ -89,12 +89,11 @@ def confirm_blob(
def confirm_properties(
*,
title: str,
items: Iterable[Tuple[str | None, str | None, bool]],
items: list[tuple[str | None, str | bytes | None, bool]],
hold: bool = False,
) -> object:
"""Confirm list of key-value pairs. The third component in the tuple should be True if
the value is to be rendered as binary with monospace font, False otherwise.
This only concerns the text style, you need to decode the value to UTF-8 in python."""
the value is to be rendered as binary with monospace font, False otherwise."""
# rust/src/ui/model_tt/layout.rs

View File

@ -1,5 +1,4 @@
from typing import TYPE_CHECKING
from ubinascii import hexlify
from trezor import io, log, loop, ui
from trezor.enums import ButtonRequestType
@ -317,6 +316,8 @@ def _show_xpub(xpub: str, title: str, cancel: str) -> ui.Layout:
title=title,
data=xpub,
verb_cancel=cancel,
extra=None,
description=None,
)
)
return content
@ -596,9 +597,6 @@ async def confirm_blob(
br_code: ButtonRequestType = BR_TYPE_OTHER,
ask_pagination: bool = False,
) -> None:
if isinstance(data, bytes):
data = hexlify(data).decode()
await raise_if_not_confirmed(
interact(
ctx,
@ -607,6 +605,7 @@ async def confirm_blob(
title=title.upper(),
description=description or "",
data=data,
extra=None,
ask_pagination=ask_pagination,
hold=hold,
)
@ -716,11 +715,8 @@ async def confirm_properties(
hold: bool = False,
br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput,
) -> None:
def handle_bytes(prop: PropertyType) -> tuple[str | None, str | None, bool]:
if isinstance(prop[1], bytes):
return (prop[0], hexlify(prop[1]).decode(), True)
else:
return (prop[0], prop[1], False)
# Monospace flag for values that are bytes.
items = [(prop[0], prop[1], isinstance(prop[1], bytes)) for prop in props]
await raise_if_not_confirmed(
interact(
@ -728,7 +724,7 @@ async def confirm_properties(
_RustLayout(
trezorui2.confirm_properties(
title=title.upper(),
items=map(handle_bytes, props),
items=items,
hold=hold,
)
),