use json_pointer::JsonPointer;
use uuid::Uuid;
use crate::{Error, Result};
use super::{DataType, DeviceClass, E173Document, Library, System};
type DynJsonPointer<'a> = JsonPointer<&'a str, Vec<&'a str>>;
pub fn validate_doc(document: &E173Document) -> Result<()> {
let mut pointer = JsonPointer::new(vec!["e173doc", "libraries"]);
for (qualified_id, version_collection) in &document.e173doc.libraries {
pointer.push(qualified_id);
validate_qualified_id(qualified_id, QualifiedIdType::Lib, &pointer)?;
for (version, library) in version_collection {
pointer.push(version);
validate_version(version, &pointer)?;
validate_library(library, &mut pointer)?;
pointer.pop();
}
pointer.pop();
}
let mut pointer = JsonPointer::new(vec!["e173doc", "deviceClasses"]);
for (qualified_id, version_collection) in &document.e173doc.device_classes {
pointer.push(qualified_id);
validate_qualified_id(qualified_id, QualifiedIdType::Dev, &pointer)?;
for (version, device_class) in version_collection {
pointer.push(version);
validate_version(version, &pointer)?;
validate_device_class_helper(device_class, &pointer)?;
pointer.pop();
}
pointer.pop();
}
for (qualified_id, system) in &document.e173doc.systems {
pointer.push(qualified_id);
validate_qualified_id(qualified_id, QualifiedIdType::Sys, &pointer)?;
validate_system(system, &pointer)?;
pointer.pop();
}
Ok(())
}
pub fn validate_device_class(device_class: &DeviceClass) -> Result<()> {
let pointer = JsonPointer::new(vec![]);
validate_device_class_helper(device_class, &pointer)
}
fn validate_device_class_helper(
_device_class: &DeviceClass,
_pointer: &DynJsonPointer<'_>,
) -> Result<()> {
Ok(())
}
fn validate_library<'a>(library: &'a Library, pointer: &mut DynJsonPointer<'a>) -> Result<()> {
for (key, param_class) in &library.parameter_classes {
pointer.push(key);
if param_class.data_type == DataType::Number && param_class.unit.is_none() {
return Err(Error::invalid_library(
"A Parameter Class with data type 'number' must have a unit",
pointer.to_string(),
));
}
if param_class.data_type != DataType::Number && param_class.unit.is_some() {
return Err(Error::invalid_library(
"A Parameter Class with a data type other than 'number' may not have a unit",
pointer.to_string(),
));
}
}
Ok(())
}
fn validate_system<'a>(_system: &'a System, _pointer: &DynJsonPointer<'a>) -> Result<()> {
Ok(())
}
enum QualifiedIdType {
Lib,
Dev,
Sys,
}
impl QualifiedIdType {
fn as_str(&self) -> &str {
match self {
Self::Lib => "lib",
Self::Dev => "dev",
Self::Sys => "sys",
}
}
fn as_descriptive_str(&self) -> &str {
match self {
Self::Lib => "library",
Self::Dev => "device class",
Self::Sys => "system",
}
}
}
#[allow(dead_code)]
enum ValidationContext {
Document,
Library,
DeviceClass,
System,
}
impl ValidationContext {
fn as_error(&self, description: impl Into<String>, pointer: &DynJsonPointer) -> Error {
match self {
Self::Document => Error::invalid_document(description, pointer.to_string()),
Self::Library => Error::invalid_library(description, pointer.to_string()),
Self::DeviceClass => Error::invalid_device_class(description, pointer.to_string()),
Self::System => Error::invalid_system(description, pointer.to_string()),
}
}
fn as_err_result<T>(
&self,
description: impl Into<String>,
pointer: &DynJsonPointer,
) -> Result<T> {
Err(self.as_error(description, pointer))
}
}
fn validate_identifier(
id: &str,
context: ValidationContext,
pointer: &DynJsonPointer<'_>,
) -> Result<()> {
if id.is_empty() {
return context.as_err_result("Identifiers must have at least one character", pointer);
}
let invalid_char = id.chars().find(|ch| {
let ch = *ch as u32;
ch < 0x25
|| (ch > 0x2d && ch < 0x30)
|| (ch > 0x39 && ch < 0x3c)
|| (ch > 0x3e && ch < 0x41)
|| (ch > 0x5a && ch < 0x5e)
|| (ch == 0x7f)
|| (ch > 0xd7ff && ch < 0xe000)
|| (ch > 0x10fff)
});
if let Some(ch) = invalid_char {
context.as_err_result(
format!("Invalid character '{ch}' ({:#x}) in identifier", ch as u32),
pointer,
)
} else {
Ok(())
}
}
macro_rules! invalid_doc {
($desc:expr, $ptr: expr) => {
Err(Error::invalid_document($desc, $ptr.to_string()))
};
}
fn validate_version(version: &str, pointer: &DynJsonPointer) -> Result<()> {
let mut parts = version.split('.');
validate_version_part(&mut parts, pointer)?;
validate_version_part(&mut parts, pointer)?;
validate_version_part(&mut parts, pointer)?;
if parts.next().is_none() {
Ok(())
} else {
invalid_doc!("Version numbers must have 3 components", pointer)
}
}
fn validate_org_id(org_id: &str, pointer: &DynJsonPointer) -> Result<()> {
if org_id.is_empty() {
invalid_doc!("Organization Identifier cannot be empty", pointer)
} else if org_id.starts_with("org.esta.e173.user.") {
let mut components = org_id.split('.');
let Some(uuid_segment) = components.nth(4) else {
return invalid_doc!(
"Qualified Identifiers beginning with 'org.esta.e173.user' must be a properly-formatted End User Identifier",
pointer
);
};
if uuid_segment.len() != 36 || Uuid::parse_str(uuid_segment).is_err() {
return invalid_doc!(
"End User Identifiers must have a properly-formatted UUID",
pointer
);
}
if components.next().is_none() {
Ok(())
} else {
invalid_doc!(
"End User Identifiers must not have any components after the UUID component",
pointer
)
}
} else {
let mut chars = org_id.chars();
while let Some(ch) = chars.next() {
let valid_nonalnum_set = "-._~!$&'()*+,;=";
if ch == '%' {
let err_msg = "Invalid percent-encoding in Organization Identifier";
let Some(d1) = chars.next() else {
return invalid_doc!(err_msg, pointer);
};
let Some(d2) = chars.next() else {
return invalid_doc!(err_msg, pointer);
};
let is_upper_hexdig = |ch: char| ch.is_ascii_digit() || matches!(ch, 'A'..='F');
if !is_upper_hexdig(d1) || !is_upper_hexdig(d2) {
return invalid_doc!(err_msg, pointer);
}
} else if !ch.is_ascii_alphanumeric() && !valid_nonalnum_set.contains(ch) {
return invalid_doc!(
format!("Invalid character '{ch}' in Organization Identifier"),
pointer
);
}
}
Ok(())
}
}
fn validate_qualified_id(id: &str, ty: QualifiedIdType, pointer: &DynJsonPointer) -> Result<()> {
let num_components = id.split('.').count();
if num_components < 3 {
return invalid_doc!(
"A Qualified Identifier must have at least 3 components",
pointer
);
}
let org_id = &id[..id
.match_indices('.')
.map(|(idx, _)| idx)
.nth(num_components - 3)
.expect("String must have num_components - 2 dots in it")];
validate_org_id(org_id, pointer)?;
let mut id_minus_org_id = id.split('.').skip(num_components - 2);
let id_type = id_minus_org_id
.next()
.expect("Iterator verified to have num_components components");
if id_type != ty.as_str() {
return invalid_doc!(
format!(
"A Qualified Identifier for a {} must have the component '{}'",
ty.as_descriptive_str(),
ty.as_str()
),
pointer
);
}
let identifier = id_minus_org_id
.next()
.expect("Iterator verified to have num_components components");
validate_identifier(identifier, ValidationContext::Document, pointer)
}
fn validate_version_part<'s>(
parts: &mut impl Iterator<Item = &'s str>,
pointer: &DynJsonPointer,
) -> Result<()> {
let part = parts.next().ok_or(Error::invalid_document(
"Version numbers must have 3 components",
pointer.to_string(),
))?;
if part.starts_with('0') && part.len() > 1 {
return invalid_doc!(format!("Invalid version number component {part}"), pointer);
}
let _: u64 = part.parse().map_err(|_| {
Error::invalid_document(
format!("Invalid version number component {part}"),
pointer.to_string(),
)
})?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
fn assert_err_with_pointer(err: Result<()>, expected_pointer: &DynJsonPointer, input: &str) {
let Err(err) = err else {
panic!("Expected error for input {input}");
};
let pointer = match err {
Error::InvalidDocument { json_path, .. } => json_path,
Error::InvalidLibrary { json_path, .. } => json_path,
Error::InvalidDeviceClass { json_path, .. } => json_path,
Error::InvalidSystem { json_path, .. } => json_path,
_ => panic!("Invalid error type {err}"),
};
assert_eq!(pointer, expected_pointer.to_string());
}
fn assert_err_with_validation_context(
err: Result<()>,
validation_context: ValidationContext,
expected_pointer: &DynJsonPointer,
) {
let err = err.unwrap_err();
match validation_context {
ValidationContext::Document => assert!(
matches!(err, Error::InvalidDocument { json_path, .. } if json_path == expected_pointer.to_string())
),
ValidationContext::Library => assert!(
matches!(err, Error::InvalidLibrary { json_path, .. } if json_path == expected_pointer.to_string())
),
ValidationContext::DeviceClass => assert!(
matches!(err, Error::InvalidDeviceClass { json_path, .. } if json_path == expected_pointer.to_string())
),
ValidationContext::System => assert!(
matches!(err, Error::InvalidSystem { json_path, .. } if json_path == expected_pointer.to_string())
),
}
}
#[test]
fn test_valid_qualified_ids_pass() {
let pointer = JsonPointer::new(vec![]);
macro_rules! test_id {
($reg_id:literal, $id:literal) => {
assert!(validate_qualified_id(
concat!($reg_id, ".lib.", $id),
QualifiedIdType::Lib,
&pointer
)
.is_ok());
assert!(validate_qualified_id(
concat!($reg_id, ".dev.", $id),
QualifiedIdType::Dev,
&pointer
)
.is_ok());
assert!(validate_qualified_id(
concat!($reg_id, ".sys.", $id),
QualifiedIdType::Sys,
&pointer
)
.is_ok())
};
}
test_id!("com", "test"); test_id!("org.esta", "test"); test_id!("com.example.sub.sub2.sub3", "test"); test_id!("org.123abc!$&'()*+,;=.034-._~.esta", "test"); test_id!("org.esta", "%&'()*+,-0123456789<=>ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~⟄⦿⊛⬪❦☰∭⁶⡖➧"); test_id!("org.esta.e173.usernotreally", "test");
test_id!(
"org.%E2%BB%96%E2%8A%BC%E2%9D%97%E2%A8%95%E2%8D%AF%E2%AE%8B%E2.esta",
"test"
); test_id!(
"org.esta.e173.user.3005457d-b41c-403a-8a1d-1409173bf2f6",
"test"
); test_id!(
"org.esta.e173.user.3005457D-B41C-403A-8A1D-1409173BF2F6",
"test"
); }
#[test]
fn test_invalid_qualified_ids_fail() {
let pointer = JsonPointer::new(vec!["foo", "bar"]);
macro_rules! test_id {
($id:expr) => {
assert_err_with_pointer(
validate_qualified_id($id, QualifiedIdType::Lib, &pointer),
&pointer,
$id,
);
assert_err_with_pointer(
validate_qualified_id($id, QualifiedIdType::Dev, &pointer),
&pointer,
$id,
);
assert_err_with_pointer(
validate_qualified_id($id, QualifiedIdType::Sys, &pointer),
&pointer,
$id,
)
};
($id:expr, $id_type:expr) => {
assert_err_with_pointer(
validate_qualified_id($id, $id_type, &pointer),
&pointer,
$id,
);
};
}
test_id!("org.esta.lib.abcd", QualifiedIdType::Dev);
test_id!("org.esta.lib.abcd", QualifiedIdType::Sys);
test_id!("");
test_id!("com");
test_id!("com.example");
test_id!("com.example.something");
test_id!("com.example.something.somethingelse");
test_id!("com.example.lib.two.segments", QualifiedIdType::Lib);
test_id!("com.exam%a8ple.something");
test_id!("com.exam%.something");
test_id!("com.exam%8.something");
test_id!(".lib.abcd");
test_id!("com.example.lib.");
let invalid_chars_regid = ":/?#[]@ ⽖↝⩮⋮⪲┰≏Ⲑ≿";
for ch in invalid_chars_regid.chars() {
let id = format!("org.abc{ch}def.lib.something");
test_id!(&id);
}
test_id!("com.example.lib.abc[de]f/g", QualifiedIdType::Lib);
test_id!("org.esta.e173.user");
test_id!("org.esta.e173.user.3005457d-b41c-403a-8a1d-1409173bf2f6");
test_id!("org.esta.e173.user.notauuid.lib.test", QualifiedIdType::Lib);
test_id!(
"org.esta.e173.user.804078eea64b43949a1646f1e1e611ab.lib.test",
QualifiedIdType::Lib
);
test_id!(
"org.esta.e173.user.3005457d-b41c-403a-8a1d-1409173bf2f6.lib.two.segments",
QualifiedIdType::Lib
);
}
#[test]
fn test_valid_identifiers_pass() {
let pointer = JsonPointer::new(vec![]);
macro_rules! test_id {
($id:expr) => {
assert!(validate_identifier($id, ValidationContext::Document, &pointer).is_ok())
};
}
test_id!("valid-id");
test_id!("%&'()*+,-0123456789<=>^_`{|}~");
test_id!("♎⫀≍═◄ⵝ❲⇯⟝♡◤⣾⭎⥒ⶻ⣞Åↄℭ⟄⦿⊛⬪❦☰∭⁶⡖➧⡌⨬⮌⍺▘ ₁┧❶┦⢂ⶆⅰ⨀➲⽀⳯⮷⃗↽⇏⭹⻲⊜❯ↆ␡╱⨣▮╮Ⱬⷖ⒄ⷵ⠷⚺⮍⛅≻⯪⾟┲⛪⸅ⷍ⎠⊴⋡⤻⮢₸⚆ⴈ⥋⒓ₓ⡢⌤⏺⻓⺁⢷∹⾒ⴜ⨚")
}
#[test]
fn test_invalid_identifiers_fail() {
let pointer = JsonPointer::new(vec!["foo", "bar"]);
macro_rules! test_id {
($id:expr) => {
assert_err_with_validation_context(
validate_identifier($id, ValidationContext::Document, &pointer),
ValidationContext::Document,
&pointer,
);
assert_err_with_validation_context(
validate_identifier($id, ValidationContext::Library, &pointer),
ValidationContext::Library,
&pointer,
);
assert_err_with_validation_context(
validate_identifier($id, ValidationContext::DeviceClass, &pointer),
ValidationContext::DeviceClass,
&pointer,
);
assert_err_with_validation_context(
validate_identifier($id, ValidationContext::System, &pointer),
ValidationContext::System,
&pointer,
);
};
}
test_id!("");
for invalid_char in 0..0x25 {
test_id!(&format!("ab{}cd", char::from_u32(invalid_char).unwrap()));
}
for invalid_char in 0x3a..0x3c {
test_id!(&format!("ab{}cd", char::from_u32(invalid_char).unwrap()));
}
for invalid_char in 0x3f..0x41 {
test_id!(&format!("ab{}cd", char::from_u32(invalid_char).unwrap()));
}
for invalid_char in 0x5b..0x5e {
test_id!(&format!("ab{}cd", char::from_u32(invalid_char).unwrap()));
}
test_id!(&format!("ab\x7fcd"));
}
#[test]
fn test_valid_version_numbers_pass() {
let pointer = JsonPointer::new(vec![]);
assert!(validate_version("1.0.0", &pointer).is_ok());
assert!(validate_version("0.1.0", &pointer).is_ok());
assert!(validate_version("1.2.3", &pointer).is_ok());
assert!(validate_version("10000.20000.30000", &pointer).is_ok());
}
#[test]
fn test_invalid_version_numbers_fail() {
let pointer = JsonPointer::new(vec!["foo", "bar"]);
macro_rules! test_version {
($vers:literal) => {
assert_err_with_pointer(validate_version($vers, &pointer), &pointer, $vers)
};
}
test_version!("01.0.0");
test_version!("1.01.0");
test_version!("1.0.01");
test_version!("1.0");
test_version!("1.0.0.3");
test_version!("1.a.b");
test_version!("1.0.0-beta.3");
test_version!("1.0.0e");
test_version!("foo");
test_version!("");
}
}