1use std::io::Read;
2use std::path::Path;
3
4use flate2::read::GzDecoder;
5use quick_xml::events::{BytesStart, Event};
6use quick_xml::Reader;
7
8use osmic_core::error::{OsmicError, OsmicResult};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ChangeAction {
13 Create,
14 Modify,
15 Delete,
16}
17
18#[derive(Debug, Clone)]
20pub struct OscChange {
21 pub action: ChangeAction,
22 pub element: OscElement,
23}
24
25#[derive(Debug, Clone)]
27pub enum OscElement {
28 Node {
29 id: i64,
30 lon: f64,
31 lat: f64,
32 version: u32,
33 visible: bool,
34 tags: Vec<(String, String)>,
35 },
36 Way {
37 id: i64,
38 version: u32,
39 visible: bool,
40 node_refs: Vec<i64>,
41 tags: Vec<(String, String)>,
42 },
43 Relation {
44 id: i64,
45 version: u32,
46 visible: bool,
47 members: Vec<RelMember>,
48 tags: Vec<(String, String)>,
49 },
50}
51
52#[derive(Debug, Clone)]
54pub struct RelMember {
55 pub member_type: String,
56 pub ref_id: i64,
57 pub role: String,
58}
59
60#[derive(Debug)]
62enum Parsing {
63 None,
64 Node {
65 id: i64,
66 lon: f64,
67 lat: f64,
68 version: u32,
69 visible: bool,
70 },
71 Way {
72 id: i64,
73 version: u32,
74 visible: bool,
75 },
76 Relation {
77 id: i64,
78 version: u32,
79 visible: bool,
80 },
81}
82
83impl OscElement {
84 pub fn id(&self) -> i64 {
85 match self {
86 OscElement::Node { id, .. } => *id,
87 OscElement::Way { id, .. } => *id,
88 OscElement::Relation { id, .. } => *id,
89 }
90 }
91}
92
93pub fn parse_osc_gz(path: &Path) -> OsmicResult<Vec<OscChange>> {
95 let file = std::fs::File::open(path)
96 .map_err(|e| OsmicError::Other(format!("Failed to open {}: {e}", path.display())))?;
97 let decoder = GzDecoder::new(file);
98 parse_osc(decoder)
99}
100
101pub fn parse_osc_gz_bytes(data: &[u8]) -> OsmicResult<Vec<OscChange>> {
103 let decoder = GzDecoder::new(data);
104 parse_osc(decoder)
105}
106
107pub fn parse_osc<R: Read>(reader: R) -> OsmicResult<Vec<OscChange>> {
109 let mut xml = Reader::from_reader(std::io::BufReader::new(reader));
110 xml.config_mut().trim_text(true);
111
112 let mut changes = Vec::new();
113 let mut current_action: Option<ChangeAction> = None;
114 let mut buf = Vec::new();
115
116 let mut elem_tags: Vec<(String, String)> = Vec::new();
118 let mut elem_nd_refs: Vec<i64> = Vec::new();
119 let mut elem_members: Vec<RelMember> = Vec::new();
120 let mut parsing = Parsing::None;
121
122 loop {
123 let event = xml.read_event_into(&mut buf);
124 match event {
125 Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
129 let is_empty = matches!(event, Ok(Event::Empty(_)));
130 let qname = e.name();
131 let name_bytes = qname.as_ref();
132 let name = std::str::from_utf8(name_bytes).unwrap_or("");
133
134 match name {
135 "create" => current_action = Some(ChangeAction::Create),
136 "modify" => current_action = Some(ChangeAction::Modify),
137 "delete" => current_action = Some(ChangeAction::Delete),
138 "node" => {
139 elem_tags.clear();
140 parsing = Parsing::Node {
141 id: attr_i64(e, "id"),
142 lon: attr_f64(e, "lon"),
143 lat: attr_f64(e, "lat"),
144 version: attr_u32(e, "version"),
145 visible: attr_bool(e, "visible"),
146 };
147 }
148 "way" => {
149 elem_tags.clear();
150 elem_nd_refs.clear();
151 parsing = Parsing::Way {
152 id: attr_i64(e, "id"),
153 version: attr_u32(e, "version"),
154 visible: attr_bool(e, "visible"),
155 };
156 }
157 "relation" => {
158 elem_tags.clear();
159 elem_members.clear();
160 parsing = Parsing::Relation {
161 id: attr_i64(e, "id"),
162 version: attr_u32(e, "version"),
163 visible: attr_bool(e, "visible"),
164 };
165 }
166 "tag" => {
167 let k = attr_str(e, "k");
168 let v = attr_str(e, "v");
169 elem_tags.push((k, v));
170 }
171 "nd" => {
172 elem_nd_refs.push(attr_i64(e, "ref"));
173 }
174 "member" => {
175 elem_members.push(RelMember {
176 member_type: attr_str(e, "type"),
177 ref_id: attr_i64(e, "ref"),
178 role: attr_str(e, "role"),
179 });
180 }
181 _ => {}
182 }
183
184 if is_empty && matches!(name, "node" | "way" | "relation") {
187 if let Some(action) = current_action {
188 if let Some(element) = finalize_element(
189 &mut parsing,
190 &mut elem_tags,
191 &mut elem_nd_refs,
192 &mut elem_members,
193 ) {
194 changes.push(OscChange { action, element });
195 }
196 }
197 parsing = Parsing::None;
198 }
199 }
200 Ok(Event::End(ref e)) => {
201 let qname = e.name();
202 let name = std::str::from_utf8(qname.as_ref()).unwrap_or("");
203 match name {
204 "create" | "modify" | "delete" => current_action = None,
205 "node" | "way" | "relation" => {
206 if let Some(action) = current_action {
207 if let Some(element) = finalize_element(
208 &mut parsing,
209 &mut elem_tags,
210 &mut elem_nd_refs,
211 &mut elem_members,
212 ) {
213 changes.push(OscChange { action, element });
214 }
215 }
216 parsing = Parsing::None;
217 }
218 _ => {}
219 }
220 }
221 Ok(Event::DocType(_)) => {
224 return Err(OsmicError::Other(
225 "DTD declarations are not allowed in OSC files".into(),
226 ));
227 }
228 Ok(Event::GeneralRef(_)) => {
233 return Err(OsmicError::Other(
234 "Entity references are not allowed in OSC files".into(),
235 ));
236 }
237 Ok(Event::Eof) => break,
238 Err(e) => {
239 return Err(OsmicError::Other(format!("XML parse error: {e}")));
240 }
241 _ => {}
242 }
243 buf.clear();
244 }
245
246 Ok(changes)
247}
248
249fn attr_str(e: &BytesStart, name: &str) -> String {
250 e.attributes()
255 .flatten()
256 .find(|a| a.key.as_ref() == name.as_bytes())
257 .and_then(|a| a.unescape_value().ok().map(|c| c.into_owned()))
258 .unwrap_or_default()
259}
260
261fn attr_i64(e: &BytesStart, name: &str) -> i64 {
262 attr_str(e, name).parse().unwrap_or(0)
263}
264
265fn attr_u32(e: &BytesStart, name: &str) -> u32 {
266 attr_str(e, name).parse().unwrap_or(0)
267}
268
269fn attr_f64(e: &BytesStart, name: &str) -> f64 {
270 attr_str(e, name).parse().unwrap_or(0.0)
271}
272
273fn attr_bool(e: &BytesStart, name: &str) -> bool {
274 attr_str(e, name) != "false"
275}
276
277fn finalize_element(
281 parsing: &mut Parsing,
282 elem_tags: &mut Vec<(String, String)>,
283 elem_nd_refs: &mut Vec<i64>,
284 elem_members: &mut Vec<RelMember>,
285) -> Option<OscElement> {
286 match std::mem::replace(parsing, Parsing::None) {
287 Parsing::Node {
288 id,
289 lon,
290 lat,
291 version,
292 visible,
293 } => Some(OscElement::Node {
294 id,
295 lon,
296 lat,
297 version,
298 visible,
299 tags: std::mem::take(elem_tags),
300 }),
301 Parsing::Way {
302 id,
303 version,
304 visible,
305 } => Some(OscElement::Way {
306 id,
307 version,
308 visible,
309 node_refs: std::mem::take(elem_nd_refs),
310 tags: std::mem::take(elem_tags),
311 }),
312 Parsing::Relation {
313 id,
314 version,
315 visible,
316 } => Some(OscElement::Relation {
317 id,
318 version,
319 visible,
320 members: std::mem::take(elem_members),
321 tags: std::mem::take(elem_tags),
322 }),
323 Parsing::None => None,
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
333 fn parse_minimal_osc() {
334 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
335<osmChange version="0.6" generator="test">
336 <create>
337 <node id="1" lon="1.0" lat="2.0" version="1" visible="true"/>
338 </create>
339 <modify>
340 <node id="2" lon="3.0" lat="4.0" version="2" visible="true">
341 <tag k="name" v="test"/>
342 </node>
343 </modify>
344 <delete>
345 <node id="3" lon="5.0" lat="6.0" version="3" visible="false"/>
346 </delete>
347</osmChange>"#;
348 let changes = parse_osc(&xml[..]).expect("valid OSC must parse");
349 assert_eq!(changes.len(), 3);
350 assert_eq!(changes[0].action, ChangeAction::Create);
351 assert_eq!(changes[1].action, ChangeAction::Modify);
352 assert_eq!(changes[2].action, ChangeAction::Delete);
353 }
354
355 #[test]
358 fn reject_xxe_external_entity() {
359 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
360<!DOCTYPE osmChange [
361 <!ENTITY xxe SYSTEM "file:///etc/passwd">
362]>
363<osmChange version="0.6">
364 <modify>
365 <node id="1" lon="0" lat="0" version="1" visible="true">
366 <tag k="name" v="&xxe;"/>
367 </node>
368 </modify>
369</osmChange>"#;
370 let err = parse_osc(&xml[..]).expect_err("XXE payload must be rejected");
371 let msg = format!("{err}");
372 assert!(
373 msg.contains("DTD"),
374 "error should mention DTD rejection, got: {msg}"
375 );
376 }
377
378 #[test]
381 fn reject_billion_laughs() {
382 let xml = br#"<?xml version="1.0"?>
383<!DOCTYPE osmChange [
384 <!ENTITY lol "lol">
385 <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
386 <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
387 <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
388]>
389<osmChange version="0.6">
390 <create>
391 <node id="1" lon="0" lat="0" version="1" visible="true">
392 <tag k="name" v="&lol4;"/>
393 </node>
394 </create>
395</osmChange>"#;
396 let err = parse_osc(&xml[..]).expect_err("billion-laughs payload must be rejected");
397 let msg = format!("{err}");
398 assert!(
399 msg.contains("DTD"),
400 "error should mention DTD rejection, got: {msg}"
401 );
402 }
403
404 #[test]
407 fn predefined_entities_allowed() {
408 let xml = br#"<?xml version="1.0"?>
409<osmChange version="0.6">
410 <create>
411 <node id="1" lon="0" lat="0" version="1" visible="true">
412 <tag k="name" v="Fish & Chips"/>
413 </node>
414 </create>
415</osmChange>"#;
416 let changes = parse_osc(&xml[..]).expect("predefined entities must parse");
417 assert_eq!(changes.len(), 1);
418 if let OscElement::Node { tags, .. } = &changes[0].element {
419 assert_eq!(tags.len(), 1);
420 assert_eq!(tags[0].0, "name");
421 assert_eq!(tags[0].1, "Fish & Chips");
422 } else {
423 panic!("expected Node, got {:?}", changes[0].element);
424 }
425 }
426}