a2lfile/
lib.rs

1//! a2lfile is a library that allows you to read, modify and write a2l files.
2//!
3//! It is fast, preserves the formatting of the input, and has support for files using standard version 1.71.
4//!
5//! # Features
6//!
7//! - `check`: perform a consistency check on the data
8//! - `cleanup`: remove unused `GROUP`s, `RECORD_LAYOUTs`, `COMPU_METHOD`s, `COMPU_(V)TAB`s and `UNIT`s
9//! - `ifdata_cleanup`: remove any `IF_DATA` blocks that could not be parsed using either the specification provided during load or the specification in the A2ML block in the file
10//! - `merge`: merge two a2l files on the `MODULE` level
11//! - `sort`: sort the data in the a2l file
12
13mod 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;
37// used internally
38use parser::A2lVersion;
39use parser::{ParseContext, ParserState};
40
41// re-export for the crate user
42pub 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    /// `FileOpenError`: An `IoError` that occurred while loading a file
54    #[error("Failed to load {filename}: {ioerror}")]
55    FileOpenError {
56        filename: PathBuf,
57        ioerror: std::io::Error,
58    },
59
60    /// `FileReadError`: An `IoError` that occurred while reading from a file
61    #[error("Could not read from {filename}: {ioerror}")]
62    FileReadError {
63        filename: PathBuf,
64        ioerror: std::io::Error,
65    },
66
67    /// `EmptyFileError`: No `A2lTokens` found in the file
68    #[error("File \"{filename}\" contains no a2l data")]
69    EmptyFileError { filename: PathBuf },
70
71    /// `InvalidBuiltinA2mlSpec`: Parse error while processing a built-in a2ml specification
72    #[error("Failed to load built-in a2ml specification: {parse_err}")]
73    InvalidBuiltinA2mlSpec { parse_err: String },
74
75    /// `TokenizerError`: Failed to tokenize the input
76    #[error("Tokenizer error: {tokenizer_error}")]
77    TokenizerError { tokenizer_error: TokenizerError },
78
79    /// `ParserError`: Invalid data, the file could not be parsed
80    #[error("Parser error: {parser_error}")]
81    ParserError { parser_error: ParserError },
82
83    /// `FileWriteError`: An `IoError` that occurred while writing from a file
84    #[error("Could not write to {filename}: {ioerror}")]
85    FileWriteError {
86        filename: PathBuf,
87        ioerror: std::io::Error,
88    },
89
90    /// `NameCollisionError`: A name collision occurred bateween two blocks of the same type
91    #[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    /// `NameCollisionError2`: A name collision occurred bateween two different blocks which share the same namespace
102    #[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    /// `CrossReferenceError`: A reference to a non-existent item was found
114    #[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    /// `LimitCheckError`: The given limits are outside of the calculated limits
126    #[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    /// `GroupStructureError`: A GROUP block cannot be both a ROOT and a sub group at the same time. it also cannot be a sub group of multiple groups
140    #[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    /// `ContentError`: A block contains invalid content of some description
148    #[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/**
158Create a new a2l file
159
160```rust
161let new_a2l = a2lfile::new();
162assert_eq!(new_a2l.project.module.len(), 1);
163```
164
165The created file is equivalent to loading a string containing
166```text
167ASAP2_VERSION 1 71
168/begin PROJECT new_project ""
169  /begin MODULE new_module ""
170  /end MODULE
171/end PROJECT
172```
173 */
174#[must_use]
175pub fn new() -> A2lFile {
176    // a minimal a2l file needs only a PROJECT containing a MODULE
177    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    // only one line break for PROJECT (after ASAP2_VERSION) instead of the default 2
184    a2l_file.project.get_layout_mut().start_offset = 1;
185    // only one line break for MODULE [0] instead of the default 2
186    a2l_file.project.module[0].get_layout_mut().start_offset = 1;
187    // also set ASAP2_VERSION 1.71
188    a2l_file.asap2_version = Some(Asap2Version::new(1, 71));
189
190    a2l_file
191}
192
193/**
194Load an a2l file
195
196`a2ml_spec` is optional and contains a String that is valid A2ML that can be used while parsing the file in addition to the A2ML that might be contained inside the A2ML block in the file.
197If a definition is provided here and there is also an A2ML block in the file, then the definition provided here will be tried first during parsing.
198
199`log_msgs` is a reference to a `Vec<A2LError>` which will receive all warnings generated during parsing
200
201`strict_parsing` toggles strict parsing: If strict parsing is enabled, most warnings become errors.
202
203# Example
204```
205# use a2lfile::A2lError;
206match a2lfile::load("example.a2l", None, true) {
207    Ok((a2l_file, log_messages)) => {/* do something with it*/},
208    Err(error_message) => println!("{error_message}")
209}
210```
211
212# Errors
213
214An `A2lError` provides details information if loading the file fails.
215 */
216pub 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
226/**
227load a2l data stored in a string
228
229`a2ldata` contains the text of an a2l file.
230
231`a2ml_spec` is optional and contains a String that is valid A2ML that can be used while parsing the file in addition to the A2ML that might be contained inside the A2ML block in the file.
232If a definition is provided here and there is also an A2ML block in the file, then the definition provided here will be tried first during parsing.
233
234`log_msgs` is a reference to a `Vec<A2LError>` which will receive all warnings generated during parsing
235
236`strict_parsing` toggles strict parsing: If strict parsing is enabled, most warnings become errors.
237
238# Example
239
240```rust
241# use a2lfile::A2lError;
242# use crate::a2lfile::A2lObjectName;
243# fn main() -> Result<(), A2lError> {
244let text = r#"
245ASAP2_VERSION 1 71
246/begin PROJECT new_project ""
247  /begin MODULE new_module ""
248  /end MODULE
249/end PROJECT
250"#;
251let (a2l, log_msgs) = a2lfile::load_from_string(&text, None, true).unwrap();
252assert_eq!(a2l.project.module[0].get_name(), "new_module");
253# Ok(())
254# }
255```
256
257# Errors
258
259An `A2lError` provides details information if loading the data fails.
260 */
261pub 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    // tokenize the input data
278    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    // create the parser state object
288    let mut parser = ParserState::new(&tokenresult, &mut log_msgs, strict_parsing);
289
290    // if a built-in A2ml specification was passed as a string, then it is parsed here
291    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    // build the a2l data structures from the tokens
300    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
307/// load an a2l fragment
308///
309/// An a2l fragment is just the bare content of a module, without the enclosing PROJECT and MODULE.
310/// Because the fragment cannot specify a version, strict parsing is not available.
311///
312/// # Errors
313///
314/// If reading or parsing of the file fails, the `A2lError` will give details about the problem.
315pub fn load_fragment(a2ldata: &str, a2ml_spec: Option<String>) -> Result<Module, A2lError> {
316    let fixed_a2ldata = format!(r#"fragment "" {a2ldata} /end MODULE"#);
317    // tokenize the input data
318    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    // create the parser state object
328    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); // doesn't really matter with strict = false
331
332    // if a built-in A2ml specification was passed as a string, then it is parsed here
333    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    // build the a2l data structures from the tokens
341    Module::parse(&mut parser, &context, 0)
342        .map_err(|parser_error| A2lError::ParserError { parser_error })
343}
344
345/// load an a2l fragment from a file
346///
347/// # Errors
348///
349/// If reading or parsing of the file fails, the `A2lError` will give details about the problem.
350pub 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    /// construct a string containing the whole a2l data of this `A2lFile` object
361    #[must_use]
362    pub fn write_to_string(&self) -> String {
363        self.stringify(0)
364    }
365
366    /// write this `A2lFile` object to the given file
367    /// the banner will be placed inside a comment at the beginning of the file; `/*` and `*/` should not be part of the banner string
368    ///
369    /// # Errors
370    ///
371    /// [`A2lError::FileWriteError`] if writing the file fails.
372    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 the first line is empty (first charachter is \n), then the banner is placed on the empty line
380            // otherwise a newline is added
381            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    /// Merge another a2l file on the MODULE level.
397    ///
398    /// The input file and the merge file must each contain exactly one MODULE.
399    /// The contents will be merged so that there is one merged MODULE in the output.
400    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 the merge file uses a newer file version, then the file version is upgraded by the merge
407        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            // ASAP2_VERSION is required in newer revisions of the standard, but old files might not have it
418            self.asap2_version = std::mem::take(&mut merge_file.asap2_version);
419        }
420    }
421
422    #[cfg(feature = "check")]
423    /// perform a consistency check on the data.
424    #[must_use]
425    pub fn check(&self) -> Vec<A2lError> {
426        checker::check(self)
427    }
428
429    #[cfg(feature = "sort")]
430    /// sort the data in the a2l file.
431    /// This changes the order in which the blocks will be written to an output file
432    pub fn sort(&mut self) {
433        sort::sort(self);
434    }
435
436    #[cfg(feature = "sort")]
437    /// sort newly added or merged blocks into sensible locations between the existing blocks
438    pub fn sort_new_items(&mut self) {
439        sort::sort_new_items(self);
440    }
441
442    #[cfg(feature = "cleanup")]
443    /// cleanup: remove unused GROUPs, `RECORD_LAYOUTs`, `COMPU_METHODs`, COMPU_(V)TABs and UNITs
444    pub fn cleanup(&mut self) {
445        cleanup::cleanup(self);
446    }
447
448    #[cfg(feature = "ifdata_cleanup")]
449    /// cleanup `IF_DATA`: remove any `IF_DATA` blocks that could not be parsed using either the
450    /// specification provided during load or the specification in the A2ML block in the file
451    pub fn ifdata_cleanup(&mut self) {
452        ifdata::remove_unknown_ifdata(self);
453    }
454}
455
456#[derive(Debug, Clone)]
457struct Filename {
458    // the full filename, which has been extended with a base path relative to the working directory
459    full: OsString,
460    // the "display" name, i.e. the name that appears in an /include directive or an error message
461    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        // create a file in a temp directory and load it
524        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        // try to load a file that does not exist
540        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        // version is missing completely
561        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        // version is damaged
576        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        // strict parsing off - no error
590        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        // strict parsing on - error
600        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        // set the current working directory to a temp dir
628        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        // version is copied if none exists
660        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        // version is updated if the merged file has a higher version
667        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        // merge modules
682        let mut a2l = new();
683        let mut a2l_2 = new();
684        // create an item in the module of a2l_2
685        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        // verify that the item was merged into the module of a2l
693        assert_eq!(a2l.project.module[0].compu_tab.len(), 1);
694    }
695
696    #[test]
697    fn test_load_fagment() {
698        // an empty string is a valid fragment
699        let result = load_fragment("", None);
700        assert!(result.is_ok());
701
702        // load a fragment with some data
703        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        // load a fragment with some data and a specification
717        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        // load a fragment with some data and an invalid specification
729        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        // random data is not a valid fragment
744        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        // load a fragment with some data
759        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        // try to load a nonexistent file
771        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}