From 3c0c6d55bd1b640e22b693710db9e8257805ff4f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 21 Sep 2026 10:14:51 +0200 Subject: [PATCH 1/4] derive: Ensure that all declared extensions are implemented Declaring an extension but not implementing it with any backend is most likely a mistake. Ideally, we would trigger a warning in this case, but this is not possible with stable Rust at the moment. So we have to emit an error instead. To test this, comment out the ASample backend in the extension-dispatch example. --- derive/src/extension_dispatch.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/derive/src/extension_dispatch.rs b/derive/src/extension_dispatch.rs index d16a73854dd..c69807b8fbe 100644 --- a/derive/src/extension_dispatch.rs +++ b/derive/src/extension_dispatch.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use proc_macro2::TokenStream; use quote::quote; @@ -47,12 +47,33 @@ impl ExtensionDispatch { )?); } } - let delegated_backends = delegated_backends + let delegated_backends: Vec<_> = delegated_backends .into_iter() .map(|(raw, delegate_to)| { DelegatedBackend::new(raw, delegate_to, &backends, &extension_attrs.extensions) }) .collect::>()?; + + // ensure that all extensions that are declared are implemented at least once + let all_extensions: HashSet<_> = extension_attrs.extensions.keys().collect(); + let backend_extensions = backends.iter().flat_map(|backend| &backend.extensions); + let delegated_backend_extensions = delegated_backends + .iter() + .flat_map(|backend| &backend.extensions); + let implemented_extensions: HashSet<_> = backend_extensions + .chain(delegated_backend_extensions) + .map(|extension| &extension.id) + .collect(); + let mut missing_extensions = all_extensions.difference(&implemented_extensions); + // TODO: this should be a loop with warnings instead, but this is currently not possible + // with stable Rust + if let Some(e) = missing_extensions.next() { + return Err(Error::new_spanned( + e, + format!("Extension {e} declared but not implemented by any backend"), + )); + } + Ok(Self { name: input.ident, generics: input.generics, From 693354651f0b86d80385059bca8ee9569ce3c8d8 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 21 Sep 2026 11:17:26 +0200 Subject: [PATCH 2/4] derive: Add doc comments --- derive/src/lib.rs | 335 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index bd33f9e42d8..292c0c15c2a 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,5 +1,7 @@ #![deny(clippy::expect_used, clippy::panic, clippy::unwrap_used)] +//! Derive macros for [`trussed`](https://docs.rs/trussed). + mod dispatch; mod extension_dispatch; mod extension_id; @@ -12,6 +14,84 @@ use dispatch::Dispatch; use extension_dispatch::ExtensionDispatch; use extension_id::ExtensionId; +/// Derives the [`trussed::backend::Dispatch`][] trait. +/// +/// This macro can only be used on structs with named fields. Options can be set using the +/// `dispatch` attribute on the struct. +/// +/// # Struct Options +/// +/// ```rust +/// # use trussed_derive::Dispatch; +/// # enum Backend { A } +/// # struct ABackend {} +/// # impl trussed::backend::Backend for ABackend { type Context = (); } +/// #[derive(Dispatch)] +/// #[dispatch(backend_id = "Backend")] +/// struct Dispatch { +/// // ... +/// # a: ABackend, +/// } +/// ``` +/// +/// ## `dispatch` attribute +/// +/// - `backend_id` (required): the name of the type for `Dispatch::BackendId`. +/// This type must be an enum with one variant per field of the struct. +/// The variant names must be the field names in camel case. +/// +/// # Example +/// +/// ```rust +/// # use trussed_derive::Dispatch; +/// # struct ManageBackend {} +/// # impl trussed::backend::Backend for ManageBackend { type Context = (); } +/// enum Backend { +/// Rsa, +/// } +/// +/// #[derive(Dispatch)] +/// #[dispatch(backend_id = "Backend")] +/// struct Dispatch { +/// rsa: trussed_rsa_alloc::SoftwareRsa, +/// } +/// ``` +/// +/// This generates the following implementation: +/// ```rust +/// # use trussed_derive::Dispatch; +/// # enum Backend { Rsa } +/// # struct Dispatch { +/// # rsa: trussed_rsa_alloc::SoftwareRsa, +/// # } +/// use trussed::{ +/// backend::{self, Backend as _}, +/// platform::Platform, service::ServiceResources, types::Context, +/// }; +/// use trussed_core::{api::{Reply, Request}, Error}; +/// +/// impl backend::Dispatch for Dispatch { +/// type BackendId = Backend; +/// type Context = ( +/// ::Context, +/// ); +/// +/// fn request( +/// &mut self, +/// backend: &Self::BackendId, +/// ctx: &mut Context, +/// request: &Request, +/// resources: &mut ServiceResources

