1mod a2ml;
14#[cfg(feature = "check")]
15mod checker;
16#[cfg(feature = "cleanup")]
17mod cleanup;
18mod ifdata;
19mod itemlist;
20mod loader;
21#[cfg(feature = "merge")]
22mod merge;
23mod module;
24mod parser;
25#[cfg(feature = "sort")]
26mod sort;
27mod specification;
28mod tokenizer;
29mod writer;
30
31use std::convert::AsRef;
32use std::ffi::OsString;
33use std::fmt::Display;
34use std::path::Path;
35use std::path::PathBuf;
36use thiserror::Error;
37use parser::A2lVersion;
39use parser::{ParseContext, ParserState};
40
41pub use a2lmacros::a2ml_specification;
43pub use a2ml::{GenericIfData, GenericIfDataTaggedItem};
44pub use itemlist::ItemList;
45pub use module::{AnyCompuTab, AnyObject, AnyTypedef};
46pub use parser::ParserError;
47pub use specification::*;
48pub use tokenizer::TokenizerError;
49
50#[derive(Debug, Error)]
51#[non_exhaustive]
52pub enum A2lError {
53 #[error("Failed to load {filename}: {ioerror}")]
55 FileOpenError {
56 filename: PathBuf,
57 ioerror: std::io::Error,
58 },
59
60 #[error("Could not read from {filename}: {ioerror}")]
62 FileReadError {
63 filename: PathBuf,
64 ioerror: std::io::Error,
65 },
66
67 #[error("File \"{filename}\" contains no a2l data")]
69 EmptyFileError { filename: PathBuf },
70
71 #[error("Failed to load built-in a2ml specification: {parse_err}")]
73 InvalidBuiltinA2mlSpec { parse_err: String },
74
75 #[error("Tokenizer error: {tokenizer_error}")]
77 TokenizerError { tokenizer_error: TokenizerError },
78
79 #[error("Parser error: {parser_error}")]
81 ParserError { parser_error: ParserError },
82
83 #[error("Could not write to {filename}: {ioerror}")]
85 FileWriteError {
86 filename: PathBuf,
87 ioerror: std::io::Error,
88 },
89
90 #[error(
92 "Name collision: {blockname} blocks on line {line_1} and {line_2} both use the name \"{item_name}\""
93 )]
94 NameCollisionError {
95 item_name: String,
96 blockname: String,
97 line_1: u32,
98 line_2: u32,
99 },
100
101 #[error(
103 "Name collision: {blockname_1} on line {line_1} and {blockname_2} on line {line_2} both use the name \"{item_name}\""
104 )]
105 NameCollisionError2 {
106 item_name: String,
107 blockname_1: String,
108 line_1: u32,
109 blockname_2: String,
110 line_2: u32,
111 },
112
113 #[error(
115 "Cross-reference error: {source_type} {source_name} on line {source_line} references a non-existent {target_type} {target_name}"
116 )]
117 CrossReferenceError {
118 source_type: String,
119 source_name: String,
120 source_line: u32,
121 target_type: String,
122 target_name: String,
123 },
124
125 #[error(
127 "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}"
128 )]
129 LimitCheckError {
130 item_name: String,
131 blockname: String,
132 line: u32,
133 lower_limit: f64,
134 upper_limit: f64,
135 calculated_lower_limit: f64,
136 calculated_upper_limit: f64,
137 },
138
139 #[error("Group structure error: GROUP {group_name} on line {line} {description}")]
141 GroupStructureError {
142 group_name: String,
143 line: u32,
144 description: String,
145 },
146
147 #[error("Content error: {blockname} {item_name} on line {line}: {description}")]
149 ContentError {
150 item_name: String,
151 blockname: String,
152 line: u32,
153 description: String,
154 },
155}
156
157#[must_use]
175pub fn new() -> A2lFile {
176 let mut project = Project::new("new_project".to_string(), String::new());
178 project.module = ItemList::default();
179 project
180 .module
181 .push(Module::new("new_module".to_string(), String::new()));
182 let mut a2l_file = A2lFile::new(project);
183 a2l_file.project.get_layout_mut().start_offset = 1;
185 a2l_file.project.module[0].get_layout_mut().start_offset = 1;
187 a2l_file.asap2_version = Some(Asap2Version::new(1, 71));
189
190 a2l_file
191}
192
193pub fn load<P: AsRef<Path>>(
217 path: P,
218 a2ml_spec: Option<String>,
219 strict_parsing: bool,
220) -> Result<(A2lFile, Vec<A2lError>), A2lError> {
221 let pathref = path.as_ref();
222 let filedata = loader::load(pathref)?;
223 load_impl(pathref, &filedata, strict_parsing, a2ml_spec)
224}
225
226pub fn load_from_string(
262 a2ldata: &str,
263 a2ml_spec: Option<String>,
264 strict_parsing: bool,
265) -> Result<(A2lFile, Vec<A2lError>), A2lError> {
266 let pathref = Path::new("");
267 load_impl(pathref, a2ldata, strict_parsing, a2ml_spec)
268}
269
270fn load_impl(
271 path: &Path,
272 filedata: &str,
273 strict_parsing: bool,
274 a2ml_spec: Option<String>,
275) -> Result<(A2lFile, Vec<A2lError>), A2lError> {
276 let mut log_msgs = Vec::<A2lError>::new();
277 let tokenresult = tokenizer::tokenize(&Filename::from(path), 0, filedata)
279 .map_err(|tokenizer_error| A2lError::TokenizerError { tokenizer_error })?;
280
281 if tokenresult.tokens.is_empty() {
282 return Err(A2lError::EmptyFileError {
283 filename: path.to_path_buf(),
284 });
285 }
286
287 let mut parser = ParserState::new(&tokenresult, &mut log_msgs, strict_parsing);
289
290 if let Some(spec) = a2ml_spec {
292 parser.a2mlspec.push(
293 a2ml::parse_a2ml(&Filename::from(path), &spec)
294 .map_err(|parse_err| A2lError::InvalidBuiltinA2mlSpec { parse_err })?
295 .0,
296 );
297 }
298
299 let a2l_file = parser
301 .parse_file()
302 .map_err(|parser_error| A2lError::ParserError { parser_error })?;
303
304 Ok((a2l_file, log_msgs))
305}
306
307pub fn load_fragment(a2ldata: &str, a2ml_spec: Option<String>) -> Result<Module, A2lError> {
316 let fixed_a2ldata = format!(r#"fragment "" {a2ldata} /end MODULE"#);
317 let tokenresult = tokenizer::tokenize(&Filename::from("(fragment)"), 0, &fixed_a2ldata)
319 .map_err(|tokenizer_error| A2lError::TokenizerError { tokenizer_error })?;
320 let firstline = tokenresult.tokens.first().map_or(1, |tok| tok.line);
321 let context = ParseContext {
322 element: "MODULE".to_string(),
323 fileid: 0,
324 line: firstline,
325 };
326
327 let mut log_msgs = Vec::<A2lError>::new();
329 let mut parser = ParserState::new(&tokenresult, &mut log_msgs, false);
330 parser.set_file_version(A2lVersion::V1_7_1); if let Some(spec) = a2ml_spec {
334 parser.a2mlspec.push(
335 a2ml::parse_a2ml(&Filename::from("(built-in)"), &spec)
336 .map_err(|parse_err| A2lError::InvalidBuiltinA2mlSpec { parse_err })?
337 .0,
338 );
339 }
340 Module::parse(&mut parser, &context, 0)
342 .map_err(|parser_error| A2lError::ParserError { parser_error })
343}
344
345pub fn load_fragment_file<P: AsRef<Path>>(
351 path: P,
352 a2ml_spec: Option<String>,
353) -> Result<Module, A2lError> {
354 let pathref = path.as_ref();
355 let filedata = loader::load(pathref)?;
356 load_fragment(&filedata, a2ml_spec)
357}
358
359impl A2lFile {
360 #[must_use]
362 pub fn write_to_string(&self) -> String {
363 self.stringify(0)
364 }
365
366 pub fn write<P: AsRef<Path>>(&self, path: P, banner: Option<&str>) -> Result<(), A2lError> {
373 let mut outstr = String::new();
374
375 let file_text = self.write_to_string();
376
377 if let Some(banner_text) = banner {
378 outstr = format!("/* {banner_text} */");
379 if !file_text.starts_with('\n') {
382 outstr.push('\n');
383 }
384 }
385 outstr.push_str(&file_text);
386
387 std::fs::write(&path, outstr).map_err(|ioerror| A2lError::FileWriteError {
388 filename: path.as_ref().to_path_buf(),
389 ioerror,
390 })?;
391
392 Ok(())
393 }
394
395 #[cfg(feature = "merge")]
396 pub fn merge_modules(&mut self, merge_file: &mut A2lFile) {
401 merge::merge_modules(
402 &mut self.project.module[0],
403 &mut merge_file.project.module[0],
404 );
405
406 if let Some(file_ver) = &mut self.asap2_version {
408 if let Some(merge_ver) = &merge_file.asap2_version
409 && (file_ver.version_no < merge_ver.version_no
410 || ((file_ver.version_no == merge_ver.version_no)
411 && (file_ver.upgrade_no < merge_ver.upgrade_no)))
412 {
413 file_ver.version_no = merge_ver.version_no;
414 file_ver.upgrade_no = merge_ver.upgrade_no;
415 }
416 } else {
417 self.asap2_version = std::mem::take(&mut merge_file.asap2_version);
419 }
420 }
421
422 #[cfg(feature = "check")]
423 #[must_use]
425 pub fn check(&self) -> Vec<A2lError> {
426 checker::check(self)
427 }
428
429 #[cfg(feature = "sort")]
430 pub fn sort(&mut self) {
433 sort::sort(self);
434 }
435
436 #[cfg(feature = "sort")]
437 pub fn sort_new_items(&mut self) {
439 sort::sort_new_items(self);
440 }
441
442 #[cfg(feature = "cleanup")]
443 pub fn cleanup(&mut self) {
445 cleanup::cleanup(self);
446 }
447
448 #[cfg(feature = "ifdata_cleanup")]
449 pub fn ifdata_cleanup(&mut self) {
452 ifdata::remove_unknown_ifdata(self);
453 }
454}
455
456#[derive(Debug, Clone)]
457struct Filename {
458 full: OsString,
460 display: String,
462}
463
464impl Filename {
465 pub(crate) fn new(full: OsString, display: &str) -> Self {
466 Self {
467 full,
468 display: display.to_string(),
469 }
470 }
471}
472
473impl From<&str> for Filename {
474 fn from(value: &str) -> Self {
475 Self {
476 full: OsString::from(value),
477 display: String::from(value),
478 }
479 }
480}
481
482impl From<&Path> for Filename {
483 fn from(value: &Path) -> Self {
484 Self {
485 display: value.to_string_lossy().to_string(),
486 full: OsString::from(value),
487 }
488 }
489}
490
491impl From<OsString> for Filename {
492 fn from(value: OsString) -> Self {
493 Self {
494 display: value.to_string_lossy().to_string(),
495 full: value,
496 }
497 }
498}
499
500impl Display for Filename {
501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502 f.write_str(&self.display)
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use tempfile::tempdir;
510
511 #[test]
512 fn load_empty_file() {
513 let result = load_from_string("", None, false);
514 assert!(result.is_err());
515 let error = result.unwrap_err();
516 assert!(matches!(error, A2lError::EmptyFileError { .. }));
517 }
518
519 #[test]
520 fn test_load_file() {
521 let dir = tempdir().unwrap();
522
523 let path = dir.path().join("test.a2l");
525 let path = path.to_str().unwrap();
526
527 let text = r#"
528 ASAP2_VERSION 1 71
529 /begin PROJECT new_project ""
530 /begin MODULE new_module ""
531 /end MODULE
532 /end PROJECT
533 "#;
534 std::fs::write(path, text).unwrap();
535
536 let (a2l, _) = load(path, None, false).unwrap();
537 assert_eq!(a2l.project.module[0].name, "new_module");
538
539 let nonexistent_path = dir.path().join("nonexistent.a2l");
541 let nonexistent_path = nonexistent_path.to_str().unwrap();
542 let result = load(nonexistent_path, None, false);
543 assert!(matches!(result, Err(A2lError::FileOpenError { .. })));
544 }
545
546 #[test]
547 fn bad_a2ml_data() {
548 let result = load_from_string(
549 r#"/begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT"#,
550 Some("x".to_string()),
551 false,
552 );
553 assert!(result.is_err());
554 let error = result.unwrap_err();
555 assert!(matches!(error, A2lError::InvalidBuiltinA2mlSpec { .. }));
556 }
557
558 #[test]
559 fn strict_parsing_version_error() {
560 let result = load_from_string(
562 r#"/begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT"#,
563 None,
564 true,
565 );
566 assert!(result.is_err());
567 let error = result.unwrap_err();
568 assert!(matches!(
569 error,
570 A2lError::ParserError {
571 parser_error: ParserError::MissingVersionInfo
572 }
573 ));
574
575 let result = load_from_string(r#"ASAP2_VERSION 1 /begin PROJECT"#, None, true);
577 assert!(result.is_err());
578 let error = result.unwrap_err();
579 assert!(matches!(
580 error,
581 A2lError::ParserError {
582 parser_error: ParserError::MissingVersionInfo
583 }
584 ));
585 }
586
587 #[test]
588 fn additional_tokens() {
589 let result = load_from_string(
591 r#"ASAP2_VERSION 1 71 /begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT abcdef"#,
592 None,
593 false,
594 );
595 assert!(result.is_ok());
596 let (_a2l, log_msgs) = result.unwrap();
597 assert_eq!(log_msgs.len(), 1);
598
599 let result = load_from_string(
601 r#"ASAP2_VERSION 1 71 /begin PROJECT x "" /begin MODULE y "" /end MODULE /end PROJECT abcdef"#,
602 None,
603 true,
604 );
605 assert!(result.is_err());
606 let error = result.unwrap_err();
607 assert!(matches!(
608 error,
609 A2lError::ParserError {
610 parser_error: ParserError::AdditionalTokensError { .. }
611 }
612 ));
613 }
614
615 #[test]
616 fn write_nonexistent_file() {
617 let a2l = new();
618 let result = a2l.write(
619 "__NONEXISTENT__/__FILE__/__PATH__/test.a2l",
620 Some("test case write_nonexistent_file()"),
621 );
622 assert!(result.is_err());
623 }
624
625 #[test]
626 fn write_with_banner() {
627 let dir = tempdir().unwrap();
629 let path = dir.path().join("test.a2l");
630 let path = path.to_str().unwrap();
631
632 let mut a2l = new();
633 a2l.asap2_version
634 .as_mut()
635 .unwrap()
636 .get_layout_mut()
637 .start_offset = 0;
638 let result = a2l.write(path, Some("test case write_nonexistent_file()"));
639 assert!(result.is_ok());
640 let file_text = String::from_utf8(std::fs::read(path).unwrap()).unwrap();
641 assert!(file_text.starts_with("/* test case write_nonexistent_file() */"));
642 std::fs::remove_file(path).unwrap();
643
644 a2l.asap2_version
645 .as_mut()
646 .unwrap()
647 .get_layout_mut()
648 .start_offset = 1;
649 let result = a2l.write(path, Some("test case write_nonexistent_file()"));
650 assert!(result.is_ok());
651 let file_text = String::from_utf8(std::fs::read(path).unwrap()).unwrap();
652 assert!(file_text.starts_with("/* test case write_nonexistent_file() */"));
653 std::fs::remove_file(path).unwrap();
654 }
655
656 #[cfg(feature = "merge")]
657 #[test]
658 fn merge() {
659 let mut a2l = new();
661 let mut a2l_2 = new();
662 a2l.asap2_version = None;
663 a2l.merge_modules(&mut a2l_2);
664 assert!(a2l.asap2_version.is_some());
665
666 let mut a2l = new();
668 let mut a2l_2 = new();
669 a2l.asap2_version = Some(Asap2Version::new(1, 50));
670 a2l.merge_modules(&mut a2l_2);
671 assert!(a2l.asap2_version.is_some());
672 assert!(matches!(
673 a2l.asap2_version,
674 Some(Asap2Version {
675 version_no: 1,
676 upgrade_no: 71,
677 ..
678 })
679 ));
680
681 let mut a2l = new();
683 let mut a2l_2 = new();
684 a2l_2.project.module[0].compu_tab.push(CompuTab::new(
686 "compu_tab".to_string(),
687 String::new(),
688 ConversionType::Identical,
689 0,
690 ));
691 a2l.project.module[0].merge(&mut a2l_2.project.module[0]);
692 assert_eq!(a2l.project.module[0].compu_tab.len(), 1);
694 }
695
696 #[test]
697 fn test_load_fagment() {
698 let result = load_fragment("", None);
700 assert!(result.is_ok());
701
702 let result = load_fragment(
704 r#"
705 /begin MEASUREMENT measurement_name ""
706 UBYTE CM.IDENTICAL 0 0 0 255
707 ECU_ADDRESS 0x13A00
708 FORMAT "%5.0" /* Note: Overwrites the format stated in the computation method */
709 DISPLAY_IDENTIFIER DI.ASAM.M.SCALAR.UBYTE.IDENTICAL /* optional display identifier */
710 /begin IF_DATA ETK KP_BLOB 0x13A00 INTERN 1 RASTER 2 /end IF_DATA
711 /end MEASUREMENT"#,
712 None,
713 );
714 assert!(result.is_ok());
715
716 let result = load_fragment(
718 r#"
719 /begin MEASUREMENT measurement_name ""
720 UBYTE CM.IDENTICAL 0 0 0 255
721 ECU_ADDRESS 0x13A00
722 /begin IF_DATA ETK KP_BLOB 0x13A00 INTERN 1 RASTER 2 /end IF_DATA
723 /end MEASUREMENT"#,
724 Some(r#"block "IF_DATA" long;"#.to_string()),
725 );
726 assert!(result.is_ok());
727
728 let result = load_fragment(
730 r#"
731 /begin MEASUREMENT measurement_name ""
732 UBYTE CM.IDENTICAL 0 0 0 255
733 ECU_ADDRESS 0x13A00
734 /begin IF_DATA ETK KP_BLOB 0x13A00 INTERN 1 RASTER 2 /end IF_DATA
735 /end MEASUREMENT"#,
736 Some(r#"lorem ipsum"#.to_string()),
737 );
738 assert!(matches!(
739 result,
740 Err(A2lError::InvalidBuiltinA2mlSpec { .. })
741 ));
742
743 let result = load_fragment("12345", None);
745 assert!(matches!(result, Err(A2lError::ParserError { .. })));
746
747 let result = load_fragment(",,,", None);
748 println!("{:?}", result);
749 assert!(matches!(result, Err(A2lError::TokenizerError { .. })));
750 }
751
752 #[test]
753 fn test_load_fagment_file() {
754 let dir = tempdir().unwrap();
755 let path = dir.path().join("fragment.a2l");
756 let path = path.to_str().unwrap();
757
758 std::fs::write(
760 path,
761 r#"
762 /begin MEASUREMENT measurement_name ""
763 UBYTE CM.IDENTICAL 0 0 0 255
764 /end MEASUREMENT"#,
765 )
766 .unwrap();
767 let result = load_fragment_file(path, None);
768 assert!(result.is_ok());
769
770 let nonexistent_path = dir.path().join("nonexistent.a2l");
772 let nonexistent_path = nonexistent_path.to_str().unwrap();
773 let result = load_fragment_file(nonexistent_path, None);
774 assert!(matches!(result, Err(A2lError::FileOpenError { .. })));
775 }
776
777 #[test]
778 fn test_filename() {
779 let filename = Filename::from("test.a2l");
780 assert_eq!(filename.to_string(), "test.a2l");
781
782 let filename = Filename::from(OsString::from("test.a2l"));
783 assert_eq!(filename.to_string(), "test.a2l");
784
785 let filename = Filename::from(Path::new("test.a2l"));
786 assert_eq!(filename.to_string(), "test.a2l");
787 }
788}