buffet: load standard GCD command definitions in CommandManager
Changed CommandManager to load the base command definitions
and use it as a base schema for loading device-specific commands
and make sure device vendors are not redefining standard commands
in breaking manner.
BUG=chromium:374861
TEST=USE=buffet P2_TEST_FILTER="buffet::*" FEATURES=test emerge-link platform2
Change-Id: I5f2d5500bc90ef918a8a6df1242bdd1fe3b78615
Reviewed-on: https://chromium-review.googlesource.com/209175
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/commands/command_dictionary.cc b/buffet/commands/command_dictionary.cc
index 38c8bce..dcba8c9 100644
--- a/buffet/commands/command_dictionary.cc
+++ b/buffet/commands/command_dictionary.cc
@@ -24,6 +24,7 @@
bool CommandDictionary::LoadCommands(const base::DictionaryValue& json,
const std::string& category,
+ const CommandDictionary* base_commands,
ErrorPtr* error) {
std::map<std::string, std::shared_ptr<const CommandDefinition>> new_defs;
@@ -32,29 +33,37 @@
// Iterate over packages
base::DictionaryValue::Iterator package_iter(json);
while (!package_iter.IsAtEnd()) {
- std::string package = package_iter.key();
+ std::string package_name = package_iter.key();
const base::DictionaryValue* package_value = nullptr;
if (!package_iter.value().GetAsDictionary(&package_value)) {
Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kTypeMismatch,
"Expecting an object for package '%s'",
- package.c_str());
+ package_name.c_str());
return false;
}
// Iterate over command definitions within the current package.
base::DictionaryValue::Iterator command_iter(*package_value);
while (!command_iter.IsAtEnd()) {
- std::string command = command_iter.key();
+ std::string command_name = command_iter.key();
+ if (command_name.empty()) {
+ Error::AddToPrintf(error, errors::commands::kDomain,
+ errors::commands::kInvalidCommandName,
+ "Unnamed command encountered in package '%s'",
+ package_name.c_str());
+ return false;
+ }
const base::DictionaryValue* command_value = nullptr;
if (!command_iter.value().GetAsDictionary(&command_value)) {
Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kTypeMismatch,
"Expecting an object for command '%s'",
- command.c_str());
+ command_name.c_str());
return false;
}
// Construct the compound command name as "pkg_name.cmd_name".
- std::string command_name = string_utils::Join('.', package, command);
+ std::string full_command_name = string_utils::Join('.', package_name,
+ command_name);
// Get the "parameters" definition of the command and read it into
// an object schema.
const base::DictionaryValue* command_schema_def = nullptr;
@@ -63,21 +72,45 @@
Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kPropertyMissing,
"Command definition '%s' is missing property '%s'",
- command_name.c_str(),
+ full_command_name.c_str(),
commands::attributes::kCommand_Parameters);
return false;
}
+
+ const ObjectSchema* base_def = nullptr;
+ if (base_commands) {
+ const CommandDefinition* cmd =
+ base_commands->FindCommand(full_command_name);
+ if (cmd)
+ base_def = cmd->GetParameters().get();
+
+ // If the base command dictionary was provided but the command was not
+ // found in it, this must be a custom (vendor) command. GCD spec states
+ // that all custom command names must begin with "_". Let's enforce
+ // this rule here.
+ if (!base_def) {
+ if (command_name.front() != '_') {
+ Error::AddToPrintf(error, errors::commands::kDomain,
+ errors::commands::kInvalidCommandName,
+ "The name of custom command '%s' in package '%s'"
+ " must start with '_'",
+ command_name.c_str(), package_name.c_str());
+ return false;
+ }
+ }
+ }
+
auto command_schema = std::make_shared<ObjectSchema>();
- if (!command_schema->FromJson(command_schema_def, nullptr, error)) {
+ if (!command_schema->FromJson(command_schema_def, base_def, error)) {
Error::AddToPrintf(error, errors::commands::kDomain,
errors::commands::kInvalidObjectSchema,
"Invalid definition for command '%s'",
- command_name.c_str());
+ full_command_name.c_str());
return false;
}
auto command_def = std::make_shared<CommandDefinition>(category,
command_schema);
- new_defs.insert(std::make_pair(command_name, command_def));
+ new_defs.insert(std::make_pair(full_command_name, command_def));
command_iter.Advance();
}
diff --git a/buffet/commands/command_dictionary.h b/buffet/commands/command_dictionary.h
index 11295c9..78f2cee 100644
--- a/buffet/commands/command_dictionary.h
+++ b/buffet/commands/command_dictionary.h
@@ -46,10 +46,14 @@
// When LoadCommands is called, all previous definitions of commands from the
// same category are removed, effectively replacing all the commands in the
// given category.
+ // Optional |base_commands| parameter specifies the definition of standard
+ // GCD commands for parameter schema validation. Can be set to nullptr if
+ // no validation is needed.
// Returns false on failure and |error| provides additional error information
// when provided.
bool LoadCommands(const base::DictionaryValue& json,
- const std::string& category, ErrorPtr* error);
+ const std::string& category,
+ const CommandDictionary* base_commands, ErrorPtr* error);
// Returns the number of command definitions in the dictionary.
size_t GetSize() const { return definitions_.size(); }
// Checks if the dictionary has no command definitions.
diff --git a/buffet/commands/command_dictionary_unittest.cc b/buffet/commands/command_dictionary_unittest.cc
index f345019..0aeab50 100644
--- a/buffet/commands/command_dictionary_unittest.cc
+++ b/buffet/commands/command_dictionary_unittest.cc
@@ -28,7 +28,7 @@
}
})");
buffet::CommandDictionary dict;
- EXPECT_TRUE(dict.LoadCommands(*json, "robotd", nullptr));
+ EXPECT_TRUE(dict.LoadCommands(*json, "robotd", nullptr, nullptr));
EXPECT_EQ(1, dict.GetSize());
EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
json = CreateDictionaryValue(R"({
@@ -41,7 +41,7 @@
}
}
})");
- EXPECT_TRUE(dict.LoadCommands(*json, "powerd", nullptr));
+ EXPECT_TRUE(dict.LoadCommands(*json, "powerd", nullptr, nullptr));
EXPECT_EQ(3, dict.GetSize());
EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
EXPECT_NE(nullptr, dict.FindCommand("base.reboot"));
@@ -57,7 +57,7 @@
// Command definition missing 'parameters' property.
auto json = CreateDictionaryValue("{'robot':{'jump':{}}}");
- EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+ EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &error));
EXPECT_EQ("parameter_missing", error->GetCode());
EXPECT_EQ("Command definition 'robot.jump' is missing property 'parameters'",
error->GetMessage());
@@ -65,33 +65,103 @@
// Command definition is not an object.
json = CreateDictionaryValue("{'robot':{'jump':0}}");
- EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+ EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &error));
EXPECT_EQ("type_mismatch", error->GetCode());
EXPECT_EQ("Expecting an object for command 'jump'", error->GetMessage());
error.reset();
// Package definition is not an object.
json = CreateDictionaryValue("{'robot':'blah'}");
- EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+ EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &error));
EXPECT_EQ("type_mismatch", error->GetCode());
EXPECT_EQ("Expecting an object for package 'robot'", error->GetMessage());
error.reset();
// Invalid command definition is not an object.
json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{'flip':0}}}}");
- EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+ EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &error));
EXPECT_EQ("invalid_object_schema", error->GetCode());
EXPECT_EQ("Invalid definition for command 'robot.jump'", error->GetMessage());
EXPECT_NE(nullptr, error->GetInnerError()); // Must have additional info.
error.reset();
+ // Empty command name.
+ json = CreateDictionaryValue("{'robot':{'':{'parameters':{'flip':0}}}}");
+ EXPECT_FALSE(dict.LoadCommands(*json, "robotd", nullptr, &error));
+ EXPECT_EQ("invalid_command_name", error->GetCode());
+ EXPECT_EQ("Unnamed command encountered in package 'robot'",
+ error->GetMessage());
+ error.reset();
+}
+
+TEST(CommandDictionary, LoadCommands_RedefineInDifferentCategory) {
// Redefine commands in different category.
- json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{}}}}");
- dict.Clear();
- dict.LoadCommands(*json, "category1", &error);
- EXPECT_FALSE(dict.LoadCommands(*json, "category2", &error));
+ buffet::CommandDictionary dict;
+ buffet::ErrorPtr error;
+ auto json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{}}}}");
+ dict.LoadCommands(*json, "category1", nullptr, &error);
+ EXPECT_FALSE(dict.LoadCommands(*json, "category2", nullptr, &error));
EXPECT_EQ("duplicate_command_definition", error->GetCode());
EXPECT_EQ("Definition for command 'robot.jump' overrides an earlier "
"definition in category 'category1'", error->GetMessage());
error.reset();
}
+
+TEST(CommandDictionary, LoadCommands_CustomCommandNaming) {
+ // Custom command must start with '_'.
+ buffet::CommandDictionary base_dict;
+ buffet::CommandDictionary dict;
+ buffet::ErrorPtr error;
+ auto json = CreateDictionaryValue(R"({
+ 'base': {
+ 'reboot': {
+ 'parameters': {'delay': 'integer'}
+ }
+ }
+ })");
+ base_dict.LoadCommands(*json, "", nullptr, &error);
+ EXPECT_TRUE(dict.LoadCommands(*json, "robotd", &base_dict, &error));
+ auto json2 = CreateDictionaryValue("{'base':{'jump':{'parameters':{}}}}");
+ EXPECT_FALSE(dict.LoadCommands(*json2, "robotd", &base_dict, &error));
+ EXPECT_EQ("invalid_command_name", error->GetCode());
+ EXPECT_EQ("The name of custom command 'jump' in package 'base' must start "
+ "with '_'", error->GetMessage());
+ error.reset();
+
+ // If the command starts with "_", then it's Ok.
+ json2 = CreateDictionaryValue("{'base':{'_jump':{'parameters':{}}}}");
+ EXPECT_TRUE(dict.LoadCommands(*json2, "robotd", &base_dict, nullptr));
+}
+
+TEST(CommandDictionary, LoadCommands_RedefineStdCommand) {
+ // Redefine commands parameter type.
+ buffet::CommandDictionary base_dict;
+ buffet::CommandDictionary dict;
+ buffet::ErrorPtr error;
+ auto json = CreateDictionaryValue(R"({
+ 'base': {
+ 'reboot': {
+ 'parameters': {'delay': 'integer'}
+ }
+ }
+ })");
+ base_dict.LoadCommands(*json, "", nullptr, &error);
+ auto json2 = CreateDictionaryValue(R"({
+ 'base': {
+ 'reboot': {
+ 'parameters': {'delay': 'string'}
+ }
+ }
+ })");
+ EXPECT_FALSE(dict.LoadCommands(*json2, "robotd", &base_dict, &error));
+ EXPECT_EQ("invalid_object_schema", error->GetCode());
+ EXPECT_EQ("Invalid definition for command 'base.reboot'",
+ error->GetMessage());
+ EXPECT_EQ("invalid_parameter_definition", error->GetInnerError()->GetCode());
+ EXPECT_EQ("Error in definition of property 'delay'",
+ error->GetInnerError()->GetMessage());
+ EXPECT_EQ("param_type_changed", error->GetFirstError()->GetCode());
+ EXPECT_EQ("Redefining a property of type integer as string",
+ error->GetFirstError()->GetMessage());
+ error.reset();
+}
diff --git a/buffet/commands/command_manager.cc b/buffet/commands/command_manager.cc
index fc04b2d..d7cf34c 100644
--- a/buffet/commands/command_manager.cc
+++ b/buffet/commands/command_manager.cc
@@ -4,10 +4,78 @@
#include "buffet/commands/command_manager.h"
+#include <base/file_util.h>
+#include <base/values.h>
+#include <base/json/json_reader.h>
+
+#include "buffet/commands/schema_constants.h"
+#include "buffet/error_codes.h"
+
namespace buffet {
const CommandDictionary& CommandManager::GetCommandDictionary() const {
return dictionary_;
}
+bool CommandManager::LoadBaseCommands(const base::DictionaryValue& json,
+ ErrorPtr* error) {
+ return base_dictionary_.LoadCommands(json, "", nullptr, error);
+}
+
+bool CommandManager::LoadBaseCommands(const base::FilePath& json_file_path,
+ ErrorPtr* error) {
+ std::unique_ptr<const base::DictionaryValue> json = LoadJsonDict(
+ json_file_path, error);
+ if (!json)
+ return false;
+ return LoadBaseCommands(*json, error);
+}
+
+bool CommandManager::LoadCommands(const base::DictionaryValue& json,
+ const std::string& category,
+ ErrorPtr* error) {
+ return dictionary_.LoadCommands(json, category, &base_dictionary_, error);
+}
+
+bool CommandManager::LoadCommands(const base::FilePath& json_file_path,
+ ErrorPtr* error) {
+ std::unique_ptr<const base::DictionaryValue> json = LoadJsonDict(
+ json_file_path, error);
+ if (!json)
+ return false;
+ std::string category = json_file_path.BaseName().RemoveExtension().value();
+ return LoadCommands(*json, category, error);
+}
+
+std::unique_ptr<const base::DictionaryValue> CommandManager::LoadJsonDict(
+ const base::FilePath& json_file_path, ErrorPtr* error) {
+ std::string json_string;
+ if (!base::ReadFileToString(json_file_path, &json_string)) {
+ Error::AddToPrintf(error, errors::file_system::kDomain,
+ errors::file_system::kFileReadError,
+ "Failed to read file '%s'",
+ json_file_path.value().c_str());
+ return std::unique_ptr<const base::DictionaryValue>();
+ }
+ std::string error_message;
+ base::Value* value = base::JSONReader::ReadAndReturnError(
+ json_string, base::JSON_PARSE_RFC, nullptr, &error_message);
+ if (!value) {
+ Error::AddToPrintf(error, errors::json::kDomain, errors::json::kParseError,
+ "Error parsing content of JSON file '%s': %s",
+ json_file_path.value().c_str(), error_message.c_str());
+ return std::unique_ptr<const base::DictionaryValue>();
+ }
+ const base::DictionaryValue* dict_value = nullptr;
+ if (!value->GetAsDictionary(&dict_value)) {
+ delete value;
+ Error::AddToPrintf(error, errors::json::kDomain,
+ errors::json::kObjectExpected,
+ "Content of file '%s' is not a JSON object",
+ json_file_path.value().c_str());
+ return std::unique_ptr<const base::DictionaryValue>();
+ }
+ return std::unique_ptr<const base::DictionaryValue>(dict_value);
+}
+
} // namespace buffet
diff --git a/buffet/commands/command_manager.h b/buffet/commands/command_manager.h
index 1700712..64037b8 100644
--- a/buffet/commands/command_manager.h
+++ b/buffet/commands/command_manager.h
@@ -5,7 +5,10 @@
#ifndef BUFFET_COMMANDS_COMMAND_MANAGER_H_
#define BUFFET_COMMANDS_COMMAND_MANAGER_H_
+#include <string>
+
#include <base/basictypes.h>
+#include <base/files/file_path.h>
#include "buffet/commands/command_dictionary.h"
@@ -21,7 +24,42 @@
// Get the command definitions for the device.
const CommandDictionary& GetCommandDictionary() const;
+ // Loads base/standard GCD command definitions.
+ // |json| is the full JSON schema of standard GCD commands. These commands
+ // are not necessarily supported by a particular device but rather
+ // all the standard commands defined by GCD standard for all known/supported
+ // device kinds.
+ // On success, returns true. Otherwise, |error| contains additional
+ // error information.
+ bool LoadBaseCommands(const base::DictionaryValue& json,
+ ErrorPtr* error);
+
+ // Same as the overload above, but takes a path to a json file to read
+ // the base command definitions from.
+ bool LoadBaseCommands(const base::FilePath& json_file_path,
+ ErrorPtr* error);
+
+ // Loads device command schema for particular category.
+ // See CommandDictionary::LoadCommands for detailed description of the
+ // parameters.
+ bool LoadCommands(const base::DictionaryValue& json,
+ const std::string& category, ErrorPtr* error);
+
+ // Same as the overload above, but takes a path to a json file to read
+ // the base command definitions from. Also, the command category is
+ // derived from file name (without extension). So, if the path points to
+ // "power_manager.json", the command category used will be "power_manager".
+ bool LoadCommands(const base::FilePath& json_file_path,
+ ErrorPtr* error);
+
private:
+ // Helper function to load a JSON file that is expected to be
+ // an object/dictionary. In case of error, returns empty unique ptr and fills
+ // in error details in |error|.
+ std::unique_ptr<const base::DictionaryValue> LoadJsonDict(
+ const base::FilePath& json_file_path, ErrorPtr* error);
+
+ CommandDictionary base_dictionary_; // Base/std command definitions/schemas.
CommandDictionary dictionary_; // Command definitions/schemas.
DISALLOW_COPY_AND_ASSIGN(CommandManager);
};
diff --git a/buffet/commands/command_manager_unittest.cc b/buffet/commands/command_manager_unittest.cc
index 1a0bfec..65241fe 100644
--- a/buffet/commands/command_manager_unittest.cc
+++ b/buffet/commands/command_manager_unittest.cc
@@ -2,11 +2,107 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <base/file_util.h>
+#include <base/json/json_writer.h>
#include <gtest/gtest.h>
#include "buffet/commands/command_manager.h"
+#include "buffet/commands/unittest_utils.h"
+
+using buffet::unittests::CreateDictionaryValue;
+
+static base::FilePath SaveJsonToTempFile(const base::DictionaryValue& dict) {
+ std::string json;
+ base::JSONWriter::Write(&dict, &json);
+ base::FilePath temp_file;
+ base::CreateTemporaryFile(&temp_file);
+ base::WriteFile(temp_file, json.data(), static_cast<int>(json.size()));
+ return temp_file;
+}
TEST(CommandManager, Empty) {
buffet::CommandManager manager;
EXPECT_TRUE(manager.GetCommandDictionary().IsEmpty());
}
+
+TEST(CommandManager, LoadBaseCommandsJSON) {
+ buffet::CommandManager manager;
+ auto json = CreateDictionaryValue(R"({
+ 'base': {
+ 'reboot': {
+ 'parameters': {'delay': 'integer'}
+ },
+ 'shutdown': {
+ 'parameters': {}
+ }
+ }
+ })");
+ EXPECT_TRUE(manager.LoadBaseCommands(*json, nullptr));
+}
+
+TEST(CommandManager, LoadBaseCommandsFile) {
+ buffet::CommandManager manager;
+ auto json = CreateDictionaryValue(R"({
+ 'base': {
+ 'reboot': {
+ 'parameters': {'delay': 'integer'}
+ },
+ 'shutdown': {
+ 'parameters': {}
+ }
+ }
+ })");
+ base::FilePath temp_file = SaveJsonToTempFile(*json);
+ EXPECT_TRUE(manager.LoadBaseCommands(temp_file, nullptr));
+ base::DeleteFile(temp_file, false);
+}
+
+TEST(CommandManager, LoadCommandsJSON) {
+ buffet::CommandManager manager;
+ auto json = CreateDictionaryValue(R"({
+ 'robot': {
+ '_jump': {
+ 'parameters': {'height': 'integer'}
+ },
+ '_speak': {
+ 'parameters': {'phrase': 'string'}
+ }
+ }
+ })");
+ EXPECT_TRUE(manager.LoadCommands(*json, "category", nullptr));
+}
+
+TEST(CommandManager, LoadCommandsFile) {
+ buffet::CommandManager manager;
+ // Load some standard command definitions first.
+ auto json = CreateDictionaryValue(R"({
+ 'base': {
+ 'reboot': {
+ 'parameters': {'delay': 'integer'}
+ },
+ 'shutdown': {
+ 'parameters': {}
+ }
+ }
+ })");
+ manager.LoadBaseCommands(*json, nullptr);
+ // Load device-supported commands.
+ json = CreateDictionaryValue(R"({
+ 'base': {
+ 'reboot': {
+ 'parameters': {'delay': 'integer'}
+ }
+ },
+ 'robot': {
+ '_jump': {
+ 'parameters': {'height': 'integer'}
+ }
+ }
+ })");
+ base::FilePath temp_file = SaveJsonToTempFile(*json);
+ EXPECT_TRUE(manager.LoadCommands(temp_file, nullptr));
+ base::DeleteFile(temp_file, false);
+ EXPECT_EQ(2, manager.GetCommandDictionary().GetSize());
+ EXPECT_NE(nullptr, manager.GetCommandDictionary().FindCommand("base.reboot"));
+ EXPECT_NE(nullptr, manager.GetCommandDictionary().FindCommand("robot._jump"));
+}
diff --git a/buffet/commands/schema_constants.cc b/buffet/commands/schema_constants.cc
index d1677c0..90c9faa 100644
--- a/buffet/commands/schema_constants.cc
+++ b/buffet/commands/schema_constants.cc
@@ -21,6 +21,7 @@
const char kUnknownProperty[] = "unexpected_parameter";
const char kInvalidObjectSchema[] = "invalid_object_schema";
const char kDuplicateCommandDef[] = "duplicate_command_definition";
+const char kInvalidCommandName[] = "invalid_command_name";
} // namespace commands
} // namespace errors
diff --git a/buffet/commands/schema_constants.h b/buffet/commands/schema_constants.h
index 6a508e0..903228e 100644
--- a/buffet/commands/schema_constants.h
+++ b/buffet/commands/schema_constants.h
@@ -24,6 +24,7 @@
extern const char kUnknownProperty[];
extern const char kInvalidObjectSchema[];
extern const char kDuplicateCommandDef[];
+extern const char kInvalidCommandName[];
} // namespace commands
} // namespace errors