, +/// ) -> Result { +/// match backend { +/// Backend::Rsa => +/// self.rsa.request(&mut ctx.core, &mut ctx.backends.0, request, resources), +/// } +/// } +/// } +/// ``` +/// +/// [`trussed::backend::Dispatch`]: https://docs.rs/trussed/latest/trussed/backend/trait.Dispatch.html #[proc_macro_derive(Dispatch, attributes(dispatch))] pub fn derive_dispatch(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -21,6 +101,218 @@ pub fn derive_dispatch(input: TokenStream) -> TokenStream { .into() } +/// Derives the [`trussed::backend::ExtensionDispatch`][] trait. +/// +/// This macro can only be used on structs with named fields. Options can be set using the +/// `dispatch` and `extensions` attributes on the struct or on the fields. +/// +/// # Struct Options +/// +/// ```rust +/// # use trussed_derive::{ExtensionId, ExtensionDispatch}; +/// # enum Backend { +/// # Staging, +/// # StagingManage, +/// # } +/// # #[derive(ExtensionId)] +/// # enum Extension { +/// # FsInfo = 0, +/// # Manage = 1, +/// # } +/// #[derive(ExtensionDispatch)] +/// #[dispatch(backend_id = "Backend", extension_id = "Extension")] +/// #[extensions( +/// FsInfo = "trussed_fs_info::FsInfoExtension", +/// Manage = "trussed_manage::ManageExtension" +/// )] +/// struct Dispatch { +/// // ... +/// # #[extensions("FsInfo")] +/// # staging: trussed_staging::StagingBackend, +/// # #[dispatch(delegate_to = "staging", no_core)] +/// # #[extensions("Manage")] +/// # staging_manage: (), +/// } +/// ```` +/// +/// ## `dispatch` attribute +/// +/// - `backend_id` (required): see [`Dispatch`][]. +/// Fields with the `#[dispatch(skip)]` attribute don’t need a corresponding enum variant. +/// - `extension_id` (required): the name of the type for `ExtensionDispatch::ExtensionID`. +/// This type must be an enum with one variant per extension defined in the `#[extensions(...)]` +/// attribute. +/// +/// ## `extensions` attribute +/// +/// The required `extensions` attribute defines a map from extension IDs to their traits. +/// +/// # Field Options +/// +/// ```rust +/// # use trussed_derive::{ExtensionId, ExtensionDispatch}; +/// # enum Backend { +/// # Staging, +/// # } +/// # #[derive(ExtensionId)] +/// # enum Extension { +/// # FsInfo = 0, +/// # Manage = 1, +/// # } +/// #[derive(ExtensionDispatch)] +/// // ... +/// # #[dispatch(backend_id = "Backend", extension_id = "Extension")] +/// # #[extensions( +/// # FsInfo = "trussed_fs_info::FsInfoExtension", +/// # Manage = "trussed_manage::ManageExtension" +/// # )] +/// struct Dispatch { +/// # #[extensions("FsInfo", "Manage")] +/// # staging: trussed_staging::StagingBackend, +/// #[dispatch(delegate_to = "staging", no_core, skip)] +/// #[extensions("FsInfo", "Manage")] +/// staging_manage: trussed_staging::StagingBackend, +/// // ... +/// } +/// ```` +/// +/// ## `dispatch` attribute +/// +/// - `delegate_to`: if set, send requests to the backend stored in the given field. +/// - `no_core`: if set, let this backend only handle extension requests, not core requests. +/// - `skip`: if set, do not treat this field as a backend. +/// +/// ## `extensions` attribute +/// +/// The optional `extensions` attribute defines a list of extension IDs for this backend. +/// +/// # Example +/// +/// ```rust +/// # use trussed_derive::{ExtensionDispatch, ExtensionId}; +/// enum Backend { +/// Staging, +/// StagingManage, +/// Rsa, +/// } +/// +/// #[derive(ExtensionId)] +/// enum Extension { +/// FsInfo = 0, +/// Manage = 1, +/// } +/// +/// #[derive(ExtensionDispatch)] +/// #[dispatch(backend_id = "Backend", extension_id = "Extension")] +/// #[extensions( +/// FsInfo = "trussed_fs_info::FsInfoExtension", +/// Manage = "trussed_manage::ManageExtension" +/// )] +/// struct Dispatch { +/// #[extensions("FsInfo")] +/// staging: trussed_staging::StagingBackend, +/// +/// #[dispatch(delegate_to = "staging", no_core)] +/// #[extensions("Manage")] +/// staging_manage: (), +/// +/// rsa: trussed_rsa_alloc::SoftwareRsa, +/// +/// #[dispatch(skip)] +/// other: String, +/// } +/// ``` +/// +/// This generates the following implementation: +/// ```rust +/// # use trussed_derive::ExtensionId; +/// # enum Backend { +/// # Staging, +/// # StagingManage, +/// # Rsa, +/// # } +/// # #[derive(ExtensionId)] +/// # enum Extension { +/// # FsInfo = 0, +/// # Manage = 1, +/// # } +/// # struct Dispatch { +/// # staging: trussed_staging::StagingBackend, +/// # staging_manage: (), +/// # rsa: trussed_rsa_alloc::SoftwareRsa, +/// # other: String, +/// # } +/// use trussed::{ +/// backend::{self, Backend as _}, +/// platform::Platform, +/// serde_extensions::{ExtensionDispatch, ExtensionImpl}, +/// service::ServiceResources, +/// types::Context, +/// }; +/// use trussed_core::{api::{reply, Reply, request, Request}, Error}; +/// +/// impl ExtensionDispatch for Dispatch { +/// type BackendId = Backend; +/// type ExtensionId = Extension; +/// type Context = ( +/// ::Context, +/// ::Context, +/// ); +/// +/// fn core_request( +/// &mut self, +/// backend: &Self::BackendId, +/// ctx: &mut Context, +/// request: &Request, +/// resources: &mut ServiceResources

