1 /*
2  * Copyright (C) 2015-2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include "GpxParser.h"
18 #include <libxml/parser.h>
19 #include <string.h>
20 #include <time.h>
21 #include <algorithm>
22 #include "StringParse.h"
23 
24 using std::string;
25 
26 // format an error message
27 template <class... Args>
formatError(const char * format,Args &&...args)28 static string formatError(const char *format, Args &&...args) {
29   char buf[100] = {};
30   snprintf(buf, sizeof(buf) - 1, format, std::forward<Args>(args)...);
31   return buf;
32 }
33 
cleanupXmlDoc(xmlDoc * doc)34 static void cleanupXmlDoc(xmlDoc *doc) {
35   xmlFreeDoc(doc);
36   xmlCleanupParser();
37 }
38 
parseLocation(xmlNode * ptNode,xmlDoc * doc,GpsFix * result,string * error)39 static bool parseLocation(xmlNode *ptNode, xmlDoc *doc, GpsFix *result,
40                           string *error) {
41   float latitude;
42   float longitude;
43 
44   xmlAttrPtr attr;
45   xmlChar *tmpStr;
46 
47   // Check for and get the latitude attribute
48   attr = xmlHasProp(ptNode, (const xmlChar *)"lat");
49   if (!attr || !(tmpStr = xmlGetProp(ptNode, (const xmlChar *)"lat"))) {
50     *error = formatError("Point missing a latitude on line %d.", ptNode->line);
51     return false;  // Return error since a point *must* have a latitude
52   } else {
53     int read = SscanfWithCLocale(reinterpret_cast<const char *>(tmpStr), "%f",
54                                  &latitude);
55     xmlFree(tmpStr);  // Caller-freed
56     if (read != 1) {
57       return false;
58     }
59   }
60 
61   // Check for and get the longitude attribute
62   attr = xmlHasProp(ptNode, (const xmlChar *)"lon");
63   if (!attr || !(tmpStr = xmlGetProp(ptNode, (const xmlChar *)"lon"))) {
64     *error = formatError("Point missing a longitude on line %d.", ptNode->line);
65     return false;  // Return error since a point *must* have a longitude
66   } else {
67     int read = SscanfWithCLocale(reinterpret_cast<const char *>(tmpStr), "%f",
68                                  &longitude);
69     xmlFree(tmpStr);  // Caller-freed
70     if (read != 1) {
71       return false;
72     }
73   }
74 
75   // The result will be valid if this point is reached
76   result->latitude = latitude;
77   result->longitude = longitude;
78 
79   // Check for potential children nodes (including time, elevation, name, and
80   // description) Note that none are actually required according to the GPX
81   // format.
82   int childCount = 0;
83   for (xmlNode *field = ptNode->children; field; field = field->next) {
84     tmpStr = nullptr;
85 
86     if (!strcmp((const char *)field->name, "time")) {
87       if ((tmpStr = xmlNodeListGetString(doc, field->children, 1))) {
88         // Convert to a number
89         struct tm time = {};
90         time.tm_isdst = -1;
91         int results = sscanf((const char *)tmpStr, "%u-%u-%uT%u:%u:%u",
92                              &time.tm_year, &time.tm_mon, &time.tm_mday,
93                              &time.tm_hour, &time.tm_min, &time.tm_sec);
94         if (results != 6) {
95           *error = formatError(
96               "Improperly formatted time on line %d.<br/>"
97               "Times must be in ISO format.",
98               ptNode->line);
99           return false;
100         }
101 
102         // Correct according to the struct tm specification
103         time.tm_year -= 1900;  // Years since 1900
104         time.tm_mon -= 1;      // Months since January, 0-11
105 
106         result->time = mktime(&time);
107         xmlFree(tmpStr);  // Caller-freed
108         childCount++;
109       }
110     } else if (!strcmp((const char *)field->name, "ele")) {
111       if ((tmpStr = xmlNodeListGetString(doc, field->children, 1))) {
112         int read = SscanfWithCLocale(reinterpret_cast<const char *>(tmpStr),
113                                      "%f", &result->elevation);
114         xmlFree(tmpStr);  // Caller-freed
115         if (read != 1) {
116           return false;
117         }
118         childCount++;
119       }
120     } else if (!strcmp((const char *)field->name, "name")) {
121       if ((tmpStr = xmlNodeListGetString(doc, field->children, 1))) {
122         result->name = reinterpret_cast<const char *>(tmpStr);
123         xmlFree(tmpStr);  // Caller-freed
124         childCount++;
125       }
126     } else if (!strcmp((const char *)field->name, "desc")) {
127       if ((tmpStr = xmlNodeListGetString(doc, field->children, 1))) {
128         result->description = reinterpret_cast<const char *>(tmpStr);
129         xmlFree(tmpStr);  // Caller-freed
130         childCount++;
131       }
132     }
133 
134     // We only care about 4 potential child fields, so quit after finding those
135     if (childCount == 4) {
136       break;
137     }
138   }
139 
140   return true;
141 }
142 
parse(xmlDoc * doc,GpsFixArray * fixes,string * error)143 static bool parse(xmlDoc *doc, GpsFixArray *fixes, string *error) {
144   xmlNode *root = xmlDocGetRootElement(doc);
145   GpsFix location;
146   bool isOk;
147 
148   for (xmlNode *child = root->children; child; child = child->next) {
149     // Individual <wpt> elements are parsed on their own
150     if (!strcmp((const char *)child->name, "wpt")) {
151       isOk = parseLocation(child, doc, &location, error);
152       if (!isOk) {
153         cleanupXmlDoc(doc);
154         return false;
155       }
156       fixes->push_back(location);
157     }
158 
159     // <rte> elements require an additional depth of parsing
160     else if (!strcmp((const char *)child->name, "rte")) {
161       for (xmlNode *rtept = child->children; rtept; rtept = rtept->next) {
162         // <rtept> elements are parsed just like <wpt> elements
163         if (!strcmp((const char *)rtept->name, "rtept")) {
164           isOk = parseLocation(rtept, doc, &location, error);
165           if (!isOk) {
166             cleanupXmlDoc(doc);
167             return false;
168           }
169           fixes->push_back(location);
170         }
171       }
172     }
173 
174     // <trk> elements require two additional depths of parsing
175     else if (!strcmp((const char *)child->name, "trk")) {
176       for (xmlNode *trkseg = child->children; trkseg; trkseg = trkseg->next) {
177         // Skip non <trkseg> elements
178         if (!strcmp((const char *)trkseg->name, "trkseg")) {
179           // <trkseg> elements an additional depth of parsing
180           for (xmlNode *trkpt = trkseg->children; trkpt; trkpt = trkpt->next) {
181             // <trkpt> elements are parsed just like <wpt> elements
182             if (!strcmp((const char *)trkpt->name, "trkpt")) {
183               isOk = parseLocation(trkpt, doc, &location, error);
184               if (!isOk) {
185                 cleanupXmlDoc(doc);
186                 return false;
187               }
188               fixes->push_back(location);
189             }
190           }
191         }
192       }
193     }
194   }
195 
196   // Sort the values by timestamp
197   std::sort(fixes->begin(), fixes->end());
198 
199   cleanupXmlDoc(doc);
200   return true;
201 }
202 
parseFile(const char * filePath,GpsFixArray * fixes,string * error)203 bool GpxParser::parseFile(const char *filePath, GpsFixArray *fixes,
204                           string *error) {
205   xmlDocPtr doc = xmlReadFile(filePath, nullptr, 0);
206   if (doc == nullptr) {
207     cleanupXmlDoc(doc);
208     *error = "GPX document not parsed successfully.";
209     return false;
210   }
211   return parse(doc, fixes, error);
212 }
213 
parseString(const char * str,int len,GpsFixArray * fixes,string * error)214 bool GpxParser::parseString(const char *str, int len, GpsFixArray *fixes,
215                             string *error) {
216   xmlDocPtr doc = xmlReadMemory(str, len, NULL, NULL, 0);
217   if (doc == nullptr) {
218     cleanupXmlDoc(doc);
219     *error = "GPX document not parsed successfully.";
220     return false;
221   }
222   return parse(doc, fixes, error);
223 }