a2lfile/
parser.rs

1use num_traits::{AsPrimitive, Num};
2use std::fmt::Display;
3use thiserror::Error;
4
5use crate::a2ml::A2mlTypeSpec;
6use crate::tokenizer::{A2lToken, A2lTokenType, TokenResult};
7use crate::{A2lError, Filename, ParseableA2lObject};
8
9const MAX_IDENT: usize = 1024;
10
11struct TokenIter<'a> {
12    tokens: &'a [A2lToken],
13    pos: usize,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub enum A2lVersion {
18    V1_5_0,
19    V1_5_1,
20    V1_6_0,
21    V1_6_1,
22    V1_7_0,
23    V1_7_1,
24}
25
26pub struct ParserState<'a> {
27    token_cursor: TokenIter<'a>,
28    filedata: &'a [String],
29    pub(crate) filenames: &'a [Filename],
30    pub(crate) last_token_position: u32,
31    sequential_id: u32,
32    pub(crate) log_msgs: &'a mut Vec<A2lError>,
33    strict: bool,
34    file_ver: A2lVersion,
35    pub(crate) a2mlspec: Vec<A2mlTypeSpec>,
36}
37
38/// describes the current parser context, giving the name of the current element and its file and line number
39#[derive(Debug, Clone)]
40pub struct ParseContext {
41    pub element: String,
42    pub fileid: usize,
43    pub line: u32,
44}
45#[derive(Debug, Error)]
46#[non_exhaustive]
47pub enum ParserError {
48    #[error(
49        "{filename}:{error_line}: expected token of type {expected_ttype:?}, got {actual_ttype:?} (\"{actual_text}\") inside block {element} starting on line {block_line}"
50    )]
51    UnexpectedTokenType {
52        filename: String,
53        error_line: u32,
54        block_line: u32,
55        element: String,
56        actual_ttype: A2lTokenType,
57        actual_text: String,
58        expected_ttype: A2lTokenType,
59    },
60
61    #[error("{filename}:{error_line}: string \"{numstr}\" could not be interpreted as a number")]
62    MalformedNumber {
63        filename: String,
64        error_line: u32,
65        numstr: String,
66    },
67
68    #[error(
69        "{filename}:{error_line}: expected an enum value, but \"{enumtxt}\" is not part of the enum (located inside block {block} starting on line {block_line})"
70    )]
71    InvalidEnumValue {
72        filename: String,
73        error_line: u32,
74        enumtxt: String,
75        block: String,
76        block_line: u32,
77    },
78
79    #[error(
80        "{filename}:{error_line}: optional element {tag} occurs too often within block {block} starting on line {block_line}"
81    )]
82    InvalidMultiplicityTooMany {
83        filename: String,
84        error_line: u32,
85        tag: String,
86        block: String,
87        block_line: u32,
88    },
89
90    #[error(
91        "{filename}:{error_line}: element {tag} is missing in block {block} starting on line {block_line}"
92    )]
93    InvalidMultiplicityNotPresent {
94        filename: String,
95        error_line: u32,
96        tag: String,
97        block: String,
98        block_line: u32,
99    },
100
101    #[error(
102        "{filename}:{error_line}: element {tag} in block {block} starting on line {block_line} must be enclosed in /begin and /end"
103    )]
104    IncorrectBlockError {
105        filename: String,
106        error_line: u32,
107        tag: String,
108        block: String,
109        block_line: u32,
110    },
111
112    #[error(
113        "{filename}:{error_line}: element {tag} in block {block} starting on line {block_line} may not be enclosed in /begin and /end"
114    )]
115    IncorrectKeywordError {
116        filename: String,
117        error_line: u32,
118        tag: String,
119        block: String,
120        block_line: u32,
121    },
122
123    #[error(
124        "{filename}:{error_line}: Wrong end tag {tag} found at the end of block {block} starting on line {block_line}"
125    )]
126    IncorrectEndTag {
127        filename: String,
128        error_line: u32,
129        tag: String,
130        block: String,
131        block_line: u32,
132    },
133
134    #[error(
135        "{filename}:{error_line}: Unknown sub-block {tag} found inside block {block} starting on line {block_line}"
136    )]
137    UnknownSubBlock {
138        filename: String,
139        error_line: u32,
140        tag: String,
141        block: String,
142        block_line: u32,
143    },
144
145    #[error(
146        "{filename}:{error_line}: encountered end of file while not done parsing block {block} starting on line {block_line}"
147    )]
148    UnexpectedEOF {
149        filename: String,
150        error_line: u32,
151        block: String,
152        block_line: u32,
153    },
154
155    #[error(
156        "{filename}:{error_line}: String \"{text}\" in block {block} is {length} bytes long, but the maximum allowed length is {max_length}"
157    )]
158    StringTooLong {
159        filename: String,
160        error_line: u32,
161        block: String,
162        text: String,
163        length: usize,
164        max_length: usize,
165    },
166
167    #[error(
168        "{filename}:{error_line}: Sub-block \"{tag}\" in block {block} is deprecated after version {limit_ver:.2}, but the file declares version {file_ver:.2}"
169    )]
170    BlockRefDeprecated {
171        filename: String,
172        error_line: u32,
173        block: String,
174        tag: String,
175        limit_ver: A2lVersion,
176        file_ver: A2lVersion,
177    },
178
179    #[error(
180        "{filename}:{error_line}: Sub-block \"{tag}\" in block {block} is available from version {limit_ver:.2}, but the file declares version {file_ver:.2}"
181    )]
182    BlockRefTooNew {
183        filename: String,
184        error_line: u32,
185        block: String,
186        tag: String,
187        limit_ver: A2lVersion,
188        file_ver: A2lVersion,
189    },
190
191    #[error(
192        "{filename}:{error_line}: Enum item \"{tag}\" in block {block} is deprecated after version {limit_ver:.2}, but the file declares version {file_ver:.2}"
193    )]
194    EnumRefDeprecated {
195        filename: String,
196        error_line: u32,
197        block: String,
198        tag: String,
199        limit_ver: A2lVersion,
200        file_ver: A2lVersion,
201    },
202
203    #[error(
204        "{filename}:{error_line}: Enum item \"{tag}\" in block {block} is available from version {limit_ver:.2}, but the file declares version {file_ver:.2}"
205    )]
206    EnumRefTooNew {
207        filename: String,
208        error_line: u32,
209        block: String,
210        tag: String,
211        limit_ver: A2lVersion,
212        file_ver: A2lVersion,
213    },
214
215    #[error("{filename}:{error_line}: /begin in block {block} is not followed by a valid tag")]
216    InvalidBegin {
217        filename: String,
218        error_line: u32,
219        block: String,
220    },
221
222    #[error(
223        "{filename}:{error_line}: The string '{ident}' in block {block} is not a valid identifier"
224    )]
225    InvalidIdentifier {
226        filename: String,
227        error_line: u32,
228        ident: String,
229        block: String,
230    },
231
232    #[error("{filename}:{error_line}: A2ML parser reports {errmsg}")]
233    A2mlError {
234        filename: String,
235        error_line: u32,
236        errmsg: String,
237    },
238
239    /// `AdditionalTokensError` parsing finished without consuming all data in the file
240    #[error(
241        "{filename}:{error_line}: unexpected additional data \"{text}...\" after parsed a2l file content"
242    )]
243    AdditionalTokensError {
244        filename: String,
245        error_line: u32,
246        text: String,
247    },
248
249    /// `MissingVerionInfo`: no version information in the file
250    #[error("File is not recognized as an a2l file. Mandatory version information is missing.")]
251    MissingVersionInfo,
252
253    /// Theversion number in the file dos not correspond to a known A2L specification
254    #[error("File has invalid version {major} {minor}")]
255    InvalidVersion { major: u16, minor: u16 },
256}
257
258pub(crate) enum BlockContent<'a> {
259    Block(&'a A2lToken, bool, u32),
260    Comment(&'a A2lToken, u32),
261    None,
262}
263
264// it pretends to be an Iter, but it really isn't
265impl<'a> TokenIter<'a> {
266    fn next(&mut self) -> Option<&'a A2lToken> {
267        if self.pos < self.tokens.len() {
268            let item = &self.tokens[self.pos];
269            self.pos += 1;
270            Some(item)
271        } else {
272            None
273        }
274    }
275
276    fn peek(&mut self) -> Option<&'a A2lToken> {
277        if self.pos < self.tokens.len() {
278            let item = &self.tokens[self.pos];
279            Some(item)
280        } else {
281            None
282        }
283    }
284
285    fn back(&mut self) {
286        self.pos -= 1;
287    }
288}
289
290impl<'a> ParserState<'a> {
291    pub(crate) fn new<'b>(
292        tokenresult: &'b TokenResult,
293        log_msgs: &'b mut Vec<A2lError>,
294        strict: bool,
295    ) -> ParserState<'b> {
296        Self::new_internal(
297            &tokenresult.tokens,
298            &tokenresult.filedata,
299            &tokenresult.filenames,
300            log_msgs,
301            strict,
302        )
303    }
304
305    pub(crate) fn new_internal<'b>(
306        tokens: &'b [A2lToken],
307        filedata: &'b [String],
308        filenames: &'b [Filename],
309        log_msgs: &'b mut Vec<A2lError>,
310        strict: bool,
311    ) -> ParserState<'b> {
312        ParserState {
313            token_cursor: TokenIter { tokens, pos: 0 },
314            filedata,
315            filenames,
316            last_token_position: 0,
317            sequential_id: 0,
318            log_msgs,
319            strict,
320            file_ver: A2lVersion::V1_7_1,
321            a2mlspec: Vec::new(),
322        }
323    }
324
325    pub(crate) fn parse_file(&mut self) -> Result<crate::A2lFile, ParserError> {
326        let firstline = self.token_cursor.tokens.first().map_or(1, |tok| tok.line);
327        // create a context for the parser
328        let context = ParseContext {
329            element: "A2L_FILE".to_string(),
330            fileid: 0,
331            line: firstline,
332        };
333
334        // try to get the file version. Starting with 1.60, the ASAP2_VERSION element is mandatory. For
335        // compatibility with old files, a missing version is only an error if strict parsing is requested
336        self.file_ver = self.parse_version(&context)?;
337
338        // parse the file
339        let a2l_file = crate::A2lFile::parse(self, &context, 0)?;
340
341        // make sure this is the end of the input, i.e. no additional data after the parsed data
342        if let Some(token) = self.peek_token() {
343            self.error_or_log(ParserError::AdditionalTokensError {
344                filename: self.filenames[token.fileid].to_string(),
345                error_line: self.last_token_position,
346                text: self.get_token_text(token).to_owned(),
347            })?;
348        }
349
350        Ok(a2l_file)
351    }
352
353    fn parse_version(&mut self, context: &ParseContext) -> Result<crate::A2lVersion, ParserError> {
354        if let Some(token) = self.peek_token() {
355            let ident = self.get_identifier(context);
356            let ver_context = ParseContext::from_token("", token);
357            if let Ok("ASAP2_VERSION") = ident.as_deref() {
358                let version_result = crate::Asap2Version::parse(self, &ver_context, 0)
359                    .map_err(|_| ParserError::MissingVersionInfo)
360                    .and_then(|ver| A2lVersion::new(ver.version_no, ver.upgrade_no));
361
362                // reset the input, the consumed tokens will be needed again by A2lFile::parse
363                self.set_tokenpos(0);
364
365                return match version_result {
366                    Ok(ver) => Ok(ver),
367                    Err(err) => {
368                        // couldn't parse the version, but the file appears to have one
369                        self.error_or_log(err)?;
370                        Ok(A2lVersion::V1_7_1)
371                    }
372                };
373            }
374        }
375        // for compatibility with 1.50 and earlier, also make it possible to catch the error and continue
376        self.set_tokenpos(0);
377        self.error_or_log(ParserError::MissingVersionInfo)?;
378        Ok(A2lVersion::V1_5_1)
379    }
380    // get_token
381    // get one token from the list of tokens and unwrap it
382    pub(crate) fn get_token(
383        &mut self,
384        context: &ParseContext,
385    ) -> Result<&'a A2lToken, ParserError> {
386        if let Some(token) = self.token_cursor.next() {
387            self.last_token_position = token.line;
388            Ok(token)
389        } else {
390            Err(ParserError::unexpected_eof(self, context))
391        }
392    }
393
394    pub(crate) fn undo_get_token(&mut self) {
395        self.token_cursor.back();
396    }
397
398    pub(crate) fn peek_token(&mut self) -> Option<&'a A2lToken> {
399        self.token_cursor.peek()
400    }
401
402    pub(crate) fn log_warning(&mut self, parse_error: ParserError) {
403        self.log_msgs.push(A2lError::ParserError {
404            parser_error: parse_error,
405        });
406    }
407
408    pub(crate) fn error_or_log(&mut self, err: ParserError) -> Result<(), ParserError> {
409        if self.strict {
410            Err(err)
411        } else {
412            self.log_warning(err);
413            Ok(())
414        }
415    }
416
417    pub(crate) fn get_tokenpos(&self) -> usize {
418        self.token_cursor.pos
419    }
420
421    pub(crate) fn set_tokenpos(&mut self, newpos: usize) {
422        self.token_cursor.pos = newpos;
423    }
424
425    pub(crate) fn get_token_text(&self, token: &'a A2lToken) -> &'a str {
426        let data = &self.filedata[token.fileid];
427        &data[token.startpos..token.endpos]
428    }
429
430    /// get the line offset of the token that was just consumed
431    /// this means that seff.token_cursor.pos is already incremented
432    /// - usually we're calculating token[pos-1] - token[pos-2]
433    /// - for token[pos==1] it returns the line number of the first token - 1
434    /// - the function shouldn't be called while pos == 0, but this case would behave like pos==1
435    pub(crate) fn get_line_offset(&self) -> u32 {
436        if self.token_cursor.pos > 1 && self.token_cursor.pos < self.token_cursor.tokens.len() {
437            let prev_line = self.token_cursor.tokens[self.token_cursor.pos - 2].line;
438            let prev_fileid = self.token_cursor.tokens[self.token_cursor.pos - 2].fileid;
439            let cur_line = self.token_cursor.tokens[self.token_cursor.pos - 1].line;
440            let cur_fileid = self.token_cursor.tokens[self.token_cursor.pos - 1].fileid;
441
442            // subtracting line numbers is only sane within a file
443            if prev_fileid == cur_fileid {
444                cur_line - prev_line
445            } else {
446                // if the tokens come from different files then there is no line offset and the
447                // value 2 is used for formatting: 2 newlines leave one line empty
448                2
449            }
450        } else {
451            self.token_cursor.tokens[0].line - 1
452        }
453    }
454
455    pub(crate) fn get_next_id(&mut self) -> u32 {
456        self.sequential_id += 1;
457        self.sequential_id
458    }
459
460    pub(crate) fn get_incfilename(&self, fileid: usize) -> Option<String> {
461        if fileid == 0 || fileid >= self.filenames.len() {
462            None
463        } else {
464            Some(self.filenames[fileid].to_string())
465        }
466    }
467
468    pub(crate) fn set_file_version(&mut self, version: A2lVersion) {
469        self.file_ver = version;
470    }
471
472    pub(crate) fn check_block_version_lower(
473        &mut self,
474        context: &ParseContext,
475        tag: &str,
476        min_ver: A2lVersion,
477    ) -> Result<(), ParserError> {
478        if self.file_ver < min_ver {
479            self.error_or_log(ParserError::BlockRefTooNew {
480                filename: self.filenames[context.fileid].to_string(),
481                error_line: self.last_token_position,
482                block: context.element.clone(),
483                tag: tag.to_string(),
484                limit_ver: min_ver,
485                file_ver: self.file_ver,
486            })?;
487        }
488        Ok(())
489    }
490
491    pub(crate) fn check_block_version_upper(
492        &mut self,
493        context: &ParseContext,
494        tag: &str,
495        max_ver: A2lVersion,
496    ) {
497        if self.file_ver > max_ver {
498            self.log_warning(ParserError::BlockRefDeprecated {
499                filename: self.filenames[context.fileid].to_string(),
500                error_line: self.last_token_position,
501                block: context.element.clone(),
502                tag: tag.to_string(),
503                limit_ver: max_ver,
504                file_ver: self.file_ver,
505            });
506        }
507    }
508
509    pub(crate) fn check_enumitem_version_lower(
510        &mut self,
511        context: &ParseContext,
512        tag: &str,
513        min_ver: A2lVersion,
514    ) -> Result<(), ParserError> {
515        if self.file_ver < min_ver {
516            self.error_or_log(ParserError::EnumRefTooNew {
517                filename: self.filenames[context.fileid].to_string(),
518                error_line: self.last_token_position,
519                block: context.element.clone(),
520                tag: tag.to_string(),
521                limit_ver: min_ver,
522                file_ver: self.file_ver,
523            })?;
524        }
525        Ok(())
526    }
527
528    pub(crate) fn check_enumitem_version_upper(
529        &mut self,
530        context: &ParseContext,
531        tag: &str,
532        max_ver: A2lVersion,
533    ) {
534        if self.file_ver > max_ver {
535            self.log_warning(ParserError::EnumRefDeprecated {
536                filename: self.filenames[context.fileid].to_string(),
537                error_line: self.last_token_position,
538                block: context.element.clone(),
539                tag: tag.to_string(),
540                limit_ver: max_ver,
541                file_ver: self.file_ver,
542            });
543        }
544    }
545
546    // expect_token get a token which has to be of a particular type (hence: expect)
547    // getting a token of any other type is a ParserError
548    pub(crate) fn expect_token(
549        &mut self,
550        context: &ParseContext,
551        token_type: A2lTokenType,
552    ) -> Result<&'a A2lToken, ParserError> {
553        let mut token = self.get_token(context)?;
554        // we never "expect" a comment token, so we skip it
555        while token.ttype == A2lTokenType::Comment {
556            token = self.get_token(context)?;
557        }
558
559        if token.ttype != token_type {
560            return Err(ParserError::unexpected_token_type(
561                self, context, token, token_type,
562            ));
563        }
564
565        Ok(token)
566    }
567
568    // get_string()
569    // Get the content of a String token as a string
570    pub(crate) fn get_string(&mut self, context: &ParseContext) -> Result<String, ParserError> {
571        let text = if let Some(
572            token @ A2lToken {
573                ttype: A2lTokenType::Identifier,
574                ..
575            },
576        ) = self.peek_token()
577        {
578            // an identifier can be used in place of a string, if the parser is not strict
579            let text = self.get_identifier(context)?;
580            self.error_or_log(ParserError::unexpected_token_type(
581                self,
582                context,
583                token,
584                A2lTokenType::String,
585            ))?;
586            text
587        } else {
588            let token = self.expect_token(context, A2lTokenType::String)?;
589            let mut text = self.get_token_text(token);
590
591            if text.starts_with('\"') {
592                text = &text[1..text.len() - 1];
593            }
594
595            unescape_string(text)
596        };
597
598        Ok(text)
599    }
600
601    // get_string_maxlen()
602    // Get the content of a String token as a string. Trigger an error if the string is longer than maxlen
603    pub(crate) fn get_string_maxlen(
604        &mut self,
605        context: &ParseContext,
606        maxlen: usize,
607    ) -> Result<String, ParserError> {
608        let text = self.get_string(context)?;
609        if text.len() > maxlen {
610            self.error_or_log(ParserError::StringTooLong {
611                filename: self.filenames[context.fileid].to_string(),
612                error_line: self.last_token_position,
613                block: context.element.clone(),
614                text: text.clone(),
615                length: text.len(),
616                max_length: maxlen,
617            })?;
618        }
619        Ok(text)
620    }
621
622    // get_identifier()
623    // Get the content of an Identifier token as a string
624    pub(crate) fn get_identifier(&mut self, context: &ParseContext) -> Result<String, ParserError> {
625        let token = self.expect_token(context, A2lTokenType::Identifier)?;
626        let text = self.get_token_text(token);
627        if text.as_bytes()[0].is_ascii_digit() || text.len() > MAX_IDENT {
628            self.error_or_log(ParserError::InvalidIdentifier {
629                filename: self.filenames[context.fileid].to_string(),
630                error_line: self.last_token_position,
631                block: context.element.clone(),
632                ident: text.to_owned(),
633            })?;
634        }
635        Ok(String::from(text))
636    }
637
638    // get_float()
639    // Get the content of a Number token as a float
640    // Since the Number token stores text internally, the text must be converted first
641    pub(crate) fn get_float(&mut self, context: &ParseContext) -> Result<f32, ParserError> {
642        let token = self.expect_token(context, A2lTokenType::Number)?;
643        let text = self.get_token_text(token);
644        // some vendor tools are defining the characteristic UpperLimit and LowerLimit
645        // (float values from specifications) using 0xNNN for characteristics that
646        // are actually integers.
647        if text.starts_with("0x") || text.starts_with("0X") {
648            match u64::from_str_radix(&text[2..], 16) {
649                Ok(num) => Ok(num as f32),
650                Err(_) => Err(ParserError::malformed_number(self, context, text)),
651            }
652        } else {
653            match text.parse::<f32>() {
654                Ok(num) => Ok(num),
655                Err(_) => Err(ParserError::malformed_number(self, context, text)),
656            }
657        }
658    }
659
660    // get_double()
661    // Get the content of a Number token as a double(f64)
662    // Since the Number token stores text internally, the text must be converted first
663    pub(crate) fn get_double(&mut self, context: &ParseContext) -> Result<f64, ParserError> {
664        let token = self.expect_token(context, A2lTokenType::Number)?;
665        let text = self.get_token_text(token);
666        // some vendor tools are defining the characteristic UpperLimit and LowerLimit
667        // (float values from specifications) using 0xNNN for characteristics that
668        // are actually integers.
669        if text.starts_with("0x") || text.starts_with("0X") {
670            match u64::from_str_radix(&text[2..], 16) {
671                Ok(num) => Ok(num as f64),
672                Err(_) => Err(ParserError::malformed_number(self, context, text)),
673            }
674        } else {
675            match text.parse::<f64>() {
676                Ok(num) => Ok(num),
677                Err(_) => Err(ParserError::malformed_number(self, context, text)),
678            }
679        }
680    }
681
682    pub(crate) fn get_integer<T>(
683        &mut self,
684        context: &ParseContext,
685    ) -> Result<(T, bool), ParserError>
686    where
687        T: Num + std::str::FromStr + Copy + 'static,
688        u64: AsPrimitive<T>,
689    {
690        let token = self.expect_token(context, A2lTokenType::Number)?;
691        let text = self.get_token_text(token);
692        if text.len() > 2 && (text.starts_with("0x") || text.starts_with("0X")) {
693            match u64::from_str_radix(&text[2..], 16) {
694                Ok(num_u64) => Ok((num_u64.as_(), true)),
695                Err(_) => Err(ParserError::malformed_number(self, context, text)),
696            }
697        } else {
698            match text.parse() {
699                Ok(num) => Ok((num, false)),
700                Err(_) => Err(ParserError::malformed_number(self, context, text)),
701            }
702        }
703    }
704
705    // get_next_tag()
706    // get the tag of the next item of a taggedstruct or taggedunion
707    // alternatively, get the next comment
708    //
709    // Handling tags and comments in the same function makes sense, because the current parser
710    // implementation can only handle comments where blocks are permitted. If the parser is ever
711    // extended to handle comments in other locations, then this function will need to be split.
712    pub(crate) fn get_next_tag_or_comment(
713        &mut self,
714        context: &ParseContext,
715    ) -> Result<BlockContent<'a>, ParserError> {
716        let mut is_block = false;
717        let tokenpos = self.get_tokenpos();
718
719        if let Some(
720            tokenval @ A2lToken {
721                ttype: A2lTokenType::Comment,
722                ..
723            },
724        ) = self.token_cursor.peek()
725        {
726            self.token_cursor.next(); // consume the peeked token
727            let start_offset = self.get_line_offset();
728            Ok(BlockContent::Comment(tokenval, start_offset))
729        } else {
730            // if the next token is /begin, then set is_block and skip the token
731            let (token_result, start_offset) = if let Some(A2lToken {
732                ttype: A2lTokenType::Begin,
733                ..
734            }) = self.token_cursor.peek()
735            {
736                is_block = true;
737                self.get_token(context)?;
738                // for blocks, we went to set start_offset to the offset of the /begin token,
739                // and then get the tag from the next token
740                let start_offset = self.get_line_offset();
741                (
742                    self.expect_token(context, A2lTokenType::Identifier),
743                    start_offset,
744                )
745            } else {
746                // if this is not a block, then just get the tag and the offset of the tag
747                (
748                    self.expect_token(context, A2lTokenType::Identifier),
749                    self.get_line_offset(),
750                )
751            };
752
753            // get the tag or return None if the token is not an Identifier
754            match token_result {
755                Ok(token) => Ok(BlockContent::Block(token, is_block, start_offset)),
756                Err(error) => {
757                    self.set_tokenpos(tokenpos);
758                    if is_block {
759                        Err(error)
760                    } else {
761                        // no tag? no problem!
762                        Ok(BlockContent::None)
763                    }
764                }
765            }
766        }
767    }
768
769    // handle_unknown_taggedstruct_tag
770    // perform error recovery if an unknown tag is found inside a taggedstruct and strict parsing is off
771    pub(crate) fn handle_unknown_taggedstruct_tag(
772        &mut self,
773        context: &ParseContext,
774        item_tag: &str,
775        item_is_block: bool,
776        stoplist: &[&str],
777    ) -> Result<(), ParserError> {
778        self.error_or_log(ParserError::unknown_sub_block(self, context, item_tag))?;
779        // make sure there actually is a next token by doing get_token() + undo_get_token() rather than peek()
780        let _ = self.get_token(context)?;
781        self.undo_get_token();
782        let startpos = self.get_tokenpos();
783        let text = self.get_token_text(&self.token_cursor.tokens[startpos]);
784        let errcontext = ParseContext::from_token(text, &self.token_cursor.tokens[startpos]);
785
786        let mut balance = 0;
787        if item_is_block {
788            balance = 1;
789        }
790
791        loop {
792            let token = self.get_token(context)?;
793            let text = self.get_token_text(token);
794            match token.ttype {
795                A2lTokenType::Begin => balance += 1,
796                A2lTokenType::End => {
797                    balance -= 1;
798                    if balance == -1 {
799                        self.token_cursor.back();
800                        break;
801                    }
802                }
803                A2lTokenType::Identifier => {
804                    if item_is_block {
805                        // the current unknown item started with /begin ITEM_TAG, so it must end with /end ITEM_TAG.
806                        // the stoplist is not relevant
807                        if balance == 0 {
808                            if text == item_tag {
809                                break;
810                            } else {
811                                return Err(ParserError::incorrect_end_tag(
812                                    self,
813                                    &errcontext,
814                                    text,
815                                ));
816                            }
817                        }
818                    } else {
819                        // this unknown item did not begin with /begin, so the end is reached when either:
820                        // - the end tag of the parent block is found (balance == -1)
821                        // - a tag belonging to the parent block (on the stoplist) is found (balance == 0)
822                        // - the sequence /begin TAG for a tag on the stoplist is encountered (balance == 1)
823                        if balance == 0 || balance == 1 {
824                            let found = stoplist.iter().find(|entry| **entry == text);
825                            if found.is_some() {
826                                // found a tag belonging to a different TaggedItem of the parent struct. Put the token back and let the parent handle it
827                                self.token_cursor.back();
828                                if balance == 1 {
829                                    self.token_cursor.back();
830                                }
831                                break;
832                            }
833                        }
834                    }
835                }
836                _ => {
837                    // once balance == 0 is reached for a block, the next tag should be an Identifier
838                    if item_is_block && balance == 0 {
839                        return Err(ParserError::incorrect_end_tag(self, &errcontext, text));
840                    }
841                    // else: ignore the token
842                }
843            }
844        }
845
846        Ok(())
847    }
848
849    pub(crate) fn handle_multiplicity_error(
850        &mut self,
851        context: &ParseContext,
852        tag: &str,
853        is_error: bool,
854    ) -> Result<(), ParserError> {
855        if is_error {
856            self.error_or_log(ParserError::invalid_multiplicity_too_many(
857                self, context, tag,
858            ))?;
859        }
860        Ok(())
861    }
862
863    pub(crate) fn require_block(
864        &self,
865        tag: &str,
866        is_block: bool,
867        context: &ParseContext,
868    ) -> Result<(), ParserError> {
869        if !is_block {
870            Err(ParserError::IncorrectBlockError {
871                filename: self.filenames[context.fileid].to_string(),
872                error_line: self.last_token_position,
873                tag: tag.to_string(),
874                block: context.element.clone(),
875                block_line: context.line,
876            })
877        } else {
878            Ok(())
879        }
880    }
881
882    pub(crate) fn require_keyword(
883        &self,
884        tag: &str,
885        is_block: bool,
886        context: &ParseContext,
887    ) -> Result<(), ParserError> {
888        if is_block {
889            Err(ParserError::IncorrectKeywordError {
890                filename: self.filenames[context.fileid].to_string(),
891                error_line: self.last_token_position,
892                tag: tag.to_string(),
893                block: context.element.clone(),
894                block_line: context.line,
895            })
896        } else {
897            Ok(())
898        }
899    }
900}
901
902impl ParseContext {
903    pub(crate) fn from_token(text: &str, token: &A2lToken) -> ParseContext {
904        ParseContext {
905            element: text.to_string(),
906            fileid: token.fileid,
907            line: token.line,
908        }
909    }
910}
911
912impl ParserError {
913    pub(crate) fn unexpected_token_type(
914        parser: &ParserState,
915        context: &ParseContext,
916        token: &A2lToken,
917        expected_ttype: A2lTokenType,
918    ) -> Self {
919        Self::UnexpectedTokenType {
920            filename: parser.filenames[context.fileid].to_string(),
921            error_line: parser.last_token_position,
922            block_line: context.line,
923            element: context.element.clone(),
924            actual_ttype: token.ttype.clone(),
925            actual_text: parser.get_token_text(token).to_owned(),
926            expected_ttype,
927        }
928    }
929
930    pub(crate) fn malformed_number(
931        parser: &ParserState,
932        context: &ParseContext,
933        numstr: &str,
934    ) -> Self {
935        Self::MalformedNumber {
936            filename: parser.filenames[context.fileid].to_string(),
937            error_line: parser.last_token_position,
938            numstr: numstr.to_owned(),
939        }
940    }
941
942    pub(crate) fn invalid_enum_value(
943        parser: &ParserState,
944        context: &ParseContext,
945        enumitem: &str,
946    ) -> Self {
947        Self::InvalidEnumValue {
948            filename: parser.filenames[context.fileid].to_string(),
949            error_line: parser.last_token_position,
950            enumtxt: enumitem.to_owned(),
951            block: context.element.clone(),
952            block_line: context.line,
953        }
954    }
955
956    pub(crate) fn invalid_multiplicity_too_many(
957        parser: &ParserState,
958        context: &ParseContext,
959        tag: &str,
960    ) -> Self {
961        Self::InvalidMultiplicityTooMany {
962            filename: parser.filenames[context.fileid].to_string(),
963            error_line: parser.last_token_position,
964            tag: tag.to_string(),
965            block: context.element.clone(),
966            block_line: context.line,
967        }
968    }
969
970    pub(crate) fn incorrect_end_tag(
971        parser: &ParserState,
972        context: &ParseContext,
973        tag: &str,
974    ) -> Self {
975        Self::IncorrectEndTag {
976            filename: parser.filenames[context.fileid].to_string(),
977            error_line: parser.last_token_position,
978            tag: tag.to_owned(),
979            block: context.element.clone(),
980            block_line: context.line,
981        }
982    }
983
984    pub(crate) fn unknown_sub_block(
985        parser: &ParserState,
986        context: &ParseContext,
987        tag: &str,
988    ) -> Self {
989        Self::UnknownSubBlock {
990            filename: parser.filenames[context.fileid].to_string(),
991            error_line: parser.last_token_position,
992            tag: tag.to_owned(),
993            block: context.element.clone(),
994            block_line: context.line,
995        }
996    }
997
998    pub(crate) fn unexpected_eof(parser: &ParserState, context: &ParseContext) -> Self {
999        Self::UnexpectedEOF {
1000            filename: parser.filenames[context.fileid].to_string(),
1001            error_line: parser.last_token_position,
1002            block: context.element.clone(),
1003            block_line: context.line,
1004        }
1005    }
1006}
1007
1008fn unescape_string(text: &str) -> String {
1009    /* first check if any unescaping is needed at all */
1010    if text.chars().any(|c| c == '\\' || c == '"') {
1011        let input_chars: Vec<char> = text.chars().collect();
1012        let mut output_chars = Vec::<char>::new();
1013
1014        let mut idx = 1;
1015        while idx < input_chars.len() {
1016            if (input_chars[idx - 1] == '\\' || input_chars[idx - 1] == '"')
1017                && input_chars[idx] == '"'
1018            {
1019                output_chars.push('"');
1020                idx += 1;
1021            } else if input_chars[idx - 1] == '\\' && input_chars[idx] == '\'' {
1022                output_chars.push('\'');
1023                idx += 1;
1024            } else if input_chars[idx - 1] == '\\' && input_chars[idx] == '\\' {
1025                output_chars.push('\\');
1026                idx += 1;
1027            } else if input_chars[idx - 1] == '\\' && input_chars[idx] == 'n' {
1028                output_chars.push('\n');
1029                idx += 1;
1030            } else if input_chars[idx - 1] == '\\' && input_chars[idx] == 'r' {
1031                output_chars.push('\r');
1032                idx += 1;
1033            } else if input_chars[idx - 1] == '\\' && input_chars[idx] == 't' {
1034                output_chars.push('\t');
1035                idx += 1;
1036            } else {
1037                output_chars.push(input_chars[idx - 1]);
1038            }
1039
1040            idx += 1;
1041        }
1042        if idx == input_chars.len() {
1043            output_chars.push(input_chars[idx - 1]);
1044        }
1045
1046        output_chars.iter().collect()
1047    } else {
1048        text.to_owned()
1049    }
1050}
1051
1052impl A2lVersion {
1053    pub fn new(major: u16, minor: u16) -> Result<Self, ParserError> {
1054        match (major, minor) {
1055            (1, 50) => Ok(A2lVersion::V1_5_0),
1056            (1, 51) => Ok(A2lVersion::V1_5_1),
1057            (1, 60) => Ok(A2lVersion::V1_6_0),
1058            (1, 61) => Ok(A2lVersion::V1_6_1),
1059            (1, 70) => Ok(A2lVersion::V1_7_0),
1060            (1, 71) => Ok(A2lVersion::V1_7_1),
1061            _ => Err(ParserError::InvalidVersion { major, minor }),
1062        }
1063    }
1064}
1065
1066impl Display for A2lVersion {
1067    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068        match self {
1069            A2lVersion::V1_5_0 => f.write_str("1.5.0"),
1070            A2lVersion::V1_5_1 => f.write_str("1.5.1"),
1071            A2lVersion::V1_6_0 => f.write_str("1.6.0"),
1072            A2lVersion::V1_6_1 => f.write_str("1.6.1"),
1073            A2lVersion::V1_7_0 => f.write_str("1.7.0"),
1074            A2lVersion::V1_7_1 => f.write_str("1.7.1"),
1075        }
1076    }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use super::*;
1082    use crate::{Filename, load_from_string, tokenizer};
1083
1084    #[test]
1085    fn parsing_numbers_test() {
1086        let input_text = r##"0 0x1 1.0e+2 1000 0 0.1 0x11 1.0e+2 0X1f 0X2F 2F F"##;
1087        let tokenresult = tokenizer::tokenize(&Filename::from("test_input"), 0, input_text);
1088        assert!(tokenresult.is_ok());
1089
1090        let tokenresult = tokenresult.unwrap();
1091        let mut log_msgs = Vec::<A2lError>::new();
1092        let mut parser = ParserState::new(&tokenresult, &mut log_msgs, true);
1093        let context = ParseContext {
1094            element: "TEST".to_string(),
1095            fileid: 0,
1096            line: 0,
1097        };
1098
1099        // uint8: 0
1100        let res = parser.get_integer::<u8>(&context);
1101        assert!(res.is_ok());
1102        let val = res.unwrap();
1103        assert_eq!(val, (0, false));
1104
1105        // uint8: 0x1
1106        let res = parser.get_integer::<u8>(&context);
1107        assert!(res.is_ok());
1108        let val = res.unwrap();
1109        assert_eq!(val, (1, true));
1110
1111        // uint8: 1.0e+2
1112        let res = parser.get_integer::<u8>(&context);
1113        assert!(res.is_err());
1114
1115        // uint8: 257
1116        let res = parser.get_integer::<u8>(&context);
1117        assert!(res.is_err());
1118
1119        // float: 0
1120        let res = parser.get_float(&context);
1121        assert!(res.is_ok());
1122        let val = res.unwrap();
1123        assert_eq!(val, 0f32);
1124
1125        // float: 0.1
1126        let res = parser.get_float(&context);
1127        assert!(res.is_ok());
1128        let val = res.unwrap();
1129        assert_eq!(val, 0.1f32);
1130
1131        // float: 0x11
1132        let res = parser.get_float(&context);
1133        assert!(res.is_ok());
1134        let val = res.unwrap();
1135        assert_eq!(val, 17f32);
1136
1137        // float: 1.0e+2
1138        let res = parser.get_float(&context);
1139        assert!(res.is_ok());
1140        let val = res.unwrap();
1141        assert_eq!(val, 100f32);
1142
1143        // float: 0X1f
1144        let res = parser.get_float(&context);
1145        assert!(res.is_ok());
1146        let val = res.unwrap();
1147        assert_eq!(val, 31f32);
1148
1149        // float: 0X2F
1150        let res = parser.get_float(&context);
1151        assert!(res.is_ok());
1152        let val = res.unwrap();
1153        assert_eq!(val, 47f32);
1154
1155        // float: 2F
1156        let res = parser.get_float(&context);
1157        assert!(res.is_err());
1158
1159        // float: F
1160        let res = parser.get_float(&context);
1161        assert!(res.is_err());
1162    }
1163
1164    #[test]
1165    fn test_unescape_string() {
1166        // no escape
1167        let result = unescape_string(" ");
1168        assert_eq!(result, " ");
1169        // "" -> "
1170        let result = unescape_string(r#""""#);
1171        assert_eq!(result, r#"""#);
1172        // \" -> "
1173        let result = unescape_string(r#"\""#);
1174        assert_eq!(result, r#"""#);
1175        // \' -> '
1176        let result = unescape_string(r#"\'"#);
1177        assert_eq!(result, r#"'"#);
1178        // \\ -> \
1179        let result = unescape_string(r#"\\"#);
1180        assert_eq!(result, r#"\"#);
1181        // \n -> (newline)
1182        let result = unescape_string(r#"\n"#);
1183        assert_eq!(result, "\n");
1184        // \r -> (carriage return)
1185        let result = unescape_string(r#"\r"#);
1186        assert_eq!(result, "\r");
1187        // \t -> (tab)
1188        let result = unescape_string(r#"\txx"#);
1189        assert_eq!(result, "\txx");
1190    }
1191
1192    #[test]
1193    fn parsing_identifiers_test() {
1194        let input_text = r##"ident 0ident 123"##;
1195        let tokenresult = tokenizer::tokenize(&Filename::from("test_input"), 0, input_text);
1196        assert!(tokenresult.is_ok());
1197
1198        let tokenresult = tokenresult.unwrap();
1199        let mut log_msgs = Vec::<A2lError>::new();
1200        let mut parser = ParserState::new(&tokenresult, &mut log_msgs, true);
1201        let context = ParseContext {
1202            element: "TEST".to_string(),
1203            fileid: 0,
1204            line: 0,
1205        };
1206
1207        // identifier: ident
1208        let res = parser.get_identifier(&context);
1209        assert!(res.is_ok());
1210        let val = res.unwrap();
1211        assert_eq!(val, "ident");
1212
1213        // bad identifier - identifiers may not start with a number
1214        let res = parser.get_identifier(&context);
1215        assert!(matches!(res, Err(ParserError::InvalidIdentifier { .. })));
1216
1217        // not an identifier
1218        let res = parser.get_identifier(&context);
1219        assert!(res.is_err());
1220    }
1221
1222    #[test]
1223    fn test_check_version() {
1224        let tokenresult = tokenizer::tokenize(&Filename::from("test_input"), 0, "").unwrap();
1225        let mut log_msgs = Vec::<A2lError>::new();
1226        let mut parser = ParserState::new(&tokenresult, &mut log_msgs, true);
1227        let context = ParseContext {
1228            element: "TEST".to_string(),
1229            fileid: 0,
1230            line: 0,
1231        };
1232        parser.file_ver = A2lVersion::V1_6_0;
1233
1234        // block version v1_5_0 <= file version V1_6_0
1235        let result = parser.check_block_version_lower(&context, "CHECK", A2lVersion::V1_5_0);
1236        assert!(result.is_ok());
1237
1238        // !block version v1_7_0 <= file version V1_6_0
1239        let result = parser.check_block_version_lower(&context, "CHECK", A2lVersion::V1_7_0);
1240        assert!(result.is_err());
1241
1242        // !block version v1_5_0 >= file version V1_6_0
1243        let num_warnings = parser.log_msgs.len();
1244        parser.check_block_version_upper(&context, "CHECK", A2lVersion::V1_5_0);
1245        assert_ne!(num_warnings, parser.log_msgs.len());
1246
1247        // block version v1_7_0 >= file version V1_6_0
1248        let num_warnings = parser.log_msgs.len();
1249        parser.check_block_version_upper(&context, "CHECK", A2lVersion::V1_7_0);
1250        assert_eq!(num_warnings, parser.log_msgs.len());
1251
1252        // enum item version V1_5_0 <= file version V1_6_0
1253        let result: Result<(), ParserError> =
1254            parser.check_enumitem_version_lower(&context, "CHECK", A2lVersion::V1_5_0);
1255        assert!(result.is_ok());
1256
1257        // !enum item version V1_7_0 <= file version V1_6_0
1258        let result = parser.check_enumitem_version_lower(&context, "CHECK", A2lVersion::V1_7_0);
1259        assert!(result.is_err());
1260
1261        // !enum item version V1_5_0 >= file version V1_6_0
1262        let num_warnings = parser.log_msgs.len();
1263        parser.check_enumitem_version_upper(&context, "CHECK", A2lVersion::V1_5_0);
1264        assert_ne!(num_warnings, parser.log_msgs.len());
1265
1266        // enum item version V1_7_0 >= file version V1_6_0
1267        let num_warnings = parser.log_msgs.len();
1268        parser.check_enumitem_version_upper(&context, "CHECK", A2lVersion::V1_7_0);
1269        assert_eq!(num_warnings, parser.log_msgs.len());
1270    }
1271
1272    #[test]
1273    fn a2lversion() {
1274        let v150 = A2lVersion::new(1, 50).unwrap();
1275        let v151 = A2lVersion::new(1, 51).unwrap();
1276        let v160 = A2lVersion::new(1, 60).unwrap();
1277        let v161 = A2lVersion::new(1, 61).unwrap();
1278        let v170 = A2lVersion::new(1, 70).unwrap();
1279        let v171 = A2lVersion::new(1, 71).unwrap();
1280
1281        assert!(v150 < v151);
1282        assert!(v151 < v160);
1283        assert!(v160 < v161);
1284        assert!(v161 < v170);
1285        assert!(v170 < v171);
1286
1287        let bad_version = A2lVersion::new(1, 80);
1288        assert!(bad_version.is_err());
1289
1290        let cpy = v171;
1291        assert_eq!(cpy, v171);
1292
1293        assert_eq!(format!("{v150}"), "1.5.0");
1294        assert_eq!(format!("{v151}"), "1.5.1");
1295        assert_eq!(format!("{v160}"), "1.6.0");
1296        assert_eq!(format!("{v161}"), "1.6.1");
1297        assert_eq!(format!("{v170}"), "1.7.0");
1298        assert_eq!(format!("{v171}"), "1.7.1");
1299    }
1300
1301    #[test]
1302    fn error_missing_version() {
1303        static DATA: &str = r#"/begin PROJECT p "" /end PROJECT"#;
1304        let a2l_file = load_from_string(DATA, None, true);
1305        assert!(a2l_file.is_err());
1306        assert!(matches!(
1307            a2l_file,
1308            Err(A2lError::ParserError {
1309                parser_error: ParserError::MissingVersionInfo
1310            })
1311        ));
1312    }
1313
1314    #[test]
1315    fn error_invalid_mult_not_present() {
1316        static DATA: &str = r#"ASAP2_VERSION 1 71"#;
1317        let a2l_file = load_from_string(DATA, None, true);
1318        assert!(matches!(
1319            a2l_file,
1320            Err(A2lError::ParserError {
1321                parser_error: ParserError::InvalidMultiplicityNotPresent { .. }
1322            })
1323        ));
1324    }
1325
1326    #[test]
1327    fn error_invalid_mult_too_many() {
1328        static DATA: &str = r#"ASAP2_VERSION 1 71 /begin PROJECT p "" /begin MODULE m "" /end MODULE /end PROJECT /begin PROJECT p2 "" /begin MODULE m "" /end MODULE /end PROJECT"#;
1329        let a2l_file = load_from_string(DATA, None, true);
1330        assert!(matches!(
1331            a2l_file,
1332            Err(A2lError::ParserError {
1333                parser_error: ParserError::InvalidMultiplicityTooMany { .. }
1334            })
1335        ));
1336    }
1337
1338    #[test]
1339    fn error_unknown_subblock() {
1340        static DATA: &str = r#"ASAP2_VERSION 1 71 /begin PROJECT p "" /begin MODULE m "" ABCDEF /end MODULE /end PROJECT"#;
1341        let a2l_file = load_from_string(DATA, None, true);
1342        assert!(matches!(
1343            a2l_file,
1344            Err(A2lError::ParserError {
1345                parser_error: ParserError::UnknownSubBlock { .. }
1346            })
1347        ));
1348    }
1349
1350    #[test]
1351    fn error_incorrect_end_tag() {
1352        static DATA: &str =
1353            r#"ASAP2_VERSION 1 71 /begin PROJECT p "" /begin MODULE m "" /end MMMMM /end PROJECT"#;
1354        let a2l_file = load_from_string(DATA, None, true);
1355        assert!(matches!(
1356            a2l_file,
1357            Err(A2lError::ParserError {
1358                parser_error: ParserError::IncorrectEndTag { .. }
1359            })
1360        ));
1361    }
1362
1363    #[test]
1364    fn error_unexpected_eof() {
1365        static DATA: &str = r#"ASAP2_VERSION 1 71 /begin PROJECT"#;
1366        let a2l_file = load_from_string(DATA, None, true);
1367        assert!(matches!(
1368            a2l_file,
1369            Err(A2lError::ParserError {
1370                parser_error: ParserError::UnexpectedEOF { .. }
1371            })
1372        ));
1373    }
1374
1375    #[test]
1376    fn error_invalid_identifier() {
1377        let data = format!(
1378            r#"ASAP2_VERSION 1 71 /begin PROJECT {} "" /begin MODULE m "" /end MODULE /end PROJECT"#,
1379            ['a'; 1025].iter().collect::<String>()
1380        );
1381        let a2l_file = load_from_string(&data, None, true);
1382        println!("a2l_file: {:#?}", a2l_file);
1383        assert!(a2l_file.is_err());
1384        assert!(matches!(
1385            a2l_file,
1386            Err(A2lError::ParserError {
1387                parser_error: ParserError::InvalidIdentifier { .. }
1388            })
1389        ));
1390    }
1391
1392    #[test]
1393    fn test_handle_unknown_taggedstruct_tag() {
1394        // balanced unknown block
1395        static DATA: &str = r#"ASAP2_VERSION 1 71 /begin PROJECT p "" /begin MODULE m ""
1396            /begin UNKNOWN_TAG abc def /begin ghi /end ghi /end UNKNOWN_TAG
1397        /end MODULE /end PROJECT"#;
1398        let result = load_from_string(DATA, None, false);
1399        assert!(result.is_ok());
1400        let (a2l_file, _) = result.unwrap();
1401        assert_eq!(a2l_file.project.module.len(), 1);
1402
1403        // unbalanced unknown block
1404        static DATA2: &str = r#"ASAP2_VERSION 1 71 /begin PROJECT p "" /begin MODULE m ""
1405            /begin UNKNOWN_TAG abc def
1406        /end MODULE /end PROJECT"#;
1407        let result = load_from_string(DATA2, None, false);
1408        assert!(matches!(
1409            result,
1410            Err(A2lError::ParserError {
1411                parser_error: ParserError::IncorrectEndTag { .. }
1412            })
1413        ));
1414
1415        //unknown keyword
1416        static DATA3: &str = r#"ASAP2_VERSION 1 71 /begin PROJECT p "" /begin MODULE m ""
1417            UNKNOWN_TAG abc def /begin ghi /end ghi
1418            /begin GROUP group_name "" ROOT
1419            /end GROUP
1420        /end MODULE /end PROJECT"#;
1421        let result = load_from_string(DATA3, None, false);
1422        assert!(result.is_ok());
1423        let (a2l_file, _) = result.unwrap();
1424        assert_eq!(a2l_file.project.module.len(), 1);
1425        assert_eq!(a2l_file.project.module[0].group.len(), 1);
1426    }
1427
1428    #[test]
1429    fn parse_with_comments() {
1430        static DATA: &str = r#"
1431        /begin TRANSFORMER Transformer
1432            "1.0.0"                          // Version info
1433            "TransformerDll32.dll"           // Name of the 32bit DLL
1434            ""                               // Name of the 64bit DLL
1435            1500                             // timeout in [ms]
1436            ON_CHANGE
1437            InverseTransformer
1438        /end TRANSFORMER"#;
1439        let module = crate::load_fragment(DATA, None).unwrap();
1440        let bi = &module.transformer[0].__block_info;
1441        assert_eq!(bi.item_location.1, 1); // offset of the version
1442        assert_eq!(bi.item_location.2, 1); // offset of the 32-bit dll name
1443        assert_eq!(bi.item_location.3, 1); // offset of the 64-bit dll name
1444        assert_eq!(bi.item_location.4, (1, false)); // offset of the timeout
1445        assert_eq!(bi.item_location.5, 1); // offset of the trigger [ON_CHANGE]
1446        assert_eq!(bi.item_location.6, 1); // offset of the inverse transformer
1447        assert_eq!(bi.end_offset, 1); // offset of the /end TRANSFORMER
1448    }
1449}