///|
fn attribute_statement_body(
line : StringView,
prefix_length : Int,
) -> Result[StringView, String] {
let body = line[prefix_length:].trim()
if !body.has_suffix(";") {
Err("attribute declaration is missing ';'")
} else {
Ok(body[:body.length() - 1].trim())
}
}
///|
fn attribute_name_and_tail(
content : StringView,
) -> Result[(String, StringView, StringView), String] {
let open_quote = match content.find("\"") {
Some(value) => value
None => return Err("attribute name is missing an opening quote")
}
let close_quote = match find_after(content, open_quote + 1, "\"") {
Some(value) => value
None => return Err("attribute name is missing a closing quote")
}
let name = content[open_quote + 1:close_quote]
if name.is_empty() || name.contains("\\") {
return Err("attribute name is empty or contains an escape")
}
Ok(
(
name.to_owned(),
content[:open_quote].trim(),
content[close_quote + 1:].trim(),
),
)
}
///|
fn attribute_type_from_text(
text : StringView,
) -> Result[DbcAttributeType, String] {
let fields = words(text)
if fields.is_empty() {
return Err("attribute type is missing")
}
match fields[0] {
"INT" | "HEX" => {
if fields.length() != 3 {
return Err("integer attribute type requires minimum and maximum")
}
let minimum = match parse_int64(fields[1]) {
Some(value) => value
None => return Err("integer attribute minimum is invalid")
}
let maximum = match parse_int64(fields[2]) {
Some(value) if value >= minimum => value
_ => return Err("integer attribute maximum is invalid")
}
if fields[0] == "INT" {
Ok(AttributeIntegerType(minimum, maximum))
} else {
Ok(AttributeHexType(minimum, maximum))
}
}
"FLOAT" => {
if fields.length() != 3 {
return Err("floating attribute type requires minimum and maximum")
}
let minimum = match parse_double(fields[1]) {
Some(value) if !value.is_nan() && !value.is_inf() => value
_ => return Err("floating attribute minimum is invalid")
}
let maximum = match parse_double(fields[2]) {
Some(value) if !value.is_nan() && !value.is_inf() && value >= minimum =>
value
_ => return Err("floating attribute maximum is invalid")
}
Ok(AttributeFloatType(minimum, maximum))
}
"STRING" =>
if fields.length() == 1 {
Ok(AttributeStringType)
} else {
Err("string attribute type has unexpected fields")
}
"ENUM" => {
let choices : Array[String] = []
let mut rest = text[4:].trim()
while !rest.is_empty() {
if !rest.has_prefix("\"") {
return Err("enumeration choice must be quoted")
}
let mut search = 1
let mut close_quote = -1
while close_quote < 0 {
let position = match find_after(rest, search, "\"") {
Some(value) => value
None => return Err("enumeration choice is missing a closing quote")
}
let mut escapes = 0
let mut before = position - 1
while before >= 1 && rest[before:before + 1] == "\\" {
escapes += 1
before -= 1
}
if escapes % 2 == 0 {
close_quote = position
} else {
search = position + 1
}
}
match parse_comment_text(rest[1:close_quote + 1]) {
Ok(value) => choices.push(value)
Err(_) => return Err("enumeration choice is invalid")
}
let trailing = rest[close_quote + 1:].trim()
if trailing.is_empty() {
rest = trailing
} else if trailing.has_prefix(",") {
rest = trailing[1:].trim()
if rest.is_empty() {
return Err("enumeration choice list ends with a comma")
}
} else {
return Err("enumeration choices must be comma-separated")
}
}
if choices.is_empty() {
Err("enumeration attribute requires at least one choice")
} else {
Ok(AttributeEnumType(choices))
}
}
_ => Err("unsupported attribute type")
}
}
///|
fn parse_attribute_definition_line(
line : StringView,
) -> Result[DbcAttributeDefinition, String] {
let content = match attribute_statement_body(line, 7) {
Ok(value) => value
Err(error) => return Err(error)
}
let (name, scope_text, type_text) = match attribute_name_and_tail(content) {
Ok(value) => value
Err(error) => return Err(error)
}
let scope = match scope_text {
"" => DatabaseAttributeScope
"BU_" => NodeAttributeScope
"BO_" => MessageAttributeScope
"SG_" => SignalAttributeScope
_ => return Err("attribute scope must be BU_, BO_, or SG_")
}
let value_type = match attribute_type_from_text(type_text) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok({ name, scope, value_type, })
}
///|
fn parse_attribute_default_line(
line : StringView,
) -> Result[(String, String), String] {
let content = match attribute_statement_body(line, 11) {
Ok(value) => value
Err(error) => return Err(error)
}
let (name, prefix, raw_value) = match attribute_name_and_tail(content) {
Ok(value) => value
Err(error) => return Err(error)
}
if !prefix.is_empty() || raw_value.is_empty() {
return Err("attribute default requires a name and value")
}
Ok((name, raw_value.to_owned()))
}
///|
fn attribute_take_word(
text : StringView,
) -> Result[(StringView, StringView), String] {
let trimmed = text.trim()
match trimmed.find(" ") {
Some(index) => Ok((trimmed[:index], trimmed[index + 1:].trim()))
None => Err("attribute target or value is incomplete")
}
}
///|
fn parse_attribute_assignment_line(
line : StringView,
) -> Result[(String, DbcAttributeTarget, String), String] {
let content = match attribute_statement_body(line, 3) {
Ok(value) => value
Err(error) => return Err(error)
}
let (name, prefix, tail) = match attribute_name_and_tail(content) {
Ok(value) => value
Err(error) => return Err(error)
}
if !prefix.is_empty() || tail.is_empty() {
return Err("attribute assignment requires a name and value")
}
let (target, raw_value) = if tail.has_prefix("BU_ ") {
let (node, value) = match attribute_take_word(tail[4:]) {
Ok(result) => result
Err(error) => return Err(error)
}
(NodeAttributeTarget(node.to_owned()), value)
} else if tail.has_prefix("BO_ ") {
let (id_text, value) = match attribute_take_word(tail[4:]) {
Ok(result) => result
Err(error) => return Err(error)
}
let id = match parse_uint(id_text) {
Some(value) if dbc_message_id_parts(value) is Some(_) => value
_ => return Err("attribute message id is invalid")
}
(MessageAttributeTarget(id), value)
} else if tail.has_prefix("SG_ ") {
let (id_text, signal_and_value) = match attribute_take_word(tail[4:]) {
Ok(result) => result
Err(error) => return Err(error)
}
let id = match parse_uint(id_text) {
Some(value) if dbc_message_id_parts(value) is Some(_) => value
_ => return Err("attribute message id is invalid")
}
let (signal, value) = match attribute_take_word(signal_and_value) {
Ok(result) => result
Err(error) => return Err(error)
}
(SignalAttributeTarget(id, signal.to_owned()), value)
} else {
(DatabaseAttributeTarget, tail)
}
if raw_value.is_empty() {
return Err("attribute assignment value is missing")
}
Ok((name, target, raw_value.to_owned()))
}
///|
fn attribute_value_from_text(
raw : StringView,
value_type : DbcAttributeType,
) -> Result[DbcAttributeValue, String] {
match value_type {
AttributeIntegerType(_, _) | AttributeHexType(_, _) =>
match parse_int64(raw) {
Some(value) => Ok(AttributeInteger(value))
None => Err("attribute value is not an integer")
}
AttributeFloatType(_, _) =>
match parse_double(raw) {
Some(value) if !value.is_nan() && !value.is_inf() =>
Ok(AttributeFloat(value))
_ => Err("attribute value is not a finite number")
}
AttributeStringType =>
if raw.has_prefix("\"") {
match parse_comment_text(raw[1:]) {
Ok(value) => Ok(AttributeText(value))
Err(_) => Err("attribute string value is invalid")
}
} else {
Err("attribute string value must be quoted")
}
AttributeEnumType(choices) =>
if raw.has_prefix("\"") {
match parse_comment_text(raw[1:]) {
Ok(value) => {
for choice in choices {
if choice == value {
return Ok(AttributeText(value))
}
}
Err("attribute enum value is not a declared choice")
}
Err(_) => Err("attribute enum value is invalid")
}
} else {
match parse_int64(raw) {
Some(value) if value >= 0 && value < choices.length().to_int64() =>
Ok(AttributeInteger(value))
_ => Err("attribute enum index is invalid")
}
}
}
}
///|
fn attribute_target_scope(target : DbcAttributeTarget) -> DbcAttributeScope {
match target {
DatabaseAttributeTarget => DatabaseAttributeScope
NodeAttributeTarget(_) => NodeAttributeScope
MessageAttributeTarget(_) => MessageAttributeScope
SignalAttributeTarget(_, _) => SignalAttributeScope
}
}
///|
fn attribute_target_exists(
nodes : Array[String],
messages : Array[Message],
target : DbcAttributeTarget,
) -> Bool {
match target {
DatabaseAttributeTarget => true
NodeAttributeTarget(node) => contains_node(nodes, node)
MessageAttributeTarget(id) => find_message_index(messages, id) is Some(_)
SignalAttributeTarget(id, signal_name) =>
match find_message_index(messages, id) {
Some(index) => contains_signal_name(messages[index], signal_name)
None => false
}
}
}
///|
fn find_attribute_definition(
definitions : Array[DbcAttributeDefinition],
name : StringView,
) -> DbcAttributeDefinition? {
for definition in definitions {
if definition.name.view() == name {
return Some(definition)
}
}
None
}
///|
pub fn Database::attribute_value(
self : Database,
name : StringView,
target : DbcAttributeTarget,
) -> DbcAttributeValue? {
if !attribute_target_exists(self.nodes, self.messages, target) {
return None
}
let definition = match
find_attribute_definition(self.attributes.definitions, name) {
Some(value) if value.scope == attribute_target_scope(target) => value
_ => return None
}
for assignment in self.attributes.assignments {
if assignment.name == definition.name && assignment.target == target {
return Some(assignment.value)
}
}
for default in self.attributes.defaults {
if default.name == definition.name {
return Some(default.value)
}
}
None
}
///|
fn attribute_scope_text(scope : DbcAttributeScope) -> String {
match scope {
DatabaseAttributeScope => ""
NodeAttributeScope => "BU_ "
MessageAttributeScope => "BO_ "
SignalAttributeScope => "SG_ "
}
}
///|
fn attribute_type_text(
value_type : DbcAttributeType,
) -> Result[String, DbcWriteError] {
match value_type {
AttributeIntegerType(minimum, maximum) =>
if minimum <= maximum {
Ok("INT \{minimum} \{maximum}")
} else {
Err(DbcWriteInvalidAttribute("integer bounds are reversed"))
}
AttributeHexType(minimum, maximum) =>
if minimum <= maximum {
Ok("HEX \{minimum} \{maximum}")
} else {
Err(DbcWriteInvalidAttribute("hexadecimal bounds are reversed"))
}
AttributeFloatType(minimum, maximum) =>
if minimum.is_nan() ||
minimum.is_inf() ||
maximum.is_nan() ||
maximum.is_inf() ||
minimum > maximum {
Err(DbcWriteInvalidAttribute("floating bounds are invalid"))
} else {
Ok("FLOAT \{minimum} \{maximum}")
}
AttributeStringType => Ok("STRING")
AttributeEnumType(choices) => {
if choices.is_empty() {
return Err(DbcWriteInvalidAttribute("enumeration has no choices"))
}
let quoted : Array[String] = []
for choice in choices {
let escaped = match escape_dbc_comment(choice) {
Ok(value) => value
Err(error) => return Err(error)
}
quoted.push("\"\{escaped}\"")
}
Ok("ENUM \{quoted.join(",")}")
}
}
}
///|
fn attribute_value_text(
value : DbcAttributeValue,
value_type : DbcAttributeType,
) -> Result[String, DbcWriteError] {
match (value_type, value) {
(AttributeIntegerType(_, _), AttributeInteger(number))
| (AttributeHexType(_, _), AttributeInteger(number)) =>
Ok(number.to_string())
(AttributeFloatType(_, _), AttributeFloat(number)) =>
if number.is_nan() || number.is_inf() {
Err(DbcWriteInvalidAttribute("floating value is not finite"))
} else {
Ok(number.to_string())
}
(AttributeStringType, AttributeText(text)) => {
let escaped = match escape_dbc_comment(text) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok("\"\{escaped}\"")
}
(AttributeEnumType(choices), AttributeInteger(index)) =>
if index >= 0 && index < choices.length().to_int64() {
Ok(index.to_string())
} else {
Err(DbcWriteInvalidAttribute("enumeration index is invalid"))
}
(AttributeEnumType(choices), AttributeText(text)) => {
for choice in choices {
if choice == text {
let escaped = match escape_dbc_comment(text) {
Ok(value) => value
Err(error) => return Err(error)
}
return Ok("\"\{escaped}\"")
}
}
Err(DbcWriteInvalidAttribute("enumeration value is undeclared"))
}
_ => Err(DbcWriteInvalidAttribute("value does not match its definition"))
}
}
///|
fn attribute_target_text(target : DbcAttributeTarget) -> String {
match target {
DatabaseAttributeTarget => ""
NodeAttributeTarget(node) => " BU_ \{node}"
MessageAttributeTarget(id) => " BO_ \{id}"
SignalAttributeTarget(id, signal) => " SG_ \{id} \{signal}"
}
}
///|
fn write_attributes(
lines : Array[String],
attributes : DbcAttributes,
) -> Result[Unit, DbcWriteError] {
for definition in attributes.definitions {
if definition.name.is_empty() ||
definition.name.view().contains("\"") ||
definition.name.view().contains("\\") {
return Err(DbcWriteInvalidAttribute("definition name is invalid"))
}
let name = match escape_dbc_comment(definition.name) {
Ok(value) => value
Err(error) => return Err(error)
}
let value_type = match attribute_type_text(definition.value_type) {
Ok(value) => value
Err(error) => return Err(error)
}
lines.push(
"BA_DEF_ \{attribute_scope_text(definition.scope)}\"\{name}\" \{value_type};",
)
}
if !attributes.definitions.is_empty() &&
(!attributes.defaults.is_empty() || !attributes.assignments.is_empty()) {
lines.push("")
}
for default in attributes.defaults {
let definition = match
find_attribute_definition(attributes.definitions, default.name) {
Some(value) => value
None => return Err(DbcWriteInvalidAttribute("default has no definition"))
}
let value = match
attribute_value_text(default.value, definition.value_type) {
Ok(text) => text
Err(error) => return Err(error)
}
let name = match escape_dbc_comment(default.name) {
Ok(text) => text
Err(error) => return Err(error)
}
lines.push("BA_DEF_DEF_ \"\{name}\" \{value};")
}
if !attributes.defaults.is_empty() && !attributes.assignments.is_empty() {
lines.push("")
}
for assignment in attributes.assignments {
let definition = match
find_attribute_definition(attributes.definitions, assignment.name) {
Some(value) => value
None =>
return Err(DbcWriteInvalidAttribute("assignment has no definition"))
}
if definition.scope != attribute_target_scope(assignment.target) {
return Err(DbcWriteInvalidAttribute("assignment scope does not match"))
}
match assignment.target {
NodeAttributeTarget(node) =>
match check_dbc_token("attribute node", node) {
Err(error) => return Err(error)
Ok(_) => ()
}
SignalAttributeTarget(_, signal) =>
match check_dbc_token("attribute signal", signal) {
Err(error) => return Err(error)
Ok(_) => ()
}
_ => ()
}
let value = match
attribute_value_text(assignment.value, definition.value_type) {
Ok(text) => text
Err(error) => return Err(error)
}
let name = match escape_dbc_comment(assignment.name) {
Ok(text) => text
Err(error) => return Err(error)
}
lines.push(
"BA_ \"\{name}\"\{attribute_target_text(assignment.target)} \{value};",
)
}
Ok(())
}