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 "KmlParser.h"
18 #include <libxml/parser.h>
19 #include <string.h>
20 #include <unistd.h>
21 #include <string>
22 #include <utility>
23 #include "StringParse.h"
24 using std::string;
25 
26 // Coordinates can be nested arbitrarily deep within a Placemark, depending
27 // on the type of object (Point, LineString, Polygon) the Placemark contains
findCoordinates(xmlNode * current)28 static xmlNode* findCoordinates(xmlNode* current) {
29   for (; current != nullptr; current = current->next) {
30     if (!strcmp((const char*)current->name, "coordinates")) {
31       return current;
32     }
33     xmlNode* children = findCoordinates(current->xmlChildrenNode);
34     if (children != nullptr) {
35       return children;
36     }
37   }
38   return nullptr;
39 }
40 
41 // Coordinates have the following format:
42 //        <coordinates> -112.265654928602,36.09447672602546,2357
43 //                ...
44 //                -112.2657374587321,36.08646312301303,2357
45 //        </coordinates>
46 // often entirely contained in a single string, necessitating regex
parseCoordinates(xmlNode * current,GpsFixArray * fixes)47 static bool parseCoordinates(xmlNode* current, GpsFixArray* fixes) {
48   xmlNode* coordinates_node = findCoordinates(current);
49   bool result = true;
50   if (coordinates_node == nullptr ||
51       coordinates_node->xmlChildrenNode == nullptr ||
52       coordinates_node->xmlChildrenNode->content == nullptr) {
53     return false;
54   }
55 
56   const char* coordinates =
57       (const char*)(coordinates_node->xmlChildrenNode->content);
58   int coordinates_len = strlen(coordinates);
59   int offset = 0, n = 0;
60   GpsFix new_fix;
61   while (3 == SscanfWithCLocale(coordinates + offset, "%f , %f , %f%n",
62                                 &new_fix.longitude, &new_fix.latitude,
63                                 &new_fix.elevation, &n)) {
64     fixes->push_back(new_fix);
65     offset += n;
66   }
67 
68   // Only allow whitespace at the end of the string to remain unconsumed.
69   for (int i = offset; i < coordinates_len && result; ++i) {
70     result = isspace(coordinates[i]);
71   }
72 
73   return result;
74 }
75 
parseGxTrack(xmlNode * children,GpsFixArray * fixes)76 static bool parseGxTrack(xmlNode* children, GpsFixArray* fixes) {
77   bool result = true;
78   for (xmlNode* current = children; result && current != nullptr;
79        current = current->next) {
80     if (current->ns && current->ns->prefix &&
81         !strcmp((const char*)current->ns->prefix, "gx") &&
82         !strcmp((const char*)current->name, "coord")) {
83       std::string coordinates{(const char*)current->xmlChildrenNode->content};
84       GpsFix new_fix;
85       result = (3 == SscanfWithCLocale(coordinates.c_str(), "%f %f %f",
86                                        &new_fix.longitude, &new_fix.latitude,
87                                        &new_fix.elevation));
88       fixes->push_back(new_fix);
89     }
90   }
91   return result;
92 }
93 
parsePlacemark(xmlNode * current,GpsFixArray * fixes)94 static bool parsePlacemark(xmlNode* current, GpsFixArray* fixes) {
95   string description;
96   string name;
97   size_t ind = string::npos;
98   // not worried about case-sensitivity since .kml files
99   // are expected to be machine-generated
100   for (; current != nullptr; current = current->next) {
101     const bool hasContent =
102         current->xmlChildrenNode && current->xmlChildrenNode->content;
103 
104     if (hasContent && !strcmp((const char*)current->name, "description")) {
105       description = (const char*)current->xmlChildrenNode->content;
106     } else if (hasContent && !strcmp((const char*)current->name, "name")) {
107       name = (const char*)current->xmlChildrenNode->content;
108     } else if (!strcmp((const char*)current->name, "Point") ||
109                !strcmp((const char*)current->name, "LineString") ||
110                !strcmp((const char*)current->name, "Polygon")) {
111       ind = (ind != string::npos ? ind : fixes->size());
112       if (!parseCoordinates(current->xmlChildrenNode, fixes)) {
113         return false;
114       }
115     } else if (current->ns && current->ns->prefix &&
116                !strcmp((const char*)current->ns->prefix, "gx") &&
117                !strcmp((const char*)current->name, "Track")) {
118       ind = (ind != string::npos ? ind : fixes->size());
119       if (!parseGxTrack(current->xmlChildrenNode, fixes)) {
120         return false;
121       }
122     }
123   }
124 
125   if (ind == string::npos || ind >= fixes->size()) {
126     return false;
127   }
128 
129   // only assign name and description to the first of the
130   // points to avoid needless repetition
131   (*fixes)[ind].description = std::move(description);
132   (*fixes)[ind].name = std::move(name);
133 
134   return true;
135 }
136 
137 // Placemarks (aka locations) can be nested arbitrarily deep
traverseSubtree(xmlNode * current,GpsFixArray * fixes,string * error)138 static bool traverseSubtree(xmlNode* current, GpsFixArray* fixes,
139                             string* error) {
140   for (; current; current = current->next) {
141     if (current->name != nullptr &&
142         !strcmp((const char*)current->name, "Placemark")) {
143       if (!parsePlacemark(current->xmlChildrenNode, fixes)) {
144         *error = "Location found with missing or malformed coordinates";
145         return false;
146       }
147     } else if (current->name != nullptr &&
148                strcmp((const char*)current->name, "text") != 0) {
149       // if it's not a Placemark we must go deeper
150       if (!traverseSubtree(current->xmlChildrenNode, fixes, error)) {
151         return false;
152       }
153     }
154   }
155   error->clear();
156   return true;
157 }
158 
parseFile(const char * filePath,GpsFixArray * fixes,string * error)159 bool KmlParser::parseFile(const char* filePath, GpsFixArray* fixes,
160                           string* error) {
161   // This initializes the library and checks potential ABI mismatches between
162   // the version it was compiled for and the actual shared library used.
163   LIBXML_TEST_VERSION
164 
165   xmlDocPtr doc = xmlReadFile(filePath, nullptr, 0);
166   if (doc == nullptr) {
167     *error = "KML document not parsed successfully.";
168     xmlFreeDoc(doc);
169     return false;
170   }
171 
172   xmlNodePtr cur = xmlDocGetRootElement(doc);
173   if (cur == nullptr) {
174     *error = "Could not get root element of parsed KML file.";
175     xmlFreeDoc(doc);
176     xmlCleanupParser();
177     return false;
178   }
179   bool isWellFormed = traverseSubtree(cur, fixes, error);
180 
181   xmlFreeDoc(doc);
182   xmlCleanupParser();
183 
184   return isWellFormed;
185 }
186 
parseString(const char * str,int len,GpsFixArray * fixes,string * error)187 bool KmlParser::parseString(const char* str, int len, GpsFixArray* fixes,
188                             string* error) {
189   // This initializes the library and checks potential ABI mismatches between
190   // the version it was compiled for and the actual shared library used.
191   LIBXML_TEST_VERSION
192 
193   xmlDocPtr doc = xmlReadMemory(str, len, NULL, NULL, 0);
194   if (doc == nullptr) {
195     *error = "KML document not parsed successfully.";
196     xmlFreeDoc(doc);
197     return false;
198   }
199 
200   xmlNodePtr cur = xmlDocGetRootElement(doc);
201   if (cur == nullptr) {
202     *error = "Could not get root element of parsed KML file.";
203     xmlFreeDoc(doc);
204     xmlCleanupParser();
205     return false;
206   }
207   bool isWellFormed = traverseSubtree(cur, fixes, error);
208 
209   xmlFreeDoc(doc);
210   xmlCleanupParser();
211 
212   return isWellFormed;
213 }
214