├── .gitignore ├── tarpaulin.toml ├── tests ├── recurse.rs ├── errors.rs ├── cbor_ivt.rs ├── cbor_cddl.rs └── json_cddl.rs ├── .github └── workflows │ ├── build_features.yml │ └── build.yml ├── LICENSE ├── Cargo.toml ├── Cranky.toml ├── src ├── value.rs ├── context.rs ├── parser │ └── parse_err.rs ├── util.rs ├── json.rs ├── lib.rs ├── cbor.rs ├── ast.rs ├── ivt.rs └── flatten.rs ├── README.md └── .Cargo.lock.msrv /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /tarpaulin.toml: -------------------------------------------------------------------------------- 1 | [tarpaulin] 2 | target-dir = "target/tarpaulin" 3 | -------------------------------------------------------------------------------- /tests/recurse.rs: -------------------------------------------------------------------------------- 1 | use cddl_cat::parser::parse_cddl; 2 | use ntest::timeout; 3 | 4 | #[test] 5 | #[timeout(5000)] // 5 seconds 6 | fn test_recursion() { 7 | parse_cddl("a = [[[[[[[[[[[[[[[[[[[[[[ int ]]]]]]]]]]]]]]]]]]]]]]").unwrap(); 8 | parse_cddl("a = {{{{{{{{{{{{{{{{{{{{{{ int }}}}}}}}}}}}}}}}}}}}}}").unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/build_features.yml: -------------------------------------------------------------------------------- 1 | name: Build Features 2 | 3 | on: 4 | push: 5 | branches: [master, ci_testing] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | cargo-build: 11 | strategy: 12 | matrix: 13 | features: ["ciborium", "serde_json"] 14 | rust_toolchain: [stable] 15 | os: [ubuntu-latest] 16 | 17 | name: Build 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: checkout 21 | uses: actions/checkout@v2 22 | 23 | - name: install rust toolchain ${{ matrix.rust_toolchain }} 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | profile: minimal 27 | toolchain: ${{ matrix.rust_toolchain }} 28 | override: true 29 | 30 | - name: cargo build 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: build 34 | args: --no-default-features --features ${{ matrix.features }} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eric Seppanen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ master, ci_testing ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | cargo-build: 11 | strategy: 12 | matrix: 13 | rust_toolchain: [stable, nightly, 1.81.0] 14 | os: [ubuntu-latest] 15 | 16 | name: Build 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: install rust toolchain ${{ matrix.rust_toolchain }} 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | profile: minimal 26 | toolchain: ${{ matrix.rust_toolchain }} 27 | override: true 28 | 29 | - name: enable Cargo.lock 30 | if: ${{ matrix.rust_toolchain == '1.81.0' }} 31 | run: cp .Cargo.lock.msrv Cargo.lock 32 | 33 | - name: cargo build 34 | uses: actions-rs/cargo@v1 35 | with: 36 | command: build 37 | args: --bins --examples --tests 38 | 39 | - name: Test with default features 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: test 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cddl-cat" 3 | description = "Parse CDDL schemas and validate CBOR or JSON serialized data" 4 | keywords = ["cddl", "cbor", "json"] 5 | categories = ["encoding", "network-programming", "parser-implementations"] 6 | version = "0.7.0" 7 | repository = "https://github.com/ericseppanen/cddl-cat" 8 | license = "MIT" 9 | authors = ["Eric Seppanen "] 10 | readme = "README.md" 11 | edition = "2018" 12 | rust-version = "1.81.0" 13 | 14 | [features] 15 | default = ["serde_json", "ciborium"] 16 | 17 | [dependencies] 18 | float-ord = "0.3.0" 19 | ciborium = { version = "0.2.2", optional = true } 20 | serde_json = { version = "1.0.0", optional = true } 21 | serde = "1.0.97" 22 | # nom's default-features are ["std", "lexical"]. 23 | nom = { version = "7.0.0", features = ["std"], default-features = false } 24 | hex = "0.4.0" 25 | strum_macros = "0.23.1" 26 | escape8259 = "0.5.0" 27 | base64 = "0.22.1" 28 | thiserror = "1.0.8" 29 | regex = "1.5.5" 30 | 31 | [dev-dependencies] 32 | serde = { version = "1.0.97", features = ["derive"] } 33 | ntest = "0.7.1" 34 | 35 | [package.metadata.release] 36 | pre-release-commit-message = "release {{version}}" 37 | -------------------------------------------------------------------------------- /tests/errors.rs: -------------------------------------------------------------------------------- 1 | use cddl_cat::parse_cddl; 2 | 3 | #[test] 4 | fn error_traits() { 5 | let bad_cddl = "!"; 6 | let err = parse_cddl(bad_cddl).unwrap_err(); 7 | 8 | // It would be unfriendly to not support Send + Sync + Unpin. 9 | // Error types should also support Error, Display, and Debug. 10 | fn has_traits1(_: &T) {} 11 | fn has_traits2(_: &T) {} 12 | 13 | has_traits1(&err); 14 | has_traits2(&err); 15 | } 16 | 17 | #[cfg(feature = "serde_json")] 18 | mod uses_json { 19 | use cddl_cat::json::validate_json_str; 20 | 21 | #[test] 22 | fn error_display() { 23 | let err = validate_json_str("x", "!", "0").unwrap_err(); 24 | assert_eq!(format!("{}", err), "Unparseable(!)"); 25 | 26 | // JSON parsing error 27 | let err = validate_json_str("x", "x = nil", "🦀").unwrap_err(); 28 | assert_eq!( 29 | format!("{}", err), 30 | "ValueError(expected value at line 1 column 1)" 31 | ); 32 | 33 | let err = validate_json_str("x", "x = nil", "0").unwrap_err(); 34 | assert_eq!(format!("{}", err), "Mismatch(expected nil)"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Cranky.toml: -------------------------------------------------------------------------------- 1 | warn = [ 2 | # rustc 3 | "unsafe_code", 4 | "missing_docs", 5 | # Restriction 6 | "clippy::clone_on_ref_ptr", 7 | "clippy::empty_structs_with_brackets", 8 | "clippy::get_unwrap", 9 | "clippy::mem_forget", 10 | "clippy::rc_buffer", 11 | "clippy::rc_mutex", 12 | "clippy::same_name_method", 13 | "clippy::string_to_string", 14 | "clippy::unnecessary_self_imports", 15 | "clippy::verbose_file_reads", 16 | # Cargo 17 | "clippy::cargo_common_metadata", 18 | "clippy::multiple_crate_versions", 19 | "clippy::wildcard_dependencies", 20 | # Pedantic 21 | "clippy::cast_lossless", 22 | "clippy::cast_possible_truncation", 23 | "clippy::cast_possible_wrap", 24 | "clippy::cast_sign_loss", 25 | "clippy::enum_glob_use", 26 | "clippy::expl_impl_clone_on_copy", 27 | "clippy::explicit_into_iter_loop", 28 | "clippy::explicit_iter_loop", 29 | "clippy::fn_params_excessive_bools", 30 | "clippy::if_not_else", 31 | "clippy::implicit_clone", 32 | "clippy::implicit_saturating_sub", 33 | "clippy::large_stack_arrays", 34 | "clippy::large_types_passed_by_value", 35 | "clippy::manual_ok_or", 36 | "clippy::map_unwrap_or", 37 | "clippy::mut_mut", 38 | "clippy::needless_bitwise_bool", 39 | "clippy::needless_continue", 40 | "clippy::range_plus_one", 41 | "clippy::ref_binding_to_reference", 42 | "clippy::ref_option_ref", 43 | "clippy::unnecessary_join", 44 | "clippy::verbose_bit_mask", 45 | ] 46 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | //! This module declares a generic Value enum for use with validation. 2 | 3 | use float_ord::FloatOrd; 4 | use std::collections::BTreeMap; 5 | use std::fmt; 6 | 7 | /// `Value` represents all the types of data we can validate. 8 | /// 9 | /// To validate a new type of data, write implementations of the `From` 10 | /// trait for that type. See the [`cbor`] module for an example. 11 | /// 12 | /// [`cbor`]: crate::cbor 13 | /// 14 | #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] 15 | #[allow(missing_docs)] 16 | pub enum Value { 17 | Null, 18 | Bool(bool), 19 | Integer(i128), 20 | Float(FloatOrd), 21 | Bytes(Vec), 22 | Text(String), 23 | Array(Vec), 24 | Map(BTreeMap), 25 | } 26 | 27 | // FloatOrd doesn't implement Debug, so we have to do all the work by hand. 28 | impl fmt::Debug for Value { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | match self { 31 | Value::Null => write!(f, "Null"), 32 | Value::Bool(x) => x.fmt(f), 33 | Value::Integer(x) => x.fmt(f), 34 | Value::Float(x) => x.0.fmt(f), 35 | Value::Bytes(x) => x.fmt(f), 36 | Value::Text(x) => x.fmt(f), 37 | Value::Array(x) => x.fmt(f), 38 | Value::Map(x) => x.fmt(f), 39 | } 40 | } 41 | } 42 | 43 | // Only exists so implementers don't need to use/see float_ord::FloatOrd 44 | impl Value { 45 | pub(crate) fn from_float>(f: F) -> Value { 46 | Value::Float(FloatOrd(f.into())) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the LookupContext trait. 2 | //! 3 | //! A [`LookupContext`] is used to specify runtime behavior for validation. 4 | //! When a validation needs to resolve a rule reference, it will ask the 5 | //! `LookupContext` to perform the name resolution. 6 | //! 7 | 8 | use crate::ivt::{RuleDef, RulesByName}; 9 | use crate::util::ValidateError; 10 | 11 | // The Node reference lives as long as the LookupContext does. 12 | type LookupResult<'a> = Result<&'a RuleDef, ValidateError>; 13 | 14 | /// A LookupContext contains any external information required for validation. 15 | /// 16 | /// Right now, that only includes a function that understands how to resolve 17 | /// a name reference to an [`ivt::Rule`]. 18 | /// 19 | /// [`ivt::Rule`]: crate::ivt::Rule 20 | /// 21 | pub trait LookupContext { 22 | /// Lookup a rule by name. 23 | fn lookup_rule<'a>(&'a self, name: &str) -> LookupResult<'a>; 24 | } 25 | 26 | /// A simple context that owns a set of rules and can lookup rules by name. 27 | #[allow(missing_docs)] 28 | pub struct BasicContext { 29 | pub rules: RulesByName, 30 | } 31 | 32 | impl BasicContext { 33 | /// Create a new BasicContext from a rules map. 34 | pub fn new(rules: RulesByName) -> BasicContext { 35 | BasicContext { rules } 36 | } 37 | } 38 | 39 | impl LookupContext for BasicContext { 40 | fn lookup_rule<'a>(&'a self, name: &str) -> LookupResult<'a> { 41 | match self.rules.get(name) { 42 | Some(rule_def) => Ok(rule_def), 43 | None => Err(ValidateError::MissingRule(name.into())), 44 | } 45 | } 46 | } 47 | 48 | #[doc(hidden)] // Only pub for integration tests 49 | #[allow(missing_docs)] 50 | pub mod tests { 51 | use super::*; 52 | 53 | /// A `LookupContext` that fails all rule lookups 54 | pub struct DummyContext; 55 | 56 | impl LookupContext for DummyContext { 57 | fn lookup_rule<'a>(&'a self, name: &str) -> LookupResult<'a> { 58 | Err(ValidateError::MissingRule(name.into())) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/parser/parse_err.rs: -------------------------------------------------------------------------------- 1 | //! Parser error types and related utilities 2 | //! 3 | 4 | use nom::error::FromExternalError; 5 | use std::borrow::Cow; 6 | use thiserror::Error; 7 | 8 | /// The "kind" of error generated during CDDL parsing. 9 | #[non_exhaustive] 10 | #[derive(Debug, PartialEq, Eq)] 11 | pub enum ErrorKind { 12 | /// An integer didn't parse correctly. 13 | MalformedInteger, 14 | /// A floating-point number didn't parse correctly. 15 | MalformedFloat, 16 | /// A hex literal didn't parse correctly. 17 | MalformedHex, 18 | /// A malformed text string 19 | MalformedText, 20 | /// A malformed base64 byte string 21 | MalformedBase64, 22 | /// A nonspecific parsing error. 23 | Unparseable, 24 | } 25 | 26 | /// An error that occurred during CDDL parsing. 27 | #[derive(Debug, Error)] 28 | // thiserror will generate a Display implementation. 29 | #[error("{kind:?}({ctx})")] 30 | pub struct ParseError { 31 | /// The "kind" of error generated during CDDL parsing. 32 | pub kind: ErrorKind, 33 | /// A snippet of text from the CDDL input that may be the cause of the error. 34 | pub ctx: String, 35 | } 36 | 37 | // Convert a temporary error into an owned 'static error. 38 | impl From> for ParseError { 39 | fn from(err: CowParseError<'_>) -> Self { 40 | ParseError { 41 | kind: err.kind, 42 | // Create an owned String from the Cow<'_, str> 43 | ctx: err.ctx.into(), 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug, PartialEq)] 49 | pub(crate) struct CowParseError<'a> { 50 | /// The "kind" of error generated during CDDL parsing. 51 | pub kind: ErrorKind, 52 | /// A snippet of text from the CDDL input that may be the cause of the error. 53 | /// 54 | /// This may contain either a borrowed 'str or an owned String. This is useful 55 | /// because many transient errors are generated during parsing and thrown away, 56 | /// and there's no point allocating memory for them until we are done parsing. 57 | pub ctx: Cow<'a, str>, 58 | } 59 | 60 | // Convert a bounded lifetime ParseError into a ParseError<'static>. 61 | 62 | impl FromExternalError for CowParseError<'_> { 63 | fn from_external_error(_input: I, _kind: nom::error::ErrorKind, _e: E) -> Self { 64 | CowParseError { 65 | kind: ErrorKind::Unparseable, 66 | ctx: "nom-error".into(), 67 | } 68 | } 69 | } 70 | 71 | pub(crate) fn parse_error<'a, S: Into>>(kind: ErrorKind, ctx: S) -> CowParseError<'a> { 72 | CowParseError { 73 | kind, 74 | ctx: ctx.into(), 75 | } 76 | } 77 | 78 | // Used when calling all_consuming() at the end of the parsing process. 79 | impl From>> for ParseError { 80 | fn from(e: nom::Err) -> ParseError { 81 | match e { 82 | nom::Err::Incomplete(_) => parse_error(ErrorKind::Unparseable, "Incomplete"), 83 | nom::Err::Error(pe) => pe, 84 | nom::Err::Failure(pe) => pe, 85 | } 86 | .into() 87 | } 88 | } 89 | 90 | // FIXME: the name collision here makes the code hard to read 91 | impl<'a, I: Into>> nom::error::ParseError for CowParseError<'a> { 92 | fn from_error_kind(input: I, _kind: nom::error::ErrorKind) -> Self { 93 | parse_error(ErrorKind::Unparseable, input) 94 | } 95 | 96 | fn append(_input: I, _kind: nom::error::ErrorKind, other: Self) -> Self { 97 | // FIXME: It's not obvious what I should do here, or 98 | // when a proper implementation will be necessary... 99 | other 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | `cddl-cat` is a library for validating encoded data against a CDDL 4 | document that describes the expected structure of the data. 5 | 6 | CDDL is a text document described by [RFC8610] that describes data 7 | structures. CDDL is not tied to any specific serialization or encoding 8 | method; it can be used to validate data that is in [CBOR] or JSON format. 9 | 10 | The goal of this library is to make CBOR or JSON data easy to validate 11 | against a CDDL schema description. 12 | 13 | `cddl-cat` supports Rust 1.81 and later. 14 | 15 | # Implementation Details 16 | 17 | - Supports CBOR and JSON encodings, controlled by the `ciborium` and 18 | `serde_json` features. 19 | 20 | - An "Intermediate Validation Tree" ([`ivt`](https://docs.rs/cddl-cat/latest/cddl-cat/ivt/)) is constructed 21 | from the CDDL AST; this removes some of the CDDL syntax detail resulting 22 | in a simplified tree that can be more easily validated. The IVT is 23 | constructed almost entirely of [`Node`](https://docs.rs/cddl-cat/latest/cddl-cat/ivt/enum.Node.html) elements, 24 | allowing recursive validation. 25 | 26 | - Validation is performed by first translating the incoming data into 27 | a generic form, so most of the validation code is completely agnostic 28 | to the serialization format. 29 | 30 | - Validation code uses a [`LookupContext`](https://docs.rs/cddl-cat/latest/cddl-cat/context/trait.LookupContext.html) object 31 | to perform all rule lookups. This will allow stacking CDDL documents or 32 | building CDDL libraries that can be used by other CDDL schemas. In the 33 | future the validation process itself may be customized by changing the 34 | `LookupContext` configuration. 35 | 36 | - Comprehensive tests (90%+ coverage). 37 | 38 | # Examples 39 | 40 | This example validates JSON-encoded data against a CDDL schema: 41 | 42 | ```rust 43 | use cddl_cat::validate_json_str; 44 | 45 | let cddl_input = "person = {name: tstr, age: int}"; 46 | let json_str = r#"{ "name": "Bob", "age": 43 }"#; 47 | 48 | validate_json_str("person", cddl_input, &json_str).unwrap(); 49 | ``` 50 | 51 | If the JSON data doesn't have the expected structure, an error will 52 | result: 53 | ```rust 54 | use cddl_cat::validate_json_str; 55 | 56 | let cddl_input = "person = {name: tstr, age: int}"; 57 | let json_str = r#"{ "name": "Bob", "age": "forty three" }"#; 58 | 59 | assert!(validate_json_str("person", cddl_input, &json_str).is_err()); 60 | ``` 61 | 62 | A similar example, verifying CBOR-encoded data against a CDDL schema: 63 | ```rust 64 | use cddl_cat::validate_cbor_bytes; 65 | use serde::Serialize; 66 | 67 | #[derive(Serialize)] 68 | struct PersonStruct { 69 | name: String, 70 | age: u32, 71 | } 72 | 73 | let input = PersonStruct { 74 | name: "Bob".to_string(), 75 | age: 43, 76 | }; 77 | let mut cbor_bytes = Vec::new(); 78 | ciborium::into_writer(&input, &mut cbor_bytes).unwrap(); 79 | let cddl_input = "person = {name: tstr, age: int}"; 80 | validate_cbor_bytes("person", cddl_input, &cbor_bytes).unwrap(); 81 | ``` 82 | Supported prelude types: 83 | - `any`, `uint`, `nint`, `int`, `bstr`, `bytes`, `tstr`, `text` 84 | - `float`, `float16`, `float32`, `float64`, `float16-32`, `float32-64` 85 | 86 | Note: float sizes are not validated. 87 | 88 | Supported CDDL features: 89 | - Basic prelude types (integers, floats, bool, nil, text strings, byte strings) 90 | - Literal int, float, bool, UTF-8 text strings 91 | - Byte strings in UTF-8, hex, or base64 92 | - Arrays and maps 93 | - Rule lookups by name 94 | - Groups 95 | - Choices (using `/` or `//` syntax) 96 | - Occurrences (`?`, `*`, `+`, or `m*n`) 97 | - Ranges (e.g. `1..7` or `1...8`) 98 | - Unwrapping (`~`) 99 | - Turn a group into a choice (`&`) 100 | - Map keys with cut syntax (`^ =>`) 101 | - Generic types 102 | - Control operators `.cbor`, `.size`, and `.regexp` 103 | 104 | Unimplemented CDDL features: 105 | - Extend type with `/=` 106 | - Extend group with `//=` 107 | - Type sockets with `$` 108 | - Group sockets with `$$` 109 | - Control operators other than those above (e.g. `.bits`, `.lt`, `.gt`...) 110 | - Group enumeration with `&` 111 | - Tagged data with `#` 112 | - Hexfloat literals (e.g. `0x1.921fb5p+1`) 113 | - Prelude types that invoke CBOR tags (e.g. `tdate` or `biguint`) 114 | 115 | [RFC8610]: https://tools.ietf.org/html/rfc8610 116 | [CBOR]: https://cbor.io/ 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! This module defines error and result types. 2 | //! 3 | 4 | use crate::parser; 5 | use std::result::Result; 6 | use thiserror::Error; 7 | 8 | /// A basic error type that contains a string. 9 | #[allow(missing_docs)] 10 | #[non_exhaustive] 11 | #[derive(Debug, Error)] 12 | pub enum ValidateError { 13 | /// An error during CDDL parsing. 14 | #[error(transparent)] 15 | ParseError(#[from] parser::ParseError), 16 | /// A logical error in the CDDL structure. 17 | #[error("Structural({0})")] 18 | Structural(String), 19 | /// A data mismatch during validation. 20 | // The difference between Mismatch and MapCut is that they trigger 21 | // slightly different internal behavior; to a human reader they mean 22 | // the same thing so we will Display them the same way. 23 | #[error("Mismatch(expected {})", .0.expected)] 24 | Mismatch(Mismatch), 25 | /// A map key-value cut error. 26 | #[error("Mismatch(expected {})", .0.expected)] 27 | MapCut(Mismatch), 28 | /// A CDDL rule lookup failed. 29 | #[error("MissingRule({0})")] 30 | MissingRule(String), 31 | /// A CDDL feature that is unsupported. 32 | #[error("Unsupported {0}")] 33 | Unsupported(String), 34 | /// A data value that can't be validated by CDDL. 35 | #[error("ValueError({0})")] 36 | ValueError(String), 37 | /// A generic type parameter was used incorrectly. 38 | #[error("GenericError")] 39 | GenericError, 40 | } 41 | 42 | impl ValidateError { 43 | /// Identify whether this error is fatal 44 | /// 45 | /// A "fatal" error is one that should fail the entire validation, even if 46 | /// it occurs inside a choice or occurrence that might otherwise succeed. 47 | pub(crate) fn is_fatal(&self) -> bool { 48 | !matches!(self, ValidateError::Mismatch(_) | ValidateError::MapCut(_)) 49 | } 50 | 51 | /// Convert a MapCut error to a Mismatch error; otherwise return the original error. 52 | pub(crate) fn erase_mapcut(self) -> ValidateError { 53 | match self { 54 | ValidateError::MapCut(m) => ValidateError::Mismatch(m), 55 | _ => self, 56 | } 57 | } 58 | 59 | pub(crate) fn is_mismatch(&self) -> bool { 60 | matches!(self, ValidateError::Mismatch(_)) 61 | } 62 | } 63 | 64 | /// A data mismatch during validation. 65 | /// 66 | /// If the CDDL specified an `int` and the data contained a string, this is 67 | /// the error that would result. 68 | #[derive(Debug, PartialEq, Eq)] 69 | pub struct Mismatch { 70 | expected: String, 71 | } 72 | 73 | /// Shortcut for creating mismatch errors. 74 | #[doc(hidden)] 75 | pub fn mismatch>(expected: E) -> ValidateError { 76 | ValidateError::Mismatch(Mismatch { 77 | expected: expected.into(), 78 | }) 79 | } 80 | 81 | /// A validation that doesn't return anything. 82 | pub type ValidateResult = Result<(), ValidateError>; 83 | 84 | // Some utility functions that are helpful when testing whether the right 85 | // error was returned. 86 | #[doc(hidden)] 87 | pub trait ErrorMatch { 88 | fn err_mismatch(&self); 89 | fn err_missing_rule(&self); 90 | fn err_generic(&self); 91 | fn err_parse(&self); 92 | fn err_structural(&self); 93 | } 94 | 95 | impl ErrorMatch for ValidateResult { 96 | #[track_caller] 97 | fn err_mismatch(&self) { 98 | match self { 99 | Err(ValidateError::Mismatch(_)) => (), 100 | _ => panic!("expected Mismatch, got {:?}", self), 101 | } 102 | } 103 | 104 | #[track_caller] 105 | fn err_missing_rule(&self) { 106 | match self { 107 | Err(ValidateError::MissingRule(_)) => (), 108 | _ => panic!("expected MissingRule, got {:?}", self), 109 | } 110 | } 111 | 112 | #[track_caller] 113 | fn err_generic(&self) { 114 | match self { 115 | Err(ValidateError::GenericError) => (), 116 | _ => panic!("expected GenericError, got {:?}", self), 117 | } 118 | } 119 | 120 | #[track_caller] 121 | fn err_parse(&self) { 122 | match self { 123 | Err(ValidateError::ParseError(_)) => (), 124 | _ => panic!("expected ParseError, got {:?}", self), 125 | } 126 | } 127 | 128 | #[track_caller] 129 | fn err_structural(&self) { 130 | match self { 131 | Err(ValidateError::Structural(_)) => (), 132 | _ => panic!("expected Structural, got {:?}", self), 133 | } 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::*; 140 | 141 | #[test] 142 | fn test_error_extras() { 143 | let e: ValidateError = mismatch(""); 144 | assert!(!e.is_fatal()); 145 | 146 | let e = ValidateError::Structural("".into()); 147 | assert!(e.is_fatal()); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/json.rs: -------------------------------------------------------------------------------- 1 | //! This module implements validation from [`serde_json::Value`]. 2 | //! 3 | //! # Examples 4 | //! 5 | //! ``` 6 | //! use cddl_cat::validate_json_str; 7 | //! 8 | //! let cddl_input = "person = {name: tstr, age: int}"; 9 | //! let json_str = r#"{ "name": "Bob", "age": 43 }"#; 10 | //! 11 | //! validate_json_str("person", cddl_input, &json_str).unwrap(); 12 | //! ``` 13 | //! 14 | 15 | use crate::context::{BasicContext, LookupContext}; 16 | use crate::flatten::flatten_from_str; 17 | use crate::ivt::RuleDef; 18 | use crate::util::{ValidateError, ValidateResult}; 19 | use crate::validate::do_validate; 20 | use crate::value::Value; 21 | use serde_json::Value as JSON_Value; 22 | use std::collections::BTreeMap; 23 | use std::convert::TryFrom; 24 | 25 | // Convert JSON `Value`s to the local `Value` type that the validate code 26 | // uses. 27 | 28 | impl TryFrom<&JSON_Value> for Value { 29 | type Error = ValidateError; 30 | 31 | fn try_from(value: &JSON_Value) -> Result { 32 | let result = match value { 33 | JSON_Value::Null => Value::Null, 34 | JSON_Value::Bool(b) => Value::Bool(*b), 35 | JSON_Value::Number(num) => { 36 | if let Some(u) = num.as_u64() { 37 | Value::Integer(i128::from(u)) 38 | } else if let Some(i) = num.as_i64() { 39 | Value::Integer(i128::from(i)) 40 | } else if let Some(f) = num.as_f64() { 41 | Value::from_float(f) 42 | } else { 43 | return Err(ValidateError::ValueError( 44 | "JSON Value::Number conversion failure".into(), 45 | )); 46 | } 47 | } 48 | JSON_Value::String(t) => Value::Text(t.clone()), 49 | JSON_Value::Array(a) => { 50 | let array: Result<_, _> = a.iter().map(Value::try_from).collect(); 51 | Value::Array(array?) 52 | } 53 | JSON_Value::Object(m) => { 54 | type MapTree = BTreeMap; 55 | let map: Result = m 56 | .iter() 57 | .map(|(k, v)| { 58 | // An iterator returning a 2-tuple can be used as (key, value) 59 | // when building a new map. 60 | Ok((Value::Text(k.clone()), Value::try_from(v)?)) 61 | }) 62 | .collect(); 63 | Value::Map(map?) 64 | } 65 | }; 66 | Ok(result) 67 | } 68 | } 69 | 70 | // A variant that consumes the JSON Value. 71 | impl TryFrom for Value { 72 | type Error = ValidateError; 73 | 74 | fn try_from(value: JSON_Value) -> Result { 75 | Value::try_from(&value) 76 | } 77 | } 78 | 79 | /// Validate already-parsed JSON data against an already-parsed CDDL schema. 80 | pub fn validate_json( 81 | rule_def: &RuleDef, 82 | value: &JSON_Value, 83 | ctx: &dyn LookupContext, 84 | ) -> ValidateResult { 85 | let value = Value::try_from(value)?; 86 | do_validate(&value, rule_def, ctx) 87 | } 88 | 89 | /// Validate JSON-encoded data against a specified rule in a UTF-8 CDDL schema. 90 | pub fn validate_json_str(name: &str, cddl: &str, json: &str) -> ValidateResult { 91 | // Parse the CDDL text and flatten it into IVT form. 92 | let flat_cddl = flatten_from_str(cddl)?; 93 | let ctx = BasicContext::new(flat_cddl); 94 | 95 | // Find the rule definition that was requested 96 | let rule_def: &RuleDef = ctx 97 | .rules 98 | .get(name) 99 | .ok_or_else(|| ValidateError::MissingRule(name.into()))?; 100 | 101 | // Deserialize the JSON bytes 102 | let json_value: JSON_Value = 103 | serde_json::from_str(json).map_err(|e| ValidateError::ValueError(format!("{}", e)))?; 104 | 105 | // Convert the JSON tree into a Value tree for validation 106 | let value = Value::try_from(json_value)?; 107 | do_validate(&value, rule_def, &ctx) 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | 114 | #[test] 115 | fn test_json_number_behavior() { 116 | // Ensures that our JSON decoder tracks number types precisely, and 117 | // doesn't, say, allow floating-point values to become integers. 118 | // serde_json does sometimes permit as_f64 to work on integers, which is 119 | // why try_from has to test u64, then i64, then f64. 120 | 121 | let json_value: JSON_Value = serde_json::from_str("1").unwrap(); 122 | assert!(json_value.as_u64().is_some()); 123 | 124 | let json_value: JSON_Value = serde_json::from_str("-1").unwrap(); 125 | assert!(json_value.as_u64().is_none()); 126 | assert!(json_value.as_i64().is_some()); 127 | 128 | let json_value: JSON_Value = serde_json::from_str("1.0").unwrap(); 129 | assert!(json_value.as_u64().is_none()); 130 | assert!(json_value.as_i64().is_none()); 131 | assert!(json_value.as_f64().is_some()); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/cbor_ivt.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "ciborium")] 2 | 3 | use cddl_cat::cbor::validate_cbor; 4 | use cddl_cat::context::{tests::DummyContext, BasicContext}; 5 | use cddl_cat::ivt::*; 6 | use cddl_cat::util::ValidateResult; 7 | use ciborium::value::Integer; 8 | use ciborium::Value; 9 | use serde::ser::Serialize; 10 | use std::collections::HashMap; 11 | 12 | // Some Node structs to test with 13 | static PRELUDE_INT: &Node = &Node::PreludeType(PreludeType::Int); 14 | static PRELUDE_TSTR: &Node = &Node::PreludeType(PreludeType::Tstr); 15 | static LITERAL_7: &Node = &Node::Literal(Literal::Int(7)); 16 | 17 | // Create a Value instance from anything that's serializable 18 | fn gen_value(value: T) -> Value { 19 | // hack: serialize to a Vec and then deserialize to a Value 20 | let mut serialized = Vec::new(); 21 | ciborium::into_writer(&value, &mut serialized).unwrap(); 22 | ciborium::from_reader(serialized.as_slice()).unwrap() 23 | } 24 | 25 | trait TestValidate { 26 | fn test_validate(&self, node: &Node) -> ValidateResult; 27 | } 28 | 29 | impl TestValidate for Value { 30 | // Create a validation context and perform validation 31 | fn test_validate(&self, node: &Node) -> ValidateResult { 32 | // We don't need to do any Rule lookups, so an empty Context will do. 33 | let ctx = DummyContext; 34 | let rule_def = RuleDef { 35 | node: node.clone(), 36 | generic_parms: Vec::new(), 37 | }; 38 | validate_cbor(&rule_def, self, &ctx) 39 | } 40 | } 41 | 42 | #[test] 43 | fn validate_prelude_int() { 44 | let node = PRELUDE_INT; 45 | gen_value(7).test_validate(node).unwrap(); 46 | gen_value("abc").test_validate(node).unwrap_err(); 47 | } 48 | 49 | #[test] 50 | fn validate_prelude_text() { 51 | let node = PRELUDE_TSTR; 52 | gen_value("abc").test_validate(node).unwrap(); 53 | gen_value(7).test_validate(node).unwrap_err(); 54 | } 55 | 56 | #[test] 57 | fn validate_literal_int() { 58 | let node = LITERAL_7; 59 | gen_value(7).test_validate(node).unwrap(); 60 | gen_value(8).test_validate(node).unwrap_err(); 61 | } 62 | 63 | #[test] 64 | fn validate_literal_text() { 65 | let node = &literal_text("abc"); 66 | Value::Text("abc".to_string()).test_validate(node).unwrap(); 67 | Value::Integer(Integer::from(8)) 68 | .test_validate(node) 69 | .unwrap_err(); 70 | } 71 | 72 | #[test] 73 | fn validate_choice() { 74 | let options = [1, 2, 3]; 75 | let options = options.iter().map(|n| literal_int(*n)).collect(); 76 | let node = &Node::Choice(Choice { options }); 77 | 78 | gen_value(1).test_validate(node).unwrap(); 79 | gen_value(2).test_validate(node).unwrap(); 80 | gen_value(3).test_validate(node).unwrap(); 81 | gen_value(4).test_validate(node).unwrap_err(); 82 | gen_value("abc").test_validate(node).unwrap_err(); 83 | } 84 | 85 | #[test] 86 | fn validate_map() { 87 | let kv_template = [ 88 | ("Alice", PreludeType::Int), 89 | ("Bob", PreludeType::Int), 90 | ("Carol", PreludeType::Int), 91 | ]; 92 | let kv_vec: Vec = kv_template 93 | .iter() 94 | .map(|kv| { 95 | let key = literal_text(kv.0); 96 | let value = Node::PreludeType(kv.1); 97 | let cut = true; 98 | Node::KeyValue(KeyValue::new(key, value, cut)) 99 | }) 100 | .collect(); 101 | let node = &Node::Map(Map { members: kv_vec }); 102 | 103 | let value_template = [("Alice", 42), ("Bob", 43), ("Carol", 44)]; 104 | let value: HashMap<&str, u32> = value_template.iter().cloned().collect(); 105 | gen_value(value).test_validate(node).unwrap(); 106 | 107 | // Missing the "Bob" key 108 | let value_template = [("Alice", 42), ("Carol", 44)]; 109 | let value: HashMap<&str, u32> = value_template.iter().cloned().collect(); 110 | let mut value = gen_value(value); 111 | value.test_validate(node).unwrap_err(); 112 | 113 | // Add the "Bob" key with the wrong type. 114 | match &mut value { 115 | Value::Map(m) => { 116 | let key = gen_value("Bob"); 117 | let value = gen_value("forty three"); 118 | m.push((key, value)); 119 | } 120 | _ => unreachable!(), 121 | } 122 | value.test_validate(node).unwrap_err(); 123 | 124 | // Has an extra "David" key 125 | let value_template = [("Alice", 42), ("Bob", 43), ("Carol", 44), ("David", 45)]; 126 | let value: HashMap<&str, u32> = value_template.iter().cloned().collect(); 127 | gen_value(value).test_validate(node).unwrap_err(); 128 | 129 | // Attempt to match against a different (non-Map) Value type 130 | Value::Integer(Integer::from(1)) 131 | .test_validate(node) 132 | .unwrap_err(); 133 | } 134 | 135 | #[test] 136 | fn validate_rule_ref() { 137 | let mut rules = RulesByName::new(); 138 | rules.insert( 139 | "seven".to_string(), 140 | RuleDef { 141 | generic_parms: Vec::new(), 142 | node: LITERAL_7.clone(), 143 | }, 144 | ); 145 | 146 | let ctx = BasicContext::new(rules); 147 | let node = Node::Rule(Rule::new_name("seven")); 148 | let rule_def = RuleDef { 149 | node, 150 | generic_parms: Vec::new(), 151 | }; 152 | 153 | validate_cbor(&rule_def, &gen_value(7), &ctx).unwrap(); 154 | validate_cbor(&rule_def, &gen_value(8), &ctx).unwrap_err(); 155 | } 156 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `cddl-cat` is a library for validating encoded data against a CDDL 2 | //! document that describes the expected structure of the data. 3 | //! 4 | //! CDDL is a text document described by [RFC8610] that describes data 5 | //! structures. CDDL is not tied to any specific serialization or encoding 6 | //! method; it can be used to validate data that is in [CBOR] or JSON format. 7 | //! 8 | //! The goal of this library is to make CBOR or JSON data easy to validate 9 | //! against a CDDL schema description. 10 | //! 11 | //! `cddl-cat` supports Rust 1.81 and later. 12 | //! 13 | //! # Implementation Details 14 | //! 15 | //! - Supports CBOR and JSON encodings, controlled by the `ciborium` and 16 | //! `serde_json` features. 17 | //! 18 | //! - An "Intermediate Validation Tree" ([`ivt`]) is constructed 19 | //! from the CDDL AST; this removes some of the CDDL syntax detail resulting 20 | //! in a simplified tree that can be more easily validated. The IVT is 21 | //! constructed almost entirely of [`Node`](crate::ivt::Node) elements, 22 | //! allowing recursive validation. 23 | //! 24 | //! - Validation is performed by first translating the incoming data into 25 | //! a generic form, so most of the validation code is completely agnostic 26 | //! to the serialization format. 27 | //! 28 | //! - Validation code uses a [`LookupContext`](crate::context::LookupContext) object 29 | //! to perform all rule lookups. This will allow stacking CDDL documents or 30 | //! building CDDL libraries that can be used by other CDDL schemas. In the 31 | //! future the validation process itself may be customized by changing the 32 | //! `LookupContext` configuration. 33 | //! 34 | //! - Comprehensive tests (90%+ coverage). 35 | //! 36 | //! # Examples 37 | //! 38 | //! This example validates JSON-encoded data against a CDDL schema: 39 | //! 40 | //! ``` 41 | //! # #[cfg(feature = "serde_json")] 42 | //! use cddl_cat::validate_json_str; 43 | //! 44 | //! let cddl_input = "person = {name: tstr, age: int}"; 45 | //! let json_str = r#"{ "name": "Bob", "age": 43 }"#; 46 | //! 47 | //! # #[cfg(feature = "serde_json")] 48 | //! validate_json_str("person", cddl_input, &json_str).unwrap(); 49 | //! ``` 50 | //! 51 | //! If the JSON data doesn't have the expected structure, an error will 52 | //! result: 53 | //! ``` 54 | //! # #[cfg(feature = "serde_json")] 55 | //! use cddl_cat::validate_json_str; 56 | //! 57 | //! let cddl_input = "person = {name: tstr, age: int}"; 58 | //! let json_str = r#"{ "name": "Bob", "age": "forty three" }"#; 59 | //! 60 | //! # #[cfg(feature = "serde_json")] 61 | //! assert!(validate_json_str("person", cddl_input, &json_str).is_err()); 62 | //! ``` 63 | //! 64 | //! A similar example, verifying CBOR-encoded data against a CDDL schema: 65 | //! ``` 66 | //! # #[cfg(feature = "ciborium")] 67 | //! use cddl_cat::validate_cbor_bytes; 68 | //! use serde::Serialize; 69 | //! 70 | //! #[derive(Serialize)] 71 | //! struct PersonStruct { 72 | //! name: String, 73 | //! age: u32, 74 | //! } 75 | //! 76 | //! let input = PersonStruct { 77 | //! name: "Bob".to_string(), 78 | //! age: 43, 79 | //! }; 80 | //! # #[cfg(feature = "ciborium")] 81 | //! let mut cbor_bytes = Vec::new(); 82 | //! ciborium::into_writer(&input, &mut cbor_bytes).unwrap(); 83 | //! let cddl_input = "person = {name: tstr, age: int}"; 84 | //! # #[cfg(feature = "ciborium")] 85 | //! validate_cbor_bytes("person", cddl_input, &cbor_bytes).unwrap(); 86 | //! ``` 87 | //! Supported prelude types: 88 | //! - `any`, `uint`, `nint`, `int`, `bstr`, `bytes`, `tstr`, `text` 89 | //! - `float`, `float16`, `float32`, `float64`, `float16-32`, `float32-64` 90 | //! 91 | //! Note: float sizes are not validated. 92 | //! 93 | //! Supported CDDL features: 94 | //! - Basic prelude types (integers, floats, bool, nil, text strings, byte strings) 95 | //! - Literal int, float, bool, UTF-8 text strings 96 | //! - Byte strings in UTF-8, hex, or base64 97 | //! - Arrays and maps 98 | //! - Rule lookups by name 99 | //! - Groups 100 | //! - Choices (using `/` or `//` syntax) 101 | //! - Occurrences (`?`, `*`, `+`, or `m*n`) 102 | //! - Ranges (e.g. `1..7` or `1...8`) 103 | //! - Unwrapping (`~`) 104 | //! - Turn a group into a choice (`&`) 105 | //! - Map keys with cut syntax (`^ =>`) 106 | //! - Generic types 107 | //! - Control operators `.cbor`, `.size`, and `.regexp` 108 | //! 109 | //! Unimplemented CDDL features: 110 | //! - Extend type with `/=` 111 | //! - Extend group with `//=` 112 | //! - Type sockets with `$` 113 | //! - Group sockets with `$$` 114 | //! - Control operators other than those above (e.g. `.bits`, `.lt`, `.gt`...) 115 | //! - Group enumeration with `&` 116 | //! - Tagged data with `#` 117 | //! - Hexfloat literals (e.g. `0x1.921fb5p+1`) 118 | //! - Prelude types that invoke CBOR tags (e.g. `tdate` or `biguint`) 119 | //! 120 | //! [RFC8610]: https://tools.ietf.org/html/rfc8610 121 | //! [CBOR]: https://cbor.io/ 122 | 123 | #![warn(missing_docs)] 124 | #![forbid(unsafe_code)] 125 | #![warn(clippy::cast_possible_truncation)] 126 | 127 | pub mod ast; 128 | pub mod context; 129 | pub mod flatten; 130 | pub mod ivt; 131 | pub mod parser; 132 | pub mod util; 133 | #[doc(inline)] 134 | pub use util::{ValidateError, ValidateResult}; 135 | pub(crate) mod validate; 136 | pub mod value; 137 | 138 | #[cfg(feature = "ciborium")] 139 | pub mod cbor; 140 | #[cfg(feature = "ciborium")] 141 | #[doc(inline)] 142 | pub use cbor::{validate_cbor, validate_cbor_bytes}; 143 | 144 | #[cfg(feature = "serde_json")] 145 | pub mod json; 146 | #[cfg(feature = "serde_json")] 147 | #[doc(inline)] 148 | pub use json::{validate_json, validate_json_str}; 149 | 150 | #[doc(inline)] 151 | pub use parser::parse_cddl; 152 | -------------------------------------------------------------------------------- /src/cbor.rs: -------------------------------------------------------------------------------- 1 | //! This module implements validation from [`ciborium::Value`]. 2 | //! 3 | //! # Examples 4 | //! 5 | //! ``` 6 | //! use cddl_cat::validate_cbor_bytes; 7 | //! use serde::Serialize; 8 | //! 9 | //! #[derive(Serialize)] 10 | //! struct PersonStruct { 11 | //! name: String, 12 | //! age: u32, 13 | //! } 14 | //! 15 | //! let input = PersonStruct { 16 | //! name: "Bob".to_string(), 17 | //! age: 43, 18 | //! }; 19 | //! let mut cbor_bytes = Vec::new(); 20 | //! ciborium::into_writer(&input, &mut cbor_bytes).unwrap(); 21 | //! let cddl_input = "person = {name: tstr, age: int}"; 22 | //! 23 | //! validate_cbor_bytes("person", cddl_input, &cbor_bytes).unwrap(); 24 | //! ``` 25 | //! 26 | //! If the caller wants to reuse the parsed CDDL IVT, replace 27 | //! `validate_cbor_bytes(...)` with: 28 | //! ``` 29 | //! # use cddl_cat::validate_cbor_bytes; 30 | //! use cddl_cat::{cbor::validate_cbor, context::BasicContext, flatten::flatten_from_str}; 31 | //! # use serde::Serialize; 32 | //! # 33 | //! # #[derive(Serialize)] 34 | //! # struct PersonStruct { 35 | //! # name: String, 36 | //! # age: u32, 37 | //! # } 38 | //! # 39 | //! # let input = PersonStruct { 40 | //! # name: "Bob".to_string(), 41 | //! # age: 43, 42 | //! # }; 43 | //! # let mut cbor_bytes = Vec::new(); 44 | //! # ciborium::into_writer(&input, &mut cbor_bytes).unwrap(); 45 | //! # let cddl_input = "person = {name: tstr, age: int}"; 46 | //! 47 | //! // Parse the CDDL text and flatten it into IVT form. 48 | //! let flat_cddl = flatten_from_str(cddl_input).unwrap(); 49 | //! // Create a Context object to store the IVT 50 | //! let ctx = BasicContext::new(flat_cddl); 51 | //! // Look up the Rule we want to validate. 52 | //! let rule_def = &ctx.rules.get("person").unwrap(); 53 | //! // Deserialize the CBOR bytes 54 | //! let cbor_value = ciborium::from_reader(cbor_bytes.as_slice()).unwrap(); 55 | //! // Perform the validation. 56 | //! validate_cbor(&rule_def, &cbor_value, &ctx).unwrap(); 57 | //! ``` 58 | 59 | use crate::context::{BasicContext, LookupContext}; 60 | use crate::flatten::flatten_from_str; 61 | use crate::ivt::RuleDef; 62 | use crate::util::{ValidateError, ValidateResult}; 63 | use crate::validate::do_validate; 64 | use crate::value::Value; 65 | use ciborium::Value as CBOR_Value; 66 | use std::collections::BTreeMap; 67 | use std::convert::TryFrom; 68 | 69 | // These conversions seem obvious and pointless, but over time they may 70 | // diverge. However, CDDL and CBOR were designed to work with one another, so 71 | // it's not surprising that they map almost perfectly. 72 | 73 | impl TryFrom<&CBOR_Value> for Value { 74 | type Error = ValidateError; 75 | 76 | fn try_from(value: &CBOR_Value) -> Result { 77 | let result = match value { 78 | CBOR_Value::Null => Value::Null, 79 | CBOR_Value::Bool(b) => Value::Bool(*b), 80 | CBOR_Value::Integer(i) => Value::Integer((*i).into()), 81 | CBOR_Value::Float(f) => Value::from_float(*f), 82 | CBOR_Value::Bytes(b) => Value::Bytes(b.clone()), 83 | CBOR_Value::Text(t) => Value::Text(t.clone()), 84 | CBOR_Value::Array(a) => { 85 | let array: Result<_, _> = a.iter().map(Value::try_from).collect(); 86 | Value::Array(array?) 87 | } 88 | CBOR_Value::Map(m) => { 89 | type MapTree = BTreeMap; 90 | let map: Result = m 91 | .iter() 92 | .map(|(k, v)| { 93 | // An iterator returning a 2-tuple can be used as (key, value) 94 | // when building a new map. 95 | Ok((Value::try_from(k)?, Value::try_from(v)?)) 96 | }) 97 | .collect(); 98 | Value::Map(map?) 99 | } 100 | _ => { 101 | // cbor::Value has a few hidden internal variants. We should 102 | // never see them, but return an error if we do. 103 | return Err(ValidateError::ValueError( 104 | "can't handle hidden cbor Value".into(), 105 | )); 106 | } 107 | }; 108 | Ok(result) 109 | } 110 | } 111 | 112 | // A variant that consumes the CBOR Value. 113 | impl TryFrom for Value { 114 | type Error = ValidateError; 115 | 116 | fn try_from(value: CBOR_Value) -> Result { 117 | Value::try_from(&value) 118 | } 119 | } 120 | 121 | /// Validate already-parsed CBOR data against an already-parsed CDDL schema. 122 | pub fn validate_cbor( 123 | rule_def: &RuleDef, 124 | value: &CBOR_Value, 125 | ctx: &dyn LookupContext, 126 | ) -> ValidateResult { 127 | let value = Value::try_from(value)?; 128 | do_validate(&value, rule_def, ctx) 129 | } 130 | 131 | /// Validate CBOR-encoded data against a specified rule in a UTF-8 CDDL schema. 132 | pub fn validate_cbor_bytes(name: &str, cddl: &str, cbor: &[u8]) -> ValidateResult { 133 | // Parse the CDDL text and flatten it into IVT form. 134 | let flat_cddl = flatten_from_str(cddl)?; 135 | let ctx = BasicContext::new(flat_cddl); 136 | 137 | // Find the rule name that was requested 138 | let rule_def: &RuleDef = ctx 139 | .rules 140 | .get(name) 141 | .ok_or_else(|| ValidateError::MissingRule(name.into()))?; 142 | 143 | // Deserialize the CBOR bytes 144 | let cbor_value: CBOR_Value = 145 | ciborium::from_reader(cbor).map_err(|e| ValidateError::ValueError(format!("{}", e)))?; 146 | 147 | // Convert the CBOR tree into a Value tree for validation 148 | let value = Value::try_from(cbor_value)?; 149 | do_validate(&value, rule_def, &ctx) 150 | } 151 | -------------------------------------------------------------------------------- /src/ast.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the Abstract Syntax Tree. 2 | //! 3 | //! It contains a tree representation of CDDL rules, closely matching the 4 | //! syntax used in the original CDDL text. 5 | //! 6 | 7 | /// A literal value, i.e. `"foo"`, `1.0`, or `h'FFF7'` 8 | /// 9 | /// CDDL ABNF grammar: 10 | /// ```text 11 | /// value = number / text / bytes 12 | /// ``` 13 | #[derive(Debug, PartialEq)] 14 | pub enum Value { 15 | /// A text-string literal. 16 | Text(String), 17 | /// An unsigned integer literal. 18 | Uint(u64), 19 | /// A negative integer literal. 20 | Nint(i64), 21 | /// A floating-point literal. 22 | Float(f64), 23 | /// A byte-tring literal. 24 | Bytes(Vec), 25 | } 26 | 27 | /// The "key" part of a key-value group member. 28 | #[derive(Debug, PartialEq)] 29 | #[allow(missing_docs)] 30 | pub enum MemberKeyVal { 31 | /// Any type specified with the `=>` separator. 32 | Type1(Type1), 33 | /// A type name. 34 | Bareword(String), 35 | /// A literal value. 36 | Value(Value), 37 | } 38 | 39 | type IsCut = bool; 40 | 41 | /// The "key" part of a key-value group member, along with its "cut" semantics. 42 | /// 43 | /// When validating a map, "cut" means that once the key has matched, no other 44 | /// keys in this CDDL group will be attempted. See RFC8610 for more details. 45 | /// 46 | #[derive(Debug, PartialEq)] 47 | pub struct MemberKey { 48 | /// The actual key definition. 49 | pub val: MemberKeyVal, 50 | /// `true` if cut semantics are specified. 51 | pub cut: IsCut, 52 | } 53 | 54 | /// A group member, typically one element of an array or map. 55 | #[derive(Debug, PartialEq)] 56 | #[allow(missing_docs)] 57 | pub struct Member { 58 | pub key: Option, 59 | pub value: Type, 60 | } 61 | 62 | /// An "occurrence" which specifies how many elements can match a group member. 63 | #[allow(missing_docs)] 64 | #[derive(Debug, PartialEq, Eq, Clone)] 65 | pub enum Occur { 66 | Optional, 67 | ZeroOrMore, 68 | OneOrMore, 69 | Numbered(usize, usize), 70 | } 71 | 72 | /// The part of a "group entry" after the occurrence. 73 | #[derive(Debug, PartialEq)] 74 | #[allow(missing_docs)] 75 | pub enum GrpEntVal { 76 | Member(Member), 77 | Groupname(String), 78 | Parenthesized(Group), 79 | } 80 | 81 | /// A group entry contains one element of a group. 82 | /// 83 | /// Each key-value pair in map, each element of an array, or each group 84 | /// (inline or referenced by name) will be stored in a `GrpEnt`. 85 | /// 86 | /// CDDL ABNF grammar: 87 | /// ```text 88 | /// grpent = [occur S] [memberkey S] type 89 | /// / [occur S] groupname [genericarg] ; preempted by above 90 | /// / [occur S] "(" S group S ")" 91 | /// ``` 92 | #[derive(Debug, PartialEq)] 93 | #[allow(missing_docs)] 94 | pub struct GrpEnt { 95 | pub occur: Option, 96 | pub val: GrpEntVal, 97 | } 98 | 99 | /// A group choice contains one of the choices making up a group. 100 | /// 101 | /// Each group choice is itself made up of individual group entries. 102 | /// 103 | /// CDDL ABNF grammar: 104 | /// ```text 105 | /// grpchoice = *(grpent optcom) 106 | /// ``` 107 | /// Translated: "zero-or-more group-entries separated by an optional comma" 108 | #[derive(Debug, PartialEq)] 109 | pub struct GrpChoice(pub Vec); 110 | 111 | /// A group contains a number of elements. 112 | /// 113 | /// Each group is itself made up of group choices, only one of which needs to 114 | /// match. 115 | /// 116 | /// CDDL ABNF grammar: 117 | /// ```text 118 | /// group = grpchoice *(S "//" S grpchoice) 119 | /// ``` 120 | #[derive(Debug, PartialEq)] 121 | pub struct Group(pub Vec); 122 | 123 | /// A name identifier with generic arguments. 124 | /// 125 | /// A name may have generic arguments, e.g. `message`. 126 | /// A name without generic arguments will have an empty generic_args Vec. 127 | /// 128 | /// CDDL ABNF grammar: 129 | /// ```text 130 | /// genericarg = "<" S type1 S *("," S type1 S ) ">" 131 | /// type2 = ... 132 | /// / typename [genericarg] 133 | /// / "~" S typename [genericarg] 134 | /// / "&" S groupname [genericarg] 135 | /// / ... 136 | /// ``` 137 | #[derive(Debug, PartialEq)] 138 | pub struct NameGeneric { 139 | /// A type or group name. 140 | pub name: String, 141 | /// Generic arguments, if any. 142 | pub generic_args: Vec, 143 | } 144 | 145 | /// Type2 is the main representation of a CDDL type. 146 | /// 147 | /// Note: not all type2 syntax is implemented. 148 | /// Types starting with `&`, `#` are not yet supported. 149 | /// 150 | /// CDDL ABNF grammar: 151 | /// ```text 152 | /// type2 = value 153 | /// / typename [genericarg] 154 | /// / "(" S type S ")" 155 | /// / "{" S group S "}" 156 | /// / "[" S group S "]" 157 | /// / "~" S typename [genericarg] 158 | /// / "&" S "(" S group S ")" 159 | /// / "&" S groupname [genericarg] 160 | /// / "#" "6" ["." uint] "(" S type S ")" 161 | /// / "#" DIGIT ["." uint] 162 | /// / "#" 163 | /// ``` 164 | #[derive(Debug, PartialEq)] 165 | #[allow(missing_docs)] 166 | pub enum Type2 { 167 | Value(Value), 168 | Typename(NameGeneric), 169 | Parethesized(Type), 170 | Map(Group), 171 | Array(Group), 172 | Unwrap(NameGeneric), 173 | ChoiceifyInline(Group), 174 | Choiceify(NameGeneric), 175 | } 176 | 177 | /// A CDDL type, with an additional range or control operator. 178 | /// 179 | /// CDDL ABNF grammar: 180 | /// ```text 181 | /// type1 = type2 [S (rangeop / ctlop) S type2] 182 | /// ``` 183 | #[derive(Debug, PartialEq)] 184 | pub enum Type1 { 185 | /// A `Type1` containing only a `Type2` with no operators 186 | Simple(Type2), 187 | /// A range (e.g. `1..10`) 188 | Range(TypeRange), 189 | /// A type with a control operator attached (e.g. `bstr .size 32`) 190 | Control(TypeControl), 191 | } 192 | 193 | /// A CDDL type, specified with a range operator. 194 | /// 195 | /// Range operators are `..` (inclusive range) and `...` (exclusive range). 196 | /// CDDL only allows the range operators on pairs of integers or floats. 197 | #[derive(Debug, PartialEq)] 198 | #[allow(missing_docs)] 199 | pub struct TypeRange { 200 | pub start: Type2, 201 | pub end: Type2, 202 | pub inclusive: bool, 203 | } 204 | 205 | /// A CDDL type, specified with a control operator. 206 | /// 207 | /// Control operators can express a range of possibilities, including 208 | /// `.size N` (limit size of a value in bytes) or `.regexp` (requiring a text 209 | /// string to match the given regular expression). 210 | #[derive(Debug, PartialEq)] 211 | #[allow(missing_docs)] 212 | pub struct TypeControl { 213 | pub target: Type2, 214 | pub arg: Type2, 215 | pub op: String, 216 | } 217 | 218 | /// A CDDL type, with choices. 219 | /// 220 | /// CDDL ABNF grammar: 221 | /// ```text 222 | /// type = type1 *(S "/" S type1) 223 | /// ``` 224 | #[derive(Debug, PartialEq)] 225 | pub struct Type(pub Vec); 226 | 227 | /// A CDDL data structure specification 228 | /// 229 | /// Each CDDL rule has a name and a syntax tree. Rules can be 230 | /// referenced by name by other rules, or even within the same rule. 231 | /// 232 | /// Note: `genericparm` is not yet supported. 233 | /// Note: "extend" assignment operators (`/=`,`//=`) are not yet supported. 234 | /// 235 | /// CDDL ABNF grammar: 236 | /// ```text 237 | /// rule = typename [genericparm] S assignt S type 238 | /// / groupname [genericparm] S assigng S grpent 239 | /// ``` 240 | #[derive(Debug, PartialEq)] 241 | pub struct Rule { 242 | /// The rule name. 243 | pub name: String, 244 | /// Generic parameters. 245 | pub generic_parms: Vec, 246 | /// The rule syntax tree. 247 | pub val: RuleVal, 248 | } 249 | 250 | /// A rule's syntax tree, in either [`Type`] or [`GrpEnt`] form. 251 | /// 252 | /// Note: `genericparm` is not yet supported. 253 | /// Note: "extend" assignment operators (`/=`,`//=`) are not yet supported. 254 | /// 255 | /// CDDL ABNF grammar: 256 | /// ```text 257 | /// rule = typename [genericparm] S assignt S type 258 | /// / groupname [genericparm] S assigng S grpent 259 | /// ``` 260 | #[derive(Debug, PartialEq)] 261 | pub enum RuleVal { 262 | /// A type assignment rule. 263 | AssignType(Type), 264 | /// A group assignment rule. 265 | AssignGroup(GrpEnt), 266 | // TODO: /= and //= operators --> ExtendType(Type), ExtendGroup(GrpEnt) 267 | } 268 | 269 | /// A CDDL specification, containing multiple rule syntax trees. 270 | /// 271 | /// This is the output from the parser for a given CDDL text input. 272 | /// 273 | #[derive(Debug, PartialEq)] 274 | pub struct Cddl { 275 | /// Rules and their syntax trees. 276 | pub rules: Vec, 277 | } 278 | 279 | /// A CDDL specification, containing multiple rule syntax trees. 280 | /// 281 | /// This is the output from the parser for a given CDDL text input. 282 | /// CddlSlice is exactly the same as [`Cddl`] except that it also 283 | /// preserves a copy of the string used to compose that rule. 284 | /// 285 | #[derive(Debug, PartialEq)] 286 | pub struct CddlSlice { 287 | /// Rules and their syntax trees. 288 | pub rules: Vec<(Rule, String)>, 289 | } 290 | -------------------------------------------------------------------------------- /.Cargo.lock.msrv: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.19" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "base64" 16 | version = "0.22.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 19 | 20 | [[package]] 21 | name = "cddl-cat" 22 | version = "0.6.2" 23 | dependencies = [ 24 | "base64", 25 | "ciborium", 26 | "escape8259", 27 | "float-ord", 28 | "hex", 29 | "nom", 30 | "ntest", 31 | "regex", 32 | "serde", 33 | "serde_json", 34 | "strum_macros", 35 | "thiserror", 36 | ] 37 | 38 | [[package]] 39 | name = "cfg-if" 40 | version = "1.0.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 43 | 44 | [[package]] 45 | name = "ciborium" 46 | version = "0.2.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 49 | dependencies = [ 50 | "ciborium-io", 51 | "ciborium-ll", 52 | "serde", 53 | ] 54 | 55 | [[package]] 56 | name = "ciborium-io" 57 | version = "0.2.2" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 60 | 61 | [[package]] 62 | name = "ciborium-ll" 63 | version = "0.2.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 66 | dependencies = [ 67 | "ciborium-io", 68 | "half", 69 | ] 70 | 71 | [[package]] 72 | name = "crunchy" 73 | version = "0.2.4" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 76 | 77 | [[package]] 78 | name = "escape8259" 79 | version = "0.5.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "ba4f4911e3666fcd7826997b4745c8224295a6f3072f1418c3067b97a67557ee" 82 | dependencies = [ 83 | "rustversion", 84 | ] 85 | 86 | [[package]] 87 | name = "float-ord" 88 | version = "0.3.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" 91 | 92 | [[package]] 93 | name = "half" 94 | version = "2.6.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 97 | dependencies = [ 98 | "cfg-if", 99 | "crunchy", 100 | ] 101 | 102 | [[package]] 103 | name = "heck" 104 | version = "0.3.3" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 107 | dependencies = [ 108 | "unicode-segmentation", 109 | ] 110 | 111 | [[package]] 112 | name = "hex" 113 | version = "0.4.3" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 116 | 117 | [[package]] 118 | name = "itoa" 119 | version = "1.0.4" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" 122 | 123 | [[package]] 124 | name = "memchr" 125 | version = "2.5.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 128 | 129 | [[package]] 130 | name = "minimal-lexical" 131 | version = "0.2.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 134 | 135 | [[package]] 136 | name = "nom" 137 | version = "7.1.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 140 | dependencies = [ 141 | "memchr", 142 | "minimal-lexical", 143 | ] 144 | 145 | [[package]] 146 | name = "ntest" 147 | version = "0.7.5" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "5c544e496c816f0a59645c0bb69097e453df203954ae2ed4b3ac4251fad69d44" 150 | dependencies = [ 151 | "ntest_proc_macro_helper", 152 | "ntest_test_cases", 153 | "ntest_timeout", 154 | ] 155 | 156 | [[package]] 157 | name = "ntest_proc_macro_helper" 158 | version = "0.7.5" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "8f52e34b414605b77efc95c3f0ecef01df0c324bcc7f68d9a9cb7a7552777e52" 161 | 162 | [[package]] 163 | name = "ntest_test_cases" 164 | version = "0.7.5" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "99a81eb400abc87063f829560bc5c5c835177703b83d1cd991960db0b2a00abe" 167 | dependencies = [ 168 | "proc-macro2", 169 | "quote", 170 | "syn", 171 | ] 172 | 173 | [[package]] 174 | name = "ntest_timeout" 175 | version = "0.7.5" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "b10db009e117aca57cbfb70ac332348f9a89d09ff7204497c283c0f7a0c96323" 178 | dependencies = [ 179 | "ntest_proc_macro_helper", 180 | "proc-macro-crate", 181 | "proc-macro2", 182 | "quote", 183 | "syn", 184 | ] 185 | 186 | [[package]] 187 | name = "once_cell" 188 | version = "1.14.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 191 | 192 | [[package]] 193 | name = "proc-macro-crate" 194 | version = "1.2.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" 197 | dependencies = [ 198 | "once_cell", 199 | "thiserror", 200 | "toml", 201 | ] 202 | 203 | [[package]] 204 | name = "proc-macro2" 205 | version = "1.0.103" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 208 | dependencies = [ 209 | "unicode-ident", 210 | ] 211 | 212 | [[package]] 213 | name = "quote" 214 | version = "1.0.42" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 217 | dependencies = [ 218 | "proc-macro2", 219 | ] 220 | 221 | [[package]] 222 | name = "regex" 223 | version = "1.6.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 226 | dependencies = [ 227 | "aho-corasick", 228 | "memchr", 229 | "regex-syntax", 230 | ] 231 | 232 | [[package]] 233 | name = "regex-syntax" 234 | version = "0.6.27" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 237 | 238 | [[package]] 239 | name = "rustversion" 240 | version = "1.0.9" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" 243 | 244 | [[package]] 245 | name = "ryu" 246 | version = "1.0.11" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 249 | 250 | [[package]] 251 | name = "serde" 252 | version = "1.0.147" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" 255 | dependencies = [ 256 | "serde_derive", 257 | ] 258 | 259 | [[package]] 260 | name = "serde_derive" 261 | version = "1.0.147" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" 264 | dependencies = [ 265 | "proc-macro2", 266 | "quote", 267 | "syn", 268 | ] 269 | 270 | [[package]] 271 | name = "serde_json" 272 | version = "1.0.87" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" 275 | dependencies = [ 276 | "itoa", 277 | "ryu", 278 | "serde", 279 | ] 280 | 281 | [[package]] 282 | name = "strum_macros" 283 | version = "0.23.1" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" 286 | dependencies = [ 287 | "heck", 288 | "proc-macro2", 289 | "quote", 290 | "rustversion", 291 | "syn", 292 | ] 293 | 294 | [[package]] 295 | name = "syn" 296 | version = "1.0.103" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" 299 | dependencies = [ 300 | "proc-macro2", 301 | "quote", 302 | "unicode-ident", 303 | ] 304 | 305 | [[package]] 306 | name = "thiserror" 307 | version = "1.0.37" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 310 | dependencies = [ 311 | "thiserror-impl", 312 | ] 313 | 314 | [[package]] 315 | name = "thiserror-impl" 316 | version = "1.0.37" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 319 | dependencies = [ 320 | "proc-macro2", 321 | "quote", 322 | "syn", 323 | ] 324 | 325 | [[package]] 326 | name = "toml" 327 | version = "0.5.9" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 330 | dependencies = [ 331 | "serde", 332 | ] 333 | 334 | [[package]] 335 | name = "unicode-ident" 336 | version = "1.0.5" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 339 | 340 | [[package]] 341 | name = "unicode-segmentation" 342 | version = "1.12.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 345 | -------------------------------------------------------------------------------- /src/ivt.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the Intermediate Validation Tree. 2 | //! 3 | //! It contains a simplified representation of a CDDL rule, flattened to only 4 | //! include the parts that are necessary for validation. 5 | //! 6 | //! This module doesn't know anything about validating specific types (e.g. 7 | //! CBOR or JSON), but it helps make writing those validators easier. 8 | 9 | use crate::ast; 10 | use std::collections::BTreeMap; 11 | use std::fmt; 12 | use strum_macros::{Display, IntoStaticStr}; 13 | 14 | /// The definition of a CDDL rule. 15 | /// 16 | /// Each rule has a name, some (optional) generic parameters, and a 17 | /// definition `Node`. 18 | #[derive(Clone, Debug, PartialEq)] 19 | pub struct RuleDef { 20 | /// Optional generic parameters. 21 | pub generic_parms: Vec, 22 | /// The Node representing the rule definition. 23 | pub node: Node, 24 | } 25 | 26 | /// A set of CDDL rules. 27 | /// 28 | /// Each rule has a name, some (optional) generic parameters, and a 29 | /// definition `Node`. 30 | pub type RulesByName = BTreeMap; 31 | 32 | /// A set of CDDL rules. 33 | /// 34 | /// Each rule has a name, some (optional) generic parameters, and a 35 | /// definition `Node`. 36 | /// 37 | /// `RulesWithStrings` is exactly like `RulesByName`, except that it 38 | /// preserves the original CDDL text for the rule, to assist in debugging. 39 | pub type RulesWithStrings = BTreeMap; 40 | 41 | /// One of the types named in the CDDL prelude. 42 | /// 43 | /// The following types are defined in [RFC8610 appendix D]: 44 | /// `any`, `bool`, `int`, `uint`, `float`, `tstr`, `bstr`. 45 | /// There are more that aren't supported by this crate yet. 46 | /// 47 | /// [RFC8610 appendix D]: https://tools.ietf.org/html/rfc8610#appendix-D 48 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Display)] 49 | #[allow(missing_docs)] 50 | pub enum PreludeType { 51 | /// Any type or embedded data structure 52 | Any, 53 | /// Nil aka null: nothing. 54 | Nil, 55 | /// A boolean value: true or false 56 | Bool, 57 | /// A positive or negative integer 58 | Int, 59 | /// An integer >= 0 60 | Uint, 61 | /// An integer < 0 62 | Nint, 63 | /// A floating-point value 64 | Float, 65 | /// A text string 66 | Tstr, 67 | /// A byte string 68 | Bstr, 69 | } 70 | 71 | /// A literal value, e.g. `7`, `1.3`, or ``"foo"``. 72 | #[derive(Debug, Clone, PartialEq)] 73 | #[allow(missing_docs)] 74 | pub enum Literal { 75 | Bool(bool), 76 | Int(i128), 77 | Float(f64), 78 | Text(String), 79 | Bytes(Vec), 80 | // TODO: nil? 81 | } 82 | 83 | impl fmt::Display for Literal { 84 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 85 | match self { 86 | Literal::Bool(b) => write!(f, "{}", b), 87 | Literal::Int(i) => write!(f, "{}", i), 88 | // FIXME: it's annoying that floating point values can omit the 89 | // decimal, which can be confused for an integer. 90 | Literal::Float(fl) => write!(f, "{}", fl), 91 | Literal::Text(s) => write!(f, "\"{}\"", s), 92 | Literal::Bytes(_) => write!(f, "LiteralBytes"), 93 | } 94 | } 95 | } 96 | 97 | /// A shortcut for `Node::Literal(Literal::Bool(b))` 98 | pub fn literal_bool(b: bool) -> Node { 99 | Node::Literal(Literal::Bool(b)) 100 | } 101 | 102 | /// A shortcut for `Node::Literal(Literal::Int(i))` 103 | /// 104 | /// This doesn't work for isize and usize, unfortunately. 105 | pub fn literal_int>(i: T) -> Node { 106 | Node::Literal(Literal::Int(i.into())) 107 | } 108 | 109 | /// A shortcut for `Node::Literal(Literal::Float(f))` 110 | pub fn literal_float>(f: T) -> Node { 111 | Node::Literal(Literal::Float(f.into())) 112 | } 113 | 114 | /// A shortcut for `Node::Literal(Literal::Text(t))` 115 | pub fn literal_text>(s: T) -> Node { 116 | Node::Literal(Literal::Text(s.into())) 117 | } 118 | 119 | /// A shortcut for `Node::Literal(Literal::Bytes(b))` 120 | pub fn literal_bytes>>(b: T) -> Node { 121 | Node::Literal(Literal::Bytes(b.into())) 122 | } 123 | 124 | /// A rule reference, linked to a dispatch object for later resolution. 125 | /// 126 | /// Resolving the rule reference is handled by the validation context. 127 | #[derive(Debug, Clone, PartialEq)] 128 | #[allow(missing_docs)] 129 | pub struct Rule { 130 | pub name: String, 131 | pub generic_args: Vec, 132 | } 133 | 134 | impl Rule { 135 | // Create a new rule reference by name 136 | #[doc(hidden)] // Only pub for integration tests 137 | pub fn new(name: &str, generic_args: Vec) -> Rule { 138 | Rule { 139 | name: name.to_string(), 140 | generic_args, 141 | } 142 | } 143 | 144 | #[doc(hidden)] // Only pub for integration tests 145 | pub fn new_name(name: &str) -> Rule { 146 | Rule { 147 | name: name.to_string(), 148 | generic_args: Vec::new(), 149 | } 150 | } 151 | } 152 | 153 | /// A Choice validates if any one of a set of options validates. 154 | #[derive(Debug, Clone, PartialEq)] 155 | #[allow(missing_docs)] 156 | pub struct Choice { 157 | pub options: Vec, 158 | } 159 | 160 | /// A key-value pair; key and value can be anything (types, arrays, maps, etc.) 161 | /// 162 | /// "Cut" means a match on this key will prevent any later keys from matching. 163 | /// 164 | /// For example, a map containing `"optional-key": "hello"` 165 | /// would be permitted to match the following: 166 | /// 167 | /// ```text 168 | /// extensible-map-example = { 169 | /// ? "optional-key" => int, 170 | /// * tstr => any 171 | /// } 172 | /// ``` 173 | /// If we add the cut symbol `^`, the same map would not match: 174 | /// 175 | /// ```text 176 | /// extensible-map-example = { 177 | /// ? "optional-key" ^ => int, 178 | /// * tstr => any 179 | /// } 180 | /// ``` 181 | /// 182 | /// Note: CDDL map members that use `:` always use cut semantics. 183 | /// 184 | /// See RFC8610 3.5.4 for more discussion. 185 | /// 186 | #[derive(Clone, PartialEq)] 187 | #[allow(missing_docs)] 188 | pub struct KeyValue { 189 | pub key: Box, 190 | pub value: Box, 191 | pub cut: IsCut, 192 | } 193 | 194 | pub(crate) type IsCut = bool; 195 | 196 | impl KeyValue { 197 | #[doc(hidden)] // Only pub for integration tests 198 | pub fn new(key: Node, value: Node, cut: IsCut) -> KeyValue { 199 | KeyValue { 200 | key: Box::new(key), 201 | value: Box::new(value), 202 | cut, 203 | } 204 | } 205 | } 206 | 207 | impl fmt::Display for KeyValue { 208 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 209 | write!(f, "{}: {}", self.key, self.value) 210 | } 211 | } 212 | 213 | // Implement Debug by hand so we can format it like a map. 214 | impl fmt::Debug for KeyValue { 215 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 216 | let mut formatter = f.debug_tuple("KeyValue"); 217 | formatter.field(&self.key).field(&self.value).finish() 218 | } 219 | } 220 | 221 | /// Specify a CDDL occurrence's limits. 222 | /// 223 | /// An "occurrence" in CDDL specifies how many times a value should repeat 224 | /// (in an array) or flag optional map keys. [RFC8610] specifies the following 225 | /// occurrence symbols: 226 | /// ```text 227 | /// "?" Optional 228 | /// "*" Zero or more 229 | /// "+" One or more 230 | /// n*m Between n and m, inclusive (n and m are both optional) 231 | /// ``` 232 | /// 233 | /// [RFC8610]: https://tools.ietf.org/html/rfc8610 234 | /// 235 | // Re-use the Occur limit type that the parser uses 236 | pub type OccurLimit = ast::Occur; 237 | 238 | /// Occurences specify how many times a value can appear. 239 | /// 240 | /// This implementation wraps the Node that the occurrence applies to. 241 | 242 | #[derive(Debug, Clone, PartialEq)] 243 | #[allow(missing_docs)] 244 | pub struct Occur { 245 | pub limit: OccurLimit, 246 | pub node: Box, 247 | } 248 | 249 | impl Occur { 250 | /// Creates a new Occur from one of the CDDL occurrence chars ?*+ 251 | pub fn new(limit: OccurLimit, node: Node) -> Occur { 252 | Occur { 253 | limit, 254 | node: Box::new(node), 255 | } 256 | } 257 | 258 | /// Get the CDDL symbol for this occurrence. 259 | /// 260 | /// Returns `?`, `*`, `+`, or `n*m` 261 | pub fn symbol(&self) -> String { 262 | match self.limit { 263 | OccurLimit::Optional => "?".into(), 264 | OccurLimit::ZeroOrMore => "*".into(), 265 | OccurLimit::OneOrMore => "+".into(), 266 | OccurLimit::Numbered(n, m) => match (n, m) { 267 | (0, std::usize::MAX) => format!("{}*", n), 268 | (_, _) => format!("{}*{}", n, m), 269 | }, 270 | } 271 | } 272 | 273 | /// Return the lower and upper limits on this occurrence 274 | /// 275 | /// Occurrences can always be represented by an inclusive [lower, upper] 276 | /// count limit. 277 | /// 278 | /// required => [1, 1] 279 | /// optional "?" => [0, 1] 280 | /// zero-or-more "*" => [0, MAX] 281 | /// one-or-more "+" => [1, MAX] 282 | pub fn limits(&self) -> (usize, usize) { 283 | match self.limit { 284 | OccurLimit::Optional => (0, 1), 285 | OccurLimit::ZeroOrMore => (0, usize::MAX), 286 | OccurLimit::OneOrMore => (1, usize::MAX), 287 | OccurLimit::Numbered(n, m) => (n, m), 288 | } 289 | } 290 | } 291 | 292 | impl fmt::Display for Occur { 293 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 294 | write!(f, "{} {}", self.symbol(), self.node) 295 | } 296 | } 297 | 298 | /// A map containing key-value pairs. 299 | #[derive(Debug, Clone, PartialEq)] 300 | #[allow(missing_docs)] 301 | pub struct Map { 302 | pub members: Vec, 303 | } 304 | 305 | /// A context-free group of key-value pairs. 306 | #[derive(Debug, Clone, PartialEq)] 307 | #[allow(missing_docs)] 308 | pub struct Group { 309 | pub members: Vec, 310 | } 311 | 312 | /// An array is a list of types in a specific order. 313 | /// 314 | /// Arrays are expected to take the form of "records" or "vectors". 315 | /// 316 | /// A "vector" array is expected to have an arbitrary-length list of a single 317 | /// type, e.g. zero-or-more integers: 318 | /// ```text 319 | /// [ * int ] 320 | /// ``` 321 | /// The type in a vector could be something complex, like a group, choice, or 322 | /// another array or map. 323 | /// 324 | /// A "record" array is a sequence of different values, each with a specific 325 | /// type. It has similar semantics to a rust tuple, though it could also 326 | /// theoretically be used to serialize a struct. 327 | /// 328 | /// CDDL syntax allows certain nonsensical or ambiguous arrays, for example: 329 | /// ```text 330 | /// thing = [ * mygroup ] 331 | /// mygroup = ( a = tstr, b = int) 332 | /// ``` 333 | /// or 334 | /// ```text 335 | /// thing = [ * "a" = int, * "b" = int ] 336 | /// ``` 337 | /// 338 | /// CDDL arrays may be composed of key-value pairs, but the keys are solely 339 | /// for information/debugging; they are ignored for validation purposes. 340 | /// 341 | #[derive(Debug, Clone, PartialEq)] 342 | #[allow(missing_docs)] 343 | pub struct Array { 344 | pub members: Vec, 345 | } 346 | 347 | /// A range of numbers. 348 | /// 349 | /// Ranges can be defined as inclusive (`..`) or exclusive (`...`). 350 | /// 351 | /// CDDL only defines ranges between two integers or between two floating 352 | /// point values. A lower bound that exceeds the upper bound is valid CDDL, 353 | /// but behaves as an empty set. 354 | #[derive(Debug, Clone, PartialEq)] 355 | #[allow(missing_docs)] 356 | pub struct Range { 357 | pub start: Box, 358 | pub end: Box, 359 | pub inclusive: bool, 360 | } 361 | 362 | impl fmt::Display for Range { 363 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 364 | let op = if self.inclusive { ".." } else { "..." }; 365 | write!(f, "{}{}{}", self.start, op, self.end) 366 | } 367 | } 368 | 369 | /// Control Operators 370 | /// 371 | /// A control operator constrains a type by adding an additional condition 372 | /// that must be met. For example, "tstr .size 10" permits only strings of 373 | /// 10 bytes or less. See RFC 8610 section 3.8 for details. 374 | #[non_exhaustive] 375 | #[derive(Debug, Clone, PartialEq)] 376 | pub enum Control { 377 | /// Limit the size in bytes. 378 | Size(CtlOpSize), 379 | /// Apply a regular expression to a text string. 380 | Regexp(CtlOpRegexp), 381 | /// Validate a nested CBOR bytestring 382 | Cbor(CtlOpCbor), 383 | } 384 | 385 | /// Control Operator `.size` 386 | /// 387 | /// `.size` is defined in RFC 8610 3.8.1. 388 | /// It sets an upper limit, measured in bytes. 389 | /// 390 | /// For example, "tstr .size 10" permits only strings of 391 | /// 10 bytes or less. See RFC 8610 section 3.8 for details. 392 | #[derive(Debug, Clone, PartialEq)] 393 | pub struct CtlOpSize { 394 | /// The type that is size-constrained. 395 | /// 396 | /// Only certain types are permitted. RFC 8610 defines `.size` for 397 | /// `tstr`, `bstr`, and unsigned integers. 398 | pub target: Box, 399 | /// The size limit, in bytes. 400 | pub size: Box, 401 | } 402 | 403 | /// Control Operator `.regexp` 404 | /// 405 | /// `.regexp` is defined in RFC 8610 3.8.3. 406 | /// 407 | #[derive(Debug, Clone)] 408 | pub struct CtlOpRegexp { 409 | /// The regular expression, in compiled form. 410 | pub(crate) re: regex::Regex, 411 | } 412 | 413 | impl PartialEq for CtlOpRegexp { 414 | fn eq(&self, other: &Self) -> bool { 415 | // We only need to compare the string form, 416 | // not the compiled form. 417 | self.re.as_str() == other.re.as_str() 418 | } 419 | } 420 | 421 | /// Control Operator `.cbor` 422 | /// 423 | /// `.cbor` is defined in RFC 8610 3.8.4 424 | /// 425 | /// A ".cbor" control on a byte string indicates that the byte string 426 | /// carries a CBOR-encoded data item. Decoded, the data item matches the 427 | /// type given as the right-hand-side argument. 428 | #[derive(Debug, Clone, PartialEq)] 429 | pub struct CtlOpCbor { 430 | /// The nested node to satisfy 431 | pub(crate) node: Box, 432 | } 433 | 434 | /// Any node in the Intermediate Validation Tree. 435 | #[derive(Debug, Clone, PartialEq, IntoStaticStr)] 436 | #[allow(missing_docs)] 437 | pub enum Node { 438 | Literal(Literal), 439 | PreludeType(PreludeType), 440 | Rule(Rule), 441 | Choice(Choice), 442 | Map(Map), 443 | Array(Array), 444 | Group(Group), 445 | KeyValue(KeyValue), 446 | Occur(Occur), 447 | Unwrap(Rule), 448 | Range(Range), 449 | Control(Control), 450 | Choiceify(Rule), 451 | ChoiceifyInline(Array), 452 | } 453 | 454 | impl fmt::Display for Node { 455 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 456 | match self { 457 | Node::Literal(l) => write!(f, "{}", l), 458 | Node::PreludeType(p) => write!(f, "{}", p), 459 | Node::KeyValue(kv) => write!(f, "{}", kv), 460 | _ => { 461 | let variant: &str = self.into(); 462 | write!(f, "{}", variant) 463 | } 464 | } 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /src/flatten.rs: -------------------------------------------------------------------------------- 1 | //! Tools for converting the [`ast`] (syntax tree) into an [`ivt`] 2 | //! (intermediate validation tree). 3 | //! 4 | //! This module is called "flatten" because its goal is to flatten syntax 5 | //! tree detail that's not useful for validation. 6 | //! 7 | //! [`ivt`]: crate::ivt 8 | //! [`ast`]: crate::ast 9 | 10 | use regex::RegexBuilder; 11 | 12 | use crate::ast; 13 | use crate::ivt::*; 14 | use crate::parser::{parse_cddl, slice_parse_cddl}; 15 | use crate::util::ValidateError; 16 | use std::convert::TryInto; 17 | 18 | /// The result of a flatten operation. 19 | pub type FlattenResult = std::result::Result; 20 | 21 | /// Convert a CDDL schema in UTF-8 form into a structured rule set. 22 | pub fn flatten_from_str(cddl_input: &str) -> FlattenResult { 23 | let cddl = parse_cddl(cddl_input)?; 24 | flatten(&cddl) 25 | } 26 | 27 | /// Convert a CDDL schema in UTF-8 form into a structured rule set, preserving the CDDL text. 28 | /// 29 | /// This works the same as `flatten_from_str`, but preserves a copy of the original 30 | /// CDDL text alongside the IVT. 31 | pub fn slice_flatten_from_str(cddl_input: &str) -> FlattenResult { 32 | let cddl = slice_parse_cddl(cddl_input)?; 33 | slice_flatten(&cddl) 34 | } 35 | 36 | /// Convert an already-parsed cddl AST into a `(name, rules)` map. 37 | pub fn flatten(cddl: &ast::Cddl) -> FlattenResult { 38 | // This first pass generates a tree of Nodes from the AST. 39 | cddl.rules.iter().map(flatten_rule).collect() 40 | } 41 | 42 | /// Convert an already-parsed cddl AST into a `(name, (rule, rule-string))` map. 43 | /// 44 | /// This works the same as `flatten`, but preserves a copy of the original 45 | /// CDDL text alongside the IVT. 46 | pub fn slice_flatten(cddl: &ast::CddlSlice) -> FlattenResult { 47 | // This first pass generates a tree of Nodes from the AST. 48 | cddl.rules 49 | .iter() 50 | .map(|(rule, s)| { 51 | let (name, flat) = flatten_rule(rule)?; 52 | // key = name, value = (Node, copy of cddl text slice) 53 | Ok((name, (flat, s.clone()))) 54 | }) 55 | .collect() 56 | } 57 | 58 | /// flatten an ast::Rule to an ivt::Node 59 | /// 60 | /// Returns (name, node) where the name is the name of the rule (which may 61 | /// be referenced in other places. 62 | fn flatten_rule(rule: &ast::Rule) -> FlattenResult<(String, RuleDef)> { 63 | use ast::RuleVal; 64 | let node = match &rule.val { 65 | RuleVal::AssignType(t) => flatten_type(t)?, 66 | RuleVal::AssignGroup(g) => flatten_groupentry(g)?, 67 | }; 68 | let ruledef = RuleDef { 69 | generic_parms: rule.generic_parms.clone(), 70 | node, 71 | }; 72 | Ok((rule.name.clone(), ruledef)) 73 | } 74 | 75 | fn flatten_type(ty: &ast::Type) -> FlattenResult { 76 | let options: FlattenResult> = ty.0.iter().map(flatten_type1).collect(); 77 | let options = options?; 78 | 79 | match options.len() { 80 | 0 => Err(ValidateError::Unsupported( 81 | "flatten type with 0 options".into(), 82 | )), 83 | 1 => Ok(options.into_iter().next().unwrap()), 84 | _ => Ok(Node::Choice(Choice { options })), 85 | } 86 | } 87 | 88 | fn flatten_type1(ty1: &ast::Type1) -> FlattenResult { 89 | use ast::Type1::{Control, Range, Simple}; 90 | match ty1 { 91 | Simple(ty2) => flatten_type2(ty2), 92 | Range(r) => flatten_range(r), 93 | Control(ctl) => flatten_control(ctl), 94 | } 95 | } 96 | 97 | // Control operators are functions that further restrict an underlying 98 | // type. 99 | // 100 | // TODO: 101 | // .bits 102 | // .regexp 103 | // .cbor .cborseq 104 | // .within .and 105 | // .lt .le .gt .ge .eq .ne. default 106 | // According to RFC 8610 3.8, new control operators may arrive later. 107 | // 108 | fn flatten_control(ctl: &ast::TypeControl) -> FlattenResult { 109 | let ctl_result = match ctl.op.as_str() { 110 | "size" => control_size(ctl)?, 111 | "regexp" => control_regex(ctl)?, 112 | "cbor" => control_cbor(ctl)?, 113 | _ => return Err(ValidateError::Unsupported("control operator".into())), 114 | }; 115 | 116 | Ok(Node::Control(ctl_result)) 117 | } 118 | 119 | // Handle the "size" control operator: 120 | // .size 121 | // The only allowed targets are bstr, tstr, and unsigned integers. 122 | // 123 | fn control_size(ctl: &ast::TypeControl) -> FlattenResult { 124 | let target = flatten_type2(&ctl.target)?; 125 | let size = flatten_type2(&ctl.arg)?; 126 | 127 | // The only allowed limit types are: 128 | // A positive literal integer 129 | // A named rule (which should resolve to a literal integer) 130 | match size { 131 | Node::Literal(Literal::Int(_)) => {} 132 | Node::Rule(_) => {} 133 | _ => return Err(ValidateError::Unsupported(".size limit type".into())), 134 | }; 135 | 136 | Ok(Control::Size(CtlOpSize { 137 | target: Box::new(target), 138 | size: Box::new(size), 139 | })) 140 | } 141 | 142 | // Handle the "size" control operator: 143 | // .size 144 | // The only allowed targets are bstr, tstr, and unsigned integers. 145 | // 146 | fn control_regex(ctl: &ast::TypeControl) -> FlattenResult { 147 | let target = flatten_type2(&ctl.target)?; 148 | let regexp_node = flatten_type2(&ctl.arg)?; 149 | 150 | // The target type must be a text string. 151 | match target { 152 | Node::PreludeType(PreludeType::Tstr) => {} 153 | _ => { 154 | return Err(ValidateError::Structural("bad regexp target type".into())); 155 | } 156 | } 157 | 158 | // The regular expression itself should be a literal string. 159 | match regexp_node { 160 | Node::Literal(Literal::Text(re_str)) => { 161 | // Compile the regex. 162 | // Limit the size of the result, so that untrusted input can't 163 | // use a lot of memory. The regex crate says this will adequately 164 | // protect against using too much CPU time as well. 165 | let re = RegexBuilder::new(&re_str) 166 | .size_limit(1 << 20) 167 | .build() 168 | .map_err(|_| ValidateError::Structural("malformed regexp".into()))?; 169 | 170 | Ok(Control::Regexp(CtlOpRegexp { re })) 171 | } 172 | _ => Err(ValidateError::Structural("improper regexp type".into())), 173 | } 174 | } 175 | 176 | // Handle the "cbor" control operator: 177 | // .cbor 178 | // The only allowed targets are bstr. 179 | // 180 | fn control_cbor(ctl: &ast::TypeControl) -> FlattenResult { 181 | let target = flatten_type2(&ctl.target)?; 182 | let node = Box::new(flatten_type2(&ctl.arg)?); 183 | 184 | // The target type must be a byte string. 185 | match target { 186 | Node::PreludeType(PreludeType::Bstr) => {} 187 | _ => { 188 | return Err(ValidateError::Structural("bad .cbor target type".into())); 189 | } 190 | } 191 | 192 | Ok(Control::Cbor(CtlOpCbor { node })) 193 | } 194 | 195 | // The only way a range start or end can be specified is with a literal 196 | // value, or with a typename. We will accept either of those, and throw 197 | // an error otherwise. Let the validator worry about whether a typename 198 | // resolves to a literal. 199 | fn range_point(point: &ast::Type2) -> FlattenResult { 200 | let node = match point { 201 | ast::Type2::Value(v) => flatten_value(v), 202 | ast::Type2::Typename(t) => flatten_name_generic(t), 203 | _ => Err(ValidateError::Structural( 204 | "bad type on range operator".into(), 205 | )), 206 | }?; 207 | // Check that the node that came back is a Rule or Literal. 208 | // Anything else (i.e. PreludeType) should cause an error. 209 | match node { 210 | Node::Rule(_) | Node::Literal(_) => Ok(node), 211 | _ => Err(ValidateError::Structural( 212 | "bad type on range operator".into(), 213 | )), 214 | } 215 | } 216 | 217 | fn flatten_range(range: &ast::TypeRange) -> FlattenResult { 218 | let start = range_point(&range.start)?; 219 | let end = range_point(&range.end)?; 220 | 221 | Ok(Node::Range(Range { 222 | start: start.into(), 223 | end: end.into(), 224 | inclusive: range.inclusive, 225 | })) 226 | } 227 | 228 | fn flatten_value(value: &ast::Value) -> FlattenResult { 229 | use ast::Value; 230 | match value { 231 | Value::Text(s) => Ok(literal_text(s)), 232 | Value::Uint(u) => { 233 | let value = num_to_i128(*u)?; 234 | Ok(literal_int(value)) 235 | } 236 | Value::Nint(n) => { 237 | let value = num_to_i128(*n)?; 238 | Ok(literal_int(value)) 239 | } 240 | Value::Float(f) => Ok(literal_float(*f)), 241 | Value::Bytes(b) => Ok(literal_bytes(b.clone())), 242 | } 243 | } 244 | 245 | fn flatten_type2(ty2: &ast::Type2) -> FlattenResult { 246 | use ast::Type2; 247 | match ty2 { 248 | Type2::Value(v) => flatten_value(v), 249 | Type2::Typename(s) => flatten_name_generic(s), 250 | Type2::Parethesized(t) => flatten_type(t), 251 | Type2::Map(g) => flatten_map(g), 252 | Type2::Array(g) => flatten_array(g), 253 | Type2::Unwrap(r) => Ok(Node::Unwrap(flatten_rule_generic(r)?)), 254 | Type2::ChoiceifyInline(g) => flatten_choiceify_inline(g), 255 | Type2::Choiceify(r) => flatten_choiceify(r), 256 | } 257 | } 258 | 259 | fn flatten_typename(name: &str) -> FlattenResult { 260 | let unsupported = |s: &str| -> FlattenResult { 261 | let msg = format!("prelude type '{}'", s); 262 | Err(ValidateError::Unsupported(msg)) 263 | }; 264 | 265 | let result = match name { 266 | "any" => Node::PreludeType(PreludeType::Any), 267 | "nil" | "null" => Node::PreludeType(PreludeType::Nil), 268 | "bool" => Node::PreludeType(PreludeType::Bool), 269 | "false" => literal_bool(false), 270 | "true" => literal_bool(true), 271 | "int" => Node::PreludeType(PreludeType::Int), 272 | "uint" => Node::PreludeType(PreludeType::Uint), 273 | "nint" => Node::PreludeType(PreludeType::Nint), 274 | "float" => Node::PreludeType(PreludeType::Float), 275 | "tstr" | "text" => Node::PreludeType(PreludeType::Tstr), 276 | "bstr" | "bytes" => Node::PreludeType(PreludeType::Bstr), 277 | 278 | // FIXME: need to store the "additional information" bits that will 279 | // preserve the expected floating-point size. For now, just pretend 280 | // that all floats are the same. 281 | "float16" | "float32" | "float64" | "float16-32" | "float32-64" => { 282 | Node::PreludeType(PreludeType::Float) 283 | } 284 | 285 | // The remaining prelude types are specified using CBOR (major, ai) 286 | // pairs. These types can only be used with CBOR (not JSON). 287 | 288 | // FIXME: preserve more information about these types so that a JSON 289 | // validator knows to reject them, and a CBOR validator has some chance 290 | // at further validation. 291 | 292 | // CBOR types that are stored as "tstr": 293 | // tdate = #6.0(tstr) 294 | // uri = #6.32(tstr) 295 | // b64url = #6.33(tstr) 296 | // b64legacy = #6.34(tstr) 297 | // regexp = #6.35(tstr) 298 | // mime-message = #6.36(tstr) 299 | "tdate" | "uri" | "b64url" | "b64legacy" | "regexp" | "mime-message" => { 300 | Node::PreludeType(PreludeType::Tstr) 301 | } 302 | 303 | // CBOR types that are stored as "bstr": 304 | // biguint = #6.2(bstr) 305 | // bignint = #6.3(bstr) 306 | // encoded-cbor = #6.24(bstr) 307 | "biguint" | "bignint" | "encoded-cbor" => Node::PreludeType(PreludeType::Bstr), 308 | 309 | // CBOR types that are stored as "any": 310 | // eb64url = #6.21(any) 311 | // eb64legacy = #6.22(any) 312 | // eb16 = #6.23(any) 313 | // cbor-any = #6.55799(any) 314 | "eb64url" | "eb64legacy" | "eb16" | "cbor-any" => Node::PreludeType(PreludeType::Any), 315 | 316 | // CBOR types that are stored as "number": 317 | // time = #6.1(number) 318 | "time" => return unsupported(name), 319 | 320 | // CBOR types that are choices of other types: 321 | // bigint = biguint / bignint 322 | // integer = int / bigint 323 | // unsigned = uint / biguint 324 | // number = int / float 325 | "bigint" | "integer" | "unsigned" | "number" => return unsupported(name), 326 | 327 | // Other miscellaneous prelude types: 328 | // decfrac = #6.4([e10: int, m: integer]) 329 | // bigfloat = #6.5([e2: int, m: integer]) 330 | // undefined = #7.23 331 | "decfrac" | "bigfloat" | "undefined" => return unsupported(name), 332 | 333 | // We failed to find this string in the standard prelude, so we will 334 | // assume it's a rule or group identifier. No further validation is 335 | // done at this time. 336 | _ => Node::Rule(Rule::new_name(name)), 337 | }; 338 | Ok(result) 339 | } 340 | 341 | // Similar to flatten_name_generic, but if a prelude type is detected, 342 | // it returns an error. This is for the "unwrap" and "choiceify" operators, 343 | // which can only be used on group references, not prelude types. 344 | // 345 | // This code doesn't validate that the rule name is actually a group; that 346 | // will happen later. 347 | fn flatten_rule_generic(name_generic: &ast::NameGeneric) -> FlattenResult { 348 | let result = flatten_name_generic(name_generic); 349 | match result { 350 | Ok(Node::Rule(r)) => Ok(r), 351 | _ => Err(ValidateError::GenericError), 352 | } 353 | } 354 | 355 | fn flatten_name_generic(name_generic: &ast::NameGeneric) -> FlattenResult { 356 | // Flatten the name 357 | let mut node = flatten_typename(&name_generic.name)?; 358 | match node { 359 | Node::Rule(ref mut r) => { 360 | // Add the args to the rule. 361 | for arg in &name_generic.generic_args { 362 | // Need to flatten each individual generic arg. 363 | let arg_node = flatten_type1(arg)?; 364 | r.generic_args.push(arg_node); 365 | } 366 | } 367 | _ => { 368 | // If the name resolves to a prelude type, and there are generic args, 369 | // return an error. 370 | if !name_generic.generic_args.is_empty() { 371 | return Err(ValidateError::GenericError); 372 | } 373 | } 374 | } 375 | Ok(node) 376 | } 377 | 378 | /// Flatten a group into a Map. 379 | fn flatten_map(group: &ast::Group) -> FlattenResult { 380 | let kvs = flatten_group(group)?; 381 | Ok(Node::Map(Map { members: kvs })) 382 | } 383 | 384 | /// Flatten a group into a Map. 385 | fn flatten_array(group: &ast::Group) -> FlattenResult { 386 | let kvs = flatten_group(group)?; 387 | Ok(Node::Array(Array { members: kvs })) 388 | } 389 | 390 | // Returns an ivt::Group node, or a vector of other nodes. 391 | fn flatten_group(group: &ast::Group) -> FlattenResult> { 392 | let group_choices = &group.0; 393 | if group_choices.len() == 1 { 394 | let groupchoice = &group_choices[0]; 395 | flatten_groupchoice(groupchoice) 396 | } else { 397 | // Emit a Choice node, containing a vector of Group nodes. 398 | let options: FlattenResult> = group_choices 399 | .iter() 400 | .map(|gc| { 401 | let inner_members = flatten_groupchoice(gc)?; 402 | Ok(Node::Group(Group { 403 | members: inner_members, 404 | })) 405 | }) 406 | .collect(); 407 | let options = options?; 408 | Ok(vec![Node::Choice(Choice { options })]) 409 | } 410 | } 411 | 412 | fn flatten_groupchoice(groupchoice: &ast::GrpChoice) -> FlattenResult> { 413 | let group_entries = &groupchoice.0; 414 | let kvs: FlattenResult> = group_entries.iter().map(flatten_groupentry).collect(); 415 | kvs 416 | } 417 | 418 | fn flatten_groupentry(group_entry: &ast::GrpEnt) -> FlattenResult { 419 | let node = flatten_groupentry_val(&group_entry.val)?; 420 | Ok(occur_wrap(&group_entry.occur, node)) 421 | } 422 | 423 | fn flatten_groupentry_val(gev: &ast::GrpEntVal) -> FlattenResult { 424 | use ast::GrpEntVal; 425 | 426 | match gev { 427 | GrpEntVal::Member(m) => flatten_member(m), 428 | GrpEntVal::Groupname(s) => flatten_typename(s), 429 | GrpEntVal::Parenthesized(g) => { 430 | let nodes = flatten_group(g)?; 431 | Ok(Node::Group(Group { members: nodes })) 432 | } 433 | } 434 | } 435 | 436 | /* Don't delete this yet; I would like to remove usize::MAX from the parser. 437 | /// Convert ast::Occur to ivt::OccurLimit 438 | impl From<&ast::Occur> for OccurLimit { 439 | fn from(occur: &ast::Occur) -> OccurLimit { 440 | match occur { 441 | ast::Occur::Optional(_) => OccurLimit::Optional, 442 | ast::Occur::ZeroOrMore(_) => OccurLimit::ZeroOrMore, 443 | ast::Occur::OneOrMore(_) => OccurLimit::OneOrMore, 444 | ast::Occur::Exact { lower, upper, .. } => { 445 | let lower: usize = match lower { 446 | Some(n) => *n, 447 | None => 0, 448 | }; 449 | let upper: usize = match upper { 450 | Some(n) => *n, 451 | None => usize::MAX, 452 | }; 453 | OccurLimit::Numbered(lower, upper) 454 | } 455 | } 456 | } 457 | } 458 | */ 459 | 460 | /// If the ast::Occur is Some, wrap the given Node in an ivt::Occur. 461 | /// 462 | /// This is an adapter between the way the `ast` does occurences (extra 463 | /// metadata attached to certain data structures) and the way `ivt` does them 464 | /// (wrapping the Node in another Node). 465 | /// 466 | /// If the occur argument is None, return the original node. 467 | /// 468 | fn occur_wrap(occur: &Option, node: Node) -> Node { 469 | match &occur { 470 | Some(o) => Node::Occur(Occur::new(o.clone(), node)), 471 | None => node, 472 | } 473 | } 474 | 475 | fn flatten_member(member: &ast::Member) -> FlattenResult { 476 | match &member.key { 477 | Some(key) => { 478 | let cut = key.cut; 479 | let key = flatten_memberkey(key)?; 480 | let value = flatten_type(&member.value)?; 481 | Ok(Node::KeyValue(KeyValue::new(key, value, cut))) 482 | } 483 | None => flatten_type(&member.value), 484 | } 485 | } 486 | 487 | // try_into(), remapping the error. 488 | fn num_to_i128(n: T) -> FlattenResult 489 | where 490 | T: TryInto + std::fmt::Display + Copy, 491 | { 492 | // This error doesn't seem possible, since isize and usize should always 493 | // fit into an i128. 494 | n.try_into().map_err(|_| { 495 | let msg = format!("integer conversion failed: {}", n); 496 | ValidateError::Structural(msg) 497 | }) 498 | } 499 | 500 | fn flatten_memberkey(memberkey: &ast::MemberKey) -> FlattenResult { 501 | use ast::MemberKeyVal; 502 | 503 | match &memberkey.val { 504 | MemberKeyVal::Bareword(s) => { 505 | // A "bareword" is a literal string that appears without quote 506 | // marks. Treat it just like we would a literal with quotes. 507 | Ok(literal_text(s.clone())) 508 | } 509 | MemberKeyVal::Type1(t1) => flatten_type1(t1), 510 | MemberKeyVal::Value(v) => flatten_value(v), 511 | } 512 | } 513 | 514 | fn flatten_choiceify(name: &ast::NameGeneric) -> FlattenResult { 515 | let rule = flatten_rule_generic(name)?; 516 | Ok(Node::Choiceify(rule)) 517 | } 518 | 519 | fn flatten_choiceify_inline(group: &ast::Group) -> FlattenResult { 520 | let kvs = flatten_group(group)?; 521 | Ok(Node::ChoiceifyInline(Array { members: kvs })) 522 | } 523 | 524 | // Useful utilities for testing the flatten code. 525 | #[cfg(test)] 526 | #[macro_use] 527 | mod test_utils { 528 | use super::*; 529 | use std::collections::BTreeMap; 530 | 531 | // Given a string, generate a rule reference. 532 | impl From<&str> for Rule { 533 | fn from(s: &str) -> Self { 534 | Rule { 535 | name: s.to_string(), 536 | generic_args: vec![], 537 | } 538 | } 539 | } 540 | 541 | // Given a string, generate a Node::Rule 542 | impl From<&str> for Node { 543 | fn from(s: &str) -> Self { 544 | Node::Rule(Rule::from(s)) 545 | } 546 | } 547 | 548 | // Given a Node (with no generic parameters), generate a RuleDef. 549 | impl From for RuleDef { 550 | fn from(n: Node) -> Self { 551 | RuleDef { 552 | generic_parms: Vec::default(), 553 | node: n, 554 | } 555 | } 556 | } 557 | 558 | // Given a list of names and Nodes, build a rules map. 559 | // Note: This requires Node instead of Into because we want to allow 560 | // multiple types of Node, which must be done by the caller. 561 | pub fn make_rules(mut list: Vec<(&str, Node)>) -> RulesByName { 562 | list.drain(..) 563 | .map(|(s, n)| (s.to_string(), RuleDef::from(n))) 564 | .collect() 565 | } 566 | 567 | // Given a single name/Node pair, build a rules map. 568 | pub fn make_rule>(name: &str, node: T) -> RulesByName { 569 | let node: Node = node.into(); 570 | let mut result = BTreeMap::new(); 571 | result.insert(name.to_string(), RuleDef::from(node)); 572 | result 573 | } 574 | 575 | // Given a single name/Node pair, build a rules map. 576 | pub fn make_generic_rule(name: &str, generic_parms: &[&str], node: Node) -> RulesByName { 577 | let mut result = BTreeMap::new(); 578 | let generic_parms: Vec = generic_parms.iter().map(|s| String::from(*s)).collect(); 579 | result.insert( 580 | name.to_string(), 581 | RuleDef { 582 | generic_parms, 583 | node, 584 | }, 585 | ); 586 | result 587 | } 588 | 589 | // A trait for generating literals. 590 | pub trait CreateLiteral { 591 | fn literal(self) -> Node; 592 | } 593 | 594 | // Create a literal string. 595 | impl CreateLiteral for &str { 596 | fn literal(self) -> Node { 597 | Node::Literal(Literal::Text(self.to_string())) 598 | } 599 | } 600 | 601 | // Create a literal integer. 602 | // This will work for both signed and unsigned rust literals. 603 | impl CreateLiteral for i64 { 604 | fn literal(self) -> Node { 605 | Node::Literal(Literal::Int(self.into())) 606 | } 607 | } 608 | 609 | // Shorthand for the tstr prelude type 610 | pub fn tstr() -> Node { 611 | Node::PreludeType(PreludeType::Tstr) 612 | } 613 | 614 | // Shorthand for creating a new map. 615 | pub fn make_map() -> Map { 616 | Map { 617 | members: Vec::new(), 618 | } 619 | } 620 | 621 | // Shorthand for creating a new array. 622 | pub fn make_array() -> Array { 623 | Array { 624 | members: Vec::new(), 625 | } 626 | } 627 | 628 | // A trait for appending something (to a Map or Array) 629 | // since it returns Self, we can chain multiple calls. 630 | pub trait Append { 631 | fn append>(self, t: T) -> Self; 632 | } 633 | 634 | impl Append for Map { 635 | fn append>(mut self, t: T) -> Self { 636 | let node: Node = t.into(); 637 | self.members.push(node); 638 | self 639 | } 640 | } 641 | 642 | impl Append for Array { 643 | fn append>(mut self, t: T) -> Self { 644 | let node: Node = t.into(); 645 | self.members.push(node); 646 | self 647 | } 648 | } 649 | 650 | // Shorthand for storing a Map inside a Node. 651 | impl From for Node { 652 | fn from(m: Map) -> Self { 653 | Node::Map(m) 654 | } 655 | } 656 | 657 | // Shorthand for storing an Array inside a Node. 658 | impl From for Node { 659 | fn from(a: Array) -> Self { 660 | Node::Array(a) 661 | } 662 | } 663 | 664 | // Shorthand for storing a KeyValue inside a Node. 665 | impl From for Node { 666 | fn from(kv: KeyValue) -> Self { 667 | Node::KeyValue(kv) 668 | } 669 | } 670 | 671 | // Shorthand for storing a Rule inside a Node. 672 | impl From for Node { 673 | fn from(r: Rule) -> Self { 674 | Node::Rule(r) 675 | } 676 | } 677 | 678 | // Shorthand for storing a Rule inside a Node. 679 | impl From for Node { 680 | fn from(r: Control) -> Self { 681 | Node::Control(r) 682 | } 683 | } 684 | 685 | #[derive(Copy, Clone)] 686 | pub enum KvCut { 687 | Cut, 688 | NoCut, 689 | } 690 | pub use KvCut::*; 691 | 692 | impl From for bool { 693 | fn from(c: KvCut) -> bool { 694 | match c { 695 | Cut => true, 696 | NoCut => false, 697 | } 698 | } 699 | } 700 | 701 | // Shorthand for creating a key-value pair, with explicit cut setting. 702 | pub fn kv(k: Node, v: Node, cut: KvCut) -> KeyValue { 703 | KeyValue { 704 | key: Box::new(k), 705 | value: Box::new(v), 706 | cut: cut.into(), 707 | } 708 | } 709 | } 710 | 711 | #[cfg(test)] 712 | mod tests { 713 | use super::test_utils::*; 714 | use super::*; 715 | 716 | #[test] 717 | fn test_flatten_literal_int() { 718 | let cddl_input = r#"thing = 1"#; 719 | let result = flatten_from_str(cddl_input).unwrap(); 720 | let expected = make_rule("thing", 1.literal()); 721 | assert_eq!(result, expected); 722 | 723 | let cddl_input = r#"thing = -1"#; 724 | let result = flatten_from_str(cddl_input).unwrap(); 725 | let expected = make_rule("thing", (-1i64).literal()); 726 | assert_eq!(result, expected); 727 | } 728 | 729 | #[test] 730 | fn test_flatten_literal_tstr() { 731 | let cddl_input = r#"thing = "abc""#; 732 | let result = flatten_from_str(cddl_input).unwrap(); 733 | let expected = make_rule("thing", "abc".literal()); 734 | assert_eq!(result, expected); 735 | } 736 | 737 | #[test] 738 | fn test_flatten_prelude_reference() { 739 | let cddl_input = r#"thing = int"#; 740 | let result = flatten_from_str(cddl_input).unwrap(); 741 | let expected = make_rule("thing", Node::PreludeType(PreludeType::Int)); 742 | assert_eq!(result, expected); 743 | } 744 | 745 | #[test] 746 | fn test_flatten_type_reference() { 747 | let cddl_input = r#"thing = foo"#; 748 | let result = flatten_from_str(cddl_input).unwrap(); 749 | assert_eq!(result, make_rule("thing", Rule::from("foo"))); 750 | } 751 | 752 | #[test] 753 | fn test_flatten_map() { 754 | // A map containing a bareword key 755 | let cddl_input = r#"thing = { foo: tstr }"#; 756 | let result = flatten_from_str(cddl_input).unwrap(); 757 | let expected = make_rule("thing", make_map().append(kv("foo".literal(), tstr(), Cut))); 758 | assert_eq!(result, expected); 759 | 760 | // A map containing a prelude type key. 761 | // Note: CDDL syntax requires type keys to use "=>" not ":", otherwise 762 | // it will assume a bareword key is being used. 763 | let cddl_input = r#"thing = { tstr => tstr }"#; 764 | let result = flatten_from_str(cddl_input).unwrap(); 765 | let expected = make_rule("thing", make_map().append(kv(tstr(), tstr(), NoCut))); 766 | assert_eq!(result, expected); 767 | 768 | // A map key name alias 769 | let cddl_input = r#"foo = "bar" thing = { foo => tstr }"#; 770 | let result = flatten_from_str(cddl_input).unwrap(); 771 | let expected = make_rules(vec![ 772 | ("foo", "bar".literal()), 773 | ( 774 | "thing", 775 | make_map().append(kv("foo".into(), tstr(), NoCut)).into(), 776 | ), 777 | ]); 778 | assert_eq!(result, expected); 779 | } 780 | 781 | #[test] 782 | fn test_flatten_generic() { 783 | let cddl_input = "message = [t, v]"; 784 | let result = flatten_from_str(cddl_input).unwrap(); 785 | let expected = make_generic_rule( 786 | "message", 787 | &["t", "v"], 788 | make_array() 789 | .append(Rule::from("t")) 790 | .append(Rule::from("v")) 791 | .into(), 792 | ); 793 | assert_eq!(result, expected); 794 | } 795 | 796 | #[test] 797 | fn test_control_op() { 798 | let cddl_input = "four_bytes = tstr .size 4"; 799 | let result = flatten_from_str(cddl_input).unwrap(); 800 | let expected = make_rule( 801 | "four_bytes", 802 | Control::Size(CtlOpSize { 803 | target: Box::new(tstr()), 804 | size: Box::new(4.literal()), 805 | }), 806 | ); 807 | assert_eq!(result, expected); 808 | } 809 | } 810 | -------------------------------------------------------------------------------- /tests/cbor_cddl.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "ciborium")] 2 | 3 | use cddl_cat::cbor::validate_cbor_bytes; 4 | use cddl_cat::util::ErrorMatch; 5 | use cddl_cat::ValidateResult; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[rustfmt::skip] // allow arbitrary indents for readability 9 | pub mod cbor { 10 | // Mostly example values from rfc7049 appendix A 11 | pub const BOOL_FALSE: &[u8] = b"\xF4"; 12 | pub const BOOL_TRUE: &[u8] = b"\xF5"; 13 | pub const NULL: &[u8] = b"\xF6"; 14 | pub const UNDEFINED: &[u8] = b"\xF7"; 15 | 16 | pub const INT_0: &[u8] = b"\x00"; 17 | pub const INT_1: &[u8] = b"\x01"; 18 | pub const INT_9: &[u8] = b"\x09"; 19 | pub const INT_23: &[u8] = b"\x17"; 20 | pub const INT_24: &[u8] = b"\x18\x18"; 21 | pub const INT_1T: &[u8] = b"\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"; 22 | pub const NINT_1000: &[u8] = b"\x39\x03\xe7"; // -1000 23 | 24 | pub const FLOAT_0_0: &[u8] = b"\xf9\x00\x00"; // #7.25 (f16) 25 | pub const FLOAT_1_0: &[u8] = b"\xf9\x3c\x00"; // #7.25 (f16) 26 | pub const FLOAT_1E5: &[u8] = b"\xfa\x47\xc3\x50\x00"; // #7.26 (f32) 27 | pub const FLOAT_1E300: &[u8] = b"\xfb\x7e\x37\xe4\x3c\x88\x00\x75\x9c"; // #7.27 (f64) 28 | 29 | pub const ARRAY_EMPTY: &[u8] = b"\x80"; // [] 30 | pub const ARRAY_123: &[u8] = b"\x83\x01\x02\x03"; // [1,2,3] 31 | pub const ARRAY_12: &[u8] = b"\x82\x01\x02"; // [1,2] 32 | pub const ARRAY_1_23_45:&[u8] = b"\x83\x01\x82\x02\x03\x82\x04\x05"; // [1, [2, 3], [4, 5]] 33 | 34 | pub const TEXT_EMPTY: &[u8] = b"\x60"; 35 | pub const TEXT_IETF: &[u8] = b"\x64\x49\x45\x54\x46"; // "IETF" 36 | pub const TEXT_CJK: &[u8] = b"\x63\xe6\xb0\xb4"; // "水" 37 | 38 | pub const BYTES_EMPTY: &[u8] = b"\x40"; 39 | pub const BYTES_1234: &[u8] = b"\x44\x01\x02\x03\x04"; // hex 01020304 40 | 41 | pub const CBOR_INT_23: &[u8] = b"\x41\x17"; // cbor(23) 42 | } 43 | 44 | fn cbor_to_vec(value: &impl Serialize) -> Result, ciborium::ser::Error> { 45 | let mut result = Vec::new(); 46 | ciborium::into_writer(value, &mut result)?; 47 | Ok(result) 48 | } 49 | 50 | #[test] 51 | fn validate_cbor_null() { 52 | let cddl_input = r#"thing = nil"#; 53 | validate_cbor_bytes("thing", cddl_input, cbor::NULL).unwrap(); 54 | validate_cbor_bytes("thing", cddl_input, cbor::INT_0).unwrap_err(); 55 | let err = validate_cbor_bytes("thing", cddl_input, cbor::BOOL_FALSE).unwrap_err(); 56 | assert_eq!(err.to_string(), "Mismatch(expected nil)"); 57 | } 58 | 59 | #[test] 60 | fn validate_cbor_bool() { 61 | let cddl_input = r#"thing = true"#; 62 | validate_cbor_bytes("thing", cddl_input, cbor::BOOL_TRUE).unwrap(); 63 | validate_cbor_bytes("thing", cddl_input, cbor::BOOL_FALSE).unwrap_err(); 64 | let err = validate_cbor_bytes("thing", cddl_input, cbor::NULL).unwrap_err(); 65 | assert_eq!(err.to_string(), "Mismatch(expected true)"); 66 | } 67 | 68 | #[test] 69 | fn validate_cbor_float() { 70 | let cddl_input = r#"thing = 0.0"#; 71 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_0_0).unwrap(); 72 | let err = validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap_err(); 73 | // FIXME: It's annoying that the rust float syntax can omit the decimal point. 74 | assert_eq!(err.to_string(), "Mismatch(expected 0)"); 75 | 76 | let cddl_input = r#"thing = float"#; 77 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 78 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1E5).unwrap(); 79 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1E300).unwrap(); 80 | 81 | let cddl_input = r#"thing = float16"#; 82 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 83 | 84 | // "Too small" floats should not cause a validation error. 85 | // "Canonical CBOR" suggests that floats should be shrunk to the smallest 86 | // size that can represent the value. So 1.0 can be stored in 16 bits, 87 | // even if the CDDL specifies float64. 88 | let cddl_input = r#"thing = float32"#; 89 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 90 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1E5).unwrap(); 91 | 92 | let cddl_input = r#"thing = float64"#; 93 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 94 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1E300).unwrap(); 95 | 96 | // TODO: check that large floats don't validate against a smaller size. 97 | // E.g. CBOR #7.27 (64-bit) shouldn't validate against "float16" or "float32". 98 | } 99 | 100 | #[test] 101 | fn validate_cbor_choice() { 102 | let cddl_input = r#"thing = 23 / 24"#; 103 | validate_cbor_bytes("thing", cddl_input, cbor::INT_23).unwrap(); 104 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap(); 105 | 106 | let cddl_input = r#"thing = (foo // bar) foo = (int / float) bar = tstr"#; 107 | validate_cbor_bytes("thing", cddl_input, cbor::INT_23).unwrap(); 108 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 109 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_IETF).unwrap(); 110 | let err = validate_cbor_bytes("thing", cddl_input, cbor::BOOL_TRUE).unwrap_err(); 111 | assert_eq!(err.to_string(), "Mismatch(expected choice of 2)"); 112 | 113 | let cddl_input = r#"thing = (foo / bar) foo = (int / float) bar = tstr"#; 114 | validate_cbor_bytes("thing", cddl_input, cbor::INT_23).unwrap(); 115 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 116 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_IETF).unwrap(); 117 | let err = validate_cbor_bytes("thing", cddl_input, cbor::BOOL_TRUE).unwrap_err(); 118 | assert_eq!(err.to_string(), "Mismatch(expected choice of 2)"); 119 | 120 | let cddl_input = r#"thing = (int / float // tstr / bstr)"#; 121 | validate_cbor_bytes("thing", cddl_input, cbor::INT_23).unwrap(); 122 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 123 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_IETF).unwrap(); 124 | let err = validate_cbor_bytes("thing", cddl_input, cbor::BOOL_TRUE).unwrap_err(); 125 | assert_eq!(err.to_string(), "Mismatch(expected choice of 2)"); 126 | } 127 | 128 | #[test] 129 | fn validate_cbor_integer() { 130 | let cddl_input = r#"thing = 1"#; 131 | validate_cbor_bytes("thing", cddl_input, cbor::INT_1).unwrap(); 132 | validate_cbor_bytes("thing", cddl_input, cbor::NULL).unwrap_err(); 133 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap_err(); 134 | validate_cbor_bytes("thing", cddl_input, cbor::BOOL_TRUE).unwrap_err(); 135 | let cddl_input = r#"thing = int"#; 136 | validate_cbor_bytes("thing", cddl_input, cbor::INT_0).unwrap(); 137 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap(); 138 | validate_cbor_bytes("thing", cddl_input, cbor::NINT_1000).unwrap(); 139 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap_err(); 140 | let cddl_input = r#"thing = uint"#; 141 | validate_cbor_bytes("thing", cddl_input, cbor::INT_0).unwrap(); 142 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap(); 143 | validate_cbor_bytes("thing", cddl_input, cbor::NINT_1000).unwrap_err(); 144 | let cddl_input = r#"thing = nint"#; 145 | validate_cbor_bytes("thing", cddl_input, cbor::NINT_1000).unwrap(); 146 | validate_cbor_bytes("thing", cddl_input, cbor::INT_0).unwrap_err(); 147 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap_err(); 148 | } 149 | 150 | #[test] 151 | fn validate_cbor_ranges() { 152 | let cddl_input = r#"thing = 1..5"#; 153 | validate_cbor_bytes("thing", cddl_input, cbor::INT_1).unwrap(); 154 | let err = validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap_err(); 155 | assert_eq!(err.to_string(), "Mismatch(expected 1..5)"); 156 | 157 | let err = validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap_err(); 158 | assert_eq!(err.to_string(), "Mismatch(expected 1..5)"); 159 | 160 | let cddl_input = r#"thing = 1..24"#; 161 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap(); 162 | 163 | let cddl_input = r#"thing = 1...24"#; 164 | validate_cbor_bytes("thing", cddl_input, cbor::INT_23).unwrap(); 165 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap_err(); 166 | 167 | let cddl_input = r#"thing = 1 .. 5.3"#; 168 | let err = validate_cbor_bytes("thing", cddl_input, cbor::INT_1).unwrap_err(); 169 | assert_eq!( 170 | err.to_string(), 171 | "Structural(mismatched types on range operator)" 172 | ); 173 | 174 | let cddl_input = r#"max=5 thing = 1..max"#; 175 | validate_cbor_bytes("thing", cddl_input, cbor::INT_1).unwrap(); 176 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap_err(); 177 | 178 | let cddl_input = r#"thing = 0.9..1.2"#; 179 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_1_0).unwrap(); 180 | validate_cbor_bytes("thing", cddl_input, cbor::FLOAT_0_0).unwrap_err(); 181 | let err = validate_cbor_bytes("thing", cddl_input, cbor::INT_1).unwrap_err(); 182 | assert_eq!(err.to_string(), "Mismatch(expected 0.9..1.2)"); 183 | 184 | let cddl_input = r#"thing = 1..uint"#; 185 | let err = validate_cbor_bytes("thing", cddl_input, cbor::INT_1).unwrap_err(); 186 | assert_eq!(err.to_string(), "Structural(bad type on range operator)"); 187 | 188 | let cddl_input = r#"thing = 1..[5]"#; 189 | let err = validate_cbor_bytes("thing", cddl_input, cbor::INT_1).unwrap_err(); 190 | assert_eq!(err.to_string(), "Structural(bad type on range operator)"); 191 | } 192 | 193 | #[test] 194 | fn validate_cbor_textstring() { 195 | // "tstr" and "text" mean the same thing. 196 | for cddl_input in &[r#"thing = tstr"#, r#"thing = text"#] { 197 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_EMPTY).unwrap(); 198 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_IETF).unwrap(); 199 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_CJK).unwrap(); 200 | let err = validate_cbor_bytes("thing", cddl_input, cbor::BYTES_EMPTY).unwrap_err(); 201 | assert_eq!(err.to_string(), "Mismatch(expected tstr)"); 202 | } 203 | } 204 | 205 | #[test] 206 | fn validate_cbor_bytestring() { 207 | // "bstr" and "bytes" mean the same thing. 208 | for cddl_input in &[r#"thing = bstr"#, r#"thing = bytes"#] { 209 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_EMPTY).unwrap(); 210 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_1234).unwrap(); 211 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_EMPTY).unwrap_err(); 212 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 213 | assert_eq!(err.to_string(), "Mismatch(expected bstr)"); 214 | } 215 | 216 | let cddl_input = r#"thing = h'01020304'"#; 217 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_1234).unwrap(); 218 | } 219 | 220 | #[test] 221 | fn validate_cbor_array() { 222 | let cddl_input = r#"thing = []"#; 223 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap(); 224 | validate_cbor_bytes("thing", cddl_input, cbor::NULL).unwrap_err(); 225 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 226 | assert_eq!(err.to_string(), "Mismatch(expected shorter array)"); 227 | 228 | let cddl_input = r#"thing = [1, 2, 3]"#; 229 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 230 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap_err(); 231 | assert_eq!(err.to_string(), "Mismatch(expected array element 1)"); 232 | } 233 | 234 | // These data structures exist so that we can serialize some more complex 235 | // beyond the RFC examples. 236 | #[derive(Debug, Serialize, Deserialize)] 237 | struct PersonStruct { 238 | name: String, 239 | age: u32, 240 | } 241 | 242 | #[derive(Debug, Serialize, Deserialize)] 243 | struct PersonTuple(String, u32); 244 | 245 | #[derive(Debug, Serialize, Deserialize)] 246 | struct BackwardsTuple(u32, String); 247 | 248 | #[derive(Debug, Serialize, Deserialize)] 249 | struct LongTuple(String, u32, u32); 250 | 251 | #[derive(Debug, Serialize, Deserialize)] 252 | struct ShortTuple(String); 253 | 254 | #[derive(Debug, Serialize, Deserialize)] 255 | struct KitchenSink(String, u32, f64, bool); 256 | 257 | #[test] 258 | fn validate_cbor_homogenous_array() { 259 | let cddl_input = r#"thing = [* int]"#; // zero or more 260 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap(); 261 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 262 | 263 | let cddl_input = r#"thing = [+ int]"#; // one or more 264 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 265 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap_err(); 266 | assert_eq!( 267 | err.to_string(), 268 | "Mismatch(expected more array element [+ Int])" 269 | ); 270 | 271 | let cddl_input = r#"thing = [? int]"#; // zero or one 272 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap(); 273 | let cbor_bytes = cbor_to_vec(&[42]).unwrap(); 274 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 275 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 276 | assert_eq!(err.to_string(), "Mismatch(expected shorter array)"); 277 | 278 | let cddl_input = r#"thing = [* tstr]"#; 279 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 280 | // FIXME: this error message is confusing. 281 | // Having consumed 0 tstr, we find that the array still has values. 282 | assert_eq!(err.to_string(), "Mismatch(expected shorter array)"); 283 | 284 | // Alias type. 285 | let cddl_input = r#"thing = [* zipcode] zipcode = int"#; 286 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap(); 287 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 288 | } 289 | 290 | #[test] 291 | fn validate_cbor_array_groups() { 292 | let cddl_input = r#"thing = [int, (int, int)]"#; 293 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 294 | 295 | // Naming a group causes it to be inlined. 296 | let cddl_input = r#"thing = [int, foo] foo = (int, int)"#; 297 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 298 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).err_mismatch(); 299 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_12).err_mismatch(); 300 | 301 | let cddl_input = r#"thing = [(int, int, int)]"#; 302 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 303 | 304 | // Consume values in groups of one, an arbitrary number of times. 305 | let cddl_input = r#"thing = [* (int)]"#; 306 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap(); 307 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 308 | 309 | // Consume values in groups of three, an arbitrary number of times. 310 | let cddl_input = r#"thing = [* (int, int, int)]"#; 311 | //validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap(); 312 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 313 | 314 | // Consume values in groups of two, an arbitrary number of times. 315 | let cddl_input = r#"thing = [* (int, int)]"#; 316 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap(); 317 | // Shouldn't match because three doesn't go into two evenly. 318 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).err_mismatch(); 319 | 320 | let cddl_input = r#"thing = [a: int, b: int, bar] bar = (c: int)"#; 321 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 322 | 323 | let cddl_input = r#"thing = [a: int, (bar)] bar = (b: int, c: int)"#; 324 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 325 | 326 | // This is incorrectly constructed, because this is a key-value with 327 | // a group name where the value should be. 328 | let cddl_input = r#"thing = [a: int, b: bar] bar = (b: int, c: int)"#; 329 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 330 | assert_eq!(err.to_string(), "Unsupported standalone group"); 331 | 332 | // This is constructed to require backtracking by the validator: 333 | // `foo` will consume an int before failing; we need to rewind to 334 | // a previous state so that `bar` will match. 335 | let cddl_input = r#"thing = [int, (foo // bar)] foo = (int, tstr) bar = (int, int)"#; 336 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 337 | 338 | // Test nested groups with lots of backtracking. 339 | let cddl_input = r#"thing = [(int, (int, bool // (int, tstr // int, int)))]"#; 340 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 341 | } 342 | 343 | #[test] 344 | fn validate_cbor_array_unwrap() { 345 | // unwrap something into the head of an array 346 | let cddl_input = r#"header = [a: int, b: int] thing = [~header c: int]"#; 347 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 348 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap_err(); 349 | 350 | // unwrap something into the tail of an array 351 | let cddl_input = r#"footer = [a: int, b: int] thing = [c: int ~footer]"#; 352 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 353 | 354 | // unwrap something into the middle of an array 355 | let cddl_input = r#"middle = [int] thing = [a: int, ~middle, c: int]"#; 356 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 357 | 358 | // add an extra rule redirection while unwrapping 359 | let cddl_input = r#"foo = int middle = [foo] thing = [a: int, ~middle, c: int]"#; 360 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 361 | 362 | // Fail if we find too few items. 363 | let cddl_input = r#"header = [a: int] thing = [~header, c: int]"#; 364 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 365 | 366 | let cddl_input = r#"footer = [a: int] thing = [c: int, ~footer]"#; 367 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 368 | 369 | // Fail if we don't find enough matching items while unwrapping. 370 | let cddl_input = r#"footer = [a: int, b: int] thing = [c: int, d: int, ~footer]"#; 371 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 372 | assert_eq!(err.to_string(), "Mismatch(expected array element Int)"); 373 | 374 | // Fail if the unwrapped name doesn't resolve. 375 | let cddl_input = r#"thing = [c: int ~footer]"#; 376 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 377 | assert_eq!(err.to_string(), "MissingRule(footer)"); 378 | 379 | // Unwrapping a map into an array isn't allowed. 380 | let cddl_input = r#"header = {a: int, b: int} thing = [~header c: int]"#; 381 | let err = validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 382 | assert_eq!(err.to_string(), "Mismatch(expected unwrap array)"); 383 | } 384 | 385 | #[test] 386 | fn validate_cbor_array_record() { 387 | let cddl_input = r#"thing = [a: int, b: int, c: int]"#; 388 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 389 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap_err(); 390 | 391 | let cddl_input = r#"thing = [a: int, b: int, c: foo] foo = int"#; 392 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 393 | 394 | let cddl_input = r#"thing = [int, int, int]"#; 395 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap(); 396 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_EMPTY).unwrap_err(); 397 | 398 | let cddl_input = r#"thing = [a: tstr, b: int]"#; 399 | 400 | let input = PersonTuple("Alice".to_string(), 42); 401 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 402 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 403 | 404 | let input = BackwardsTuple(43, "Carol".to_string()); 405 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 406 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 407 | 408 | let input = LongTuple("David".to_string(), 44, 45); 409 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 410 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 411 | 412 | let input = ShortTuple("Eve".to_string()); 413 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 414 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 415 | 416 | let cddl_input = r#"thing = [a: tstr, b: uint, c: float, d: bool]"#; 417 | 418 | let input = KitchenSink("xyz".to_string(), 17, 9.9, false); 419 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 420 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 421 | 422 | // FIXME: there isn't any way at present to serialize a struct 423 | // into a CBOR array. See https://github.com/pyfisch/cbor/issues/107 424 | //let input = PersonStruct{name: "Bob".to_string(), age: 43}; 425 | //let cbor_bytes = cbor_to_vec(&input).unwrap(); 426 | //validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 427 | 428 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 429 | } 430 | 431 | #[test] 432 | fn validate_cbor_map_unwrap() { 433 | let input = PersonStruct { 434 | name: "Bob".to_string(), 435 | age: 43, 436 | }; 437 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 438 | let cddl_input = r#"thing = {name: tstr, ~agroup} agroup = {age: int}"#; 439 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 440 | 441 | // Unwrapping an array into a map isn't allowed. 442 | let cddl_input = r#"thing = {name: tstr, ~agroup} agroup = [age: int]"#; 443 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 444 | assert_eq!(err.to_string(), "Mismatch(expected unwrap map)"); 445 | } 446 | 447 | #[test] 448 | fn validate_cbor_map_group() { 449 | let input = PersonStruct { 450 | name: "Bob".to_string(), 451 | age: 43, 452 | }; 453 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 454 | let cddl_input = r#"thing = {name: tstr, agroup} agroup = (age: int)"#; 455 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 456 | 457 | let cddl_input = r#"thing = {agroup} agroup = (age: int, name: tstr)"#; 458 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 459 | 460 | let cddl_input = r#"thing = {((agroup))} agroup = (age: int, name: tstr)"#; 461 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 462 | 463 | let cddl_input = r#"thing = {agroup empty} agroup = (age: int, name: tstr) empty = ()"#; 464 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 465 | 466 | let cddl_input = 467 | r#"thing = {agroup maybe} agroup = (age: int, name: tstr) maybe = (? minor: bool)"#; 468 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 469 | 470 | let cddl_input = r#"thing = {name: tstr, agroup} agroup = (wrong: int)"#; 471 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 472 | assert_eq!(err.to_string(), r#"Mismatch(expected map{"wrong"})"#); 473 | 474 | let cddl_input = r#"thing = {name: tstr, agroup} agroup = (age: bool)"#; 475 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 476 | assert_eq!(err.to_string(), "Mismatch(expected bool)"); 477 | 478 | // This is constructed to require backtracking by the validator: 479 | // `foo` will consume `age` before failing; we need to rewind to 480 | // a previous state so that `bar` will match. 481 | let cddl_input = 482 | r#"thing = { foo // bar } foo = (name: tstr, age: bool) bar = (name: tstr, age: int)"#; 483 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 484 | 485 | // Test nested groups with lots of backtracking. 486 | let cddl_input = r#"thing = { (name: tstr, photo: bstr // 487 | (name: tstr, fail: bool // name: tstr, age: int)) }"#; 488 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 489 | 490 | // Test a group where none of the variants match. 491 | let cddl_input = 492 | r#"thing = { foo // bar } foo = (name: tstr, age: bool) bar = (name: tstr, age: float)"#; 493 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).err_mismatch(); 494 | } 495 | 496 | #[test] 497 | fn validate_cbor_map() { 498 | let input = PersonStruct { 499 | name: "Bob".to_string(), 500 | age: 43, 501 | }; 502 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 503 | let cddl_input = r#"thing = {name: tstr, age: int}"#; 504 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 505 | 506 | let cddl_input = r#"thing = {name: tstr, ? age: int}"#; 507 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 508 | 509 | // Ensure that keys are optional if the occurrence is "?" or "*" 510 | // and required if the occurrence is "+" 511 | let cddl_input = r#"thing = {name: tstr, age: int, ? minor: bool}"#; 512 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 513 | let cddl_input = r#"thing = {name: tstr, age: int, * minor: bool}"#; 514 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 515 | let cddl_input = r#"thing = {name: tstr, age: int, + minor: bool}"#; 516 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 517 | assert_eq!( 518 | err.to_string(), 519 | r#"Mismatch(expected map{+ "minor": Bool}])"# 520 | ); 521 | 522 | let cddl_input = r#"thing = {name: tstr, age: tstr}"#; 523 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 524 | assert_eq!(err.to_string(), "Mismatch(expected tstr)"); 525 | 526 | let cddl_input = r#"thing = {name: tstr}"#; 527 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 528 | assert_eq!(err.to_string(), "Mismatch(expected shorter map)"); 529 | 530 | // "* keytype => valuetype" is the expected syntax for collecting 531 | // any remaining key/value pairs of the expected type. 532 | let cddl_input = r#"thing = {* tstr => any}"#; 533 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 534 | let cddl_input = r#"thing = {name: tstr, * tstr => any}"#; 535 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 536 | let cddl_input = r#"thing = {name: tstr, age: int, * tstr => any}"#; 537 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 538 | let cddl_input = r#"thing = {+ tstr => any}"#; 539 | validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap(); 540 | 541 | // Should fail because the CBOR input has two entries that can't be 542 | // collected because the key type doesn't match. 543 | let cddl_input = r#"thing = {* int => any}"#; 544 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 545 | assert_eq!(err.to_string(), "Mismatch(expected shorter map)"); 546 | 547 | let cddl_input = r#"thing = {name: tstr, age: int, minor: bool}"#; 548 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 549 | assert_eq!(err.to_string(), r#"Mismatch(expected map{"minor"})"#); 550 | 551 | let cddl_input = r#"thing = {x: int, y: int, z: int}"#; 552 | validate_cbor_bytes("thing", cddl_input, cbor::ARRAY_123).unwrap_err(); 553 | } 554 | 555 | #[derive(Debug, Serialize)] 556 | struct StreetNumber { 557 | street: String, 558 | number: u32, 559 | name: String, 560 | zip_code: u32, 561 | } 562 | 563 | #[derive(Debug, Serialize)] 564 | struct POBox { 565 | po_box: u32, 566 | name: String, 567 | zip_code: u32, 568 | } 569 | 570 | #[derive(Debug, Serialize)] 571 | struct Pickup { 572 | per_pickup: bool, 573 | } 574 | 575 | #[test] 576 | fn validate_choice_example() { 577 | // This is an example from RFC8610 2.2.2 578 | // The only modification from the RFC example is to substitute "_" for "-" in barewords, 579 | // for compatibility with ciborium. 580 | let cddl_input = r#" 581 | address = { delivery } 582 | 583 | delivery = ( 584 | street: tstr, ? number: uint, city // 585 | po_box: uint, city // 586 | per_pickup: true ) 587 | 588 | city = ( 589 | name: tstr, zip_code: uint 590 | )"#; 591 | 592 | let input = POBox { 593 | po_box: 101, 594 | name: "San Francisco".to_string(), 595 | zip_code: 94103, 596 | }; 597 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 598 | validate_cbor_bytes("address", cddl_input, &cbor_bytes).unwrap(); 599 | 600 | let input = StreetNumber { 601 | street: "Eleventh St.".to_string(), 602 | number: 375, 603 | name: "San Francisco".to_string(), 604 | zip_code: 94103, 605 | }; 606 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 607 | validate_cbor_bytes("address", cddl_input, &cbor_bytes).unwrap(); 608 | 609 | let input = Pickup { per_pickup: true }; 610 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 611 | validate_cbor_bytes("address", cddl_input, &cbor_bytes).unwrap(); 612 | } 613 | 614 | #[test] 615 | fn validate_choiceify_example() { 616 | // This is an example from RFC8610 2.2.2 617 | // The only modification from the RFC example is to substitute "_" for "-" in barewords, 618 | // for compatibility with ciborium. 619 | let cddl_input = r#" 620 | terminal-color = &basecolors 621 | basecolors = ( 622 | black: 0, red: 1, green: 2, yellow: 3, 623 | blue: 4, magenta: 5, cyan: 6, white: 7, 624 | ) 625 | extended-color = &( 626 | basecolors, 627 | orange: 8, pink: 9, purple: 10, brown: 11, 628 | )"#; 629 | 630 | // This tests the & operator against a named rule 631 | validate_cbor_bytes("terminal-color", cddl_input, cbor::INT_1).unwrap(); 632 | validate_cbor_bytes("terminal-color", cddl_input, cbor::INT_23).err_mismatch(); 633 | 634 | // This tests the & operator against an inline group. 635 | validate_cbor_bytes("extended-color", cddl_input, cbor::INT_1).unwrap(); 636 | validate_cbor_bytes("extended-color", cddl_input, cbor::INT_9).unwrap(); 637 | validate_cbor_bytes("extended-color", cddl_input, cbor::INT_23).err_mismatch(); 638 | } 639 | 640 | #[test] 641 | fn test_fatal_propagation() { 642 | // Ensure that standalone choices can't conceal fatal errors. 643 | let cddl_input = r#"thing = (bad_rule / bool)"#; 644 | let err = validate_cbor_bytes("thing", cddl_input, cbor::BOOL_TRUE).unwrap_err(); 645 | assert_eq!(err.to_string(), "MissingRule(bad_rule)"); 646 | 647 | // Ensure that array choices can't conceal fatal errors. 648 | let cddl_input = r#"thing = [a: (bad_rule / tstr), b: int]"#; 649 | let input = PersonTuple("Alice".to_string(), 42); 650 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 651 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 652 | assert_eq!(err.to_string(), "MissingRule(bad_rule)"); 653 | 654 | // Ensure that map choices can't conceal fatal errors. 655 | let input = PersonStruct { 656 | name: "Bob".to_string(), 657 | age: 43, 658 | }; 659 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 660 | let cddl_input = r#"thing = {name: (bad_rule / tstr), age: int}"#; 661 | let err = validate_cbor_bytes("thing", cddl_input, &cbor_bytes).unwrap_err(); 662 | assert_eq!(err.to_string(), "MissingRule(bad_rule)"); 663 | } 664 | 665 | #[test] 666 | fn cbor_control_size() { 667 | let cddl_input = r#"thing = bstr .size 3"#; 668 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_EMPTY).unwrap(); 669 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_1234).err_mismatch(); 670 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_EMPTY).err_mismatch(); 671 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_CJK).err_mismatch(); 672 | 673 | let cddl_input = r#"thing = tstr .size 3"#; 674 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_EMPTY).unwrap(); 675 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_CJK).unwrap(); 676 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_IETF).err_mismatch(); 677 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_EMPTY).err_mismatch(); 678 | 679 | let cddl_input = r#"thing = uint .size 3"#; 680 | validate_cbor_bytes("thing", cddl_input, cbor::INT_0).unwrap(); 681 | validate_cbor_bytes("thing", cddl_input, cbor::INT_24).unwrap(); 682 | validate_cbor_bytes("thing", cddl_input, cbor::INT_1T).err_mismatch(); 683 | validate_cbor_bytes("thing", cddl_input, cbor::NINT_1000).err_mismatch(); 684 | validate_cbor_bytes("thing", cddl_input, cbor::TEXT_EMPTY).err_mismatch(); 685 | 686 | let cddl_input = r#"thing = uint .size 5"#; 687 | validate_cbor_bytes("thing", cddl_input, cbor::INT_1T).unwrap(); 688 | 689 | let cddl_input = r#"thing = uint .size 16"#; 690 | validate_cbor_bytes("thing", cddl_input, cbor::INT_1T).unwrap(); 691 | 692 | let cddl_input = r#"thing = uint .size 999999"#; 693 | validate_cbor_bytes("thing", cddl_input, cbor::INT_1T).unwrap(); 694 | 695 | let cddl_input = r#"thing = bstr .size 0.1"#; 696 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_EMPTY).unwrap_err(); 697 | 698 | let cddl_input = r#"thing = bstr .size -1"#; 699 | validate_cbor_bytes("thing", cddl_input, cbor::BYTES_EMPTY).unwrap_err(); 700 | } 701 | 702 | #[test] 703 | fn cbor_control_cbor() { 704 | let cddl_input = r#"thing = bytes .cbor uint"#; 705 | validate_cbor_bytes("thing", cddl_input, cbor::CBOR_INT_23).unwrap(); 706 | 707 | let cddl_input = r#" 708 | thing = bytes .cbor foo 709 | foo = uint 710 | "#; 711 | validate_cbor_bytes("thing", cddl_input, cbor::CBOR_INT_23).unwrap(); 712 | 713 | let cddl_input = r#" 714 | thing = bytes .cbor foo 715 | foo = t 716 | "#; 717 | validate_cbor_bytes("thing", cddl_input, cbor::CBOR_INT_23).unwrap(); 718 | } 719 | 720 | #[track_caller] 721 | fn validate_cbor_tstr(name: &str, cddl: &str, input: &str) -> ValidateResult { 722 | let cbor_bytes = cbor_to_vec(&input).unwrap(); 723 | validate_cbor_bytes(name, cddl, &cbor_bytes) 724 | } 725 | 726 | #[test] 727 | fn cbor_control_regexp() { 728 | // Should match strings that look like integers with no leading zeroes. 729 | let cddl_input = r#" nolz = tstr .regexp "^(0|[1-9][0-9]*)$" "#; 730 | validate_cbor_tstr("nolz", cddl_input, "0").unwrap(); 731 | validate_cbor_tstr("nolz", cddl_input, "1").unwrap(); 732 | validate_cbor_tstr("nolz", cddl_input, "20").unwrap(); 733 | validate_cbor_tstr("nolz", cddl_input, "23").unwrap(); 734 | validate_cbor_tstr("nolz", cddl_input, "123").unwrap(); 735 | validate_cbor_tstr("nolz", cddl_input, "01").err_mismatch(); 736 | validate_cbor_tstr("nolz", cddl_input, "0a").err_mismatch(); 737 | validate_cbor_tstr("nolz", cddl_input, "").err_mismatch(); 738 | 739 | // Any string that starts with "A" 740 | let cddl_input = r#" pat = tstr .regexp "^A" "#; 741 | validate_cbor_tstr("pat", cddl_input, "A").unwrap(); 742 | validate_cbor_tstr("pat", cddl_input, "ABC").unwrap(); 743 | validate_cbor_tstr("pat", cddl_input, "AAA").unwrap(); 744 | validate_cbor_tstr("pat", cddl_input, "ZA").err_mismatch(); 745 | validate_cbor_tstr("pat", cddl_input, "").err_mismatch(); 746 | 747 | // A string with "BB" anywhere inside. 748 | let cddl_input = r#" pat = tstr .regexp "BB" "#; 749 | validate_cbor_tstr("pat", cddl_input, "BB").unwrap(); 750 | validate_cbor_tstr("pat", cddl_input, "ABCBBA").unwrap(); 751 | validate_cbor_tstr("pat", cddl_input, "ABCBA").err_mismatch(); 752 | 753 | // bad target node type (bstr) 754 | let cddl_input = r#" pat = bstr .regexp "CCC" "#; 755 | validate_cbor_tstr("pat", cddl_input, "CCC").err_structural(); 756 | 757 | // bad argument node type (integer) 758 | let cddl_input = r#" pat = tstr .regexp 1234 "#; 759 | validate_cbor_tstr("pat", cddl_input, "1234").err_structural(); 760 | 761 | // This is an example from RFC8610 2.2.2 762 | let cddl_input = r#" nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+" "#; 763 | validate_cbor_tstr("nai", cddl_input, "N1@CH57HF.4Znqe0.dYJRN.igjf").unwrap(); 764 | } 765 | -------------------------------------------------------------------------------- /tests/json_cddl.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde_json")] 2 | 3 | use cddl_cat::json::validate_json_str; 4 | use cddl_cat::util::ErrorMatch; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[test] 8 | fn validate_json_null() { 9 | let cddl_input = r#"thing = nil"#; 10 | validate_json_str("thing", cddl_input, "null").unwrap(); 11 | validate_json_str("thing", cddl_input, "0").unwrap_err(); 12 | validate_json_str("thing", cddl_input, "false").unwrap_err(); 13 | } 14 | 15 | #[test] 16 | fn validate_json_bool() { 17 | let cddl_input = r#"thing = true"#; 18 | validate_json_str("thing", cddl_input, "true").unwrap(); 19 | validate_json_str("thing", cddl_input, "false").unwrap_err(); 20 | validate_json_str("thing", cddl_input, "null").unwrap_err(); 21 | } 22 | 23 | #[test] 24 | fn validate_json_float() { 25 | let cddl_input = r#"thing = 0.0"#; 26 | validate_json_str("thing", cddl_input, "0.0").unwrap(); 27 | validate_json_str("thing", cddl_input, "1.0").unwrap_err(); 28 | 29 | let cddl_input = r#"thing = float"#; 30 | validate_json_str("thing", cddl_input, "1.0").unwrap(); 31 | validate_json_str("thing", cddl_input, "1e5").unwrap(); 32 | validate_json_str("thing", cddl_input, "1e300").unwrap(); 33 | 34 | let cddl_input = r#"thing = float16"#; 35 | validate_json_str("thing", cddl_input, "1.0").unwrap(); 36 | 37 | // "Too small" floats should not cause a validation error. 38 | // JSON doesn't preserve the original size. 39 | let cddl_input = r#"thing = float32"#; 40 | validate_json_str("thing", cddl_input, "1.0").unwrap(); 41 | validate_json_str("thing", cddl_input, "1e5").unwrap(); 42 | 43 | let cddl_input = r#"thing = float64"#; 44 | validate_json_str("thing", cddl_input, "1.0").unwrap(); 45 | validate_json_str("thing", cddl_input, "1e300").unwrap(); 46 | 47 | // TODO: check that large floats don't validate against a smaller size. 48 | // We could try converting f64 to f23 and back to see if it changes. 49 | } 50 | 51 | #[test] 52 | fn validate_json_choice() { 53 | let cddl_input = r#"thing = 23 / 24"#; 54 | validate_json_str("thing", cddl_input, "23").unwrap(); 55 | validate_json_str("thing", cddl_input, "24").unwrap(); 56 | 57 | let cddl_input = r#"thing = (foo // bar) foo = (int / float) bar = tstr"#; 58 | validate_json_str("thing", cddl_input, "23").unwrap(); 59 | validate_json_str("thing", cddl_input, "1.0").unwrap(); 60 | validate_json_str("thing", cddl_input, r#""JSON""#).unwrap(); 61 | validate_json_str("thing", cddl_input, "true").unwrap_err(); 62 | } 63 | 64 | #[test] 65 | fn validate_json_integer() { 66 | let cddl_input = r#"thing = 1"#; 67 | validate_json_str("thing", cddl_input, "null").unwrap_err(); 68 | validate_json_str("thing", cddl_input, "1.0").unwrap_err(); 69 | validate_json_str("thing", cddl_input, "true").unwrap_err(); 70 | let cddl_input = r#"thing = int"#; 71 | validate_json_str("thing", cddl_input, "0").unwrap(); 72 | validate_json_str("thing", cddl_input, "24").unwrap(); 73 | validate_json_str("thing", cddl_input, "-1000").unwrap(); 74 | validate_json_str("thing", cddl_input, "1.0").unwrap_err(); 75 | let cddl_input = r#"thing = uint"#; 76 | validate_json_str("thing", cddl_input, "0").unwrap(); 77 | validate_json_str("thing", cddl_input, "24").unwrap(); 78 | validate_json_str("thing", cddl_input, "-1000").unwrap_err(); 79 | let cddl_input = r#"thing = nint"#; 80 | validate_json_str("thing", cddl_input, "-1000").unwrap(); 81 | validate_json_str("thing", cddl_input, "0").unwrap_err(); 82 | validate_json_str("thing", cddl_input, "24").unwrap_err(); 83 | } 84 | 85 | #[test] 86 | fn validate_json_textstring() { 87 | // "tstr" and "text" mean the same thing. 88 | for cddl_input in &[r#"thing = tstr"#, r#"thing = text"#] { 89 | validate_json_str("thing", cddl_input, r#""""#).unwrap(); 90 | validate_json_str("thing", cddl_input, r#""JSON""#).unwrap(); 91 | validate_json_str("thing", cddl_input, r#""水""#).unwrap(); 92 | } 93 | } 94 | 95 | #[test] 96 | fn validate_json_array() { 97 | let cddl_input = r#"thing = []"#; 98 | validate_json_str("thing", cddl_input, "[]").unwrap(); 99 | validate_json_str("thing", cddl_input, "null").unwrap_err(); 100 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 101 | 102 | let cddl_input = r#"thing = [1, 2, 3]"#; 103 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 104 | } 105 | 106 | // These data structures exist so that we can serialize some more complex 107 | // beyond the RFC examples. 108 | #[derive(Debug, Serialize, Deserialize)] 109 | struct PersonStruct { 110 | name: String, 111 | age: u32, 112 | } 113 | 114 | #[derive(Debug, Serialize, Deserialize)] 115 | struct PersonTuple(String, u32); 116 | 117 | #[derive(Debug, Serialize, Deserialize)] 118 | struct BackwardsTuple(u32, String); 119 | 120 | #[derive(Debug, Serialize, Deserialize)] 121 | struct LongTuple(String, u32, u32); 122 | 123 | #[derive(Debug, Serialize, Deserialize)] 124 | struct ShortTuple(String); 125 | 126 | #[derive(Debug, Serialize, Deserialize)] 127 | struct KitchenSink(String, u32, f64, bool); 128 | 129 | #[test] 130 | fn validate_json_homogenous_array() { 131 | let cddl_input = r#"thing = [* int]"#; // zero or more 132 | validate_json_str("thing", cddl_input, "[]").unwrap(); 133 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 134 | let cddl_input = r#"thing = [+ int]"#; // one or more 135 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 136 | validate_json_str("thing", cddl_input, "[]").unwrap_err(); 137 | let cddl_input = r#"thing = [? int]"#; // zero or one 138 | validate_json_str("thing", cddl_input, "[]").unwrap(); 139 | let json_str = serde_json::to_string(&[42]).unwrap(); 140 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 141 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 142 | 143 | let cddl_input = r#"thing = [* tstr]"#; 144 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 145 | 146 | // Alias type. Note the rule we want to validate must come first. 147 | let cddl_input = r#"thing = [* zipcode] zipcode = int"#; 148 | validate_json_str("thing", cddl_input, "[]").unwrap(); 149 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 150 | } 151 | 152 | #[test] 153 | fn validate_json_array_groups() { 154 | let cddl_input = r#"thing = [int, (int, int)]"#; 155 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 156 | 157 | let cddl_input = r#"thing = [(int, int, int)]"#; 158 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 159 | 160 | // Consume values in groups of one, an arbitrary number of times. 161 | let cddl_input = r#"thing = [* (int)]"#; 162 | validate_json_str("thing", cddl_input, "[]").unwrap(); 163 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 164 | 165 | // Consume values in groups of three, an arbitrary number of times. 166 | let cddl_input = r#"thing = [* (int, int, int)]"#; 167 | validate_json_str("thing", cddl_input, "[]").unwrap(); 168 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 169 | 170 | // Consume values in groups of two, an arbitrary number of times. 171 | let cddl_input = r#"thing = [* (int, int)]"#; 172 | validate_json_str("thing", cddl_input, "[]").unwrap(); 173 | // Shouldn't match because three doesn't go into two evenly. 174 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 175 | 176 | let cddl_input = r#"thing = [a: int, b: int, bar] bar = (c: int)"#; 177 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 178 | 179 | let cddl_input = r#"thing = [a: int, (bar)] bar = (b: int, c: int)"#; 180 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 181 | 182 | // This is incorrectly constructed, because this is a key-value with 183 | // a group name where the value should be. 184 | let cddl_input = r#"thing = [a: int, b: bar] bar = (b: int, c: int)"#; 185 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 186 | } 187 | 188 | #[test] 189 | fn validate_json_array_unwrap() { 190 | // unwrap something into the head of an array 191 | let cddl_input = r#"header = [a: int, b: int] thing = [~header c: int]"#; 192 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 193 | validate_json_str("thing", cddl_input, "[]").unwrap_err(); 194 | // unwrap something into the tail of an array 195 | let cddl_input = r#"footer = [a: int, b: int] thing = [c: int ~footer]"#; 196 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 197 | 198 | // unwrap something into the middle of an array 199 | let cddl_input = r#"middle = [int] thing = [a: int, ~middle, c: int]"#; 200 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 201 | 202 | // add an extra rule redirection while unwrapping 203 | let cddl_input = r#"foo = int middle = [foo] thing = [a: int, ~middle, c: int]"#; 204 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 205 | 206 | // Fail if we find too few items. 207 | let cddl_input = r#"header = [a: int] thing = [~header, c: int]"#; 208 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 209 | let cddl_input = r#"footer = [a: int] thing = [c: int, ~footer]"#; 210 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 211 | 212 | // Fail if we don't find enough matching items while unwrapping. 213 | let cddl_input = r#"footer = [a: int, b: int] thing = [c: int, d: int, ~footer]"#; 214 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 215 | 216 | // Fail if the unwrapped name doesn't resolve. 217 | let cddl_input = r#"thing = [c: int ~footer]"#; 218 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 219 | 220 | // Unwrapping a map into an array isn't allowed. 221 | let cddl_input = r#"header = {a: int, b: int} thing = [~header c: int]"#; 222 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 223 | } 224 | 225 | #[test] 226 | fn validate_json_array_record() { 227 | let cddl_input = r#"thing = [a: int, b: int, c: int]"#; 228 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 229 | validate_json_str("thing", cddl_input, "[]").unwrap_err(); 230 | 231 | let cddl_input = r#"thing = [a: int, b: int, c: foo] foo = int"#; 232 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 233 | 234 | let cddl_input = r#"thing = [int, int, int]"#; 235 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 236 | validate_json_str("thing", cddl_input, "[]").unwrap_err(); 237 | 238 | let cddl_input = r#"thing = [a: tstr, b: int]"#; 239 | 240 | let input = PersonTuple("Alice".to_string(), 42); 241 | let json_str = serde_json::to_string(&input).unwrap(); 242 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 243 | 244 | let input = BackwardsTuple(43, "Carol".to_string()); 245 | let json_str = serde_json::to_string(&input).unwrap(); 246 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 247 | 248 | let input = LongTuple("David".to_string(), 44, 45); 249 | let json_str = serde_json::to_string(&input).unwrap(); 250 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 251 | 252 | let input = ShortTuple("Eve".to_string()); 253 | let json_str = serde_json::to_string(&input).unwrap(); 254 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 255 | 256 | let cddl_input = r#"thing = [a: tstr, b: uint, c: float, d: bool]"#; 257 | 258 | let input = KitchenSink("xyz".to_string(), 17, 9.9, false); 259 | let json_str = serde_json::to_string(&input).unwrap(); 260 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 261 | 262 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 263 | } 264 | 265 | #[test] 266 | fn validate_json_map_unwrap() { 267 | let input = PersonStruct { 268 | name: "Bob".to_string(), 269 | age: 43, 270 | }; 271 | let json_str = serde_json::to_string(&input).unwrap(); 272 | let cddl_input = r#"thing = {name: tstr, ~agroup} agroup = {age: int}"#; 273 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 274 | 275 | // Unwrapping an array into a map isn't allowed. 276 | let cddl_input = r#"thing = {name: tstr, ~agroup} agroup = [age: int]"#; 277 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 278 | } 279 | 280 | #[test] 281 | fn validate_json_map_group() { 282 | let input = PersonStruct { 283 | name: "Bob".to_string(), 284 | age: 43, 285 | }; 286 | let json_str = serde_json::to_string(&input).unwrap(); 287 | let cddl_input = r#"thing = {name: tstr, agroup} agroup = (age: int)"#; 288 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 289 | 290 | let cddl_input = r#"thing = {agroup} agroup = (age: int, name: tstr)"#; 291 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 292 | 293 | let cddl_input = r#"thing = {((agroup))} agroup = (age: int, name: tstr)"#; 294 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 295 | 296 | let cddl_input = r#"thing = {agroup empty} agroup = (age: int, name: tstr) empty = ()"#; 297 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 298 | 299 | let cddl_input = 300 | r#"thing = {agroup maybe} agroup = (age: int, name: tstr) maybe = (? minor: bool)"#; 301 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 302 | 303 | let cddl_input = r#"thing = {name: tstr, agroup} agroup = (wrong: int)"#; 304 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 305 | 306 | let cddl_input = r#"thing = {name: tstr, agroup} agroup = (age: bool)"#; 307 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 308 | } 309 | 310 | #[test] 311 | fn validate_json_map() { 312 | let input = PersonStruct { 313 | name: "Bob".to_string(), 314 | age: 43, 315 | }; 316 | let json_str = serde_json::to_string(&input).unwrap(); 317 | let cddl_input = r#"thing = {name: tstr, age: int}"#; 318 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 319 | 320 | let cddl_input = r#"thing = {name: tstr, ? age: int}"#; 321 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 322 | 323 | // Ensure that keys are optional if the occurrence is "?" or "*" 324 | // and required if the occurrence is "+" 325 | let cddl_input = r#"thing = {name: tstr, age: int, ? minor: bool}"#; 326 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 327 | let cddl_input = r#"thing = {name: tstr, age: int, * minor: bool}"#; 328 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 329 | let cddl_input = r#"thing = {name: tstr, age: int, + minor: bool}"#; 330 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 331 | 332 | let cddl_input = r#"thing = {name: tstr, age: tstr}"#; 333 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 334 | 335 | let cddl_input = r#"thing = {name: tstr}"#; 336 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 337 | 338 | // "* keytype => valuetype" is the expected syntax for collecting 339 | // any remaining key/value pairs of the expected type. 340 | let cddl_input = r#"thing = {* tstr => any}"#; 341 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 342 | let cddl_input = r#"thing = {name: tstr, * tstr => any}"#; 343 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 344 | let cddl_input = r#"thing = {name: tstr, age: int, * tstr => any}"#; 345 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 346 | let cddl_input = r#"thing = {+ tstr => any}"#; 347 | validate_json_str("thing", cddl_input, &json_str).unwrap(); 348 | 349 | // Should fail because the JSON input has two entries that can't be 350 | // collected because the key type doesn't match. 351 | let cddl_input = r#"thing = {* int => any}"#; 352 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 353 | 354 | let cddl_input = r#"thing = {name: tstr, age: int, minor: bool}"#; 355 | validate_json_str("thing", cddl_input, &json_str).unwrap_err(); 356 | 357 | let cddl_input = r#"thing = {x: int, y: int, z: int}"#; 358 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap_err(); 359 | } 360 | 361 | #[test] 362 | fn validate_json_map_cut() { 363 | let json_str = r#"{ "foo": "not-an-int" }"#; 364 | 365 | // This uses non-cut semantics: the "foo" key matches, but because the value 366 | // doesn't match we allow "foo" to match the second member instead. 367 | let cddl = r#" 368 | thing = { 369 | ? "foo" => int, ; non-cut is the default for "=>" 370 | tstr => tstr, 371 | }"#; 372 | validate_json_str("thing", cddl, json_str).unwrap(); 373 | 374 | // This uses cut semantics: the "foo" key matches, but because the value 375 | // doesn't match we prevent it from matching any later rules. 376 | let cddl = r#" 377 | thing = { 378 | ? "foo" ^ => int, ; cut is indicated by "^" 379 | tstr => tstr, 380 | }"#; 381 | let err = validate_json_str("thing", cddl, json_str).unwrap_err(); 382 | assert_eq!(err.to_string(), "Mismatch(expected int)"); 383 | 384 | // Only "=>" can ever be non-cut. Members using ":" always get 385 | // cut semantics. 386 | let cddl = r#" 387 | thing = { 388 | ? "foo": int, ; cut is implied by ":" 389 | tstr => tstr, 390 | }"#; 391 | validate_json_str("thing", cddl, json_str).unwrap_err(); 392 | 393 | // Just a sanity check to ensure that non-cut matches work. 394 | let json_str = r#"{ "foo": 17, "bar": "baz" }"#; 395 | let cddl = r#" 396 | thing = { 397 | ? "foo" => int, 398 | tstr => tstr, 399 | }"#; 400 | validate_json_str("thing", cddl, json_str).unwrap(); 401 | 402 | // Same as the previous, but with the catch-all statement first. 403 | let cddl = r#" 404 | thing = { 405 | tstr => tstr, 406 | ? "foo" => int, 407 | }"#; 408 | validate_json_str("thing", cddl, json_str).unwrap(); 409 | 410 | // It's not really possible to enforce cut semantics on choices, because 411 | // in a map, choices between key-value pairs are represented as groups. 412 | // We want the "cut" to end at the group boundary, so that things like 413 | // this can work: 414 | // 415 | // palette_entry = (color: tstr, position: int) 416 | // rgba = (color: int, alpha: int) 417 | // { palette_entry // rgba } 418 | // Each map is unambiguous by itself; we shouldn't fail the second group 419 | // because the first group happened to use "color" to mean a different 420 | // thing. 421 | // 422 | // Make sure this decision sticks, at least until we change that policy. 423 | let cddl = r#"thing = { "foo": int // tstr => tstr }"#; 424 | let json_str = r#"{ "foo": "not-int" }"#; 425 | validate_json_str("thing", cddl, json_str).unwrap(); 426 | 427 | // This example should fail; the cut semantics should cause the non- 428 | // matching key-value pair to be ignored from further match consideration. 429 | // Depending on the order we inspect the map, we risk validating this JSON 430 | // because the "zzz" failure may only terminate the occurrence; we need to 431 | // ensure it fails the validation of the entire map. 432 | let json_str = r#"{ "aaa": 17, "zzz": "baz" }"#; 433 | let cddl = r#"thing = {* tstr ^ => int }"#; 434 | let err = validate_json_str("thing", cddl, json_str).unwrap_err(); 435 | assert_eq!(err.to_string(), "Mismatch(expected int)"); 436 | } 437 | 438 | #[derive(Debug, Serialize)] 439 | struct StreetNumber { 440 | street: String, 441 | number: u32, 442 | name: String, 443 | zip_code: u32, 444 | } 445 | 446 | #[derive(Debug, Serialize)] 447 | struct POBox { 448 | po_box: u32, 449 | name: String, 450 | zip_code: u32, 451 | } 452 | 453 | #[derive(Debug, Serialize)] 454 | struct Pickup { 455 | per_pickup: bool, 456 | } 457 | 458 | #[test] 459 | fn validate_choice_example() { 460 | // This is an example from RFC8610 2.2.2 461 | // The only modification from the RFC example is to substitute "_" for "-" in barewords, 462 | // for compatibility with serde_json. 463 | let cddl_input = r#" 464 | address = { delivery } 465 | 466 | delivery = ( 467 | street: tstr, ? number: uint, city // 468 | po_box: uint, city // 469 | per_pickup: true ) 470 | 471 | city = ( 472 | name: tstr, zip_code: uint 473 | )"#; 474 | 475 | let input = POBox { 476 | po_box: 101, 477 | name: "San Francisco".to_string(), 478 | zip_code: 94103, 479 | }; 480 | let json_str = serde_json::to_string(&input).unwrap(); 481 | validate_json_str("address", cddl_input, &json_str).unwrap(); 482 | 483 | let input = StreetNumber { 484 | street: "Eleventh St.".to_string(), 485 | number: 375, 486 | name: "San Francisco".to_string(), 487 | zip_code: 94103, 488 | }; 489 | let json_str = serde_json::to_string(&input).unwrap(); 490 | validate_json_str("address", cddl_input, &json_str).unwrap(); 491 | 492 | let json_str = r#"{ 493 | "street": "Eleventh St.", 494 | "name": "San Francisco", 495 | "zip_code": 94103 496 | }"#; 497 | validate_json_str("address", cddl_input, json_str).unwrap(); 498 | 499 | // missing zip_code 500 | let json_str = r#"{ 501 | "street": "Eleventh St.", 502 | "name": "San Francisco" 503 | }"#; 504 | validate_json_str("address", cddl_input, json_str).err_mismatch(); 505 | 506 | let input = Pickup { per_pickup: true }; 507 | let json_str = serde_json::to_string(&input).unwrap(); 508 | validate_json_str("address", cddl_input, &json_str).unwrap(); 509 | } 510 | 511 | #[test] 512 | fn json_generic_basic() { 513 | let cddl_input = r#"identity = T thing = identity"#; 514 | validate_json_str("thing", cddl_input, "0").unwrap(); 515 | validate_json_str("thing", cddl_input, r#""abc""#).err_mismatch(); 516 | validate_json_str("identity", cddl_input, "0").err_generic(); 517 | 518 | let cddl_input = r#"double = (T, T) thing = [int, double]"#; 519 | validate_json_str("thing", cddl_input, "[1, 2, 3]").unwrap(); 520 | validate_json_str("thing", cddl_input, "[1.0, 2, 3]").err_mismatch(); 521 | validate_json_str("thing", cddl_input, "[1, 2, 3.0]").err_mismatch(); 522 | 523 | let cddl_input = "message = [t, v] thing = message"; 524 | validate_json_str("thing", cddl_input, r#"["JSON", 123]"#).unwrap(); 525 | validate_json_str("thing", cddl_input, r#"[123, "JSON"]"#).err_mismatch(); 526 | 527 | let cddl_input = r#"identity = T thing = [identity<(int)>]"#; 528 | validate_json_str("thing", cddl_input, "[1]").unwrap(); 529 | 530 | let cddl_input = r#"identity = (T) thing = [identity<(int)>]"#; 531 | validate_json_str("thing", cddl_input, "[1]").unwrap(); 532 | 533 | let cddl_input = r#"double = (T, T) thing = [double<[int, int]>]"#; 534 | validate_json_str("thing", cddl_input, "[[1, 2], [3, 4]]").unwrap(); 535 | 536 | let cddl_input = r#"double = (T, T) identity = I thing = [double>]"#; 537 | validate_json_str("thing", cddl_input, "[1, 2]").unwrap(); 538 | } 539 | 540 | #[test] 541 | fn json_generic_map() { 542 | let cddl_input = "identity = T thing = {identity => identity}"; 543 | validate_json_str("thing", cddl_input, r#" { "abc": 0 } "#).unwrap(); 544 | 545 | // The "key:value" syntax only allows barewords or values; it doesn't allow 546 | // generic arguments. 547 | let cddl_input = "identity = T thing = {identity: identity}"; 548 | validate_json_str("thing", cddl_input, r#" { "abc": 0 } "#).err_parse(); 549 | } 550 | 551 | #[test] 552 | fn json_generic_occurrence() { 553 | let cddl_input = r#"one_or_more = (+ T) thing = [one_or_more]"#; 554 | validate_json_str("thing", cddl_input, "[4]").unwrap(); 555 | validate_json_str("thing", cddl_input, "[4, 5, 6]").unwrap(); 556 | validate_json_str("thing", cddl_input, "[]").err_mismatch(); 557 | 558 | let cddl_input = r#"three_to_five = (3*5 T) thing = [three_to_five]"#; 559 | validate_json_str("thing", cddl_input, r#"["one", "two"]"#).err_mismatch(); 560 | validate_json_str("thing", cddl_input, r#"["one", "two", "three"]"#).unwrap(); 561 | validate_json_str("thing", cddl_input, r#"["one", "two", "three", "four"]"#).unwrap(); 562 | validate_json_str("thing", cddl_input, r#"["1st","2nd","3rd","4th","5th"]"#).unwrap(); 563 | validate_json_str("thing", cddl_input, r#"["a","b","c","d","e","f"]"#).err_mismatch(); 564 | } 565 | 566 | #[test] 567 | fn json_generic_socket_example() { 568 | // TODO: use e.g. "bstr .size 4" once the .size control operator 569 | // is supported. 570 | let cddl_input = r#" 571 | port = uint 572 | socket_addr = (HOST, port) 573 | hostname = tstr 574 | ipv4_addr = [uint, uint, uint, uint] 575 | ipv4_host = hostname / ipv4_addr 576 | ipv4_socket = socket_addr 577 | sock_struct = { name: tstr, sock: [ipv4_socket]} 578 | "#; 579 | validate_json_str( 580 | "sock_struct", 581 | cddl_input, 582 | r#" { "name": "foo", "sock": ["foo.dev", 8080]} "#, 583 | ) 584 | .unwrap(); 585 | validate_json_str( 586 | "sock_struct", 587 | cddl_input, 588 | r#" { "name": "foo", "sock": [[10,0,0,1], 8080]} "#, 589 | ) 590 | .unwrap(); 591 | } 592 | 593 | #[test] 594 | fn json_generic_name_overlap() { 595 | let cddl_input = r#" 596 | IP = [uint, uint, uint, uint] 597 | PORT = uint 598 | socket = [IP, PORT] 599 | name = tstr 600 | conn = [name, socket, IP] ; This is the generic parameter named IP 601 | iptype = "v4" / "v6" 602 | thing = conn ; This is the rule named IP 603 | "#; 604 | validate_json_str("thing", cddl_input, r#"["foo", [[10,0,0,1], 8080], "v4"]"#).unwrap(); 605 | } 606 | 607 | #[test] 608 | fn json_generic_nested() { 609 | let cddl_input = r#" 610 | double = (X, X) 611 | triple = (Y, Y, Y) 612 | sextuple = double> 613 | thing = [sextuple] 614 | "#; 615 | validate_json_str("thing", cddl_input, "[1, 2, 3, 4, 5, 6]").unwrap(); 616 | validate_json_str("thing", cddl_input, "[[1, 2, 3], [4, 5, 6]]").err_mismatch(); 617 | validate_json_str("thing", cddl_input, "[1, 2, 3, 4, 5]").err_mismatch(); 618 | validate_json_str("thing", cddl_input, "[1, 2, 3, 4, 5, 6, 7]").err_mismatch(); 619 | 620 | // Check to see if we get confused when the same generic parameter name 621 | // gets used across multiple levels. 622 | let cddl_input = r#" 623 | double = (T, T) 624 | triple = (T, T, T) 625 | sextuple = double> 626 | thing = [sextuple] 627 | "#; 628 | validate_json_str("thing", cddl_input, "[1, 2, 3, 4, 5, 6]").unwrap(); 629 | 630 | // Verify that we can't access generic names in places we shouldn't. 631 | let cddl_input = r#" 632 | double = (X, X) 633 | triple = (X, Y, Y) 634 | sextuple = double> 635 | thing = [sextuple] 636 | "#; 637 | validate_json_str("thing", cddl_input, "[1, 2, 3, 4, 5, 6]").err_missing_rule(); 638 | 639 | let cddl_input = r#" 640 | double = (X, X) 641 | triple = (Z, Y, Y) 642 | sextuple = double> 643 | thing = [sextuple] 644 | "#; 645 | validate_json_str("thing", cddl_input, "[1, 2, 3, 4, 5, 6]").err_missing_rule(); 646 | } 647 | 648 | #[test] 649 | fn json_generic_rfc8610() { 650 | // Generic examples from RFC8610 651 | let cddl_input = r#" 652 | messages = message<"reboot", "now"> / message<"sleep", 1..100> 653 | message = {type: t, value: v} 654 | "#; 655 | validate_json_str("messages", cddl_input, r#"{"type":"reboot","value":"now"}"#).unwrap(); 656 | validate_json_str("messages", cddl_input, r#"{"type":"sleep", "value":15}"#).unwrap(); 657 | validate_json_str("messages", cddl_input, r#"{"type":"no", "value": "now"}"#).err_mismatch(); 658 | validate_json_str("messages", cddl_input, r#"{"type":"sleep", "value":150}"#).err_mismatch(); 659 | } 660 | 661 | #[test] 662 | fn json_generic_malformed() { 663 | // Obviously bad rule lookup 664 | let cddl_input = r#"double = (T, T) thing = [T, double]"#; 665 | validate_json_str("thing", cddl_input, "[1, 2, 3]").err_missing_rule(); 666 | 667 | // Missing parameters 668 | let cddl_input = r#"double = (T, T) thing = [double]"#; 669 | validate_json_str("thing", cddl_input, "[1, 2]").err_generic(); 670 | 671 | // Wrong number of parameters 672 | let cddl_input = r#"pair = (T, U) thing = [pair]"#; 673 | validate_json_str("thing", cddl_input, "[1, 2]").err_generic(); 674 | let cddl_input = r#"pair = (T, U) thing = [pair]"#; 675 | validate_json_str("thing", cddl_input, "[1, 2]").err_generic(); 676 | } 677 | 678 | #[test] 679 | fn json_control_size() { 680 | let cddl_input = r#"thing = uint .size 3"#; 681 | validate_json_str("thing", cddl_input, "0").unwrap(); 682 | validate_json_str("thing", cddl_input, "256").unwrap(); 683 | validate_json_str("thing", cddl_input, "16777215").unwrap(); 684 | validate_json_str("thing", cddl_input, "16777216").err_mismatch(); 685 | validate_json_str("thing", cddl_input, "-256").err_mismatch(); 686 | 687 | // indirection 688 | let cddl_input = r#"limit = 3 numb = uint thing = numb .size limit"#; 689 | validate_json_str("thing", cddl_input, "0").unwrap(); 690 | validate_json_str("thing", cddl_input, "256").unwrap(); 691 | validate_json_str("thing", cddl_input, "16777215").unwrap(); 692 | validate_json_str("thing", cddl_input, "16777216").err_mismatch(); 693 | validate_json_str("thing", cddl_input, "-256").err_mismatch(); 694 | 695 | let cddl_input = r#"thing = tstr .size 10"#; 696 | validate_json_str("thing", cddl_input, r#""""#).unwrap(); 697 | validate_json_str("thing", cddl_input, r#""JSON""#).unwrap(); 698 | validate_json_str("thing", cddl_input, r#""水""#).unwrap(); 699 | validate_json_str("thing", cddl_input, r#""水水水水""#).err_mismatch(); 700 | validate_json_str("thing", cddl_input, r#""abcdefghij""#).unwrap(); 701 | validate_json_str("thing", cddl_input, r#""abcdefghijk""#).err_mismatch(); 702 | 703 | // .size is not allowed on signed integers. 704 | let cddl_input = r#"thing = int .size 3"#; 705 | validate_json_str("thing", cddl_input, "0").unwrap_err(); 706 | 707 | // bad target node type 708 | let cddl_input = r#"thing = [uint] .size 3"#; 709 | validate_json_str("thing", cddl_input, "0").unwrap_err(); 710 | 711 | // bad argument node type 712 | let cddl_input = r#"thing = uint .size 0.1"#; 713 | validate_json_str("thing", cddl_input, "0").unwrap_err(); 714 | } 715 | 716 | #[test] 717 | fn json_control_regexp() { 718 | // Should match strings that look like integers with no leading zeroes. 719 | let cddl_input = r#" nolz = tstr .regexp "^(0|[1-9][0-9]*)$" "#; 720 | validate_json_str("nolz", cddl_input, r#" "0" "#).unwrap(); 721 | validate_json_str("nolz", cddl_input, r#" "1" "#).unwrap(); 722 | validate_json_str("nolz", cddl_input, r#" "20" "#).unwrap(); 723 | validate_json_str("nolz", cddl_input, r#" "23" "#).unwrap(); 724 | validate_json_str("nolz", cddl_input, r#" "123" "#).unwrap(); 725 | validate_json_str("nolz", cddl_input, r#" "01" "#).err_mismatch(); 726 | validate_json_str("nolz", cddl_input, r#" "0a" "#).err_mismatch(); 727 | validate_json_str("nolz", cddl_input, r#" "" "#).err_mismatch(); 728 | 729 | // Any string that starts with "A" 730 | let cddl_input = r#" pat = tstr .regexp "^A" "#; 731 | validate_json_str("pat", cddl_input, r#" "A" "#).unwrap(); 732 | validate_json_str("pat", cddl_input, r#" "ABC" "#).unwrap(); 733 | validate_json_str("pat", cddl_input, r#" "AAA" "#).unwrap(); 734 | validate_json_str("pat", cddl_input, r#" "ZA" "#).err_mismatch(); 735 | validate_json_str("pat", cddl_input, r#" "" "#).err_mismatch(); 736 | 737 | // A string with "BB" anywhere inside. 738 | let cddl_input = r#" pat = tstr .regexp "BB" "#; 739 | validate_json_str("pat", cddl_input, r#" "BB" "#).unwrap(); 740 | validate_json_str("pat", cddl_input, r#" "ABCBBA" "#).unwrap(); 741 | validate_json_str("pat", cddl_input, r#" "ABCBA" "#).err_mismatch(); 742 | 743 | // bad target node type (bstr) 744 | let cddl_input = r#" pat = bstr .regexp "CCC" "#; 745 | validate_json_str("pat", cddl_input, r#" "CCC" "#).err_structural(); 746 | 747 | // bad argument node type (integer) 748 | let cddl_input = r#" pat = tstr .regexp 1234 "#; 749 | validate_json_str("pat", cddl_input, r#" "1234" "#).err_structural(); 750 | 751 | // This is an example from RFC8610 2.2.2 752 | let cddl_input = r#" nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+" "#; 753 | validate_json_str("nai", cddl_input, r#""N1@CH57HF.4Znqe0.dYJRN.igjf""#).unwrap(); 754 | } 755 | 756 | #[test] 757 | fn json_infinite_recursion() { 758 | let cddl_input = r#"thing1 = thing2 thing2 = thing1"#; 759 | validate_json_str("thing1", cddl_input, "0").unwrap_err(); 760 | } 761 | 762 | #[test] 763 | fn json_choiceify_map() { 764 | let cddl_input = r#" 765 | person = { name: tstr, age: uint, &extra } 766 | extra = ( aaa: bbb, ccc: ddd ) 767 | aaa = ( one: tstr ) 768 | bbb = ( two: tstr ) 769 | ccc = ( three: tstr ) 770 | ddd = ( four: tstr ) 771 | "#; 772 | 773 | // Group-into-choice discards the key and only uses the value. 774 | // So the keys "one" and "three" are ignored. 775 | // The values "two" and "four" must refer to a group to make 776 | // sense in a map context. 777 | let json = r#"{"name": "Alice", "age": 33 }"#; 778 | validate_json_str("person", cddl_input, json).err_mismatch(); 779 | let json = r#"{"name": "Alice", "age": 33, "one": "X" }"#; 780 | validate_json_str("person", cddl_input, json).err_mismatch(); 781 | let json = r#"{"name": "Alice", "age": 33, "two": "X" }"#; 782 | validate_json_str("person", cddl_input, json).unwrap(); 783 | let json = r#"{"name": "Alice", "age": 33, "three": "X" }"#; 784 | validate_json_str("person", cddl_input, json).err_mismatch(); 785 | let json = r#"{"name": "Alice", "age": 33, "four": "X" }"#; 786 | validate_json_str("person", cddl_input, json).unwrap(); 787 | let json = r#"{"name": "Alice", "four": "X" }"#; 788 | validate_json_str("person", cddl_input, json).err_mismatch(); 789 | 790 | // With an extra level of name indirection 791 | let cddl_input = r#" 792 | person = { name: tstr, age: uint, &extra1 } 793 | extra1 = extra2 794 | extra2 = ( aaa: bbb, ccc: ddd ) 795 | aaa = ( one: tstr ) 796 | bbb = ( two: tstr ) 797 | ccc = ( three: tstr ) 798 | ddd = ( four: tstr ) 799 | "#; 800 | let json = r#"{"name": "Alice", "age": 33, "one": "X" }"#; 801 | validate_json_str("person", cddl_input, json).err_mismatch(); 802 | let json = r#"{"name": "Alice", "age": 33, "two": "X" }"#; 803 | validate_json_str("person", cddl_input, json).unwrap(); 804 | let json = r#"{"name": "Alice", "age": 33, "three": "X" }"#; 805 | validate_json_str("person", cddl_input, json).err_mismatch(); 806 | let json = r#"{"name": "Alice", "age": 33, "four": "X" }"#; 807 | validate_json_str("person", cddl_input, json).unwrap(); 808 | 809 | // FIXME: this doesn't work correctly 810 | if false { 811 | // Testing the inline choiceify 812 | let cddl_input = r#" 813 | person = { name: tstr, age: uint, &(aaa: bbb, ccc: ddd) } 814 | aaa = ( one: tstr ) 815 | bbb = ( two: tstr ) 816 | ccc = ( three: tstr ) 817 | ddd = ( four: tstr ) 818 | "#; 819 | let json = r#"{"name": "Alice", "age": 33 }"#; 820 | validate_json_str("person", cddl_input, json).err_mismatch(); 821 | let json = r#"{"name": "Alice", "age": 33, "one": "X" }"#; 822 | validate_json_str("person", cddl_input, json).err_mismatch(); 823 | let json = r#"{"name": "Alice", "age": 33, "two": "X" }"#; 824 | validate_json_str("person", cddl_input, json).unwrap(); 825 | let json = r#"{"name": "Alice", "age": 33, "three": "X" }"#; 826 | validate_json_str("person", cddl_input, json).err_mismatch(); 827 | let json = r#"{"name": "Alice", "age": 33, "four": "X" }"#; 828 | validate_json_str("person", cddl_input, json).unwrap(); 829 | let json = r#"{"name": "Alice", "four": "X" }"#; 830 | validate_json_str("person", cddl_input, json).err_mismatch(); 831 | } 832 | 833 | // A group should be able to include another group by name. 834 | // FIXME: Also try this without the key "thing1", since it's ignored anyway. 835 | let cddl_input = r#" 836 | map = { top: tstr, &groupa } 837 | groupa = ( thing1: groupb ) 838 | groupb = ( choice1: uint ) 839 | "#; 840 | let json = r#"{ "top": "yes", "choice1": 99 }"#; 841 | validate_json_str("map", cddl_input, json).unwrap(); 842 | 843 | // FIXME: this doesn't work when "groupb" is a value without a key. 844 | if false { 845 | let cddl_input = r#" 846 | map = { top: tstr, &groupa } 847 | groupa = ( groupb ) 848 | groupb = ( choice1: uint ) 849 | "#; 850 | let json = r#"{ "top": "yes", "choice1": 99 }"#; 851 | validate_json_str("map", cddl_input, json).unwrap(); 852 | } 853 | 854 | // FIXME: this doesn't work, due to the "groupb" addition. 855 | // I think it's because map validation doesn't correctly handle groups 856 | // referring to other groups by name 857 | if false { 858 | let cddl_input = r#" 859 | map = { top: tstr, &groupa } 860 | groupa = ( thing1: groupb ) 861 | groupb = ( choice1: uint, groupc ) 862 | groupc = ( choice2: uint ) 863 | "#; 864 | let json = r#"{ "top": "yes", "choice1": 99 }"#; 865 | validate_json_str("map", cddl_input, json).unwrap(); 866 | } 867 | 868 | // Trying to choiceify something that's not a group 869 | let cddl_input = r#" 870 | map = { &oops } 871 | oops = { one: 1, two: 2 } 872 | "#; 873 | let json = r#"{ "test": 99 }"#; 874 | validate_json_str("map", cddl_input, json).err_structural(); 875 | } 876 | 877 | #[test] 878 | fn json_choiceify_array() { 879 | let cddl_input = r#" 880 | triple = [ tstr, bool, &primes ] 881 | primes = (2, 3, 5, more_primes) 882 | more_primes = (7, 11) 883 | "#; 884 | 885 | // Group-into-choice discards the key and only uses the value. 886 | // So the keys "one" and "three" are ignored. 887 | // The values "two" and "four" must refer to a group to make 888 | // sense in a map context. 889 | let json = r#"[ "foo", true, 2 ]"#; 890 | validate_json_str("triple", cddl_input, json).unwrap(); 891 | let json = r#"[ "foo", true, 11 ]"#; 892 | validate_json_str("triple", cddl_input, json).unwrap(); 893 | let json = r#"[ "foo", true ]"#; 894 | validate_json_str("triple", cddl_input, json).err_mismatch(); 895 | let json = r#"[ "foo", true, 4 ]"#; 896 | validate_json_str("triple", cddl_input, json).err_mismatch(); 897 | 898 | // Multiple layers of name redirection 899 | let cddl_input = r#" 900 | array = [ &primes ] 901 | primes = more_primes 902 | more_primes = (2, 3, 5) 903 | "#; 904 | let json = r#"[ 2 ]"#; 905 | validate_json_str("array", cddl_input, json).unwrap(); 906 | 907 | let json = r#"[ 4 ]"#; 908 | validate_json_str("array", cddl_input, json).err_mismatch(); 909 | 910 | // Trying to choiceify something that's not a group 911 | let cddl_input = r#" 912 | array = [ &oops ] 913 | oops = [2, 3, 5] 914 | "#; 915 | let json = r#"[ 2 ]"#; 916 | validate_json_str("array", cddl_input, json).err_structural(); 917 | } 918 | --------------------------------------------------------------------------------