use std::borrow::Cow;
use crate::ast::*;
use crate::lexer::{Lexer, SyntaxKind, Token};
use crate::source::{ByteOffset, SourceDocument, SourceRange};
use crate::types::{Access, AccessKeyword, DiagCode, Diagnostic, DiagnosticConfig, Status};
use tracing::{debug, debug_span, info_span, trace};
type TcBody = (
Option<QuotedString>,
StatusClause,
QuotedString,
Option<QuotedString>,
SyntaxClause,
);
fn cover(first: SourceRange, last: SourceRange) -> SourceRange {
SourceRange::cover(first, last).expect("parser ranges belong to one source document")
}
pub struct Parser<'src, 'cfg> {
document: &'src SourceDocument,
lexer: Lexer<'src, 'cfg>,
buf: [Token; 3],
last_span: SourceRange,
lexer_diagnostic_cursor: usize,
diagnostics: Vec<Diagnostic>,
diag_config: &'cfg DiagnosticConfig,
eof_token: Token,
}
fn next_non_comment(lexer: &mut Lexer<'_, '_>) -> Token {
loop {
let tok = lexer.next_token();
if tok.kind != SyntaxKind::Comment {
return tok;
}
}
}
impl<'src, 'cfg> Parser<'src, 'cfg> {
pub fn new(document: &'src SourceDocument, diag_config: &'cfg DiagnosticConfig) -> Self {
let mut lexer = Lexer::new(document, diag_config);
let eof_span = document
.empty_range(document.bytes().len())
.expect("the document EOF is a valid source position");
let eof_token = Token {
kind: SyntaxKind::EofToken,
span: eof_span,
};
let buf = [
next_non_comment(&mut lexer),
next_non_comment(&mut lexer),
next_non_comment(&mut lexer),
];
debug!(
target: "mib_rs::parser",
component = "parser",
"parser initialized",
);
Parser {
document,
lexer,
buf,
last_span: document
.empty_range(0)
.expect("the document start is a valid source position"),
lexer_diagnostic_cursor: 0,
diagnostics: Vec::new(),
diag_config,
eof_token,
}
}
fn peek(&self) -> Token {
self.buf[0]
}
fn peek_nth(&self, n: usize) -> Token {
if n < self.buf.len() {
self.buf[n]
} else {
self.eof_token
}
}
fn advance(&mut self) -> Token {
let tok = self.buf[0];
self.buf[0] = self.buf[1];
self.buf[1] = self.buf[2];
self.buf[2] = next_non_comment(&mut self.lexer);
self.last_span = tok.span;
tok
}
fn check(&self, kind: SyntaxKind) -> bool {
self.peek().kind == kind
}
fn expect(&mut self, kind: SyntaxKind) -> Result<Token, Diagnostic> {
if self.check(kind) {
Ok(self.advance())
} else {
Err(self.make_error(format!("expected {}", kind.display_name())))
}
}
fn current_span(&self) -> SourceRange {
self.peek().span
}
fn range_to_current_start(&self, start: SourceRange) -> SourceRange {
let current = self.current_span();
assert_eq!(start.source(), self.document.id());
assert_eq!(current.source(), self.document.id());
self.document
.range(start.start().as_usize()..current.start().as_usize())
.expect("parser range endpoints are ordered within its source document")
}
fn is_eof(&self) -> bool {
self.peek().kind == SyntaxKind::EofToken
}
fn text(&self, span: SourceRange) -> Cow<'src, str> {
String::from_utf8_lossy(
self.document
.slice(span)
.expect("parser ranges belong to its source document"),
)
}
fn make_error(&self, message: String) -> Diagnostic {
Diagnostic {
severity: self.diag_config.effective_severity(DiagCode::ParseError),
code: DiagCode::ParseError,
message,
module: None,
range: Some(self.current_span()),
}
}
fn record_parse_error(&mut self, mut diag: Diagnostic) {
if !self.diag_config.should_collect(diag.code) {
return;
}
diag.severity = self.diag_config.effective_severity(diag.code);
self.diagnostics.push(diag);
}
fn emit_diagnostic(&mut self, code: DiagCode, span: SourceRange, message: impl Into<String>) {
if !self.diag_config.should_collect(code) {
return;
}
self.diagnostics.push(Diagnostic {
severity: self.diag_config.effective_severity(code),
code,
message: message.into(),
module: None,
range: Some(span),
});
}
fn emit_diagnostic_with<F>(&mut self, code: DiagCode, span: SourceRange, message: F)
where
F: FnOnce() -> String,
{
if !self.diag_config.should_collect(code) {
return;
}
self.diagnostics.push(Diagnostic {
severity: self.diag_config.effective_severity(code),
code,
message: message(),
module: None,
range: Some(span),
});
}
fn make_ident(&self, token: Token) -> Ident {
Ident {
name: self.text(token.span).to_string(),
span: token.span,
}
}
fn make_ident_with_validation(&mut self, token: Token) -> Ident {
let name = self.text(token.span).to_string();
self.validate_identifier(&name, token.span);
Ident {
name,
span: token.span,
}
}
fn validate_identifier(&mut self, name: &str, span: SourceRange) {
if name.contains('_') {
self.emit_diagnostic_with(DiagCode::IdentifierUnderscore, span, || {
format!("identifier {:?} contains underscore (RFC violation)", name)
});
}
if name.ends_with('-') {
self.emit_diagnostic_with(DiagCode::IdentifierHyphenEnd, span, || {
format!("identifier {:?} ends with hyphen", name)
});
}
if name.len() > 64 {
self.emit_diagnostic_with(DiagCode::IdentifierLength64, span, || {
format!(
"identifier {:?} exceeds 64 character limit ({} chars)",
name,
name.len()
)
});
} else if name.len() > 32 {
self.emit_diagnostic_with(DiagCode::IdentifierLength32, span, || {
format!(
"identifier {:?} exceeds 32 character recommendation ({} chars)",
name,
name.len()
)
});
}
}
fn validate_value_reference(&mut self, name: &str, span: SourceRange) {
if let Some(first) = name.bytes().next()
&& first.is_ascii_uppercase()
{
self.emit_diagnostic_with(DiagCode::BadIdentifierCase, span, || {
format!("{:?} should start with a lowercase letter", name)
});
}
}
fn expect_identifier(&mut self) -> Result<Token, Diagnostic> {
if self.peek().kind.is_identifier() {
return Ok(self.advance());
}
if self.check(SyntaxKind::ForbiddenKeyword) {
let token = self.advance();
let name = self.text(token.span).to_string();
self.emit_diagnostic_with(DiagCode::KeywordReserved, token.span, || {
format!("identifier {:?} is a reserved ASN.1 keyword", name)
});
return Ok(token);
}
Err(self.make_error("expected identifier".to_string()))
}
fn expect_index_object(&mut self) -> Result<Token, Diagnostic> {
if self.peek().kind.is_identifier() || self.peek().kind.is_type_keyword() {
return Ok(self.advance());
}
Err(self.make_error("expected index object".to_string()))
}
fn expect_enum_label(&mut self) -> Result<Token, Diagnostic> {
if self.peek().kind.is_identifier() || self.peek().kind.is_keyword() {
return Ok(self.advance());
}
Err(self.make_error("expected enum label".to_string()))
}
fn parse_identifier_as_ident(&mut self) -> Result<Ident, Diagnostic> {
let token = self.expect_identifier()?;
Ok(self.make_ident(token))
}
fn parse_quoted_string(&mut self) -> Result<QuotedString, Diagnostic> {
if !self.check(SyntaxKind::QuotedString) {
return Err(self.make_error("expected quoted string".to_string()));
}
let token = self.advance();
let full_text = self.text(token.span);
let content = if full_text.len() >= 2 && full_text.ends_with('"') {
&full_text[1..full_text.len() - 1]
} else if !full_text.is_empty() {
&full_text[1..]
} else {
""
};
let value = content.replace("\"\"", "\"");
Ok(QuotedString {
value,
span: token.span,
})
}
fn parse_optional_reference(&mut self) -> Result<Option<QuotedString>, Diagnostic> {
if !self.check(SyntaxKind::KwReference) {
return Ok(None);
}
self.advance();
Ok(Some(self.parse_quoted_string()?))
}
fn parse_u32(&mut self, span: SourceRange, context: &str) -> u32 {
let text = self.text(span);
match text.parse::<u32>() {
Ok(v) => v,
Err(_) => {
self.emit_diagnostic(
DiagCode::InvalidU32,
span,
format!("invalid {} (not a valid u32)", context),
);
0
}
}
}
fn parse_i64(&mut self, span: SourceRange, context: &str) -> i64 {
let text = self.text(span);
match text.parse::<i64>() {
Ok(v) => v,
Err(_) => {
self.emit_diagnostic(
DiagCode::InvalidI64,
span,
format!("invalid {} (not a valid i64)", context),
);
0
}
}
}
fn skip_braced_content(&mut self, consume_close: bool) {
let mut depth: u32 = 1;
while depth > 0 && !self.is_eof() {
match self.peek().kind {
SyntaxKind::LBrace => {
depth += 1;
self.advance();
}
SyntaxKind::RBrace => {
depth -= 1;
if depth > 0 || consume_close {
self.advance();
}
}
_ => {
self.advance();
}
}
}
}
fn collect_module_diagnostics(
&mut self,
ownership_end: ByteOffset,
module_name: Option<&str>,
) -> Vec<Diagnostic> {
let lexer_diagnostics = self.lexer.diagnostics();
let owned_count = lexer_diagnostics[self.lexer_diagnostic_cursor..]
.iter()
.take_while(|diag| {
diag.range
.is_some_and(|range| range.start() < ownership_end)
})
.count();
let owned_end = self.lexer_diagnostic_cursor + owned_count;
let mut combined = Vec::with_capacity(owned_count + self.diagnostics.len());
combined.extend_from_slice(&lexer_diagnostics[self.lexer_diagnostic_cursor..owned_end]);
self.lexer_diagnostic_cursor = owned_end;
combined.append(&mut self.diagnostics);
if let Some(module_name) = module_name {
for diagnostic in &mut combined {
diagnostic.module = Some(module_name.to_owned());
}
}
combined
}
fn parse_identifier_list(&mut self) -> Result<Vec<Ident>, Diagnostic> {
let mut idents = Vec::new();
loop {
if self.check(SyntaxKind::RBrace) || self.is_eof() {
break;
}
let token = self.expect_identifier()?;
idents.push(self.make_ident(token));
if !self.check(SyntaxKind::Comma) {
break;
}
self.advance(); }
Ok(idents)
}
fn parse_braced_identifier_list(&mut self) -> Result<Vec<Ident>, Diagnostic> {
self.expect(SyntaxKind::LBrace)?;
let idents = self.parse_identifier_list()?;
self.expect(SyntaxKind::RBrace)?;
Ok(idents)
}
fn recover_to_definition(&mut self) {
while !self.is_eof() && !self.check(SyntaxKind::KwEnd) {
let current = self.peek().kind;
let next = self.peek_nth(1).kind;
if (current.is_identifier() && next.is_macro_keyword())
|| (current.is_identifier() && next == SyntaxKind::ColonColonEqual)
|| (current == SyntaxKind::UppercaseIdent
&& next == SyntaxKind::KwTextualConvention)
|| (current == SyntaxKind::UppercaseIdent && next == SyntaxKind::KwMacro)
|| (current.is_identifier()
&& next == SyntaxKind::KwObject
&& self.peek_nth(2).kind == SyntaxKind::KwIdentifier)
{
break;
}
self.advance();
}
}
pub fn parse_modules(&mut self) -> Vec<Module> {
let mut modules = Vec::new();
while !self.is_eof() {
let module = self.parse_one_module();
let failed = module.name.is_none();
modules.push(module);
if failed {
break;
}
}
if modules.is_empty() {
let module = self.parse_one_module();
modules.push(module);
}
modules
}
fn parse_one_module(&mut self) -> Module {
self.diagnostics.clear();
let start = self.current_span();
let name = match self.parse_module_header() {
Ok(name) => name,
Err(diag) => {
self.record_parse_error(diag);
debug!(
target: "mib_rs::parser",
component = "parser",
reason = "module_header_parse_failed",
"failed to parse module header",
);
let span = cover(start, self.current_span());
return Module {
name: None,
imports: Vec::new(),
body: Vec::new(),
span,
diagnostics: self.collect_module_diagnostics(self.eof_token.span.end(), None),
};
}
};
let module_name = name.name.clone();
let module_span = debug_span!(
target: "mib_rs::parser",
"parse_module",
component = "parser",
module = %module_name,
);
let _module_guard = module_span.enter();
debug!(
target: "mib_rs::parser",
component = "parser",
module = %module_name,
"parsing module",
);
let mut imports = Vec::new();
if self.check(SyntaxKind::KwImports) {
match self.parse_imports() {
Ok(imp) => {
debug!(
target: "mib_rs::parser",
component = "parser",
module = %module_name,
import_count = imp.len(),
"parsed imports",
);
imports = imp;
}
Err(diag) => {
debug!(
target: "mib_rs::parser",
component = "parser",
module = %module_name,
reason = "imports_parse_failed",
"failed to parse imports",
);
self.record_parse_error(diag);
}
}
}
let mut body: Vec<Definition> = Vec::new();
while !self.check(SyntaxKind::KwEnd) && !self.is_eof() {
while self.check(SyntaxKind::KwExports) {
self.advance();
if self.check(SyntaxKind::Semicolon) {
self.advance();
}
}
if self.check(SyntaxKind::KwEnd) || self.is_eof() {
break;
}
let pos_before = self.current_span();
trace!(
target: "mib_rs::parser",
component = "parser",
phase = "definitions",
offset = pos_before.start().get(),
first = %self.peek().kind.display_name(),
second = %self.peek_nth(1).kind.display_name(),
"parsing definition",
);
match self.parse_definition() {
Ok(def) => body.push(def),
Err(diag) => {
self.record_parse_error(diag);
self.recover_to_definition();
if self.current_span() == pos_before {
self.advance();
}
}
}
}
if self.check(SyntaxKind::KwEnd) {
self.advance();
} else {
self.record_parse_error(self.make_error("expected END".to_string()));
}
let span = cover(start, self.last_span);
let ownership_end = if self.is_eof() {
self.eof_token.span.end()
} else {
self.last_span.end()
};
let diagnostics = self.collect_module_diagnostics(ownership_end, Some(&module_name));
debug!(
target: "mib_rs::parser",
module = %name.name,
component = "parser",
definition_count = body.len(),
diagnostic_count = diagnostics.len(),
"parsing complete",
);
Module {
name: Some(name),
imports,
body,
span,
diagnostics,
}
}
fn parse_module_header(&mut self) -> Result<Ident, Diagnostic> {
let name_token = self.expect_identifier()?;
let name = self.make_ident_with_validation(name_token);
if self.check(SyntaxKind::LBrace) {
self.advance();
self.skip_braced_content(true);
}
self.expect(SyntaxKind::KwDefinitions)?;
self.expect(SyntaxKind::ColonColonEqual)?;
self.expect(SyntaxKind::KwBegin)?;
Ok(name)
}
fn parse_imports(&mut self) -> Result<Vec<ImportClause>, Diagnostic> {
self.expect(SyntaxKind::KwImports)?;
let mut clauses = Vec::new();
while !self.check(SyntaxKind::Semicolon) && !self.check(SyntaxKind::KwEnd) && !self.is_eof()
{
let start = self.current_span();
let mut symbols = Vec::new();
loop {
let kind = self.peek().kind;
if kind == SyntaxKind::KwFrom
|| kind == SyntaxKind::Semicolon
|| kind == SyntaxKind::KwEnd
|| kind == SyntaxKind::EofToken
{
break;
}
if kind.is_macro_keyword() || kind.is_type_keyword() || kind.is_identifier() {
let token = self.advance();
symbols.push(self.make_ident(token));
} else {
return Err(self.make_error("expected symbol or FROM".to_string()));
}
if self.check(SyntaxKind::Comma) {
self.advance();
}
}
self.expect(SyntaxKind::KwFrom)?;
let module_token = self.expect(SyntaxKind::UppercaseIdent)?;
let from_module = self.make_ident(module_token);
let span = cover(start, self.last_span);
clauses.push(ImportClause {
symbols,
from_module,
span,
});
}
if self.check(SyntaxKind::Semicolon) {
self.advance();
} else {
self.record_parse_error(self.make_error("unexpected end of imports".to_string()));
}
Ok(clauses)
}
fn parse_definition(&mut self) -> Result<Definition, Diagnostic> {
let first = self.peek().kind;
let second = self.peek_nth(1).kind;
match second {
SyntaxKind::KwObject
if first.is_identifier() && self.peek_nth(2).kind == SyntaxKind::KwIdentifier =>
{
self.parse_value_assignment()
}
SyntaxKind::KwObjectType if first.is_identifier() => self.parse_object_type(),
SyntaxKind::KwModuleIdentity if first.is_identifier() => self.parse_module_identity(),
SyntaxKind::KwObjectIdentity if first.is_identifier() => self.parse_object_identity(),
SyntaxKind::KwNotificationType if first.is_identifier() => {
self.parse_notification_type()
}
SyntaxKind::KwTrapType if first.is_identifier() => self.parse_trap_type(),
SyntaxKind::KwTextualConvention if first == SyntaxKind::UppercaseIdent => {
self.parse_textual_convention()
}
SyntaxKind::KwObjectGroup if first.is_identifier() => self.parse_object_group(),
SyntaxKind::KwNotificationGroup if first.is_identifier() => {
self.parse_notification_group()
}
SyntaxKind::KwModuleCompliance if first.is_identifier() => {
self.parse_module_compliance()
}
SyntaxKind::KwAgentCapabilities if first.is_identifier() => {
self.parse_agent_capabilities()
}
SyntaxKind::ColonColonEqual
if first == SyntaxKind::UppercaseIdent
|| first == SyntaxKind::LowercaseIdent
|| first.is_type_keyword() =>
{
if self.peek_nth(2).kind == SyntaxKind::KwTextualConvention {
return self.parse_textual_convention_with_assignment();
}
if first == SyntaxKind::LowercaseIdent {
let name = self.text(self.peek().span).to_string();
self.emit_diagnostic_with(
DiagCode::BadIdentifierCase,
self.peek().span,
|| {
format!(
"type assignment {:?} should start with an uppercase letter",
name
)
},
);
}
self.parse_type_assignment()
}
SyntaxKind::KwMacro
if first == SyntaxKind::UppercaseIdent || first.is_macro_keyword() =>
{
self.parse_macro_definition()
}
_ => Err(self.make_error(format!(
"unexpected token: {}",
self.peek().kind.display_name()
))),
}
}
fn parse_object_type(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(SyntaxKind::KwObjectType)?;
self.expect(SyntaxKind::KwSyntax)?;
let syntax = self.parse_syntax_clause()?;
let units = if self.check(SyntaxKind::KwUnits) {
self.advance();
Some(self.parse_quoted_string()?)
} else {
None
};
let access = self.parse_access_clause()?;
let status = if self.check(SyntaxKind::KwStatus) {
Some(self.parse_status_clause()?)
} else {
None
};
let description = if self.check(SyntaxKind::KwDescription) {
self.advance();
Some(self.parse_quoted_string()?)
} else {
None
};
let reference = self.parse_optional_reference()?;
let (index, augments) = self.parse_index_or_augments()?;
let defval = if self.check(SyntaxKind::KwDefval) {
Some(self.parse_defval_clause()?)
} else {
None
};
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(Definition::ObjectType(Box::new(ObjectTypeDef {
name,
syntax: Some(syntax),
units,
access: Some(access),
status,
description,
reference,
index,
augments,
defval,
oid,
span,
})))
}
fn parse_module_identity(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(SyntaxKind::KwModuleIdentity)?;
self.expect(SyntaxKind::KwLastUpdated)?;
let last_updated = self.parse_quoted_string()?;
self.expect(SyntaxKind::KwOrganization)?;
let organization = self.parse_quoted_string()?;
self.expect(SyntaxKind::KwContactInfo)?;
let contact_info = self.parse_quoted_string()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let mut revisions = Vec::new();
while self.check(SyntaxKind::KwRevision) {
let rev_start = self.current_span();
self.advance();
let date = self.parse_quoted_string()?;
self.expect(SyntaxKind::KwDescription)?;
let rev_desc = self.parse_quoted_string()?;
let rev_span = cover(rev_start, rev_desc.span);
revisions.push(RevisionClause {
date,
description: rev_desc,
span: rev_span,
});
}
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(Definition::ModuleIdentity(ModuleIdentityDef {
name,
last_updated,
organization,
contact_info,
description,
revisions,
oid,
span,
}))
}
fn parse_object_identity(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(SyntaxKind::KwObjectIdentity)?;
let status = self.parse_status_clause()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let reference = self.parse_optional_reference()?;
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(Definition::ObjectIdentity(ObjectIdentityDef {
name,
status,
description,
reference,
oid,
span,
}))
}
fn parse_notification_type(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(SyntaxKind::KwNotificationType)?;
let objects = if self.check(SyntaxKind::KwObjects) {
self.advance();
self.parse_braced_identifier_list()?
} else {
Vec::new()
};
let status = self.parse_status_clause()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let reference = self.parse_optional_reference()?;
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(Definition::NotificationType(NotificationTypeDef {
name,
objects,
status,
description,
reference,
oid,
span,
}))
}
fn parse_trap_type(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.expect(SyntaxKind::KwTrapType)?;
self.expect(SyntaxKind::KwEnterprise)?;
let enterprise_token = self.expect_identifier()?;
let enterprise = self.make_ident(enterprise_token);
let variables = if self.check(SyntaxKind::KwVariables) {
self.advance();
self.parse_braced_identifier_list()?
} else {
Vec::new()
};
let description = if self.check(SyntaxKind::KwDescription) {
self.advance();
Some(self.parse_quoted_string()?)
} else {
None
};
let reference = self.parse_optional_reference()?;
self.expect(SyntaxKind::ColonColonEqual)?;
let num_token = self.expect(SyntaxKind::Number)?;
let trap_number = self.parse_u32(num_token.span, "trap number");
let span = cover(start, self.last_span);
Ok(Definition::TrapType(TrapTypeDef {
name,
enterprise,
variables,
description,
reference,
trap_number,
span,
}))
}
fn parse_textual_convention(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.expect(SyntaxKind::KwTextualConvention)?;
let (display_hint, status, description, reference, syntax) =
self.parse_textual_convention_body()?;
let span = cover(start, syntax.span);
Ok(Definition::TextualConvention(TextualConventionDef {
name,
display_hint,
status,
description,
reference,
syntax,
span,
}))
}
fn parse_textual_convention_with_assignment(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.expect(SyntaxKind::ColonColonEqual)?;
self.expect(SyntaxKind::KwTextualConvention)?;
let (display_hint, status, description, reference, syntax) =
self.parse_textual_convention_body()?;
let span = cover(start, syntax.span);
Ok(Definition::TextualConvention(TextualConventionDef {
name,
display_hint,
status,
description,
reference,
syntax,
span,
}))
}
fn parse_textual_convention_body(&mut self) -> Result<TcBody, Diagnostic> {
let display_hint = if self.check(SyntaxKind::KwDisplayHint) {
self.advance();
Some(self.parse_quoted_string()?)
} else {
None
};
let status = self.parse_status_clause()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let reference = self.parse_optional_reference()?;
self.expect(SyntaxKind::KwSyntax)?;
let syntax = self.parse_syntax_clause()?;
Ok((display_hint, status, description, reference, syntax))
}
fn parse_type_assignment(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.expect(SyntaxKind::ColonColonEqual)?;
let syntax = self.parse_type_syntax()?;
let span = cover(start, syntax.span());
Ok(Definition::TypeAssignment(TypeAssignmentDef {
name,
syntax,
span,
}))
}
fn parse_value_assignment(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(SyntaxKind::KwObject)?;
self.expect(SyntaxKind::KwIdentifier)?;
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(Definition::ValueAssignment(ValueAssignmentDef {
name,
oid,
span,
}))
}
fn parse_object_group(&mut self) -> Result<Definition, Diagnostic> {
self.parse_group_def(
SyntaxKind::KwObjectGroup,
SyntaxKind::KwObjects,
|name, objects, status, description, reference, oid, span| {
Definition::ObjectGroup(ObjectGroupDef {
name,
objects,
status,
description,
reference,
oid,
span,
})
},
)
}
fn parse_notification_group(&mut self) -> Result<Definition, Diagnostic> {
self.parse_group_def(
SyntaxKind::KwNotificationGroup,
SyntaxKind::KwNotifications,
|name, notifications, status, description, reference, oid, span| {
Definition::NotificationGroup(NotificationGroupDef {
name,
notifications,
status,
description,
reference,
oid,
span,
})
},
)
}
fn parse_group_def<F>(
&mut self,
macro_kw: SyntaxKind,
members_kw: SyntaxKind,
build: F,
) -> Result<Definition, Diagnostic>
where
F: FnOnce(
Ident,
Vec<Ident>,
StatusClause,
QuotedString,
Option<QuotedString>,
OidAssignment,
SourceRange,
) -> Definition,
{
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(macro_kw)?;
self.expect(members_kw)?;
let members = self.parse_braced_identifier_list()?;
let status = self.parse_status_clause()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let reference = self.parse_optional_reference()?;
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(build(
name,
members,
status,
description,
reference,
oid,
span,
))
}
fn parse_module_compliance(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(SyntaxKind::KwModuleCompliance)?;
let status = self.parse_status_clause()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let reference = self.parse_optional_reference()?;
let mut modules = Vec::new();
while self.check(SyntaxKind::KwModule) {
modules.push(self.parse_compliance_module()?);
}
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(Definition::ModuleCompliance(ModuleComplianceDef {
name,
status,
description,
reference,
modules,
oid,
span,
}))
}
fn parse_compliance_module(&mut self) -> Result<ComplianceModule, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::KwModule)?;
let module_name = if self.check(SyntaxKind::UppercaseIdent) {
let token = self.advance();
Some(self.make_ident(token))
} else {
None
};
let module_oid = if self.check(SyntaxKind::LBrace) {
Some(self.parse_oid_assignment()?)
} else {
None
};
let mandatory_groups = if self.check(SyntaxKind::KwMandatoryGroups) {
self.advance();
self.parse_braced_identifier_list()?
} else {
Vec::new()
};
let mut compliances = Vec::new();
while self.check(SyntaxKind::KwGroup) || self.check(SyntaxKind::KwObject) {
if self.check(SyntaxKind::KwGroup) {
compliances.push(Compliance::Group(self.parse_compliance_group()?));
} else {
compliances.push(Compliance::Object(Box::new(
self.parse_compliance_object()?,
)));
}
}
let span = cover(start, self.last_span);
Ok(ComplianceModule {
module_name,
module_oid,
mandatory_groups,
compliances,
span,
})
}
fn parse_compliance_group(&mut self) -> Result<ComplianceGroup, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::KwGroup)?;
let group = self.parse_identifier_as_ident()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let span = cover(start, description.span);
Ok(ComplianceGroup {
group,
description,
span,
})
}
fn parse_compliance_object(&mut self) -> Result<ComplianceObject, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::KwObject)?;
let object = self.parse_identifier_as_ident()?;
let (syntax, write_syntax) = self.parse_optional_syntax_clauses()?;
let min_access = if self.check(SyntaxKind::KwMinAccess) {
Some(self.parse_access_clause()?)
} else {
None
};
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let span = cover(start, description.span);
Ok(ComplianceObject {
object,
syntax,
write_syntax,
min_access,
description,
span,
})
}
fn parse_optional_syntax_clauses(
&mut self,
) -> Result<(Option<SyntaxClause>, Option<SyntaxClause>), Diagnostic> {
let syntax = if self.check(SyntaxKind::KwSyntax) {
self.advance();
Some(self.parse_syntax_clause()?)
} else {
None
};
let write_syntax = if self.check(SyntaxKind::KwWriteSyntax) {
self.advance();
Some(self.parse_syntax_clause()?)
} else {
None
};
Ok((syntax, write_syntax))
}
fn parse_agent_capabilities(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident_with_validation(name_token);
self.validate_value_reference(&name.name, name_token.span);
self.expect(SyntaxKind::KwAgentCapabilities)?;
self.expect(SyntaxKind::KwProductRelease)?;
let product_release = self.parse_quoted_string()?;
let status = self.parse_status_clause()?;
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let reference = self.parse_optional_reference()?;
let mut supports = Vec::new();
while self.check(SyntaxKind::KwSupports) {
supports.push(self.parse_supports_module()?);
}
self.expect(SyntaxKind::ColonColonEqual)?;
let oid = self.parse_oid_assignment()?;
let span = cover(start, oid.span);
Ok(Definition::AgentCapabilities(AgentCapabilitiesDef {
name,
product_release,
status,
description,
reference,
supports,
oid,
span,
}))
}
fn parse_supports_module(&mut self) -> Result<SupportsModule, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::KwSupports)?;
let module_name = self.parse_identifier_as_ident()?;
let module_oid = if self.check(SyntaxKind::LBrace) {
Some(self.parse_oid_assignment()?)
} else {
None
};
self.expect(SyntaxKind::KwIncludes)?;
let includes = self.parse_braced_identifier_list()?;
let mut variations = Vec::new();
while self.check(SyntaxKind::KwVariation) {
variations.push(self.parse_variation_clause()?);
}
let span = cover(start, self.last_span);
Ok(SupportsModule {
module_name,
module_oid,
includes,
variations,
span,
})
}
fn parse_variation_clause(&mut self) -> Result<Variation, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::KwVariation)?;
let name = self.parse_identifier_as_ident()?;
let (syntax, write_syntax) = self.parse_optional_syntax_clauses()?;
let access = if self.check(SyntaxKind::KwAccess) {
Some(self.parse_access_clause()?)
} else {
None
};
let creation_requires = if self.check(SyntaxKind::KwCreationRequires) {
self.advance();
self.parse_braced_identifier_list()?
} else {
Vec::new()
};
let defval = if self.check(SyntaxKind::KwDefval) {
Some(self.parse_defval_clause()?)
} else {
None
};
self.expect(SyntaxKind::KwDescription)?;
let description = self.parse_quoted_string()?;
let span = cover(start, description.span);
Ok(Variation {
name,
syntax,
write_syntax,
access,
creation_requires,
defval,
description,
span,
})
}
fn parse_macro_definition(&mut self) -> Result<Definition, Diagnostic> {
let start = self.current_span();
let name_token = self.advance();
let name = self.make_ident(name_token);
self.expect(SyntaxKind::KwMacro)?;
if self.check(SyntaxKind::KwEnd) {
self.advance();
}
let span = cover(start, self.last_span);
Ok(Definition::MacroDefinition(MacroDefinitionDef {
name,
span,
}))
}
fn parse_access_clause(&mut self) -> Result<AccessClause, Diagnostic> {
let start = self.current_span();
let keyword = if self.check(SyntaxKind::KwMaxAccess) {
self.advance();
AccessKeyword::MaxAccess
} else if self.check(SyntaxKind::KwAccess) {
self.advance();
AccessKeyword::Access
} else if self.check(SyntaxKind::KwMinAccess) {
self.advance();
AccessKeyword::MinAccess
} else {
return Err(self.make_error("expected MAX-ACCESS, MIN-ACCESS, or ACCESS".to_string()));
};
let value = match self.peek().kind {
SyntaxKind::KwReadOnly => {
self.advance();
Access::ReadOnly
}
SyntaxKind::KwReadWrite => {
self.advance();
Access::ReadWrite
}
SyntaxKind::KwReadCreate => {
self.advance();
Access::ReadCreate
}
SyntaxKind::KwNotAccessible => {
self.advance();
Access::NotAccessible
}
SyntaxKind::KwAccessibleForNotify => {
self.advance();
Access::AccessibleForNotify
}
SyntaxKind::KwWriteOnly => {
self.advance();
Access::WriteOnly
}
SyntaxKind::KwNotImplemented => {
self.advance();
Access::NotImplemented
}
_ => return Err(self.make_error("expected access value".to_string())),
};
let span = cover(start, self.last_span);
Ok(AccessClause {
keyword,
value,
span,
})
}
fn parse_status_clause(&mut self) -> Result<StatusClause, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::KwStatus)?;
let value = match self.peek().kind {
SyntaxKind::KwCurrent => {
self.advance();
Status::Current
}
SyntaxKind::KwDeprecated => {
self.advance();
Status::Deprecated
}
SyntaxKind::KwObsolete => {
self.advance();
Status::Obsolete
}
SyntaxKind::KwMandatory => {
self.advance();
Status::Mandatory
}
SyntaxKind::KwOptional => {
self.advance();
Status::Optional
}
_ => return Err(self.make_error("expected status value".to_string())),
};
let span = cover(start, self.last_span);
Ok(StatusClause { value, span })
}
fn parse_index_or_augments(
&mut self,
) -> Result<(Option<IndexClause>, Option<AugmentsClause>), Diagnostic> {
if self.check(SyntaxKind::KwIndex) {
let start = self.current_span();
self.advance();
self.expect(SyntaxKind::LBrace)?;
let mut items = Vec::new();
loop {
if self.check(SyntaxKind::RBrace) || self.is_eof() {
break;
}
let item_start = self.current_span();
let implied = if self.check(SyntaxKind::KwImplied) {
self.advance();
true
} else {
false
};
let obj_token = self.expect_index_object()?;
let object =
if obj_token.kind == SyntaxKind::KwOctet && self.check(SyntaxKind::KwString) {
let string_token = self.advance();
Ident {
name: "OCTET STRING".to_string(),
span: cover(obj_token.span, string_token.span),
}
} else {
self.make_ident(obj_token)
};
let item_span = cover(item_start, self.last_span);
items.push(IndexItem {
implied,
object,
span: item_span,
});
if self.check(SyntaxKind::Comma) {
self.advance();
} else {
break;
}
}
self.expect(SyntaxKind::RBrace)?;
let span = cover(start, self.last_span);
return Ok((Some(IndexClause { items, span }), None));
}
if self.check(SyntaxKind::KwAugments) {
let start = self.current_span();
self.advance();
self.expect(SyntaxKind::LBrace)?;
let target_token = self.expect_identifier()?;
let target = self.make_ident(target_token);
self.expect(SyntaxKind::RBrace)?;
let span = cover(start, self.last_span);
return Ok((None, Some(AugmentsClause { target, span })));
}
Ok((None, None))
}
fn parse_defval_clause(&mut self) -> Result<DefValClause, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::KwDefval)?;
self.expect(SyntaxKind::LBrace)?;
let value = self.parse_defval()?;
self.expect(SyntaxKind::RBrace)?;
let span = cover(start, self.last_span);
Ok(DefValClause { value, span })
}
fn parse_defval(&mut self) -> Result<DefVal, Diagnostic> {
match self.peek().kind {
SyntaxKind::Number => {
let token = self.advance();
let text = self.text(token.span);
if let Ok(v) = text.parse::<i64>() {
Ok(DefVal::Integer(v))
} else if let Ok(v) = text.parse::<u64>() {
Ok(DefVal::Unsigned(v))
} else {
self.emit_diagnostic(DiagCode::InvalidI64, token.span, "invalid DEFVAL number");
Ok(DefVal::Integer(0))
}
}
SyntaxKind::NegativeNumber => {
let token = self.advance();
let v = self.parse_i64(token.span, "DEFVAL number");
Ok(DefVal::Integer(v))
}
SyntaxKind::QuotedString => {
let qs = self.parse_quoted_string()?;
Ok(DefVal::String(qs))
}
SyntaxKind::HexString => {
let token = self.advance();
let full_text = self.text(token.span);
let content = strip_string_literal(&full_text);
Ok(DefVal::HexString {
content: content.to_string(),
span: token.span,
})
}
SyntaxKind::BinString => {
let token = self.advance();
let full_text = self.text(token.span);
let content = strip_string_literal(&full_text);
Ok(DefVal::BinaryString {
content: content.to_string(),
span: token.span,
})
}
SyntaxKind::LowercaseIdent | SyntaxKind::UppercaseIdent => {
let token = self.advance();
Ok(DefVal::Identifier(self.make_ident(token)))
}
SyntaxKind::LBrace => {
self.advance(); self.parse_defval_braced_content()
}
kind if kind.is_keyword() => {
let token = self.advance();
Ok(DefVal::Identifier(self.make_ident(token)))
}
_ => {
let start = self.current_span();
Ok(DefVal::Unparsed {
span: cover(start, self.current_span()),
})
}
}
}
fn parse_defval_braced_content(&mut self) -> Result<DefVal, Diagnostic> {
let start = self.current_span();
if self.check(SyntaxKind::RBrace) {
let span = cover(start, self.current_span());
self.advance(); return Ok(DefVal::Bits {
labels: Vec::new(),
span,
});
}
let kind = self.peek().kind;
if kind == SyntaxKind::Number {
let result = self.parse_defval_oid_components(start)?;
self.expect(SyntaxKind::RBrace)?; return Ok(result);
}
if kind.is_identifier() || kind.is_keyword() {
let first_token = self.advance();
let first_ident = self.make_ident(first_token);
if self.check(SyntaxKind::Comma) || self.check(SyntaxKind::RBrace) {
let result = self.parse_defval_bits_labels(start, first_ident)?;
self.expect(SyntaxKind::RBrace)?; return Ok(result);
}
let result = self.parse_defval_oid_with_first_ident(start, first_ident)?;
self.expect(SyntaxKind::RBrace)?; return Ok(result);
}
self.skip_braced_content(true); Ok(DefVal::Unparsed {
span: cover(start, self.current_span()),
})
}
fn parse_defval_bits_labels(
&mut self,
start: SourceRange,
first: Ident,
) -> Result<DefVal, Diagnostic> {
let mut labels = vec![first];
while self.check(SyntaxKind::Comma) {
self.advance(); if self.check(SyntaxKind::RBrace) || self.is_eof() {
break;
}
let token = self.expect_enum_label()?;
labels.push(self.make_ident(token));
}
Ok(DefVal::Bits {
span: self.range_to_current_start(start),
labels,
})
}
fn parse_defval_oid_with_first_ident(
&mut self,
start: SourceRange,
first: Ident,
) -> Result<DefVal, Diagnostic> {
let first_component = if self.check(SyntaxKind::LParen) {
self.advance();
let num_token = self.expect(SyntaxKind::Number)?;
let num = self.parse_u32(num_token.span, "OID component");
self.expect(SyntaxKind::RParen)?;
OidComponent::NamedNumber {
span: cover(first.span, self.last_span),
name: first,
num,
}
} else {
OidComponent::Name(first)
};
let mut components = vec![first_component];
self.collect_defval_oid_components(&mut components)?;
Ok(DefVal::ObjectIdentifier {
span: self.range_to_current_start(start),
components,
})
}
fn parse_defval_oid_components(&mut self, start: SourceRange) -> Result<DefVal, Diagnostic> {
let mut components = Vec::new();
self.collect_defval_oid_components(&mut components)?;
Ok(DefVal::ObjectIdentifier {
span: self.range_to_current_start(start),
components,
})
}
fn collect_defval_oid_components(
&mut self,
components: &mut Vec<OidComponent>,
) -> Result<(), Diagnostic> {
while !self.check(SyntaxKind::RBrace) && !self.is_eof() {
let kind = self.peek().kind;
if kind == SyntaxKind::Number
|| kind == SyntaxKind::LowercaseIdent
|| kind == SyntaxKind::UppercaseIdent
{
let comp = self.parse_oid_component("OID component number")?;
components.push(comp);
} else {
break;
}
}
Ok(())
}
fn parse_syntax_clause(&mut self) -> Result<SyntaxClause, Diagnostic> {
let start = self.current_span();
let syntax = self.parse_type_syntax()?;
let span = cover(start, syntax.span());
Ok(SyntaxClause { syntax, span })
}
fn parse_type_syntax(&mut self) -> Result<TypeSyntax, Diagnostic> {
let start = self.current_span();
let base = match self.peek().kind {
SyntaxKind::KwInteger => {
self.advance();
if self.check(SyntaxKind::LBrace) {
let named_numbers = self.parse_named_numbers()?;
let span = cover(start, self.last_span);
TypeSyntax::IntegerEnum {
base: None,
named_numbers,
span,
}
} else {
TypeSyntax::TypeRef(Ident {
name: "INTEGER".to_string(),
span: cover(start, self.last_span),
})
}
}
SyntaxKind::KwBits => {
self.advance();
if self.check(SyntaxKind::LBrace) {
self.expect(SyntaxKind::LBrace)?;
let named_bits = self.parse_named_number_list()?;
self.expect(SyntaxKind::RBrace)?;
let span = cover(start, self.last_span);
TypeSyntax::Bits { named_bits, span }
} else {
TypeSyntax::TypeRef(Ident {
name: "BITS".to_string(),
span: cover(start, self.last_span),
})
}
}
SyntaxKind::KwOctet => {
self.advance();
self.expect(SyntaxKind::KwString)?;
if self.check(SyntaxKind::LParen) {
let constraint = self.parse_constraint()?;
let span = cover(start, constraint.span());
TypeSyntax::Constrained {
base: Box::new(TypeSyntax::OctetString {
span: cover(start, self.last_span),
}),
constraint,
span,
}
} else {
TypeSyntax::OctetString {
span: cover(start, self.last_span),
}
}
}
SyntaxKind::KwObject => {
self.advance();
self.expect(SyntaxKind::KwIdentifier)?;
TypeSyntax::ObjectIdentifier {
span: cover(start, self.last_span),
}
}
SyntaxKind::KwSequence => {
self.advance();
if self.check(SyntaxKind::KwOf) {
self.advance();
let entry_token = self.expect_identifier()?;
let entry_type = self.make_ident(entry_token);
TypeSyntax::SequenceOf {
entry_type,
span: cover(start, self.last_span),
}
} else {
self.expect(SyntaxKind::LBrace)?;
let fields = self.parse_sequence_fields()?;
self.expect(SyntaxKind::RBrace)?;
TypeSyntax::Sequence {
fields,
span: cover(start, self.last_span),
}
}
}
SyntaxKind::KwChoice => {
self.advance();
self.expect(SyntaxKind::LBrace)?;
let alternatives = self.parse_sequence_fields()?;
self.expect(SyntaxKind::RBrace)?;
TypeSyntax::Choice {
alternatives,
span: cover(start, self.last_span),
}
}
SyntaxKind::KwCounter32
| SyntaxKind::KwCounter64
| SyntaxKind::KwGauge32
| SyntaxKind::KwUnsigned32
| SyntaxKind::KwTimeTicks
| SyntaxKind::KwIpAddress
| SyntaxKind::KwOpaque
| SyntaxKind::KwCounter
| SyntaxKind::KwGauge
| SyntaxKind::KwNetworkAddress => {
let token = self.advance();
let name = self.text(token.span).to_string();
TypeSyntax::TypeRef(Ident {
name,
span: token.span,
})
}
SyntaxKind::ForbiddenKeyword if self.text(self.peek().span) == "NULL" => {
let token = self.advance();
TypeSyntax::TypeRef(self.make_ident(token))
}
SyntaxKind::UppercaseIdent | SyntaxKind::LowercaseIdent => {
let token = self.advance();
let ident = self.make_ident(token);
if token.kind == SyntaxKind::LowercaseIdent {
self.emit_diagnostic_with(DiagCode::BadIdentifierCase, token.span, || {
format!(
"type reference {:?} should start with an uppercase letter",
ident.name
)
});
}
if self.check(SyntaxKind::LParen) {
let constraint = self.parse_constraint()?;
let span = cover(start, constraint.span());
TypeSyntax::Constrained {
base: Box::new(TypeSyntax::TypeRef(ident)),
constraint,
span,
}
} else if self.check(SyntaxKind::LBrace) {
let named_numbers = self.parse_named_numbers()?;
let span = cover(start, self.last_span);
TypeSyntax::IntegerEnum {
base: Some(ident),
named_numbers,
span,
}
} else {
TypeSyntax::TypeRef(ident)
}
}
SyntaxKind::LBracket => {
self.advance();
if self.check(SyntaxKind::KwApplication) || self.check(SyntaxKind::KwUniversal) {
self.advance();
}
if self.check(SyntaxKind::Number) {
self.advance();
}
self.expect(SyntaxKind::RBracket)?;
if self.check(SyntaxKind::KwImplicit) {
self.advance();
}
let underlying = self.parse_type_syntax()?;
let span = cover(start, underlying.span());
TypeSyntax::Tagged {
underlying: Box::new(underlying),
span,
}
}
_ => {
return Err(self.make_error("expected type syntax".to_string()));
}
};
if self.check(SyntaxKind::LParen) && !matches!(base, TypeSyntax::Constrained { .. }) {
let constraint = self.parse_constraint()?;
let span = cover(start, constraint.span());
return Ok(TypeSyntax::Constrained {
base: Box::new(base),
constraint,
span,
});
}
Ok(base)
}
fn parse_constraint(&mut self) -> Result<Constraint, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::LParen)?;
if self.check(SyntaxKind::KwSize) {
self.advance();
self.expect(SyntaxKind::LParen)?;
let ranges = self.parse_range_list()?;
self.expect(SyntaxKind::RParen)?;
self.expect(SyntaxKind::RParen)?;
let span = cover(start, self.last_span);
Ok(Constraint::Size { ranges, span })
} else {
let ranges = self.parse_range_list()?;
self.expect(SyntaxKind::RParen)?;
let span = cover(start, self.last_span);
Ok(Constraint::Range { ranges, span })
}
}
fn parse_range_list(&mut self) -> Result<Vec<Range>, Diagnostic> {
let mut ranges = Vec::new();
loop {
let range_start = self.current_span();
let min = self.parse_range_value()?;
let max = if self.check(SyntaxKind::DotDot) {
self.advance();
Some(self.parse_range_value()?)
} else {
None
};
let range_span = cover(range_start, self.last_span);
ranges.push(Range {
min,
max,
span: range_span,
});
if self.check(SyntaxKind::Pipe) {
self.advance();
} else {
break;
}
}
Ok(ranges)
}
fn parse_range_value(&mut self) -> Result<RangeValue, Diagnostic> {
match self.peek().kind {
SyntaxKind::Number => {
let token = self.advance();
let text = self.text(token.span);
if let Ok(v) = text.parse::<u64>() {
Ok(RangeValue::Unsigned(v))
} else {
self.emit_diagnostic(
DiagCode::UnknownRangeValue,
token.span,
format!("range value {text:?} is outside the supported integer range"),
);
Ok(RangeValue::Raw(text.into_owned()))
}
}
SyntaxKind::NegativeNumber => {
let token = self.advance();
let text = self.text(token.span);
if let Ok(v) = text.parse::<i64>() {
Ok(RangeValue::Signed(v))
} else {
self.emit_diagnostic(
DiagCode::UnknownRangeValue,
token.span,
format!("range value {text:?} is outside the supported integer range"),
);
Ok(RangeValue::Raw(text.into_owned()))
}
}
SyntaxKind::HexString => {
let token = self.advance();
let full_text = self.text(token.span);
let hex_part = strip_string_literal(&full_text);
let normalized: String = hex_part
.chars()
.filter(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
.collect();
match u64::from_str_radix(&normalized, 16) {
Ok(v) => Ok(RangeValue::Unsigned(v)),
Err(_) => {
self.emit_diagnostic(
DiagCode::InvalidHexRange,
token.span,
"invalid hex range value",
);
Ok(RangeValue::Raw(full_text.into_owned()))
}
}
}
SyntaxKind::UppercaseIdent | SyntaxKind::ForbiddenKeyword => {
let token = self.advance();
Ok(RangeValue::Named(self.make_ident(token)))
}
_ => Err(self.make_error("expected range value".to_string())),
}
}
fn parse_named_numbers(&mut self) -> Result<Vec<NamedNumber>, Diagnostic> {
self.expect(SyntaxKind::LBrace)?;
let list = self.parse_named_number_list()?;
self.expect(SyntaxKind::RBrace)?;
Ok(list)
}
fn parse_named_number_list(&mut self) -> Result<Vec<NamedNumber>, Diagnostic> {
let mut items = Vec::new();
loop {
if self.check(SyntaxKind::RBrace) || self.is_eof() {
break;
}
let nn_start = self.current_span();
let label_token = self.expect_enum_label()?;
let label = self.make_ident(label_token);
self.expect(SyntaxKind::LParen)?;
let value = if self.check(SyntaxKind::NegativeNumber) {
let num_token = self.advance();
self.parse_i64(num_token.span, "named number")
} else {
let num_token = self.expect(SyntaxKind::Number)?;
self.parse_i64(num_token.span, "named number")
};
self.expect(SyntaxKind::RParen)?;
let nn_span = cover(nn_start, self.last_span);
items.push(NamedNumber {
name: label,
value,
span: nn_span,
});
if self.check(SyntaxKind::Comma) {
self.advance();
} else if !self.check(SyntaxKind::RBrace) && !self.is_eof() {
self.emit_diagnostic(
DiagCode::MissingComma,
self.current_span(),
"missing comma in named number list",
);
}
}
Ok(items)
}
fn parse_sequence_fields(&mut self) -> Result<Vec<SequenceField>, Diagnostic> {
let mut fields = Vec::new();
while !self.check(SyntaxKind::RBrace) && !self.is_eof() {
let field_start = self.current_span();
let name_token = self.expect_identifier()?;
let name = self.make_ident(name_token);
let syntax = self.parse_type_syntax()?;
let field_span = cover(field_start, syntax.span());
fields.push(SequenceField {
name,
syntax,
span: field_span,
});
if self.check(SyntaxKind::Comma) {
self.advance();
}
}
Ok(fields)
}
fn parse_oid_assignment(&mut self) -> Result<OidAssignment, Diagnostic> {
let start = self.current_span();
self.expect(SyntaxKind::LBrace)?;
let mut components = Vec::new();
while !self.check(SyntaxKind::RBrace) && !self.is_eof() {
let comp = self.parse_oid_component("OID component")?;
components.push(comp);
}
self.expect(SyntaxKind::RBrace)?;
let span = cover(start, self.last_span);
Ok(OidAssignment { components, span })
}
fn parse_oid_component(
&mut self,
named_number_context: &str,
) -> Result<OidComponent, Diagnostic> {
match self.peek().kind {
SyntaxKind::Number => {
let token = self.advance();
let value = self.parse_u32(token.span, "OID component");
Ok(OidComponent::Number {
value,
span: token.span,
})
}
SyntaxKind::LowercaseIdent | SyntaxKind::UppercaseIdent => {
let token = self.advance();
let ident = self.make_ident(token);
if self.check(SyntaxKind::Dot) && token.kind == SyntaxKind::UppercaseIdent {
let next = self.peek_nth(1).kind;
if next == SyntaxKind::LowercaseIdent || next == SyntaxKind::UppercaseIdent {
self.advance(); let name_token = self.advance();
let name_ident = self.make_ident(name_token);
if self.check(SyntaxKind::LParen) {
self.advance();
let num_token = self.expect(SyntaxKind::Number)?;
let num = self.parse_u32(num_token.span, named_number_context);
self.expect(SyntaxKind::RParen)?;
let span = cover(ident.span, self.last_span);
return Ok(OidComponent::QualifiedNamedNumber {
module_name: ident,
name: name_ident,
num,
span,
});
}
let span = cover(ident.span, name_ident.span);
return Ok(OidComponent::QualifiedName {
module_name: ident,
name: name_ident,
span,
});
}
}
if self.check(SyntaxKind::LParen) {
self.advance();
let num_token = self.expect(SyntaxKind::Number)?;
let num = self.parse_u32(num_token.span, named_number_context);
self.expect(SyntaxKind::RParen)?;
let span = cover(ident.span, self.last_span);
return Ok(OidComponent::NamedNumber {
name: ident,
num,
span,
});
}
Ok(OidComponent::Name(ident))
}
_ => Err(self.make_error("expected OID component".to_string())),
}
}
}
fn strip_string_literal(s: &str) -> &str {
let s = s.strip_prefix('\'').unwrap_or(s);
if let Some(pos) = s.rfind('\'') {
&s[..pos]
} else {
s
}
}
pub fn parse(document: &SourceDocument, diag_config: &DiagnosticConfig) -> Vec<Module> {
let span = info_span!(
target: "mib_rs::parser",
"parse",
component = "parser",
source = document.label(),
byte_count = document.bytes().len(),
reporting = ?diag_config.reporting,
);
let _guard = span.enter();
let mut parser = Parser::new(document, diag_config);
let modules = parser.parse_modules();
debug!(
target: "mib_rs::parser",
component = "parser",
module_count = modules.len(),
"parse complete",
);
modules
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::{SourceOrigin, SourceSet};
use crate::types::Severity;
use std::sync::Arc;
fn parse_with_config(input: &str, config: &DiagnosticConfig) -> Vec<Module> {
let mut sources = SourceSet::new();
let id = sources
.insert(
SourceOrigin::memory("parser-test"),
"parser-test",
Arc::from(input.as_bytes()),
)
.unwrap();
parse(sources.get(id).unwrap(), config)
}
fn parse_str(input: &str) -> Vec<Module> {
parse_with_config(input, &DiagnosticConfig::default())
}
fn parse_strict(input: &str) -> Vec<Module> {
parse_with_config(input, &DiagnosticConfig::verbose())
}
#[test]
fn empty_input() {
let modules = parse_str("");
assert_eq!(modules.len(), 1);
assert!(modules[0].name.is_none());
assert_eq!(modules[0].span.byte_range(), 0..0);
}
#[test]
fn minimal_module() {
let input = "TEST-MIB DEFINITIONS ::= BEGIN\nEND\n";
let modules = parse_str(input);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "TEST-MIB");
assert!(modules[0].imports.is_empty());
assert!(modules[0].body.is_empty());
}
#[test]
fn lexer_diagnostic_in_initial_lookahead_is_preserved() {
let input = "TEST-MIB { 01 } DEFINITIONS ::= BEGIN\nEND\n";
let modules = parse_strict(input);
assert_eq!(modules.len(), 1);
let diagnostics: Vec<_> = modules[0]
.diagnostics
.iter()
.filter(|diag| diag.code == DiagCode::NumberLeadingZero)
.collect();
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].range.unwrap().byte_range(), 11..13);
assert_eq!(
diagnostics[0].range.unwrap().source(),
modules[0].span.source()
);
assert_eq!(diagnostics[0].module.as_deref(), Some("TEST-MIB"));
}
#[test]
fn malformed_terminal_header_preserves_lookahead_lexer_diagnostic() {
let input = "BAD FOO 01\n";
let modules = parse_strict(input);
assert_eq!(modules.len(), 1);
assert!(modules[0].name.is_none());
let diagnostics: Vec<_> = modules[0]
.diagnostics
.iter()
.filter(|diag| diag.code == DiagCode::NumberLeadingZero)
.collect();
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].range.unwrap().byte_range(), 8..10);
assert_eq!(
diagnostics[0].range.unwrap().source(),
modules[0].span.source()
);
}
#[test]
fn leading_skipped_lexer_diagnostic_is_attached_to_first_module() {
let input = "@\nTEST-MIB DEFINITIONS ::= BEGIN\nEND\n";
let modules = parse_strict(input);
assert_eq!(modules.len(), 1);
let diagnostics: Vec<_> = modules[0]
.diagnostics
.iter()
.filter(|diag| diag.code == DiagCode::UnexpectedCharacter)
.collect();
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].range.unwrap().byte_range(), 0..1);
assert_eq!(
diagnostics[0].range.unwrap().source(),
modules[0].span.source()
);
}
#[test]
fn trailing_skipped_lexer_diagnostic_is_attached_to_last_module() {
let input = "TEST-MIB DEFINITIONS ::= BEGIN\n@";
let modules = parse_strict(input);
assert_eq!(modules.len(), 1);
let diagnostics: Vec<_> = modules[0]
.diagnostics
.iter()
.filter(|diag| diag.code == DiagCode::UnexpectedCharacter)
.collect();
assert_eq!(diagnostics.len(), 1);
let at = input.find('@').unwrap();
assert_eq!(diagnostics[0].range.unwrap().byte_range(), at..at + 1);
assert_eq!(
diagnostics[0].range.unwrap().source(),
modules[0].span.source()
);
assert!(
modules[0]
.diagnostics
.iter()
.any(|diag| diag.code == DiagCode::ParseError)
);
}
#[test]
fn lexer_diagnostic_from_next_module_is_not_attached_to_previous_module() {
let input = "FIRST-MIB DEFINITIONS ::= BEGIN\nEND\n\
SECOND-MIB { 02 } DEFINITIONS ::= BEGIN\nEND\n";
let modules = parse_strict(input);
assert_eq!(modules.len(), 2);
assert_eq!(modules[0].name.as_ref().unwrap().name, "FIRST-MIB");
assert_eq!(modules[1].name.as_ref().unwrap().name, "SECOND-MIB");
assert!(
modules[0]
.diagnostics
.iter()
.all(|diag| diag.code != DiagCode::NumberLeadingZero)
);
let diagnostics: Vec<_> = modules[1]
.diagnostics
.iter()
.filter(|diag| diag.code == DiagCode::NumberLeadingZero)
.collect();
assert_eq!(diagnostics.len(), 1);
let number_start = input.find("02").unwrap();
assert_eq!(
diagnostics[0].range.unwrap().byte_range(),
number_start..number_start + 2
);
assert_eq!(
diagnostics[0].range.unwrap().source(),
modules[1].span.source()
);
}
#[test]
fn module_with_imports() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32
FROM SNMPv2-SMI
DisplayString
FROM SNMPv2-TC;
END
"#;
let modules = parse_str(input);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].imports.len(), 2);
assert_eq!(modules[0].imports[0].symbols.len(), 3);
assert_eq!(modules[0].imports[0].from_module.name, "SNMPv2-SMI");
assert_eq!(modules[0].imports[1].symbols.len(), 1);
assert_eq!(modules[0].imports[1].symbols[0].name, "DisplayString");
assert_eq!(modules[0].imports[1].from_module.name, "SNMPv2-TC");
}
#[test]
fn many_exports_clauses_are_skipped_iteratively() {
const EXPORTS_COUNT: usize = 100_000;
let mut input = String::from("TEST-MIB DEFINITIONS ::= BEGIN\n");
for _ in 0..EXPORTS_COUNT {
input.push_str("EXPORTS;\n");
}
input.push_str("testObj OBJECT IDENTIFIER ::= { iso 3 }\nEND\n");
let modules = parse_str(&input);
assert_eq!(modules.len(), 1);
assert!(modules[0].diagnostics.is_empty());
assert_eq!(modules[0].body.len(), 1);
assert!(matches!(
&modules[0].body[0],
Definition::ValueAssignment(d) if d.name.name == "testObj"
));
}
#[test]
fn exports_comment_semicolons_do_not_expose_the_remaining_body() {
for exports_body in ["foo -- ignored;\nbar;", "foo -- ignored; -- bar;"] {
let input = format!(
"TEST-MIB DEFINITIONS ::= BEGIN\nEXPORTS {exports_body}\ntestObj OBJECT IDENTIFIER ::= {{ iso 3 }}\nEND\n"
);
let modules = parse_str(&input);
assert_eq!(modules.len(), 1);
assert!(modules[0].diagnostics.is_empty());
assert_eq!(modules[0].body.len(), 1);
assert!(matches!(
&modules[0].body[0],
Definition::ValueAssignment(d) if d.name.name == "testObj"
));
}
}
#[test]
fn value_assignment() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT IDENTIFIER ::= { iso 3 }
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
assert!(matches!(
&modules[0].body[0],
Definition::ValueAssignment(d) if d.name.name == "testObj"
));
}
#[test]
fn object_type() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
ifIndex OBJECT-TYPE
SYNTAX INTEGER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "The index."
::= { ifEntry 1 }
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
assert_eq!(d.name.name, "ifIndex");
assert!(d.syntax.is_some());
assert!(d.access.is_some());
assert!(d.status.is_some());
assert_eq!(d.status.as_ref().unwrap().value, Status::Current);
assert!(d.description.is_some());
assert_eq!(d.oid.components.len(), 2);
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn module_identity() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testMIB MODULE-IDENTITY
LAST-UPDATED "200606140000Z"
ORGANIZATION "Test"
CONTACT-INFO "test@test"
DESCRIPTION "A test MIB."
REVISION "200606140000Z"
DESCRIPTION "Initial version."
::= { enterprises 1 }
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
match &modules[0].body[0] {
Definition::ModuleIdentity(d) => {
assert_eq!(d.name.name, "testMIB");
assert_eq!(d.last_updated.value, "200606140000Z");
assert_eq!(d.revisions.len(), 1);
}
other => panic!("expected ModuleIdentity, got {:?}", other),
}
}
#[test]
fn quoted_string_decodes_doubled_quotes_but_retains_raw_range() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testMIB MODULE-IDENTITY
LAST-UPDATED "200606140000Z"
ORGANIZATION "The ""quoted"" organization"
CONTACT-INFO "test@test"
DESCRIPTION "A test MIB."
::= { enterprises 1 }
END
"#;
let modules = parse_str(input);
let Definition::ModuleIdentity(identity) = &modules[0].body[0] else {
panic!("expected module identity");
};
assert_eq!(identity.organization.value, "The \"quoted\" organization");
assert_eq!(
&input[identity.organization.span.byte_range()],
r#""The ""quoted"" organization""#
);
assert!(modules[0].diagnostics.is_empty());
}
#[test]
fn type_assignment() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestType ::= INTEGER (0..255)
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => {
assert_eq!(d.name.name, "TestType");
assert!(matches!(d.syntax, TypeSyntax::Constrained { .. }));
}
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn textual_convention() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
MyString ::= TEXTUAL-CONVENTION
STATUS current
DESCRIPTION "A string."
SYNTAX OCTET STRING (SIZE (0..255))
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
assert!(matches!(
&modules[0].body[0],
Definition::TextualConvention(d) if d.name.name == "MyString"
));
}
#[test]
fn notification_type() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testNotif NOTIFICATION-TYPE
OBJECTS { testObj }
STATUS current
DESCRIPTION "A notification."
::= { testMIB 1 }
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
match &modules[0].body[0] {
Definition::NotificationType(d) => {
assert_eq!(d.name.name, "testNotif");
assert_eq!(d.objects.len(), 1);
}
other => panic!("expected NotificationType, got {:?}", other),
}
}
#[test]
fn trap_type() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testTrap TRAP-TYPE
ENTERPRISE testEnterprise
VARIABLES { testObj }
DESCRIPTION "A trap."
::= 1
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
match &modules[0].body[0] {
Definition::TrapType(d) => {
assert_eq!(d.name.name, "testTrap");
assert_eq!(d.enterprise.name, "testEnterprise");
assert_eq!(d.trap_number, 1);
assert_eq!(d.variables.len(), 1);
}
other => panic!("expected TrapType, got {:?}", other),
}
}
#[test]
fn object_group() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testGroup OBJECT-GROUP
OBJECTS { testObj1, testObj2 }
STATUS current
DESCRIPTION "A group."
::= { testMIB 2 }
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 1);
match &modules[0].body[0] {
Definition::ObjectGroup(d) => {
assert_eq!(d.name.name, "testGroup");
assert_eq!(d.objects.len(), 2);
}
other => panic!("expected ObjectGroup, got {:?}", other),
}
}
#[test]
fn error_recovery() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
@@@ garbage @@@
testObj OBJECT IDENTIFIER ::= { iso 3 }
END
"#;
let modules = parse_str(input);
assert!(!modules[0].body.is_empty() || !modules[0].diagnostics.is_empty());
}
#[test]
fn multiple_definitions() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestType ::= INTEGER
testObj OBJECT IDENTIFIER ::= { iso 3 }
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 2);
}
#[test]
fn oid_components() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT IDENTIFIER ::= { iso org(3) dod(6) 1 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::ValueAssignment(d) => {
assert_eq!(d.oid.components.len(), 4);
assert!(matches!(&d.oid.components[0], OidComponent::Name(id) if id.name == "iso"));
assert!(
matches!(&d.oid.components[1], OidComponent::NamedNumber { name, num, .. } if name.name == "org" && *num == 3)
);
}
other => panic!("expected ValueAssignment, got {:?}", other),
}
}
#[test]
fn integer_enum_syntax() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestStatus ::= INTEGER { up(1), down(2), testing(3) }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => {
match &d.syntax {
TypeSyntax::IntegerEnum {
base,
named_numbers,
..
} => {
assert!(base.is_none()); assert_eq!(named_numbers.len(), 3);
assert_eq!(named_numbers[0].name.name, "up");
assert_eq!(named_numbers[0].value, 1);
}
other => panic!("expected IntegerEnum, got {:?}", other),
}
}
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn sequence_of_type() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestTable ::= SEQUENCE OF TestEntry
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => {
assert!(
matches!(&d.syntax, TypeSyntax::SequenceOf { entry_type, .. } if entry_type.name == "TestEntry")
);
}
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn object_type_with_index() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testEntry OBJECT-TYPE
SYNTAX TestEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "A row."
INDEX { testIndex }
::= { testTable 1 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
assert!(d.index.is_some());
assert_eq!(d.index.as_ref().unwrap().items.len(), 1);
assert_eq!(d.index.as_ref().unwrap().items[0].object.name, "testIndex");
assert!(!d.index.as_ref().unwrap().items[0].implied);
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn object_type_with_augments() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testEntry OBJECT-TYPE
SYNTAX TestEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "A row."
AUGMENTS { otherEntry }
::= { testTable 1 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
assert!(d.augments.is_some());
assert_eq!(d.augments.as_ref().unwrap().target.name, "otherEntry");
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn size_constraint() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestStr ::= OCTET STRING (SIZE (0..255))
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::Constrained {
base, constraint, ..
} => {
assert!(matches!(**base, TypeSyntax::OctetString { .. }));
match constraint {
Constraint::Size { ranges, .. } => {
assert_eq!(ranges.len(), 1);
}
other => panic!("expected Size constraint, got {:?}", other),
}
}
other => panic!("expected Constrained, got {:?}", other),
},
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn defval_integer() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX INTEGER
MAX-ACCESS read-write
STATUS current
DESCRIPTION "test"
DEFVAL { 42 }
::= { test 1 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
assert!(d.defval.is_some());
assert!(matches!(
d.defval.as_ref().unwrap().value,
DefVal::Integer(42)
));
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn defval_bits() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX BITS { flag1(0), flag2(1) }
MAX-ACCESS read-write
STATUS current
DESCRIPTION "test"
DEFVAL { { flag1 } }
::= { test 1 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
assert!(d.defval.is_some());
match &d.defval.as_ref().unwrap().value {
DefVal::Bits { labels, span } => {
assert_eq!(labels.len(), 1);
assert_eq!(labels[0].name, "flag1");
let start = input.rfind("flag1 }").unwrap();
let end = start + "flag1 ".len();
assert_eq!(span.byte_range(), start..end);
assert_eq!(&input[span.byte_range()], "flag1 ");
}
other => panic!("expected Bits DEFVAL, got {:?}", other),
}
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn identifier_underscore_diagnostic() {
let input = r#"TEST_MIB DEFINITIONS ::= BEGIN
END
"#;
let modules = parse_strict(input);
assert!(
modules[0]
.diagnostics
.iter()
.any(|d| d.code == DiagCode::IdentifierUnderscore)
);
}
#[test]
fn macro_definition_skipped() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
OBJECT-TYPE MACRO ::= BEGIN
TYPE NOTATION ::= stuff stuff
END
testObj OBJECT IDENTIFIER ::= { iso 3 }
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 2);
assert!(matches!(
&modules[0].body[0],
Definition::MacroDefinition(_)
));
assert!(matches!(
&modules[0].body[1],
Definition::ValueAssignment(_)
));
}
#[test]
fn macro_definition_quoted_end_preserves_following_definition() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
FOO MACRO ::= BEGIN
TYPE NOTATION ::= "END"
VALUE NOTATION ::= value(VALUE ObjectName)
END
testObj OBJECT IDENTIFIER ::= { iso 3 }
END
"#;
let modules = parse_strict(input);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].body.len(), 2);
assert!(matches!(
&modules[0].body[0],
Definition::MacroDefinition(_)
));
assert!(matches!(
&modules[0].body[1],
Definition::ValueAssignment(d) if d.name.name == "testObj"
));
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::UnterminatedString),
"quoted END in a MACRO body must not corrupt following syntax"
);
}
#[test]
fn tagged_type() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestType ::= [APPLICATION 0] IMPLICIT OCTET STRING
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => {
assert!(matches!(&d.syntax, TypeSyntax::Tagged { .. }));
}
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn module_compliance() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testCompliance MODULE-COMPLIANCE
STATUS current
DESCRIPTION "Test compliance."
MODULE
MANDATORY-GROUPS { testGroup }
GROUP testOptGroup
DESCRIPTION "Optional group."
OBJECT testObj
MIN-ACCESS read-only
DESCRIPTION "Object refinement."
::= { testMIB 3 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::ModuleCompliance(d) => {
assert_eq!(d.modules.len(), 1);
assert_eq!(d.modules[0].mandatory_groups.len(), 1);
assert_eq!(d.modules[0].compliances.len(), 2);
}
other => panic!("expected ModuleCompliance, got {:?}", other),
}
}
#[test]
fn agent_capabilities() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testCapability AGENT-CAPABILITIES
PRODUCT-RELEASE "1.0"
STATUS current
DESCRIPTION "Test capability."
SUPPORTS SNMPv2-MIB
INCLUDES { systemGroup }
VARIATION sysDescr
ACCESS read-only
DESCRIPTION "Read-only."
::= { testMIB 4 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::AgentCapabilities(d) => {
assert_eq!(d.supports.len(), 1);
assert_eq!(d.supports[0].includes.len(), 1);
assert_eq!(d.supports[0].variations.len(), 1);
}
other => panic!("expected AgentCapabilities, got {:?}", other),
}
}
#[test]
fn range_constraint_with_pipe() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestType ::= INTEGER (1..10 | 20..30)
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::Constrained { constraint, .. } => match constraint {
Constraint::Range { ranges, .. } => {
assert_eq!(ranges.len(), 2);
}
other => panic!("expected Range, got {:?}", other),
},
other => panic!("expected Constrained, got {:?}", other),
},
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn strip_string_literal_helper() {
assert_eq!(strip_string_literal("'0A1B'H"), "0A1B");
assert_eq!(strip_string_literal("'01010101'B"), "01010101");
assert_eq!(strip_string_literal("''H"), "");
}
#[test]
fn two_modules_in_one_file() {
let input = r#"MOD-A DEFINITIONS ::= BEGIN
END
MOD-B DEFINITIONS ::= BEGIN
END
"#;
let modules = parse_str(input);
assert_eq!(modules.len(), 2);
assert_eq!(modules[0].name.as_ref().unwrap().name, "MOD-A");
assert_eq!(modules[1].name.as_ref().unwrap().name, "MOD-B");
}
#[test]
fn choice_type() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestChoice ::= CHOICE { a INTEGER, b OCTET STRING }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::Choice { alternatives, .. } => {
assert_eq!(alternatives.len(), 2);
}
other => panic!("expected Choice, got {:?}", other),
},
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn null_type_does_not_admit_other_forbidden_keywords() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
SimpleSyntax ::= CHOICE { number INTEGER, empty NULL }
RejectedSyntax ::= TRUE
END
"#;
let modules = parse_strict(input);
assert!(matches!(
&modules[0].body[0],
Definition::TypeAssignment(d)
if matches!(
&d.syntax,
TypeSyntax::Choice { alternatives, .. }
if matches!(
&alternatives[1].syntax,
TypeSyntax::TypeRef(ident) if ident.name == "NULL"
)
)
));
assert!(
modules[0]
.diagnostics
.iter()
.any(|d| d.code == DiagCode::ParseError),
"other reserved ASN.1 words must not be accepted as named types"
);
}
#[test]
fn type_keyword_type_assignment() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
IpAddress ::= [APPLICATION 0] IMPLICIT OCTET STRING (SIZE (4))
Counter32 ::= [APPLICATION 1] IMPLICIT INTEGER (0..4294967295)
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].body.len(), 2);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => assert_eq!(d.name.name, "IpAddress"),
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn named_number_missing_comma() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestStatus ::= INTEGER { up(1) down(2) testing(3) }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
let missing_comma_count = modules[0]
.diagnostics
.iter()
.filter(|d| d.code == DiagCode::MissingComma)
.count();
assert_eq!(
missing_comma_count, 2,
"expected 2 missing-comma diagnostics"
);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::IntegerEnum { named_numbers, .. } => {
assert_eq!(named_numbers.len(), 3);
}
other => panic!("expected IntegerEnum, got {:?}", other),
},
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn negative_named_number() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestType ::= INTEGER { negative(-1), zero(0), positive(1) }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::IntegerEnum { named_numbers, .. } => {
assert_eq!(named_numbers.len(), 3);
assert_eq!(named_numbers[0].value, -1);
assert_eq!(named_numbers[1].value, 0);
assert_eq!(named_numbers[2].value, 1);
}
other => panic!("expected IntegerEnum, got {:?}", other),
},
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn qualified_oid_component() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT IDENTIFIER ::= { SNMPv2-SMI.enterprises 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ValueAssignment(d) => {
assert_eq!(d.oid.components.len(), 2);
assert!(matches!(
&d.oid.components[0],
OidComponent::QualifiedName { module_name, name, .. }
if module_name.name == "SNMPv2-SMI" && name.name == "enterprises"
));
}
other => panic!("expected ValueAssignment, got {:?}", other),
}
}
#[test]
fn constrained_type_ref() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX DisplayString (SIZE (0..255))
MAX-ACCESS read-only
STATUS current
DESCRIPTION "test"
::= { test 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
let syntax = d.syntax.as_ref().unwrap();
match &syntax.syntax {
TypeSyntax::Constrained {
base, constraint, ..
} => {
assert!(
matches!(base.as_ref(), TypeSyntax::TypeRef(id) if id.name == "DisplayString")
);
assert!(matches!(constraint, Constraint::Size { .. }));
}
other => panic!("expected Constrained, got {:?}", other),
}
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn restricted_integer_enum() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX RowStatus { active(1), notInService(2) }
MAX-ACCESS read-write
STATUS current
DESCRIPTION "test"
::= { test 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
let syntax = d.syntax.as_ref().unwrap();
match &syntax.syntax {
TypeSyntax::IntegerEnum {
base,
named_numbers,
..
} => {
assert!(base.is_some());
assert_eq!(base.as_ref().unwrap().name, "RowStatus");
assert_eq!(named_numbers.len(), 2);
}
other => panic!("expected IntegerEnum, got {:?}", other),
}
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn object_type_smiv1_access() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX INTEGER
ACCESS read-only
STATUS mandatory
DESCRIPTION "SMIv1 style."
::= { test 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
let access = d.access.as_ref().unwrap();
assert_eq!(access.keyword, AccessKeyword::Access);
assert_eq!(access.value, Access::ReadOnly);
assert_eq!(d.status.as_ref().unwrap().value, Status::Mandatory);
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn defval_hex_string() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX OCTET STRING
MAX-ACCESS read-write
STATUS current
DESCRIPTION "test"
DEFVAL { 'FF00'H }
::= { test 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectType(d) => match &d.defval.as_ref().unwrap().value {
DefVal::HexString { content, .. } => assert_eq!(content, "FF00"),
other => panic!("expected HexString DEFVAL, got {:?}", other),
},
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn defval_oid() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX OBJECT IDENTIFIER
MAX-ACCESS read-write
STATUS current
DESCRIPTION "test"
DEFVAL { { 1 3 6 1 } }
::= { test 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectType(d) => match &d.defval.as_ref().unwrap().value {
DefVal::ObjectIdentifier { components, span } => {
assert_eq!(components.len(), 4);
let start = input.find("1 3 6 1 }").unwrap();
let end = start + "1 3 6 1 ".len();
assert_eq!(span.byte_range(), start..end);
assert_eq!(&input[span.byte_range()], "1 3 6 1 ");
}
other => panic!("expected OID DEFVAL, got {:?}", other),
},
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn defval_oid_first_ident_and_qualified_component_range() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX OBJECT IDENTIFIER
MAX-ACCESS read-write
STATUS current
DESCRIPTION "test"
DEFVAL { { iso(1) SNMPv2-SMI.enterprises 7 } }
::= { test 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectType(d) => match &d.defval.as_ref().unwrap().value {
DefVal::ObjectIdentifier { components, span } => {
assert_eq!(components.len(), 3);
assert!(matches!(components[0], OidComponent::NamedNumber { .. }));
assert!(matches!(components[1], OidComponent::QualifiedName { .. }));
let start = input.find("iso(1) SNMPv2-SMI.enterprises 7 }").unwrap();
let end = start + "iso(1) SNMPv2-SMI.enterprises 7 ".len();
assert_eq!(span.byte_range(), start..end);
assert_eq!(
&input[span.byte_range()],
"iso(1) SNMPv2-SMI.enterprises 7 "
);
}
other => panic!("expected OID DEFVAL, got {:?}", other),
},
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn object_identity() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testOID OBJECT-IDENTITY
STATUS current
DESCRIPTION "An OID registration."
::= { testMIB 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectIdentity(d) => {
assert_eq!(d.name.name, "testOID");
}
other => panic!("expected ObjectIdentity, got {:?}", other),
}
}
#[test]
fn notification_group() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testNotifGroup NOTIFICATION-GROUP
NOTIFICATIONS { testNotif1, testNotif2 }
STATUS current
DESCRIPTION "A notification group."
::= { testMIB 5 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::NotificationGroup(d) => {
assert_eq!(d.name.name, "testNotifGroup");
assert_eq!(d.notifications.len(), 2);
}
other => panic!("expected NotificationGroup, got {:?}", other),
}
}
#[test]
fn hex_range_value() {
let input = "TEST-MIB DEFINITIONS ::= BEGIN\n\
Whitespace ::= INTEGER ('7f ff'H | '7f\tff'H | '7f\rff'H | '7f\nff'H)\n\
Malformed ::= INTEGER ('0G'H..'10000000000000000'H)\n\
END\n";
let modules = parse_strict(input);
let module = &modules[0];
assert!(
module
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
let ranges = match &module.body[0] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::Constrained {
constraint: Constraint::Range { ranges, .. },
..
} => ranges,
other => panic!("expected constrained range, got {other:?}"),
},
other => panic!("expected TypeAssignment, got {other:?}"),
};
assert_eq!(ranges.len(), 4);
assert!(
ranges.iter().all(|range| {
matches!(range.min, RangeValue::Unsigned(value) if value == 0x7fff)
})
);
let malformed = match &module.body[1] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::Constrained {
constraint: Constraint::Range { ranges, .. },
..
} => &ranges[0],
other => panic!("expected constrained range, got {other:?}"),
},
other => panic!("expected TypeAssignment, got {other:?}"),
};
assert_eq!(malformed.min, RangeValue::Raw("'0G'H".to_string()));
assert_eq!(
malformed.max,
Some(RangeValue::Raw("'10000000000000000'H".to_string()))
);
assert_eq!(
module
.diagnostics
.iter()
.filter(|d| d.code == DiagCode::InvalidHexRange)
.count(),
2
);
let mut ignored = DiagnosticConfig::verbose();
ignored.ignore.push("invalid-hex-range".to_string());
let ignored_modules = parse_with_config(input, &ignored);
assert!(
ignored_modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::InvalidHexRange)
);
}
#[test]
fn overflowing_decimal_range_values_preserve_raw_source() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
Overflow ::= INTEGER (18446744073709551616)
Underflow ::= INTEGER (-9223372036854775809)
END
"#;
let modules = parse_strict(input);
let module = &modules[0];
let raw_values: Vec<&str> = module
.body
.iter()
.map(|definition| match definition {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::Constrained {
constraint: Constraint::Range { ranges, .. },
..
} => match &ranges[0].min {
RangeValue::Raw(raw) => raw.as_str(),
other => panic!("expected raw range value, got {other:?}"),
},
other => panic!("expected constrained range, got {other:?}"),
},
other => panic!("expected type assignment, got {other:?}"),
})
.collect();
assert_eq!(raw_values, ["18446744073709551616", "-9223372036854775809"]);
assert_eq!(
module
.diagnostics
.iter()
.filter(|d| d.code == DiagCode::UnknownRangeValue)
.count(),
2
);
assert!(
module
.diagnostics
.iter()
.all(|d| d.code != DiagCode::InvalidI64)
);
}
#[test]
fn keyword_as_enum_label() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
TestType ::= INTEGER { current(1), deprecated(2), optional(3) }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::TypeAssignment(d) => match &d.syntax {
TypeSyntax::IntegerEnum { named_numbers, .. } => {
assert_eq!(named_numbers.len(), 3);
assert_eq!(named_numbers[0].name.name, "current");
assert_eq!(named_numbers[1].name.name, "deprecated");
assert_eq!(named_numbers[2].name.name, "optional");
}
other => panic!("expected IntegerEnum, got {:?}", other),
},
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn object_identifier_syntax() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX OBJECT IDENTIFIER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "An OID."
::= { test 1 }
END
"#;
let modules = parse_str(input);
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
let syntax = d.syntax.as_ref().unwrap();
assert!(matches!(
&syntax.syntax,
TypeSyntax::ObjectIdentifier { .. }
));
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn module_oid_before_definitions() {
let input = r#"TEST-MIB { iso 3 } DEFINITIONS ::= BEGIN
END
"#;
let modules = parse_str(input);
assert_eq!(modules[0].name.as_ref().unwrap().name, "TEST-MIB");
assert!(
modules[0]
.diagnostics
.iter()
.all(|d| d.code != DiagCode::ParseError)
);
}
#[test]
fn implied_index() {
let input = r#"TEST-MIB DEFINITIONS ::= BEGIN
testEntry OBJECT-TYPE
SYNTAX TestEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "row"
INDEX { testIdx, IMPLIED testName }
::= { testTable 1 }
END
"#;
let modules = parse_str(input);
match &modules[0].body[0] {
Definition::ObjectType(d) => {
let index = d.index.as_ref().unwrap();
assert_eq!(index.items.len(), 2);
assert!(!index.items[0].implied);
assert!(index.items[1].implied);
assert_eq!(index.items[1].object.name, "testName");
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn integral_overflow_retains_zero_and_following_definitions() {
let input = r#"OVERFLOW-MIB DEFINITIONS ::= BEGIN
OverflowEnum ::= INTEGER { high(9223372036854775808), low(-9223372036854775809), ok(1) }
OverflowBits ::= BITS { high(9223372036854775809), low(-9223372036854775810), ok(2) }
overflowOid OBJECT IDENTIFIER ::= { 4294967296 named(4294967297) REMOTE-MIB.qualified(4294967298) }
positiveDefault OBJECT-TYPE
SYNTAX INTEGER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "positive"
DEFVAL { 18446744073709551616 }
::= { overflowOid 1 }
validUnsignedDefault OBJECT-TYPE
SYNTAX Counter64
MAX-ACCESS read-only
STATUS current
DESCRIPTION "valid unsigned"
DEFVAL { 18446744073709551615 }
::= { overflowOid 4 }
negativeDefault OBJECT-TYPE
SYNTAX INTEGER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "negative"
DEFVAL { -9223372036854775811 }
::= { overflowOid 2 }
oidDefault OBJECT-TYPE
SYNTAX OBJECT IDENTIFIER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "OID"
DEFVAL { { first(4294967299) 4294967300 REMOTE-MIB.qualified(4294967301) later(4294967302) } }
::= { overflowOid 3 }
numericFirstDefault OBJECT-TYPE
SYNTAX OBJECT IDENTIFIER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "numeric-first OID"
DEFVAL { { 4294967304 later(4294967305) } }
::= { overflowOid 5 }
overflowTrap TRAP-TYPE
ENTERPRISE overflowOid
DESCRIPTION "trap"
::= 4294967303
after OBJECT IDENTIFIER ::= { overflowOid 7 }
END
"#;
let modules = parse_strict(input);
assert_eq!(modules.len(), 1);
let module = &modules[0];
assert_eq!(module.body.len(), 10);
assert!(
module
.diagnostics
.iter()
.all(|diagnostic| diagnostic.code != DiagCode::ParseError)
);
let definition = |name: &str| {
module
.body
.iter()
.find(|definition| definition.name().is_some_and(|ident| ident.name == name))
.unwrap_or_else(|| panic!("missing definition {name}"))
};
for name in ["OverflowEnum", "OverflowBits"] {
let numbers = match definition(name) {
Definition::TypeAssignment(definition) => match &definition.syntax {
TypeSyntax::IntegerEnum { named_numbers, .. }
| TypeSyntax::Bits {
named_bits: named_numbers,
..
} => named_numbers,
other => panic!("expected named-number syntax, got {other:?}"),
},
other => panic!("expected type assignment, got {other:?}"),
};
assert_eq!(numbers.len(), 3);
assert_eq!(numbers[0].value, 0);
assert_eq!(numbers[1].value, 0);
assert_eq!(numbers[2].value, if name == "OverflowEnum" { 1 } else { 2 });
}
let oid = match definition("overflowOid") {
Definition::ValueAssignment(definition) => &definition.oid.components,
other => panic!("expected value assignment, got {other:?}"),
};
assert!(matches!(oid[0], OidComponent::Number { value: 0, .. }));
assert!(matches!(oid[1], OidComponent::NamedNumber { num: 0, .. }));
assert!(matches!(
oid[2],
OidComponent::QualifiedNamedNumber { num: 0, .. }
));
for name in ["positiveDefault", "negativeDefault"] {
let defval = match definition(name) {
Definition::ObjectType(definition) => &definition.defval.as_ref().unwrap().value,
other => panic!("expected object type, got {other:?}"),
};
assert_eq!(defval, &DefVal::Integer(0));
}
let unsigned_defval = match definition("validUnsignedDefault") {
Definition::ObjectType(definition) => &definition.defval.as_ref().unwrap().value,
other => panic!("expected object type, got {other:?}"),
};
assert_eq!(unsigned_defval, &DefVal::Unsigned(u64::MAX));
let oid_defval = match definition("oidDefault") {
Definition::ObjectType(definition) => &definition.defval.as_ref().unwrap().value,
other => panic!("expected object type, got {other:?}"),
};
let components = match oid_defval {
DefVal::ObjectIdentifier { components, .. } => components,
other => panic!("expected OID DEFVAL, got {other:?}"),
};
assert!(matches!(
components.as_slice(),
[
OidComponent::NamedNumber { num: 0, .. },
OidComponent::Number { value: 0, .. },
OidComponent::QualifiedNamedNumber { num: 0, .. },
OidComponent::NamedNumber { num: 0, .. }
]
));
let numeric_first_defval = match definition("numericFirstDefault") {
Definition::ObjectType(definition) => &definition.defval.as_ref().unwrap().value,
other => panic!("expected object type, got {other:?}"),
};
assert!(matches!(
numeric_first_defval,
DefVal::ObjectIdentifier { components, .. }
if matches!(
components.as_slice(),
[
OidComponent::Number { value: 0, .. },
OidComponent::NamedNumber { num: 0, .. }
]
)
));
match definition("overflowTrap") {
Definition::TrapType(definition) => assert_eq!(definition.trap_number, 0),
other => panic!("expected trap type, got {other:?}"),
}
assert!(matches!(
definition("after"),
Definition::ValueAssignment(_)
));
let invalid_i64: Vec<_> = module
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == DiagCode::InvalidI64)
.map(|diagnostic| {
let range = diagnostic.range.unwrap().byte_range();
&input[range]
})
.collect();
assert_eq!(
invalid_i64,
[
"9223372036854775808",
"-9223372036854775809",
"9223372036854775809",
"-9223372036854775810",
"18446744073709551616",
"-9223372036854775811",
]
);
let valid_unsigned_start = input.find("18446744073709551615").unwrap();
assert!(module.diagnostics.iter().all(|diagnostic| {
diagnostic.code != DiagCode::InvalidI64
|| diagnostic.range.unwrap().byte_range()
!= (valid_unsigned_start..valid_unsigned_start + 20)
}));
let invalid_u32: Vec<_> = module
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == DiagCode::InvalidU32)
.map(|diagnostic| {
let range = diagnostic.range.unwrap().byte_range();
&input[range]
})
.collect();
assert_eq!(
invalid_u32,
[
"4294967296",
"4294967297",
"4294967298",
"4294967299",
"4294967300",
"4294967301",
"4294967302",
"4294967304",
"4294967305",
"4294967303",
]
);
let overflow_diagnostics: Vec<_> = module
.diagnostics
.iter()
.filter(|diagnostic| {
matches!(diagnostic.code, DiagCode::InvalidI64 | DiagCode::InvalidU32)
})
.map(|diagnostic| {
let range = diagnostic.range.unwrap().byte_range();
(
diagnostic.code,
&input[range],
diagnostic.message.as_str(),
diagnostic.severity,
)
})
.collect();
assert_eq!(
overflow_diagnostics,
[
(
DiagCode::InvalidI64,
"9223372036854775808",
"invalid named number (not a valid i64)",
Severity::Error
),
(
DiagCode::InvalidI64,
"-9223372036854775809",
"invalid named number (not a valid i64)",
Severity::Error
),
(
DiagCode::InvalidI64,
"9223372036854775809",
"invalid named number (not a valid i64)",
Severity::Error
),
(
DiagCode::InvalidI64,
"-9223372036854775810",
"invalid named number (not a valid i64)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967296",
"invalid OID component (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967297",
"invalid OID component (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967298",
"invalid OID component (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidI64,
"18446744073709551616",
"invalid DEFVAL number",
Severity::Error
),
(
DiagCode::InvalidI64,
"-9223372036854775811",
"invalid DEFVAL number (not a valid i64)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967299",
"invalid OID component (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967300",
"invalid OID component (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967301",
"invalid OID component number (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967302",
"invalid OID component number (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967304",
"invalid OID component (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967305",
"invalid OID component number (not a valid u32)",
Severity::Error
),
(
DiagCode::InvalidU32,
"4294967303",
"invalid trap number (not a valid u32)",
Severity::Error
),
]
);
}
#[test]
fn integral_overflow_recovery_respects_diagnostic_policy() {
let input = r#"POLICY-MIB DEFINITIONS ::= BEGIN
PolicyEnum ::= INTEGER { huge(9223372036854775808) }
policyOid OBJECT IDENTIFIER ::= { root 4294967296 }
after OBJECT IDENTIFIER ::= { root 1 }
END
"#;
let assert_recovery = |module: &Module| {
assert_eq!(module.body.len(), 3);
match &module.body[0] {
Definition::TypeAssignment(definition) => match &definition.syntax {
TypeSyntax::IntegerEnum { named_numbers, .. } => {
assert_eq!(named_numbers[0].value, 0);
}
other => panic!("expected enum, got {other:?}"),
},
other => panic!("expected type assignment, got {other:?}"),
}
match &module.body[1] {
Definition::ValueAssignment(definition) => assert!(matches!(
definition.oid.components.as_slice(),
[OidComponent::Name(_), OidComponent::Number { value: 0, .. }]
)),
other => panic!("expected value assignment, got {other:?}"),
}
assert_eq!(module.body[2].name().unwrap().name, "after");
};
let mut ignored = DiagnosticConfig::verbose();
ignored
.ignore
.extend(["invalid-i64".to_string(), "invalid-u32".to_string()]);
let ignored_modules = parse_with_config(input, &ignored);
assert_recovery(&ignored_modules[0]);
assert!(ignored_modules[0].diagnostics.iter().all(|diagnostic| {
!matches!(diagnostic.code, DiagCode::InvalidI64 | DiagCode::InvalidU32)
}));
let mut overridden = DiagnosticConfig::verbose();
overridden
.overrides
.insert(DiagCode::InvalidI64, Severity::Minor);
overridden
.overrides
.insert(DiagCode::InvalidU32, Severity::Warning);
let overridden_modules = parse_with_config(input, &overridden);
assert_recovery(&overridden_modules[0]);
let diagnostics: Vec<_> = overridden_modules[0]
.diagnostics
.iter()
.filter(|diagnostic| {
matches!(diagnostic.code, DiagCode::InvalidI64 | DiagCode::InvalidU32)
})
.map(|diagnostic| (diagnostic.code, diagnostic.severity))
.collect();
assert_eq!(
diagnostics,
[
(DiagCode::InvalidI64, Severity::Minor),
(DiagCode::InvalidU32, Severity::Warning),
]
);
}
}