1use polars::prelude::*;
17
18use crate::formats::columns::{Builder, Cell, Kind};
19
20pub const MAX_MARKUP: usize = 1 << 20;
22pub const MAX_TEXT: usize = 4096;
24pub const MAX_DEPTH: usize = 64;
26const MAX_NAME: usize = 64;
28pub const BATCH_ROWS: usize = 65_536;
30pub const BATCH_TEXT: usize = 32 << 20;
33
34pub const CORE: [(&str, Kind); 9] = [
36 ("time", Kind::Time),
37 ("lat", Kind::F64),
38 ("lon", Kind::F64),
39 ("ele", Kind::F64),
40 ("kind", Kind::Str),
41 ("track", Kind::U32),
42 ("track_name", Kind::Str),
43 ("segment", Kind::U32),
44 ("gap", Kind::F64),
45];
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49enum Role {
50 Gpx,
51 Track,
52 Route,
53 Segment,
54 TrackName,
56 Point,
57 PointField,
59 Extensions,
60 ExtField,
62 Other,
63}
64
65#[derive(Debug)]
66struct Frame {
67 name: String,
68 role: Role,
69 text: Vec<u8>,
71 children: bool,
72 href: Option<String>,
74}
75
76#[derive(Debug, Default)]
77struct Point {
78 kind: &'static str,
79 lat: Option<f64>,
80 lon: Option<f64>,
81 ele: Option<f64>,
82 time: Option<i64>,
83 fields: Vec<(usize, String)>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct FieldColumn {
89 pub name: String,
90 pub integers: bool,
92 pub numbers: bool,
94}
95
96#[derive(Debug, Default, Clone, PartialEq, Eq)]
98pub struct Stats {
99 pub points: u64,
100 pub tracks: u64,
101 pub routes: u64,
102 pub waypoints: u64,
103 pub fields_dropped: u64,
105 pub long_names: u64,
107 pub bad_times: u64,
109 pub truncated: bool,
111}
112
113pub struct GpxReader {
115 buf: Vec<u8>,
116 pos: usize,
117 stack: Vec<Frame>,
118 root_seen: bool,
119 rows: Builder,
120 fields: Vec<FieldColumn>,
121 point: Option<Point>,
122 track: Option<u32>,
123 track_name: Option<String>,
124 segment: Option<u32>,
125 segments: u32,
126 stats: Stats,
127 last_time: Option<i64>,
129 held: usize,
131 max_fields: usize,
133}
134
135impl Default for GpxReader {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141pub fn looks_like(head: &[u8]) -> bool {
143 let head = head.strip_prefix(b"\xef\xbb\xbf").unwrap_or(head);
144 let mut rest = head.trim_ascii_start();
145 loop {
147 if let Some(after) = rest.strip_prefix(b"<?") {
148 let Some(end) = find(after, b"?>") else {
149 return false;
150 };
151 rest = after[end + 2..].trim_ascii_start();
152 } else if let Some(after) = rest.strip_prefix(b"<!--") {
153 let Some(end) = find(after, b"-->") else {
154 return false;
155 };
156 rest = after[end + 3..].trim_ascii_start();
157 } else {
158 break;
159 }
160 }
161 rest.strip_prefix(b"<gpx")
162 .and_then(|r| r.first())
163 .is_some_and(|b| b.is_ascii_whitespace() || *b == b'>' || *b == b'/')
164}
165
166fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
167 hay.windows(needle.len()).position(|w| w == needle)
168}
169
170fn unescape(raw: &[u8]) -> String {
173 let text = String::from_utf8_lossy(raw);
174 if !text.contains('&') {
175 return text.into_owned();
176 }
177 let mut out = String::with_capacity(text.len());
178 let mut rest = &*text;
179 while let Some(amp) = rest.find('&') {
180 out.push_str(&rest[..amp]);
181 rest = &rest[amp..];
182 let Some(semi) = rest[..rest.floor_char_boundary(12)].find(';') else {
184 out.push('&');
185 rest = &rest[1..];
186 continue;
187 };
188 let entity = &rest[1..semi];
189 let ch = match entity {
190 "lt" => Some('<'),
191 "gt" => Some('>'),
192 "amp" => Some('&'),
193 "quot" => Some('"'),
194 "apos" => Some('\''),
195 _ => entity
196 .strip_prefix("#x")
197 .or_else(|| entity.strip_prefix("#X"))
198 .map(|hex| u32::from_str_radix(hex, 16))
199 .or_else(|| entity.strip_prefix('#').map(str::parse::<u32>))
200 .and_then(|n| n.ok())
201 .and_then(char::from_u32),
202 };
203 match ch {
204 Some(ch) => {
205 out.push(ch);
206 rest = &rest[semi + 1..];
207 }
208 None => {
209 out.push('&');
210 rest = &rest[1..];
211 }
212 }
213 }
214 out.push_str(rest);
215 out
216}
217
218pub fn parse_time(s: &str) -> Option<i64> {
221 let s = s.trim();
222 if let Ok(t) = chrono::DateTime::parse_from_rfc3339(s) {
223 return Some(t.timestamp_millis());
224 }
225 if let Ok(t) = chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f%z") {
227 return Some(t.timestamp_millis());
228 }
229 ["%Y-%m-%dT%H:%M:%S%.f", "%Y-%m-%d %H:%M:%S%.f"]
230 .iter()
231 .find_map(|f| chrono::NaiveDateTime::parse_from_str(s, f).ok())
232 .map(|t| t.and_utc().timestamp_millis())
233}
234
235fn coordinate(s: Option<&str>, limit: f64) -> Option<f64> {
236 s?.trim()
237 .parse::<f64>()
238 .ok()
239 .filter(|v| v.is_finite() && v.abs() <= limit)
240}
241
242fn local(name: &str) -> &str {
244 name.rsplit_once(':').map_or(name, |(_, l)| l)
245}
246
247struct Tag<'a> {
249 name: &'a str,
250 attrs: Vec<(&'a str, String)>,
251 closing: bool,
252 empty: bool,
253}
254
255fn parse_tag(inner: &[u8]) -> Option<Tag<'_>> {
257 let text = std::str::from_utf8(inner).ok()?;
258 let (closing, text) = match text.strip_prefix('/') {
259 Some(rest) => (true, rest),
260 None => (false, text),
261 };
262 let (empty, text) = match text.strip_suffix('/') {
263 Some(rest) => (true, rest),
264 None => (false, text),
265 };
266 let end = text
267 .find(|c: char| c.is_ascii_whitespace())
268 .unwrap_or(text.len());
269 let name = &text[..end];
270 if name.is_empty() {
271 return None;
272 }
273 let mut attrs = Vec::new();
274 let mut rest = &text[end..];
275 while let Some(eq) = rest.find('=') {
276 let key = rest[..eq].trim();
277 let after = rest[eq + 1..].trim_start();
278 let quote = after.chars().next()?;
279 if quote != '"' && quote != '\'' {
280 return None;
281 }
282 let close = after[1..].find(quote)?;
283 attrs.push((local(key), unescape(&after.as_bytes()[1..1 + close])));
284 rest = &after[close + 2..];
285 }
286 Some(Tag {
287 name: local(name),
288 attrs,
289 closing,
290 empty,
291 })
292}
293
294fn markup_end(bytes: &[u8]) -> Option<usize> {
297 let after = |start: usize, close: &[u8]| {
298 bytes
299 .get(start..)
300 .and_then(|b| find(b, close))
301 .map(|at| start + at + close.len())
302 };
303 if bytes.starts_with(b"<!--") {
304 after(4, b"-->")
305 } else if bytes.starts_with(b"<![CDATA[") {
306 after(9, b"]]>")
307 } else if bytes.starts_with(b"<?") {
308 after(2, b"?>")
309 } else if bytes.starts_with(b"<!") {
310 let gt = bytes.iter().position(|&b| b == b'>')?;
312 let Some(open) = bytes[..gt].iter().position(|&b| b == b'[') else {
313 return Some(gt + 1);
314 };
315 let mut at = open;
317 loop {
318 at += 1 + bytes.get(at + 1..)?.iter().position(|&b| b == b']')?;
319 let rest = &bytes[at + 1..];
320 let space = rest.iter().take_while(|b| b.is_ascii_whitespace()).count();
321 match rest.get(space) {
322 Some(b'>') => return Some(at + 1 + space + 1),
323 Some(_) => {}
324 None => return None,
325 }
326 }
327 } else {
328 let mut quote = None;
330 for (i, &b) in bytes.iter().enumerate().skip(1) {
331 match (quote, b) {
332 (None, b'"' | b'\'') => quote = Some(b),
333 (Some(q), b) if b == q => quote = None,
334 (None, b'>') => return Some(i + 1),
335 _ => {}
336 }
337 }
338 None
339 }
340}
341
342fn undecided(bytes: &[u8]) -> bool {
345 bytes.len() < 9 && (b"<![CDATA[".starts_with(bytes) || b"<!--".starts_with(bytes))
346}
347
348impl GpxReader {
349 pub fn new() -> Self {
350 Self {
351 buf: Vec::new(),
352 pos: 0,
353 stack: Vec::new(),
354 root_seen: false,
355 rows: Builder::new(&CORE),
356 fields: Vec::new(),
357 point: None,
358 track: None,
359 track_name: None,
360 segment: None,
361 segments: 0,
362 stats: Stats::default(),
363 held: 0,
364 last_time: None,
365 max_fields: crate::limits::get().gpx_fields,
366 }
367 }
368
369 pub fn stats(&self) -> &Stats {
370 &self.stats
371 }
372
373 pub fn fields(&self) -> &[FieldColumn] {
375 &self.fields
376 }
377
378 pub fn push(&mut self, bytes: &[u8]) -> Result<(), String> {
380 if self.pos > 0 {
382 self.buf.drain(..self.pos);
383 self.pos = 0;
384 }
385 self.buf.extend_from_slice(bytes);
386 self.scan(false)
387 }
388
389 pub fn take_batch(&mut self) -> PolarsResult<Option<DataFrame>> {
391 if self.rows.len() < BATCH_ROWS && self.held < BATCH_TEXT {
392 return Ok(None);
393 }
394 self.held = 0;
395 self.rows.take().map(Some)
396 }
397
398 pub fn finish(&mut self) -> Result<DataFrame, String> {
400 self.scan(true)?;
401 if !self.root_seen {
402 return Err("Not a GPX file: it has no <gpx> element.".to_string());
403 }
404 if !self.stack.is_empty() || self.pos < self.buf.len() {
405 self.stats.truncated = true;
406 }
407 self.rows.take().map_err(|e| e.to_string())
408 }
409
410 fn scan(&mut self, at_end: bool) -> Result<(), String> {
411 while self.pos < self.buf.len() {
412 let rest = &self.buf[self.pos..];
413 if rest[0] != b'<' {
414 let end = rest.iter().position(|&b| b == b'<').unwrap_or(rest.len());
415 let (start, stop) = (self.pos, self.pos + end);
416 self.text(start, stop, false);
417 self.pos = stop;
418 continue;
419 }
420 if !at_end && undecided(rest) {
421 return Ok(());
422 }
423 let Some(len) = markup_end(rest) else {
424 if rest.len() > MAX_MARKUP {
425 return Err(format!(
426 "The GPX file has a tag or comment longer than {} MiB.",
427 MAX_MARKUP >> 20
428 ));
429 }
430 return Ok(());
432 };
433 if len > MAX_MARKUP {
434 return Err(format!(
435 "The GPX file has a tag or comment longer than {} MiB.",
436 MAX_MARKUP >> 20
437 ));
438 }
439 let (start, stop) = (self.pos, self.pos + len);
440 self.pos = stop;
441 if self.buf[start..stop].starts_with(b"<![CDATA[") {
442 self.text(start + 9, stop - 3, true);
443 } else if !(self.buf[start + 1] == b'!' || self.buf[start + 1] == b'?') {
444 let inner = self.buf[start + 1..stop - 1].to_vec();
445 if let Some(tag) = parse_tag(&inner) {
446 if tag.closing {
447 self.end(tag.name);
448 } else {
449 self.start(&tag)?;
450 if tag.empty {
451 self.end(tag.name);
452 }
453 }
454 }
455 }
456 }
457 Ok(())
458 }
459
460 fn text(&mut self, start: usize, stop: usize, cdata: bool) {
463 let Some(frame) = self.stack.last_mut() else {
464 return;
465 };
466 if !matches!(
467 frame.role,
468 Role::TrackName | Role::PointField | Role::ExtField
469 ) {
470 return;
471 }
472 let room = MAX_TEXT.saturating_sub(frame.text.len());
473 if room == 0 {
474 return;
475 }
476 let piece = &self.buf[start..stop];
477 if cdata {
478 for &b in piece {
479 if frame.text.len() >= MAX_TEXT {
480 break;
481 }
482 match b {
483 b'&' => frame.text.extend_from_slice(b"&"),
484 b => frame.text.push(b),
485 }
486 }
487 } else {
488 frame
489 .text
490 .extend_from_slice(&piece[..piece.len().min(room)]);
491 }
492 }
493
494 fn start(&mut self, tag: &Tag) -> Result<(), String> {
495 if self.stack.len() >= MAX_DEPTH {
496 return Err(format!(
497 "The GPX file nests elements more than {MAX_DEPTH} deep."
498 ));
499 }
500 if !self.root_seen {
501 if tag.name != "gpx" {
502 return Err(format!(
503 "Not a GPX file: its first element is <{}>.",
504 tag.name.chars().take(40).collect::<String>()
505 ));
506 }
507 self.root_seen = true;
508 } else if self.stack.is_empty() {
509 self.stack.push(Frame::new(tag.name, Role::Other));
511 return Ok(());
512 }
513 let parent = self.stack.last_mut().map(|frame| {
514 frame.children = true;
515 frame.role
516 });
517 let role = match (parent, tag.name) {
518 (None, _) => Role::Gpx,
519 (Some(Role::Gpx), "trk") => {
520 self.track = Some(self.stats.tracks as u32);
521 self.stats.tracks += 1;
522 self.track_name = None;
523 self.segments = 0;
524 Role::Track
525 }
526 (Some(Role::Gpx), "rte") => {
527 self.track = Some(self.stats.routes as u32);
528 self.stats.routes += 1;
529 self.track_name = None;
530 Role::Route
531 }
532 (Some(Role::Gpx), "wpt") => self.point_starts("waypoint", tag),
533 (Some(Role::Track), "trkseg") => {
534 self.segment = Some(self.segments);
535 self.last_time = None;
536 self.segments = self.segments.saturating_add(1);
537 Role::Segment
538 }
539 (Some(Role::Track | Role::Route), "name") => Role::TrackName,
540 (Some(Role::Route), "rtept") => self.point_starts("route", tag),
541 (Some(Role::Segment), "trkpt") => self.point_starts("track", tag),
542 (Some(Role::Point), "extensions") => Role::Extensions,
543 (Some(Role::Point), _) => Role::PointField,
544 (Some(Role::Extensions | Role::ExtField), _) => Role::ExtField,
545 _ => Role::Other,
546 };
547 let mut frame = Frame::new(tag.name, role);
548 if role == Role::PointField && tag.name == "link" {
549 frame.href = tag
550 .attrs
551 .iter()
552 .find(|(k, _)| *k == "href")
553 .map(|(_, v)| v.clone());
554 }
555 self.stack.push(frame);
556 Ok(())
557 }
558
559 fn point_starts(&mut self, kind: &'static str, tag: &Tag) -> Role {
560 let attr = |name: &str| {
561 tag.attrs
562 .iter()
563 .find(|(k, _)| *k == name)
564 .map(|(_, v)| v.as_str())
565 };
566 self.point = Some(Point {
567 kind,
568 lat: coordinate(attr("lat"), 90.0),
569 lon: coordinate(attr("lon"), 180.0),
570 ..Point::default()
571 });
572 Role::Point
573 }
574
575 fn end(&mut self, name: &str) {
576 let Some(at) = self.stack.iter().rposition(|frame| frame.name == name) else {
579 return;
580 };
581 while self.stack.len() > at {
582 let frame = self.stack.pop().expect("above `at`");
583 self.close(frame);
584 }
585 }
586
587 fn close(&mut self, frame: Frame) {
588 match frame.role {
589 Role::Track | Role::Route => {
590 self.track = None;
591 self.track_name = None;
592 self.segment = None;
593 }
594 Role::Segment => self.segment = None,
595 Role::TrackName => {
596 let name = unescape(&frame.text).trim().to_string();
597 self.track_name = (!name.is_empty()).then_some(name);
598 }
599 Role::Point => self.point_ends(),
600 Role::PointField => {
601 let value = match frame.href {
602 Some(href) => href,
603 None => unescape(&frame.text).trim().to_string(),
604 };
605 let Some(point) = self.point.as_mut() else {
606 return;
607 };
608 match frame.name.as_str() {
609 "ele" => point.ele = value.parse().ok().filter(|v: &f64| v.is_finite()),
610 "time" => {
611 point.time = parse_time(&value);
612 if point.time.is_none() && !value.is_empty() {
613 self.stats.bad_times += 1;
614 }
615 }
616 name => self.field(name, value),
617 }
618 }
619 Role::ExtField if !frame.children => {
620 let value = unescape(&frame.text).trim().to_string();
621 self.field(&frame.name, value);
622 }
623 _ => {}
624 }
625 }
626
627 fn field(&mut self, name: &str, value: String) {
629 if value.is_empty() || self.point.is_none() {
630 return;
631 }
632 let name = if CORE.iter().any(|(core, _)| *core == name) {
634 format!("ext_{name}")
635 } else {
636 name.to_string()
637 };
638 let column = match self.fields.iter().position(|f| f.name == name) {
639 Some(at) => at,
640 None if name.len() > MAX_NAME => {
641 self.stats.long_names += 1;
642 return;
643 }
644 None if self.fields.len() < self.max_fields => {
645 self.rows.add_column(&name, Kind::Str);
646 self.fields.push(FieldColumn {
647 name,
648 integers: true,
649 numbers: true,
650 });
651 self.fields.len() - 1
652 }
653 None => {
654 self.stats.fields_dropped += 1;
655 return;
656 }
657 };
658 let info = &mut self.fields[column];
659 info.integers &= value.parse::<i64>().is_ok();
660 info.numbers &= value.parse::<f64>().is_ok_and(f64::is_finite);
661 let point = self.point.as_mut().expect("checked above");
662 if !point.fields.iter().any(|(at, _)| *at == column) {
663 point.fields.push((column, value));
664 }
665 }
666
667 fn point_ends(&mut self) {
668 let Some(point) = self.point.take() else {
669 return;
670 };
671 self.stats.points += 1;
672 if point.kind == "waypoint" {
673 self.stats.waypoints += 1;
674 }
675 let in_track = point.kind != "waypoint";
676 let gap = match (point.kind, point.time) {
680 ("track", Some(time)) => self
681 .last_time
682 .replace(time)
683 .map(|last| time - last)
684 .filter(|ms| *ms >= -super::nmea::BACK_MS)
685 .map(|ms| ms as f64 / 1000.0),
686 _ => None,
687 };
688 let cells = [
689 Cell::Time(point.time),
690 Cell::F64(point.lat),
691 Cell::F64(point.lon),
692 Cell::F64(point.ele),
693 Cell::Str(Some(point.kind.to_string())),
694 Cell::U32(self.track.filter(|_| in_track)),
695 Cell::Str(self.track_name.clone().filter(|_| in_track)),
696 Cell::U32(self.segment.filter(|_| point.kind == "track")),
697 Cell::F64(gap),
698 ];
699 self.held += point.fields.iter().map(|(_, v)| v.len()).sum::<usize>()
700 + self.track_name.as_ref().map_or(0, String::len);
701 let core = cells.into_iter().enumerate();
702 let fields = point
703 .fields
704 .into_iter()
705 .map(|(at, value)| (CORE.len() + at, Cell::Str(Some(value))));
706 self.rows.push_sparse(core.chain(fields));
707 }
708}
709
710impl Frame {
711 fn new(name: &str, role: Role) -> Self {
712 Self {
713 name: name.chars().take(MAX_NAME * 2).collect(),
714 role,
715 text: Vec::new(),
716 children: false,
717 href: None,
718 }
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 const SAMPLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
727<!-- written by hand -->
728<gpx version="1.1" creator="test" xmlns:gpxtpx="http://example.com/tpx">
729 <metadata><name>Morning</name><time>2024-05-01T06:00:00Z</time></metadata>
730 <wpt lat="47.1" lon="8.5"><name>Start & finish</name><sym>Flag</sym></wpt>
731 <trk>
732 <name><![CDATA[Ride & run]]></name>
733 <trkseg>
734 <trkpt lat="47.2" lon="8.6"><ele>410.5</ele><time>2024-05-01T06:00:01Z</time>
735 <extensions><gpxtpx:TrackPointExtension><gpxtpx:hr>141</gpxtpx:hr><gpxtpx:cad>80</gpxtpx:cad></gpxtpx:TrackPointExtension></extensions>
736 </trkpt>
737 <trkpt lat='47.3' lon='8.7'><ele>411</ele><time>2024-05-01T08:00:02.500+02:00</time>
738 <extensions><gpxtpx:TrackPointExtension><gpxtpx:hr>142</gpxtpx:hr><gpxtpx:cad>n/a</gpxtpx:cad></gpxtpx:TrackPointExtension></extensions>
739 </trkpt>
740 </trkseg>
741 <trkseg><trkpt lat="47.4" lon="8.8"/></trkseg>
742 </trk>
743 <rte><name>Way</name><rtept lat="1" lon="2"><link href="http://x/?a=1&b=2"><text>t</text></link></rtept></rte>
744</gpx>"#;
745
746 fn read_in(text: &[u8], piece: usize) -> (DataFrame, GpxReader) {
747 let mut reader = GpxReader::new();
748 for chunk in text.chunks(piece) {
749 reader.push(chunk).unwrap();
750 }
751 let df = reader.finish().unwrap();
752 (df, reader)
753 }
754
755 fn strs(df: &DataFrame, name: &str) -> Vec<Option<String>> {
756 df.column(name)
757 .unwrap()
758 .str()
759 .unwrap()
760 .iter()
761 .map(|v| v.map(str::to_string))
762 .collect()
763 }
764
765 #[test]
766 fn every_point_is_a_row() {
767 for piece in [SAMPLE.len(), 1, 5] {
769 let (df, reader) = read_in(SAMPLE.as_bytes(), piece);
770 assert_eq!(df.height(), 5, "{df}");
771 assert_eq!(
772 strs(&df, "kind"),
773 ["waypoint", "track", "track", "track", "route"].map(|s| Some(s.to_string()))
774 );
775 assert_eq!(
776 strs(&df, "track_name"),
777 [
778 None,
779 Some("Ride & run"),
780 Some("Ride & run"),
781 Some("Ride & run"),
782 Some("Way")
783 ]
784 .map(|s| s.map(str::to_string))
785 );
786 let segment: Vec<_> = df
787 .column("segment")
788 .unwrap()
789 .u32()
790 .unwrap()
791 .iter()
792 .collect();
793 assert_eq!(segment, [None, Some(0), Some(0), Some(1), None]);
794 let gap: Vec<_> = df.column("gap").unwrap().f64().unwrap().iter().collect();
795 assert_eq!(gap, [None, None, Some(1.5), None, None], "within a segment");
796 let time: Vec<_> = df
797 .column("time")
798 .unwrap()
799 .datetime()
800 .unwrap()
801 .physical()
802 .iter()
803 .collect();
804 assert_eq!(time[1], Some(1_714_543_201_000));
805 assert_eq!(time[2], Some(1_714_543_202_500), "the offset is applied");
806 assert_eq!(
807 parse_time("2024-05-01T08:00:02.500+0200"),
808 time[2],
809 "an offset without its colon"
810 );
811 assert_eq!(
812 strs(&df, "hr")[1..3],
813 [Some("141".into()), Some("142".into())]
814 );
815 assert_eq!(strs(&df, "name")[0], Some("Start & finish".into()));
816 assert_eq!(strs(&df, "link")[4], Some("http://x/?a=1&b=2".into()));
817 let fields = reader.fields();
818 let hr = fields.iter().find(|f| f.name == "hr").unwrap();
819 assert!(hr.integers);
820 let cad = fields.iter().find(|f| f.name == "cad").unwrap();
821 assert!(!cad.numbers, "n/a is not a number");
822 assert_eq!(reader.stats().points, 5);
823 assert!(!reader.stats().truncated);
824 }
825 }
826
827 #[test]
828 fn not_gpx_and_cut_short() {
829 let mut reader = GpxReader::new();
830 assert!(reader.push(b"<kml><Document/></kml>").is_err());
831 let mut reader = GpxReader::new();
832 assert!(reader.finish().is_err(), "nothing at all");
833 let cut = &SAMPLE.as_bytes()[..SAMPLE.find("</trkseg>").unwrap()];
834 let (df, reader) = read_in(cut, 64);
835 assert_eq!(df.height(), 3, "the points before the cut");
836 assert!(reader.stats().truncated);
837 assert!(looks_like(SAMPLE.as_bytes()));
838 assert!(looks_like(b"<gpx>"));
839 assert!(!looks_like(b"<gpxx>"));
840 assert!(!looks_like(b"<?xml version='1.0'?><kml>"));
841 }
842
843 #[test]
844 fn hostile_input_is_bounded() {
845 let mut reader = GpxReader::new();
847 reader.push(b"<gpx>").unwrap();
848 let wide: String = (0..200)
849 .map(|i| {
850 let text = "z".repeat(MAX_TEXT);
851 format!("<wpt lat=\"1\" lon=\"1\"><f{i}>{text}</f{i}></wpt>")
852 })
853 .collect();
854 let mut batches = 0;
855 for _ in 0..BATCH_TEXT / (200 * MAX_TEXT) + 2 {
856 reader.push(wide.as_bytes()).unwrap();
857 assert!(reader.rows.len() * MAX_TEXT <= BATCH_TEXT + 200 * MAX_TEXT);
858 batches += usize::from(reader.take_batch().unwrap().is_some());
859 }
860 assert!(batches > 0);
861 let mut text = b"<gpx><wpt lat=\"1\" lon=\"2\"><desc>".to_vec();
863 text.extend(std::iter::repeat_n(b'x', 3 * MAX_MARKUP));
864 text.extend_from_slice(b"</desc></wpt></gpx>");
865 let (df, _) = read_in(&text, 1 << 16);
866 assert_eq!(strs(&df, "desc")[0].as_ref().unwrap().len(), MAX_TEXT);
867 let mut reader = GpxReader::new();
869 reader.push(b"<gpx><wpt a=\"").unwrap();
870 let junk = vec![b'y'; 1 << 16];
871 let refused = (0..(MAX_MARKUP >> 16) + 2).any(|_| reader.push(&junk).is_err());
872 assert!(refused);
873 let deep = "<a>".repeat(MAX_DEPTH + 1);
875 let mut reader = GpxReader::new();
876 assert!(reader.push(format!("<gpx>{deep}").as_bytes()).is_err());
877 assert_eq!(unescape(b"AB&bogus;&"), "AB&bogus;&");
878 let laughs = br#"<?xml version="1.0"?>
880<!DOCTYPE gpx [
881 <!ENTITY lol "lol">
882 <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
883] >
884<gpx><wpt lat="1" lon="2"><name>&lol2;</name></wpt></gpx>"#;
885 let (df, reader) = read_in(laughs, 7);
886 assert_eq!(strs(&df, "name"), [Some("&lol2;".into())]);
887 assert!(!reader.stats().truncated);
888 assert_eq!(
890 unescape("a &\u{fffd}\u{fffd}\u{fffd}\u{fffd};".as_bytes()),
891 "a &\u{fffd}\u{fffd}\u{fffd}\u{fffd};"
892 );
893 }
894}