ifc_lite_landxml/
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
5use crate::{
6    capture::{Capture, PairListTarget, PolylineCategory, ProfileCurveCapture},
7    preflight::preflight_xml_tokens,
8    semantics::{positive_id, references, triple, units},
9    xml::{
10        attr, attributes, character_references, error, normalize_encoding, required, split_name,
11        Result,
12    },
13    LandXmlCancellation, LandXmlCapabilities, LandXmlCoordinateSystem,
14    LandXmlDiagnosticCode as Code, LandXmlExtension, LandXmlLimits, LandXmlPoint, LandXmlPolyline,
15    LandXmlRenderState, LandXmlSourceId, LandXmlSurface, LandXmlSurfaceKind, LandXmlTinDocument,
16};
17use quick_xml::{
18    events::{BytesStart, Event},
19    Reader,
20};
21mod capture;
22mod document;
23mod finalize;
24mod limits;
25mod path;
26mod profiles;
27pub(crate) mod state;
28mod text;
29mod version;
30
31pub(crate) use document::parse_landxml_document;
32pub(crate) use finalize::{require_units_for_renderable_tin, surface_draws_to_scale};
33pub(crate) use state::Parser;
34use state::{retained_properties, Frame, SurfaceBuilder};
35pub use version::*;
36
37/// Parse exact LandXML 1.2 TIN semantics with default resource limits.
38pub fn parse_landxml_tin(input: &[u8]) -> Result<LandXmlTinDocument> {
39    parse_landxml_tin_with_cancel(input, &LandXmlLimits::default(), None)
40}
41
42/// Parse exact LandXML 1.2 TIN semantics with host limits and cancellation.
43pub fn parse_landxml_tin_with_cancel(
44    input: &[u8],
45    limits: &LandXmlLimits,
46    cancelled: Option<&dyn LandXmlCancellation>,
47) -> Result<LandXmlTinDocument> {
48    if input.len() > limits.max_bytes {
49        return Err(error(Code::InputTooLarge, "input exceeds byte limit"));
50    }
51    // #5175: validate a caller-supplied assumed-unit override up front, so
52    // an unknown token refuses before any XML is read rather than partway
53    // through a parse.
54    let assumed_units = crate::semantics::resolve_assumed_units(limits)?;
55    let input = normalize_encoding(input, limits, cancelled)?;
56    preflight_xml_tokens(&input, limits, cancelled)?;
57    let mut parser = Parser::new(limits, cancelled, assumed_units);
58    let mut reader = Reader::from_reader(input.as_slice());
59    reader.config_mut().trim_text(false);
60    let mut buffer = Vec::new();
61    loop {
62        parser.check_cancel_and_work(1)?;
63        let event = reader
64            .read_event_into(&mut buffer)
65            .map_err(|_| error(Code::InvalidXml, "malformed XML"))?;
66        if matches!(event, Event::Eof) {
67            break;
68        }
69        parser.consume_event(event)?;
70        buffer.clear();
71    }
72    if !parser.frames.is_empty() {
73        return Err(error(Code::InvalidXml, "unclosed XML element"));
74    }
75    parser.finish()
76}
77
78impl Parser<'_> {
79    /// Consume an event emitted by quick-xml.  Kept crate-visible so the
80    /// chunk driver and the complete reader share all semantic transitions.
81    pub(crate) fn consume_event(&mut self, event: Event<'_>) -> Result<()> {
82        match event {
83            Event::Start(start) => self.start(&start),
84            Event::Empty(start) => {
85                self.start(&start)?;
86                self.end(None)
87            }
88            Event::End(end) => self.end(Some(end.name().as_ref().as_bytes())),
89            Event::Text(text) => self.text(text.as_ref().as_bytes()),
90            Event::CData(text) => self.cdata(text.as_ref().as_bytes()),
91            Event::DocType(_) => Err(error(Code::DtdForbidden, "DOCTYPE is not allowed")),
92            _ => Ok(()),
93        }
94    }
95    fn start(&mut self, start: &BytesStart<'_>) -> Result<()> {
96        if self.frames.len() >= self.limits.max_depth {
97            return Err(error(Code::LimitExceeded, "XML depth limit exceeded"));
98        }
99        let name = start.name();
100        let (_, local, prefix) = split_name(name.as_ref().as_bytes(), self.limits.max_name_bytes)?;
101        let (attributes, namespaces, references) = attributes(start, &self.limits)?;
102        self.check_cancel_and_work(attributes.len())?;
103        self.check_character_references(references)?;
104        let mut inherited = self
105            .frames
106            .last()
107            .map_or_else(std::collections::HashMap::new, |frame| {
108                frame.namespaces.clone()
109            });
110        inherited.extend(namespaces);
111        let namespace = inherited.get(prefix).map(String::as_str);
112        if self.frames.is_empty() {
113            if self.root_seen {
114                return Err(error(
115                    Code::InvalidXml,
116                    "LandXML document has multiple root elements",
117                ));
118            }
119            self.root_seen = true;
120            if local != "LandXML" {
121                return Err(error(Code::InvalidSemantic, "root element is not LandXML"));
122            }
123            let capability = classify_landxml_version(namespace, attr(&attributes, "version"));
124            if !capability.supports_tin_ingestion() {
125                return Err(match capability {
126                    crate::LandXmlVersionCapability::NotLandXml => error(
127                        Code::UnsupportedNamespace,
128                        "root namespace is not a recognized LandXML namespace",
129                    ),
130                    _ => error(
131                        Code::UnsupportedVersion,
132                        "LandXML namespaces require a known version declaration",
133                    ),
134                });
135            }
136            self.schema = capability
137                .schema()
138                .expect("supported capability has a schema")
139                .to_owned();
140            self.target_namespace = namespace.map(str::to_owned);
141            if let Some(diagnostic) = compatibility_version_diagnostic(
142                capability,
143                attr(&attributes, "version").expect("supported capability has a version"),
144            ) {
145                self.record_capability_diagnostic(diagnostic)?;
146            }
147            self.version = required(&attributes, "version", "LandXML")?.to_owned();
148        }
149        let target = namespace == self.target_namespace.as_deref();
150        let sibling_ordinal = self.frames.last_mut().map_or(1, |parent| {
151            let ordinal = parent
152                .child_ordinals
153                .entry((target, local.to_owned()))
154                .or_insert(0);
155            *ordinal += 1;
156            *ordinal
157        });
158        // Record one root per unknown extension subtree.  This preserves the
159        // producer-visible shape without recursively copying unbounded vendor
160        // payloads, and never mistakes an extension's `Surface` for LandXML.
161        let extension_root = !target
162            && !self.frames.is_empty()
163            && self.frames.last().is_some_and(|frame| frame.target);
164        if extension_root {
165            if self.extensions.len() >= self.limits.max_extensions {
166                return Err(error(
167                    Code::LimitExceeded,
168                    "extension record limit exceeded",
169                ));
170            }
171            self.extensions.push(LandXmlExtension {
172                namespace: namespace.unwrap_or("").to_owned(),
173                local_name: local.to_owned(),
174                path: self.path_with(local),
175            });
176        }
177        self.frames.push(Frame {
178            local: local.to_owned(),
179            target,
180            sibling_ordinal,
181            child_ordinals: std::collections::HashMap::new(),
182            overlay_name: matches!(local, "Boundary" | "Breakline" | "Contour")
183                .then(|| attr(&attributes, "name").map(str::to_owned))
184                .flatten(),
185            overlay_kind: matches!(local, "Boundary" | "Breakline" | "Contour")
186                .then(|| {
187                    attr(&attributes, "bndType")
188                        .or_else(|| attr(&attributes, "brkType"))
189                        .or_else(|| attr(&attributes, "contType"))
190                        .map(str::to_owned)
191                })
192                .flatten(),
193            overlay_properties: matches!(local, "Boundary" | "Breakline" | "Contour")
194                .then(|| retained_properties(&attributes))
195                .unwrap_or_default(),
196            namespaces: inherited,
197        });
198        if !target {
199            if local == "Corridor" {
200                self.record_preserved_only(
201                    local,
202                    crate::LandXmlPreservedOnlyExtensionKind::Corridor,
203                )?;
204            } else if local == "StringLine" {
205                self.record_preserved_only(
206                    local,
207                    crate::LandXmlPreservedOnlyExtensionKind::StringLine,
208                )?;
209            }
210            return Ok(());
211        }
212        match local {
213            "CoordinateSystem" if self.is_path(&["LandXML", "CoordinateSystem"]) => {
214                if self.coordinate_system.is_some() {
215                    self.warnings.push(
216                        "LandXML declares multiple root CoordinateSystem records; retained the last declaration"
217                            .to_owned(),
218                    );
219                }
220                self.coordinate_system = Some(LandXmlCoordinateSystem {
221                    horizontal_datum: attr(&attributes, "horizontalDatum")
222                        .map(|value| value.to_owned()),
223                    vertical_datum: attr(&attributes, "verticalDatum")
224                        .map(|value| value.to_owned()),
225                    epsg_code: attr(&attributes, "epsgCode").map(|value| value.to_owned()),
226                    name: attr(&attributes, "name").map(|value| value.to_owned()),
227                });
228            }
229            "Surface" if self.is_path(&["LandXML", "Surfaces", "Surface"]) => {
230                if self.surfaces_seen >= self.limits.max_surfaces {
231                    return Err(error(Code::LimitExceeded, "surface limit exceeded"));
232                }
233                self.surfaces_seen += 1;
234                self.surface = Some(SurfaceBuilder {
235                    name: required(&attributes, "name", "Surface")?.to_owned(),
236                    kind: LandXmlSurfaceKind::Other,
237                    points: Vec::new(),
238                    canonical_vertices: Vec::new(),
239                    source_data_points: Vec::new(),
240                    ids: std::collections::HashSet::new(),
241                    faces: Vec::new(),
242                    face_visibility: Vec::new(),
243                    hidden_face_count: 0,
244                    boundaries: Vec::new(),
245                    breaklines: Vec::new(),
246                    contours: Vec::new(),
247                    properties: retained_properties(&attributes),
248                    definition_properties: crate::LandXmlProperties::new(),
249                })
250            }
251            "Definition" if self.is_path(&["LandXML", "Surfaces", "Surface", "Definition"]) => {
252                if let Some(surface) = &mut self.surface {
253                    surface.definition_properties = retained_properties(&attributes);
254                    surface.kind = match attr(&attributes, "surfType")
255                        .map(str::to_ascii_uppercase)
256                        .as_deref()
257                    {
258                        Some("TIN") => LandXmlSurfaceKind::Tin,
259                        Some("GRID") => LandXmlSurfaceKind::Grid,
260                        Some("VOLUME") => LandXmlSurfaceKind::Volume,
261                        _ => LandXmlSurfaceKind::Other,
262                    };
263                }
264            }
265            "Metric" | "Imperial" if self.is_path(&["LandXML", "Units", local]) => {
266                if self.units.is_some() {
267                    return Err(error(
268                        Code::InvalidSemantic,
269                        "LandXML may declare units only once",
270                    ));
271                }
272                self.units = Some(units(&attributes)?);
273                // #5175: a declared <Units> element always wins over a
274                // caller-supplied assumed-unit override. Silently dropping
275                // the override here would hide that the two might disagree,
276                // so warn once, non-fatally, at the exact point we learn the
277                // declaration exists.
278                if self.assumed_units.is_some() {
279                    self.warnings.push(
280                        "an assumed linear unit override was supplied but LandXML/Units is \
281                         declared; the declared units were used and the override was ignored"
282                            .to_owned(),
283                    );
284                }
285            }
286            "P" if self.is_path(&["LandXML", "Surfaces", "Surface", "Definition", "Pnts", "P"])
287                && self
288                    .surface
289                    .as_ref()
290                    .is_some_and(|surface| surface.kind == LandXmlSurfaceKind::Tin) =>
291            {
292                self.capture = Some(Capture::Point {
293                    id: positive_id(required(&attributes, "id", "point")?)?,
294                    depth: self.frames.len(),
295                    text: String::new(),
296                })
297            }
298            "F" if self.is_path(&[
299                "LandXML",
300                "Surfaces",
301                "Surface",
302                "Definition",
303                "Faces",
304                "F",
305            ]) && self
306                .surface
307                .as_ref()
308                .is_some_and(|surface| surface.kind == LandXmlSurfaceKind::Tin) =>
309            {
310                self.capture = Some(Capture::Face {
311                    depth: self.frames.len(),
312                    text: String::new(),
313                    hidden: matches!(attr(&attributes, "i"), Some("1" | "true")),
314                })
315            }
316            "PntList3D" | "PntList2D" if self.source_data_point_dimension().is_some() => {
317                self.capture = Some(Capture::SourcePoints {
318                    depth: self.frames.len(),
319                    text: String::new(),
320                    source_path: self.capture_path(),
321                    coordinate_dimension: self
322                        .source_data_point_dimension()
323                        .expect("guarded above"),
324                })
325            }
326            "PntList3D" | "PntList2D" if self.overlay_category().is_some() => {
327                let overlay = self.frames.get(self.frames.len().saturating_sub(2));
328                self.capture = Some(Capture::Polyline {
329                    depth: self.frames.len(),
330                    text: String::new(),
331                    category: self.overlay_category().expect("guarded above"),
332                    name: overlay.and_then(|frame| frame.overlay_name.clone()),
333                    kind: overlay.and_then(|frame| frame.overlay_kind.clone()),
334                    properties: overlay.map_or_else(crate::LandXmlProperties::new, |frame| {
335                        frame.overlay_properties.clone()
336                    }),
337                    source_path: self.capture_path(),
338                    coordinate_dimension: if local == "PntList3D" { 3 } else { 2 },
339                })
340            }
341            _ => {}
342        }
343        self.start_road_semantics(local, &attributes)?;
344        Ok(())
345    }
346
347    fn end(&mut self, closing: Option<&[u8]>) -> Result<()> {
348        let frame = self
349            .frames
350            .last()
351            .cloned()
352            .ok_or_else(|| error(Code::InvalidXml, "unexpected closing element"))?;
353        if let Some(closing) = closing {
354            let (_, local, _) = split_name(closing, self.limits.max_name_bytes)?;
355            if local != frame.local {
356                return Err(error(Code::InvalidXml, "mismatched closing element"));
357            }
358        }
359        if self
360            .capture
361            .as_ref()
362            .is_some_and(|capture| capture.depth() == self.frames.len())
363        {
364            self.finish_capture()?;
365        }
366        if self.is_path(&["LandXML", "Surfaces", "Surface"]) {
367            self.finish_surface()?;
368        }
369        self.finish_road_element()?;
370        let closes_root = self.frames.len() == 1;
371        self.frames.pop();
372        if closes_root {
373            self.root_closed = true;
374        }
375        Ok(())
376    }
377
378    fn is_path(&self, expected: &[&str]) -> bool {
379        self.frames.len() == expected.len()
380            && self
381                .frames
382                .iter()
383                .zip(expected)
384                .all(|(frame, local)| frame.target && frame.local == *local)
385    }
386}