1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
extern crate proc_macro;
extern crate proc_macro2;

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::parse_macro_input;
use syn::punctuated::Punctuated;
use syn::{Expr, ExprLit, ExprPath, Lit, LitStr, Token};

mod parsers;

/// Prepare a format string and associated arguments for use with an EVE
/// coprocessor widget which supports the `OPT_FORMAT` option.
#[proc_macro]
pub fn eve_format(input: TokenStream) -> TokenStream {
    let call = parse_macro_input!(input as EVEFormat);
    let fmt = call.fmt;
    let args = call.args;

    let fmt_src = &fmt.value().into_bytes()[..];
    let mut format_chars: Vec<u8> = Vec::with_capacity(fmt_src.len());
    let mut arg_elems: Punctuated<Expr, syn::Token![,]> = Punctuated::new();

    let int_variant_path: ExprPath =
        syn::parse_str("::evegfx::commands::strfmt::Argument::Int").unwrap();
    let uint_variant_path: ExprPath =
        syn::parse_str("::evegfx::commands::strfmt::Argument::UInt").unwrap();
    let char_variant_path: ExprPath =
        syn::parse_str("::evegfx::commands::strfmt::Argument::Char").unwrap();
    let string_variant_path: ExprPath =
        syn::parse_str("::evegfx::commands::strfmt::Argument::String").unwrap();

    let mut remain = fmt_src.clone();
    let mut next_arg = 0;
    let mut needs_fmt = false;
    while remain.len() > 0 {
        use parsers::Token::*;
        let (token, next) = parsers::next_token(remain);
        remain = next;
        match token {
            Literal(bytes) => {
                format_chars.extend(bytes);
            }
            Verb(bytes) => {
                needs_fmt = true;
                if next_arg >= args.len() {
                    let err = syn::Error::new(
                        fmt.span(),
                        format!("not enough arguments to populate {} verbs", next_arg + 1),
                    );
                    return err.into_compile_error().into();
                }
                let given_expr = args[next_arg].clone();
                next_arg += 1;

                format_chars.extend(bytes);
                // Our parser ensures that a format verb always includes at
                // least two bytes: the % and the verb letter. There might
                // be other stuff in between but we don't need to worry
                // about those because they'll be interpreted by EVE's
                // coprocessor, not by us. Our only goal here is to figure
                // out which enum variant to select for the argument.
                let mode = *bytes.last().unwrap();
                match mode {
                    b'd' | b'i' => {
                        let arg_expr = enum_variant_expr(int_variant_path.clone(), given_expr);
                        arg_elems.push(arg_expr);
                    }
                    b'u' | b'o' | b'x' | b'X' => {
                        let arg_expr = enum_variant_expr(uint_variant_path.clone(), given_expr);
                        arg_elems.push(arg_expr);
                    }
                    b'c' => {
                        let arg_expr = enum_variant_expr(char_variant_path.clone(), given_expr);
                        arg_elems.push(arg_expr);
                    }
                    b's' => {
                        let arg_expr = enum_variant_expr(string_variant_path.clone(), given_expr);
                        arg_elems.push(arg_expr);
                    }
                    // TODO: string pointers (%s) too
                    _ => {
                        // This is safe because our parser only allows ASCII
                        // letters as format strings.
                        use std::convert::TryInto;
                        let letter: char = mode.try_into().unwrap();

                        let err = syn::Error::new(
                            fmt.span(),
                            format!("unsupported format verb '%{}'", letter),
                        );
                        return err.into_compile_error().into();
                    }
                }
            }
            Percent(bytes) => {
                needs_fmt = true;
                format_chars.extend(bytes);
            }
            Null(_) => {
                // EVE's coprocessor considers a literal null to be a string
                // terminator, so we'll encode it instead as a format sequence
                // that inserts the null character in order to avoid producing
                // an invalid message.
                needs_fmt = true;
                format_chars.extend(b"%c");
                let arg_expr = enum_variant_expr(
                    uint_variant_path.clone(),
                    Expr::Lit(ExprLit {
                        attrs: Vec::new(),
                        lit: Lit::Int(syn::LitInt::new("0", fmt.span())),
                    }),
                );
                arg_elems.push(arg_expr);
            }
            Unterminated(_) => {
                let err = syn::Error::new(fmt.span(), "unterminated format sequence");
                return err.into_compile_error().into();
            }
            Invalid(_) => {
                let err = syn::Error::new(fmt.span(), "invalid format sequence");
                return err.into_compile_error().into();
            }
        };
    }

    if next_arg < args.len() {
        use syn::spanned::Spanned;
        let error_span = args[next_arg].span();
        let err = syn::Error::new(error_span, "too many arguments for format string");
        return err.into_compile_error().into();
    }

    // EVE expects format strings to be null-terminated.
    format_chars.push(0);

    let mut args: Punctuated<Expr, syn::Token![,]> = Punctuated::new();
    args.push(byte_string_expr(&format_chars, fmt.span()));
    if needs_fmt {
        args.push(array_slice_expr(arg_elems));
    }

    if needs_fmt {
        quote!(
            ::evegfx::commands::strfmt::Message::new(#args)
        )
    } else {
        quote!(
            ::evegfx::commands::strfmt::Message::new_literal(#args)
        )
    }
    .into()
}

fn enum_variant_expr(path: ExprPath, val: Expr) -> syn::Expr {
    let mut args: Punctuated<Expr, syn::Token![,]> = Punctuated::new();
    args.push(val);
    syn::Expr::Call(syn::ExprCall {
        attrs: Vec::new(),
        func: Box::new(Expr::Path(path)),
        args: args,
        paren_token: syn::token::Paren {
            span: Span::call_site(),
        },
    })
}

fn byte_string_expr(bytes: &[u8], span: proc_macro2::Span) -> syn::Expr {
    syn::Expr::Lit(syn::ExprLit {
        attrs: Vec::new(),
        lit: syn::LitByteStr::new(bytes, span).into(),
    })
}

fn array_slice_expr(elems: Punctuated<syn::Expr, syn::Token![,]>) -> syn::Expr {
    syn::Expr::Reference(syn::ExprReference {
        attrs: Vec::new(),
        and_token: syn::token::And {
            spans: [Span::call_site()],
        },
        mutability: None,
        expr: Box::new(syn::Expr::Array(syn::ExprArray {
            attrs: Vec::new(),
            bracket_token: syn::token::Bracket {
                span: Span::call_site(),
            },
            elems: elems,
        })),
        raw: std::default::Default::default(),
    })
}

struct EVEFormat {
    fmt: LitStr,
    args: Punctuated<Expr, syn::Token![,]>,
}

impl Parse for EVEFormat {
    fn parse(input: ParseStream) -> Result<Self> {
        let fmt: LitStr = input.parse()?;
        let args: Punctuated<Expr, syn::Token![,]> = if let Ok(_) = input.parse::<Token![,]>() {
            Punctuated::parse_terminated(input)?
        } else {
            Punctuated::new()
        };

        Ok(EVEFormat {
            fmt: fmt,
            args: args,
        })
    }
}