chromeos-dbus-bindings: XML parser

Add an expat-based XML parser to parse an XML interface
description into a structure.

BUG=chromium:404505
TEST=New unit tests

Change-Id: Ibc8e2b50d7e33ff209158962db3f6f97b8cfac97
Reviewed-on: https://chromium-review.googlesource.com/213242
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Paul Stewart <pstew@chromium.org>
Tested-by: Paul Stewart <pstew@chromium.org>
diff --git a/chromeos-dbus-bindings/chromeos-dbus-bindings.gyp b/chromeos-dbus-bindings/chromeos-dbus-bindings.gyp
index fafc408..3633561 100644
--- a/chromeos-dbus-bindings/chromeos-dbus-bindings.gyp
+++ b/chromeos-dbus-bindings/chromeos-dbus-bindings.gyp
@@ -20,11 +20,55 @@
   },
   'targets': [
     {
+      'target_name': 'libchromeos-dbus-bindings',
+      'type': 'static_library',
+      'sources': [
+        'xml_interface_parser.cc',
+      ],
+      'variables': {
+        'exported_deps': [
+          'expat',
+        ],
+        'deps': ['<@(exported_deps)'],
+      },
+      'all_dependent_settings': {
+        'variables': {
+          'deps': [
+            '<@(exported_deps)',
+          ],
+        },
+      },
+      'link_settings': {
+        'variables': {
+          'deps': [
+            'expat',
+          ],
+        },
+      },
+    },
+    {
       'target_name': 'generate-chromeos-dbus-bindings',
       'type': 'executable',
+      'dependencies': ['libchromeos-dbus-bindings'],
       'sources': [
         'generate_chromeos_dbus_bindings.cc',
       ]
     },
   ],
+  'conditions': [
+    ['USE_test == 1', {
+      'targets': [
+        {
+          'target_name': 'chromeos_dbus_bindings_unittest',
+          'type': 'executable',
+          'dependencies': ['libchromeos-dbus-bindings'],
+          'includes': ['../../platform2/common-mk/common_test.gypi'],
+          'sources': [
+            'testrunner.cc',
+            'xml_interface_parser_unittest.cc',
+          ],
+        },
+      ],
+    }],
+  ],
 }
diff --git a/chromeos-dbus-bindings/generate_chromeos_dbus_bindings.cc b/chromeos-dbus-bindings/generate_chromeos_dbus_bindings.cc
index b7836ba..c043079 100644
--- a/chromeos-dbus-bindings/generate_chromeos_dbus_bindings.cc
+++ b/chromeos-dbus-bindings/generate_chromeos_dbus_bindings.cc
@@ -5,8 +5,11 @@
 #include <string>
 
 #include <base/command_line.h>
+#include <base/files/file_path.h>
 #include <base/logging.h>
 
+#include "chromeos-dbus-bindings/xml_interface_parser.h"
+
 namespace switches {
 
 static const char kHelp[] = "help";
@@ -35,5 +38,11 @@
 
   std::string input = cl->GetSwitchValueASCII(switches::kInput);
 
+  chromeos_dbus_bindings::XmlInterfaceParser parser;
+  if (!parser.ParseXmlInterfaceFile(base::FilePath(input))) {
+    LOG(ERROR) << "Failed to parse interface file.";
+    return 1;
+  }
+
   return 0;
 }
