ifc_lite_landxml/
pipe_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    classify_landxml_version, compatibility_version_diagnostic,
7    preflight::preflight_xml_tokens,
8    xml::{attr, attributes, error, normalize_encoding, split_name, Result},
9    LandXmlCancellation, LandXmlDiagnosticCode as Code, LandXmlLimits, LandXmlPipeNetworkDocument,
10    LandXmlSourceId,
11};
12use quick_xml::{
13    events::{BytesStart, Event},
14    Reader,
15};
16pub(crate) use state::PipeParser;
17use state::{properties, FeatureBuilder, Frame, NetworkBuilder};
18use std::collections::{HashMap, HashSet};
19mod convert;
20mod driver;
21mod features;
22mod finalize;
23mod handlers;
24mod inverts;
25mod state;
26mod units;
27/// Parse exact LandXML 1.2 pipe networks with default resource limits.
28/// This native source parser exposes no renderer, WASM, or invented IFC path.
29pub fn parse_landxml_pipe_networks(input: &[u8]) -> Result<LandXmlPipeNetworkDocument> {
30    parse_landxml_pipe_networks_with_cancel(input, &LandXmlLimits::default(), None)
31}
32/// Parse exact LandXML 1.2 pipe networks with host limits and cancellation.
33pub fn parse_landxml_pipe_networks_with_cancel(
34    input: &[u8],
35    limits: &LandXmlLimits,
36    cancelled: Option<&dyn LandXmlCancellation>,
37) -> Result<LandXmlPipeNetworkDocument> {
38    if input.len() > limits.max_bytes {
39        return Err(error(Code::InputTooLarge, "input exceeds byte limit"));
40    }
41    let input = normalize_encoding(input, limits, cancelled)?;
42    preflight_xml_tokens(&input, limits, cancelled)?;
43    let mut parser = PipeParser::new(limits, cancelled, true);
44    let mut reader = Reader::from_reader(input.as_slice());
45    reader.config_mut().trim_text(false);
46    let mut buffer = Vec::new();
47    loop {
48        let event = reader
49            .read_event_into(&mut buffer)
50            .map_err(|_| error(Code::InvalidXml, "malformed XML"))?;
51        if matches!(event, Event::Eof) {
52            break;
53        }
54        parser.consume_event(event)?;
55        buffer.clear();
56    }
57    if !parser.frames.is_empty() || !parser.root_seen || !parser.root_closed {
58        return Err(error(Code::InvalidXml, "unclosed XML element"));
59    }
60    parser.finish_stream()
61}
62
63impl<'a> PipeParser<'a> {
64    fn new(
65        limits: &LandXmlLimits,
66        cancelled: Option<&'a dyn LandXmlCancellation>,
67        require_pipe_networks: bool,
68    ) -> Self {
69        Self {
70            limits: limits.clone(),
71            cancelled,
72            require_pipe_networks,
73            work: 0,
74            character_references: 0,
75            frames: Vec::new(),
76            root_units: None,
77            schema: String::new(),
78            version: String::new(),
79            capability_diagnostics: Vec::new(),
80            target_namespace: None,
81            root_seen: false,
82            root_closed: false,
83            network: None,
84            structure: None,
85            pipe: None,
86            capture: None,
87            features_open: Vec::new(),
88            networks: Vec::new(),
89            collections: Vec::new(),
90            features: Vec::new(),
91            pending_networks: Vec::new(),
92            refusals: Vec::new(),
93            preflight_refusal_batches: Vec::new(),
94            refusal_keys: HashSet::new(),
95            pipe_networks_seen: 0,
96            structures_seen: 0,
97            pipes_seen: 0,
98            inverts_seen: 0,
99            flows_seen: 0,
100            points_seen: 0,
101            references_seen: 0,
102            pipe_network_collections: 0,
103            network_ordinal: 0,
104            feature_ordinals: HashMap::new(),
105        }
106    }
107
108    /// Consume a quick-xml event through the same state transitions used by
109    /// the bounded pull wrapper and the resumable stream driver.
110    pub(crate) fn consume_event(&mut self, event: Event<'_>) -> Result<()> {
111        self.check_cancel_and_work(1)?;
112        match event {
113            Event::Start(start) => self.start(&start),
114            Event::Empty(start) => {
115                self.start(&start)?;
116                self.end(None)
117            }
118            Event::End(end) => self.end(Some(end.name().as_ref().as_bytes())),
119            Event::Text(text) => self.text(text.as_ref().as_bytes()),
120            Event::CData(text) => self.cdata(text.as_ref().as_bytes()),
121            Event::DocType(_) => Err(error(Code::DtdForbidden, "DOCTYPE is not allowed")),
122            _ => Ok(()),
123        }
124    }
125
126    pub(crate) fn has_open_frames(&self) -> bool {
127        !self.frames.is_empty()
128    }
129
130    fn start(&mut self, start: &BytesStart<'_>) -> Result<()> {
131        if self.frames.len() >= self.limits.max_depth {
132            return Err(error(Code::LimitExceeded, "XML depth limit exceeded"));
133        }
134        let name = start.name();
135        let (_, local, prefix) = split_name(name.as_ref().as_bytes(), self.limits.max_name_bytes)?;
136        let (attributes, namespaces, references) = attributes(start, &self.limits)?;
137        self.check_cancel_and_work(attributes.len())?;
138        self.check_character_references(references)?;
139        let mut inherited = self
140            .frames
141            .last()
142            .map_or_else(HashMap::new, |frame| frame.namespaces.clone());
143        inherited.extend(namespaces);
144        let namespace = inherited.get(prefix).map(String::as_str);
145        if self.frames.is_empty() {
146            if self.root_seen {
147                return Err(error(
148                    Code::InvalidXml,
149                    "LandXML document has multiple roots",
150                ));
151            }
152            if local != "LandXML" {
153                return Err(error(Code::InvalidSemantic, "root element is not LandXML"));
154            }
155            let capability = classify_landxml_version(namespace, attr(&attributes, "version"));
156            if !capability.supports_tin_ingestion() {
157                return Err(match capability {
158                    crate::LandXmlVersionCapability::NotLandXml => error(
159                        Code::UnsupportedNamespace,
160                        "root namespace is not a recognized LandXML namespace",
161                    ),
162                    _ => error(
163                        Code::UnsupportedVersion,
164                        "LandXML namespaces require a known version declaration",
165                    ),
166                });
167            }
168            self.schema = capability
169                .schema()
170                .expect("supported capability has a schema")
171                .to_owned();
172            self.target_namespace = namespace.map(str::to_owned);
173            if let Some(diagnostic) = compatibility_version_diagnostic(
174                capability,
175                attr(&attributes, "version").expect("supported capability has a version"),
176            ) {
177                self.capability_diagnostics.push(diagnostic);
178            }
179            self.version = attr(&attributes, "version")
180                .expect("supported capability has a version")
181                .to_owned();
182            self.root_seen = true;
183        }
184        let target = namespace == self.target_namespace.as_deref();
185        self.frames.push(Frame {
186            local: local.to_owned(),
187            target,
188            namespaces: inherited,
189        });
190        if let Some(capture) = self.capture.as_mut() {
191            if self.frames.len() > capture.depth {
192                capture
193                    .invalid_reason
194                    .get_or_insert_with(|| "Center must contain direct coordinates".to_owned());
195            }
196        }
197        if !target {
198            return Ok(());
199        }
200        let properties = properties(&attributes);
201        match local {
202            "PipeNetworks" if self.is_path(&["LandXML", "PipeNetworks"]) => {
203                self.pipe_network_collections += 1;
204                self.network_ordinal = 0;
205                self.collections.push(crate::LandXmlPipeNetworkCollection {
206                    source_id: LandXmlSourceId(format!(
207                        "landxml:pipe-networks:{}",
208                        self.pipe_network_collections
209                    )),
210                    source_path: format!("LandXML/PipeNetworks[{}]", self.pipe_network_collections),
211                    properties,
212                });
213            }
214            "PipeNetwork" if self.is_path(&["LandXML", "PipeNetworks", "PipeNetwork"]) => {
215                Self::reserve(
216                    &mut self.pipe_networks_seen,
217                    self.limits.max_pipe_networks,
218                    "pipe network",
219                )?;
220                self.network_ordinal += 1;
221                if !matches!(
222                    attr(&attributes, "pipeNetType"),
223                    Some("water" | "storm" | "sanitary" | "other")
224                ) {
225                    return Err(error(
226                        Code::InvalidSemantic,
227                        "PipeNetwork has invalid pipeNetType",
228                    ));
229                }
230                let source_path = format!(
231                    "LandXML/PipeNetworks[{}]/PipeNetwork[{}]",
232                    self.pipe_network_collections, self.network_ordinal
233                );
234                self.network = Some(NetworkBuilder {
235                    source_id: LandXmlSourceId(format!(
236                        "landxml:pipe-network:{}:{}",
237                        self.pipe_network_collections, self.network_ordinal
238                    )),
239                    source_path,
240                    name: attr(&attributes, "name").map(str::to_owned),
241                    pipe_network_type: attr(&attributes, "pipeNetType").map(str::to_owned),
242                    properties,
243                    structure_units: None,
244                    pipe_units: None,
245                    structures: Vec::new(),
246                    pipes: Vec::new(),
247                    structure_ordinal: 0,
248                    pipe_ordinal: 0,
249                    structure_collection: 0,
250                    pipe_collection: 0,
251                    structures_in_collection: 0,
252                    pipes_in_collection: 0,
253                    saw_structs: false,
254                    saw_pipes: false,
255                    features: Vec::new(),
256                });
257            }
258            "Feature" if self.feature_owner().is_some() => {
259                let (owner_source_id, owner_path) = self.feature_owner().expect("checked owner");
260                let ordinal = self
261                    .feature_ordinals
262                    .entry(owner_source_id.clone())
263                    .or_insert(0);
264                *ordinal += 1;
265                let ordinal = *ordinal;
266                self.features_open.push(FeatureBuilder {
267                    source_id: LandXmlSourceId(format!("{}:feature:{ordinal}", owner_source_id.0)),
268                    source_path: format!("{owner_path}/Feature[{ordinal}]"),
269                    owner_source_id,
270                    properties,
271                    depth: self.frames.len(),
272                });
273            }
274            "Property" if self.feature_property_path() => {
275                if let Some(feature) = self.features_open.last_mut() {
276                    let key = attr(&attributes, "label")
277                        .or_else(|| attr(&attributes, "name"))
278                        .ok_or_else(|| {
279                            error(
280                                Code::InvalidSemantic,
281                                "Feature Property requires label or name",
282                            )
283                        })?;
284                    feature.properties.insert(
285                        key.to_owned(),
286                        attr(&attributes, "value").unwrap_or_default().to_owned(),
287                    );
288                }
289            }
290            "Structs" if self.is_path(&["LandXML", "PipeNetworks", "PipeNetwork", "Structs"]) => {
291                let network = self.network.as_mut().expect("Structs has network");
292                network.structure_collection += 1;
293                network.structures_in_collection = 0;
294                network.structure_units = None;
295                network.saw_structs = true;
296            }
297            "Pipes" if self.is_path(&["LandXML", "PipeNetworks", "PipeNetwork", "Pipes"]) => {
298                let network = self.network.as_mut().expect("Pipes has network");
299                network.pipe_collection += 1;
300                network.pipes_in_collection = 0;
301                network.pipe_units = None;
302                network.saw_pipes = true;
303            }
304            "Struct"
305                if self.is_path(&[
306                    "LandXML",
307                    "PipeNetworks",
308                    "PipeNetwork",
309                    "Structs",
310                    "Struct",
311                ]) =>
312            {
313                self.start_structure(properties, &attributes)?
314            }
315            "Pipe"
316                if self.is_path(&["LandXML", "PipeNetworks", "PipeNetwork", "Pipes", "Pipe"]) =>
317            {
318                self.start_pipe(properties, &attributes)?
319            }
320            "Metric" | "Imperial" => self.record_units(properties)?,
321            "Center" => self.start_center(&attributes)?,
322            "Invert"
323                if self.is_path(&[
324                    "LandXML",
325                    "PipeNetworks",
326                    "PipeNetwork",
327                    "Structs",
328                    "Struct",
329                    "Invert",
330                ]) =>
331            {
332                self.start_invert(properties, &attributes)?
333            }
334            "PipeFlow"
335                if self.is_path(&[
336                    "LandXML",
337                    "PipeNetworks",
338                    "PipeNetwork",
339                    "Pipes",
340                    "Pipe",
341                    "PipeFlow",
342                ]) =>
343            {
344                self.start_flow(properties, &attributes, false)?
345            }
346            "StructFlow"
347                if self.is_path(&[
348                    "LandXML",
349                    "PipeNetworks",
350                    "PipeNetwork",
351                    "Structs",
352                    "Struct",
353                    "StructFlow",
354                ]) =>
355            {
356                self.start_flow(properties, &attributes, true)?
357            }
358            "CircPipe" | "ElliPipe" | "EggPipe" | "RectPipe" | "Channel" => {
359                self.start_pipe_part(local, properties)?
360            }
361            "CircStruct" | "RectStruct" | "InletStruct" | "OutletStruct" | "Connection" => {
362                self.start_structure_part(local, properties)?
363            }
364            _ => {}
365        }
366        Ok(())
367    }
368
369    fn is_path(&self, expected: &[&str]) -> bool {
370        self.frames.len() == expected.len()
371            && self
372                .frames
373                .iter()
374                .zip(expected)
375                .all(|(frame, local)| frame.target && frame.local == *local)
376    }
377}
378
379impl PipeParser<'static> {
380    /// Construct the optional pipe-family participant for the canonical
381    /// LandXML stream. Terrain-only documents remain valid stream inputs.
382    pub(crate) fn new_stream(limits: LandXmlLimits) -> Self {
383        Self::new(&limits, None, false)
384    }
385}