, +/// ) -> Result { +/// match backend { +/// Backend::Staging => +/// self.staging.request(&mut ctx.core, &mut ctx.backends.0, request, resources), +/// Backend::StagingManage => Err(Error::RequestNotAvailable), +/// Backend::Rsa => +/// self.rsa.request(&mut ctx.core, &mut ctx.backends.1, request, resources), +/// } +/// } +/// +/// fn extension_request( +/// &mut self, +/// backend: &Self::BackendId, +/// extension: &Self::ExtensionId, +/// ctx: &mut Context, +/// request: &request::SerdeExtension, +/// resources: &mut ServiceResources

, +/// ) -> Result { +/// match backend { +/// Backend::Staging => match extension { +/// Self::ExtensionId::FsInfo => +/// ExtensionImpl::::extension_request_serialized( +/// &mut self.staging, +/// &mut ctx.core, +/// &mut ctx.backends.0, +/// request, +/// resources, +/// ), +/// _ => Err(Error::RequestNotAvailable), +/// } +/// Backend::StagingManage => match extension { +/// Self::ExtensionId::Manage => +/// ExtensionImpl::::extension_request_serialized( +/// &mut self.staging, +/// &mut ctx.core, +/// &mut ctx.backends.0, +/// request, +/// resources, +/// ), +/// _ => Err(Error::RequestNotAvailable), +/// } +/// Backend::Rsa => Err(Error::RequestNotAvailable), +/// } +/// } +/// } +/// ``` +/// +/// [`trussed::backend::ExtensionDispatch`]: https://docs.rs/trussed/latest/trussed/backend/trait.ExtensionDispatch.html #[proc_macro_derive(ExtensionDispatch, attributes(dispatch, extensions))] pub fn derive_extension_dispatch(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -30,6 +322,49 @@ pub fn derive_extension_dispatch(input: TokenStream) -> TokenStream { .into() } +/// Derives conversion traits for extension ID enums. +/// +/// This macro can only be used on enums. All variants must have an explicit discriminant. The +/// derive macro generates `From for u8` and `TryFrom for T` when used on `T`. +/// +/// # Example +/// +/// ```rust +/// # use trussed_derive::ExtensionId; +/// #[derive(ExtensionId)] +/// enum Extension { +/// Auth = 0, +/// Manage = 1, +/// } +/// ``` +/// +/// This generates the following implementations: +/// ```rust +/// # enum Extension { +/// # Auth = 0, +/// # Manage = 1, +/// # } +/// impl From for u8 { +/// fn from(extension: Extension) -> u8 { +/// match extension { +/// Extension::Auth => 0, +/// Extension::Manage => 1, +/// } +/// } +/// } +/// +/// impl TryFrom for Extension { +/// type Error = trussed_core::Error; +/// +/// fn try_from(value: u8) -> Result { +/// match value { +/// 0 => Ok(Extension::Auth), +/// 1 => Ok(Extension::Manage), +/// _ => Err(trussed_core::Error::InternalError), +/// } +/// } +/// } +/// ``` #[proc_macro_derive(ExtensionId)] pub fn derive_extension_id(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); From fe7c38bf95e7ef81ef63f98b3f755f79d459638c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 21 Sep 2026 15:39:28 +0200 Subject: [PATCH 3/4] derive: Enable additional lints --- derive/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 292c0c15c2a..ca4a63fbd01 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,4 +1,5 @@ -#![deny(clippy::expect_used, clippy::panic, clippy::unwrap_used)] +#![deny(unsafe_code, clippy::expect_used, clippy::panic, clippy::unwrap_used)] +#![warn(missing_docs)] //! Derive macros for [`trussed`](https://docs.rs/trussed). From 3a68a787b47f53f84344a8ba3346242f89d3e6a6 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 21 Sep 2026 15:45:00 +0200 Subject: [PATCH 4/4] Release trussed-derive v0.1.0 --- .github/workflows/cd-test.yml | 2 ++ derive/CHANGELOG.md | 9 +++++++++ derive/Cargo.toml | 9 +++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 derive/CHANGELOG.md diff --git a/.github/workflows/cd-test.yml b/.github/workflows/cd-test.yml index d63d8575c17..d85f19f41e9 100644 --- a/.github/workflows/cd-test.yml +++ b/.github/workflows/cd-test.yml @@ -14,5 +14,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - run: cargo publish --dry-run --package trussed-core if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-publish-check-trussed-core') }} + - run: cargo publish --dry-run --package trussed-derive + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-publish-check-trussed-derive') }} - run: cargo publish --dry-run --package trussed if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-publish-check-trussed') }} diff --git a/derive/CHANGELOG.md b/derive/CHANGELOG.md new file mode 100644 index 00000000000..6a0cfc04ff0 --- /dev/null +++ b/derive/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +- + +## [v0.1.0](https://github.com/trussed-dev/trussed/releases/tag/derive-v0.1.0) (2026-09-21) + +Initial release compatible with `trussed` v0.2 and `trussed-core` v0.2. diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 586996db6c5..ce202177d58 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "trussed-derive" version = "0.1.0" +description = "Derive macros for the trussed crate" authors.workspace = true edition.workspace = true @@ -17,5 +18,9 @@ syn = "2.0.53" [dev-dependencies] serde = { version = "1.0", default-features = false } -trussed = { path = "..", features = ["serde-extensions", "virt"] } -trussed-core = { path = "../core", features = ["serde-extensions"] } +trussed = { version = "0.2", features = ["serde-extensions", "virt"] } +trussed-core = { version = "0.2", features = ["serde-extensions"] } +trussed-fs-info = "0.3" +trussed-manage = "0.3" +trussed-rsa-alloc = "0.5" +trussed-staging = { version = "0.5", features = ["fs-info", "manage"] }