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