mirror of
https://github.com/valitydev/osquery-1.git
synced 2024-11-08 18:33:54 +00:00
678 lines
19 KiB
C++
678 lines
19 KiB
C++
/*
|
|
* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <osquery/database.h>
|
|
#include <osquery/flags.h>
|
|
#include <osquery/logger.h>
|
|
#include <osquery/query.h>
|
|
|
|
#include "osquery/core/json.h"
|
|
|
|
namespace pt = boost::property_tree;
|
|
namespace rj = rapidjson;
|
|
|
|
namespace osquery {
|
|
|
|
DECLARE_bool(decorations_top_level);
|
|
|
|
uint64_t Query::getPreviousEpoch() const {
|
|
uint64_t epoch = 0;
|
|
std::string raw;
|
|
auto status = getDatabaseValue(kQueries, name_ + "epoch", raw);
|
|
if (status.ok()) {
|
|
epoch = std::stoul(raw);
|
|
}
|
|
return epoch;
|
|
}
|
|
|
|
Status Query::getPreviousQueryResults(QueryData& results) const {
|
|
std::string raw;
|
|
auto status = getDatabaseValue(kQueries, name_, raw);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
status = deserializeQueryDataJSON(raw, results);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
std::vector<std::string> Query::getStoredQueryNames() {
|
|
std::vector<std::string> results;
|
|
scanDatabaseKeys(kQueries, results);
|
|
return results;
|
|
}
|
|
|
|
bool Query::isQueryNameInDatabase() const {
|
|
auto names = Query::getStoredQueryNames();
|
|
return std::find(names.begin(), names.end(), name_) != names.end();
|
|
}
|
|
|
|
static inline void saveQuery(const std::string& name,
|
|
const std::string& query) {
|
|
setDatabaseValue(kQueries, "query." + name, query);
|
|
}
|
|
|
|
bool Query::isNewQuery() const {
|
|
std::string query;
|
|
getDatabaseValue(kQueries, "query." + name_, query);
|
|
return (query != query_.query);
|
|
}
|
|
|
|
Status Query::addNewResults(const QueryData& qd, const uint64_t epoch) const {
|
|
DiffResults dr;
|
|
return addNewResults(qd, epoch, dr, false);
|
|
}
|
|
|
|
Status Query::addNewResults(const QueryData& current_qd,
|
|
const uint64_t current_epoch,
|
|
DiffResults& dr,
|
|
bool calculate_diff) const {
|
|
// The current results are 'fresh' when not calculating a differential.
|
|
bool fresh_results = !calculate_diff;
|
|
if (!isQueryNameInDatabase()) {
|
|
// This is the first encounter of the scheduled query.
|
|
fresh_results = true;
|
|
LOG(INFO) << "Storing initial results for new scheduled query: " << name_;
|
|
saveQuery(name_, query_.query);
|
|
} else if (getPreviousEpoch() != current_epoch) {
|
|
fresh_results = true;
|
|
LOG(INFO) << "New Epoch " << current_epoch << " for scheduled query "
|
|
<< name_;
|
|
} else if (isNewQuery()) {
|
|
// This query is 'new' in that the previous results may be invalid.
|
|
LOG(INFO) << "Scheduled query has been updated: " + name_;
|
|
saveQuery(name_, query_.query);
|
|
}
|
|
|
|
// Use a 'target' avoid copying the query data when serializing and saving.
|
|
// If a differential is requested and needed the target remains the original
|
|
// query data, otherwise the content is moved to the differential's added set.
|
|
const auto* target_gd = ¤t_qd;
|
|
if (!fresh_results && calculate_diff) {
|
|
// Get the rows from the last run of this query name.
|
|
QueryData previous_qd;
|
|
auto status = getPreviousQueryResults(previous_qd);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
// Calculate the differential between previous and current query results.
|
|
dr = diff(previous_qd, current_qd);
|
|
fresh_results = (!dr.added.empty() || !dr.removed.empty());
|
|
} else {
|
|
dr.added = std::move(current_qd);
|
|
target_gd = &dr.added;
|
|
}
|
|
|
|
if (fresh_results) {
|
|
// Replace the "previous" query data with the current.
|
|
std::string json;
|
|
auto status = serializeQueryDataJSON(*target_gd, json);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
status = setDatabaseValue(kQueries, name_, json);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
status = setDatabaseValue(
|
|
kQueries, name_ + "epoch", std::to_string(current_epoch));
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeRow(const Row& r, pt::ptree& tree) {
|
|
try {
|
|
for (auto& i : r) {
|
|
tree.put<std::string>(i.first, i.second);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeRowRJ(const Row& r, rj::Document& d) {
|
|
try {
|
|
for (auto& i : r) {
|
|
d.AddMember(rj::Value(i.first.c_str(), d.GetAllocator()).Move(),
|
|
rj::Value(i.second.c_str(), d.GetAllocator()).Move(),
|
|
d.GetAllocator());
|
|
}
|
|
} catch (const std::exception& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeRow(const Row& r, const ColumnNames& cols, pt::ptree& tree) {
|
|
try {
|
|
for (auto& c : cols) {
|
|
tree.add<std::string>(c, r.at(c));
|
|
}
|
|
} catch (const std::exception& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeRowRJ(const Row& r, const ColumnNames& cols, rj::Document& d) {
|
|
try {
|
|
for (auto& c : cols) {
|
|
d.AddMember(rj::Value(c.c_str(), d.GetAllocator()).Move(),
|
|
rj::Value(r.at(c).c_str(), d.GetAllocator()).Move(),
|
|
d.GetAllocator());
|
|
}
|
|
} catch (const std::exception& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeRowJSON(const Row& r, std::string& json) {
|
|
pt::ptree tree;
|
|
auto status = serializeRow(r, tree);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
std::ostringstream output;
|
|
try {
|
|
pt::write_json(output, tree, false);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
// The content could not be represented as JSON.
|
|
return Status(1, e.what());
|
|
}
|
|
json = output.str();
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeRowJSONRJ(const Row& r, std::string& json) {
|
|
rj::Document d(rj::kObjectType);
|
|
auto status = serializeRowRJ(r, d);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
rj::StringBuffer sb;
|
|
rj::Writer<rj::StringBuffer> writer(sb);
|
|
d.Accept(writer);
|
|
json = sb.GetString();
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeRow(const pt::ptree& tree, Row& r) {
|
|
for (const auto& i : tree) {
|
|
if (i.first.length() > 0) {
|
|
r[i.first] = i.second.data();
|
|
}
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeRowRJ(const rj::Value& v, Row& r) {
|
|
if (!v.IsObject()) {
|
|
return Status(1, "Row not an object");
|
|
}
|
|
for (const auto& i : v.GetObject()) {
|
|
std::string name(i.name.GetString());
|
|
std::string value(i.value.GetString());
|
|
if (name.length() > 0) {
|
|
r[name] = value;
|
|
}
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeRowJSON(const std::string& json, Row& r) {
|
|
pt::ptree tree;
|
|
try {
|
|
std::stringstream input;
|
|
input << json;
|
|
pt::read_json(input, tree);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
return deserializeRow(tree, r);
|
|
}
|
|
|
|
Status deserializeRowJSONRJ(const std::string& json, Row& r) {
|
|
rj::Document d;
|
|
if (d.Parse(json.c_str()).HasParseError()) {
|
|
return Status(1, "Error serializing JSON");
|
|
}
|
|
return deserializeRowRJ(d, r);
|
|
}
|
|
|
|
Status serializeQueryData(const QueryData& q, pt::ptree& tree) {
|
|
for (const auto& r : q) {
|
|
pt::ptree serialized;
|
|
auto status = serializeRow(r, serialized);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
tree.push_back(std::make_pair("", serialized));
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryData(const QueryData& q,
|
|
const ColumnNames& cols,
|
|
pt::ptree& tree) {
|
|
for (const auto& r : q) {
|
|
pt::ptree serialized;
|
|
auto status = serializeRow(r, cols, serialized);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
tree.push_back(std::make_pair("", serialized));
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryDataJSON(const QueryData& q, std::string& json) {
|
|
pt::ptree tree;
|
|
auto status = serializeQueryData(q, tree);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
std::ostringstream output;
|
|
try {
|
|
pt::write_json(output, tree, false);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
// The content could not be represented as JSON.
|
|
return Status(1, e.what());
|
|
}
|
|
json = output.str();
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryDataJSONRJ(const QueryData& q, std::string& json) {
|
|
rj::Document d;
|
|
d.SetArray();
|
|
auto status = serializeQueryDataRJ(q, d);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
rj::StringBuffer sb;
|
|
rj::Writer<rj::StringBuffer> writer(sb);
|
|
d.Accept(writer);
|
|
json = sb.GetString();
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeQueryData(const pt::ptree& tree, QueryData& qd) {
|
|
for (const auto& i : tree) {
|
|
Row r;
|
|
auto status = deserializeRow(i.second, r);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
qd.push_back(r);
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeQueryDataRJ(const rj::Value& v, QueryData& qd) {
|
|
if (!v.IsArray()) {
|
|
return Status(1, "Not an array");
|
|
}
|
|
for (const auto& i : v.GetArray()) {
|
|
Row r;
|
|
auto status = deserializeRowRJ(i, r);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
qd.push_back(r);
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeQueryDataJSON(const std::string& json, QueryData& qd) {
|
|
pt::ptree tree;
|
|
try {
|
|
std::stringstream input;
|
|
input << json;
|
|
pt::read_json(input, tree);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
return deserializeQueryData(tree, qd);
|
|
}
|
|
|
|
Status serializeDiffResults(const DiffResults& d, pt::ptree& tree) {
|
|
// Serialize and add "removed" first.
|
|
// A property tree is somewhat ordered, this provides a loose contract to
|
|
// the logger plugins and their aggregations, allowing them to parse chunked
|
|
// lines. Note that the chunking is opaque to the database functions.
|
|
pt::ptree removed;
|
|
auto status = serializeQueryData(d.removed, removed);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
tree.add_child("removed", removed);
|
|
|
|
pt::ptree added;
|
|
status = serializeQueryData(d.added, added);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
tree.add_child("added", added);
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeDiffResults(const pt::ptree& tree, DiffResults& dr) {
|
|
if (tree.count("removed") > 0) {
|
|
auto status = deserializeQueryData(tree.get_child("removed"), dr.removed);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
if (tree.count("added") > 0) {
|
|
auto status = deserializeQueryData(tree.get_child("added"), dr.added);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeDiffResultsJSON(const DiffResults& d, std::string& json) {
|
|
pt::ptree tree;
|
|
auto status = serializeDiffResults(d, tree);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
std::ostringstream output;
|
|
try {
|
|
pt::write_json(output, tree, false);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
// The content could not be represented as JSON.
|
|
return Status(1, e.what());
|
|
}
|
|
json = output.str();
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
DiffResults diff(const QueryData& old, const QueryData& current) {
|
|
DiffResults r;
|
|
QueryData overlap;
|
|
|
|
for (const auto& i : current) {
|
|
auto item = std::find(old.begin(), old.end(), i);
|
|
if (item != old.end()) {
|
|
overlap.push_back(i);
|
|
} else {
|
|
r.added.push_back(i);
|
|
}
|
|
}
|
|
|
|
std::multiset<Row> overlap_set(overlap.begin(), overlap.end());
|
|
std::multiset<Row> old_set(old.begin(), old.end());
|
|
std::set_difference(old_set.begin(),
|
|
old_set.end(),
|
|
overlap_set.begin(),
|
|
overlap_set.end(),
|
|
std::back_inserter(r.removed));
|
|
return r;
|
|
}
|
|
|
|
inline void addLegacyFieldsAndDecorations(const QueryLogItem& item,
|
|
pt::ptree& tree) {
|
|
// Apply legacy fields.
|
|
tree.put<std::string>("name", item.name);
|
|
tree.put<std::string>("hostIdentifier", item.identifier);
|
|
tree.put<std::string>("calendarTime", item.calendar_time);
|
|
tree.put<size_t>("unixTime", item.time);
|
|
tree.put<uint64_t>("epoch", item.epoch);
|
|
|
|
// Append the decorations.
|
|
if (item.decorations.size() > 0) {
|
|
auto decorator_parent = std::ref(tree);
|
|
if (!FLAGS_decorations_top_level) {
|
|
tree.add_child("decorations", pt::ptree());
|
|
decorator_parent = tree.get_child("decorations");
|
|
}
|
|
for (const auto& name : item.decorations) {
|
|
decorator_parent.get().put<std::string>(name.first, name.second);
|
|
}
|
|
}
|
|
}
|
|
|
|
inline void getLegacyFieldsAndDecorations(const pt::ptree& tree,
|
|
QueryLogItem& item) {
|
|
if (tree.count("decorations") > 0) {
|
|
auto& decorations = tree.get_child("decorations");
|
|
for (const auto& name : decorations) {
|
|
item.decorations[name.first] = name.second.data();
|
|
}
|
|
}
|
|
|
|
item.name = tree.get<std::string>("name", "");
|
|
item.identifier = tree.get<std::string>("hostIdentifier", "");
|
|
item.calendar_time = tree.get<std::string>("calendarTime", "");
|
|
item.time = tree.get<int>("unixTime", 0);
|
|
}
|
|
|
|
Status serializeQueryLogItem(const QueryLogItem& item, pt::ptree& tree) {
|
|
pt::ptree results_tree;
|
|
if (item.results.added.size() > 0 || item.results.removed.size() > 0) {
|
|
auto status = serializeDiffResults(item.results, results_tree);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
tree.add_child("diffResults", results_tree);
|
|
} else {
|
|
auto status = serializeQueryData(item.snapshot_results, results_tree);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
tree.add_child("snapshot", results_tree);
|
|
tree.put<std::string>("action", "snapshot");
|
|
}
|
|
|
|
addLegacyFieldsAndDecorations(item, tree);
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryLogItemJSON(const QueryLogItem& i, std::string& json) {
|
|
pt::ptree tree;
|
|
auto status = serializeQueryLogItem(i, tree);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
std::ostringstream output;
|
|
try {
|
|
pt::write_json(output, tree, false);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
// The content could not be represented as JSON.
|
|
return Status(1, e.what());
|
|
}
|
|
json = output.str();
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeQueryLogItem(const pt::ptree& tree, QueryLogItem& item) {
|
|
if (tree.count("diffResults") > 0) {
|
|
auto status =
|
|
deserializeDiffResults(tree.get_child("diffResults"), item.results);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
} else if (tree.count("snapshot") > 0) {
|
|
auto status =
|
|
deserializeQueryData(tree.get_child("snapshot"), item.snapshot_results);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
}
|
|
|
|
getLegacyFieldsAndDecorations(tree, item);
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status deserializeQueryLogItemJSON(const std::string& json,
|
|
QueryLogItem& item) {
|
|
pt::ptree tree;
|
|
try {
|
|
std::stringstream input;
|
|
input << json;
|
|
pt::read_json(input, tree);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
return deserializeQueryLogItem(tree, item);
|
|
}
|
|
|
|
Status serializeEvent(const QueryLogItem& item,
|
|
const pt::ptree& event,
|
|
pt::ptree& tree) {
|
|
addLegacyFieldsAndDecorations(item, tree);
|
|
pt::ptree columns;
|
|
for (auto& i : event) {
|
|
// Yield results as a "columns." map to avoid namespace collisions.
|
|
columns.put<std::string>(i.first, i.second.get_value<std::string>());
|
|
}
|
|
|
|
tree.add_child("columns", columns);
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryLogItemAsEvents(const QueryLogItem& i, pt::ptree& tree) {
|
|
pt::ptree diff_results;
|
|
// Note, snapshot query results will bypass the "AsEvents" call, even when
|
|
// log_result_events is set. This is because the schedule will call an
|
|
// explicit ::logSnapshotQuery, which does not check for the result_events
|
|
// configuration.
|
|
auto status = serializeDiffResults(i.results, diff_results);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
for (auto& action : diff_results) {
|
|
for (auto& row : action.second) {
|
|
pt::ptree event;
|
|
serializeEvent(i, row.second, event);
|
|
event.put<std::string>("action", action.first);
|
|
tree.push_back(std::make_pair("", event));
|
|
}
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryLogItemAsEventsJSON(const QueryLogItem& i,
|
|
std::vector<std::string>& items) {
|
|
pt::ptree tree;
|
|
auto status = serializeQueryLogItemAsEvents(i, tree);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
for (auto& event : tree) {
|
|
std::ostringstream output;
|
|
try {
|
|
pt::write_json(output, event.second, false);
|
|
} catch (const pt::json_parser::json_parser_error& e) {
|
|
return Status(1, e.what());
|
|
}
|
|
items.push_back(output.str());
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryDataRJ(const QueryData& q, rj::Document& d) {
|
|
if (!d.IsArray()) {
|
|
return Status(1, "Document is not an array");
|
|
}
|
|
for (const auto& r : q) {
|
|
rj::Document serialized;
|
|
serialized.SetObject();
|
|
auto status = serializeRowRJ(r, serialized);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
if (serialized.GetObject().MemberCount()) {
|
|
d.PushBack(rj::Value(serialized, d.GetAllocator()).Move(),
|
|
d.GetAllocator());
|
|
}
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeQueryDataRJ(const QueryData& q,
|
|
const ColumnNames& cols,
|
|
rj::Document& d) {
|
|
for (const auto& r : q) {
|
|
rj::Document serialized;
|
|
serialized.SetObject();
|
|
auto status = serializeRowRJ(r, cols, serialized);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
if (serialized.GetObject().MemberCount()) {
|
|
d.PushBack(rj::Value(serialized, d.GetAllocator()).Move(),
|
|
d.GetAllocator());
|
|
}
|
|
}
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
Status serializeDiffResultsRJ(const DiffResults& d, rj::Document& doc) {
|
|
// Serialize and add "removed" first.
|
|
// A property tree is somewhat ordered, this provides a loose contract to
|
|
// the logger plugins and their aggregations, allowing them to parse chunked
|
|
// lines. Note that the chunking is opaque to the database functions.
|
|
rj::Document removed;
|
|
auto status = serializeQueryDataRJ(d.removed, removed);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
doc.AddMember(rj::Value("removed", doc.GetAllocator()).Move(),
|
|
rj::Value(removed, doc.GetAllocator()).Move(),
|
|
doc.GetAllocator());
|
|
|
|
rj::Document added;
|
|
status = serializeQueryDataRJ(d.added, added);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
doc.AddMember(rj::Value("added", doc.GetAllocator()).Move(),
|
|
rj::Value(added, doc.GetAllocator()).Move(),
|
|
doc.GetAllocator());
|
|
return Status(0, "OK");
|
|
}
|
|
|
|
bool addUniqueRowToQueryData(QueryData& q, const Row& r) {
|
|
if (std::find(q.begin(), q.end(), r) != q.end()) {
|
|
return false;
|
|
}
|
|
q.push_back(r);
|
|
return true;
|
|
}
|
|
}
|