Parse multiple <app> tag.
SoM and product will have different app id in Android Things, and in
Omaha response they will be in separate <app> tag.
Each <app> will have its own <urls>, <packages>, <actiions>, etc.
Bug: 36252799
Test: update_engine_unittest
Change-Id: I47f46155a7362dba2a3010b4372a8f254c78fb30
(cherry picked from commit 558084b960a1b0a81e43395c12400c54a4b0c3c0)
diff --git a/omaha_request_action.cc b/omaha_request_action.cc
index f3948ce..3f9f7c4 100644
--- a/omaha_request_action.cc
+++ b/omaha_request_action.cc
@@ -379,21 +379,25 @@
bool app_cohort_set = false;
bool app_cohorthint_set = false;
bool app_cohortname_set = false;
- string updatecheck_status;
string updatecheck_poll_interval;
map<string, string> updatecheck_attrs;
string daystart_elapsed_days;
string daystart_elapsed_seconds;
- vector<string> url_codebase;
- string manifest_version;
- map<string, string> action_postinstall_attrs;
- struct Package {
- string name;
- string size;
- string hash;
+ struct App {
+ vector<string> url_codebase;
+ string manifest_version;
+ map<string, string> action_postinstall_attrs;
+ string updatecheck_status;
+
+ struct Package {
+ string name;
+ string size;
+ string hash;
+ };
+ vector<Package> packages;
};
- vector<Package> packages;
+ vector<App> apps;
};
namespace {
@@ -418,6 +422,7 @@
}
if (data->current_path == "/response/app") {
+ data->apps.emplace_back();
if (attrs.find("cohort") != attrs.end()) {
data->app_cohort_set = true;
data->app_cohort = attrs["cohort"];
@@ -431,9 +436,10 @@
data->app_cohortname = attrs["cohortname"];
}
} else if (data->current_path == "/response/app/updatecheck") {
- // There is only supposed to be a single <updatecheck> element.
- data->updatecheck_status = attrs["status"];
- data->updatecheck_poll_interval = attrs["PollInterval"];
+ if (!data->apps.empty())
+ data->apps.back().updatecheck_status = attrs["status"];
+ if (data->updatecheck_poll_interval.empty())
+ data->updatecheck_poll_interval = attrs["PollInterval"];
// Omaha sends arbitrary key-value pairs as extra attributes starting with
// an underscore.
for (const auto& attr : attrs) {
@@ -446,21 +452,24 @@
data->daystart_elapsed_seconds = attrs["elapsed_seconds"];
} else if (data->current_path == "/response/app/updatecheck/urls/url") {
// Look at all <url> elements.
- data->url_codebase.push_back(attrs["codebase"]);
+ if (!data->apps.empty())
+ data->apps.back().url_codebase.push_back(attrs["codebase"]);
} else if (data->current_path ==
"/response/app/updatecheck/manifest/packages/package") {
// Look at all <package> elements.
- data->packages.push_back({.name = attrs["name"],
- .size = attrs["size"],
- .hash = attrs["hash_sha256"]});
+ if (!data->apps.empty())
+ data->apps.back().packages.push_back({.name = attrs["name"],
+ .size = attrs["size"],
+ .hash = attrs["hash_sha256"]});
} else if (data->current_path == "/response/app/updatecheck/manifest") {
// Get the version.
- data->manifest_version = attrs[kTagVersion];
+ if (!data->apps.empty())
+ data->apps.back().manifest_version = attrs[kTagVersion];
} else if (data->current_path ==
"/response/app/updatecheck/manifest/actions/action") {
// We only care about the postinstall action.
- if (attrs["event"] == "postinstall") {
- data->action_postinstall_attrs = attrs;
+ if (attrs["event"] == "postinstall" && !data->apps.empty()) {
+ data->apps.back().action_postinstall_attrs = std::move(attrs);
}
}
}
@@ -759,15 +768,102 @@
prefs->SetInt64(kPrefsLastRollCallPingDay, daystart.ToInternalValue());
return true;
}
+
+// Parses the package node in the given XML document and populates
+// |output_object| if valid. Returns true if we should continue the parsing.
+// False otherwise, in which case it sets any error code using |completer|.
+bool ParsePackage(OmahaParserData::App* app,
+ OmahaResponse* output_object,
+ ScopedActionCompleter* completer) {
+ if (app->updatecheck_status == "noupdate") {
+ if (!app->packages.empty()) {
+ LOG(ERROR) << "No update in this <app> but <package> is not empty.";
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ return true;
+ }
+ if (app->packages.empty()) {
+ LOG(ERROR) << "Omaha Response has no packages";
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ if (app->url_codebase.empty()) {
+ LOG(ERROR) << "No Omaha Response URLs";
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ LOG(INFO) << "Found " << app->url_codebase.size() << " url(s)";
+ vector<string> metadata_sizes =
+ base::SplitString(app->action_postinstall_attrs[kTagMetadataSize],
+ ":",
+ base::TRIM_WHITESPACE,
+ base::SPLIT_WANT_ALL);
+ vector<string> metadata_signatures =
+ base::SplitString(app->action_postinstall_attrs[kTagMetadataSignatureRsa],
+ ":",
+ base::TRIM_WHITESPACE,
+ base::SPLIT_WANT_ALL);
+ for (size_t i = 0; i < app->packages.size(); i++) {
+ const auto& package = app->packages[i];
+ if (package.name.empty()) {
+ LOG(ERROR) << "Omaha Response has empty package name";
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ LOG(INFO) << "Found package " << package.name;
+
+ OmahaResponse::Package out_package;
+ for (const string& codebase : app->url_codebase) {
+ if (codebase.empty()) {
+ LOG(ERROR) << "Omaha Response URL has empty codebase";
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ out_package.payload_urls.push_back(codebase + package.name);
+ }
+ // Parse the payload size.
+ base::StringToUint64(package.size, &out_package.size);
+ if (out_package.size <= 0) {
+ LOG(ERROR) << "Omaha Response has invalid payload size: " << package.size;
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ LOG(INFO) << "Payload size = " << out_package.size << " bytes";
+
+ if (i < metadata_sizes.size())
+ base::StringToUint64(metadata_sizes[i], &out_package.metadata_size);
+ LOG(INFO) << "Payload metadata size = " << out_package.metadata_size
+ << " bytes";
+
+ if (i < metadata_signatures.size())
+ out_package.metadata_signature = metadata_signatures[i];
+ LOG(INFO) << "Payload metadata signature = "
+ << out_package.metadata_signature;
+
+ out_package.hash = package.hash;
+ if (out_package.hash.empty()) {
+ LOG(ERROR) << "Omaha Response has empty hash_sha256 value";
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ LOG(INFO) << "Payload hash = " << out_package.hash;
+ output_object->packages.push_back(std::move(out_package));
+ }
+
+ return true;
+}
+
} // namespace
bool OmahaRequestAction::ParseResponse(OmahaParserData* parser_data,
OmahaResponse* output_object,
ScopedActionCompleter* completer) {
- if (parser_data->updatecheck_status.empty()) {
+ if (parser_data->apps.empty()) {
completer->set_code(ErrorCode::kOmahaResponseInvalid);
return false;
}
+ LOG(INFO) << "Found " << parser_data->apps.size() << " <app>.";
// chromium-os:37289: The PollInterval is not supported by Omaha server
// currently. But still keeping this existing code in case we ever decide to
@@ -824,8 +920,9 @@
// Package has to be parsed after Params now because ParseParams need to make
// sure that postinstall action exists.
- if (!ParsePackage(parser_data, output_object, completer))
- return false;
+ for (auto& app : parser_data->apps)
+ if (!ParsePackage(&app, output_object, completer))
+ return false;
return true;
}
@@ -833,104 +930,37 @@
bool OmahaRequestAction::ParseStatus(OmahaParserData* parser_data,
OmahaResponse* output_object,
ScopedActionCompleter* completer) {
- const string& status = parser_data->updatecheck_status;
- if (status == "noupdate") {
- LOG(INFO) << "No update.";
- output_object->update_exists = false;
+ output_object->update_exists = false;
+ for (size_t i = 0; i < parser_data->apps.size(); i++) {
+ const string& status = parser_data->apps[i].updatecheck_status;
+ if (status == "noupdate") {
+ LOG(INFO) << "No update for <app> " << i;
+ } else if (status == "ok") {
+ output_object->update_exists = true;
+ } else {
+ LOG(ERROR) << "Unknown Omaha response status: " << status;
+ completer->set_code(ErrorCode::kOmahaResponseInvalid);
+ return false;
+ }
+ }
+ if (!output_object->update_exists) {
SetOutputObject(*output_object);
completer->set_code(ErrorCode::kSuccess);
- return false;
}
- if (status != "ok") {
- LOG(ERROR) << "Unknown Omaha response status: " << status;
- completer->set_code(ErrorCode::kOmahaResponseInvalid);
- return false;
- }
-
- return true;
-}
-
-bool OmahaRequestAction::ParsePackage(OmahaParserData* parser_data,
- OmahaResponse* output_object,
- ScopedActionCompleter* completer) {
- if (parser_data->packages.empty()) {
- LOG(ERROR) << "Omaha Response has no packages";
- completer->set_code(ErrorCode::kOmahaResponseInvalid);
- return false;
- }
- if (parser_data->url_codebase.empty()) {
- LOG(ERROR) << "No Omaha Response URLs";
- completer->set_code(ErrorCode::kOmahaResponseInvalid);
- return false;
- }
- LOG(INFO) << "Found " << parser_data->url_codebase.size() << " url(s)";
-
- vector<string> metadata_sizes =
- base::SplitString(parser_data->action_postinstall_attrs[kTagMetadataSize],
- ":",
- base::TRIM_WHITESPACE,
- base::SPLIT_WANT_ALL);
- vector<string> metadata_signatures = base::SplitString(
- parser_data->action_postinstall_attrs[kTagMetadataSignatureRsa],
- ":",
- base::TRIM_WHITESPACE,
- base::SPLIT_WANT_ALL);
-
- for (size_t i = 0; i < parser_data->packages.size(); i++) {
- const auto& package = parser_data->packages[i];
- if (package.name.empty()) {
- LOG(ERROR) << "Omaha Response has empty package name";
- completer->set_code(ErrorCode::kOmahaResponseInvalid);
- return false;
- }
- LOG(INFO) << "Found package " << package.name;
-
- OmahaResponse::Package out_package;
- for (const string& codebase : parser_data->url_codebase) {
- if (codebase.empty()) {
- LOG(ERROR) << "Omaha Response URL has empty codebase";
- completer->set_code(ErrorCode::kOmahaResponseInvalid);
- return false;
- }
- out_package.payload_urls.push_back(codebase + package.name);
- }
- // Parse the payload size.
- base::StringToUint64(package.size, &out_package.size);
- if (out_package.size <= 0) {
- LOG(ERROR) << "Omaha Response has invalid payload size: " << package.size;
- completer->set_code(ErrorCode::kOmahaResponseInvalid);
- return false;
- }
- LOG(INFO) << "Payload size = " << out_package.size << " bytes";
-
- if (i < metadata_sizes.size())
- base::StringToUint64(metadata_sizes[i], &out_package.metadata_size);
- LOG(INFO) << "Payload metadata size = " << out_package.metadata_size
- << " bytes";
-
- if (i < metadata_signatures.size())
- out_package.metadata_signature = metadata_signatures[i];
- LOG(INFO) << "Payload metadata signature = "
- << out_package.metadata_signature;
-
- out_package.hash = package.hash;
- if (out_package.hash.empty()) {
- LOG(ERROR) << "Omaha Response has empty hash_sha256 value";
- completer->set_code(ErrorCode::kOmahaResponseInvalid);
- return false;
- }
- LOG(INFO) << "Payload hash = " << out_package.hash;
- output_object->packages.push_back(std::move(out_package));
- }
-
- return true;
+ return output_object->update_exists;
}
bool OmahaRequestAction::ParseParams(OmahaParserData* parser_data,
OmahaResponse* output_object,
ScopedActionCompleter* completer) {
- output_object->version = parser_data->manifest_version;
+ map<string, string> attrs;
+ for (auto& app : parser_data->apps) {
+ if (!app.manifest_version.empty() && output_object->version.empty())
+ output_object->version = app.manifest_version;
+ if (!app.action_postinstall_attrs.empty() && attrs.empty())
+ attrs = app.action_postinstall_attrs;
+ }
if (output_object->version.empty()) {
LOG(ERROR) << "Omaha Response does not have version in manifest!";
completer->set_code(ErrorCode::kOmahaResponseInvalid);
@@ -940,7 +970,6 @@
LOG(INFO) << "Received omaha response to update to version "
<< output_object->version;
- map<string, string> attrs = parser_data->action_postinstall_attrs;
if (attrs.empty()) {
LOG(ERROR) << "Omaha Response has no postinstall event action";
completer->set_code(ErrorCode::kOmahaResponseInvalid);
diff --git a/omaha_request_action.h b/omaha_request_action.h
index 2915a6a..924da40 100644
--- a/omaha_request_action.h
+++ b/omaha_request_action.h
@@ -274,13 +274,6 @@
OmahaResponse* output_object,
ScopedActionCompleter* completer);
- // Parses the package node in the given XML document and populates
- // |output_object| if valid. Returns true if we should continue the parsing.
- // False otherwise, in which case it sets any error code using |completer|.
- bool ParsePackage(OmahaParserData* parser_data,
- OmahaResponse* output_object,
- ScopedActionCompleter* completer);
-
// Parses the other parameters in the given XML document and populates
// |output_object| if valid. Returns true if we should continue the parsing.
// False otherwise, in which case it sets any error code using |completer|.
diff --git a/omaha_request_action_unittest.cc b/omaha_request_action_unittest.cc
index 3ee6769..6d56603 100644
--- a/omaha_request_action_unittest.cc
+++ b/omaha_request_action_unittest.cc
@@ -76,16 +76,22 @@
string entity_str;
if (include_entity)
entity_str = "<!DOCTYPE response [<!ENTITY CrOS \"ChromeOS\">]>";
- return
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
- entity_str + "<response protocol=\"3.0\">"
- "<daystart elapsed_seconds=\"100\"/>"
- "<app appid=\"" + app_id + "\" " +
- (include_cohorts ? "cohort=\"" + cohort + "\" cohorthint=\"" +
- cohorthint + "\" cohortname=\"" + cohortname + "\" " : "") +
- " status=\"ok\">"
- "<ping status=\"ok\"/>"
- "<updatecheck status=\"noupdate\"/></app></response>";
+ return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + entity_str +
+ "<response protocol=\"3.0\">"
+ "<daystart elapsed_seconds=\"100\"/>"
+ "<app appid=\"" +
+ app_id + "\" " +
+ (include_cohorts
+ ? "cohort=\"" + cohort + "\" cohorthint=\"" + cohorthint +
+ "\" cohortname=\"" + cohortname + "\" "
+ : "") +
+ " status=\"ok\">"
+ "<ping status=\"ok\"/>"
+ "<updatecheck status=\"noupdate\"/></app>" +
+ (multi_app_no_update
+ ? "<app><updatecheck status=\"noupdate\"/></app>"
+ : "") +
+ "</response>";
}
string GetUpdateResponse() const {
@@ -116,10 +122,9 @@
"hash_sha256=\"hash2\"/>"
: "") +
"</packages>"
- "<actions><action event=\"postinstall\" "
- "ChromeOSVersion=\"" +
- version + "\" MoreInfo=\"" + more_info_url + "\" Prompt=\"" +
- prompt +
+ "<actions><action event=\"postinstall\" MetadataSize=\"11" +
+ (multi_package ? ":22" : "") + "\" ChromeOSVersion=\"" + version +
+ "\" MoreInfo=\"" + more_info_url + "\" Prompt=\"" + prompt +
"\" "
"IsDelta=\"true\" "
"IsDeltaPayload=\"true\" "
@@ -133,7 +138,21 @@
(disable_p2p_for_downloading ? "DisableP2PForDownloading=\"true\" "
: "") +
(disable_p2p_for_sharing ? "DisableP2PForSharing=\"true\" " : "") +
- "/></actions></manifest></updatecheck></app></response>";
+ "/></actions></manifest></updatecheck></app>" +
+ (multi_app
+ ? "<app><updatecheck status=\"ok\"><urls><url codebase=\"" +
+ codebase2 +
+ "\"/></urls><manifest><packages>"
+ "<package name=\"package3\" size=\"333\" "
+ "hash_sha256=\"hash3\"/></packages>"
+ "<actions><action event=\"postinstall\" "
+ "MetadataSize=\"33\"/></actions>"
+ "</manifest></updatecheck></app>"
+ : "") +
+ (multi_app_no_update
+ ? "<app><updatecheck status=\"noupdate\"/></app>"
+ : "") +
+ "</response>";
}
// Return the payload URL, which is split in two fields in the XML response.
@@ -146,6 +165,7 @@
string more_info_url = "http://more/info";
string prompt = "true";
string codebase = "http://code/base/";
+ string codebase2 = "http://code/base/2/";
string filename = "file.signed";
string hash = "4841534831323334";
string needsadmin = "false";
@@ -167,7 +187,11 @@
// Whether to include the CrOS <!ENTITY> in the XML response.
bool include_entity = false;
- // Whether to include more than one package.
+ // Whether to include more than one app.
+ bool multi_app = false;
+ // Whether to include an additional app with no update.
+ bool multi_app_no_update = false;
+ // Whether to include more than one package in an app.
bool multi_package = false;
};
@@ -450,6 +474,22 @@
EXPECT_FALSE(response.update_exists);
}
+TEST_F(OmahaRequestActionTest, MultiAppNoUpdateTest) {
+ OmahaResponse response;
+ fake_update_response_.multi_app_no_update = true;
+ ASSERT_TRUE(TestUpdateCheck(nullptr, // request_params
+ fake_update_response_.GetNoUpdateResponse(),
+ -1,
+ false, // ping_only
+ ErrorCode::kSuccess,
+ metrics::CheckResult::kNoUpdateAvailable,
+ metrics::CheckReaction::kUnset,
+ metrics::DownloadErrorCode::kUnset,
+ &response,
+ nullptr));
+ EXPECT_FALSE(response.update_exists);
+}
+
// Test that all the values in the response are parsed in a normal update
// response.
TEST_F(OmahaRequestActionTest, ValidUpdateTest) {
@@ -503,8 +543,96 @@
response.packages[1].payload_urls[0]);
EXPECT_EQ(fake_update_response_.hash, response.packages[0].hash);
EXPECT_EQ(fake_update_response_.size, response.packages[0].size);
+ EXPECT_EQ(11u, response.packages[0].metadata_size);
ASSERT_EQ(2u, response.packages.size());
+ EXPECT_EQ(string("hash2"), response.packages[1].hash);
EXPECT_EQ(222u, response.packages[1].size);
+ EXPECT_EQ(22u, response.packages[1].metadata_size);
+}
+
+TEST_F(OmahaRequestActionTest, MultiAppUpdateTest) {
+ OmahaResponse response;
+ fake_update_response_.multi_app = true;
+ ASSERT_TRUE(TestUpdateCheck(nullptr, // request_params
+ fake_update_response_.GetUpdateResponse(),
+ -1,
+ false, // ping_only
+ ErrorCode::kSuccess,
+ metrics::CheckResult::kUpdateAvailable,
+ metrics::CheckReaction::kUpdating,
+ metrics::DownloadErrorCode::kUnset,
+ &response,
+ nullptr));
+ EXPECT_TRUE(response.update_exists);
+ EXPECT_EQ(fake_update_response_.version, response.version);
+ EXPECT_EQ(fake_update_response_.GetPayloadUrl(),
+ response.packages[0].payload_urls[0]);
+ EXPECT_EQ(fake_update_response_.codebase2 + "package3",
+ response.packages[1].payload_urls[0]);
+ EXPECT_EQ(fake_update_response_.hash, response.packages[0].hash);
+ EXPECT_EQ(fake_update_response_.size, response.packages[0].size);
+ EXPECT_EQ(11u, response.packages[0].metadata_size);
+ ASSERT_EQ(2u, response.packages.size());
+ EXPECT_EQ(string("hash3"), response.packages[1].hash);
+ EXPECT_EQ(333u, response.packages[1].size);
+ EXPECT_EQ(33u, response.packages[1].metadata_size);
+}
+
+TEST_F(OmahaRequestActionTest, MultiAppPartialUpdateTest) {
+ OmahaResponse response;
+ fake_update_response_.multi_app_no_update = true;
+ ASSERT_TRUE(TestUpdateCheck(nullptr, // request_params
+ fake_update_response_.GetUpdateResponse(),
+ -1,
+ false, // ping_only
+ ErrorCode::kSuccess,
+ metrics::CheckResult::kUpdateAvailable,
+ metrics::CheckReaction::kUpdating,
+ metrics::DownloadErrorCode::kUnset,
+ &response,
+ nullptr));
+ EXPECT_TRUE(response.update_exists);
+ EXPECT_EQ(fake_update_response_.version, response.version);
+ EXPECT_EQ(fake_update_response_.GetPayloadUrl(),
+ response.packages[0].payload_urls[0]);
+ EXPECT_EQ(fake_update_response_.hash, response.packages[0].hash);
+ EXPECT_EQ(fake_update_response_.size, response.packages[0].size);
+ EXPECT_EQ(11u, response.packages[0].metadata_size);
+ ASSERT_EQ(1u, response.packages.size());
+}
+
+TEST_F(OmahaRequestActionTest, MultiAppMultiPackageUpdateTest) {
+ OmahaResponse response;
+ fake_update_response_.multi_app = true;
+ fake_update_response_.multi_package = true;
+ ASSERT_TRUE(TestUpdateCheck(nullptr, // request_params
+ fake_update_response_.GetUpdateResponse(),
+ -1,
+ false, // ping_only
+ ErrorCode::kSuccess,
+ metrics::CheckResult::kUpdateAvailable,
+ metrics::CheckReaction::kUpdating,
+ metrics::DownloadErrorCode::kUnset,
+ &response,
+ nullptr));
+ EXPECT_TRUE(response.update_exists);
+ EXPECT_EQ(fake_update_response_.version, response.version);
+ EXPECT_EQ(fake_update_response_.GetPayloadUrl(),
+ response.packages[0].payload_urls[0]);
+ EXPECT_EQ(fake_update_response_.codebase + "package2",
+ response.packages[1].payload_urls[0]);
+ EXPECT_EQ(fake_update_response_.codebase2 + "package3",
+ response.packages[2].payload_urls[0]);
+ EXPECT_EQ(fake_update_response_.hash, response.packages[0].hash);
+ EXPECT_EQ(fake_update_response_.size, response.packages[0].size);
+ EXPECT_EQ(11u, response.packages[0].metadata_size);
+ ASSERT_EQ(3u, response.packages.size());
+ EXPECT_EQ(string("hash2"), response.packages[1].hash);
+ EXPECT_EQ(222u, response.packages[1].size);
+ EXPECT_EQ(22u, response.packages[1].metadata_size);
+ EXPECT_EQ(string("hash3"), response.packages[2].hash);
+ EXPECT_EQ(333u, response.packages[2].size);
+ EXPECT_EQ(33u, response.packages[2].metadata_size);
}
TEST_F(OmahaRequestActionTest, ExtraHeadersSentTest) {