ifc_lite_landxml/plan/
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 1.2 COGO and plan element names.
6
7use quick_xml::{events::Event, Reader};
8
9use super::{
10    model::LandXmlPlanReferenceIndex, LandXmlCgPoint, LandXmlMonument, LandXmlParcel,
11    LandXmlPlanDocument, LandXmlPlanFeature,
12};
13use crate::{
14    classify_landxml_version, compatibility_version_diagnostic,
15    preflight::preflight_xml_tokens,
16    semantics::units,
17    xml::{
18        attr, attributes, character_references, error, normalize_encoding, split_name, unescape,
19        Attributes, Result,
20    },
21    LandXmlCancellation, LandXmlCapabilityDiagnostic, LandXmlDiagnosticCode as Code, LandXmlLimits,
22    LandXmlSourceId, LandXmlUnits, LandXmlVersionCapability,
23};
24
25mod actions;
26mod driver;
27mod state;
28mod value;
29use state::{Active, Capture, Frame, GeometryBuilder};
30pub(super) use value::area_scale;
31/// Plan-record bounds layered over the shared hostile-XML limits.
32#[derive(Clone, Debug)]
33pub struct LandXmlPlanLimits {
34    pub xml: LandXmlLimits,
35    pub max_cogo_points: usize,
36    pub max_monuments: usize,
37    pub max_plan_features: usize,
38    pub max_parcels: usize,
39    pub max_geometry: usize,
40    pub max_vertices: usize,
41}
42impl Default for LandXmlPlanLimits {
43    fn default() -> Self {
44        Self {
45            xml: LandXmlLimits::default(),
46            max_cogo_points: 5_000_000,
47            max_monuments: 1_000_000,
48            max_plan_features: 1_000_000,
49            max_parcels: 1_000_000,
50            max_geometry: 5_000_000,
51            max_vertices: 10_000_000,
52        }
53    }
54}
55
56/// Parse COGO points, monuments, parcels and plan features from LandXML 1.2.
57pub fn parse_landxml_plan(input: &[u8]) -> Result<LandXmlPlanDocument> {
58    parse_landxml_plan_with_cancel(input, &LandXmlPlanLimits::default(), None)
59}
60/// Parse plan records while bounding work and polling the caller's cancellation hook.
61pub fn parse_landxml_plan_with_cancel(
62    input: &[u8],
63    limits: &LandXmlPlanLimits,
64    cancelled: Option<&dyn LandXmlCancellation>,
65) -> Result<LandXmlPlanDocument> {
66    if input.len() > limits.xml.max_bytes {
67        return Err(error(Code::InputTooLarge, "input exceeds byte limit"));
68    }
69    let input = normalize_encoding(input, &limits.xml, cancelled)?;
70    preflight_xml_tokens(&input, &limits.xml, cancelled)?;
71    let mut parser = Parser::new(limits, cancelled);
72    let mut reader = Reader::from_reader(input.as_slice());
73    reader.config_mut().trim_text(false);
74    let mut buffer = Vec::new();
75    loop {
76        let event = reader
77            .read_event_into(&mut buffer)
78            .map_err(|_| error(Code::InvalidXml, "malformed XML"))?;
79        if matches!(event, Event::Eof) {
80            break;
81        }
82        parser.consume_event(event)?;
83        buffer.clear();
84    }
85    if !parser.root_seen {
86        return Err(error(Code::InvalidXml, "LandXML document is empty"));
87    }
88    if !parser.frames.is_empty() {
89        return Err(error(Code::InvalidXml, "unclosed XML element"));
90    }
91    Ok(parser.document())
92}
93
94// Child action modules need the builders while the one-pass driver needs only
95// the opaque parser type and its methods.
96#[allow(private_interfaces)]
97pub(crate) struct Parser<'a> {
98    pub(super) limits: LandXmlPlanLimits,
99    pub(super) cancelled: Option<&'a dyn LandXmlCancellation>,
100    pub(super) work: usize,
101    pub(super) characters: usize,
102    pub(super) references: usize,
103    pub(super) frames: Vec<Frame>,
104    pub(super) units: Option<LandXmlUnits>,
105    pub(super) area_unit: Option<String>,
106    pub(super) area_scale_to_square_meters: Option<f64>,
107    pub(super) schema: String,
108    pub(super) version: String,
109    pub(super) capability_diagnostics: Vec<LandXmlCapabilityDiagnostic>,
110    pub(super) target_namespace: Option<String>,
111    pub(super) root_seen: bool,
112    pub(super) cogo_points: Vec<LandXmlCgPoint>,
113    pub(super) reference_index: LandXmlPlanReferenceIndex,
114    pub(super) monuments: Vec<LandXmlMonument>,
115    pub(super) features: Vec<LandXmlPlanFeature>,
116    pub(super) parcels: Vec<LandXmlParcel>,
117    pub(super) active: Vec<Active>,
118    pub(super) active_depths: Vec<usize>,
119    pub(super) geometry: Option<GeometryBuilder>,
120    pub(super) capture: Option<Capture>,
121    pub(super) scope_stack: Vec<(usize, LandXmlSourceId)>,
122    pub(super) reference_scope: Option<LandXmlSourceId>,
123    pub(super) scope_ordinal: usize,
124    pub(super) cogo_ordinal: usize,
125    pub(super) monument_ordinal: usize,
126    pub(super) vertices: usize,
127    pub(super) geometry_count: usize,
128    pub(super) feature_ordinal: usize,
129    pub(super) parcel_ordinal: usize,
130}
131impl<'a> Parser<'a> {
132    fn new(limits: &LandXmlPlanLimits, cancelled: Option<&'a dyn LandXmlCancellation>) -> Self {
133        Self {
134            limits: limits.clone(),
135            cancelled,
136            work: 0,
137            characters: 0,
138            references: 0,
139            frames: Vec::new(),
140            units: None,
141            area_unit: None,
142            area_scale_to_square_meters: None,
143            schema: String::new(),
144            version: String::new(),
145            capability_diagnostics: Vec::new(),
146            target_namespace: None,
147            root_seen: false,
148            cogo_points: Vec::new(),
149            reference_index: LandXmlPlanReferenceIndex::default(),
150            monuments: Vec::new(),
151            features: Vec::new(),
152            parcels: Vec::new(),
153            active: Vec::new(),
154            active_depths: Vec::new(),
155            geometry: None,
156            capture: None,
157            scope_stack: Vec::new(),
158            reference_scope: None,
159            scope_ordinal: 0,
160            cogo_ordinal: 0,
161            monument_ordinal: 0,
162            vertices: 0,
163            geometry_count: 0,
164            feature_ordinal: 0,
165            parcel_ordinal: 0,
166        }
167    }
168    pub(super) fn check(&mut self, added: usize) -> Result<()> {
169        if self
170            .cancelled
171            .is_some_and(LandXmlCancellation::is_cancelled)
172        {
173            return Err(error(Code::Cancelled, "ingestion cancelled"));
174        }
175        self.work = self
176            .work
177            .checked_add(added)
178            .ok_or_else(|| error(Code::LimitExceeded, "work limit exceeded"))?;
179        if self.work > self.limits.xml.max_work {
180            return Err(error(Code::LimitExceeded, "work limit exceeded"));
181        }
182        Ok(())
183    }
184
185    pub(crate) fn consume_event(&mut self, event: Event<'_>) -> Result<()> {
186        self.check(1)?;
187        match event {
188            Event::Start(value) => self.start(&value),
189            Event::Empty(value) => {
190                self.start(&value)?;
191                self.end(None)
192            }
193            Event::End(value) => self.end(Some(value.name().as_ref().as_bytes())),
194            Event::Text(value) => self.text(value.as_ref().as_bytes()),
195            Event::CData(value) => self.cdata(value.as_ref().as_bytes()),
196            Event::DocType(_) => Err(error(Code::DtdForbidden, "DOCTYPE is not allowed")),
197            _ => Ok(()),
198        }
199    }
200
201    pub(crate) fn has_open_frames(&self) -> bool {
202        !self.frames.is_empty()
203    }
204}
205
206impl Parser<'static> {
207    pub(crate) fn new_stream(limits: LandXmlPlanLimits) -> Self {
208        Self::new(&limits, None)
209    }
210}