cbor-macro 0.1.0

Macros for entering CBOR in diagnostic notation (EDN) or pretty printed format
Documentation
//! Macros for entering CBOR in diagnostic notation (EDN) or pretty printed format in Rust source
//! as binary constants
//!
//! This is useful for test vectors, or when data structures that are handed out do not not depend
//! on runtime data.

/// Create a CBOR slice out of diagnostic notation (EDN)
///
/// It requires that the EDN also complies with Rust's tokenization rules. This is the case for
/// many simple expressions:
///
/// ```
/// use cbor_macro::cbor;
/// let _ = cbor!([1, 2, "three"]);
/// let _ = cbor!({-1: 4, "x" "y": 1(1234567890), []: 3.14, simple(0): 1});
/// ```
///
/// Not all EDN is valid by Rust's tokenizer rules, including "complex" comments and some forms of
/// string escaping:
///
/// ```compile_fail
/// # use cbor_macro::cbor;
/// let _ = cbor!([1 / Funny comment :-) /, 2]);
/// ```
/// ```compile_fail
/// # use cbor_macro::cbor;
/// let _ = cbor!("zero \u0000");
/// ```
///
/// Also, there may exist EDN values that, while being both valid EDN and tokenizable Rust, lose
/// information in the course of tokenization, and are reconstructed wrong. No working examples
/// were found so far; the most "promising" candidates are line breaks within a string, exotic
/// escapes, and application oriented literals.
///
/// For such cases, the [`cbo!`] macro provides a solution.
///
/// The output of the macro is a [cboritem::CborItem], which is a newtype around a slice. Thus,
/// when storing the output of `cbor!` in static memory (as it is often useful), define a const to
/// avoid the added indirection and static memory usage of the extra pointer:
///
/// ```
/// # use cbor_macro::cbor;
/// fn aead_feed_ad(data: &[u8]) { /* ... */ }
/// use cboritem::CborItem;
/// const ALGS: CborItem<'static> = cbor!([10, null, -8, -27]);
/// aead_feed_ad(&ALGS);
/// ```
///
/// Note that as always with macros, any style of parentheses around the expression is discarded,
/// and not reported. Thus, while `cbor![1]` is legal, it is still just the number `1`, not an
/// array containing 1.
#[proc_macro]
pub fn cbor(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // FIXME: If any char is in input, raise a warning -- those just don't work that way.

    let reconstructed = format!("{}", &input);
    let parsed = cbor_diag::parse_diag(reconstructed).expect("Error parsing CBOR literal");
    let binary = parsed.to_bytes();

    // Once span join is stable, we could switch over to that. It'd have upsides and downsides --
    // join might fail (but probably only when macros are involved), and concatenating the source
    // tokens relies on no whitespace being significant in EDN (which AIU is OK).

    let input_spans: Option<Vec<_>> = input
        .into_iter()
        .map(|tree| tree.span().source_text())
        .collect();

    if let Some(parts) = input_spans {
        let joint = parts.join("");

        let source_parsed =
            cbor_diag::parse_diag(joint).expect("CBOR from source is invalid, but stringified token tree was valid. This is possibly a bug in cbor-macro. As a workaround, you can use the cbo!(R#\"...\"#) macro.");
        if source_parsed.to_bytes() != binary {
            panic!("CBOR got misrepresented by Rust tokenization and re-stringification. Use cbo!(r#\"...\"#) instead.");
        }
    } else {
        // Not sure what level of error is appropriate here
        panic!("Some parts of the CBOR expression were not found in source files, CBOR could not be verified. Use cbo!(r#\"...\") instead.")
    }

    proc_macro::TokenStream::from(quote::quote! {
        // UNSAFE: We just constructed this as a CBOR item
        unsafe { cboritem::CborItem::new(&[ #(#binary),* ])
    } })
}

/// Create a CBOR slice out of the diagnostic notation (EDN) inside the given string.
///
/// This macro is useful for any EDN that does not happen to be compatible with Rust's
/// tokenization.
///
/// This is called `cbo` because it is most useful with r-strings:
///
/// ```
/// use cbor_macro::cbo;
/// let parsed = cbo!(r#" ['CD', / Sad comment :-( / 123] "#);
/// ```
#[proc_macro]
pub fn cbo(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = syn::parse_macro_input!(input as syn::LitStr);

    let parsed = cbor_diag::parse_diag(input.value()).expect("Error parsing CBOR literal");
    let binary = parsed.to_bytes();
    proc_macro::TokenStream::from(quote::quote! {
        // UNSAFE: We just constructed this as a CBOR item
        unsafe { cboritem::CborItem::new(&[ #(#binary),* ])
    } })
}

/// Create a CBOR slice out of the pretty-printed hexadecimal CBOR
///
/// This macro is useful for when precise encoding is required exceeding what EDN can provide.
/// (Note that this is mainly a tools issue, with this crate not understanding all of EDN that
/// would allow great control: EDN itself can express well-formed CBOR.)
///
/// ```
/// use cbor_macro::pretty;
/// let parsed = pretty!("
///     83    # array(3)
///        01 # unsigned(1)
///        02 # unsigned(2)
///        03 # unsigned(3)
///     ");
/// ```
///
/// This macro validates that the hex decoded data is really well-formed, and transfers that
/// information to the program by marking it as a [cboritem::CborItem].
///
/// ```compile_fail
/// # use cbor_macro::pretty;
/// let _ = pretty!("01 01 01");
/// ```
#[proc_macro]
pub fn pretty(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let hex = syn::parse_macro_input!(input as syn::LitStr);

    let parsed = cbor_diag::parse_hex(hex.value()).expect("CBOR is not well-formed");
    let binary = parsed.to_bytes();
    proc_macro::TokenStream::from(quote::quote! {
        // UNSAFE: We just constructed this as a CBOR item
        unsafe { cboritem::CborItem::new(&[ #(#binary),* ])
    } })
}

/// Create a slice out of a hex string that uses the convention of pretty CBOR
///
/// Unlike [`pretty!()`], this does not check whether the data is valid CBOR, and returns a slice
/// instead.
///
/// The function is essentially equivalent to the [hex-literal crate's `hex!` macro], but allows
/// comments in the style of CBOR's pretty printing.
///
/// ```
/// # use cbor_macro::pretty_bytes;
/// const PACKET: &'static [u8] = pretty_bytes!(r#"
///     ff 63 48 8a 60     # application dependent header
///     65                 # text(5)
///        68656C6C6F      # "hello"
/// "#);
/// ```
///
/// [hex-literal crate's `hex!` macro]: https://docs.rs/hex-literal
#[proc_macro]
pub fn pretty_bytes(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let hex = syn::parse_macro_input!(input as syn::LitStr);

    let comment = regex::RegexBuilder::new("#.*$")
        .multi_line(true)
        .build()
        .unwrap();
    let space = regex::Regex::new("[[:space:]]").unwrap();

    let hex = hex.value();
    let hex = comment.replace_all(&hex, "");
    let hex = space.replace_all(&hex, "");
    let hex: &str = hex.as_ref();

    let binary = hex::decode(hex).expect("Invalid hex");
    proc_macro::TokenStream::from(quote::quote! {
        {
            let result = &[ #(#binary),* ];
            result
        }
    })
}