autosar_data/
lib.rs

1//! Crate autosar-data
2//!
3//! This crate provides functionality to read, modify and write Autosar arxml files,
4//! both separately and in projects consisting of multiple files.
5//!
6//! Features:
7//!
8//!  - read and write arxml files
9//!  - fully validate all data when it is loaded
10//!  - non-strict mode so that invalid but structurally sound data can be loaded
11//!  - various element operations to modify and create sub-elements, data and attributes
12//!  - support for Autosar paths and cross references
13//!  - all operations are thread safe, e.g. it is possible to load mutliple files on separate threads
14//!
15//! # Examples
16//!
17//! ```no_run
18//! use autosar_data::*;
19//! # fn main() -> Result<(), AutosarDataError> {
20//! /* load a multi-file data model */
21//! let model = AutosarModel::new();
22//! let (file_1, warnings_1) = model.load_file("some_file.arxml", false)?;
23//! let (file_2, warnings_2) = model.load_file("other_file.arxml", false)?;
24//! /* load a buffer */
25//! # let buffer = b"";
26//! let (file_3, _) = model.load_buffer(buffer, "filename.arxml", true)?;
27//!
28//! /* write all files of the model */
29//! model.write()?;
30//!
31//! /* alternatively: */
32//! for file in model.files() {
33//!     let file_data = file.serialize();
34//!     // do something with file_data
35//! }
36//!
37//! /* iterate over all elements in all files */
38//! for (depth, element) in model.elements_dfs() {
39//!     if element.is_identifiable() {
40//!         /* the element is identifiable using an Autosar path */
41//!         println!("{depth}: {}, {}", element.element_name(), element.path()?);
42//!     } else {
43//!         println!("{depth}: {}", element.element_name());
44//!     }
45//! }
46//!
47//! /* get an element by its Autosar path */
48//! let pdu_element = model.get_element_by_path("/Package/Mid/PduName").unwrap();
49//!
50//! /* work with the content of elements */
51//! if let Some(length) = pdu_element
52//!     .get_sub_element(ElementName::Length)
53//!     .and_then(|elem| elem.character_data())
54//!     .and_then(|cdata| cdata.string_value())
55//! {
56//!     println!("Pdu Length: {length}");
57//! }
58//!
59//! /* modify the attributes of an element */
60//! pdu_element.set_attribute_string(AttributeName::Uuid, "12ab34cd-1234-1234-1234-12ab34cd56ef");
61//! pdu_element.remove_attribute(AttributeName::Uuid);
62//!
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! # Example Programs
68//!
69//! Two complete example programs can be found in the examples directory of the source repostitory. They are:
70//!
71//!  - businfo, which extracts information about bus settings, frames, pdus and signals from an autosar ECU extract
72//!  - `generate_files`, which for each Autosar version generates an arxml file containing a least one instance of every specified element
73//!
74
75#![warn(missing_docs)]
76
77use autosar_data_specification::{AttributeSpec, CharacterDataSpec, ContentMode, ElementType};
78use fxhash::{FxBuildHasher, FxHashMap};
79use indexmap::IndexMap;
80pub use iterators::*;
81use parking_lot::RwLock;
82use parser::ArxmlParser;
83use smallvec::SmallVec;
84use std::collections::HashSet;
85use std::path::{Path, PathBuf};
86use std::sync::{Arc, Weak};
87use std::{fs::File, io::Read};
88use thiserror::Error;
89
90mod arxmlfile;
91mod autosarmodel;
92mod chardata;
93mod element;
94mod elementraw;
95mod iterators;
96mod lexer;
97mod parser;
98
99// allow public access to the error sub-types
100pub use lexer::ArxmlLexerError;
101pub use parser::ArxmlParserError;
102
103// reexport some of the info from the specification
104pub use autosar_data_specification::AttributeName;
105pub use autosar_data_specification::AutosarVersion;
106pub use autosar_data_specification::ElementName;
107pub use autosar_data_specification::EnumItem;
108
109type FxIndexMap<K, V> = IndexMap<K, V, FxBuildHasher>;
110
111/// `AutosarModel` is the top level data type in the autosar-data crate.
112///
113/// An instance of `AutosarModel` is required for all other operations.
114///
115/// The model contains the hierarchy of Autosar elements. It can be created manually or loaded from one or more arxml files.
116/// It stores the association between elements and files.
117/// In addition, this top-level structure provides caching of Autosar paths, to allow quick resolution of cross-references.
118#[derive(Clone)]
119pub struct AutosarModel(Arc<RwLock<AutosarModelRaw>>);
120
121// Weak reference to an instance of AutosarModel
122#[derive(Clone)]
123pub(crate) struct WeakAutosarModel(Weak<RwLock<AutosarModelRaw>>);
124
125/// The inner autosar data model (unlocked)
126///
127/// The model contains the hierarchy of Autosar elements. It can be created manually or loaded from one or more arxml files.
128/// It stores the association between elements and files.
129/// In addition, this top-level structure provides caching of Autosar paths, to allow quick resolution of cross-references.
130pub(crate) struct AutosarModelRaw {
131    root_element: Element,
132    files: Vec<ArxmlFile>,
133    /// `identifiables` is a `HashMap` of all named elements, needed to resolve references without doing a full search.
134    identifiables: FxIndexMap<String, WeakElement>,
135    /// `reference_origins` is a `HashMap` of all referencing elements. This is needed to efficiently fix up the references when a referenced element is renamed.
136    reference_origins: FxHashMap<String, Vec<WeakElement>>,
137}
138
139/// The error type `AutosarDataError` wraps all errors that can be generated anywhere in the crate
140#[derive(Error, Debug)]
141#[non_exhaustive]
142pub enum AutosarDataError {
143    /// `IoErrorRead`: An `IoError` that occurred while reading a file
144    #[error("Failed to read {}: {ioerror}", .filename.to_string_lossy())]
145    IoErrorRead {
146        /// The filename that caused the error
147        filename: PathBuf,
148        /// The underlying `std::io::Error`
149        ioerror: std::io::Error,
150    },
151
152    /// `IoErrorOpen`: an `IoError` that occurres while opening a file
153    #[error("Failed to open {}: {ioerror}", .filename.to_string_lossy())]
154    IoErrorOpen {
155        /// The filename that caused the error
156        filename: PathBuf,
157        /// The underlying `std::io::Error`
158        ioerror: std::io::Error,
159    },
160
161    /// `IoErrorWrite`: An `IoError` that occurred while writing a file
162    #[error("Failed to write {}: {ioerror}", .filename.to_string_lossy())]
163    IoErrorWrite {
164        /// The filename that caused the error
165        filename: PathBuf,
166        /// The underlying `std::io::Error`
167        ioerror: std::io::Error,
168    },
169
170    /// `DuplicateFilenameError`: The model can'#'t contain two files with identical names
171    #[error("Could not {verb} file {}: A file with this name is already loaded", .filename.to_string_lossy())]
172    DuplicateFilenameError {
173        /// description of the operation that failed
174        verb: &'static str,
175        /// The filename that caused the error
176        filename: PathBuf,
177    },
178
179    /// `LexerError`: An error originating in the lexer, such as unclodes strings, mismatched '<' and '>', etc
180    #[error("Failed to tokenize {} on line {line}: {source}", .filename.to_string_lossy())]
181    LexerError {
182        /// The filename that caused the error
183        filename: PathBuf,
184        /// The line number where the error occurred
185        line: usize,
186        /// The underlying `ArxmlLexerError`
187        source: ArxmlLexerError,
188    },
189
190    /// `ParserError`: A parser error
191    #[error("Failed to parse {}:{line}: {source}", .filename.to_string_lossy())]
192    ParserError {
193        /// The filename that caused the error
194        filename: PathBuf,
195        /// The line number where the error occurred
196        line: usize,
197        /// The underlying `ArxmlParserError`
198        source: ArxmlParserError,
199    },
200
201    /// A file could not be loaded into the model, because the Autosar paths of the new data overlapped with the Autosar paths of the existing data
202    #[error("Loading failed: element path {path} of new data in {} overlaps with the existing loaded data", .filename.to_string_lossy())]
203    OverlappingDataError {
204        /// The filename that caused the error
205        filename: PathBuf,
206        /// Autosar path of the element that caused the error
207        path: String,
208    },
209
210    /// An operation failed because one of the elements involved is in the deleted state and will be freed once its reference count reaches zero
211    #[error("Operation failed: the item has been deleted")]
212    ItemDeleted,
213
214    /// A sub element could not be created at or moved to the given position
215    #[error("Invalid position for an element of this kind")]
216    InvalidPosition,
217
218    /// The Autosar version of the new file or element did not match the version already in use
219    #[error("Version mismatch between existing {} and new {}", .version_cur, .version_new)]
220    VersionMismatch {
221        /// The current version of the model
222        version_cur: AutosarVersion,
223        /// The version of the new file or element
224        version_new: AutosarVersion,
225    },
226
227    /// The Autosar version is not compatible with the data
228    #[error("Version {} is not compatible with the element data", .version)]
229    VersionIncompatibleData {
230        /// The incompatible version
231        version: AutosarVersion,
232    },
233
234    /// A function that only applies to identifiable elements was called on an element which is not identifiable
235    #[error("The element at {} is not identifiable", .xmlpath)]
236    ElementNotIdentifiable {
237        /// The "xml path" (a string representation of the path to the element) where the error occurred
238        xmlpath: String,
239    },
240
241    /// An item name is required to perform this action
242    #[error("An item name is required for element {}", .element)]
243    ItemNameRequired {
244        /// The element where the item name is required
245        element: ElementName,
246    },
247
248    /// The element has the wrong content type for the requested operation, e.g. inserting elements when the content type only allows character data
249    #[error("Incorrect content type for element {}", .element)]
250    IncorrectContentType {
251        /// The element where the content type is incorrect
252        element: ElementName,
253    },
254
255    /// Could not insert a sub element, because it conflicts with an existing sub element
256    #[error("Element insertion conflict: {} could not be inserted in {} ({})", .element, .parent, .parent_path)]
257    ElementInsertionConflict {
258        /// The name of the parent element
259        parent: ElementName,
260        /// The name of the element that could not be inserted
261        element: ElementName,
262        /// path of the parent element
263        parent_path: String,
264    },
265
266    /// The `ElementName` is not a valid sub element according to the specification.
267    #[error("Element {} is not a valid sub element of {}", .element, .parent)]
268    InvalidSubElement {
269        /// The name of the parent element
270        parent: ElementName,
271        /// The name of the element that is not a valid sub element
272        element: ElementName,
273    },
274
275    /// Remove operation failed: the given element is not a sub element of the element from which it was supposed to be removed
276    #[error("element {} not found in parent {}", .target, .parent)]
277    ElementNotFound {
278        /// The name of the element that was not found
279        target: ElementName,
280        /// The name of the parent element
281        parent: ElementName,
282    },
283
284    /// [`Element::remove_sub_element`] cannot remove the SHORT-NAME of identifiable elements, as this would render the data invalid
285    #[error("the SHORT-NAME sub element may not be removed")]
286    ShortNameRemovalForbidden,
287
288    /// get/set reference target was called for an element that is not a reference
289    #[error("The current element is not a reference")]
290    NotReferenceElement,
291
292    /// The reference is invalid
293    #[error("The reference is not valid")]
294    InvalidReference,
295
296    /// An element could not be renamed, since this item name is already used by a different element
297    #[error("Duplicate item name {} in {}", .item_name, .element)]
298    DuplicateItemName {
299        /// The name of the element that could not be renamed
300        element: ElementName,
301        /// The target name that caused the error
302        item_name: String,
303    },
304
305    /// Cannot move an element into its own sub element
306    #[error("Cannot move an element into its own sub element")]
307    ForbiddenMoveToSubElement,
308
309    /// Cannot copy an element (or a hierarchy including the element) into itself
310    #[error("Cannot create a copy that includes the destination")]
311    ForbiddenCopyOfParent,
312
313    /// A parent element is currently locked by a different operation. The operation wa aborted to avoid a deadlock.
314    #[error("A parent element is currently locked by a different operation")]
315    ParentElementLocked,
316
317    /// The attribute is invalid here
318    #[error("The attribute is not valid for this element")]
319    InvalidAttribute,
320
321    /// The attribute value is invalid
322    #[error("The given value is not valid for this attribute")]
323    InvalidAttributeValue,
324
325    /// The file is from a different model and may not be used in this operation
326    #[error("The file is from a different model and may not be used in this operation")]
327    InvalidFile,
328
329    /// The file is empty and cannot be serialized
330    #[error("The file is empty and cannot be serialized")]
331    EmptyFile,
332
333    /// The newly loaded file diverges from the combined model on an element which is not splittable according to the metamodel
334    #[error("The new file could not be merged, because it diverges from the model on non-splittable element {}", .path)]
335    InvalidFileMerge {
336        /// The path of the element where the merge failed
337        path: String,
338    },
339
340    /// The operation cannot be completed because the model does not contain any files
341    #[error("The operation cannot be completed because the model does not contain any files")]
342    NoFilesInModel,
343
344    /// Modifying the fileset of this element is not allowed
345    #[error(
346        "Modifying the fileset of this element is not allowed, because the parent of the element is not marked as splittable"
347    )]
348    FilesetModificationForbidden,
349}
350
351/// An Autosar arxml file
352#[derive(Clone)]
353pub struct ArxmlFile(Arc<RwLock<ArxmlFileRaw>>);
354
355/// Weak reference to an arxml file
356///
357/// (see the documentation of [`std::sync::Arc`] for an explanation of weak references)
358#[derive(Clone)]
359pub struct WeakArxmlFile(Weak<RwLock<ArxmlFileRaw>>);
360
361/// The data of an arxml file
362pub(crate) struct ArxmlFileRaw {
363    pub(crate) version: AutosarVersion,
364    model: WeakAutosarModel,
365    pub(crate) filename: PathBuf,
366    pub(crate) xml_standalone: Option<bool>, // preserve the xml standalone attribute
367}
368
369/// An arxml element
370///
371/// This is a wrapper type which provides all the necessary manipulation functions.
372#[derive(Clone)]
373pub struct Element(Arc<RwLock<ElementRaw>>);
374
375/// Weak reference to an Element
376///
377/// (see the documentation of [`std::sync::Arc`] for an explanation of weak references)
378///
379/// This `WeakElement` can be held indefinitely without forcing the referenced data to remain valid.
380/// When access is needed, the method `upgrade()` will attempt to get a strong reference and return an [Element]
381#[derive(Clone)]
382pub struct WeakElement(Weak<RwLock<ElementRaw>>);
383
384/// The data of an arxml element
385pub(crate) struct ElementRaw {
386    pub(crate) parent: ElementOrModel,
387    pub(crate) elemname: ElementName,
388    pub(crate) elemtype: ElementType,
389    pub(crate) content: SmallVec<[ElementContent; 4]>,
390    pub(crate) attributes: SmallVec<[Attribute; 1]>,
391    pub(crate) file_membership: HashSet<WeakArxmlFile>,
392    pub(crate) comment: Option<String>,
393}
394
395/// A single attribute of an arxml element
396#[derive(Clone, PartialEq, Eq)]
397pub struct Attribute {
398    /// The name of the attribute
399    pub attrname: AttributeName,
400    /// The content of the attribute
401    pub content: CharacterData,
402}
403
404/// One content item inside an arxml element
405///
406/// Elements may contain other elements, character data, or a mixture of both, depending on their type.
407#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
408pub enum ElementContent {
409    /// A sub element
410    Element(Element),
411    /// Character data
412    CharacterData(CharacterData),
413}
414
415/// The enum `CharacterData` provides typed access to the content of elements and attributes
416///
417/// Example:
418///
419/// In the xml string ```<SHORT-NAME>SomeName</SHORT-NAME>``` the character data
420/// "`SomeName`" will be loaded as `CharacterData::String("SomeName`"), while the content of the
421/// attribute <... DEST="UNIT"> will be loaded as `CharacterData::Enum(EnumItem::Unit`)
422#[derive(Debug, PartialEq, Clone)]
423pub enum CharacterData {
424    /// Character data is an enum value
425    Enum(EnumItem),
426    /// Character data is a string
427    String(String),
428    /// Character data is an unsigned integer
429    UnsignedInteger(u64),
430    /// Character data is a floating point number
431    Float(f64),
432}
433
434/// The content type of an [Element]
435#[derive(Debug, Eq, PartialEq, Clone, Copy)]
436pub enum ContentType {
437    /// The element only contains other elements
438    Elements,
439    /// The element only contains character data
440    CharacterData,
441    /// The element contains both character data and sub elements
442    Mixed,
443}
444
445/// Holds a weak reference to either an element or an arxml file
446///
447/// This enum is used for references to the parent of each element. For all elements other than the
448/// root element, the parent is an element. The root element itself has a reference to the `ArxmlFile` structure.
449#[derive(Clone)]
450pub(crate) enum ElementOrModel {
451    Element(WeakElement),
452    Model(WeakAutosarModel),
453    None, // needed while constructing the data trees, otherwise there's a chicken vs. egg problem
454}
455
456/// Possible kinds of compatibility errors that can be found by `ArxmlFile::check_version_compatibility()`
457pub enum CompatibilityError {
458    /// The element is not allowed in the target version
459    IncompatibleElement {
460        /// The element that is not allowed
461        element: Element,
462        /// The version mask of the element which indicates all allowed versions
463        version_mask: u32,
464    },
465    /// The attribute is not allowed in the target version
466    IncompatibleAttribute {
467        /// The element that contains the incompatible attribute
468        element: Element,
469        /// The incompatible attribute
470        attribute: AttributeName,
471        /// The version mask of the element which indicates all versions where the attribute is allowed
472        version_mask: u32,
473    },
474    /// The attribute value is not allowed in the target version
475    IncompatibleAttributeValue {
476        /// The element that contains the incompatible attribute
477        element: Element,
478        /// The incompatible attribute
479        attribute: AttributeName,
480        /// The incompatible attribute value
481        attribute_value: String,
482        /// The version mask of the element which indicates all versions where the attribute value is allowed
483        version_mask: u32,
484    },
485}
486
487/// information about a sub element
488///
489/// This structure is returned by [`Element::list_valid_sub_elements()`]
490pub struct ValidSubElementInfo {
491    /// name of the potential sub element
492    pub element_name: ElementName,
493    /// is the sub element named, i.e. does it need to be created with [`Element::create_named_sub_element()`]
494    pub is_named: bool,
495    /// is the sub element currently allowed, given the existing content of the element. Note that some sub elements are mutually exclusive.
496    pub is_allowed: bool,
497}
498
499const CHECK_FILE_SIZE: usize = 4096; // 4kb
500
501/// Check a file to see if it looks like an arxml file
502///
503/// Reads the beginning of the given file and checks if the data starts with a valid arxml header.
504/// If a header is found it immediately returns true and does not check any further data
505///
506/// The function returns false if the file cannot be read or if the data does not start with an arxml header
507///
508/// # Parameters
509/// - filename: name of the file to check
510///
511/// # Example
512///
513/// ```
514/// # let filename = "";
515/// if autosar_data::check_file(filename) {
516///     // it looks like an arxml file
517/// }
518/// ```
519pub fn check_file<P: AsRef<Path>>(filename: P) -> bool {
520    let mut buffer: [u8; CHECK_FILE_SIZE] = [0; CHECK_FILE_SIZE];
521
522    if File::open(filename).and_then(|mut file| file.read(&mut buffer)).is_ok() {
523        check_buffer(&buffer)
524    } else {
525        false
526    }
527}
528
529/// Check a buffer to see if the content looks like arxml data
530///
531/// The function returns true if the buffer starts with a valid arxml header (after skipping whitespace and comments).
532/// This function does not check anything after the header.
533///
534/// # Parameters
535/// - buffer: u8 slice containing the data to check
536///
537/// # Example
538/// ```
539/// # let buffer = Vec::new();
540/// if autosar_data::check_buffer(&buffer) {
541///     // it looks like arxml data
542/// }
543/// ```
544#[must_use]
545pub fn check_buffer(buffer: &[u8]) -> bool {
546    let mut parser = ArxmlParser::new(PathBuf::from("none"), buffer, false);
547    parser.check_arxml_header()
548}
549
550/// Custom Debug implementation for `Attribute`, in order to provide better formatting
551impl std::fmt::Debug for Attribute {
552    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553        write!(f, "Attribute: {:?} = {:?}", self.attrname, self.content)
554    }
555}
556
557/// provide PartialOrd for attributes; this is used while sorting elements
558impl PartialOrd for Attribute {
559    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
560        Some(self.cmp(other))
561    }
562}
563
564/// provide Ord for attributes; this is used while sorting elements
565impl Ord for Attribute {
566    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
567        self.attrname
568            .to_str()
569            .cmp(other.attrname.to_str())
570            .then(self.content.cmp(&other.content))
571    }
572}
573
574#[cfg(test)]
575mod test {
576    use std::{error::Error, io::Write, path::PathBuf};
577    use tempfile::tempdir;
578
579    use crate::*;
580
581    #[test]
582    fn error_traits() {
583        let err = AutosarDataError::ParserError {
584            filename: PathBuf::from("filename.arxml"),
585            line: 123,
586            source: crate::parser::ArxmlParserError::InvalidArxmlFileHeader,
587        };
588        assert!(err.source().is_some());
589        let errstr = format!("{err}");
590        let errdbg = format!("{err:#?}");
591        assert!(errstr != errdbg);
592    }
593
594    #[test]
595    fn test_check_file() {
596        let dir = tempdir().unwrap();
597
598        // called on a directory rather than a file -> false
599        assert!(!check_file(dir.path()));
600
601        // nonexistent file -> false
602        let nonexistent = dir.path().with_file_name("nonexistent.arxml");
603        assert!(!check_file(nonexistent));
604
605        // arbitrary non-arxml data -> false
606        let not_arxml_file = dir.path().with_file_name("not_arxml.bin");
607        File::create(&not_arxml_file)
608            .and_then(|mut file| write!(file, "text"))
609            .unwrap();
610        assert!(!check_file(not_arxml_file));
611
612        // file containing a valid arxml header -> true
613        let header = r#"<?xml version="1.0" encoding="utf-8"?>
614        <!-- comment --><!-- comment 2 -->
615        <AUTOSAR xsi:schemaLocation="http://autosar.org/schema/r4.0 AUTOSAR_00050.xsd" xmlns="http://autosar.org/schema/r4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">"#;
616        let arxml_file = dir.path().with_file_name("file.arxml");
617        File::create(&arxml_file)
618            .and_then(|mut file| file.write(header.as_bytes()))
619            .unwrap();
620        assert!(check_file(arxml_file));
621
622        assert!(check_buffer(header.as_bytes()));
623    }
624
625    #[test]
626    fn attribute_order() {
627        // attribute ordering: first by name, then by value
628        let a1 = Attribute {
629            attrname: AttributeName::Uuid,
630            content: CharacterData::String("Value1".to_string()),
631        };
632
633        let a2 = Attribute {
634            attrname: AttributeName::Uuid,
635            content: CharacterData::String("Value2".to_string()),
636        };
637        assert!(a1 < a2);
638
639        let a3 = Attribute {
640            attrname: AttributeName::T,
641            content: CharacterData::String("xyz".to_string()),
642        };
643        assert!(a3 < a1);
644
645        // PartialOrd
646        assert!(a1.partial_cmp(&a2) == Some(std::cmp::Ordering::Less));
647    }
648}