svd2rust/
util.rs

1use std::borrow::Cow;
2
3pub use crate::config::{Case, IdentFormat};
4use crate::{
5    svd::{Access, Device, Field, RegisterInfo, RegisterProperties},
6    Config,
7};
8use inflections::Inflect;
9use proc_macro2::{Ident, Span, TokenStream};
10use quote::quote;
11use std::collections::HashSet;
12use svd_rs::{MaybeArray, Peripheral, PeripheralInfo};
13
14use syn::{
15    punctuated::Punctuated, token::PathSep, Lit, LitInt, PathArguments, PathSegment, Type, TypePath,
16};
17
18use anyhow::{anyhow, Result};
19
20pub const BITS_PER_BYTE: u32 = 8;
21
22/// List of chars that some vendors use in their peripheral/field names but
23/// that are not valid in Rust ident
24const BLACKLIST_CHARS: &[char] = &['(', ')', '[', ']', '/', ' ', '-'];
25
26fn to_pascal_case(s: &str) -> String {
27    if !s.contains('_') {
28        s.to_pascal_case()
29    } else {
30        let mut string = String::new();
31        let mut parts = s.split('_').peekable();
32        if let Some(&"") = parts.peek() {
33            string.push('_');
34        }
35        while let Some(p) = parts.next() {
36            if p.is_empty() {
37                continue;
38            }
39            string.push_str(&p.to_pascal_case());
40            match parts.peek() {
41                Some(nxt)
42                    if p.ends_with(|s: char| s.is_numeric())
43                        && nxt.starts_with(|s: char| s.is_numeric()) =>
44                {
45                    string.push('_');
46                }
47                Some(&"") => {
48                    string.push('_');
49                }
50                _ => {}
51            }
52        }
53        string
54    }
55}
56
57impl Case {
58    pub fn cow_to_case<'a>(&self, cow: Cow<'a, str>) -> Cow<'a, str> {
59        match self {
60            Self::Constant => match cow {
61                Cow::Borrowed(s) if s.is_constant_case() => cow,
62                _ => cow.to_constant_case().into(),
63            },
64            Self::Pascal => match cow {
65                Cow::Borrowed(s) if s.is_pascal_case() => cow,
66                _ => to_pascal_case(&cow).into(),
67            },
68            Self::Snake => match cow {
69                Cow::Borrowed(s) if s.is_snake_case() => cow,
70                _ => cow.to_snake_case().into(),
71            },
72        }
73    }
74
75    pub fn sanitize<'a>(&self, s: &'a str) -> Cow<'a, str> {
76        let s = sanitize(s);
77        self.cow_to_case(s)
78    }
79}
80
81fn sanitize(s: &str) -> Cow<'_, str> {
82    if s.contains(BLACKLIST_CHARS) {
83        Cow::Owned(s.replace(BLACKLIST_CHARS, ""))
84    } else {
85        s.into()
86    }
87}
88
89pub fn ident(name: &str, config: &Config, fmt: &str, span: Span) -> Ident {
90    Ident::new(
91        &config
92            .ident_formats
93            .get(fmt)
94            .expect("Missing {fmt} entry")
95            .sanitize(name),
96        span,
97    )
98}
99
100impl IdentFormat {
101    pub fn apply<'a>(&self, name: &'a str) -> Cow<'a, str> {
102        let name = match &self.case {
103            Some(case) => case.sanitize(name),
104            _ => sanitize(name),
105        };
106        if self.prefix.is_empty() && self.suffix.is_empty() {
107            name
108        } else {
109            format!("{}{}{}", self.prefix, name, self.suffix).into()
110        }
111    }
112    pub fn sanitize<'a>(&self, name: &'a str) -> Cow<'a, str> {
113        let s = self.apply(name);
114        let s = if s.as_bytes().first().unwrap_or(&0).is_ascii_digit() {
115            Cow::from(format!("_{}", s))
116        } else {
117            s
118        };
119        match self.case {
120            Some(Case::Snake) | None => sanitize_keyword(s),
121            _ => s,
122        }
123    }
124}
125
126pub fn ident_str(name: &str, fmt: &IdentFormat) -> String {
127    let name = match &fmt.case {
128        Some(case) => case.sanitize(name),
129        _ => sanitize(name),
130    };
131    format!("{}{}{}", fmt.prefix, name, fmt.suffix)
132}
133
134pub fn sanitize_keyword(sc: Cow<str>) -> Cow<str> {
135    const KEYWORDS: [&str; 56] = [
136        "abstract", "alignof", "as", "async", "await", "become", "box", "break", "const",
137        "continue", "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for",
138        "gen", "if", "impl", "in", "let", "loop", "macro", "match", "mod", "move", "mut",
139        "offsetof", "override", "priv", "proc", "pub", "pure", "ref", "return", "self", "sizeof",
140        "static", "struct", "super", "trait", "true", "try", "type", "typeof", "unsafe", "unsized",
141        "use", "virtual", "where", "while", "yield",
142    ];
143    if KEYWORDS.contains(&sc.as_ref()) {
144        sc + "_"
145    } else {
146        sc
147    }
148}
149
150pub fn respace(s: &str) -> String {
151    s.split_whitespace()
152        .collect::<Vec<_>>()
153        .join(" ")
154        .replace(r"\n", "\n")
155}
156
157pub fn escape_brackets(s: &str) -> String {
158    s.split('[')
159        .fold(String::new(), |acc, x| {
160            if acc.is_empty() {
161                x.to_string()
162            } else if acc.ends_with('\\') {
163                acc + "[" + x
164            } else {
165                acc + "\\[" + x
166            }
167        })
168        .split(']')
169        .fold(String::new(), |acc, x| {
170            if acc.is_empty() {
171                x.to_string()
172            } else if acc.ends_with('\\') {
173                acc + "]" + x
174            } else {
175                acc + "\\]" + x
176            }
177        })
178}
179
180/// Escape basic html tags and brackets
181pub fn escape_special_chars(s: &str) -> Cow<'_, str> {
182    if s.contains('[') {
183        escape_brackets(s).into()
184    } else {
185        s.into()
186    }
187}
188
189pub fn name_of<T: FullName>(maybe_array: &MaybeArray<T>, ignore_group: bool) -> String {
190    let fullname = maybe_array.fullname(ignore_group);
191    if maybe_array.is_array() {
192        fullname.remove_dim().into()
193    } else {
194        fullname.into()
195    }
196}
197
198pub fn access_of(properties: &RegisterProperties, fields: Option<&[Field]>) -> Access {
199    properties.access.unwrap_or_else(|| {
200        if let Some(fields) = fields {
201            if fields.iter().all(|f| f.access == Some(Access::ReadOnly)) {
202                Access::ReadOnly
203            } else if fields.iter().all(|f| f.access == Some(Access::WriteOnce)) {
204                Access::WriteOnce
205            } else if fields
206                .iter()
207                .all(|f| f.access == Some(Access::ReadWriteOnce))
208            {
209                Access::ReadWriteOnce
210            } else if fields
211                .iter()
212                .all(|f| f.access == Some(Access::WriteOnly) || f.access == Some(Access::WriteOnce))
213            {
214                Access::WriteOnly
215            } else {
216                Access::ReadWrite
217            }
218        } else {
219            Access::ReadWrite
220        }
221    })
222}
223
224pub fn digit_or_hex(n: u64) -> LitInt {
225    if n < 10 {
226        unsuffixed(n)
227    } else {
228        hex(n)
229    }
230}
231
232/// Turns `n` into an unsuffixed separated hex token
233pub fn hex(n: u64) -> LitInt {
234    let (h4, h3, h2, h1) = (
235        (n >> 48) & 0xffff,
236        (n >> 32) & 0xffff,
237        (n >> 16) & 0xffff,
238        n & 0xffff,
239    );
240    LitInt::new(
241        &(if h4 != 0 {
242            format!("0x{h4:04x}_{h3:04x}_{h2:04x}_{h1:04x}")
243        } else if h3 != 0 {
244            format!("0x{h3:04x}_{h2:04x}_{h1:04x}")
245        } else if h2 != 0 {
246            format!("0x{h2:04x}_{h1:04x}")
247        } else if h1 & 0xff00 != 0 {
248            format!("0x{h1:04x}")
249        } else if h1 != 0 {
250            format!("0x{:02x}", h1 & 0xff)
251        } else {
252            "0".to_string()
253        }),
254        Span::call_site(),
255    )
256}
257
258/// Turns non-zero `n` into an unsuffixed separated hex token
259pub fn hex_nonzero(n: u64) -> Option<LitInt> {
260    (n != 0).then(|| hex(n))
261}
262
263/// Turns `n` into an unsuffixed token
264pub fn unsuffixed(n: impl Into<u64>) -> LitInt {
265    LitInt::new(&n.into().to_string(), Span::call_site())
266}
267
268pub fn unsuffixed_or_bool(n: u64, width: u32) -> Lit {
269    if width == 1 {
270        Lit::Bool(syn::LitBool::new(n != 0, Span::call_site()))
271    } else {
272        Lit::Int(unsuffixed(n))
273    }
274}
275
276pub fn new_syn_u32(len: u32, span: Span) -> syn::Expr {
277    syn::Expr::Lit(syn::ExprLit {
278        attrs: Vec::new(),
279        lit: syn::Lit::Int(syn::LitInt::new(&len.to_string(), span)),
280    })
281}
282
283pub fn zst_type() -> Type {
284    Type::Tuple(syn::TypeTuple {
285        paren_token: syn::token::Paren::default(),
286        elems: Punctuated::new(),
287    })
288}
289
290pub fn name_to_ty(name: Ident) -> Type {
291    let mut segments = Punctuated::new();
292    segments.push(path_segment(name));
293    syn::Type::Path(type_path(segments))
294}
295
296pub fn block_path_to_ty(
297    bpath: &svd_parser::expand::BlockPath,
298    config: &Config,
299    span: Span,
300) -> TypePath {
301    let mut path = config.settings.crate_path.clone().unwrap_or_default().0;
302    path.segments.push(path_segment(ident(
303        &bpath.peripheral.remove_dim(),
304        config,
305        "peripheral_mod",
306        span,
307    )));
308    for ps in &bpath.path {
309        path.segments.push(path_segment(ident(
310            &ps.remove_dim(),
311            config,
312            "cluster_mod",
313            span,
314        )));
315    }
316    TypePath { qself: None, path }
317}
318
319pub fn register_path_to_ty(
320    rpath: &svd_parser::expand::RegisterPath,
321    config: &Config,
322    span: Span,
323) -> TypePath {
324    let mut p = block_path_to_ty(&rpath.block, config, span);
325    p.path.segments.push(path_segment(ident(
326        &rpath.name.remove_dim(),
327        config,
328        "register_mod",
329        span,
330    )));
331    p
332}
333
334pub fn ident_to_path(ident: Ident) -> TypePath {
335    let mut segments = Punctuated::new();
336    segments.push(path_segment(ident));
337    type_path(segments)
338}
339
340pub fn type_path(segments: Punctuated<PathSegment, PathSep>) -> TypePath {
341    TypePath {
342        qself: None,
343        path: syn::Path {
344            leading_colon: None,
345            segments,
346        },
347    }
348}
349
350pub fn path_segment(ident: Ident) -> PathSegment {
351    PathSegment {
352        ident,
353        arguments: PathArguments::None,
354    }
355}
356
357pub trait U32Ext {
358    fn size_to_str(&self) -> Result<&str>;
359    fn to_ty(&self) -> Result<Ident>;
360    fn to_ty_width(&self) -> Result<u32>;
361}
362
363impl U32Ext for u32 {
364    fn size_to_str(&self) -> Result<&str> {
365        Ok(match *self {
366            8 => "u8",
367            16 => "u16",
368            32 => "u32",
369            64 => "u64",
370            _ => return Err(anyhow!("can't convert {self} bits into register size type")),
371        })
372    }
373    fn to_ty(&self) -> Result<Ident> {
374        Ok(Ident::new(
375            match *self {
376                1 => "bool",
377                2..=8 => "u8",
378                9..=16 => "u16",
379                17..=32 => "u32",
380                33..=64 => "u64",
381                _ => {
382                    return Err(anyhow!(
383                        "can't convert {self} bits into a Rust integral type"
384                    ))
385                }
386            },
387            Span::call_site(),
388        ))
389    }
390
391    fn to_ty_width(&self) -> Result<u32> {
392        Ok(match *self {
393            1 => 1,
394            2..=8 => 8,
395            9..=16 => 16,
396            17..=32 => 32,
397            33..=64 => 64,
398            _ => {
399                return Err(anyhow!(
400                    "can't convert {self} bits into a Rust integral type width"
401                ))
402            }
403        })
404    }
405}
406
407pub fn build_rs(config: &Config) -> TokenStream {
408    let extra_build = config.extra_build();
409
410    quote! {
411        //! Builder file for Peripheral access crate generated by svd2rust tool
412
413        use std::env;
414        use std::fs::File;
415        use std::io::Write;
416        use std::path::PathBuf;
417
418        fn main() {
419            if env::var_os("CARGO_FEATURE_RT").is_some() {
420                // Put the linker script somewhere the linker can find it
421                let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
422                File::create(out.join("device.x"))
423                    .unwrap()
424                    .write_all(include_bytes!("device.x"))
425                    .unwrap();
426                println!("cargo:rustc-link-search={}", out.display());
427
428                println!("cargo:rerun-if-changed=device.x");
429
430                #extra_build
431            }
432
433            println!("cargo:rerun-if-changed=build.rs");
434        }
435    }
436}
437
438pub trait DimSuffix {
439    fn expand_dim(&self, suffix: &str) -> Cow<'_, str>;
440    fn remove_dim(&self) -> Cow<'_, str> {
441        self.expand_dim("")
442    }
443}
444
445impl DimSuffix for str {
446    fn expand_dim(&self, suffix: &str) -> Cow<'_, str> {
447        if self.contains("%s") {
448            self.replace(if self.contains("[%s]") { "[%s]" } else { "%s" }, suffix)
449                .into()
450        } else {
451            self.into()
452        }
453    }
454}
455
456pub trait FullName {
457    fn fullname(&self, ignore_group: bool) -> Cow<'_, str>;
458}
459
460impl FullName for RegisterInfo {
461    fn fullname(&self, ignore_group: bool) -> Cow<'_, str> {
462        fullname(&self.name, &self.alternate_group, ignore_group)
463    }
464}
465
466pub fn fullname<'a>(name: &'a str, group: &Option<String>, ignore_group: bool) -> Cow<'a, str> {
467    match &group {
468        Some(group) if !ignore_group => format!("{group}_{}", name).into(),
469        _ => name.into(),
470    }
471}
472
473impl FullName for PeripheralInfo {
474    fn fullname(&self, _ignore_group: bool) -> Cow<'_, str> {
475        self.name.as_str().into()
476    }
477}
478
479pub fn group_names<'a>(d: &'a Device, feature_format: &'a IdentFormat) -> Vec<Cow<'a, str>> {
480    let set: HashSet<_> = d
481        .peripherals
482        .iter()
483        .filter_map(|p| p.group_name.as_ref())
484        .map(|name| feature_format.apply(name))
485        .collect();
486    let mut v: Vec<_> = set.into_iter().collect();
487    v.sort();
488    v
489}
490
491pub fn peripheral_names(d: &Device, feature_format: &IdentFormat) -> Vec<String> {
492    let mut v = Vec::new();
493    for p in &d.peripherals {
494        match p {
495            Peripheral::Single(info) => {
496                v.push(feature_format.apply(&info.name).remove_dim().into());
497            }
498            Peripheral::Array(info, dim) => {
499                v.extend(svd_rs::array::names(info, dim).map(|n| feature_format.apply(&n).into()));
500            }
501        }
502    }
503    v.sort();
504    v
505}
506
507#[test]
508fn pascalcase() {
509    assert_eq!(to_pascal_case("_reserved"), "_Reserved");
510    assert_eq!(to_pascal_case("_FOO_BAR_"), "_FooBar_");
511    assert_eq!(to_pascal_case("FOO_BAR1"), "FooBar1");
512    assert_eq!(to_pascal_case("FOO_BAR_1"), "FooBar1");
513    assert_eq!(to_pascal_case("FOO_BAR_1_2"), "FooBar1_2");
514    assert_eq!(to_pascal_case("FOO_BAR_1_2_"), "FooBar1_2_");
515}