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"] } 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, diff --git a/derive/src/lib.rs b/derive/src/lib.rs index bd33f9e42d8..ca4a63fbd01 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,4 +1,7 @@ -#![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). mod dispatch; mod extension_dispatch; @@ -12,6 +15,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 +102,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 +323,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);