tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

import_attribute.rs (5984B)


      1 // Copyright 2023 The Chromium Authors
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 use proc_macro2::Span;
      6 use quote::quote;
      7 use syn::parse::{Parse, ParseStream};
      8 use syn::{parse_macro_input, Error, Ident, Lit, Token};
      9 
     10 #[proc_macro]
     11 pub fn import(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     12    let imports = parse_macro_input!(input as ImportList).imports;
     13 
     14    let mut stream = proc_macro2::TokenStream::new();
     15    for i in imports {
     16        let public = &i.reexport;
     17        let mangled_crate_name = &i.target.mangled_crate_name;
     18        let name = i.alias.as_ref().unwrap_or(&i.target.gn_name);
     19        stream.extend(quote! {
     20          #public extern crate #mangled_crate_name as #name;
     21        });
     22    }
     23    stream.into()
     24 }
     25 
     26 struct ImportList {
     27    imports: Vec<Import>,
     28 }
     29 
     30 struct Import {
     31    target: GnTarget,
     32    alias: Option<Ident>,
     33    reexport: Option<Token![pub]>,
     34 }
     35 
     36 struct GnTarget {
     37    mangled_crate_name: Ident,
     38    gn_name: Ident,
     39 }
     40 
     41 impl GnTarget {
     42    fn parse(s: &str, span: Span) -> Result<GnTarget, String> {
     43        if !s.starts_with("//") {
     44            return Err(String::from("expected absolute GN path (should start with //)"));
     45        }
     46 
     47        let mut path: Vec<&str> = s[2..].split('/').collect();
     48 
     49        let gn_name = {
     50            if path.starts_with(&["third_party", "rust"]) {
     51                return Err(String::from(
     52                    "import! macro should not be used for third_party crates",
     53                ));
     54            }
     55 
     56            let last = path.pop().unwrap();
     57            let (split_last, gn_name) = match last.split_once(':') {
     58                Some((last, name)) => (last, name),
     59                None => (last, last),
     60            };
     61            path.push(split_last);
     62 
     63            gn_name
     64        };
     65 
     66        for p in &path {
     67            if p.contains(':') {
     68                return Err(String::from("unexpected ':' in GN path component"));
     69            }
     70            if p.is_empty() {
     71                return Err(String::from("unexpected empty GN path component"));
     72            }
     73        }
     74 
     75        let mangled_crate_name =
     76            escape_non_identifier_chars(&format!("{}:{gn_name}", path.join("/")))?;
     77 
     78        Ok(GnTarget {
     79            mangled_crate_name: Ident::new(&mangled_crate_name, span),
     80            gn_name: syn::parse_str::<Ident>(gn_name).map_err(|e| format!("{e}"))?,
     81        })
     82    }
     83 }
     84 
     85 impl Parse for ImportList {
     86    fn parse(input: ParseStream) -> syn::Result<Self> {
     87        let mut imports: Vec<Import> = Vec::new();
     88 
     89        while !input.is_empty() {
     90            let reexport = <Token![pub]>::parse(input).ok();
     91 
     92            let str_span = input.span();
     93 
     94            let target = match Lit::parse(input) {
     95                Err(_) => {
     96                    return Err(Error::new(str_span, "expected a GN path as a string literal"));
     97                }
     98                Ok(Lit::Str(label)) => match GnTarget::parse(&label.value(), str_span) {
     99                    Ok(target) => target,
    100                    Err(e) => {
    101                        return Err(Error::new(
    102                            str_span,
    103                            format!("invalid GN path {}: {}", quote::quote! {#label}, e),
    104                        ));
    105                    }
    106                },
    107                Ok(lit) => {
    108                    return Err(Error::new(
    109                        str_span,
    110                        format!(
    111                            "expected a GN path as string literal, found '{}' literal",
    112                            quote::quote! {#lit}
    113                        ),
    114                    ));
    115                }
    116            };
    117            let alias = match <Token![as]>::parse(input) {
    118                Ok(_) => Some(Ident::parse(input)?),
    119                Err(_) => None,
    120            };
    121            <syn::Token![;]>::parse(input)?;
    122 
    123            imports.push(Import { target, alias, reexport });
    124        }
    125 
    126        Ok(Self { imports })
    127    }
    128 }
    129 
    130 /// Escapes non-identifier characters in `symbol`.
    131 ///
    132 /// Importantly, this is
    133 /// [an injective function](https://en.wikipedia.org/wiki/Injective_function)
    134 /// which means that different inputs are never mapped to the same output.
    135 ///
    136 /// This is based on a similar function in
    137 /// https://github.com/google/crubit/blob/22ab04aef9f7cc56d8600c310c7fe20999ffc41b/common/code_gen_utils.rs#L59-L71
    138 /// The main differences are:
    139 ///
    140 /// * Only a limited set of special characters is supported, because this makes
    141 ///   it easier to replicate the escaping algorithm in `.gni` files, using just
    142 ///   `string_replace` calls.
    143 /// * No dependency on `unicode_ident` crate means that instead of
    144 ///   `is_xid_continue` a more restricted call to `char::is_ascii_alphanumeric`
    145 ///   is used.
    146 /// * No support for escaping leading digits.
    147 /// * The escapes are slightly different (e.g. `/` frequently appears in GN
    148 ///   paths and therefore here we map it to a nice `_s` rather than to `_x002f`)
    149 fn escape_non_identifier_chars(symbol: &str) -> Result<String, String> {
    150    assert!(!symbol.is_empty()); // Caller is expected to verify.
    151    if symbol.chars().next().unwrap().is_ascii_digit() {
    152        return Err("Leading digits are not supported".to_string());
    153    }
    154 
    155    // Escaping every character can at most double the size of the string.
    156    let mut result = String::with_capacity(symbol.len() * 2);
    157    for c in symbol.chars() {
    158        // NOTE: TargetName=>CrateName mangling algorithm should be updated
    159        // simultaneously in 3 places: here, //build/rust/rust_target.gni,
    160        // //build/rust/rust_static_library.gni.
    161        match c {
    162            '_' => result.push_str("_u"),
    163            '/' => result.push_str("_s"),
    164            ':' => result.push_str("_c"),
    165            '-' => result.push_str("_d"),
    166            c if c.is_ascii_alphanumeric() => result.push(c),
    167            _ => return Err(format!("Unsupported character in GN path component: `{c}`")),
    168        }
    169    }
    170 
    171    Ok(result)
    172 }