From 6ccc11d15663bf40f3405a14529adcc874b396c0 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 8 Oct 2026 19:03:10 +0200 Subject: [PATCH 1/4] Add tests for app selection --- dispatch/tests/select.rs | 205 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 dispatch/tests/select.rs diff --git a/dispatch/tests/select.rs b/dispatch/tests/select.rs new file mode 100644 index 0000000..8d73d12 --- /dev/null +++ b/dispatch/tests/select.rs @@ -0,0 +1,205 @@ +use apdu_app::{App, Result}; +use apdu_dispatch::{ + dispatch::ApduDispatch, + interchanges::{Channel, Data, Requester}, +}; +use heapless::VecView; +use hex_literal::hex; +use iso7816::{command::CommandView, Aid, Interface}; + +const AID_PIV: &[u8] = &hex!("A000000308000010000100"); +const AID_OPENPGP: &[u8] = &hex!("D276000124010304000FFFFFFFFF0000"); + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +struct Calls { + select: usize, + deselect: usize, + call: usize, +} + +struct TestApp { + aid: Aid, + calls: Calls, +} + +impl TestApp { + fn new(aid: Aid) -> Self { + Self { + aid, + calls: Calls::default(), + } + } +} + +impl iso7816::App for TestApp { + fn aid(&self) -> Aid { + self.aid + } +} + +impl App for TestApp { + fn select( + &mut self, + _interface: Interface, + _apdu: CommandView<'_>, + _reply: &mut VecView, + ) -> Result { + self.calls.select += 1; + Ok(()) + } + + fn deselect(&mut self) { + self.calls.deselect += 1; + } + + fn call( + &mut self, + _interface: Interface, + _apdu: CommandView<'_>, + _reply: &mut VecView, + ) -> Result { + self.calls.call += 1; + Ok(()) + } +} + +struct Channels { + contact: Channel, + contactless: Channel, +} + +impl Channels { + fn new() -> Self { + Self { + contact: Channel::new(), + contactless: Channel::new(), + } + } +} + +struct Dispatch<'a, const N: usize> { + contact_rq: Requester<'a>, + contactless_rq: Requester<'a>, + dispatch: ApduDispatch<'a>, + apps: [TestApp; N], +} + +impl<'a, const N: usize> Dispatch<'a, N> { + fn new(channels: &'a Channels, aids: [Aid; N]) -> Self { + let (contact_rq, contact_rp) = channels.contact.split().unwrap(); + let (contactless_rq, contactless_rp) = channels.contactless.split().unwrap(); + let dispatch = ApduDispatch::new(contact_rp, contactless_rp); + let apps = aids.map(TestApp::new); + Self { + contact_rq, + contactless_rq, + dispatch, + apps, + } + } + + fn send_command(&mut self, interface: Interface, command: &[u8]) -> Option { + let rq = match interface { + Interface::Contact => &mut self.contact_rq, + Interface::Contactless => &mut self.contactless_rq, + }; + rq.request(command.try_into().unwrap()).unwrap(); + let mut apps: Vec<&mut dyn App> = self.apps.iter_mut().map(|app| app as _).collect(); + self.dispatch.poll(&mut apps); + rq.take_response() + } + + fn calls(&self) -> [Calls; N] { + self.apps.each_ref().map(|app| app.calls) + } +} + +fn select(aid: &[u8]) -> Vec { + let mut cmd = Vec::from(hex!("00A40400")); + cmd.push(aid.len().try_into().unwrap()); + cmd.extend(aid); + cmd +} + +#[test] +fn test_simple() { + let aids = [Aid::try_new(AID_PIV).unwrap()]; + let mut calls = [Calls::default()]; + let channels = Channels::new(); + let mut dispatch = Dispatch::new(&channels, aids); + assert_eq!(dispatch.calls(), calls); + + let dummy_cmd = hex!("00000000"); + let select_cmd = select(AID_PIV); + + let response = dispatch.send_command(Interface::Contact, &dummy_cmd); + assert_eq!(response.unwrap(), &hex!("6A82")); + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &select_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[0].select += 1; + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &select_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[0].select += 1; + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &dummy_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[0].call += 1; + assert_eq!(dispatch.calls(), calls); +} + +#[test] +fn test_multi() { + let aids = [ + Aid::try_new(AID_PIV).unwrap(), + Aid::try_new(AID_OPENPGP).unwrap(), + ]; + let mut calls = [Calls::default(), Calls::default()]; + let channels = Channels::new(); + let mut dispatch = Dispatch::new(&channels, aids); + assert_eq!(dispatch.calls(), calls); + + let dummy_cmd = hex!("00000000"); + let select_piv_cmd = select(AID_PIV); + let select_openpgp_cmd = select(AID_OPENPGP); + + let response = dispatch.send_command(Interface::Contact, &dummy_cmd); + assert_eq!(response.unwrap(), &hex!("6A82")); + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &select_piv_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[0].select += 1; + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &select_piv_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[0].select += 1; + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &dummy_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[0].call += 1; + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &select_openpgp_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + // TODO: this is a bug -- the first application should be deselected, not the second + calls[1].deselect += 1; + calls[1].select += 1; + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &select_openpgp_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[1].select += 1; + assert_eq!(dispatch.calls(), calls); + + let response = dispatch.send_command(Interface::Contact, &dummy_cmd); + assert_eq!(response.unwrap(), &hex!("9000")); + calls[1].call += 1; + assert_eq!(dispatch.calls(), calls); +} From 6b618a81b06bd4e82fac171351fe80d4368dd637 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 8 Oct 2026 19:16:15 +0200 Subject: [PATCH 2/4] Call App::deselect for deselected (not selected) app Fixes: https://github.com/trussed-dev/apdu-dispatch/issues/36 --- dispatch/CHANGELOG.md | 2 +- dispatch/src/dispatch.rs | 5 ++--- dispatch/tests/select.rs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dispatch/CHANGELOG.md b/dispatch/CHANGELOG.md index 5dfe773..e20e762 100644 --- a/dispatch/CHANGELOG.md +++ b/dispatch/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- +- Call `App::deselect` for the deselected app, not for the selected app ([#36](https://github.com/trussed-dev/apdu-dispatch/issues/36)) ## [0.4.0] diff --git a/dispatch/src/dispatch.rs b/dispatch/src/dispatch.rs index c795594..2205d41 100644 --- a/dispatch/src/dispatch.rs +++ b/dispatch/src/dispatch.rs @@ -376,8 +376,6 @@ impl<'pipe> ApduDispatch<'pipe> { // For PIV, "SELECT" is NOP if it was already selected, but this is // not necessarily the case for other apps - // if there is a selected app with a different AID, deselect it - // select specified app in any case if let Some(app) = Self::find_app(Some(&aid), apps) { info!("Selected app"); @@ -389,10 +387,11 @@ impl<'pipe> ApduDispatch<'pipe> { _ => panic!("Unexpected buffer state."), }; + // if there is a selected app with a different AID, deselect it let old_aid = self.current_aid.replace(aid); if let Some(old_aid) = old_aid { if old_aid != aid { - let app = Self::find_app(self.current_aid.as_ref(), apps).unwrap(); + let app = Self::find_app(Some(&old_aid), apps).unwrap(); // for now all apps will be happy with this. app.deselect(); } diff --git a/dispatch/tests/select.rs b/dispatch/tests/select.rs index 8d73d12..dd2bc70 100644 --- a/dispatch/tests/select.rs +++ b/dispatch/tests/select.rs @@ -188,8 +188,7 @@ fn test_multi() { let response = dispatch.send_command(Interface::Contact, &select_openpgp_cmd); assert_eq!(response.unwrap(), &hex!("9000")); - // TODO: this is a bug -- the first application should be deselected, not the second - calls[1].deselect += 1; + calls[0].deselect += 1; calls[1].select += 1; assert_eq!(dispatch.calls(), calls); From 8381e2cbe965c47a406a488acd681d07e9be6a7e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 8 Oct 2026 19:19:25 +0200 Subject: [PATCH 3/4] Simplify find_apps signature --- dispatch/src/dispatch.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/dispatch/src/dispatch.rs b/dispatch/src/dispatch.rs index 2205d41..5ad8ba3 100644 --- a/dispatch/src/dispatch.rs +++ b/dispatch/src/dispatch.rs @@ -121,20 +121,14 @@ impl<'pipe> ApduDispatch<'pipe> { // It would be nice to store `current_app` instead of constantly looking up by AID, // but that won't work due to ownership rules fn find_app<'a, 'b>( - aid: Option<&Aid>, + aid: &Aid, apps: &'a mut [&'b mut dyn App], ) -> Option<&'a mut &'b mut dyn App> { - // match aid { - // Some(aid) => apps.iter_mut().find(|app| aid.starts_with(app.rid())), - // None => None, - // } - aid.and_then(move |aid| { - debug!("matching {:?}", aid); - apps.iter_mut().find(|app| { - // aid.starts_with(app.aid().truncated()) - debug!("...against {:?}", app.aid()); - app.aid().matches(aid) - }) + debug!("matching {:?}", aid); + apps.iter_mut().find(|app| { + // aid.starts_with(app.aid().truncated()) + debug!("...against {:?}", app.aid()); + app.aid().matches(aid) }) } @@ -377,7 +371,7 @@ impl<'pipe> ApduDispatch<'pipe> { // not necessarily the case for other apps // select specified app in any case - if let Some(app) = Self::find_app(Some(&aid), apps) { + if let Some(app) = Self::find_app(&aid, apps) { info!("Selected app"); let mut response = response::Data::new(); let result = match &self.buffer.raw { @@ -391,7 +385,7 @@ impl<'pipe> ApduDispatch<'pipe> { let old_aid = self.current_aid.replace(aid); if let Some(old_aid) = old_aid { if old_aid != aid { - let app = Self::find_app(Some(&old_aid), apps).unwrap(); + let app = Self::find_app(&old_aid, apps).unwrap(); // for now all apps will be happy with this. app.deselect(); } @@ -408,7 +402,11 @@ impl<'pipe> ApduDispatch<'pipe> { fn handle_app_command(&mut self, apps: &mut [&mut dyn App], interface: Interface) { // if there is a selected app, send it the command let mut response = response::Data::new(); - if let Some(app) = Self::find_app(self.current_aid.as_ref(), apps) { + if let Some(app) = self + .current_aid + .as_ref() + .and_then(|aid| Self::find_app(aid, apps)) + { let result = match &self.buffer.raw { RawApduBuffer::Request(apdu) => app.call(interface, apdu.as_view(), &mut response), _ => panic!("Unexpected buffer state."), From 0e38403374d519f652010cc93804ed7e4a19ae0f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 8 Oct 2026 19:21:23 +0200 Subject: [PATCH 4/4] Release apdu-dispatch v0.4.1 --- dispatch/CHANGELOG.md | 8 +++++++- dispatch/Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dispatch/CHANGELOG.md b/dispatch/CHANGELOG.md index e20e762..993dde2 100644 --- a/dispatch/CHANGELOG.md +++ b/dispatch/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- + +## [0.4.1] + - Call `App::deselect` for the deselected app, not for the selected app ([#36](https://github.com/trussed-dev/apdu-dispatch/issues/36)) ## [0.4.0] @@ -71,7 +75,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased]: https://github.com/trussed-dev/apdu-dispatch/compare/0.3.1...HEAD +[Unreleased]: https://github.com/trussed-dev/apdu-dispatch/compare/dispatch-0.4.1...HEAD +[0.4.1]: https://github.com/trussed-dev/apdu-dispatch/compare/dispatch-0.4.0...dispatch-0.4.1 +[0.4.0]: https://github.com/trussed-dev/apdu-dispatch/compare/0.3.1...dispatch-0.4.0 [0.3.1]: https://github.com/trussed-dev/apdu-dispatch/compare/0.3.0...0.3.1 [0.3.0]: https://github.com/trussed-dev/apdu-dispatch/compare/0.2.0...0.3.0 [0.2.0]: https://github.com/trussed-dev/apdu-dispatch/compare/0.1.2...0.2.0 diff --git a/dispatch/Cargo.toml b/dispatch/Cargo.toml index dffdba2..2b66239 100644 --- a/dispatch/Cargo.toml +++ b/dispatch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apdu-dispatch" -version = "0.4.0" +version = "0.4.1" description = "Dispatch layer after nfc-device and usbd-ccid" authors.workspace = true