/* * Copyright (C) 2015-2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "KmlParser.h" #include #include #include #include #include #include "StringParse.h" using std::string; // Coordinates can be nested arbitrarily deep within a Placemark, depending // on the type of object (Point, LineString, Polygon) the Placemark contains static xmlNode* findCoordinates(xmlNode* current) { for (; current != nullptr; current = current->next) { if (!strcmp((const char*)current->name, "coordinates")) { return current; } xmlNode* children = findCoordinates(current->xmlChildrenNode); if (children != nullptr) { return children; } } return nullptr; } // Coordinates have the following format: // -112.265654928602,36.09447672602546,2357 // ... // -112.2657374587321,36.08646312301303,2357 // // often entirely contained in a single string, necessitating regex static bool parseCoordinates(xmlNode* current, GpsFixArray* fixes) { xmlNode* coordinates_node = findCoordinates(current); bool result = true; if (coordinates_node == nullptr || coordinates_node->xmlChildrenNode == nullptr || coordinates_node->xmlChildrenNode->content == nullptr) { return false; } const char* coordinates = (const char*)(coordinates_node->xmlChildrenNode->content); int coordinates_len = strlen(coordinates); int offset = 0, n = 0; GpsFix new_fix; while (3 == SscanfWithCLocale(coordinates + offset, "%f , %f , %f%n", &new_fix.longitude, &new_fix.latitude, &new_fix.elevation, &n)) { fixes->push_back(new_fix); offset += n; } // Only allow whitespace at the end of the string to remain unconsumed. for (int i = offset; i < coordinates_len && result; ++i) { result = isspace(coordinates[i]); } return result; } static bool parseGxTrack(xmlNode* children, GpsFixArray* fixes) { bool result = true; for (xmlNode* current = children; result && current != nullptr; current = current->next) { if (current->ns && current->ns->prefix && !strcmp((const char*)current->ns->prefix, "gx") && !strcmp((const char*)current->name, "coord")) { std::string coordinates{(const char*)current->xmlChildrenNode->content}; GpsFix new_fix; result = (3 == SscanfWithCLocale(coordinates.c_str(), "%f %f %f", &new_fix.longitude, &new_fix.latitude, &new_fix.elevation)); fixes->push_back(new_fix); } } return result; } static bool parsePlacemark(xmlNode* current, GpsFixArray* fixes) { string description; string name; size_t ind = string::npos; // not worried about case-sensitivity since .kml files // are expected to be machine-generated for (; current != nullptr; current = current->next) { const bool hasContent = current->xmlChildrenNode && current->xmlChildrenNode->content; if (hasContent && !strcmp((const char*)current->name, "description")) { description = (const char*)current->xmlChildrenNode->content; } else if (hasContent && !strcmp((const char*)current->name, "name")) { name = (const char*)current->xmlChildrenNode->content; } else if (!strcmp((const char*)current->name, "Point") || !strcmp((const char*)current->name, "LineString") || !strcmp((const char*)current->name, "Polygon")) { ind = (ind != string::npos ? ind : fixes->size()); if (!parseCoordinates(current->xmlChildrenNode, fixes)) { return false; } } else if (current->ns && current->ns->prefix && !strcmp((const char*)current->ns->prefix, "gx") && !strcmp((const char*)current->name, "Track")) { ind = (ind != string::npos ? ind : fixes->size()); if (!parseGxTrack(current->xmlChildrenNode, fixes)) { return false; } } } if (ind == string::npos || ind >= fixes->size()) { return false; } // only assign name and description to the first of the // points to avoid needless repetition (*fixes)[ind].description = std::move(description); (*fixes)[ind].name = std::move(name); return true; } // Placemarks (aka locations) can be nested arbitrarily deep static bool traverseSubtree(xmlNode* current, GpsFixArray* fixes, string* error) { for (; current; current = current->next) { if (current->name != nullptr && !strcmp((const char*)current->name, "Placemark")) { if (!parsePlacemark(current->xmlChildrenNode, fixes)) { *error = "Location found with missing or malformed coordinates"; return false; } } else if (current->name != nullptr && strcmp((const char*)current->name, "text") != 0) { // if it's not a Placemark we must go deeper if (!traverseSubtree(current->xmlChildrenNode, fixes, error)) { return false; } } } error->clear(); return true; } bool KmlParser::parseFile(const char* filePath, GpsFixArray* fixes, string* error) { // This initializes the library and checks potential ABI mismatches between // the version it was compiled for and the actual shared library used. LIBXML_TEST_VERSION xmlDocPtr doc = xmlReadFile(filePath, nullptr, 0); if (doc == nullptr) { *error = "KML document not parsed successfully."; xmlFreeDoc(doc); return false; } xmlNodePtr cur = xmlDocGetRootElement(doc); if (cur == nullptr) { *error = "Could not get root element of parsed KML file."; xmlFreeDoc(doc); xmlCleanupParser(); return false; } bool isWellFormed = traverseSubtree(cur, fixes, error); xmlFreeDoc(doc); xmlCleanupParser(); return isWellFormed; } bool KmlParser::parseString(const char* str, int len, GpsFixArray* fixes, string* error) { // This initializes the library and checks potential ABI mismatches between // the version it was compiled for and the actual shared library used. LIBXML_TEST_VERSION xmlDocPtr doc = xmlReadMemory(str, len, NULL, NULL, 0); if (doc == nullptr) { *error = "KML document not parsed successfully."; xmlFreeDoc(doc); return false; } xmlNodePtr cur = xmlDocGetRootElement(doc); if (cur == nullptr) { *error = "Could not get root element of parsed KML file."; xmlFreeDoc(doc); xmlCleanupParser(); return false; } bool isWellFormed = traverseSubtree(cur, fixes, error); xmlFreeDoc(doc); xmlCleanupParser(); return isWellFormed; }