use syn::{
punctuated::Punctuated, Attribute, Error, Expr, ExprLit, Lit, LitInt, LitStr, Meta,
MetaNameValue, Path, Token,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Tag {
pub(crate) value: u64,
pub(crate) bits: usize,
}
impl Tag {
pub(crate) const EMPTY: Self = Self { value: 0, bits: 0 };
pub(crate) fn is_empty(self) -> bool {
self.bits == 0
}
pub(crate) fn overlaps(self, other: Self) -> bool {
if self.is_empty() || other.is_empty() {
return false;
}
let (shorter, longer) = if self.bits <= other.bits {
(self, other)
} else {
(other, self)
};
let shift = longer.bits - shorter.bits;
(longer.value >> shift) == shorter.value
}
}
#[derive(Default)]
pub(crate) struct StructAttrs {
pub(crate) tag: Option<Tag>,
}
#[derive(Default)]
pub(crate) struct VariantAttrs {
pub(crate) tag: Option<Tag>,
}
#[derive(Default)]
pub(crate) struct FieldAttrs {
pub(crate) bit_len: Option<usize>,
pub(crate) read_with: Option<Path>,
pub(crate) write_with: Option<Path>,
pub(crate) default: Option<FieldDefault>,
pub(crate) skip_read: bool,
pub(crate) skip_write: bool,
}
#[derive(Clone)]
pub(crate) enum FieldDefault {
Trait,
With(Path),
}
impl FieldAttrs {
pub(crate) fn needs_default_trait(&self) -> bool {
self.skip_read && !matches!(self.default, Some(FieldDefault::With(_)))
}
}
pub(crate) fn parse_struct_attrs(attrs: &[Attribute]) -> syn::Result<StructAttrs> {
let mut parsed = StructAttrs::default();
let mut errors = None;
for attr in tlb_attrs(attrs) {
match parse_meta_list(attr) {
Ok(items) => {
for item in items {
match item {
Meta::NameValue(value) if value.path.is_ident("tag") => {
set_once(
&mut parsed.tag,
parse_tag_value(&value)?,
value.path.clone(),
&mut errors,
);
}
other => push_error(
&mut errors,
Error::new_spanned(other, "only `tag` is supported on structs"),
),
}
}
}
Err(error) => push_error(&mut errors, error),
}
}
finish(parsed, errors)
}
pub(crate) fn parse_enum_attrs(attrs: &[Attribute]) -> syn::Result<()> {
let mut errors = None;
for attr in tlb_attrs(attrs) {
match parse_meta_list(attr) {
Ok(items) => {
for item in items {
push_error(
&mut errors,
Error::new_spanned(item, "`#[tlb(...)]` is not supported on enums"),
);
}
}
Err(error) => push_error(&mut errors, error),
}
}
match errors {
Some(error) => Err(error),
None => Ok(()),
}
}
pub(crate) fn parse_variant_attrs(attrs: &[Attribute]) -> syn::Result<VariantAttrs> {
let mut parsed = VariantAttrs::default();
let mut errors = None;
for attr in tlb_attrs(attrs) {
match parse_meta_list(attr) {
Ok(items) => {
for item in items {
match item {
Meta::NameValue(value) if value.path.is_ident("tag") => {
set_once(
&mut parsed.tag,
parse_tag_value(&value)?,
value.path.clone(),
&mut errors,
);
}
other => push_error(
&mut errors,
Error::new_spanned(other, "only `tag` is supported on enum variants"),
),
}
}
}
Err(error) => push_error(&mut errors, error),
}
}
finish(parsed, errors)
}
pub(crate) fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result<FieldAttrs> {
let mut parsed = FieldAttrs::default();
let mut errors = None;
for attr in tlb_attrs(attrs) {
match parse_meta_list(attr) {
Ok(items) => {
for item in items {
match item {
Meta::NameValue(value) if value.path.is_ident("bit_len") => {
set_once(
&mut parsed.bit_len,
parse_bit_len_value(&value)?,
value.path.clone(),
&mut errors,
);
}
Meta::NameValue(value) if value.path.is_ident("read_with") => {
set_once(
&mut parsed.read_with,
parse_path_value(&value, "read_with")?,
value.path.clone(),
&mut errors,
);
}
Meta::NameValue(value) if value.path.is_ident("write_with") => {
set_once(
&mut parsed.write_with,
parse_path_value(&value, "write_with")?,
value.path.clone(),
&mut errors,
);
}
Meta::NameValue(value) if value.path.is_ident("default_with") => {
set_once(
&mut parsed.default,
FieldDefault::With(parse_path_value(&value, "default_with")?),
value.path.clone(),
&mut errors,
);
}
Meta::Path(path) if path.is_ident("default") => {
set_once(
&mut parsed.default,
FieldDefault::Trait,
path.clone(),
&mut errors,
);
}
Meta::Path(path) if path.is_ident("skip") => {
set_flag(
&mut parsed.skip_read,
path.clone(),
"duplicate `skip` / `skip_read` attribute",
&mut errors,
);
set_flag(
&mut parsed.skip_write,
path.clone(),
"duplicate `skip` / `skip_write` attribute",
&mut errors,
);
}
Meta::Path(path) if path.is_ident("skip_read") => {
set_flag(
&mut parsed.skip_read,
path,
"duplicate `skip` / `skip_read` attribute",
&mut errors,
);
}
Meta::Path(path) if path.is_ident("skip_write") => {
set_flag(
&mut parsed.skip_write,
path,
"duplicate `skip` / `skip_write` attribute",
&mut errors,
);
}
other => push_error(
&mut errors,
Error::new_spanned(other, "unsupported field attribute"),
),
}
}
}
Err(error) => push_error(&mut errors, error),
}
}
if parsed.skip_read && parsed.read_with.is_some() {
push_error(
&mut errors,
Error::new(
proc_macro2::Span::call_site(),
"`skip_read` cannot be combined with `read_with`",
),
);
}
if parsed.skip_write && parsed.write_with.is_some() {
push_error(
&mut errors,
Error::new(
proc_macro2::Span::call_site(),
"`skip_write` cannot be combined with `write_with`",
),
);
}
if parsed.default.is_some() && !parsed.skip_read {
push_error(
&mut errors,
Error::new(
proc_macro2::Span::call_site(),
"`default` and `default_with` require `skip` or `skip_read`",
),
);
}
if parsed.skip_read && parsed.skip_write && parsed.bit_len.is_some() {
push_error(
&mut errors,
Error::new(
proc_macro2::Span::call_site(),
"`bit_len` has no effect when both read and write are skipped",
),
);
}
finish(parsed, errors)
}
fn set_flag(flag: &mut bool, path: Path, message: &str, errors: &mut Option<Error>) {
if *flag {
push_error(errors, Error::new_spanned(path, message));
} else {
*flag = true;
}
}
fn tlb_attrs(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
attrs.iter().filter(|attr| attr.path().is_ident("tlb"))
}
fn parse_meta_list(attr: &Attribute) -> syn::Result<Punctuated<Meta, Token![,]>> {
attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
}
fn parse_tag_value(value: &MetaNameValue) -> syn::Result<Tag> {
let literal = parse_string_literal(&value.value, "tag")?;
parse_tag_literal(&literal)
}
fn parse_bit_len_value(value: &MetaNameValue) -> syn::Result<usize> {
let literal = parse_integer_literal(&value.value, "bit_len")?;
literal.base10_parse()
}
fn parse_path_value(value: &MetaNameValue, key: &str) -> syn::Result<Path> {
let literal = parse_string_literal(&value.value, key)?;
literal.parse().map_err(|error| {
Error::new(
literal.span(),
format!("failed to parse `{key}` path: {error}"),
)
})
}
fn parse_string_literal(expr: &Expr, key: &str) -> syn::Result<LitStr> {
match expr {
Expr::Lit(ExprLit {
lit: Lit::Str(value),
..
}) => Ok(value.clone()),
_ => Err(Error::new_spanned(
expr,
format!("`{key}` must be a string literal"),
)),
}
}
fn parse_integer_literal(expr: &Expr, key: &str) -> syn::Result<LitInt> {
match expr {
Expr::Lit(ExprLit {
lit: Lit::Int(value),
..
}) => Ok(value.clone()),
_ => Err(Error::new_spanned(
expr,
format!("`{key}` must be an integer literal"),
)),
}
}
fn parse_tag_literal(literal: &LitStr) -> syn::Result<Tag> {
let value = literal.value();
if value == "$_" || value == "#_" {
return Ok(Tag::EMPTY);
}
if let Some(bits) = value.strip_prefix('$') {
if bits.is_empty() {
return Err(Error::new(literal.span(), "binary tags must not be empty"));
}
if bits.len() > 64 {
return Err(Error::new(
literal.span(),
"binary tags longer than 64 bits are not supported",
));
}
if !bits.chars().all(|ch| matches!(ch, '0' | '1')) {
return Err(Error::new(
literal.span(),
"binary tags may only contain `0` and `1`",
));
}
return u64::from_str_radix(bits, 2)
.map(|value| Tag {
value,
bits: bits.len(),
})
.map_err(|error| Error::new(literal.span(), error));
}
if let Some(hex) = value.strip_prefix('#') {
if hex.is_empty() {
return Err(Error::new(literal.span(), "hex tags must not be empty"));
}
if hex.len() > 16 {
return Err(Error::new(
literal.span(),
"hex tags longer than 64 bits are not supported",
));
}
if !hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
return Err(Error::new(
literal.span(),
"hex tags may only contain hexadecimal digits",
));
}
return u64::from_str_radix(hex, 16)
.map(|value| Tag {
value,
bits: hex.len() * 4,
})
.map_err(|error| Error::new(literal.span(), error));
}
Err(Error::new(
literal.span(),
"tags must start with `$` for binary or `#` for hexadecimal",
))
}
fn set_once<T>(slot: &mut Option<T>, value: T, path: Path, errors: &mut Option<Error>) {
if slot.replace(value).is_some() {
push_error(
errors,
Error::new_spanned(path, "duplicate attribute is not allowed"),
);
}
}
fn finish<T>(value: T, errors: Option<Error>) -> syn::Result<T> {
match errors {
Some(error) => Err(error),
None => Ok(value),
}
}
fn push_error(slot: &mut Option<Error>, error: Error) {
if let Some(existing) = slot {
existing.combine(error);
} else {
*slot = Some(error);
}
}
#[cfg(test)]
mod tests {
use super::{parse_field_attrs, parse_tag_literal, FieldDefault, Tag};
use syn::{parse_quote, Attribute, LitStr};
#[test]
fn parse_tag_literal_should_support_binary_and_hex_forms() {
let binary =
parse_tag_literal(&LitStr::new("$001", proc_macro2::Span::call_site())).unwrap();
let hex = parse_tag_literal(&LitStr::new("#1f2", proc_macro2::Span::call_site())).unwrap();
assert_eq!(binary, Tag { value: 1, bits: 3 });
assert_eq!(
hex,
Tag {
value: 0x1f2,
bits: 12
}
);
}
#[test]
fn parse_tag_literal_should_support_empty_tags() {
let empty = parse_tag_literal(&LitStr::new("$_", proc_macro2::Span::call_site())).unwrap();
assert_eq!(empty, Tag::EMPTY);
}
#[test]
fn overlaps_should_detect_prefix_conflicts() {
let short = Tag { value: 0, bits: 1 };
let long = Tag { value: 1, bits: 2 };
assert!(short.overlaps(long));
}
#[test]
fn parse_field_attrs_should_support_skip_and_default_with() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[tlb(skip, default_with = "make_value")])];
let parsed = parse_field_attrs(&attrs).unwrap();
assert!(parsed.skip_read);
assert!(parsed.skip_write);
match parsed.default {
Some(FieldDefault::With(path)) => {
assert!(path.is_ident("make_value"));
}
_ => panic!("expected `default_with` to be parsed"),
}
}
#[test]
fn parse_field_attrs_should_require_skip_for_default() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[tlb(default)])];
let error = parse_field_attrs(&attrs).err().unwrap();
assert!(error
.to_string()
.contains("`default` and `default_with` require `skip` or `skip_read`"));
}
#[test]
fn parse_field_attrs_should_reject_skip_read_with_read_hook() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[tlb(skip_read, read_with = "custom")])];
let error = parse_field_attrs(&attrs).err().unwrap();
assert!(error
.to_string()
.contains("`skip_read` cannot be combined with `read_with`"));
}
}