Skip to main content

pidgin_lang/
parser.rs

1use std::collections::BTreeMap;
2
3use winnow::Parser;
4
5use crate::ast::PgnPacket;
6use crate::errors::ParseError;
7use crate::lexer::{header_line, field_line};
8
9const MAX_PACKET_BYTES: usize = 1_000_000;
10const MAX_FIELDS: usize = 100;
11const MAX_FIELD_LENGTH: usize = 10_000;
12
13pub fn parse_packet(input: &str) -> Result<PgnPacket, ParseError> {
14    // Strip UTF-8 BOM if present
15    let input = input.strip_prefix('\u{FEFF}').unwrap_or(input);
16
17    if input.len() > MAX_PACKET_BYTES {
18        return Err(ParseError::PacketTooLarge(input.len()));
19    }
20
21    let input = input.trim();
22    let mut lines: Vec<&str> = input.lines().collect();
23
24    while lines.last().is_some_and(|l| l.trim().is_empty()) {
25        lines.pop();
26    }
27
28    if lines.is_empty() {
29        return Err(ParseError::MissingHeader);
30    }
31
32    let mut header_idx = None;
33    for (i, line) in lines.iter().enumerate() {
34        let trimmed = line.trim();
35        if trimmed.is_empty() || trimmed.starts_with('#') {
36            continue;
37        }
38        header_idx = Some(i);
39        break;
40    }
41
42    let header_idx = header_idx.ok_or(ParseError::MissingHeader)?;
43    let header_line_str = lines[header_idx].trim();
44
45    if !header_line_str.starts_with('@') {
46        return Err(ParseError::MissingHeader);
47    }
48
49    let mut hdr_input = header_line_str;
50    let (directive, run_id) = header_line
51        .parse_next(&mut hdr_input)
52        .map_err(|_| ParseError::MissingHeader)?;
53
54    if run_id.is_empty() {
55        return Err(ParseError::EmptyRunId);
56    }
57
58    let mut fields = BTreeMap::new();
59
60    for (line_idx, line) in lines[header_idx + 1..].iter().enumerate() {
61        let trimmed = line.trim();
62        if trimmed.is_empty() || trimmed.starts_with('#') {
63            continue;
64        }
65
66        if fields.len() >= MAX_FIELDS {
67            return Err(ParseError::TooManyFields(MAX_FIELDS));
68        }
69
70        let mut field_input = trimmed;
71        let (name, value) = field_line
72            .parse_next(&mut field_input)
73            .map_err(|_| ParseError::InvalidField(header_idx + 1 + line_idx))?;
74
75        if name.len() > MAX_FIELD_LENGTH {
76            return Err(ParseError::FieldTooLong(header_idx + 1 + line_idx));
77        }
78
79        if fields.contains_key(&name) {
80            return Err(ParseError::DuplicateField(name));
81        }
82
83        match &value {
84            crate::ast::FieldValue::Scalar(s) => {
85                if s.len() > MAX_FIELD_LENGTH {
86                    return Err(ParseError::FieldTooLong(header_idx + 1 + line_idx));
87                }
88            }
89            crate::ast::FieldValue::List(items) => {
90                for item in items {
91                    if item.len() > MAX_FIELD_LENGTH {
92                        return Err(ParseError::FieldTooLong(header_idx + 1 + line_idx));
93                    }
94                }
95            }
96        }
97
98        fields.insert(name, value);
99    }
100
101    Ok(PgnPacket {
102        directive,
103        run_id,
104        fields,
105    })
106}