make ui call generic

This commit is contained in:
Lukas Bielesch
2026-02-04 13:42:46 +01:00
parent 74ff8cf8fd
commit dd9f54ea50
15 changed files with 197 additions and 100 deletions

View File

@@ -643,6 +643,7 @@ static void _librust_qstrs(void) {
MP_QSTR_regulatory__title;
MP_QSTR_reject_pairing;
MP_QSTR_remaining_shares;
MP_QSTR_remote;
MP_QSTR_request_bip39;
MP_QSTR_request_complete_repaint;
MP_QSTR_request_duration;

View File

@@ -157,3 +157,17 @@ impl Drop for IpcMessage<'_> {
unsafe { ffi::ipc_message_free(&mut lowlevel_message) };
}
}
#[derive(Copy, Clone, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::FromPrimitive)]
#[repr(u16)]
pub enum CoreIpcService {
Lifecycle = 0,
Ui = 1,
WireStart = 2,
WireContinue = 3,
WireEnd = 4,
Crypto = 5,
Util = 6,
#[num_enum(catch_all)]
Unknown(u16),
}

View File

@@ -1351,9 +1351,10 @@ extern "C" fn new_process_ipc_message(n_args: usize, args: *const Obj, kwargs: *
let obj: Obj = kwargs.get(Qstr::MP_QSTR_data)?;
let data = unwrap!(unsafe { crate::micropython::buffer::get_buffer(obj) });
let remote: u8 = kwargs.get(Qstr::MP_QSTR_remote)?.try_into()?;
// Pass the slice directly to the trait function
let layout = ModelUI::process_ipc_message(data)?;
let layout = ModelUI::process_ipc_message(data, remote)?;
Ok(layout.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@@ -2281,6 +2282,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// def process_ipc_message(
/// *,
/// data: bytes,
/// remote: int,
/// ) -> LayoutObj[UiResult]:
/// """Process an IPC message by deserializing it and dispatching to the appropriate UI function."""
Qstr::MP_QSTR_process_ipc_message => obj_fn_kw!(0, new_process_ipc_message).as_obj(),

View File

@@ -1320,7 +1320,7 @@ impl FirmwareUI for UIBolt {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
}
fn process_ipc_message(_data: &[u8]) -> Result<Gc<LayoutObj>, Error> {
fn process_ipc_message(_data: &[u8], _remote: u8) -> Result<Gc<LayoutObj>, Error> {
Err(Error::NotImplementedError)
}
}

View File

@@ -1492,7 +1492,7 @@ impl FirmwareUI for UICaesar {
Ok(layout)
}
fn process_ipc_message(_data: &[u8]) -> Result<Gc<LayoutObj>, Error> {
fn process_ipc_message(_data: &[u8], _remote: u8) -> Result<Gc<LayoutObj>, Error> {
Err(Error::NotImplementedError)
}
}

View File

@@ -1357,7 +1357,7 @@ impl FirmwareUI for UIDelizia {
Ok(flow)
}
fn process_ipc_message(_data: &[u8]) -> Result<Gc<LayoutObj>, Error> {
fn process_ipc_message(_data: &[u8], _remote: u8) -> Result<Gc<LayoutObj>, Error> {
Err(Error::NotImplementedError)
}
}

View File

@@ -1,5 +1,5 @@
use crate::{
ipc::{IpcMessage, RemoteSysTask},
ipc::{CoreIpcService, IpcMessage, RemoteSysTask},
strutil::TString,
ui::{
cache::PageCache,
@@ -38,8 +38,8 @@ pub struct LongContentScreen<'a> {
}
impl<'a> LongContentScreen<'a> {
pub fn new(title: TString<'static>, pages: usize) -> Self {
let content = LongContent::new(pages as u16);
pub fn new(title: TString<'static>, pages: usize, remote: u8) -> Self {
let content = LongContent::new(pages as u16, remote);
let mut action_bar = ActionBar::new_cancel_confirm();
action_bar.update(content.pager);
@@ -139,15 +139,17 @@ struct LongContent {
cache: PageCache,
area: Rect,
state: ContentState,
remote: u8,
}
impl LongContent {
fn new(pages: u16) -> Self {
fn new(pages: u16, remote: u8) -> Self {
Self {
pager: Pager::new(pages),
cache: PageCache::new(),
area: Rect::zero(),
state: ContentState::Uninit,
remote,
}
}
@@ -196,8 +198,8 @@ impl LongContent {
)
.unwrap();
let msg = IpcMessage::new(9, &bytes);
unwrap!(msg.send(RemoteSysTask::Unknown(2), 6));
let msg = IpcMessage::new(idx as u16, &bytes);
unwrap!(msg.send(RemoteSysTask::Unknown(self.remote), CoreIpcService::Util.into()));
ctx.request_anim_frame();
}
}
@@ -218,14 +220,15 @@ impl Component for LongContent {
}
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if let Some(data) = IpcMessage::try_receive(RemoteSysTask::Unknown(2)) {
if let Some(message) = IpcMessage::try_receive(RemoteSysTask::Unknown(2)) {
debug_assert!(matches!(
self.state,
ContentState::Uninit | ContentState::Waiting(_)
));
self.state = match self.state {
ContentState::Uninit => {
self.cache.init(data.data());
debug_assert!(message.id() == 0);
self.cache.init(message.data());
ctx.request_paint();
if self.pager.has_next() {
let idx = self.pager.next() as usize;
@@ -238,15 +241,17 @@ impl Component for LongContent {
ContentState::Waiting(next_page)
if self.pager.current() + 1 == next_page as u16 =>
{
debug_assert!(message.id() == next_page as u16);
debug_assert!(self.cache.is_at_head());
self.cache.push_head(data.data());
self.cache.push_head(message.data());
ContentState::Ready
}
ContentState::Waiting(prev_page)
if self.pager.current() == prev_page as u16 + 1 =>
{
debug_assert!(message.id() == prev_page as u16);
debug_assert!(self.cache.is_at_tail());
self.cache.push_tail(data.data());
self.cache.push_tail(message.data());
ContentState::Ready
}
_ => {

View File

@@ -124,8 +124,12 @@ impl FirmwareUI for UIEckhart {
Ok(layout)
}
fn confirm_long(title: TString<'static>, pages: usize) -> Result<impl LayoutMaybeTrace, Error> {
let screen = LongContentScreen::new(title, pages);
fn confirm_long(
title: TString<'static>,
pages: usize,
remote: u8,
) -> Result<impl LayoutMaybeTrace, Error> {
let screen = LongContentScreen::new(title, pages, remote);
let layout = RootComponent::new(screen);
Ok(layout)
}
@@ -1744,7 +1748,7 @@ impl FirmwareUI for UIEckhart {
Ok(flow)
}
fn process_ipc_message(data: &[u8]) -> Result<Gc<LayoutObj>, Error> {
fn process_ipc_message(data: &[u8], remote: u8) -> Result<Gc<LayoutObj>, Error> {
// Safe helper to convert archived string to TString using Deref
fn tstr_from_archived<const N: usize>(s: &ArchivedStringN<N>) -> TString<'static> {
unsafe { StrBuffer::from_ptr_and_len(s.data.as_ptr(), s.len as usize) }.into()
@@ -1778,6 +1782,7 @@ impl FirmwareUI for UIEckhart {
let layout = Self::confirm_long(
tstr_from_archived(title),
unwrap!(usize::try_from(pages.to_native())),
remote,
)?;
LayoutObj::new_root(layout)
}

View File

@@ -42,6 +42,7 @@ pub trait FirmwareUI {
fn confirm_long(
title: TString<'static>,
pages: usize,
remote: u8,
) -> Result<impl LayoutMaybeTrace, Error>;
fn confirm_address(
@@ -513,9 +514,10 @@ pub trait FirmwareUI {
///
/// # Arguments
/// * `data` - Byte slice containing the rkyv-encoded TrezorEnum message
/// * `app_id` - Application identifier
///
/// # Returns
/// A Result containing the layout object or an error if deserialization
/// fails
fn process_ipc_message(data: &[u8]) -> Result<Gc<LayoutObj>, Error>;
fn process_ipc_message(data: &[u8], extapp_id: u8) -> Result<Gc<LayoutObj>, Error>;
}

View File

@@ -868,6 +868,7 @@ def tutorial() -> LayoutObj[UiResult]:
def process_ipc_message(
*,
data: bytes,
remote: int,
) -> LayoutObj[UiResult]:
"""Process an IPC message by deserializing it and dispatching to the appropriate UI function."""

View File

@@ -34,7 +34,7 @@ _SERVICE_WIRE_START = const(2)
_SERVICE_WIRE_CONTINUE = const(3)
_SERVICE_WIRE_END = const(4)
_SERVICE_CRYPTO = const(5)
_SERVICE_LONG_CONFIRM = const(6)
_SERVICE_UTIL = const(6)
def fn_id(service: int, message_id: int) -> int:
@@ -88,7 +88,7 @@ async def run(request: ExtAppMessage) -> ExtAppResponse:
if service == _SERVICE_UI:
result = await interact(
trezorui_api.process_ipc_message(data=bytes(msg.data)),
trezorui_api.process_ipc_message(data=bytes(msg.data), remote=msg.remote),
None,
raise_on_cancel=None,
)

View File

@@ -13,8 +13,7 @@ use trezor_structs::{DerivationPath, LongString, TrezorCryptoEnum};
use crate::core_services::services_or_die;
use crate::ipc::IpcMessage;
use crate::low_level_api::ffi::HDNode;
use crate::service::{CoreIpcService, Error};
use crate::service::{CoreIpcService, Error, NoUtilHandler};
use crate::util::Timeout;
pub type ArchivedTrezorCryptoResult = Archived<TrezorCryptoResult>;
pub type ArchivedTrezorCryptoEnum = Archived<TrezorCryptoEnum>;
@@ -29,7 +28,12 @@ type CryptoResult = Result<TrezorCryptoResult>;
fn ipc_crypto_call(value: &TrezorCryptoEnum) -> CryptoResult {
let bytes = to_bytes::<Failure>(value).unwrap();
let message = IpcMessage::new(0, &bytes);
let result = services_or_die().call(CoreIpcService::Crypto, &message, Timeout::max())?;
let result = services_or_die().call(
CoreIpcService::Crypto,
&message,
Timeout::max(),
&NoUtilHandler {},
)?;
// Safe validation using bytecheck before accessing archived data
let archived = rkyv::access::<ArchivedTrezorCryptoResult, Failure>(result.data()).unwrap();

View File

@@ -67,6 +67,10 @@ impl IpcMessage<'_> {
pub fn data(&self) -> &[u8] {
self.data
}
pub fn remote(&self) -> RemoteSysTask {
self.remote
}
}
impl<'a> IpcMessage<'a> {

View File

@@ -10,6 +10,49 @@ pub const CORE_SERVICE_REMOTE: RemoteSysTask = RemoteSysTask::CoreApp;
use trezor_structs::ArchivedUtilEnum;
// ============================================================================
// Trait-based Call Abstraction
// ============================================================================
/// Context for handling utility messages, contains info from the original request
pub struct UtilContext {
pub service: u16,
pub id: u16,
pub remote: RemoteSysTask,
}
/// Result of handling a utility message
pub enum UtilHandleResult {
/// Continue waiting for more messages
Continue,
/// Unexpected message received
Unexpected,
}
/// Trait for handling utility service messages during IPC calls
pub trait UtilHandler {
/// Returns true if this handler expects utility messages
fn expects_util_messages(&self) -> bool;
/// Handle an incoming utility enum message
/// Returns `UtilHandleResult::Continue` if handled successfully and should keep waiting
/// Returns `UtilHandleResult::Unexpected` if the message was not expected
fn handle(&self, ctx: &UtilContext, archived: &ArchivedUtilEnum) -> UtilHandleResult;
}
/// No utility message handling - for regular calls
pub struct NoUtilHandler;
impl UtilHandler for NoUtilHandler {
fn expects_util_messages(&self) -> bool {
false
}
fn handle(&self, _ctx: &UtilContext, _archived: &ArchivedUtilEnum) -> UtilHandleResult {
// Should never receive utility messages
UtilHandleResult::Unexpected
}
}
#[derive(uDebug, Copy, Clone, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::FromPrimitive)]
#[repr(u16)]
pub enum CoreIpcService {
@@ -19,10 +62,9 @@ pub enum CoreIpcService {
WireContinue = 3,
WireEnd = 4,
Crypto = 5,
LongConfirm = 6,
Util = 6,
#[num_enum(catch_all)]
Unknown(u16),
Ping = 0xffff,
}
pub struct IpcRemote<'a, T> {
@@ -38,7 +80,7 @@ pub enum Error<'a> {
UnexpectedResponse(IpcMessage<'a>),
}
impl<'a, T: Into<u16>> IpcRemote<'a, T> {
impl<'a, T: Into<u16> + Copy> IpcRemote<'a, T> {
pub const fn new(inbox: IpcInbox<'a>) -> Self {
Self {
inbox,
@@ -65,25 +107,7 @@ impl<'a, T: Into<u16>> IpcRemote<'a, T> {
service: T,
message: &IpcMessage,
timeout: Timeout,
) -> Result<IpcMessage<'a>, Error<'a>> {
let service_id = service.into();
message
.send(self.inbox.remote(), service_id)
.map_err(|_| Error::FailedToSend)?;
let reply = self.receive(timeout)?;
if reply.service() != service_id {
Err(Error::UnexpectedService(reply))
} else {
Ok(reply)
}
}
pub fn call_long(
&self,
service: T,
message: &IpcMessage,
timeout: Timeout,
long_content: &str,
util_handler: &dyn UtilHandler,
) -> Result<IpcMessage<'a>, Error<'a>> {
let service_id = service.into();
message
@@ -91,31 +115,21 @@ impl<'a, T: Into<u16>> IpcRemote<'a, T> {
.map_err(|_| Error::FailedToSend)?;
loop {
let reply = self.receive(timeout)?;
if reply.service() == 6 {
if reply.service() == u16::from(CoreIpcService::Util)
&& util_handler.expects_util_messages()
{
let util_ctx = UtilContext {
service: reply.service(),
id: reply.id(),
remote: reply.remote(),
};
let data = reply.data();
let archived = unsafe { rkyv::access_unchecked::<ArchivedUtilEnum>(data) };
match archived {
ArchivedUtilEnum::RequestPage { idx } => {
let page_idx = idx.to_native() as usize;
// Find byte range for the requested char slice
let mut chars = long_content.chars();
let start_byte = chars
.by_ref()
.take(page_idx * crate::ui::CHARS_PER_PAGE)
.map(|c| c.len_utf8())
.sum::<usize>();
let slice_len = chars
.take(crate::ui::CHARS_PER_PAGE)
.map(|c| c.len_utf8())
.sum::<usize>();
let slice = &long_content.as_bytes()[start_byte..start_byte + slice_len];
// TODO implement Debug for Api error
let _ = IpcMessage::new(10, slice).send(RemoteSysTask::CoreApp, 6);
}
_ => return Err(Error::UnexpectedResponse(reply)),
match util_handler.handle(&util_ctx, archived) {
UtilHandleResult::Continue => continue,
UtilHandleResult::Unexpected => return Err(Error::UnexpectedResponse(reply)),
}
} else if reply.service() != service_id {
return Err(Error::UnexpectedService(reply));

View File

@@ -23,18 +23,64 @@ use rkyv::api::low::deserialize;
use rkyv::rancor::Failure;
use rkyv::to_bytes;
pub use trezor_structs::TrezorUiResult;
use trezor_structs::{LongString, PropsList, ShortString, TrezorUiEnum};
use trezor_structs::{ArchivedUtilEnum, LongString, PropsList, ShortString, TrezorUiEnum};
use crate::core_services::services_or_die;
use crate::error;
use crate::ipc::IpcMessage;
use crate::service::{CoreIpcService, Error};
use crate::service::{
CoreIpcService, Error, NoUtilHandler, UtilContext, UtilHandleResult, UtilHandler,
};
use crate::util::Timeout;
pub type ArchivedTrezorUiResult = Archived<TrezorUiResult>;
pub type ArchivedTrezorUiEnum = Archived<TrezorUiEnum>;
pub const CHARS_PER_PAGE: usize = 96;
/// Long content handler - responds to page requests
pub struct LongContentHandler<'a>(pub &'a str);
impl<'a> LongContentHandler<'a> {
fn send_page(&self, ctx: &UtilContext, page_idx: usize) {
let long_content = self.0;
// Find byte range for the requested char slice
let mut chars = long_content.chars();
let start_byte = chars
.by_ref()
.take(page_idx * crate::ui::CHARS_PER_PAGE)
.map(|c| c.len_utf8())
.sum::<usize>();
let slice_len = chars
.take(crate::ui::CHARS_PER_PAGE)
.map(|c| c.len_utf8())
.sum::<usize>();
let slice = &long_content.as_bytes()[start_byte..start_byte + slice_len];
// Send response with the same service ID as the request
let _ = IpcMessage::new(ctx.id, slice).send(ctx.remote, ctx.service);
}
}
impl<'a> UtilHandler for LongContentHandler<'a> {
fn expects_util_messages(&self) -> bool {
true
}
fn handle(&self, ctx: &UtilContext, archived: &ArchivedUtilEnum) -> UtilHandleResult {
match archived {
ArchivedUtilEnum::RequestPage { idx } => {
let page_idx = idx.to_native() as usize;
self.send_page(ctx, page_idx);
UtilHandleResult::Continue
}
// Only RequestPage is allowed for LongContentHandler
_ => UtilHandleResult::Unexpected,
}
}
}
// ============================================================================
// Helper Functions
// ============================================================================
@@ -42,33 +88,39 @@ pub const CHARS_PER_PAGE: usize = 96;
type Result<T> = core::result::Result<T, Error<'static>>;
type UiResult = Result<TrezorUiResult>;
/// Send a UI enum over IPC and get the response
fn ipc_ui_call(value: &TrezorUiEnum) -> UiResult {
/// Send a UI enum over IPC with optional long content
fn ipc_ui_call(value: &TrezorUiEnum, util_handler: &dyn UtilHandler) -> UiResult {
let bytes = to_bytes::<Failure>(value).unwrap();
let message = IpcMessage::new(0, &bytes);
let result = services_or_die().call(CoreIpcService::Ui, &message, Timeout::max())?;
// Safe validation using bytecheck before accessing archived data
let archived = rkyv::access::<ArchivedTrezorUiResult, Failure>(result.data()).unwrap();
let deserialized = deserialize::<TrezorUiResult, Failure>(archived).unwrap();
Ok(deserialized)
}
fn ipc_ui_long_call(value: &TrezorUiEnum, long_content: &str) -> UiResult {
let bytes = to_bytes::<Failure>(value).unwrap();
let message = IpcMessage::new(0, &bytes);
let result =
services_or_die().call_long(CoreIpcService::Ui, &message, Timeout::max(), long_content)?;
services_or_die().call(CoreIpcService::Ui, &message, Timeout::max(), util_handler)?;
// Safe validation using bytecheck before accessing archived data
let archived = rkyv::access::<ArchivedTrezorUiResult, Failure>(result.data()).unwrap();
let deserialized = deserialize::<TrezorUiResult, Failure>(archived).unwrap();
Ok(deserialized)
}
/// Send a UI enum over IPC and get the response
// fn ipc_ui_call(value: &TrezorUiEnum) -> UiResult {
// ipc_ui_call_with_content(value, &NoUtilHandler)
// }
// fn ipc_ui_call_with_content(value: &TrezorUiEnum, util_handler: &dyn UtilHandler) -> UiResult {
// let bytes = to_bytes::<Failure>(value).unwrap();
// let message = IpcMessage::new(0, &bytes);
// let result = services_or_die().call(CoreIpcService::Ui, &message, Timeout::max(), util_handler)?;
// // Safe validation using bytecheck before accessing archived data
// let archived = rkyv::access::<ArchivedTrezorUiResult, Failure>(result.data()).unwrap();
// let deserialized = deserialize::<TrezorUiResult, Failure>(archived).unwrap();
// Ok(deserialized)
// }
/// Send a UI call and expect a boolean confirmation result
fn ipc_ui_call_confirm(value: TrezorUiEnum) -> UiResult {
match ipc_ui_call(&value) {
match ipc_ui_call(&value, &NoUtilHandler {}) {
Ok(TrezorUiResult::Confirmed) => Ok(TrezorUiResult::Confirmed),
Ok(_) => Ok(TrezorUiResult::Cancelled),
Err(e) => {
@@ -80,10 +132,14 @@ fn ipc_ui_call_confirm(value: TrezorUiEnum) -> UiResult {
/// Send a UI call that doesn't expect a meaningful response
fn ipc_ui_call_void(value: TrezorUiEnum) -> Result<()> {
ipc_ui_call(&value)?;
ipc_ui_call(&value, &NoUtilHandler {})?;
Ok(())
}
// fn ipc_ui_long_call(value: &TrezorUiEnum, long_content: &str) -> UiResult {
// ipc_ui_call_with_content(value, &LongContentHandler(long_content))
// }
// ============================================================================
// Public API Functions
// ============================================================================
@@ -105,7 +161,7 @@ pub fn confirm_long_value(title: &str, content: &str) -> UiResult {
pages: (content.chars().count() as usize + CHARS_PER_PAGE - 1) / CHARS_PER_PAGE,
};
match ipc_ui_long_call(&value, content) {
match ipc_ui_call(&value, &LongContentHandler(content)) {
Ok(TrezorUiResult::Confirmed) => Ok(TrezorUiResult::Confirmed),
Ok(_) => Ok(TrezorUiResult::Cancelled),
Err(e) => {
@@ -149,7 +205,7 @@ pub fn request_string(prompt: &str) -> UiResult {
let value = TrezorUiEnum::RequestString {
prompt: ShortString::from_str(prompt).unwrap(),
};
let result = ipc_ui_call(&value)?;
let result = ipc_ui_call(&value, &NoUtilHandler {})?;
match result {
TrezorUiResult::String(_) => Ok(result),
_ => Ok(TrezorUiResult::Cancelled),
@@ -166,7 +222,7 @@ pub fn request_number(title: &str, content: &str, initial: u32, min: u32, max: u
max,
};
let result = ipc_ui_call(&value)?;
let result = ipc_ui_call(&value, &NoUtilHandler {})?;
match result {
TrezorUiResult::Integer(_) => Ok(result),
_ => Ok(TrezorUiResult::Cancelled),
@@ -177,20 +233,9 @@ pub fn show_public_key(key: &str) -> UiResult {
let value = TrezorUiEnum::ShowPublicKey {
key: LongString::from_str(key).unwrap(),
};
let result = ipc_ui_call(&value)?;
let result = ipc_ui_call(&value, &NoUtilHandler {})?;
match result {
TrezorUiResult::Confirmed => Ok(result),
_ => Ok(TrezorUiResult::Cancelled),
}
}
/// Send a ping message (for testing)
pub fn ping(msg: &str) -> Result<()> {
let ping = IpcMessage::new(0, msg.as_bytes());
let resp = services_or_die().call(CoreIpcService::Ping, &ping, Timeout::max())?;
if resp.data() == msg.as_bytes() {
Ok(())
} else {
Err(Error::UnexpectedResponse(resp))
}
}