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