ifc_lite_landxml/alignment/
parser.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Bounded pull parsing for exact LandXML alignment element names.
6
7mod actions;
8mod driver;
9mod state;
10
11use std::collections::HashMap;
12
13use quick_xml::{events::Event, Reader};
14
15use super::{LandXmlAlignment, LandXmlAlignmentDocument, LandXmlSuperelevationEventKind};
16use crate::{
17    preflight::preflight_xml_tokens,
18    xml::{error, normalize_encoding, Result},
19    LandXmlCancellation, LandXmlCapabilityDiagnostic, LandXmlDiagnosticCode as Code, LandXmlLimits,
20    LandXmlUnits,
21};
22
23/// Alignment-specific record bounds layered over the shared hostile-XML bounds.
24#[derive(Clone, Debug)]
25pub struct LandXmlAlignmentLimits {
26    pub xml: LandXmlLimits,
27    pub max_alignments: usize,
28    pub max_alignment_segments: usize,
29    pub max_alignment_points: usize,
30    pub max_station_equations: usize,
31    pub max_cant_stations: usize,
32    pub max_superelevation_events: usize,
33}
34impl Default for LandXmlAlignmentLimits {
35    fn default() -> Self {
36        Self {
37            xml: LandXmlLimits::default(),
38            max_alignments: 100_000,
39            max_alignment_segments: 5_000_000,
40            max_alignment_points: 5_000_000,
41            max_station_equations: 1_000_000,
42            max_cant_stations: 5_000_000,
43            max_superelevation_events: 5_000_000,
44        }
45    }
46}
47/// Parse LandXML 1.2 horizontal alignments with default resource limits.
48pub fn parse_landxml_alignments(input: &[u8]) -> Result<LandXmlAlignmentDocument> {
49    parse_landxml_alignments_with_cancel(input, &LandXmlAlignmentLimits::default(), None)
50}
51/// Parse a LandXML source while retaining an empty alignment collection when
52/// the document contains terrain-only content. This is for format adapters
53/// that own both source families; callers needing an alignment require the
54/// strict [`parse_landxml_alignments`] entry point above.
55pub fn parse_landxml_alignments_optional(input: &[u8]) -> Result<LandXmlAlignmentDocument> {
56    parse_landxml_alignments_inner(input, &LandXmlAlignmentLimits::default(), None, false)
57}
58/// Parse a source with explicit resource limits while allowing terrain-only
59/// documents to return an empty alignment collection.
60pub fn parse_landxml_alignments_optional_with_cancel(
61    input: &[u8],
62    limits: &LandXmlAlignmentLimits,
63    cancelled: Option<&dyn LandXmlCancellation>,
64) -> Result<LandXmlAlignmentDocument> {
65    parse_landxml_alignments_inner(input, limits, cancelled, false)
66}
67/// Parse alignment source records with cancellation and explicit record limits.
68pub fn parse_landxml_alignments_with_cancel(
69    input: &[u8],
70    limits: &LandXmlAlignmentLimits,
71    cancelled: Option<&dyn LandXmlCancellation>,
72) -> Result<LandXmlAlignmentDocument> {
73    parse_landxml_alignments_inner(input, limits, cancelled, true)
74}
75fn parse_landxml_alignments_inner(
76    input: &[u8],
77    limits: &LandXmlAlignmentLimits,
78    cancelled: Option<&dyn LandXmlCancellation>,
79    require_alignment: bool,
80) -> Result<LandXmlAlignmentDocument> {
81    if input.len() > limits.xml.max_bytes {
82        return Err(error(Code::InputTooLarge, "input exceeds byte limit"));
83    }
84    let input = normalize_encoding(input, &limits.xml, cancelled)?;
85    preflight_xml_tokens(&input, &limits.xml, cancelled)?;
86    let mut parser = Parser::new(limits, cancelled, require_alignment);
87    let mut reader = Reader::from_reader(input.as_slice());
88    reader.config_mut().trim_text(false);
89    let mut buffer = Vec::new();
90    loop {
91        parser.check(1)?;
92        let event = reader
93            .read_event_into(&mut buffer)
94            .map_err(|_| error(Code::InvalidXml, "malformed XML"))?;
95        if matches!(event, Event::Eof) {
96            break;
97        }
98        parser.consume_event(event)?;
99        buffer.clear();
100    }
101    if !parser.frames.is_empty() {
102        return Err(error(Code::InvalidXml, "unclosed XML element"));
103    }
104    parser.finish()
105}
106#[derive(Clone)]
107pub(super) struct Frame {
108    pub(super) local: String,
109    pub(super) target: bool,
110    pub(super) namespaces: HashMap<String, String>,
111}
112pub(super) enum Capture {
113    Point {
114        local: String,
115        depth: usize,
116        pnt_ref: Option<String>,
117        text: String,
118    },
119    PointList {
120        dimension: usize,
121        depth: usize,
122        text: String,
123    },
124    Superelevation {
125        kind: LandXmlSuperelevationEventKind,
126        depth: usize,
127        text: String,
128    },
129}
130impl Capture {
131    pub(super) fn depth(&self) -> usize {
132        match self {
133            Self::Point { depth, .. }
134            | Self::PointList { depth, .. }
135            | Self::Superelevation { depth, .. } => *depth,
136        }
137    }
138}
139pub(crate) struct Parser<'a> {
140    pub(super) limits: LandXmlAlignmentLimits,
141    pub(super) cancelled: Option<&'a dyn LandXmlCancellation>,
142    pub(super) work: usize,
143    pub(super) character_references: usize,
144    pub(super) frames: Vec<Frame>,
145    pub(super) units: Option<LandXmlUnits>,
146    pub(super) alignments: Vec<LandXmlAlignment>,
147    pub(super) segments_seen: usize,
148    pub(super) alignment_points_seen: usize,
149    pub(super) station_equations_seen: usize,
150    pub(super) cant_stations_seen: usize,
151    pub(super) superelevation_events_seen: usize,
152    pub(super) alignment: Option<state::AlignmentBuilder>,
153    pub(super) capture: Option<Capture>,
154    pub(super) warnings: Vec<String>,
155    pub(super) schema: String,
156    pub(super) version: String,
157    pub(super) capability_diagnostics: Vec<LandXmlCapabilityDiagnostic>,
158    pub(super) target_namespace: Option<String>,
159    pub(super) root_seen: bool,
160    pub(super) root_closed: bool,
161    pub(super) require_alignment: bool,
162}
163impl<'a> Parser<'a> {
164    fn new(
165        limits: &LandXmlAlignmentLimits,
166        cancelled: Option<&'a dyn LandXmlCancellation>,
167        require_alignment: bool,
168    ) -> Self {
169        Self {
170            limits: limits.clone(),
171            cancelled,
172            work: 0,
173            character_references: 0,
174            frames: Vec::new(),
175            units: None,
176            alignments: Vec::new(),
177            segments_seen: 0,
178            alignment_points_seen: 0,
179            station_equations_seen: 0,
180            cant_stations_seen: 0,
181            superelevation_events_seen: 0,
182            alignment: None,
183            capture: None,
184            warnings: Vec::new(),
185            schema: String::new(),
186            version: String::new(),
187            capability_diagnostics: Vec::new(),
188            target_namespace: None,
189            root_seen: false,
190            root_closed: false,
191            require_alignment,
192        }
193    }
194
195    pub(crate) fn consume_event(&mut self, event: Event<'_>) -> Result<()> {
196        match event {
197            Event::Start(value) => self.start(&value),
198            Event::Empty(value) => {
199                self.start(&value)?;
200                self.end(None)
201            }
202            Event::End(value) => self.end(Some(value.name().as_ref().as_bytes())),
203            Event::Text(value) => self.text(value.as_ref().as_bytes()),
204            Event::CData(value) => self.cdata(value.as_ref().as_bytes()),
205            Event::DocType(_) => Err(error(Code::DtdForbidden, "DOCTYPE is not allowed")),
206            _ => Ok(()),
207        }
208    }
209
210    pub(crate) fn has_open_frames(&self) -> bool {
211        !self.frames.is_empty()
212    }
213}
214
215impl Parser<'static> {
216    pub(crate) fn new_stream(limits: LandXmlAlignmentLimits) -> Self {
217        Self::new(&limits, None, false)
218    }
219}