use cadmpeg_core::decode::DecodeContext;
use cadmpeg_core::CodecError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct AdmittedSchemaIdentifier {
state: AdmittedSchemaState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum AdmittedSchemaState {
Valid {
text: String,
},
ObjectIdentifierOutOfRange {
text: String,
name: String,
component: String,
},
}
impl AdmittedSchemaIdentifier {
pub(super) fn admit(identifier: String) -> Option<Self> {
let out_of_range = match schema_identifier_form(&identifier) {
SchemaIdentifierForm::Valid => None,
SchemaIdentifierForm::ObjectIdentifierOutOfRange { name, component } => {
Some((name.to_owned(), component.to_owned()))
}
SchemaIdentifierForm::Invalid => return None,
};
Some(Self {
state: match out_of_range {
None => AdmittedSchemaState::Valid { text: identifier },
Some((name, component)) => AdmittedSchemaState::ObjectIdentifierOutOfRange {
text: identifier,
name,
component,
},
},
})
}
pub(super) fn text(&self) -> &str {
match &self.state {
AdmittedSchemaState::Valid { text }
| AdmittedSchemaState::ObjectIdentifierOutOfRange { text, .. } => text,
}
}
pub(super) fn out_of_range(&self) -> Option<(&str, &str)> {
match &self.state {
AdmittedSchemaState::Valid { .. } => None,
AdmittedSchemaState::ObjectIdentifierOutOfRange {
name, component, ..
} => Some((name, component)),
}
}
pub(super) fn numeric_object_identifier(
&self,
ctx: &DecodeContext<'_>,
) -> Result<Option<Vec<u64>>, CodecError> {
let Some((_, Some(object_identifier))) = split_schema_identifier(self.text()) else {
return Ok(None);
};
let mut components = object_identifier.split_whitespace();
let Some(root) = components.next().and_then(schema_oid_root_number) else {
return Ok(None);
};
let mut numbers = Vec::new();
ctx.push_vec(
&mut numbers,
u64::from(root),
"step_schema_object_identifier_components",
)?;
for component in components {
let ComponentForm::Number(number) = schema_oid_component_form(component) else {
return Ok(None);
};
let Ok(number) = number.parse() else {
return Ok(None);
};
ctx.push_vec(
&mut numbers,
number,
"step_schema_object_identifier_components",
)?;
}
Ok((numbers.len() >= 2).then_some(numbers))
}
}
enum SchemaIdentifierForm<'a> {
Valid,
ObjectIdentifierOutOfRange {
name: &'a str,
component: &'a str,
},
Invalid,
}
fn schema_identifier_form(identifier: &str) -> SchemaIdentifierForm<'_> {
let identifier = identifier.trim();
if identifier.is_empty() || identifier.chars().count() > 1024 {
return SchemaIdentifierForm::Invalid;
}
let Some((name, object_identifier)) = split_schema_identifier(identifier) else {
return SchemaIdentifierForm::Invalid;
};
if !valid_schema_name(name) {
return SchemaIdentifierForm::Invalid;
}
let Some(object_identifier) = object_identifier else {
return SchemaIdentifierForm::Valid;
};
match schema_object_identifier_form(object_identifier) {
ObjectIdentifierForm::Valid => SchemaIdentifierForm::Valid,
ObjectIdentifierForm::ComponentOutOfRange(component) => {
SchemaIdentifierForm::ObjectIdentifierOutOfRange { name, component }
}
ObjectIdentifierForm::Invalid => SchemaIdentifierForm::Invalid,
}
}
pub(crate) fn split_schema_identifier(identifier: &str) -> Option<(&str, Option<&str>)> {
let identifier = identifier.trim();
let Some((name, object_identifier)) = identifier.split_once('{') else {
return Some((identifier, None));
};
let object_identifier = object_identifier.strip_suffix('}')?;
Some((name.trim_end(), Some(object_identifier)))
}
pub(super) fn valid_schema_identifier(identifier: &str) -> bool {
matches!(
schema_identifier_form(identifier),
SchemaIdentifierForm::Valid
)
}
fn valid_schema_name(name: &str) -> bool {
let mut bytes = name.bytes();
bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic())
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
}
const ROOT_INDEX: usize = 0;
const SECOND_INDEX: usize = 1;
enum ObjectIdentifierForm<'a> {
Valid,
ComponentOutOfRange(&'a str),
Invalid,
}
fn schema_object_identifier_form(value: &str) -> ObjectIdentifierForm<'_> {
let mut components = value.split_whitespace();
let Some(first) = components.next() else {
return ObjectIdentifierForm::Invalid;
};
let Some(second) = components.next() else {
return ObjectIdentifierForm::Invalid;
};
let root = schema_oid_root_number(first);
let mut out_of_range = None;
for (index, component) in [first, second].into_iter().chain(components).enumerate() {
let form = schema_oid_component_form(component);
if matches!(form, ComponentForm::Invalid) {
return ObjectIdentifierForm::Invalid;
}
if schema_oid_component_out_of_range(index, root, &form) {
out_of_range = out_of_range.or(Some(component));
}
}
out_of_range.map_or(ObjectIdentifierForm::Valid, |component| {
ObjectIdentifierForm::ComponentOutOfRange(component)
})
}
enum ComponentForm<'a> {
Unnumbered,
Number(&'a str),
Negative,
Invalid,
}
fn schema_oid_component_form(component: &str) -> ComponentForm<'_> {
if valid_schema_oid_name(component) {
return ComponentForm::Unnumbered;
}
let Some((name, number)) = component.split_once('(') else {
return schema_oid_number_form(component);
};
let Some(number) = number.strip_suffix(')') else {
return ComponentForm::Invalid;
};
if !valid_schema_oid_name(name) {
return ComponentForm::Invalid;
}
if valid_schema_oid_name(number) {
return ComponentForm::Unnumbered;
}
schema_oid_number_form(number)
}
fn schema_oid_number_form(value: &str) -> ComponentForm<'_> {
if valid_schema_oid_number(value) {
return ComponentForm::Number(value);
}
if value.strip_prefix('-').is_some_and(valid_schema_oid_number) {
return ComponentForm::Negative;
}
ComponentForm::Invalid
}
fn schema_oid_component_out_of_range(
index: usize,
root: Option<u8>,
form: &ComponentForm<'_>,
) -> bool {
if index == ROOT_INDEX {
return root.is_none();
}
if matches!(form, ComponentForm::Negative) {
return true;
}
index == SECOND_INDEX
&& matches!(root, Some(0 | 1))
&& matches!(*form, ComponentForm::Number(number) if !valid_schema_oid_second_arc(number))
}
fn valid_schema_oid_second_arc(number: &str) -> bool {
number.len() < 2 || (number.len() == 2 && number.as_bytes()[0] <= b'3')
}
fn valid_schema_oid_number(value: &str) -> bool {
!value.is_empty()
&& value.bytes().all(|byte| byte.is_ascii_digit())
&& (value == "0" || !value.starts_with('0'))
}
fn valid_schema_oid_name(value: &str) -> bool {
let mut bytes = value.bytes();
let Some(first) = bytes.next() else {
return false;
};
if !first.is_ascii_lowercase() {
return false;
}
let mut previous_hyphen = false;
for byte in bytes {
if byte.is_ascii_alphabetic() || byte.is_ascii_digit() {
previous_hyphen = false;
} else if byte == b'-' && !previous_hyphen {
previous_hyphen = true;
} else {
return false;
}
}
!value.ends_with('-')
}
fn schema_oid_root_number(component: &str) -> Option<u8> {
match schema_oid_component_form(component) {
ComponentForm::Number("0") => Some(0),
ComponentForm::Number("1") => Some(1),
ComponentForm::Number("2") => Some(2),
ComponentForm::Unnumbered => schema_oid_named_root_value(component),
ComponentForm::Number(_) | ComponentForm::Negative | ComponentForm::Invalid => None,
}
}
fn schema_oid_named_root_value(component: &str) -> Option<u8> {
match component {
"itu-t" | "ccitt" => Some(0),
"iso" => Some(1),
"joint-iso-itu-t" | "joint-iso-ccitt" => Some(2),
_ => None,
}
}
#[cfg(test)]
mod tests;