[sw/crypto] Begin implementation of cryptotest tool

This tool will be used to generate cryptolib test drivers by parsing
highly constrained C structs and synthesizing values thereof based on
testvectors. This commit adds the first part of this: the restricted
C struct parser.

Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/sw/host/cryptotest/BUILD b/sw/host/cryptotest/BUILD
new file mode 100644
index 0000000..d7c071e
--- /dev/null
+++ b/sw/host/cryptotest/BUILD
@@ -0,0 +1,19 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+load("//third_party/cargo:crates.bzl", "all_crate_deps")
+
+rust_library(
+    name = "cryptotest_parser",
+    srcs = ["cryptotest_parser.rs"],
+    # FIXME(lowRISC/opentitan#12038): stealing opentitanlib's deps until we get rid
+    # of Cargo.
+    deps = all_crate_deps(package_name = "sw/host/opentitanlib"),
+)
+
+rust_test(
+    name = "cryptotest_parser_test",
+    crate = ":cryptotest_parser",
+)
\ No newline at end of file
diff --git a/sw/host/cryptotest/cryptotest_parser.rs b/sw/host/cryptotest/cryptotest_parser.rs
new file mode 100644
index 0000000..0a43aac
--- /dev/null
+++ b/sw/host/cryptotest/cryptotest_parser.rs
@@ -0,0 +1,907 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+//! Parser for cryptolib test fixtures.
+//!
+//! This file consumes `.c` files and performs a very basic parse on them
+//! to extract the test vector type and any annotations on it.
+//!
+//! This library expects the `.c` file to contain a struct definition
+//! conforming to the following reduced grammar, modulo whitespace.
+//!
+//! ```text
+//! // cryptotest:struct
+//! // Comments...
+//! typedef struct $name? {
+//!   // Comments...
+//!   $type $field;
+//!   // More fields...
+//! } $name;
+//! ```
+//!
+//! Here, `// Comments...` is any number of single-line comments, `$field` is any
+//! valid C identifier, and `$type` is one of the following:
+//! - `bool`
+//! - `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t`
+//! - `int8_t`, `int16_t`, `int32_t`, `int64_t`
+//! - `size_t`
+//! - `const char *` (always a NUL-terminated string).
+//!
+//! In the future, this list will include enumerations defined in the cryptolib's ABI.
+//!
+//! Additionally, the following compound types are recognized:
+//! - `$Int field[N];`, where `$Int` is a non-`const char *` type above and N is an
+//!   integer literal.
+//! - `const $Int *field;`, for a variable-length array of integers.
+//! - `const char *const *field;`, for a variable-length array of strings.
+//! - `const $Int (*field)[N];`, for a variable-length array of fixed arrays of ints.
+//!
+//! Comments that do not begin with `// cryptotest:` are ignored; the rest are parsed
+//! as annotations.
+
+use regex::Regex;
+
+/// A test vector struct.
+///
+/// See the [module docs](self) for information on the grammar.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Struct {
+    /// The name of the struct (as specified by the `typedef`).
+    pub name: String,
+    /// The struct's fields.
+    pub fields: Vec<Field>,
+    /// Any `// cryptotest:` annotations at the top of the struct.
+    pub annots: Vec<Annotation>,
+}
+
+/// A field of a [`Struct`].
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Field {
+    /// The name of the field.
+    pub name: String,
+    /// The type of the field.
+    pub ty: FieldType,
+    /// Any `// cryptotest:` annotations on this field.
+    pub annots: Vec<Annotation>,
+}
+
+/// A [`Field`]'s type.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum FieldType {
+    /// An array field, represented by a raw `const` pointer. The length of
+    /// the array must be specified by another field of the struct
+    /// via `// cryptotest:len`.
+    Array(Scalar),
+    /// A scalar field, consisting of a single value.
+    Scalar(Scalar),
+}
+
+/// An integer type recognized by cryptotest.
+///
+/// Not only does this include the `stdint.h` types, but it also includes cryptolib
+/// enum types known a priori to the parser.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum Int {
+    Bool,
+    U8,
+    U16,
+    U32,
+    U64,
+    I8,
+    I16,
+    I32,
+    I64,
+    Size,
+}
+
+impl Int {
+    /// Parses an `Int` from its C name.
+    pub fn from_name(name: &str) -> Result<Self, Error> {
+        match name {
+            "bool" => Ok(Int::Bool),
+            "uint8_t" => Ok(Int::U8),
+            "uint16_t" => Ok(Int::U16),
+            "uint32_t" => Ok(Int::U32),
+            "uint64_t" => Ok(Int::U64),
+            "int8_t" => Ok(Int::I8),
+            "int16_t" => Ok(Int::I16),
+            "int32_t" => Ok(Int::I32),
+            "int64_t" => Ok(Int::I64),
+            "size_t" => Ok(Int::Size),
+            _ => Err(Error::UnknownIntType(name)),
+        }
+    }
+}
+
+/// A scalar type, i.e., types that do not have external size information.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Scalar {
+    /// An [`Int`] type.
+    Int(Int),
+    /// A vector type, i.e. a fixed-length array, of some kind of integer.
+    Vec(Int, usize),
+    /// A C-style (i.e., NUL-terminated) string, represented as a `const char*`
+    /// pointer.
+    CStr,
+}
+
+/// An annotation recognized by the parser for specifying how cryptotest should
+/// interpret fields.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Annotation {
+    /// The `cryptotest:struct` directive at the top of a struct that is used to
+    /// find where the struct starts.
+    Struct,
+    /// The `cryptotest:len` directive that must be present on each array
+    /// field, which specifies which field specifies its length. The length present
+    /// at runtime may be specified in units of single bits, bytes, or 32-bit words.
+    Len(String, LenUnit),
+}
+
+/// A length unit used by [`Annotation::Len`].
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum LenUnit {
+    Bits,
+    Bytes,
+    Words,
+}
+
+// The "grammar" above is designed to be so simple we can parse it with a pile of
+// regular expressions.
+lazy_static::lazy_static! {
+  static ref STRUCT_START: Regex =
+    Regex::new(r"^typedef\s+struct\s+([a-zA-Z_][0-9a-zA-Z_]*)?\s*\{").unwrap();
+  static ref STRUCT_END: Regex =
+    Regex::new(r"^}\s*([a-zA-Z_][0-9a-zA-Z_]*)\s*;").unwrap();
+  static ref INT_FIELD: Regex =
+    Regex::new(r"^([a-zA-Z_][0-9a-zA-Z_]*)\s+([a-zA-Z_][0-9a-zA-Z_]*)\s*;").unwrap();
+  static ref STRING_FIELD: Regex =
+    Regex::new(r"^const\s+char\s*\*\s*([a-zA-Z_][0-9a-zA-Z_]*)\s*;").unwrap();
+  static ref VECTOR_FIELD: Regex =
+    Regex::new(r"^([a-zA-Z_][0-9a-zA-Z_]*)\s+([a-zA-Z_][0-9a-zA-Z_]*)\s*\[\s*(\w+)\s*\]\s*;").unwrap();
+  static ref INT_ARRAY_FIELD: Regex =
+    Regex::new(r"^const\s+([a-zA-Z_][0-9a-zA-Z_]*)\s*\*\s*([a-zA-Z_][0-9a-zA-Z_]*)\s*;").unwrap();
+  static ref STRING_ARRAY_FIELD: Regex =
+    Regex::new(r"^const\s+char\s*\*\s*const\s*\*\s*([a-zA-Z_][0-9a-zA-Z_]*)\s*;").unwrap();
+  static ref VECTOR_ARRAY_FIELD: Regex =
+    Regex::new(r"^const\s+([a-zA-Z_][0-9a-zA-Z_]*)\s*\(\s*\*\s*([a-zA-Z_][0-9a-zA-Z_]*)\s*\)\s*\[\s*(\w+)\s*\]\s*;").unwrap();
+}
+
+const STRUCT_COMMENT: &str = "// cryptotest:struct\n";
+
+/// An error produced by [`Struct::parse()`].
+#[derive(Debug, thiserror::Error, PartialEq, Eq)]
+pub enum Error<'c> {
+    #[error("missing `// cryptotest:struct` comment")]
+    NoStructFound,
+    #[error("missing `typedef struct {{` prologue")]
+    MissingStructPrologue,
+    #[error("missing `}} name;` epilogue")]
+    MissingStructEpilogue,
+    #[error("unknown integer type: `{0}`")]
+    UnknownIntType(&'c str),
+    #[error(transparent)]
+    BadInt(#[from] std::num::ParseIntError),
+    #[error("expected a field but found something else")]
+    ExpectedField,
+    #[error("unknown array length unit: `{0}`")]
+    UnknownLenUnit(&'c str),
+    #[error("unknown annotation: `{0}`")]
+    UnknownAnnotation(&'c str),
+}
+
+impl Struct {
+    /// Parses a `Struct` from the given C file. This will only parse the first
+    /// `cryptolib:struct` encountered.
+    pub fn parse(mut c_file: &str) -> Result<Struct, Error> {
+        // First, find the struct.
+        let struct_start = c_file.find(STRUCT_COMMENT).ok_or(Error::NoStructFound)?;
+        c_file = &c_file[struct_start..];
+        let mut annots = Vec::new();
+        for comment in munch_comments(&mut c_file) {
+            if let Some(annot) = parse_annotation(comment)? {
+                annots.push(annot);
+            }
+        }
+
+        // Next, find and tear off the struct header.
+        let header = STRUCT_START
+            .find(c_file)
+            .ok_or(Error::MissingStructPrologue)?;
+        c_file = &c_file[header.end()..];
+
+        // Now, parse as many fields as we can.
+        let mut fields = Vec::new();
+        loop {
+            c_file = c_file.trim_start();
+            let mut annots = Vec::new();
+            for comment in munch_comments(&mut c_file) {
+                if let Some(annot) = parse_annotation(comment)? {
+                    annots.push(annot);
+                }
+            }
+
+            if let Some(field) = INT_FIELD.captures(c_file) {
+                c_file = &c_file[field.get(0).unwrap().end()..];
+                let int = field.get(1).unwrap().as_str();
+                let name = field.get(2).unwrap().as_str();
+                fields.push(Field {
+                    name: name.to_string(),
+                    ty: FieldType::Scalar(Scalar::Int(Int::from_name(int)?)),
+                    annots,
+                });
+            } else if let Some(field) = STRING_FIELD.captures(c_file) {
+                c_file = &c_file[field.get(0).unwrap().end()..];
+                let name = field.get(1).unwrap().as_str();
+                fields.push(Field {
+                    name: name.to_string(),
+                    ty: FieldType::Scalar(Scalar::CStr),
+                    annots,
+                });
+            } else if let Some(field) = VECTOR_FIELD.captures(c_file) {
+                c_file = &c_file[field.get(0).unwrap().end()..];
+                let int = field.get(1).unwrap().as_str();
+                let name = field.get(2).unwrap().as_str();
+                let count = field.get(3).unwrap().as_str();
+                fields.push(Field {
+                    name: name.to_string(),
+                    ty: FieldType::Scalar(Scalar::Vec(Int::from_name(int)?, count.parse()?)),
+                    annots,
+                });
+            } else if let Some(field) = INT_ARRAY_FIELD.captures(c_file) {
+                c_file = &c_file[field.get(0).unwrap().end()..];
+                let int = field.get(1).unwrap().as_str();
+                let name = field.get(2).unwrap().as_str();
+                fields.push(Field {
+                    name: name.to_string(),
+                    ty: FieldType::Array(Scalar::Int(Int::from_name(int)?)),
+                    annots,
+                });
+            } else if let Some(field) = STRING_ARRAY_FIELD.captures(c_file) {
+                c_file = &c_file[field.get(0).unwrap().end()..];
+                let name = field.get(1).unwrap().as_str();
+                fields.push(Field {
+                    name: name.to_string(),
+                    ty: FieldType::Array(Scalar::CStr),
+                    annots,
+                });
+            } else if let Some(field) = VECTOR_ARRAY_FIELD.captures(c_file) {
+                c_file = &c_file[field.get(0).unwrap().end()..];
+                let int = field.get(1).unwrap().as_str();
+                let name = field.get(2).unwrap().as_str();
+                let count = field.get(3).unwrap().as_str();
+                fields.push(Field {
+                    name: name.to_string(),
+                    ty: FieldType::Array(Scalar::Vec(Int::from_name(int)?, count.parse()?)),
+                    annots,
+                });
+            } else if c_file.starts_with("}") {
+                break;
+            } else {
+                return Err(Error::ExpectedField);
+            }
+        }
+
+        let end = STRUCT_END
+            .captures(c_file)
+            .ok_or(Error::MissingStructEpilogue)?;
+
+        Ok(Struct {
+            name: end.get(1).unwrap().as_str().to_string(),
+            fields,
+            annots,
+        })
+    }
+}
+
+/// Strips any leading `//` comments from the start of `c_file`, and returns them,
+/// including the `//` prefix.
+fn munch_comments<'a>(c_file: &mut &'a str) -> Vec<&'a str> {
+    let mut comments = Vec::new();
+    loop {
+        *c_file = c_file.trim_start();
+        if !c_file.starts_with("//") {
+            return comments;
+        }
+        let comment_end = c_file.find("\n").unwrap_or(c_file.len());
+        let (comment, rest) = c_file.split_at(comment_end);
+        comments.push(comment);
+        *c_file = rest;
+    }
+}
+
+fn parse_annotation(comment: &str) -> Result<Option<Annotation>, Error> {
+    let comment = match comment.strip_prefix("// cryptotest:") {
+        Some(comment) => comment,
+        None => return Ok(None),
+    };
+
+    match comment.split(" ").collect::<Vec<_>>().as_slice() {
+        ["struct"] => Ok(Some(Annotation::Struct)),
+        ["len", field, units] => {
+            let units = match *units {
+                "bits" => LenUnit::Bits,
+                "bytes" => LenUnit::Bytes,
+                "words" => LenUnit::Words,
+                _ => return Err(Error::UnknownLenUnit(units)),
+            };
+            Ok(Some(Annotation::Len(field.to_string(), units)))
+        }
+        _ => Err(Error::UnknownAnnotation(comment)),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn empty() {
+        assert!(Struct::parse("").is_err())
+    }
+
+    #[test]
+    fn missing_prologue() {
+        assert!(Struct::parse("// cryptotest:struct").is_err())
+    }
+
+    #[test]
+    fn missing_typedef() {
+        assert!(Struct::parse("// cryptotest:struct\nstruct").is_err())
+    }
+
+    #[test]
+    fn empty_struct() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct {} foo_t;
+            "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn missing_name() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {};
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn int_field() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct foo {
+                    // My cool field!
+                    uint32_t x;
+                } foo_t;
+                "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![Field {
+                    name: "x".to_string(),
+                    ty: FieldType::Scalar(Scalar::Int(Int::U32)),
+                    annots: vec![],
+                }],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn missing_field_name() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                uint32_t;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn char_field() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                char is_not_allowed;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn bad_annotation() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                // cryptotest:omelette
+                uint32_t something;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn all_int_fields() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct foo {
+                    bool b;
+                    uint8_t u8;
+                    uint16_t u16;
+                    uint32_t u32;
+                    uint64_t u64;
+                    int8_t i8;
+                    int16_t i16;
+                    int32_t i32;
+                    int64_t i64;
+                    size_t sz;
+                } foo_t;
+                "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![
+                    Field {
+                        name: "b".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::Bool)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "u8".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::U8)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "u16".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::U16)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "u32".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::U32)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "u64".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::U64)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "i8".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::I8)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "i16".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::I16)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "i32".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::I32)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "i64".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::I64)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "sz".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::Size)),
+                        annots: vec![],
+                    },
+                ],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn vec_field() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct foo {
+                    uint32_t key[25];
+                    uint8_t bytes[1];
+                    bool flag;
+                    int8_t more_bytes[9001];
+                } foo_t;
+                "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![
+                    Field {
+                        name: "key".to_string(),
+                        ty: FieldType::Scalar(Scalar::Vec(Int::U32, 25)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "bytes".to_string(),
+                        ty: FieldType::Scalar(Scalar::Vec(Int::U8, 1)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "flag".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::Bool)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "more_bytes".to_string(),
+                        ty: FieldType::Scalar(Scalar::Vec(Int::I8, 9001)),
+                        annots: vec![],
+                    },
+                ],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn missing_vec_field_name() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                uint32_t[4];
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn transposed_vec_field_name() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                uint32_t[4] foo;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn non_literal_vec_len() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                uint32_t foo[kLen];
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn cstr_field() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct foo {
+                    bool flag;
+                    const char *plaintext;
+                } foo_t;
+                "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![
+                    Field {
+                        name: "flag".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::Bool)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "plaintext".to_string(),
+                        ty: FieldType::Scalar(Scalar::CStr),
+                        annots: vec![],
+                    },
+                ],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn missing_cstr_const() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                char *mut_str;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn missing_cstr_star() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                const char mut_str;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn cstr_right_const() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                char *const mut_str;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn int_array_field() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct foo {
+                    size_t word_count;
+                    // cryptotest:len word_count words
+                    const uint32_t *ciphertext;
+                } foo_t;
+                "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![
+                    Field {
+                        name: "word_count".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::Size)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "ciphertext".to_string(),
+                        ty: FieldType::Array(Scalar::Int(Int::U32)),
+                        annots: vec![Annotation::Len("word_count".to_string(), LenUnit::Words)],
+                    },
+                ],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn missing_int_array_const() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count words
+                uint32_t *mut;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn mystery_len() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count mystery
+                uint32_t *mut;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn vec_array_field() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct foo {
+                    size_t word_count;
+                    // cryptotest:len word_count bytes
+                    const uint32_t (*keys)[32];
+                } foo_t;
+                "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![
+                    Field {
+                        name: "word_count".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::Size)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "keys".to_string(),
+                        ty: FieldType::Array(Scalar::Vec(Int::U32, 32)),
+                        annots: vec![Annotation::Len("word_count".to_string(), LenUnit::Bytes)],
+                    },
+                ],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn missing_vec_array_const() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count bytes
+                uint32_t (*keys)[32];
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn missing_vec_array_star() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count bytes
+                uint32_t (keys)[32];
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn missing_vec_array_parens() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count bytes
+                uint32_t *keys[32];
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn cstr_array_field() {
+        assert_eq!(
+            Struct::parse(
+                "
+                // cryptotest:struct
+                typedef struct foo {
+                    size_t word_count;
+                    // cryptotest:len word_count bytes
+                    const char* const* sonnets;
+                } foo_t;
+                "
+            ),
+            Ok(Struct {
+                name: "foo_t".to_string(),
+                fields: vec![
+                    Field {
+                        name: "word_count".to_string(),
+                        ty: FieldType::Scalar(Scalar::Int(Int::Size)),
+                        annots: vec![],
+                    },
+                    Field {
+                        name: "sonnets".to_string(),
+                        ty: FieldType::Array(Scalar::CStr),
+                        annots: vec![Annotation::Len("word_count".to_string(), LenUnit::Bytes)],
+                    },
+                ],
+                annots: vec![Annotation::Struct],
+            })
+        )
+    }
+
+    #[test]
+    fn missing_cstr_array_inner_const() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count bytes
+                char* const* sonnets;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn missing_cstr_array_outer_const() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count bytes
+                const char** sonnets;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn missing_cstr_array_outer_star() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count bytes
+                const char* const sonnets;
+            };
+            "
+        )
+        .is_err())
+    }
+
+    #[test]
+    fn missing_cstr_array_inner_star() {
+        assert!(Struct::parse(
+            "
+            // cryptotest:struct
+            typedef struct {
+                size_t word_count;
+                // cryptotest:len word_count bytes
+                const char const* sonnets;
+            };
+            "
+        )
+        .is_err())
+    }
+}