diff --git a/chromeos-dbus-bindings/interface.h b/chromeos-dbus-bindings/interface.h
new file mode 100644
index 0000000..04de002
--- /dev/null
+++ b/chromeos-dbus-bindings/interface.h
@@ -0,0 +1,57 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_DBUS_BINDINGS_INTERFACE_H_
+#define CHROMEOS_DBUS_BINDINGS_INTERFACE_H_
+
+#include <string>
+#include <vector>
+
+namespace chromeos_dbus_bindings {
+
+struct Interface {
+  struct Argument {
+    Argument(const std::string& name_in,
+             const std::string& type_in) : name(name_in), type(type_in) {}
+    std::string name;
+    std::string type;
+  };
+  struct Method {
+    Method(const std::string& name_in,
+           const std::vector<Argument>& input_arguments_in,
+           const std::vector<Argument>& output_arguments_in)
+        : name(name_in),
+          input_arguments(input_arguments_in),
+          output_arguments(output_arguments_in) {}
+    Method(const std::string& name_in,
+           const std::vector<Argument>& input_arguments_in)
+        : name(name_in),
+          input_arguments(input_arguments_in) {}
+    explicit Method(const std::string& name_in) : name(name_in) {}
+    std::string name;
+    std::vector<Argument> input_arguments;
+    std::vector<Argument> output_arguments;
+  };
+  struct Signal {
+    Signal(const std::string& name_in,
+           const std::vector<Argument>& arguments_in)
+        : name(name_in), arguments(arguments_in) {}
+    explicit Signal(const std::string& name_in) : name(name_in) {}
+    std::string name;
+    std::vector<Argument> arguments;
+  };
+
+  Interface() = default;
+  Interface(const std::string& name_in,
+            const std::vector<Method>& methods_in,
+            const std::vector<Signal>& signals_in)
+      : name(name_in), methods(methods_in), signals(signals_in) {}
+  std::string name;
+  std::vector<Method> methods;
+  std::vector<Signal> signals;
+};
+
+}  // namespace chromeos_dbus_bindings
+
+#endif  // CHROMEOS_DBUS_BINDINGS_INTERFACE_H_
diff --git a/chromeos-dbus-bindings/testrunner.cc b/chromeos-dbus-bindings/testrunner.cc
new file mode 100644
index 0000000..766c3f2
--- /dev/null
+++ b/chromeos-dbus-bindings/testrunner.cc
@@ -0,0 +1,16 @@
+// Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <base/at_exit.h>
+#include <base/command_line.h>
+#include <chromeos/syslog_logging.h>
+#include <gtest/gtest.h>
+
+int main(int argc, char** argv) {
+  base::AtExitManager exit_manager;
+  CommandLine::Init(argc, argv);
+  chromeos::InitLog(chromeos::kLogToStderr);
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/chromeos-dbus-bindings/xml_interface_parser.cc b/chromeos-dbus-bindings/xml_interface_parser.cc
new file mode 100644
index 0000000..d75c2a7
--- /dev/null
+++ b/chromeos-dbus-bindings/xml_interface_parser.cc
@@ -0,0 +1,194 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos-dbus-bindings/xml_interface_parser.h"
+
+#include <utility>
+
+#include <base/file_util.h>
+#include <base/files/file_path.h>
+#include <base/logging.h>
+#include <base/stl_util.h>
+
+using std::string;
+using std::vector;
+
+namespace chromeos_dbus_bindings {
+
+// static
+const char XmlInterfaceParser::kArgumentTag[] = "arg";
+const char XmlInterfaceParser::kInterfaceTag[] = "interface";
+const char XmlInterfaceParser::kMethodTag[] = "method";
+const char XmlInterfaceParser::kNodeTag[] = "node";
+const char XmlInterfaceParser::kSignalTag[] = "signal";
+const char XmlInterfaceParser::kNameAttribute[] = "name";
+const char XmlInterfaceParser::kTypeAttribute[] = "type";
+const char XmlInterfaceParser::kDirectionAttribute[] = "direction";
+const char XmlInterfaceParser::kArgumentDirectionIn[] = "in";
+const char XmlInterfaceParser::kArgumentDirectionOut[] = "out";
+
+bool XmlInterfaceParser::ParseXmlInterfaceFile(
+    const base::FilePath& interface_file) {
+  string contents;
+  if (!base::ReadFileToString(interface_file, &contents)) {
+    LOG(ERROR) << "Failed to read file " << interface_file.value();
+    return false;
+  }
+  auto parser = XML_ParserCreate(nullptr);
+  XML_SetUserData(parser, this);
+  XML_SetElementHandler(parser,
+                        &XmlInterfaceParser::HandleElementStart,
+                        &XmlInterfaceParser::HandleElementEnd);
+  const int kIsFinal = XML_TRUE;
+
+  element_path_.clear();
+  XML_Status res = XML_Parse(parser,
+                             contents.c_str(),
+                             contents.size(),
+                             kIsFinal);
+  XML_ParserFree(parser);
+
+  if (res != XML_STATUS_OK) {
+    LOG(ERROR) << "XML parse failure";
+    return false;
+  }
+
+  CHECK(element_path_.empty());
+  return true;
+}
+
+void XmlInterfaceParser::OnOpenElement(
+    const string& element_name, const XmlAttributeMap& attributes) {
+  element_path_.push_back(element_name);
+  if (element_path_ == vector<string> { kNodeTag, kInterfaceTag }) {
+    string interface_name = GetValidatedElementName(attributes, kInterfaceTag);
+    CHECK(interface_.name.empty())
+        << "Found a second interface named " << interface_name << ". "
+        << "Interface " << interface_.name << " has already been parsed.";
+    interface_.name = interface_name;
+  } else if (element_path_ == vector<string> {
+                 kNodeTag, kInterfaceTag, kMethodTag }) {
+    interface_.methods.push_back(
+        Interface::Method(GetValidatedElementName(attributes, kMethodTag)));
+  } else if (element_path_ == vector<string> {
+                 kNodeTag, kInterfaceTag, kMethodTag, kArgumentTag }) {
+    AddMethodArgument(attributes);
+  } else if (element_path_ == vector<string> {
+                 kNodeTag, kInterfaceTag, kSignalTag }) {
+    interface_.signals.push_back(
+        Interface::Signal(GetValidatedElementName(attributes, kSignalTag)));
+  } else if (element_path_ == vector<string> {
+                 kNodeTag, kInterfaceTag, kSignalTag, kArgumentTag }) {
+    AddSignalArgument(attributes);
+  }
+}
+
+void XmlInterfaceParser::AddMethodArgument(const XmlAttributeMap& attributes) {
+  CHECK(!interface_.methods.empty())
+      << " we have a method argument but the interface has no methods";
+  const string& argument_direction = GetValidatedElementAttribute(
+      attributes, kArgumentTag, kDirectionAttribute);
+  vector<Interface::Argument>* argument_list;
+  if (argument_direction == kArgumentDirectionIn) {
+    argument_list = &interface_.methods.back().input_arguments;
+  } else if (argument_direction == kArgumentDirectionOut) {
+    argument_list = &interface_.methods.back().output_arguments;
+  } else {
+    LOG(FATAL) << "Unknown method argument direction " << argument_direction;
+  }
+  argument_list->push_back(ParseArgument(attributes, kMethodTag));
+}
+
+void XmlInterfaceParser::AddSignalArgument(const XmlAttributeMap& attributes) {
+  CHECK(interface_.signals.size())
+      << " we have a signal argument but the interface has no signals";
+  interface_.signals.back().arguments.push_back(
+      ParseArgument(attributes, kSignalTag));
+}
+
+void XmlInterfaceParser::OnCloseElement(const string& element_name) {
+  LOG(INFO) << "Close Element " << element_name;
+  CHECK(!element_path_.empty());
+  CHECK_EQ(element_path_.back(), element_name);
+  element_path_.pop_back();
+}
+
+// static
+bool XmlInterfaceParser::GetElementAttribute(
+    const XmlAttributeMap& attributes,
+    const string& element_type,
+    const string& element_key,
+    string* element_value) {
+  if (!ContainsKey(attributes, element_key)) {
+    return false;
+  }
+  *element_value = attributes.find(element_key)->second;
+  LOG(INFO) << "Got " << element_type << " element with "
+            << element_key << " = " << *element_value;
+  return true;
+}
+
+// static
+string XmlInterfaceParser::GetValidatedElementAttribute(
+    const XmlAttributeMap& attributes,
+    const string& element_type,
+    const string& element_key) {
+  string element_value;
+  CHECK(GetElementAttribute(attributes,
+                            element_type,
+                            element_key,
+                            &element_value))
+      << element_type << " does not contain a " << element_key << " attribute";
+  CHECK(!element_value.empty()) << element_type << " " << element_key
+                              << " attribute is empty";
+  return element_value;
+}
+
+// static
+string XmlInterfaceParser::GetValidatedElementName(
+    const XmlAttributeMap& attributes,
+    const string& element_type) {
+  return GetValidatedElementAttribute(attributes, element_type, kNameAttribute);
+}
+
+// static
+Interface::Argument XmlInterfaceParser::ParseArgument(
+    const XmlAttributeMap& attributes, const string& element_type) {
+  string element_and_argument = element_type + " " + kArgumentTag;
+  string argument_name;
+  // Since the "name" field is optional, use the un-validated variant.
+  GetElementAttribute(attributes,
+                      element_and_argument,
+                      kNameAttribute,
+                      &argument_name);
+
+  string argument_type = GetValidatedElementAttribute(
+      attributes, element_and_argument, kTypeAttribute);
+  return Interface::Argument(argument_name, argument_type);
+}
+
+// static
+void XmlInterfaceParser::HandleElementStart(void* user_data,
+                                            const XML_Char* element,
+                                            const XML_Char** attr) {
+  XmlAttributeMap attributes;
+  if (attr != nullptr) {
+    for (size_t n = 0; attr[n] != nullptr && attr[n+1] != nullptr; n += 2) {
+      auto key = attr[n];
+      auto value = attr[n + 1];
+      attributes.insert(std::make_pair(key, value));
+    }
+  }
+  auto parser = reinterpret_cast<XmlInterfaceParser*>(user_data);
+  parser->OnOpenElement(element, attributes);
+}
+
+// static
+void XmlInterfaceParser::HandleElementEnd(void* user_data,
+                                          const XML_Char* element) {
+  auto parser = reinterpret_cast<XmlInterfaceParser*>(user_data);
+  parser->OnCloseElement(element);
+}
+
+}  // namespace chromeos_dbus_bindings
diff --git a/chromeos-dbus-bindings/xml_interface_parser.h b/chromeos-dbus-bindings/xml_interface_parser.h
new file mode 100644
index 0000000..84c7dd8
--- /dev/null
+++ b/chromeos-dbus-bindings/xml_interface_parser.h
@@ -0,0 +1,105 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_DBUS_BINDINGS_XML_INTERFACE_PARSER_H_
+#define CHROMEOS_DBUS_BINDINGS_XML_INTERFACE_PARSER_H_
+
+#include <expat.h>
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <base/macros.h>
+
+#include "chromeos-dbus-bindings/interface.h"
+
+namespace base {
+
+class FilePath;
+
+}  // namespace base
+
+namespace chromeos_dbus_bindings {
+
+class XmlInterfaceParser {
+ public:
+  using XmlAttributeMap = std::map<std::string, std::string>;
+
+  XmlInterfaceParser() = default;
+  virtual ~XmlInterfaceParser() = default;
+
+  virtual bool ParseXmlInterfaceFile(const base::FilePath& interface_file);
+  const Interface& interface() const { return interface_; }
+
+ private:
+  friend class XmlInterfaceParserTest;
+
+  // XML tag names.
+  static const char kArgumentTag[];
+  static const char kInterfaceTag[];
+  static const char kMethodTag[];
+  static const char kNodeTag[];
+  static const char kSignalTag[];
+
+  // XML attribute names.
+  static const char kNameAttribute[];
+  static const char kTypeAttribute[];
+  static const char kDirectionAttribute[];
+
+  // XML argument directions.
+  static const char kArgumentDirectionIn[];
+  static const char kArgumentDirectionOut[];
+
+  // Element callbacks on |this| called by HandleElementStart() and
+  // HandleElementEnd(), respectively.
+  void OnOpenElement(const std::string& element_name,
+                     const XmlAttributeMap& attributes);
+  void OnCloseElement(const std::string& element_name);
+
+  // Methods for appending individual argument elements to the parser.
+  void AddMethodArgument(const XmlAttributeMap& attributes);
+  void AddSignalArgument(const XmlAttributeMap& attributes);
+
+  // Finds the |element_key| element in |attributes|.  Returns true and sets
+  // |element_value| on success.  Returns false otherwise.
+  static bool GetElementAttribute(const XmlAttributeMap& attributes,
+                                  const std::string& element_type,
+                                  const std::string& element_key,
+                                  std::string* element_value);
+
+  // Asserts that a non-empty |element_key| attribute appears in |attributes|.
+  // Returns the name on success, triggers a CHECK() otherwise.
+  static std::string GetValidatedElementAttribute(
+      const XmlAttributeMap& attributes,
+      const std::string& element_type,
+      const std::string& element_key);
+
+  // Calls GetValidatedElementAttribute() for for the "name" property.
+  static std::string GetValidatedElementName(
+      const XmlAttributeMap& attributes,
+      const std::string& element_type);
+
+  // Method for extracting signal/method tag attributes to a struct.
+  static Interface::Argument ParseArgument(const XmlAttributeMap& attributes,
+                                           const std::string& element_type);
+
+  // Expat element callback functions.
+  static void HandleElementStart(void* user_data,
+                                 const XML_Char* element,
+                                 const XML_Char** attr);
+  static void HandleElementEnd(void* user_data, const XML_Char* element);
+
+  // The output of the parse.
+  Interface interface_;
+
+  // Tracks where in the element traversal our parse has taken us.
+  std::vector<std::string> element_path_;
+
+  DISALLOW_COPY_AND_ASSIGN(XmlInterfaceParser);
+};
+
+}  // namespace chromeos_dbus_bindings
+
+#endif  // CHROMEOS_DBUS_BINDINGS_XML_INTERFACE_PARSER_H_
diff --git a/chromeos-dbus-bindings/xml_interface_parser_unittest.cc b/chromeos-dbus-bindings/xml_interface_parser_unittest.cc
new file mode 100644
index 0000000..4add88b
--- /dev/null
+++ b/chromeos-dbus-bindings/xml_interface_parser_unittest.cc
@@ -0,0 +1,115 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos-dbus-bindings/xml_interface_parser.h"
+
+#include <base/file_util.h>
+#include <base/files/file_path.h>
+#include <base/files/scoped_temp_dir.h>
+#include <gtest/gtest.h>
+
+#include "chromeos-dbus-bindings/interface.h"
+
+using std::string;
+using testing::Test;
+
+namespace chromeos_dbus_bindings {
+
+namespace {
+
+const char kBadInterfaceFileContents0[] = "This has no resemblance to XML";
+const char kBadInterfaceFileContents1[] = "<node>";
+const char kGoodInterfaceFileContents[] =
+    "<node>\n"
+    "  <interface name=\"fi.w1.wpa_supplicant1.Interface\">\n"
+    "    <method name=\"Scan\">\n"
+    "      <arg name=\"args\" type=\"a{sv}\" direction=\"in\"/>\n"
+    "    </method>\n"
+    "    <method name=\"GetBlob\">\n"
+    "      <arg name=\"name\" type=\"s\" direction=\"in\"/>\n"
+    "      <arg name=\"data\" type=\"ay\" direction=\"out\"/>\n"
+    "    </method>\n"
+    "    <property name=\"Capabilities\" type=\"a{sv}\" access=\"read\"/>\n"
+    "    <signal name=\"BSSRemoved\">\n"
+    "      <arg name=\"BSS\" type=\"o\"/>\n"
+    "    </signal>\n"
+    "  </interface>\n"
+    "</node>\n";
+const char kInterfaceName[] = "fi.w1.wpa_supplicant1.Interface";
+const char kScanMethod[] = "Scan";
+const char kArgsArgument[] = "args";
+const char kArrayStringVariantType[] = "a{sv}";
+const char kGetBlobMethod[] = "GetBlob";
+const char kNameArgument[] = "name";
+const char kDataArgument[] = "data";
+const char kStringType[] = "s";
+const char kArrayByteType[] = "ay";
+const char kBssRemovedSignal[] = "BSSRemoved";
+const char kBssArgument[] = "BSS";
+const char kObjectType[] = "o";
+}  // namespace
+
+class XmlInterfaceParserTest : public Test {
+ public:
+  void SetUp() override {
+    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+  }
+
+ protected:
+  bool ParseXmlContents(const string& contents) {
+    base::FilePath path = temp_dir_.path().Append("interface.xml");
+    EXPECT_TRUE(base::WriteFile(path, contents.c_str(), contents.size()));
+    return parser_.ParseXmlInterfaceFile(path);
+  }
+
+  base::ScopedTempDir temp_dir_;
+  XmlInterfaceParser parser_;
+};
+
+TEST_F(XmlInterfaceParserTest, BadInputFile) {
+  EXPECT_FALSE(parser_.ParseXmlInterfaceFile(base::FilePath()));
+  EXPECT_FALSE(ParseXmlContents(kBadInterfaceFileContents0));
+  EXPECT_FALSE(ParseXmlContents(kBadInterfaceFileContents1));
+}
+
+TEST_F(XmlInterfaceParserTest, GoodInputFile) {
+  EXPECT_TRUE(ParseXmlContents(kGoodInterfaceFileContents));
+  const Interface& interface = parser_.interface();
+  EXPECT_EQ(kInterfaceName, interface.name);
+  ASSERT_EQ(2, interface.methods.size());
+  ASSERT_EQ(1, interface.signals.size());
+
+  // <method name="Scan">
+  EXPECT_EQ(kScanMethod, interface.methods[0].name);
+  ASSERT_EQ(1, interface.methods[0].input_arguments.size());
+
+  // <arg name="args" type="a{sv}" direction="in"/>
+  EXPECT_EQ(kArgsArgument, interface.methods[0].input_arguments[0].name);
+  EXPECT_EQ(kArrayStringVariantType,
+            interface.methods[0].input_arguments[0].type);
+  EXPECT_EQ(0, interface.methods[0].output_arguments.size());
+
+  // <method name="GetBlob">
+  EXPECT_EQ(kGetBlobMethod, interface.methods[1].name);
+  EXPECT_EQ(1, interface.methods[1].input_arguments.size());
+  EXPECT_EQ(1, interface.methods[1].output_arguments.size());
+
+  // <arg name="name" type="s" direction="in"/>
+  EXPECT_EQ(kNameArgument, interface.methods[1].input_arguments[0].name);
+  EXPECT_EQ(kStringType, interface.methods[1].input_arguments[0].type);
+
+  // <arg name="data" type="ay" direction="out"/>
+  EXPECT_EQ(kDataArgument, interface.methods[1].output_arguments[0].name);
+  EXPECT_EQ(kArrayByteType, interface.methods[1].output_arguments[0].type);
+
+  // <signal name="BSSRemoved">
+  EXPECT_EQ(kBssRemovedSignal, interface.signals[0].name);
+  EXPECT_EQ(1, interface.signals[0].arguments.size());
+
+  // <arg name="BSS" type="o"/>
+  EXPECT_EQ(kBssArgument, interface.signals[0].arguments[0].name);
+  EXPECT_EQ(kObjectType, interface.signals[0].arguments[0].type);
+}
+
+}  // namespace chromeos_dbus_bindings