gpx_extractor.c (42554B)
1 /* 2 This file is part of libextractor. 3 Copyright (C) 2026 Vidyut Samanta and Christian Grothoff 4 5 libextractor is free software; you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published 7 by the Free Software Foundation; either version 3, or (at your 8 option) any later version. 9 10 libextractor is distributed in the hope that it will be useful, but 11 WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with libextractor; see the file COPYING. If not, write to the 17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 Boston, MA 02110-1301, USA. 19 */ 20 /** 21 * @file plugins/gpx_extractor.c 22 * @brief plugin to support GPX (GPS Exchange Format) track logs 23 * @author Christian Grothoff 24 * 25 * GPX is raw location history: where a device was, when, and -- through 26 * the `creator' attribute -- which device or application recorded it. 27 * That makes the header of a GPX file one of the more identifying things 28 * a volume can contain, which is why this plugin exists. 29 * 30 * This is deliberately *not* an XML parser. libextractor has no XML 31 * dependency and this plugin must stay unconditional, so what follows is 32 * a bounded, single-pass scanner over at most #GPX_SCAN_CAP bytes. Its 33 * limits, stated once here rather than repeated at every call site: 34 * 35 * - Comments (`<!-- ... -->') and processing instructions are not 36 * skipped, so an element name that only occurs inside a comment is 37 * still seen. For the fields below that means a false positive, never 38 * an out-of-bounds read. 39 * - CDATA is recognised only when it opens the content of an element 40 * whose text we ask for; a CDATA section in the middle of mixed 41 * content is treated as markup. 42 * - Only the five predefined entities and numeric character references 43 * are expanded; a document-defined entity is passed through verbatim. 44 * - Element content is the run of text up to the next `<'; nested 45 * elements inside a value are not concatenated. 46 * - Namespace prefixes are skipped, so `<gpx:name>' and `<name>' are the 47 * same element here. A document that binds a prefix to some *other* 48 * namespace would be misread; GPX files in the wild do not. 49 * - Attribute values are read as raw text; the scanner does not care 50 * whether a document is well-formed. 51 * 52 * None of that can make the scanner read outside the buffer or fail to 53 * make forward progress: every loop below is bounded by the buffer 54 * length and advances at least one byte per iteration. 55 */ 56 #include "platform.h" 57 #include "extractor.h" 58 #include "forensics.h" 59 60 #include <math.h> 61 62 63 /** 64 * How many bytes of the file we are willing to look at. GPX track logs 65 * routinely run to tens of megabytes; a first pass over a volume must 66 * not read all of that. Counts derived from a truncated scan are 67 * reported as lower bounds, with a comment saying so. 68 */ 69 #define GPX_SCAN_CAP (1024 * 1024) 70 71 /** 72 * Longest start tag we will scan for its closing `>'. Keeps a file made 73 * of one unterminated tag from turning the per-element work into a scan 74 * of the whole buffer. 75 */ 76 #define GPX_MAX_TAG 8192 77 78 /** 79 * Longest element text content we will look at. 80 */ 81 #define GPX_MAX_TEXT 4096 82 83 /** 84 * How far into the file the `<gpx' element has to appear. 85 */ 86 #define GPX_MAGIC_WINDOW 2048 87 88 /** 89 * Most attributes we parse out of a single start tag. 90 */ 91 #define GPX_MAX_ATTRS 64 92 93 /** 94 * Mean Earth radius in metres, as used for the haversine sum. 95 */ 96 #define GPX_EARTH_RADIUS 6371000.0 97 98 99 /** 100 * Everything we picked up while walking the track points. 101 */ 102 struct GpxScan 103 { 104 /** 105 * Bounding box of the points seen, west/south/east/north. 106 */ 107 double minlon; 108 double minlat; 109 double maxlon; 110 double maxlat; 111 112 /** 113 * Coordinates of the first point, which is where the recording 114 * started. 115 */ 116 double first_lat; 117 double first_lon; 118 119 /** 120 * Elevation range seen inside track points. 121 */ 122 double minele; 123 double maxele; 124 125 /** 126 * Sum of the great-circle distances between consecutive track points. 127 */ 128 double distance; 129 130 /** 131 * Latitude/longitude of the previous track point, for @e distance. 132 */ 133 double prev_lat; 134 double prev_lon; 135 136 /** 137 * First and last timestamp seen inside a track point. 138 */ 139 int64_t first_time; 140 int64_t last_time; 141 142 /** 143 * Number of `<trkpt>', `<wpt>' and `<rtept>' elements. 144 */ 145 uint64_t trkpts; 146 uint64_t wpts; 147 uint64_t rtepts; 148 149 /** 150 * Non-zero once at least one point of any kind was seen; before that 151 * an `<ele>' or `<time>' belongs to the metadata, not to a point. 152 */ 153 int seen_point; 154 155 /** 156 * Non-zero once #prev_lat / #prev_lon hold a track point. 157 */ 158 int have_prev; 159 160 /** 161 * Non-zero once the bounding box fields are meaningful. 162 */ 163 int have_bbox; 164 165 /** 166 * Non-zero once the elevation range fields are meaningful. 167 */ 168 int have_ele; 169 }; 170 171 172 /** 173 * Can @a c appear in an XML name (after the first character)? 174 * 175 * @param c character to test 176 * @return 1 if @a c is a name character, 0 if not 177 */ 178 static int 179 gpx_name_char (char c) 180 { 181 return ( ( ('a' <= c) && ('z' >= c) ) || 182 ( ('A' <= c) && ('Z' >= c) ) || 183 ( ('0' <= c) && ('9' >= c) ) || 184 ('_' == c) || ('-' == c) || ('.' == c) ); 185 } 186 187 188 /** 189 * Is the element that starts at @a pos named @a tag? 190 * 191 * A namespace prefix is skipped, so `<gpx:name' matches "name". 192 * 193 * @param buf the buffer 194 * @param len number of bytes in @a buf 195 * @param pos offset of the `<' 196 * @param tag element name to compare against 197 * @return 1 on a match, 0 otherwise 198 */ 199 static int 200 gpx_tag_is (const char *buf, 201 size_t len, 202 size_t pos, 203 const char *tag) 204 { 205 size_t i; 206 size_t start; 207 size_t tlen = strlen (tag); 208 209 if ( (pos >= len) || 210 ('<' != buf[pos]) ) 211 return 0; 212 i = pos + 1; 213 start = i; 214 while ( (i < len) && 215 (i - start < 64) && 216 gpx_name_char (buf[i]) ) 217 i++; 218 if ( (i < len) && 219 (':' == buf[i]) ) 220 { 221 i++; 222 start = i; 223 while ( (i < len) && 224 (i - start < 64) && 225 gpx_name_char (buf[i]) ) 226 i++; 227 } 228 if (i - start != tlen) 229 return 0; 230 return (0 == memcmp (&buf[start], 231 tag, 232 tlen)); 233 } 234 235 236 /** 237 * Find the `>' that closes the start tag beginning at @a pos. Quoted 238 * attribute values may contain `>', so quoting is tracked. 239 * 240 * @param buf the buffer 241 * @param len number of bytes in @a buf 242 * @param pos offset of the `<' 243 * @return offset of the `>', or `(size_t) -1' if there is none within 244 * #GPX_MAX_TAG bytes 245 */ 246 static size_t 247 gpx_tag_end (const char *buf, 248 size_t len, 249 size_t pos) 250 { 251 char quote = 0; 252 253 for (size_t i = pos; (i < len) && (i - pos < GPX_MAX_TAG); i++) 254 { 255 char c = buf[i]; 256 257 if (0 != quote) 258 { 259 if (c == quote) 260 quote = 0; 261 continue; 262 } 263 if ( ('"' == c) || 264 ('\'' == c) ) 265 { 266 quote = c; 267 continue; 268 } 269 if ('>' == c) 270 return i; 271 } 272 return (size_t) -1; 273 } 274 275 276 /** 277 * Find the value of attribute @a name in the start tag at @a pos. 278 * 279 * @param buf the buffer 280 * @param len number of bytes in @a buf 281 * @param pos offset of the `<' 282 * @param name attribute name, without a namespace prefix 283 * @param[out] vstart offset of the first byte of the value 284 * @param[out] vlen number of bytes in the value 285 * @return 1 if the attribute was found, 0 if not 286 */ 287 static int 288 gpx_attr (const char *buf, 289 size_t len, 290 size_t pos, 291 const char *name, 292 size_t *vstart, 293 size_t *vlen) 294 { 295 size_t end = gpx_tag_end (buf, 296 len, 297 pos); 298 size_t i; 299 size_t nlen = strlen (name); 300 301 if (((size_t) -1) == end) 302 return 0; 303 i = pos + 1; 304 while ( (i < end) && 305 (gpx_name_char (buf[i]) || 306 (':' == buf[i]) ) ) 307 i++; 308 for (unsigned int n = 0; n < GPX_MAX_ATTRS; n++) 309 { 310 size_t astart; 311 size_t alen; 312 size_t vs; 313 size_t ve; 314 char quote; 315 316 while ( (i < end) && 317 ( (' ' == buf[i]) || ('\t' == buf[i]) || 318 ('\r' == buf[i]) || ('\n' == buf[i]) ) ) 319 i++; 320 if (i >= end) 321 break; 322 astart = i; 323 while ( (i < end) && 324 (gpx_name_char (buf[i]) || 325 (':' == buf[i]) ) ) 326 i++; 327 if (i == astart) 328 { 329 i++; /* not a name: skip a byte so we always progress */ 330 continue; 331 } 332 alen = i - astart; 333 /* a prefixed attribute matches on its local part */ 334 for (size_t k = 0; k < alen; k++) 335 if (':' == buf[astart + k]) 336 { 337 astart += k + 1; 338 alen -= k + 1; 339 break; 340 } 341 while ( (i < end) && 342 ( (' ' == buf[i]) || ('\t' == buf[i]) || 343 ('\r' == buf[i]) || ('\n' == buf[i]) ) ) 344 i++; 345 if ( (i >= end) || 346 ('=' != buf[i]) ) 347 continue; /* valueless attribute; i already advanced past a name */ 348 i++; 349 while ( (i < end) && 350 ( (' ' == buf[i]) || ('\t' == buf[i]) || 351 ('\r' == buf[i]) || ('\n' == buf[i]) ) ) 352 i++; 353 if (i >= end) 354 break; 355 quote = buf[i]; 356 if ( ('"' == quote) || 357 ('\'' == quote) ) 358 { 359 i++; 360 vs = i; 361 while ( (i < end) && 362 (quote != buf[i]) ) 363 i++; 364 ve = i; 365 if (i < end) 366 i++; 367 } 368 else 369 { 370 vs = i; 371 while ( (i < end) && 372 (' ' != buf[i]) && ('\t' != buf[i]) && 373 ('\r' != buf[i]) && ('\n' != buf[i]) && 374 ('/' != buf[i]) ) 375 i++; 376 ve = i; 377 } 378 if ( (alen == nlen) && 379 (0 == memcmp (&buf[astart], 380 name, 381 nlen)) ) 382 { 383 *vstart = vs; 384 *vlen = ve - vs; 385 return 1; 386 } 387 } 388 return 0; 389 } 390 391 392 /** 393 * Text content of the element whose start tag begins at @a pos. 394 * 395 * @param buf the buffer 396 * @param len number of bytes in @a buf 397 * @param pos offset of the `<' 398 * @param[out] tstart offset of the first content byte 399 * @param[out] tlen number of content bytes 400 * @return 1 if content was found, 0 if the element is empty or the tag 401 * is malformed 402 */ 403 static int 404 gpx_text (const char *buf, 405 size_t len, 406 size_t pos, 407 size_t *tstart, 408 size_t *tlen) 409 { 410 size_t end = gpx_tag_end (buf, 411 len, 412 pos); 413 size_t i; 414 size_t j; 415 416 if (((size_t) -1) == end) 417 return 0; 418 if ( (end > pos) && 419 ('/' == buf[end - 1]) ) 420 return 0; /* self-closing element, no content */ 421 i = end + 1; 422 if ( (i + 9 <= len) && 423 (0 == memcmp (&buf[i], 424 "<![CDATA[", 425 9)) ) 426 { 427 j = i + 9; 428 while ( (j + 3 <= len) && 429 (j - i < GPX_MAX_TEXT) ) 430 { 431 if (0 == memcmp (&buf[j], 432 "]]>", 433 3)) 434 { 435 *tstart = i + 9; 436 *tlen = j - (i + 9); 437 return 1; 438 } 439 j++; 440 } 441 return 0; 442 } 443 j = i; 444 while ( (j < len) && 445 ('<' != buf[j]) && 446 (j - i < GPX_MAX_TEXT) ) 447 j++; 448 *tstart = i; 449 *tlen = j - i; 450 return (0 != *tlen); 451 } 452 453 454 /** 455 * Find the next element named @a tag at or after @a from, staying below 456 * @a limit. 457 * 458 * @param buf the buffer 459 * @param limit offset one past the last byte to search 460 * @param from where to start searching 461 * @param tag element name 462 * @return offset of the `<', or `(size_t) -1' if not found 463 */ 464 static size_t 465 gpx_find (const char *buf, 466 size_t limit, 467 size_t from, 468 const char *tag) 469 { 470 for (size_t i = from; i < limit; i++) 471 { 472 if ('<' != buf[i]) 473 continue; 474 if (gpx_tag_is (buf, 475 limit, 476 i, 477 tag)) 478 return i; 479 } 480 return (size_t) -1; 481 } 482 483 484 /** 485 * Expand the five predefined XML entities and numeric character 486 * references in @a in. Anything else is copied through unchanged. 487 * 488 * @param in input text 489 * @param inlen number of bytes in @a in 490 * @param[out] out where to write the result 491 * @param outsize number of bytes available in @a out 492 * @return number of bytes written to @a out 493 */ 494 static size_t 495 gpx_unescape (const char *in, 496 size_t inlen, 497 char *out, 498 size_t outsize) 499 { 500 size_t o = 0; 501 size_t i = 0; 502 503 while ( (i < inlen) && 504 (o + 5 < outsize) ) 505 { 506 size_t j; 507 uint32_t cp = 0; 508 509 if ('&' != in[i]) 510 { 511 out[o++] = in[i++]; 512 continue; 513 } 514 /* find the terminating `;' within a short window */ 515 j = i + 1; 516 while ( (j < inlen) && 517 (j - i < 12) && 518 (';' != in[j]) ) 519 j++; 520 if ( (j >= inlen) || 521 (';' != in[j]) ) 522 { 523 out[o++] = in[i++]; 524 continue; 525 } 526 /* j is the offset of the `;', so the reference is j + 1 - i bytes */ 527 if ( (4 == j + 1 - i) && 528 (0 == memcmp (&in[i], "<", 4)) ) 529 cp = '<'; 530 else if ( (4 == j + 1 - i) && 531 (0 == memcmp (&in[i], ">", 4)) ) 532 cp = '>'; 533 else if ( (5 == j + 1 - i) && 534 (0 == memcmp (&in[i], "&", 5)) ) 535 cp = '&'; 536 else if ( (6 == j + 1 - i) && 537 (0 == memcmp (&in[i], """, 6)) ) 538 cp = '"'; 539 else if ( (6 == j + 1 - i) && 540 (0 == memcmp (&in[i], "'", 6)) ) 541 cp = '\''; 542 else if ( (j - i > 2) && 543 ('#' == in[i + 1]) ) 544 { 545 size_t k = i + 2; 546 int base = 10; 547 548 if ( (k < j) && 549 ( ('x' == in[k]) || ('X' == in[k]) ) ) 550 { 551 base = 16; 552 k++; 553 } 554 if (k == j) 555 { 556 out[o++] = in[i++]; 557 continue; 558 } 559 while (k < j) 560 { 561 int d; 562 563 if ( ('0' <= in[k]) && ('9' >= in[k]) ) 564 d = in[k] - '0'; 565 else if ( (16 == base) && ('a' <= in[k]) && ('f' >= in[k]) ) 566 d = in[k] - 'a' + 10; 567 else if ( (16 == base) && ('A' <= in[k]) && ('F' >= in[k]) ) 568 d = in[k] - 'A' + 10; 569 else 570 break; 571 if (cp > 0x110000 / (uint32_t) base) 572 { 573 cp = 0; 574 break; 575 } 576 cp = cp * (uint32_t) base + (uint32_t) d; 577 k++; 578 } 579 if ( (k != j) || 580 (0 == cp) || 581 (cp > 0x10FFFF) || 582 ( (0xD800 <= cp) && (0xDFFF >= cp) ) ) 583 { 584 out[o++] = in[i++]; 585 continue; 586 } 587 } 588 else 589 { 590 out[o++] = in[i++]; 591 continue; /* unknown entity: leave it alone */ 592 } 593 if (cp < 0x80) 594 { 595 out[o++] = (char) cp; 596 } 597 else if (cp < 0x800) 598 { 599 out[o++] = (char) (0xC0 | (cp >> 6)); 600 out[o++] = (char) (0x80 | (cp & 0x3F)); 601 } 602 else if (cp < 0x10000) 603 { 604 out[o++] = (char) (0xE0 | (cp >> 12)); 605 out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F)); 606 out[o++] = (char) (0x80 | (cp & 0x3F)); 607 } 608 else 609 { 610 out[o++] = (char) (0xF0 | (cp >> 18)); 611 out[o++] = (char) (0x80 | ((cp >> 12) & 0x3F)); 612 out[o++] = (char) (0x80 | ((cp >> 6) & 0x3F)); 613 out[o++] = (char) (0x80 | (cp & 0x3F)); 614 } 615 i = j + 1; 616 } 617 return o; 618 } 619 620 621 /** 622 * Emit a stretch of XML text after expanding entity references. 623 * 624 * @param ec extraction context 625 * @param type meta data type 626 * @param data the text 627 * @param len number of bytes in @a data 628 * @return 1 if the caller should stop extracting, 0 to continue 629 */ 630 static int 631 gpx_emit_xml (struct EXTRACTOR_ExtractContext *ec, 632 enum EXTRACTOR_MetaType type, 633 const char *data, 634 size_t len) 635 { 636 char buf[EXTRACTOR_FORENSIC_MAX_STRING]; 637 size_t out; 638 639 if (len > sizeof (buf) - 8) 640 len = sizeof (buf) - 8; 641 out = gpx_unescape (data, 642 len, 643 buf, 644 sizeof (buf)); 645 return EXTRACTOR_forensic_emit_text_ (ec, 646 "gpx", 647 type, 648 buf, 649 out); 650 } 651 652 653 /** 654 * Parse a decimal number out of a stretch of bytes that is not 655 * NUL-terminated. 656 * 657 * @param data the bytes 658 * @param len number of bytes in @a data 659 * @param[out] value where to store the result 660 * @return 1 on success, 0 if @a data does not start with a number 661 */ 662 static int 663 gpx_parse_double (const char *data, 664 size_t len, 665 double *value) 666 { 667 char tmp[64]; 668 char *endp; 669 double v; 670 size_t i = 0; 671 size_t o = 0; 672 673 while ( (i < len) && 674 ( (' ' == data[i]) || ('\t' == data[i]) || 675 ('\r' == data[i]) || ('\n' == data[i]) ) ) 676 i++; 677 while ( (i < len) && 678 (o < sizeof (tmp) - 1) && 679 ( ( ('0' <= data[i]) && ('9' >= data[i]) ) || 680 ('+' == data[i]) || ('-' == data[i]) || 681 ('.' == data[i]) || ('e' == data[i]) || ('E' == data[i]) ) ) 682 tmp[o++] = data[i++]; 683 tmp[o] = '\0'; 684 if (0 == o) 685 return 0; 686 v = strtod (tmp, 687 &endp); 688 if ( (endp == tmp) || 689 (! isfinite (v)) ) 690 return 0; 691 *value = v; 692 return 1; 693 } 694 695 696 /** 697 * Days since 1970-01-01 for a proleptic Gregorian date. (Howard 698 * Hinnant's `days_from_civil'.) 699 * 700 * @param y year 701 * @param m month, 1-12 702 * @param d day of month, 1-31 703 * @return day number, negative before the epoch 704 */ 705 static int64_t 706 gpx_days_from_civil (int64_t y, 707 int64_t m, 708 int64_t d) 709 { 710 int64_t era; 711 int64_t yoe; 712 int64_t doy; 713 int64_t doe; 714 715 y -= (m <= 2); 716 era = (y >= 0 ? y : y - 399) / 400; 717 yoe = y - era * 400; 718 doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; 719 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; 720 return era * 146097 + doe - 719468; 721 } 722 723 724 /** 725 * Parse an ISO 8601 timestamp as GPX writes it: 726 * `YYYY-MM-DDThh:mm:ss[.fff][Z|(+|-)hh[:mm]]'. A missing zone is taken 727 * as UTC, which is what the GPX schema requires anyway. 728 * 729 * @param s the text 730 * @param len number of bytes in @a s 731 * @param[out] when where to store seconds since the Unix epoch 732 * @return 1 on success, 0 if @a s is not a timestamp 733 */ 734 static int 735 gpx_parse_iso8601 (const char *s, 736 size_t len, 737 int64_t *when) 738 { 739 int64_t v[6] = { 0, 0, 0, 0, 0, 0 }; 740 static const size_t widths[6] = { 4, 2, 2, 2, 2, 2 }; 741 static const char seps[5] = { '-', '-', 'T', ':', ':' }; 742 size_t i = 0; 743 int64_t off = 0; 744 745 while ( (i < len) && 746 ( (' ' == s[i]) || ('\t' == s[i]) || 747 ('\r' == s[i]) || ('\n' == s[i]) ) ) 748 i++; 749 for (unsigned int f = 0; f < 6; f++) 750 { 751 if (i + widths[f] > len) 752 return 0; 753 for (size_t k = 0; k < widths[f]; k++) 754 { 755 if ( ('0' > s[i + k]) || 756 ('9' < s[i + k]) ) 757 return 0; 758 v[f] = v[f] * 10 + (s[i + k] - '0'); 759 } 760 i += widths[f]; 761 if (f < 5) 762 { 763 if (i >= len) 764 return 0; 765 if ( (2 == f) && 766 (' ' == s[i]) ) 767 i++; /* `YYYY-MM-DD hh:mm:ss' also occurs */ 768 else if (seps[f] == s[i]) 769 i++; 770 else 771 return 0; 772 } 773 } 774 if ( (v[1] < 1) || (v[1] > 12) || 775 (v[2] < 1) || (v[2] > 31) || 776 (v[3] > 23) || (v[4] > 59) || (v[5] > 60) ) 777 return 0; 778 if ( (i < len) && 779 ('.' == s[i]) ) 780 { 781 i++; 782 while ( (i < len) && 783 ('0' <= s[i]) && ('9' >= s[i]) ) 784 i++; 785 } 786 if ( (i < len) && 787 ( ('+' == s[i]) || ('-' == s[i]) ) ) 788 { 789 int neg = ('-' == s[i]); 790 int64_t oh = 0; 791 int64_t om = 0; 792 793 i++; 794 if (i + 2 > len) 795 return 0; 796 for (size_t k = 0; k < 2; k++) 797 { 798 if ( ('0' > s[i + k]) || ('9' < s[i + k]) ) 799 return 0; 800 oh = oh * 10 + (s[i + k] - '0'); 801 } 802 i += 2; 803 if ( (i < len) && 804 (':' == s[i]) ) 805 i++; 806 if (i + 2 <= len) 807 { 808 if ( ('0' <= s[i]) && ('9' >= s[i]) && 809 ('0' <= s[i + 1]) && ('9' >= s[i + 1]) ) 810 { 811 om = (s[i] - '0') * 10 + (s[i + 1] - '0'); 812 i += 2; 813 } 814 } 815 if ( (oh > 23) || (om > 59) ) 816 return 0; 817 off = oh * 3600 + om * 60; 818 if (neg) 819 off = -off; 820 } 821 *when = gpx_days_from_civil (v[0], 822 v[1], 823 v[2]) * 86400 824 + v[3] * 3600 + v[4] * 60 + v[5] 825 - off; 826 return 1; 827 } 828 829 830 /** 831 * Great-circle distance between two points, in metres. 832 * 833 * @param lat1 latitude of the first point, in degrees 834 * @param lon1 longitude of the first point, in degrees 835 * @param lat2 latitude of the second point, in degrees 836 * @param lon2 longitude of the second point, in degrees 837 * @return distance in metres 838 */ 839 static double 840 gpx_haversine (double lat1, 841 double lon1, 842 double lat2, 843 double lon2) 844 { 845 double rad = M_PI / 180.0; 846 double dlat = (lat2 - lat1) * rad; 847 double dlon = (lon2 - lon1) * rad; 848 double a; 849 850 a = sin (dlat / 2.0) * sin (dlat / 2.0) 851 + cos (lat1 * rad) * cos (lat2 * rad) 852 * sin (dlon / 2.0) * sin (dlon / 2.0); 853 if (a < 0.0) 854 a = 0.0; 855 if (a > 1.0) 856 a = 1.0; 857 return 2.0 * GPX_EARTH_RADIUS * asin (sqrt (a)); 858 } 859 860 861 /** 862 * Record a point of any kind in @a sc. 863 * 864 * @param sc scan state 865 * @param lat latitude in degrees 866 * @param lon longitude in degrees 867 * @param track 1 if this is a `<trkpt>' (which also feeds the distance 868 * sum), 0 for a waypoint or route point 869 */ 870 static void 871 gpx_note_point (struct GpxScan *sc, 872 double lat, 873 double lon, 874 int track) 875 { 876 if ( (lat < -90.0) || (lat > 90.0) || 877 (lon < -180.0) || (lon > 180.0) ) 878 return; /* out of range: not a coordinate, do not let it skew the box */ 879 if (! sc->have_bbox) 880 { 881 sc->minlat = sc->maxlat = lat; 882 sc->minlon = sc->maxlon = lon; 883 sc->first_lat = lat; 884 sc->first_lon = lon; 885 sc->have_bbox = 1; 886 } 887 else 888 { 889 if (lat < sc->minlat) 890 sc->minlat = lat; 891 if (lat > sc->maxlat) 892 sc->maxlat = lat; 893 if (lon < sc->minlon) 894 sc->minlon = lon; 895 if (lon > sc->maxlon) 896 sc->maxlon = lon; 897 } 898 if (track) 899 { 900 if (sc->have_prev) 901 sc->distance += gpx_haversine (sc->prev_lat, 902 sc->prev_lon, 903 lat, 904 lon); 905 sc->prev_lat = lat; 906 sc->prev_lon = lon; 907 sc->have_prev = 1; 908 } 909 } 910 911 912 /** 913 * Walk the buffer once, tallying points, coordinates, elevations and 914 * timestamps. 915 * 916 * The walk is linear and every iteration advances at least one byte, so 917 * it is bounded by @a len regardless of what the file contains. 918 * 919 * @param buf the buffer 920 * @param len number of bytes in @a buf 921 * @param[out] sc scan state to fill in 922 */ 923 static void 924 gpx_walk_points (const char *buf, 925 size_t len, 926 struct GpxScan *sc) 927 { 928 for (size_t i = 0; i < len; i++) 929 { 930 size_t vs; 931 size_t vl; 932 double lat; 933 double lon; 934 /* 0 = not a point, 1 = <trkpt>, 2 = <wpt>, 3 = <rtept> */ 935 unsigned int kind; 936 937 if ('<' != buf[i]) 938 continue; 939 if (gpx_tag_is (buf, len, i, "trkpt")) 940 { 941 kind = 1; 942 } 943 else if (gpx_tag_is (buf, len, i, "wpt")) 944 { 945 kind = 2; 946 } 947 else if (gpx_tag_is (buf, len, i, "rtept")) 948 { 949 kind = 3; 950 } 951 else if (sc->seen_point && 952 gpx_tag_is (buf, len, i, "ele") ) 953 { 954 double ele; 955 956 if (gpx_text (buf, len, i, &vs, &vl) && 957 gpx_parse_double (&buf[vs], vl, &ele) && 958 (ele > -20000.0) && (ele < 20000.0) ) 959 { 960 if (! sc->have_ele) 961 { 962 sc->minele = sc->maxele = ele; 963 sc->have_ele = 1; 964 } 965 else 966 { 967 if (ele < sc->minele) 968 sc->minele = ele; 969 if (ele > sc->maxele) 970 sc->maxele = ele; 971 } 972 } 973 continue; 974 } 975 else if (sc->seen_point && 976 gpx_tag_is (buf, len, i, "time") ) 977 { 978 int64_t when; 979 980 if (gpx_text (buf, len, i, &vs, &vl) && 981 gpx_parse_iso8601 (&buf[vs], vl, &when) ) 982 { 983 if (0 == sc->first_time) 984 sc->first_time = when; 985 sc->last_time = when; 986 } 987 continue; 988 } 989 else 990 { 991 continue; 992 } 993 if (1 == kind) 994 sc->trkpts++; 995 else if (2 == kind) 996 sc->wpts++; 997 else 998 sc->rtepts++; 999 sc->seen_point = 1; 1000 if (gpx_attr (buf, len, i, "lat", &vs, &vl) && 1001 gpx_parse_double (&buf[vs], vl, &lat) && 1002 gpx_attr (buf, len, i, "lon", &vs, &vl) && 1003 gpx_parse_double (&buf[vs], vl, &lon) ) 1004 gpx_note_point (sc, 1005 lat, 1006 lon, 1007 (1 == kind)); 1008 } 1009 } 1010 1011 1012 /** 1013 * Does @a creator name a piece of hardware rather than an application? 1014 * 1015 * Purely a substring test against the GPS makers whose devices write 1016 * GPX. It only decides whether we *additionally* report the value as 1017 * #EXTRACTOR_METATYPE_DEVICE_MODEL; the value is always reported as 1018 * #EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE. 1019 * 1020 * @param creator the `creator' attribute value 1021 * @param len number of bytes in @a creator 1022 * @return 1 if it looks like a device model, 0 otherwise 1023 */ 1024 static int 1025 gpx_looks_like_device (const char *creator, 1026 size_t len) 1027 { 1028 static const char *vendors[] = { 1029 "garmin", "etrex", "oregon", "gpsmap", "edge", "fenix", "forerunner", 1030 "montana", "dakota", "colorado", "magellan", "suunto", "polar", 1031 "wahoo", "elemnt", "bryton", "lezyne", "sigma rox", "coros", 1032 "tomtom", "holux", "qstarz", "columbus", "igotu", "trimble", 1033 NULL 1034 }; 1035 char low[128]; 1036 size_t n = len; 1037 1038 if (n > sizeof (low) - 1) 1039 n = sizeof (low) - 1; 1040 for (size_t i = 0; i < n; i++) 1041 { 1042 char c = creator[i]; 1043 1044 if ( ('A' <= c) && ('Z' >= c) ) 1045 c = (char) (c - 'A' + 'a'); 1046 low[i] = c; 1047 } 1048 low[n] = '\0'; 1049 for (unsigned int i = 0; NULL != vendors[i]; i++) 1050 if (NULL != strstr (low, 1051 vendors[i])) 1052 return 1; 1053 return 0; 1054 } 1055 1056 1057 /** 1058 * Report everything the `<metadata>' block holds. 1059 * 1060 * @param ec extraction context 1061 * @param buf the buffer 1062 * @param len number of bytes in @a buf 1063 * @return 1 if the caller should stop extracting, 0 to continue 1064 */ 1065 static int 1066 gpx_do_metadata (struct EXTRACTOR_ExtractContext *ec, 1067 const char *buf, 1068 size_t len) 1069 { 1070 size_t md; 1071 size_t mdend; 1072 size_t p; 1073 size_t vs; 1074 size_t vl; 1075 1076 md = gpx_find (buf, 1077 len, 1078 0, 1079 "metadata"); 1080 if (((size_t) -1) == md) 1081 { 1082 /* GPX 1.0 has no <metadata> element: name, desc, author, email, 1083 url, time, keywords and bounds are direct children of <gpx>. 1084 Treat everything from the root element up to the first <wpt>, 1085 <rte> or <trk> as the metadata block. */ 1086 md = gpx_find (buf, 1087 len, 1088 0, 1089 "gpx"); 1090 if (((size_t) -1) == md) 1091 return 0; 1092 mdend = len; 1093 for (size_t i = md + 1; i < len; i++) 1094 { 1095 if ('<' != buf[i]) 1096 continue; 1097 if (gpx_tag_is (buf, len, i, "wpt") || 1098 gpx_tag_is (buf, len, i, "rte") || 1099 gpx_tag_is (buf, len, i, "trk") ) 1100 { 1101 mdend = i; 1102 break; 1103 } 1104 } 1105 } 1106 else 1107 { 1108 /* the block ends at `</metadata>'; if that is missing (truncated 1109 file), fall back to the end of the buffer */ 1110 mdend = len; 1111 for (size_t i = md; i + 11 <= len; i++) 1112 if (0 == memcmp (&buf[i], 1113 "</metadata>", 1114 11)) 1115 { 1116 mdend = i; 1117 break; 1118 } 1119 } 1120 p = gpx_find (buf, mdend, md + 1, "name"); 1121 if ( (((size_t) -1) != p) && 1122 gpx_text (buf, mdend, p, &vs, &vl) && 1123 gpx_emit_xml (ec, EXTRACTOR_METATYPE_TITLE, &buf[vs], vl) ) 1124 return 1; 1125 p = gpx_find (buf, mdend, md + 1, "desc"); 1126 if ( (((size_t) -1) != p) && 1127 gpx_text (buf, mdend, p, &vs, &vl) && 1128 gpx_emit_xml (ec, EXTRACTOR_METATYPE_DESCRIPTION, &buf[vs], vl) ) 1129 return 1; 1130 p = gpx_find (buf, mdend, md + 1, "keywords"); 1131 if ( (((size_t) -1) != p) && 1132 gpx_text (buf, mdend, p, &vs, &vl) && 1133 gpx_emit_xml (ec, EXTRACTOR_METATYPE_KEYWORDS, &buf[vs], vl) ) 1134 return 1; 1135 /* The author is `<author><name>' in GPX 1.1 and a plain 1136 `<author>text</author>' in GPX 1.0. */ 1137 p = gpx_find (buf, mdend, md + 1, "author"); 1138 if (((size_t) -1) != p) 1139 { 1140 size_t a = gpx_find (buf, mdend, p + 1, "name"); 1141 int emitted = 0; 1142 1143 if ( (((size_t) -1) != a) && 1144 gpx_text (buf, mdend, a, &vs, &vl) ) 1145 { 1146 emitted = 1; 1147 if (gpx_emit_xml (ec, EXTRACTOR_METATYPE_AUTHOR_NAME, &buf[vs], vl)) 1148 return 1; 1149 } 1150 if ( (! emitted) && 1151 gpx_text (buf, mdend, p, &vs, &vl) && 1152 gpx_emit_xml (ec, EXTRACTOR_METATYPE_AUTHOR_NAME, &buf[vs], vl) ) 1153 return 1; 1154 } 1155 /* `<email id="user" domain="host"/>' in GPX 1.1 -- split so that the 1156 file does not read as a harvestable address -- and a plain 1157 `<email>user@host</email>' in GPX 1.0. */ 1158 p = gpx_find (buf, mdend, md + 1, "email"); 1159 if (((size_t) -1) != p) 1160 { 1161 size_t ds; 1162 size_t dl; 1163 1164 if (gpx_attr (buf, mdend, p, "id", &vs, &vl) && 1165 gpx_attr (buf, mdend, p, "domain", &ds, &dl) ) 1166 { 1167 if (EXTRACTOR_forensic_emit_ (ec, 1168 "gpx", 1169 EXTRACTOR_METATYPE_AUTHOR_EMAIL, 1170 "%.*s@%.*s", 1171 (int) vl, 1172 &buf[vs], 1173 (int) dl, 1174 &buf[ds])) 1175 return 1; 1176 } 1177 else if (gpx_text (buf, mdend, p, &vs, &vl) && 1178 gpx_emit_xml (ec, 1179 EXTRACTOR_METATYPE_AUTHOR_EMAIL, 1180 &buf[vs], 1181 vl) ) 1182 { 1183 return 1; 1184 } 1185 } 1186 /* `<copyright author="...">' with an optional `<license>' child in 1187 GPX 1.1; a plain text element in GPX 1.0. */ 1188 p = gpx_find (buf, mdend, md + 1, "copyright"); 1189 if (((size_t) -1) != p) 1190 { 1191 if (gpx_attr (buf, mdend, p, "author", &vs, &vl)) 1192 { 1193 if (gpx_emit_xml (ec, EXTRACTOR_METATYPE_COPYRIGHT, &buf[vs], vl)) 1194 return 1; 1195 } 1196 else if (gpx_text (buf, mdend, p, &vs, &vl) && 1197 gpx_emit_xml (ec, EXTRACTOR_METATYPE_COPYRIGHT, &buf[vs], vl) ) 1198 { 1199 return 1; 1200 } 1201 { 1202 size_t l = gpx_find (buf, mdend, p + 1, "license"); 1203 1204 if ( (((size_t) -1) != l) && 1205 gpx_text (buf, mdend, l, &vs, &vl) && 1206 gpx_emit_xml (ec, EXTRACTOR_METATYPE_LICENSE, &buf[vs], vl) ) 1207 return 1; 1208 } 1209 } 1210 /* every `<link href="...">' in the block, capped */ 1211 { 1212 size_t l = md; 1213 unsigned int n = 0; 1214 1215 while (n < EXTRACTOR_FORENSIC_MAX_ITEMS) 1216 { 1217 l = gpx_find (buf, mdend, l + 1, "link"); 1218 if (((size_t) -1) == l) 1219 break; 1220 if (gpx_attr (buf, mdend, l, "href", &vs, &vl)) 1221 { 1222 n++; 1223 if (gpx_emit_xml (ec, EXTRACTOR_METATYPE_URL, &buf[vs], vl)) 1224 return 1; 1225 } 1226 } 1227 } 1228 /* GPX 1.0 spells the same thing `<url>' */ 1229 p = gpx_find (buf, mdend, md + 1, "url"); 1230 if ( (((size_t) -1) != p) && 1231 gpx_text (buf, mdend, p, &vs, &vl) && 1232 gpx_emit_xml (ec, EXTRACTOR_METATYPE_URL, &buf[vs], vl) ) 1233 return 1; 1234 p = gpx_find (buf, mdend, md + 1, "time"); 1235 if (((size_t) -1) != p) 1236 { 1237 int64_t when; 1238 1239 if (gpx_text (buf, mdend, p, &vs, &vl)) 1240 { 1241 if (gpx_parse_iso8601 (&buf[vs], vl, &when)) 1242 { 1243 if (EXTRACTOR_forensic_emit_unix_time_ (ec, 1244 "gpx", 1245 EXTRACTOR_METATYPE_CREATION_DATE, 1246 when)) 1247 return 1; 1248 } 1249 else if (gpx_emit_xml (ec, 1250 EXTRACTOR_METATYPE_UNKNOWN_DATE, 1251 &buf[vs], 1252 vl)) 1253 { 1254 return 1; 1255 } 1256 } 1257 } 1258 return 0; 1259 } 1260 1261 1262 /** 1263 * Main entry method for the GPX extraction plugin. 1264 * 1265 * @param ec extraction context provided to the plugin 1266 */ 1267 void 1268 EXTRACTOR_gpx_extract_method (struct EXTRACTOR_ExtractContext *ec); 1269 1270 void 1271 EXTRACTOR_gpx_extract_method (struct EXTRACTOR_ExtractContext *ec) 1272 { 1273 char head[GPX_MAGIC_WINDOW]; 1274 char *buf = NULL; 1275 size_t hlen = 0; 1276 size_t len = 0; 1277 size_t cap; 1278 uint64_t fsize; 1279 size_t root; 1280 size_t vs; 1281 size_t vl; 1282 struct GpxScan sc; 1283 int truncated; 1284 1285 /* Bail out on the first few bytes: almost nothing we are handed is 1286 GPX, and we must not pay for the ones that are not. */ 1287 { 1288 void *data; 1289 ssize_t ret; 1290 1291 if (0 != ec->seek (ec->cls, 1292 0, 1293 SEEK_SET)) 1294 return; 1295 while (hlen < sizeof (head)) 1296 { 1297 ret = ec->read (ec->cls, 1298 &data, 1299 sizeof (head) - hlen); 1300 if (0 >= ret) 1301 break; 1302 if (((size_t) ret) > sizeof (head) - hlen) 1303 return; /* the IPC layer is misbehaving */ 1304 memcpy (&head[hlen], 1305 data, 1306 (size_t) ret); 1307 hlen += (size_t) ret; 1308 } 1309 } 1310 if (hlen < 16) 1311 return; 1312 root = gpx_find (head, 1313 hlen, 1314 0, 1315 "gpx"); 1316 if (((size_t) -1) == root) 1317 return; /* not GPX */ 1318 /* GPX is XML; require the declaration or at least that the document 1319 starts with markup, so that a text file merely containing "<gpx" is 1320 not claimed. */ 1321 { 1322 size_t k = 0; 1323 1324 if ( (hlen >= 3) && 1325 (0 == memcmp (head, "\xef\xbb\xbf", 3)) ) 1326 k = 3; /* UTF-8 BOM */ 1327 while ( (k < hlen) && 1328 ( (' ' == head[k]) || ('\t' == head[k]) || 1329 ('\r' == head[k]) || ('\n' == head[k]) ) ) 1330 k++; 1331 if ( (k >= hlen) || 1332 ('<' != head[k]) ) 1333 return; 1334 } 1335 1336 fsize = ec->get_size (ec->cls); 1337 cap = GPX_SCAN_CAP; 1338 if ( (UINT64_MAX != fsize) && 1339 (fsize < (uint64_t) cap) ) 1340 cap = (size_t) fsize; 1341 if (cap < hlen) 1342 cap = hlen; 1343 buf = malloc (cap); 1344 if (NULL == buf) 1345 return; 1346 if (0 != ec->seek (ec->cls, 1347 0, 1348 SEEK_SET)) 1349 { 1350 free (buf); 1351 return; 1352 } 1353 while (len < cap) 1354 { 1355 void *data; 1356 ssize_t ret; 1357 1358 ret = ec->read (ec->cls, 1359 &data, 1360 cap - len); 1361 if (0 >= ret) 1362 break; 1363 if (((size_t) ret) > cap - len) 1364 break; /* the IPC layer is misbehaving */ 1365 memcpy (&buf[len], 1366 data, 1367 (size_t) ret); 1368 len += (size_t) ret; 1369 } 1370 if (len < 16) 1371 { 1372 free (buf); 1373 return; 1374 } 1375 truncated = ( (UINT64_MAX == fsize) || 1376 (fsize > (uint64_t) len) ); 1377 1378 if (0 != ec->proc (ec->cls, 1379 "gpx", 1380 EXTRACTOR_METATYPE_MIMETYPE, 1381 EXTRACTOR_METAFORMAT_UTF8, 1382 "text/plain", 1383 "application/gpx+xml", 1384 strlen ("application/gpx+xml") + 1)) 1385 goto out; 1386 1387 root = gpx_find (buf, 1388 (len < GPX_MAGIC_WINDOW) ? len : GPX_MAGIC_WINDOW, 1389 0, 1390 "gpx"); 1391 if (((size_t) -1) != root) 1392 { 1393 /* The `creator' attribute is the single most identifying field in 1394 the format: it names the device or the application that produced 1395 the track. */ 1396 if (gpx_attr (buf, len, root, "creator", &vs, &vl)) 1397 { 1398 if (gpx_emit_xml (ec, 1399 EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, 1400 &buf[vs], 1401 vl)) 1402 goto out; 1403 if (gpx_looks_like_device (&buf[vs], vl) && 1404 gpx_emit_xml (ec, 1405 EXTRACTOR_METATYPE_DEVICE_MODEL, 1406 &buf[vs], 1407 vl) ) 1408 goto out; 1409 } 1410 if (gpx_attr (buf, len, root, "version", &vs, &vl) && 1411 gpx_emit_xml (ec, 1412 EXTRACTOR_METATYPE_FORMAT_VERSION, 1413 &buf[vs], 1414 vl) ) 1415 goto out; 1416 } 1417 /* GPX coordinates are always WGS 84; the schema does not allow 1418 anything else, so this is a statement about the format. */ 1419 if (EXTRACTOR_forensic_emit_ (ec, 1420 "gpx", 1421 EXTRACTOR_METATYPE_COORDINATE_SYSTEM, 1422 "WGS 84")) 1423 goto out; 1424 if (gpx_do_metadata (ec, 1425 buf, 1426 len)) 1427 goto out; 1428 1429 memset (&sc, 1430 0, 1431 sizeof (sc)); 1432 gpx_walk_points (buf, 1433 len, 1434 &sc); 1435 1436 /* An explicit <bounds> beats what we computed: it covers the whole 1437 file, ours only covers what we scanned. */ 1438 { 1439 size_t b = gpx_find (buf, len, 0, "bounds"); 1440 double minlat; 1441 double minlon; 1442 double maxlat; 1443 double maxlon; 1444 int have = 0; 1445 1446 if (((size_t) -1) != b) 1447 { 1448 if (gpx_attr (buf, len, b, "minlat", &vs, &vl) && 1449 gpx_parse_double (&buf[vs], vl, &minlat) && 1450 gpx_attr (buf, len, b, "minlon", &vs, &vl) && 1451 gpx_parse_double (&buf[vs], vl, &minlon) && 1452 gpx_attr (buf, len, b, "maxlat", &vs, &vl) && 1453 gpx_parse_double (&buf[vs], vl, &maxlat) && 1454 gpx_attr (buf, len, b, "maxlon", &vs, &vl) && 1455 gpx_parse_double (&buf[vs], vl, &maxlon) && 1456 (minlat >= -90.0) && (maxlat <= 90.0) && 1457 (minlon >= -180.0) && (maxlon <= 180.0) ) 1458 have = 1; 1459 } 1460 if ( (! have) && 1461 sc.have_bbox) 1462 { 1463 minlat = sc.minlat; 1464 minlon = sc.minlon; 1465 maxlat = sc.maxlat; 1466 maxlon = sc.maxlon; 1467 have = 1; 1468 } 1469 if (have && 1470 EXTRACTOR_forensic_emit_ (ec, 1471 "gpx", 1472 EXTRACTOR_METATYPE_BOUNDING_BOX, 1473 "%.6f,%.6f,%.6f,%.6f", 1474 minlon, 1475 minlat, 1476 maxlon, 1477 maxlat) ) 1478 goto out; 1479 } 1480 if (sc.have_bbox) 1481 { 1482 /* Where the recording started; in a personal track log that is very 1483 often the recorder's home. */ 1484 if (EXTRACTOR_forensic_emit_ (ec, 1485 "gpx", 1486 EXTRACTOR_METATYPE_GPS_LATITUDE, 1487 "%.6f", 1488 sc.first_lat)) 1489 goto out; 1490 if (EXTRACTOR_forensic_emit_ (ec, 1491 "gpx", 1492 EXTRACTOR_METATYPE_GPS_LONGITUDE, 1493 "%.6f", 1494 sc.first_lon)) 1495 goto out; 1496 } 1497 if (EXTRACTOR_forensic_emit_ (ec, 1498 "gpx", 1499 EXTRACTOR_METATYPE_ENTRY_COUNT, 1500 "%llu", 1501 (unsigned long long) sc.trkpts)) 1502 goto out; 1503 if (EXTRACTOR_forensic_emit_ (ec, 1504 "gpx", 1505 EXTRACTOR_METATYPE_COMMENT, 1506 "%llu track points, %llu waypoints," 1507 " %llu route points", 1508 (unsigned long long) sc.trkpts, 1509 (unsigned long long) sc.wpts, 1510 (unsigned long long) sc.rtepts)) 1511 goto out; 1512 if ( (0 != sc.first_time) && 1513 (sc.last_time > sc.first_time) ) 1514 { 1515 int64_t d = sc.last_time - sc.first_time; 1516 1517 if (EXTRACTOR_forensic_emit_ (ec, 1518 "gpx", 1519 EXTRACTOR_METATYPE_DURATION, 1520 "%lld:%02lld:%02lld", 1521 (long long) (d / 3600), 1522 (long long) ((d / 60) % 60), 1523 (long long) (d % 60))) 1524 goto out; 1525 } 1526 if (0 != sc.first_time) 1527 { 1528 /* When the first point was recorded; for a track without a 1529 <metadata><time> this is the only timestamp in the file. */ 1530 if (EXTRACTOR_forensic_emit_unix_time_ (ec, 1531 "gpx", 1532 EXTRACTOR_METATYPE_CREATION_DATE, 1533 sc.first_time)) 1534 goto out; 1535 if (EXTRACTOR_forensic_emit_unix_time_ (ec, 1536 "gpx", 1537 EXTRACTOR_METATYPE_MODIFICATION_DATE, 1538 sc.last_time)) 1539 goto out; 1540 } 1541 if (sc.have_ele) 1542 { 1543 if (sc.maxele - sc.minele < 0.05) 1544 { 1545 if (EXTRACTOR_forensic_emit_ (ec, 1546 "gpx", 1547 EXTRACTOR_METATYPE_LOCATION_ELEVATION, 1548 "%.1f m", 1549 sc.minele)) 1550 goto out; 1551 } 1552 else if (EXTRACTOR_forensic_emit_ (ec, 1553 "gpx", 1554 EXTRACTOR_METATYPE_LOCATION_ELEVATION, 1555 "%.1f-%.1f m", 1556 sc.minele, 1557 sc.maxele)) 1558 { 1559 goto out; 1560 } 1561 } 1562 if (sc.distance >= 1.0) 1563 { 1564 /* Haversine sum over the track points we scanned -- free, since we 1565 walked them anyway. Under the scan cap this is a lower bound. */ 1566 if (EXTRACTOR_forensic_emit_ (ec, 1567 "gpx", 1568 EXTRACTOR_METATYPE_DISTANCE, 1569 "%.0f m", 1570 sc.distance)) 1571 goto out; 1572 } 1573 /* Counts from a partial scan are lower bounds, and saying so is the 1574 whole point: silently under-reporting how many points a track has 1575 would be worse than not reporting at all. */ 1576 if (truncated && 1577 EXTRACTOR_forensic_emit_ (ec, 1578 "gpx", 1579 EXTRACTOR_METATYPE_COMMENT, 1580 "scan truncated at 1 MiB; the counts," 1581 " bounding box, duration and distance" 1582 " above cover only that prefix of the" 1583 " file") ) 1584 goto out; 1585 out: 1586 free (buf); 1587 } 1588 1589 1590 /* end of gpx_extractor.c */