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/// Declares the capability requirements of a function.
37///
38/// When all parameters use `impl Has<P>` bounds, the compiler already enforces
39/// the trait bounds and this macro emits only a `#[doc]` attribute.
40///
41/// When concrete parameter types are used (e.g., context structs), use `on = param`
42/// to identify the capability parameter. The macro emits a compile-time assertion
43/// that the parameter type implements `Has<P>` for each declared permission.
44///
45/// # Usage
46///
47/// ```rust,ignore
48/// // With impl bounds — no `on` needed
49/// #[capsec::requires(fs::read, net::connect)]
50/// fn sync_data(cap: &(impl Has<FsRead> + Has<NetConnect>)) -> Result<()> {
51///     // ...
52/// }
53///
54/// // With concrete context type — use `on = param`
55/// #[capsec::requires(fs::read, net::connect, on = ctx)]
56/// fn sync_data(config: &Config, ctx: &AppCtx) -> Result<()> {
57///     // ...
58/// }
59/// ```
60///
61/// # Supported permission paths
62///
63/// Both shorthand and explicit forms are accepted:
64///
65/// | Shorthand | Explicit | Permission type |
66/// |-----------|----------|-----------------|
67/// | `fs::read` | `FsRead` | `capsec_core::permission::FsRead` |
68/// | `fs::write` | `FsWrite` | `capsec_core::permission::FsWrite` |
69/// | `net::connect` | `NetConnect` | `capsec_core::permission::NetConnect` |
70/// | `net::bind` | `NetBind` | `capsec_core::permission::NetBind` |
71/// | `env::read` | `EnvRead` | `capsec_core::permission::EnvRead` |
72/// | `env::write` | `EnvWrite` | `capsec_core::permission::EnvWrite` |
73/// | `spawn` | `Spawn` | `capsec_core::permission::Spawn` |
74/// | `all` | `Ambient` | `capsec_core::permission::Ambient` |
75#[proc_macro_attribute]
76pub fn requires(attr: TokenStream, item: TokenStream) -> TokenStream {
77    let attr2: proc_macro2::TokenStream = attr.into();
78    let func = parse_macro_input!(item as ItemFn);
79
80    match requires_inner(attr2, &func) {
81        Ok(tokens) => tokens.into(),
82        Err(e) => e.into_compile_error().into(),
83    }
84}
85
86fn requires_inner(
87    attr: proc_macro2::TokenStream,
88    func: &ItemFn,
89) -> syn::Result<proc_macro2::TokenStream> {
90    let metas: Punctuated<Meta, Token![,]> =
91        syn::parse::Parser::parse2(Punctuated::parse_terminated, attr)?;
92
93    // Separate `on = param` from permission metas
94    let mut on_param: Option<syn::Ident> = None;
95    let mut perm_metas: Vec<&Meta> = Vec::new();
96
97    for meta in &metas {
98        if let Meta::NameValue(nv) = meta
99            && nv.path.is_ident("on")
100        {
101            if let syn::Expr::Path(ep) = &nv.value
102                && let Some(ident) = ep.path.get_ident()
103            {
104                on_param = Some(ident.clone());
105                continue;
106            }
107            return Err(syn::Error::new_spanned(&nv.value, "expected an identifier"));
108        }
109        perm_metas.push(meta);
110    }
111
112    // Resolve permission types
113    let mut cap_types = Vec::new();
114    for meta in &perm_metas {
115        cap_types.push(resolve::meta_to_permission_type(meta)?);
116    }
117
118    // Build doc string
119    let doc_string = format!(
120        "capsec::requires({})",
121        cap_types
122            .iter()
123            .map(|c| quote!(#c).to_string())
124            .collect::<Vec<_>>()
125            .join(", ")
126    );
127
128    // Check if any parameter uses `impl` trait bounds
129    let has_impl_bounds = func.sig.inputs.iter().any(|arg| {
130        if let FnArg::Typed(pat_type) = arg {
131            contains_impl_trait(&pat_type.ty)
132        } else {
133            false
134        }
135    });
136
137    // Build assertion block if needed
138    let assertion = if let Some(ref param_name) = on_param {
139        // Find the parameter and extract its type
140        let param_type = find_param_type(&func.sig, param_name)?;
141        let inner_type = unwrap_references(&param_type);
142
143        let assert_fns: Vec<_> = cap_types
144            .iter()
145            .enumerate()
146            .map(|(i, perm_ty)| {
147                let fn_name = format_ident!("_assert_has_{}", i);
148                quote! {
149                    fn #fn_name<T: capsec_core::has::Has<#perm_ty>>() {}
150                }
151            })
152            .collect();
153
154        let assert_calls: Vec<_> = (0..cap_types.len())
155            .map(|i| {
156                let fn_name = format_ident!("_assert_has_{}", i);
157                quote! { #fn_name::<#inner_type>(); }
158            })
159            .collect();
160
161        Some(quote! {
162            const _: () = {
163                #(#assert_fns)*
164                fn _check() {
165                    #(#assert_calls)*
166                }
167            };
168        })
169    } else if !has_impl_bounds && !func.sig.inputs.is_empty() && !cap_types.is_empty() {
170        // Concrete types present but no `on` keyword
171        return Err(syn::Error::new_spanned(
172            &func.sig,
173            "#[capsec::requires] on a function with concrete parameter types requires \
174             `on = <param>` to identify the capability parameter.\n\
175             Example: #[capsec::requires(fs::read, on = ctx)]",
176        ));
177    } else {
178        None
179    };
180
181    let func_vis = &func.vis;
182    let func_sig = &func.sig;
183    let func_block = &func.block;
184    let func_attrs = &func.attrs;
185
186    Ok(quote! {
187        #(#func_attrs)*
188        #[doc = #doc_string]
189        #func_vis #func_sig {
190            #assertion
191            #func_block
192        }
193    })
194}
195
196fn contains_impl_trait(ty: &Type) -> bool {
197    match ty {
198        Type::ImplTrait(_) => true,
199        Type::Reference(r) => contains_impl_trait(&r.elem),
200        Type::Paren(p) => contains_impl_trait(&p.elem),
201        _ => false,
202    }
203}
204
205fn find_param_type(sig: &syn::Signature, name: &syn::Ident) -> syn::Result<Type> {
206    for arg in &sig.inputs {
207        if let FnArg::Typed(pat_type) = arg
208            && let Pat::Ident(pi) = &*pat_type.pat
209            && pi.ident == *name
210        {
211            return Ok((*pat_type.ty).clone());
212        }
213    }
214    Err(syn::Error::new_spanned(
215        name,
216        format!("parameter '{}' not found in function signature", name),
217    ))
218}
219
220fn unwrap_references(ty: &Type) -> &Type {
221    match ty {
222        Type::Reference(r) => unwrap_references(&r.elem),
223        Type::Paren(p) => unwrap_references(&p.elem),
224        _ => ty,
225    }
226}
227
228/// Marks a function as capability-free.
229///
230/// This is a declaration for the `cargo capsec check` lint tool — any ambient
231/// authority call found inside a `#[deny]` function will be flagged as a violation.
232///
233/// The macro itself does not enforce anything at compile time (there's no type-system
234/// mechanism to prevent `std::fs` imports). Enforcement is in the lint tool.
235///
236/// # Usage
237///
238/// ```rust,ignore
239/// // Deny all I/O
240/// #[capsec::deny(all)]
241/// fn pure_transform(input: &[u8]) -> Vec<u8> {
242///     input.iter().map(|b| b.wrapping_add(1)).collect()
243/// }
244///
245/// // Deny only network access
246/// #[capsec::deny(net)]
247/// fn local_only(cap: &impl Has<FsRead>) -> Vec<u8> {
248///     capsec::fs::read("/tmp/data", cap).unwrap()
249/// }
250/// ```
251///
252/// # Supported categories
253///
254/// `all`, `fs`, `net`, `env`, `process`
255#[proc_macro_attribute]
256pub fn deny(attr: TokenStream, item: TokenStream) -> TokenStream {
257    let denied = parse_macro_input!(attr with Punctuated::<Meta, Token![,]>::parse_terminated);
258
259    let item_clone: proc_macro2::TokenStream = item.clone().into();
260    let func = match syn::parse::<ItemFn>(item) {
261        Ok(f) => f,
262        Err(e) => {
263            let err = e.into_compile_error();
264            return quote! { #err #item_clone }.into();
265        }
266    };
267
268    let deny_names: Vec<String> = denied
269        .iter()
270        .map(|meta| {
271            meta.path()
272                .get_ident()
273                .map(|i| i.to_string())
274                .unwrap_or_default()
275        })
276        .collect();
277
278    let doc_string = format!("capsec::deny({})", deny_names.join(", "));
279
280    let func_vis = &func.vis;
281    let func_sig = &func.sig;
282    let func_block = &func.block;
283    let func_attrs = &func.attrs;
284
285    let expanded = quote! {
286        #(#func_attrs)*
287        #[doc = #doc_string]
288        #func_vis #func_sig
289            #func_block
290    };
291
292    expanded.into()
293}
294
295/// Injects `CapRoot` creation into a function entry point.
296///
297/// Removes the first parameter (which must be typed as `CapRoot`) and prepends
298/// `let {param_name} = capsec::root();` to the function body.
299///
300/// # Usage
301///
302/// ```rust,ignore
303/// #[capsec::main]
304/// fn main(root: CapRoot) {
305///     let fs = root.fs_read();
306///     // ...
307/// }
308/// ```
309///
310/// # With `#[tokio::main]`
311///
312/// Place `#[capsec::main]` above `#[tokio::main]`:
313///
314/// ```rust,ignore
315/// #[capsec::main]
316/// #[tokio::main]
317/// async fn main(root: CapRoot) { ... }
318/// ```
319#[proc_macro_attribute]
320pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
321    let func = parse_macro_input!(item as ItemFn);
322
323    match main_inner(&func) {
324        Ok(tokens) => tokens.into(),
325        Err(e) => e.into_compile_error().into(),
326    }
327}
328
329fn main_inner(func: &ItemFn) -> syn::Result<proc_macro2::TokenStream> {
330    if func.sig.inputs.is_empty() {
331        if func.sig.asyncness.is_some() {
332            return Err(syn::Error::new_spanned(
333                &func.sig,
334                "#[capsec::main] found no CapRoot parameter. If combining with #[tokio::main], \
335                 place #[capsec::main] above #[tokio::main]:\n\n  \
336                 #[capsec::main]\n  \
337                 #[tokio::main]\n  \
338                 async fn main(root: CapRoot) { ... }",
339            ));
340        }
341        return Err(syn::Error::new_spanned(
342            &func.sig,
343            "#[capsec::main] expected first parameter of type CapRoot",
344        ));
345    }
346
347    // Extract first parameter
348    let first_arg = &func.sig.inputs[0];
349    let (param_name, param_type) = match first_arg {
350        FnArg::Typed(pat_type) => {
351            let name = if let Pat::Ident(pi) = &*pat_type.pat {
352                pi.ident.clone()
353            } else {
354                return Err(syn::Error::new_spanned(
355                    &pat_type.pat,
356                    "#[capsec::main] expected a simple identifier for the CapRoot parameter",
357                ));
358            };
359            (name, &*pat_type.ty)
360        }
361        FnArg::Receiver(r) => {
362            return Err(syn::Error::new_spanned(
363                r,
364                "#[capsec::main] cannot be used on methods with self",
365            ));
366        }
367    };
368
369    // Validate type is CapRoot
370    let type_str = quote!(#param_type).to_string().replace(' ', "");
371    if type_str != "CapRoot" && type_str != "capsec::CapRoot" {
372        return Err(syn::Error::new_spanned(
373            param_type,
374            "first parameter must be CapRoot",
375        ));
376    }
377
378    // Build new signature without the first parameter
379    let remaining_params: Vec<_> = func.sig.inputs.iter().skip(1).collect();
380    let func_attrs = &func.attrs;
381    let func_vis = &func.vis;
382    let func_name = &func.sig.ident;
383    let func_generics = &func.sig.generics;
384    let func_output = &func.sig.output;
385    let func_asyncness = &func.sig.asyncness;
386    let func_block = &func.block;
387
388    Ok(quote! {
389        #(#func_attrs)*
390        #func_vis #func_asyncness fn #func_name #func_generics(#(#remaining_params),*) #func_output {
391            let #param_name = capsec::root();
392            #func_block
393        }
394    })
395}
396
397/// Transforms a struct with permission-type fields into a capability context.
398///
399/// Generates:
400/// - Field types rewritten from `PermType` to `Cap<PermType>` (or `SendCap<PermType>`)
401/// - A `new(root: &CapRoot) -> Self` constructor
402/// - `impl Has<P>` for each field's permission type
403///
404/// # Usage
405///
406/// ```rust,ignore
407/// #[capsec::context]
408/// struct AppCtx {
409///     fs: FsRead,
410///     net: NetConnect,
411/// }
412///
413/// // Send variant for async/threaded code:
414/// #[capsec::context(send)]
415/// struct AsyncCtx {
416///     fs: FsRead,
417///     net: NetConnect,
418/// }
419/// ```
420#[proc_macro_attribute]
421pub fn context(attr: TokenStream, item: TokenStream) -> TokenStream {
422    let attr2: proc_macro2::TokenStream = attr.into();
423    let input = parse_macro_input!(item as ItemStruct);
424
425    match context_inner(attr2, &input) {
426        Ok(tokens) => tokens.into(),
427        Err(e) => e.into_compile_error().into(),
428    }
429}
430
431fn context_inner(
432    attr: proc_macro2::TokenStream,
433    input: &ItemStruct,
434) -> syn::Result<proc_macro2::TokenStream> {
435    // Parse `send` flag
436    let attr_str = attr.to_string();
437    let is_send = match attr_str.trim() {
438        "" => false,
439        "send" => true,
440        other => {
441            return Err(syn::Error::new_spanned(
442                &attr,
443                format!("unexpected attribute '{}', expected empty or 'send'", other),
444            ));
445        }
446    };
447
448    // Reject generics
449    if !input.generics.params.is_empty() {
450        return Err(syn::Error::new_spanned(
451            &input.generics,
452            "#[capsec::context] does not support generic structs",
453        ));
454    }
455
456    // Get named fields
457    let fields = match &input.fields {
458        syn::Fields::Named(f) => f,
459        _ => {
460            return Err(syn::Error::new_spanned(
461                input,
462                "#[capsec::context] requires a struct with named fields",
463            ));
464        }
465    };
466
467    // Validate fields and collect permission info
468    let mut field_infos: Vec<(syn::Ident, syn::Ident)> = Vec::new(); // (field_name, perm_ident)
469    let mut seen_perms: std::collections::HashSet<String> = std::collections::HashSet::new();
470
471    for field in &fields.named {
472        let field_name = field.ident.as_ref().unwrap().clone();
473        let ty = &field.ty;
474
475        // Check for tuple types
476        if let Type::Tuple(_) = ty {
477            return Err(syn::Error::new_spanned(
478                ty,
479                "tuple permission types are not supported in context structs — use separate fields instead",
480            ));
481        }
482
483        // Extract type ident (last segment of path)
484        let perm_ident = match ty {
485            Type::Path(tp) => {
486                if let Some(seg) = tp.path.segments.last() {
487                    seg.ident.clone()
488                } else {
489                    return Err(syn::Error::new_spanned(
490                        ty,
491                        format!(
492                            "field '{}' has type '{}', which is not a capsec permission type. \
493                             Expected one of: {}",
494                            field_name,
495                            quote!(#ty),
496                            KNOWN_PERMISSIONS.join(", ")
497                        ),
498                    ));
499                }
500            }
501            _ => {
502                return Err(syn::Error::new_spanned(
503                    ty,
504                    format!(
505                        "field '{}' has type '{}', which is not a capsec permission type. \
506                         Expected one of: {}",
507                        field_name,
508                        quote!(#ty),
509                        KNOWN_PERMISSIONS.join(", ")
510                    ),
511                ));
512            }
513        };
514
515        let perm_str = perm_ident.to_string();
516
517        // Validate against known permissions
518        if !KNOWN_PERMISSIONS.contains(&perm_str.as_str()) {
519            return Err(syn::Error::new_spanned(
520                ty,
521                format!(
522                    "field '{}' has type '{}', which is not a capsec permission type. \
523                     Expected one of: {}",
524                    field_name,
525                    perm_str,
526                    KNOWN_PERMISSIONS.join(", ")
527                ),
528            ));
529        }
530
531        // Check for duplicates
532        if !seen_perms.insert(perm_str.clone()) {
533            return Err(syn::Error::new_spanned(
534                ty,
535                format!(
536                    "duplicate permission type '{}' — each permission can only appear once in a context struct",
537                    perm_str
538                ),
539            ));
540        }
541
542        field_infos.push((field_name, perm_ident));
543    }
544
545    let struct_name = &input.ident;
546    let struct_vis = &input.vis;
547    let struct_attrs = &input.attrs;
548
549    // Generate struct fields with rewritten types
550    let struct_fields: Vec<_> = field_infos
551        .iter()
552        .map(|(name, perm)| {
553            if is_send {
554                quote! { #name: capsec_core::cap::SendCap<capsec_core::permission::#perm> }
555            } else {
556                quote! { #name: capsec_core::cap::Cap<capsec_core::permission::#perm> }
557            }
558        })
559        .collect();
560
561    // Generate constructor fields
562    let constructor_fields: Vec<_> = field_infos
563        .iter()
564        .map(|(name, perm)| {
565            if is_send {
566                quote! { #name: root.grant::<capsec_core::permission::#perm>().make_send() }
567            } else {
568                quote! { #name: root.grant::<capsec_core::permission::#perm>() }
569            }
570        })
571        .collect();
572
573    // Generate Has<P> impls
574    let has_impls: Vec<_> = field_infos
575        .iter()
576        .map(|(name, perm)| {
577            quote! {
578                impl capsec_core::has::Has<capsec_core::permission::#perm> for #struct_name {
579                    fn cap_ref(&self) -> capsec_core::cap::Cap<capsec_core::permission::#perm> {
580                        self.#name.cap_ref()
581                    }
582                }
583            }
584        })
585        .collect();
586
587    Ok(quote! {
588        #(#struct_attrs)*
589        #struct_vis struct #struct_name {
590            #(#struct_fields,)*
591        }
592
593        impl #struct_name {
594            /// Creates a new context by granting all capabilities from the root.
595            pub fn new(root: &capsec_core::root::CapRoot) -> Self {
596                Self {
597                    #(#constructor_fields,)*
598                }
599            }
600        }
601
602        #(#has_impls)*
603    })
604}