1use 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;
27pub fn parse_landxml_pipe_networks(input: &[u8]) -> Result<LandXmlPipeNetworkDocument> {
30 parse_landxml_pipe_networks_with_cancel(input, &LandXmlLimits::default(), None)
31}
32pub 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 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 pub(crate) fn new_stream(limits: LandXmlLimits) -> Self {
383 Self::new(&limits, None, false)
384 }
385}