Skip to main content

capsec_macro/
lib.rs

1//! # capsec-macro
2//!
3//! Procedural macros for the `capsec` capability-based security system.
4//!
5//! Provides attribute macros:
6//!
7//! - [`requires`] — declares and validates a function's capability requirements.
8//! - [`deny`] — marks a function as capability-free for the lint tool.
9//! - [`main`] — injects `CapRoot` creation into a function entry point.
10//! - [`context`] — generates `Has<P>` impls and constructor for a capability context struct.
11//!
12//! These macros are re-exported by the `capsec` facade crate. You don't need to
13//! depend on `capsec-macro` directly.
14
15mod resolve;
16
17use proc_macro::TokenStream;
18use quote::{format_ident, quote};
19use syn::punctuated::Punctuated;
20use syn::{FnArg, ItemFn, ItemStruct, Meta, Pat, Token, Type, parse_macro_input};
21
22/// The set of known permission type names (bare idents).
23const KNOWN_PERMISSIONS: &[&str] = &[
24    "FsRead",
25    "FsWrite",
26    "FsAll",
27    "NetConnect",
28    "NetBind",
29    "NetAll",
30    "EnvRead",
31    "EnvWrite",
32    "Spawn",
33    "Ambient",
34];
35
36/// Defines a user-defined permission type for capability-based security.
37///
38/// Generates the `Permission` trait impl (with seal token), `Has<P>` impls for
39/// `Cap<P>` and `SendCap<P>`, and optionally `Subsumes` impls for permission hierarchies.
40///
41/// # Usage
42///
43/// ```rust,ignore
44/// #[capsec::permission]
45/// pub struct DbRead;
46///
47/// #[capsec::permission]
48/// pub struct DbWrite;
49///
50/// #[capsec::permission(subsumes = [DbRead, DbWrite])]
51/// pub struct DbAll;
52/// ```
53///
54/// # What it generates
55///
56/// For `#[capsec::permission] pub struct DbRead;`:
57/// - `impl Permission for DbRead` with the seal token
58/// - `impl Has<DbRead> for Cap<DbRead>`
59/// - `impl Has<DbRead> for SendCap<DbRead>`
60///
61/// For `#[capsec::permission(subsumes = [DbRead, DbWrite])] pub struct DbAll;`:
62/// - All of the above, plus:
63/// - `impl Subsumes<DbRead> for DbAll`
64/// - `impl Has<DbRead> for Cap<DbAll>` and `SendCap<DbAll>`
65/// - Same for `DbWrite`
66#[proc_macro_attribute]
67pub fn permission(attr: TokenStream, item: TokenStream) -> TokenStream {
68    let attr2: proc_macro2::TokenStream = attr.into();
69    let input = parse_macro_input!(item as ItemStruct);
70
71    match permission_inner(attr2, &input) {
72        Ok(tokens) => tokens.into(),
73        Err(e) => e.into_compile_error().into(),
74    }
75}
76
77fn permission_inner(
78    attr: proc_macro2::TokenStream,
79    input: &ItemStruct,
80) -> syn::Result<proc_macro2::TokenStream> {
81    // Validate: must be a unit struct (no fields)
82    match &input.fields {
83        syn::Fields::Unit => {}
84        _ => {
85            return Err(syn::Error::new_spanned(
86                input,
87                "#[capsec::permission] requires a unit struct (no fields)",
88            ));
89        }
90    }
91
92    // Reject generics
93    if !input.generics.params.is_empty() {
94        return Err(syn::Error::new_spanned(
95            &input.generics,
96            "#[capsec::permission] does not support generic structs",
97        ));
98    }
99
100    let struct_name = &input.ident;
101    let struct_vis = &input.vis;
102    let struct_attrs = &input.attrs;
103
104    // Parse subsumes = [...] from attribute
105    let subsumes = parse_subsumes(attr)?;
106
107    // Generate Permission impl with seal token
108    let permission_impl = quote! {
109        impl capsec_core::permission::Permission for #struct_name {
110            type __CapsecSeal = capsec_core::__private::SealProof;
111        }
112    };
113
114    // Note: Has<Self> for Cap<Self> and SendCap<Self> are already covered by
115    // the blanket impls in capsec_core::has:
116    //   impl<P: Permission> Has<P> for Cap<P>
117    //   impl<P: Permission> Has<P> for SendCap<P>
118    // So we only need to generate Subsumes-related Has impls.
119
120    // Generate Subsumes impls + Has<Sub> + CapProvider<Sub> for Cap<Self>/SendCap<Self>
121    let subsumes_impls: Vec<_> = subsumes
122        .iter()
123        .map(|sub| {
124            quote! {
125                impl capsec_core::permission::Subsumes<#sub> for #struct_name {}
126
127                impl capsec_core::has::Has<#sub> for capsec_core::cap::Cap<#struct_name> {
128                    fn cap_ref(&self) -> capsec_core::cap::Cap<#sub> {
129                        capsec_core::cap::Cap::__capsec_new_derived(capsec_core::__private::__capsec_seal())
130                    }
131                }
132
133                impl capsec_core::has::Has<#sub> for capsec_core::cap::SendCap<#struct_name> {
134                    fn cap_ref(&self) -> capsec_core::cap::Cap<#sub> {
135                        capsec_core::cap::Cap::__capsec_new_derived(capsec_core::__private::__capsec_seal())
136                    }
137                }
138
139                impl capsec_core::cap_provider::CapProvider<#sub> for capsec_core::cap::Cap<#struct_name> {
140                    fn provide_cap(&self, _target: &str) -> Result<capsec_core::cap::Cap<#sub>, capsec_core::error::CapSecError> {
141                        Ok(capsec_core::cap::Cap::__capsec_new_derived(capsec_core::__private::__capsec_seal()))
142                    }
143                }
144
145                impl capsec_core::cap_provider::CapProvider<#sub> for capsec_core::cap::SendCap<#struct_name> {
146                    fn provide_cap(&self, _target: &str) -> Result<capsec_core::cap::Cap<#sub>, capsec_core::error::CapSecError> {
147                        Ok(capsec_core::cap::Cap::__capsec_new_derived(capsec_core::__private::__capsec_seal()))
148                    }
149                }
150            }
151        })
152        .collect();
153
154    Ok(quote! {
155        #(#struct_attrs)*
156        #struct_vis struct #struct_name;
157
158        #permission_impl
159        #(#subsumes_impls)*
160    })
161}
162
163/// Parses `subsumes = [A, B, C]` from macro attribute tokens.
164fn parse_subsumes(attr: proc_macro2::TokenStream) -> syn::Result<Vec<syn::Path>> {
165    if attr.is_empty() {
166        return Ok(Vec::new());
167    }
168
169    // Parse as Meta::NameValue: subsumes = [...]
170    let meta: Meta = syn::parse2(attr)?;
171    match &meta {
172        Meta::NameValue(nv) if nv.path.is_ident("subsumes") => {
173            // The value should be an array expression: [A, B, C]
174            if let syn::Expr::Array(arr) = &nv.value {
175                let mut paths = Vec::new();
176                let mut seen = std::collections::HashSet::new();
177                for elem in &arr.elems {
178                    if let syn::Expr::Path(ep) = elem {
179                        let path_str = quote::quote!(#ep).to_string();
180                        if !seen.insert(path_str.clone()) {
181                            return Err(syn::Error::new_spanned(
182                                elem,
183                                format!("duplicate subsumes entry: {}", path_str),
184                            ));
185                        }
186                        paths.push(ep.path.clone());
187                    } else {
188                        return Err(syn::Error::new_spanned(
189                            elem,
190                            "expected a permission type path in subsumes list",
191                        ));
192                    }
193                }
194                Ok(paths)
195            } else {
196                Err(syn::Error::new_spanned(
197                    &nv.value,
198                    "expected an array [A, B, C] for subsumes",
199                ))
200            }
201        }
202        _ => Err(syn::Error::new_spanned(
203            &meta,
204            "expected `subsumes = [...]`",
205        )),
206    }
207}
208
209/// Declares the capability requirements of a function.
210///
211/// When all parameters use `impl Has<P>` bounds, the compiler already enforces
212/// the trait bounds and this macro emits only a `#[doc]` attribute.
213///
214/// When concrete parameter types are used (e.g., context structs), use `on = param`
215/// to identify the capability parameter. The macro emits a compile-time assertion
216/// that the parameter type implements `Has<P>` for each declared permission.
217///
218/// # Usage
219///
220/// ```rust,ignore
221/// // With impl bounds — no `on` needed
222/// #[capsec::requires(fs::read, net::connect)]
223/// fn sync_data(cap: &(impl Has<FsRead> + Has<NetConnect>)) -> Result<()> {
224///     // ...
225/// }
226///
227/// // With concrete context type — use `on = param`
228/// #[capsec::requires(fs::read, net::connect, on = ctx)]
229/// fn sync_data(config: &Config, ctx: &AppCtx) -> Result<()> {
230///     // ...
231/// }
232/// ```
233///
234/// # Supported permission paths
235///
236/// Both shorthand and explicit forms are accepted:
237///
238/// | Shorthand | Explicit | Permission type |
239/// |-----------|----------|-----------------|
240/// | `fs::read` | `FsRead` | `capsec_core::permission::FsRead` |
241/// | `fs::write` | `FsWrite` | `capsec_core::permission::FsWrite` |
242/// | `net::connect` | `NetConnect` | `capsec_core::permission::NetConnect` |
243/// | `net::bind` | `NetBind` | `capsec_core::permission::NetBind` |
244/// | `env::read` | `EnvRead` | `capsec_core::permission::EnvRead` |
245/// | `env::write` | `EnvWrite` | `capsec_core::permission::EnvWrite` |
246/// | `spawn` | `Spawn` | `capsec_core::permission::Spawn` |
247/// | `all` | `Ambient` | `capsec_core::permission::Ambient` |
248#[proc_macro_attribute]
249pub fn requires(attr: TokenStream, item: TokenStream) -> TokenStream {
250    let attr2: proc_macro2::TokenStream = attr.into();
251    let func = parse_macro_input!(item as ItemFn);
252
253    match requires_inner(attr2, &func) {
254        Ok(tokens) => tokens.into(),
255        Err(e) => e.into_compile_error().into(),
256    }
257}
258
259fn requires_inner(
260    attr: proc_macro2::TokenStream,
261    func: &ItemFn,
262) -> syn::Result<proc_macro2::TokenStream> {
263    let metas: Punctuated<Meta, Token![,]> =
264        syn::parse::Parser::parse2(Punctuated::parse_terminated, attr)?;
265
266    // Separate `on = param` from permission metas
267    let mut on_param: Option<syn::Ident> = None;
268    let mut perm_metas: Vec<&Meta> = Vec::new();
269
270    for meta in &metas {
271        if let Meta::NameValue(nv) = meta
272            && nv.path.is_ident("on")
273        {
274            if let syn::Expr::Path(ep) = &nv.value
275                && let Some(ident) = ep.path.get_ident()
276            {
277                on_param = Some(ident.clone());
278                continue;
279            }
280            return Err(syn::Error::new_spanned(&nv.value, "expected an identifier"));
281        }
282        perm_metas.push(meta);
283    }
284
285    // Resolve permission types
286    let mut cap_types = Vec::new();
287    for meta in &perm_metas {
288        cap_types.push(resolve::meta_to_permission_type(meta)?);
289    }
290
291    // Build doc string
292    let doc_string = format!(
293        "capsec::requires({})",
294        cap_types
295            .iter()
296            .map(|c| quote!(#c).to_string())
297            .collect::<Vec<_>>()
298            .join(", ")
299    );
300
301    // Check if any parameter uses `impl` trait bounds
302    let has_impl_bounds = func.sig.inputs.iter().any(|arg| {
303        if let FnArg::Typed(pat_type) = arg {
304            contains_impl_trait(&pat_type.ty)
305        } else {
306            false
307        }
308    });
309
310    // Build assertion block or auto-generate where bounds
311    let mut modified_sig: Option<syn::Signature> = None;
312    let assertion = if let Some(ref param_name) = on_param {
313        // Find the parameter and extract its type
314        let param_type = find_param_type(&func.sig, param_name)?;
315        let inner_type = unwrap_references(&param_type);
316
317        // Check if the inner type is a generic type parameter
318        let generic_ident = func.sig.generics.type_params().find_map(|tp| {
319            if let Type::Path(ref type_path) = *inner_type
320                && type_path.path.is_ident(&tp.ident)
321            {
322                Some(tp.ident.clone())
323            } else {
324                None
325            }
326        });
327
328        if let Some(ref gen_ident) = generic_ident {
329            // Mode 4: Auto-generate CapProvider<P> bounds on the generic type parameter
330            let mut sig = func.sig.clone();
331            let where_clause = sig.generics.make_where_clause();
332            let bounds: Vec<_> = cap_types
333                .iter()
334                .map(|perm_ty| quote! { capsec_core::cap_provider::CapProvider<#perm_ty> })
335                .collect();
336            where_clause
337                .predicates
338                .push(syn::parse2(quote! { #gen_ident: #(#bounds)+* }).unwrap());
339            modified_sig = Some(sig);
340            None // no assertion block needed
341        } else {
342            // Mode 2: Concrete type — generate assertion block
343            let assert_fns: Vec<_> = cap_types
344                .iter()
345                .enumerate()
346                .map(|(i, perm_ty)| {
347                    let fn_name = format_ident!("_assert_has_{}", i);
348                    quote! {
349                        fn #fn_name<T: capsec_core::cap_provider::CapProvider<#perm_ty>>() {}
350                    }
351                })
352                .collect();
353
354            let assert_calls: Vec<_> = (0..cap_types.len())
355                .map(|i| {
356                    let fn_name = format_ident!("_assert_has_{}", i);
357                    quote! { #fn_name::<#inner_type>(); }
358                })
359                .collect();
360
361            Some(quote! {
362                const _: () = {
363                    #(#assert_fns)*
364                    fn _check() {
365                        #(#assert_calls)*
366                    }
367                };
368            })
369        }
370    } else if !has_impl_bounds && !func.sig.inputs.is_empty() && !cap_types.is_empty() {
371        // Concrete types present but no `on` keyword
372        return Err(syn::Error::new_spanned(
373            &func.sig,
374            "#[capsec::requires] on a function with concrete parameter types requires \
375             `on = <param>` to identify the capability parameter.\n\
376             Example: #[capsec::requires(fs::read, on = ctx)]",
377        ));
378    } else {
379        None
380    };
381
382    let func_vis = &func.vis;
383    let func_sig = modified_sig.as_ref().unwrap_or(&func.sig);
384    let func_block = &func.block;
385    let func_attrs = &func.attrs;
386
387    Ok(quote! {
388        #(#func_attrs)*
389        #[doc = #doc_string]
390        #func_vis #func_sig {
391            #assertion
392            #func_block
393        }
394    })
395}
396
397fn contains_impl_trait(ty: &Type) -> bool {
398    match ty {
399        Type::ImplTrait(_) => true,
400        Type::Reference(r) => contains_impl_trait(&r.elem),
401        Type::Paren(p) => contains_impl_trait(&p.elem),
402        _ => false,
403    }
404}
405
406fn find_param_type(sig: &syn::Signature, name: &syn::Ident) -> syn::Result<Type> {
407    for arg in &sig.inputs {
408        if let FnArg::Typed(pat_type) = arg
409            && let Pat::Ident(pi) = &*pat_type.pat
410            && pi.ident == *name
411        {
412            return Ok((*pat_type.ty).clone());
413        }
414    }
415    Err(syn::Error::new_spanned(
416        name,
417        format!("parameter '{}' not found in function signature", name),
418    ))
419}
420
421fn unwrap_references(ty: &Type) -> &Type {
422    match ty {
423        Type::Reference(r) => unwrap_references(&r.elem),
424        Type::Paren(p) => unwrap_references(&p.elem),
425        _ => ty,
426    }
427}
428
429/// Marks a function as capability-free.
430///
431/// This is a declaration for the `cargo capsec check` lint tool — any ambient
432/// authority call found inside a `#[deny]` function will be flagged as a violation.
433///
434/// The macro itself does not enforce anything at compile time (there's no type-system
435/// mechanism to prevent `std::fs` imports). Enforcement is in the lint tool.
436///
437/// # Usage
438///
439/// ```rust,ignore
440/// // Deny all I/O
441/// #[capsec::deny(all)]
442/// fn pure_transform(input: &[u8]) -> Vec<u8> {
443///     input.iter().map(|b| b.wrapping_add(1)).collect()
444/// }
445///
446/// // Deny only network access
447/// #[capsec::deny(net)]
448/// fn local_only(cap: &impl Has<FsRead>) -> Vec<u8> {
449///     capsec::fs::read("/tmp/data", cap).unwrap()
450/// }
451/// ```
452///
453/// # Supported categories
454///
455/// `all`, `fs`, `net`, `env`, `process`
456#[proc_macro_attribute]
457pub fn deny(attr: TokenStream, item: TokenStream) -> TokenStream {
458    let denied = parse_macro_input!(attr with Punctuated::<Meta, Token![,]>::parse_terminated);
459
460    let item_clone: proc_macro2::TokenStream = item.clone().into();
461    let func = match syn::parse::<ItemFn>(item) {
462        Ok(f) => f,
463        Err(e) => {
464            let err = e.into_compile_error();
465            return quote! { #err #item_clone }.into();
466        }
467    };
468
469    let deny_names: Vec<String> = denied
470        .iter()
471        .map(|meta| {
472            meta.path()
473                .get_ident()
474                .map(|i| i.to_string())
475                .unwrap_or_default()
476        })
477        .collect();
478
479    let doc_string = format!("capsec::deny({})", deny_names.join(", "));
480
481    let func_vis = &func.vis;
482    let func_sig = &func.sig;
483    let func_block = &func.block;
484    let func_attrs = &func.attrs;
485
486    let expanded = quote! {
487        #(#func_attrs)*
488        #[doc = #doc_string]
489        #func_vis #func_sig
490            #func_block
491    };
492
493    expanded.into()
494}
495
496/// Injects `CapRoot` creation into a function entry point.
497///
498/// Removes the first parameter (which must be typed as `CapRoot`) and prepends
499/// `let {param_name} = capsec::root();` to the function body.
500///
501/// # Usage
502///
503/// ```rust,ignore
504/// #[capsec::main]
505/// fn main(root: CapRoot) {
506///     let fs = root.fs_read();
507///     // ...
508/// }
509/// ```
510///
511/// # With `#[tokio::main]`
512///
513/// Place `#[capsec::main]` above `#[tokio::main]`:
514///
515/// ```rust,ignore
516/// #[capsec::main]
517/// #[tokio::main]
518/// async fn main(root: CapRoot) { ... }
519/// ```
520#[proc_macro_attribute]
521pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
522    let func = parse_macro_input!(item as ItemFn);
523
524    match main_inner(&func) {
525        Ok(tokens) => tokens.into(),
526        Err(e) => e.into_compile_error().into(),
527    }
528}
529
530fn main_inner(func: &ItemFn) -> syn::Result<proc_macro2::TokenStream> {
531    if func.sig.inputs.is_empty() {
532        if func.sig.asyncness.is_some() {
533            return Err(syn::Error::new_spanned(
534                &func.sig,
535                "#[capsec::main] found no CapRoot parameter. If combining with #[tokio::main], \
536                 place #[capsec::main] above #[tokio::main]:\n\n  \
537                 #[capsec::main]\n  \
538                 #[tokio::main]\n  \
539                 async fn main(root: CapRoot) { ... }",
540            ));
541        }
542        return Err(syn::Error::new_spanned(
543            &func.sig,
544            "#[capsec::main] expected first parameter of type CapRoot",
545        ));
546    }
547
548    // Extract first parameter
549    let first_arg = &func.sig.inputs[0];
550    let (param_name, param_type) = match first_arg {
551        FnArg::Typed(pat_type) => {
552            let name = if let Pat::Ident(pi) = &*pat_type.pat {
553                pi.ident.clone()
554            } else {
555                return Err(syn::Error::new_spanned(
556                    &pat_type.pat,
557                    "#[capsec::main] expected a simple identifier for the CapRoot parameter",
558                ));
559            };
560            (name, &*pat_type.ty)
561        }
562        FnArg::Receiver(r) => {
563            return Err(syn::Error::new_spanned(
564                r,
565                "#[capsec::main] cannot be used on methods with self",
566            ));
567        }
568    };
569
570    // Validate type is CapRoot
571    let type_str = quote!(#param_type).to_string().replace(' ', "");
572    if type_str != "CapRoot" && type_str != "capsec::CapRoot" {
573        return Err(syn::Error::new_spanned(
574            param_type,
575            "first parameter must be CapRoot",
576        ));
577    }
578
579    // Build new signature without the first parameter
580    let remaining_params: Vec<_> = func.sig.inputs.iter().skip(1).collect();
581    let func_attrs = &func.attrs;
582    let func_vis = &func.vis;
583    let func_name = &func.sig.ident;
584    let func_generics = &func.sig.generics;
585    let func_output = &func.sig.output;
586    let func_asyncness = &func.sig.asyncness;
587    let func_block = &func.block;
588
589    Ok(quote! {
590        #(#func_attrs)*
591        #func_vis #func_asyncness fn #func_name #func_generics(#(#remaining_params),*) #func_output {
592            let #param_name = capsec::root();
593            #func_block
594        }
595    })
596}
597
598/// Transforms a struct with permission-type fields into a capability context.
599///
600/// Generates:
601/// - Field types rewritten from `PermType` to `Cap<PermType>` (or `SendCap<PermType>`)
602/// - A `new(root: &CapRoot) -> Self` constructor
603/// - `impl Has<P>` for each field's permission type
604///
605/// # Usage
606///
607/// ```rust,ignore
608/// #[capsec::context]
609/// struct AppCtx {
610///     fs: FsRead,
611///     net: NetConnect,
612/// }
613///
614/// // Send variant for async/threaded code:
615/// #[capsec::context(send)]
616/// struct AsyncCtx {
617///     fs: FsRead,
618///     net: NetConnect,
619/// }
620/// ```
621#[proc_macro_attribute]
622pub fn context(attr: TokenStream, item: TokenStream) -> TokenStream {
623    let attr2: proc_macro2::TokenStream = attr.into();
624    let input = parse_macro_input!(item as ItemStruct);
625
626    match context_inner(attr2, &input) {
627        Ok(tokens) => tokens.into(),
628        Err(e) => e.into_compile_error().into(),
629    }
630}
631
632fn context_inner(
633    attr: proc_macro2::TokenStream,
634    input: &ItemStruct,
635) -> syn::Result<proc_macro2::TokenStream> {
636    // Parse `send` flag
637    let attr_str = attr.to_string();
638    let is_send = match attr_str.trim() {
639        "" => false,
640        "send" => true,
641        other => {
642            return Err(syn::Error::new_spanned(
643                &attr,
644                format!("unexpected attribute '{}', expected empty or 'send'", other),
645            ));
646        }
647    };
648
649    // Reject generics
650    if !input.generics.params.is_empty() {
651        return Err(syn::Error::new_spanned(
652            &input.generics,
653            "#[capsec::context] does not support generic structs",
654        ));
655    }
656
657    // Get named fields
658    let fields = match &input.fields {
659        syn::Fields::Named(f) => f,
660        _ => {
661            return Err(syn::Error::new_spanned(
662                input,
663                "#[capsec::context] requires a struct with named fields",
664            ));
665        }
666    };
667
668    // Validate fields and collect permission info
669    // Each entry: (field_name, resolved_perm_type_tokens)
670    let mut field_infos: Vec<(syn::Ident, proc_macro2::TokenStream)> = Vec::new();
671    let mut seen_perms: std::collections::HashSet<String> = std::collections::HashSet::new();
672
673    for field in &fields.named {
674        let field_name = field.ident.as_ref().unwrap().clone();
675        let ty = &field.ty;
676
677        // Check for tuple types
678        if let Type::Tuple(_) = ty {
679            return Err(syn::Error::new_spanned(
680                ty,
681                "tuple permission types are not supported in context structs — use separate fields instead",
682            ));
683        }
684
685        // Extract type path
686        let (perm_key, perm_tokens) = match ty {
687            Type::Path(tp) => {
688                if let Some(seg) = tp.path.segments.last() {
689                    let ident_str = seg.ident.to_string();
690                    // Known built-in? Qualify with capsec_core::permission::
691                    if KNOWN_PERMISSIONS.contains(&ident_str.as_str()) {
692                        let ident = &seg.ident;
693                        (ident_str, quote! { capsec_core::permission::#ident })
694                    } else {
695                        // Custom permission — pass through original type path
696                        (ident_str, quote! { #tp })
697                    }
698                } else {
699                    return Err(syn::Error::new_spanned(
700                        ty,
701                        format!("field '{}' has an empty type path", field_name,),
702                    ));
703                }
704            }
705            _ => {
706                return Err(syn::Error::new_spanned(
707                    ty,
708                    format!(
709                        "field '{}' has type '{}', which is not a valid permission type",
710                        field_name,
711                        quote!(#ty),
712                    ),
713                ));
714            }
715        };
716
717        // Check for duplicates
718        if !seen_perms.insert(perm_key.clone()) {
719            return Err(syn::Error::new_spanned(
720                ty,
721                format!(
722                    "duplicate permission type '{}' — each permission can only appear once in a context struct",
723                    perm_key
724                ),
725            ));
726        }
727
728        field_infos.push((field_name, perm_tokens));
729    }
730
731    let struct_name = &input.ident;
732    let struct_vis = &input.vis;
733    let struct_attrs = &input.attrs;
734
735    // Generate struct fields with rewritten types
736    let struct_fields: Vec<_> = field_infos
737        .iter()
738        .map(|(name, perm)| {
739            if is_send {
740                quote! { #name: capsec_core::cap::SendCap<#perm> }
741            } else {
742                quote! { #name: capsec_core::cap::Cap<#perm> }
743            }
744        })
745        .collect();
746
747    // Generate constructor fields
748    let constructor_fields: Vec<_> = field_infos
749        .iter()
750        .map(|(name, perm)| {
751            if is_send {
752                quote! { #name: root.grant::<#perm>().make_send() }
753            } else {
754                quote! { #name: root.grant::<#perm>() }
755            }
756        })
757        .collect();
758
759    // Generate Has<P> impls
760    let has_impls: Vec<_> = field_infos
761        .iter()
762        .map(|(name, perm)| {
763            quote! {
764                impl capsec_core::has::Has<#perm> for #struct_name {
765                    fn cap_ref(&self) -> capsec_core::cap::Cap<#perm> {
766                        self.#name.cap_ref()
767                    }
768                }
769            }
770        })
771        .collect();
772
773    // Generate CapProvider<P> impls (delegates to Has<P>)
774    let cap_provider_impls: Vec<_> = field_infos
775        .iter()
776        .map(|(_name, perm)| {
777            quote! {
778                impl capsec_core::cap_provider::CapProvider<#perm> for #struct_name {
779                    fn provide_cap(&self, _target: &str) -> Result<capsec_core::cap::Cap<#perm>, capsec_core::error::CapSecError> {
780                        Ok(<Self as capsec_core::has::Has<#perm>>::cap_ref(self))
781                    }
782                }
783            }
784        })
785        .collect();
786
787    Ok(quote! {
788        #(#struct_attrs)*
789        #struct_vis struct #struct_name {
790            #(#struct_fields,)*
791        }
792
793        impl #struct_name {
794            /// Creates a new context by granting all capabilities from the root.
795            pub fn new(root: &capsec_core::root::CapRoot) -> Self {
796                Self {
797                    #(#constructor_fields,)*
798                }
799            }
800        }
801
802        #(#has_impls)*
803        #(#cap_provider_impls)*
804    })
805}