mod a2ml;
#[cfg(feature = "check")]
mod checker;
#[cfg(feature = "cleanup")]
mod cleanup;
mod ifdata;
mod itemlist;
mod loader;
#[cfg(feature = "merge")]
mod merge;
mod module;
mod parser;
#[cfg(feature = "sort")]
mod sort;
mod specification;
mod tokenizer;
mod writer;
use std::convert::AsRef;
use std::ffi::OsString;
use std::fmt::Display;
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use parser::A2lVersion;
use parser::{ParseContext, ParserState};
pub use a2lmacros::a2ml_specification;
pub use a2ml::{GenericIfData, GenericIfDataTaggedItem};
pub use itemlist::ItemList;
pub use module::{AnyCompuTab, AnyObject, AnyTypedef};
pub use parser::ParserError;
pub use specification::*;
pub use tokenizer::TokenizerError;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum A2lError {
#[error("Failed to load {filename}: {ioerror}")]
FileOpenError {
filename: PathBuf,
ioerror: std::io::Error,
},
#[error("Could not read from {filename}: {ioerror}")]
FileReadError {
filename: PathBuf,
ioerror: std::io::Error,
},
#[error("File \"{filename}\" contains no a2l data")]
EmptyFileError { filename: PathBuf },
#[error("Failed to load built-in a2ml specification: {parse_err}")]
InvalidBuiltinA2mlSpec { parse_err: String },
#[error("Tokenizer error: {tokenizer_error}")]
TokenizerError { tokenizer_error: TokenizerError },
#[error("Parser error: {parser_error}")]
ParserError { parser_error: ParserError },
#[error("Could not write to {filename}: {ioerror}")]
FileWriteError {
filename: PathBuf,
ioerror: std::io::Error,
},
#[error(
"Name collision: {blockname} blocks on line {line_1} and {line_2} both use the name \"{item_name}\""
)]
NameCollisionError {
item_name: String,
blockname: String,
line_1: u32,
line_2: u32,
},
#[error(
"Name collision: {blockname_1} on line {line_1} and {blockname_2} on line {line_2} both use the name \"{item_name}\""
)]
NameCollisionError2 {
item_name: String,
blockname_1: String,
line_1: u32,
blockname_2: String,
line_2: u32,
},
#[error(
"Cross-reference error: {source_type} {source_name} on line {source_line} references a non-existent {target_type} {target_name}"
)]
CrossReferenceError {
source_type: String,
source_name: String,
source_line: u32,
target_type: String,
target_name: String,
},
#[error(
"Limit check error: {blockname} {item_name} on line {line} has limits {lower_limit} .. {upper_limit}, but the calculated limits are {calculated_lower_limit} .. {calculated_upper_limit}"
)]
LimitCheckError {
item_name: String,
blockname: String,
line: u32,
lower_limit: f64,
upper_limit: f64,
calculated_lower_limit: f64,
calculated_upper_limit: f64,
},
#[error("Group structure error: GROUP {group_name} on line {line} {description}")]
GroupStructureError {
group_name: String,
line: u32,
description: String,
},
#[error("Content error: {blockname} {item_name} on line {line}: {description}")]
ContentError {
item_name: String,
blockname: String,
line: u32,
description: String,
},
}
#[must_use]
pub fn new() -> A2lFile {
let mut project = Project::new("new_project".to_string(), String::new());
project.module = ItemList::default();
project
.module
.push(Module::new("new_module".to_string(), String::new()));
let mut a2l_file = A2lFile::new(project);
a2l_file.project.get_layout_mut().start_offset = 1;
a2l_file.project.module[0].get_layout_mut().start_offset = 1;
a2l_file.asap2_version = Some(Asap2Version::new(1, 71));
a2l_file
}
pub fn load<P: AsRef<Path>>(
path: P,
a2ml_spec: Option<String>,
strict_parsing: bool,
) -> Result<(A2lFile, Vec<A2lError>), A2lError> {
let pathref = path.as_ref();
let filedata = loader::load(pathref)?;
load_impl(pathref, &filedata, strict_parsing, a2ml_spec)
}
pub fn load_from_string(
a2ldata: &str,
a2ml_spec: Option<String>,
strict_parsing: bool,
) -> Result<(A2lFile, Vec<A2lError>), A2lError> {
let pathref = Path::new("");
load_impl(pathref, a2ldata, strict_parsing, a2ml_spec)
}
fn load_impl(
path: &Path,
filedata: &str,
strict_parsing: bool,
a2ml_spec: Option<String>,
) -> Result<(A2lFile, Vec<A2lError>), A2lError> {
let mut log_msgs = Vec::<A2lError>::new();
let tokenresult = tokenizer::tokenize(&Filename::from(path), 0, filedata)
.map_err(|tokenizer_error| A2lError::TokenizerError { tokenizer_error })?;
if tokenresult.tokens.is_empty() {
return Err(A2lError::EmptyFileError {
filename: path.to_path_buf(),
});
}
let mut parser = ParserState::new(&tokenresult, &mut log_msgs, strict_parsing);
if let Some(spec) = a2ml_spec {
parser.a2mlspec.push(
a2ml::parse_a2ml(&Filename::from(path), &spec)
.map_err(|parse_err| A2lError::InvalidBuiltinA2mlSpec { parse_err })?
.0,
);
}
let a2l_file = parser
.parse_file()
.map_err(|parser_error| A2lError::ParserError { parser_error })?;
Ok((a2l_file, log_msgs))
}
pub fn load_fragment(a2ldata: &str, a2ml_spec: Option<String>) -> Result<Module, A2lError> {
let fixed_a2ldata = format!(r#"fragment "" {a2ldata} /end MODULE"#);
let tokenresult = tokenizer::tokenize(&Filename::from("(fragment)"), 0, &fixed_a2ldata)
.map_err(|tokenizer_error| A2lError::TokenizerError { tokenizer_error })?;
let firstline = tokenresult.tokens.first().map_or(1, |tok| tok.line);
let context = ParseContext {
element: "MODULE".to_string(),
fileid: 0,
line: firstline,
};
let mut log_msgs = Vec::<A2lError>::new();
let mut parser = ParserState::new(&tokenresult, &mut log_msgs, false);
parser.set_file_version(A2lVersion::V1_7_1);
if let Some(spec) = a2ml_spec {
parser.a2mlspec.push(
a2ml::parse_a2ml(&Filename::from("(built-in)"), &spec)
.map_err(|parse_err| A2lError::InvalidBuiltinA2mlSpec { parse_err })?
.0,
);
}
Module::parse(&mut parser, &context, 0)
.map_err(|parser_error| A2lError::ParserError { parser_error })
}
pub fn load_fragment_file<P: AsRef<Path>>(
path: P,
a2ml_spec: Option<String>,
) -> Result<Module, A2lError> {
let pathref = path.as_ref();
let filedata = loader::load(pathref)?;
load_fragment(&filedata, a2ml_spec)
}
impl A2lFile {
#[must_use]
pub fn write_to_string(&self) -> String {
self.stringify(0)
}
pub fn write<P: AsRef<Path>>(&self, path: P, banner: Option<&str>) -> Result<(), A2lError> {
let mut outstr = String::new();
let file_text = self.write_to_string();
if let Some(banner_text) = banner {
outstr = format!("/* {banner_text} */");
if !file_text.starts_with('\n') {
outstr.push('\n');
}
}
outstr.push_str(&file_text);
std::fs::write(&path, outstr).map_err(|ioerror| A2lError::FileWriteError {
filename: path.as_ref().to_path_buf(),
ioerror,
})?;
Ok(())
}
#[cfg(feature = "merge")]
pub fn merge_modules(&mut self, merge_file: &mut A2lFile) {
merge::merge_modules(
&mut self.project.module[0],
&mut merge_file.project.module[0],
);
if let Some(file_ver) = &mut self.asap2_version {
if let Some(merge_ver) = &merge_file.asap2_version
&& (file_ver.version_no < merge_ver.version_no
|| ((file_ver.version_no == merge_ver.version_no)
&& (file_ver.upgrade_no < merge_ver.upgrade_no)))
{
file_ver.version_no = merge_ver.version_no;
file_ver.upgrade_no = merge_ver.upgrade_no;
}
} else {
self.asap2_version = std::mem::take(&mut merge_file.asap2_version);
}
}
#[cfg(feature = "check")]
#[must_use]
pub fn check(&self) -> Vec<A2lError> {
checker::check(self)
}
#[cfg(feature = "sort")]
pub fn sort(&mut self) {
sort::sort(self);
}
#[cfg(feature = "sort")]
pub fn sort_new_items(&mut self) {
sort::sort_new_items(self);
}
#[cfg(feature = "cleanup")]
pub fn cleanup(&mut self) {
cleanup::cleanup(self);
}
#[cfg(feature = "ifdata_cleanup")]
pub fn ifdata_cleanup(&mut self) {
ifdata::remove_unknown_ifdata(self);
}
}
#[derive(Debug, Clone)]
struct Filename {
full: OsString,
display: String,
}
impl Filename {
pub(crate) fn new(full: OsString, display: &str) -> Self {
Self {
full,
display: display.to_string(),
}
}
}
impl From<&str> for Filename {
fn from(value: &str) -> Self {
Self {
full: OsString::from(value),
display: String::from(value),
}
}
}
impl From<&Path> for Filename {
fn from(value: &Path) -> Self {
Self {
display: value.to_string_lossy().to_string(),
full: OsString::from(value),
}
}
}
impl From<OsString> for Filename {
fn from(value: OsString) -> Self {
Self {
display: value.to_string_lossy().to_string(),
full: value,
}
}
}
impl Display for Filename {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.display)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn load_empty_file() {
let result = load_from_string("", None, false);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, A2lError::EmptyFileError { .. }));
}
#[test]
fn test_load_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.a2l");
let path = path.to_str().unwrap();
let text = r#"
ASAP2_VERSION 1 71
/begin PROJECT new_project ""
/begin MODULE new_module ""
/end MODULE
/end PROJECT
"#;
std::fs::write(path, text).unwrap();
let (a2l, _) = load(path, None, false).unwrap();
assert_eq!(a2l.project.module[0].name, "new_module");
let nonexistent_path = dir.path().join("nonexistent.a2l");
let nonexistent_path = nonexistent_path.to_str().unwrap();
let result = load(nonexistent_path, None, false);
assert!(matches!(result, Err(A2lError::FileOpenError { .. })));
}
#[test]
fn bad_a2ml_data() {
let result = load_from_string(
r#"/begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT"#,
Some("x".to_string()),
false,
);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, A2lError::InvalidBuiltinA2mlSpec { .. }));
}
#[test]
fn strict_parsing_version_error() {
let result = load_from_string(
r#"/begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT"#,
None,
true,
);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
A2lError::ParserError {
parser_error: ParserError::MissingVersionInfo
}
));
let result = load_from_string(r#"ASAP2_VERSION 1 /begin PROJECT"#, None, true);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
A2lError::ParserError {
parser_error: ParserError::MissingVersionInfo
}
));
}
#[test]
fn additional_tokens() {
let result = load_from_string(
r#"ASAP2_VERSION 1 71 /begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT abcdef"#,
None,
false,
);
assert!(result.is_ok());
let (_a2l, log_msgs) = result.unwrap();
assert_eq!(log_msgs.len(), 1);
let result = load_from_string(
r#"ASAP2_VERSION 1 71 /begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT abcdef"#,
None,
true,
);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(
error,
A2lError::ParserError {
parser_error: ParserError::AdditionalTokensError { .. }
}
));
}
#[test]
fn write_nonexistent_file() {
let a2l = new();
let result = a2l.write(
"__NONEXISTENT__/__FILE__/__PATH__/test.a2l",
Some("test case write_nonexistent_file()"),
);
assert!(result.is_err());
}
#[test]
fn write_with_banner() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.a2l");
let path = path.to_str().unwrap();
let mut a2l = new();
a2l.asap2_version
.as_mut()
.unwrap()
.get_layout_mut()
.start_offset = 0;
let result = a2l.write(path, Some("test case write_nonexistent_file()"));
assert!(result.is_ok());
let file_text = String::from_utf8(std::fs::read(path).unwrap()).unwrap();
assert!(file_text.starts_with("/* test case write_nonexistent_file() */"));
std::fs::remove_file(path).unwrap();
a2l.asap2_version
.as_mut()
.unwrap()
.get_layout_mut()
.start_offset = 1;
let result = a2l.write(path, Some("test case write_nonexistent_file()"));
assert!(result.is_ok());
let file_text = String::from_utf8(std::fs::read(path).unwrap()).unwrap();
assert!(file_text.starts_with("/* test case write_nonexistent_file() */"));
std::fs::remove_file(path).unwrap();
}
#[cfg(feature = "merge")]
#[test]
fn merge() {
let mut a2l = new();
let mut a2l_2 = new();
a2l.asap2_version = None;
a2l.merge_modules(&mut a2l_2);
assert!(a2l.asap2_version.is_some());
let mut a2l = new();
let mut a2l_2 = new();
a2l.asap2_version = Some(Asap2Version::new(1, 50));
a2l.merge_modules(&mut a2l_2);
assert!(a2l.asap2_version.is_some());
assert!(matches!(
a2l.asap2_version,
Some(Asap2Version {
version_no: 1,
upgrade_no: 71,
..
})
));
let mut a2l = new();
let mut a2l_2 = new();
a2l_2.project.module[0].compu_tab.push(CompuTab::new(
"compu_tab".to_string(),
String::new(),
ConversionType::Identical,
0,
));
a2l.project.module[0].merge(&mut a2l_2.project.module[0]);
assert_eq!(a2l.project.module[0].compu_tab.len(), 1);
}
#[test]
fn test_load_fagment() {
let result = load_fragment("", None);
assert!(result.is_ok());
let result = load_fragment(
r#"
/begin MEASUREMENT measurement_name ""
UBYTE CM.IDENTICAL 0 0 0 255
ECU_ADDRESS 0x13A00
FORMAT "%5.0" /* Note: Overwrites the format stated in the computation method */
DISPLAY_IDENTIFIER DI.ASAM.M.SCALAR.UBYTE.IDENTICAL /* optional display identifier */
/begin IF_DATA ETK KP_BLOB 0x13A00 INTERN 1 RASTER 2 /end IF_DATA
/end MEASUREMENT"#,
None,
);
assert!(result.is_ok());
let result = load_fragment(
r#"
/begin MEASUREMENT measurement_name ""
UBYTE CM.IDENTICAL 0 0 0 255
ECU_ADDRESS 0x13A00
/begin IF_DATA ETK KP_BLOB 0x13A00 INTERN 1 RASTER 2 /end IF_DATA
/end MEASUREMENT"#,
Some(r#"block "IF_DATA" long;"#.to_string()),
);
assert!(result.is_ok());
let result = load_fragment(
r#"
/begin MEASUREMENT measurement_name ""
UBYTE CM.IDENTICAL 0 0 0 255
ECU_ADDRESS 0x13A00
/begin IF_DATA ETK KP_BLOB 0x13A00 INTERN 1 RASTER 2 /end IF_DATA
/end MEASUREMENT"#,
Some(r#"lorem ipsum"#.to_string()),
);
assert!(matches!(
result,
Err(A2lError::InvalidBuiltinA2mlSpec { .. })
));
let result = load_fragment("12345", None);
assert!(matches!(result, Err(A2lError::ParserError { .. })));
let result = load_fragment(",,,", None);
println!("{:?}", result);
assert!(matches!(result, Err(A2lError::TokenizerError { .. })));
}
#[test]
fn test_load_fagment_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("fragment.a2l");
let path = path.to_str().unwrap();
std::fs::write(
path,
r#"
/begin MEASUREMENT measurement_name ""
UBYTE CM.IDENTICAL 0 0 0 255
/end MEASUREMENT"#,
)
.unwrap();
let result = load_fragment_file(path, None);
assert!(result.is_ok());
let nonexistent_path = dir.path().join("nonexistent.a2l");
let nonexistent_path = nonexistent_path.to_str().unwrap();
let result = load_fragment_file(nonexistent_path, None);
assert!(matches!(result, Err(A2lError::FileOpenError { .. })));
}
#[test]
fn test_filename() {
let filename = Filename::from("test.a2l");
assert_eq!(filename.to_string(), "test.a2l");
let filename = Filename::from(OsString::from("test.a2l"));
assert_eq!(filename.to_string(), "test.a2l");
let filename = Filename::from(Path::new("test.a2l"));
assert_eq!(filename.to_string(), "test.a2l");
}
}