2023-01-28 14:11:46 +00:00
|
|
|
#include <string>
|
|
|
|
#include <vector>
|
|
|
|
#include <stdexcept>
|
|
|
|
|
|
|
|
#include "log.h"
|
|
|
|
#include "filters.h"
|
|
|
|
|
|
|
|
Filter::~Filter() {
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IntegerFilter::IntegerFilter(FilterKey key_, size_t other_, bool inverted_, bool disabled)
|
|
|
|
: key(key_), other(other_), inverted(inverted_), _disabled(disabled) {
|
|
|
|
if (key_ != FilterKey::PID && key_ != FilterKey::TID) {
|
|
|
|
throw std::runtime_error("IntegerFilter is passed a non-integer key");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void IntegerFilter::updated() {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IntegerFilter::disabled() const {
|
|
|
|
return this->_disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
void IntegerFilter::disabled(bool disabled) {
|
|
|
|
this->_disabled = disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<Filter> IntegerFilter::clone() const {
|
|
|
|
return std::make_unique<IntegerFilter>(this->key, this->other, this->inverted, this->_disabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::string* IntegerFilter::error() const {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IntegerFilter::match(const LogcatEntry& entry) const {
|
|
|
|
bool matched;
|
|
|
|
switch (this->key) {
|
|
|
|
case FilterKey::PID: matched = entry.pid == this->other; break;
|
|
|
|
case FilterKey::TID: matched = entry.tid == this->other; break;
|
|
|
|
default: throw std::runtime_error("IntegerFilter::match received unhandled key");
|
|
|
|
};
|
|
|
|
return !this->inverted ? matched : !matched;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-01-30 02:41:42 +00:00
|
|
|
StringFilter::StringFilter(FilterKey key_, std::string other_, bool inverted_, bool exact_match_, bool disabled)
|
|
|
|
: key(key_), other(std::move(other_)), inverted(inverted_), exact_match(exact_match_), _disabled(disabled) {
|
2023-01-28 14:11:46 +00:00
|
|
|
if (key_ != FilterKey::User && key_ != FilterKey::Tag && key_ != FilterKey::Message) {
|
|
|
|
throw std::runtime_error("StringFilter is passed a non-string key");
|
|
|
|
}
|
|
|
|
this->updated();
|
|
|
|
}
|
|
|
|
|
|
|
|
void StringFilter::updated() {
|
|
|
|
this->_regex.reset();
|
|
|
|
this->_error.clear();
|
|
|
|
|
|
|
|
if (this->other.empty()) {
|
|
|
|
this->_error = "String must not be empty";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!this->other.starts_with("regex:")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this->other.size() == 6) {
|
|
|
|
this->_error = "Regex must not be empty";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
this->_regex.emplace(&this->other.c_str()[6]);
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
this->_error = e.what();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool StringFilter::disabled() const {
|
|
|
|
return this->_disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
void StringFilter::disabled(bool disabled) {
|
|
|
|
this->_disabled = disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<Filter> StringFilter::clone() const {
|
2023-01-30 02:41:42 +00:00
|
|
|
return std::make_unique<StringFilter>(this->key, this->other, this->inverted, this->exact_match, this->_disabled);
|
2023-01-28 14:11:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const std::string* StringFilter::error() const {
|
|
|
|
return !this->_error.empty() ? &this->_error : nullptr;
|
|
|
|
}
|
|
|
|
|
2023-01-30 02:41:42 +00:00
|
|
|
bool StringFilter::_match_no_regex(const LogcatEntry& entry) const {
|
|
|
|
bool matched;
|
|
|
|
if (this->exact_match) {
|
|
|
|
switch (this->key) {
|
|
|
|
case FilterKey::User: matched = entry.user.value_or("") == this->other; break;
|
|
|
|
case FilterKey::Tag: matched = entry.tag == this->other; break;
|
|
|
|
case FilterKey::Message: matched = entry.message == this->other; break;
|
|
|
|
default: throw std::runtime_error("StringFilter::match received unhandled key");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch (this->key) {
|
|
|
|
case FilterKey::User: matched = entry.user.value_or("").find(this->other) != std::string::npos; break;
|
|
|
|
case FilterKey::Tag: matched = entry.tag.find(this->other) != std::string::npos; break;
|
|
|
|
case FilterKey::Message: matched = entry.message.find(this->other) != std::string::npos; break;
|
|
|
|
default: throw std::runtime_error("StringFilter::match received unhandled key");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return !this->inverted ? matched : !matched;
|
|
|
|
}
|
|
|
|
|
2023-01-28 14:11:46 +00:00
|
|
|
bool StringFilter::match(const LogcatEntry& entry) const {
|
|
|
|
bool matched;
|
|
|
|
|
|
|
|
if (this->error()) {
|
|
|
|
log(std::string("StringFilter::match called despite there being an error: ") + *this->error());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (!this->other.starts_with("regex:")) {
|
2023-01-30 02:41:42 +00:00
|
|
|
return this->_match_no_regex(entry);
|
2023-01-28 14:11:46 +00:00
|
|
|
}
|
|
|
|
if (!this->_regex) {
|
|
|
|
log("StringFilter::match called with a regex despite there being no regex");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string str;
|
|
|
|
regmatch_t pmatch[1];
|
|
|
|
switch (this->key) {
|
|
|
|
case FilterKey::User: str = entry.user.value_or(""); break;
|
|
|
|
case FilterKey::Tag: str = entry.tag; break;
|
|
|
|
case FilterKey::Message: str = entry.message; break;
|
|
|
|
default: throw std::runtime_error("StringFilter::match received unhandled key");
|
|
|
|
}
|
|
|
|
pmatch[0].rm_so = 0;
|
|
|
|
pmatch[0].rm_eo = static_cast<regoff_t>(str.size());
|
|
|
|
|
2023-01-30 02:41:42 +00:00
|
|
|
matched = this->_regex->match(str.data(), 1, pmatch, REG_STARTEND);
|
|
|
|
if (this->exact_match) {
|
|
|
|
matched = matched && pmatch[0].rm_so == 0 && static_cast<size_t>(pmatch[0].rm_eo) == str.size();
|
|
|
|
}
|
2023-01-28 14:11:46 +00:00
|
|
|
return !this->inverted ? matched : !matched;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
BufferFilter::BufferFilter(unsigned int wanted_, bool disabled) : wanted(wanted_), _disabled(disabled) {
|
|
|
|
}
|
|
|
|
|
|
|
|
void BufferFilter::updated() {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool BufferFilter::disabled() const {
|
|
|
|
return this->_disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
void BufferFilter::disabled(bool disabled) {
|
|
|
|
this->_disabled = disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<Filter> BufferFilter::clone() const {
|
|
|
|
return std::make_unique<BufferFilter>(this->wanted, this->_disabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::string* BufferFilter::error() const {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool BufferFilter::match(const LogcatEntry& entry) const {
|
|
|
|
return this->wanted & static_cast<unsigned int>(entry.buffer) || this->wanted == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PriorityFilter::PriorityFilter(unsigned int wanted_, bool disabled) : wanted(wanted_), _disabled(disabled) {
|
|
|
|
}
|
|
|
|
|
|
|
|
void PriorityFilter::updated() {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool PriorityFilter::disabled() const {
|
|
|
|
return this->_disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
void PriorityFilter::disabled(bool disabled) {
|
|
|
|
this->_disabled = disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<Filter> PriorityFilter::clone() const {
|
|
|
|
return std::make_unique<PriorityFilter>(this->wanted, this->_disabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::string* PriorityFilter::error() const {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool PriorityFilter::match(const LogcatEntry& entry) const {
|
|
|
|
return this->wanted & static_cast<unsigned int>(entry.priority) || this->wanted == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
GroupFilter::GroupFilter(std::vector<std::unique_ptr<Filter>> filters_, GroupFilter::Type type_, bool disabled)
|
|
|
|
: filters(std::move(filters_)), type(type_), _disabled(disabled) {
|
|
|
|
}
|
|
|
|
|
|
|
|
void GroupFilter::updated() {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GroupFilter::disabled() const {
|
|
|
|
return this->_disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
void GroupFilter::disabled(bool disabled) {
|
|
|
|
this->_disabled = disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<Filter> GroupFilter::clone() const {
|
|
|
|
std::vector<std::unique_ptr<Filter>> new_filters;
|
|
|
|
new_filters.reserve(this->filters.size());
|
|
|
|
|
|
|
|
for (const std::unique_ptr<Filter>& filter : this->filters) {
|
|
|
|
new_filters.push_back(filter->clone());
|
|
|
|
}
|
|
|
|
|
|
|
|
return std::make_unique<GroupFilter>(std::move(new_filters), this->type, this->_disabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::string* GroupFilter::error() const {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GroupFilter::match(const LogcatEntry& entry) const {
|
|
|
|
bool had_matched = false;
|
|
|
|
|
|
|
|
for (const std::unique_ptr<Filter>& filter : this->filters) {
|
|
|
|
if (filter->disabled() || filter->error()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool matched = filter->match(entry);
|
|
|
|
if (this->type == GroupFilter::Type::Any && matched) {
|
|
|
|
return true;
|
|
|
|
} else if (this->type == GroupFilter::Type::None && matched) {
|
|
|
|
return false;
|
|
|
|
} else if (this->type == GroupFilter::Type::All && !matched) {
|
|
|
|
return false;
|
|
|
|
} else if (this->type == GroupFilter::Type::One) {
|
|
|
|
if (had_matched && matched) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (matched) {
|
|
|
|
had_matched = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this->type == GroupFilter::Type::Any) {
|
|
|
|
return false;
|
|
|
|
} else if (this->type == GroupFilter::Type::One && !had_matched) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-02-05 15:57:46 +00:00
|
|
|
void copy_filters(Filters& __restrict filters, const Filters& __restrict other) {
|
2023-01-28 14:11:46 +00:00
|
|
|
filters.clear();
|
|
|
|
filters.reserve(other.size());
|
|
|
|
|
|
|
|
for (const auto &[title, filter] : other) {
|
|
|
|
filters.push_back(std::make_pair(title, filter->clone()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-01 15:22:08 +00:00
|
|
|
static bool matches(const LogcatEntry& entry, const Filters& filters, bool return_true_if_empty) {
|
|
|
|
for (const auto &[_, filter] : filters) {
|
2023-01-28 14:11:46 +00:00
|
|
|
if (filter->disabled() || filter->error()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (filter->match(entry)) {
|
|
|
|
return true;
|
|
|
|
}
|
2023-02-01 15:22:08 +00:00
|
|
|
return_true_if_empty = false;
|
2023-01-28 14:11:46 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 15:22:08 +00:00
|
|
|
return return_true_if_empty;
|
|
|
|
}
|
|
|
|
|
2023-02-05 15:57:46 +00:00
|
|
|
bool matches(const LogcatEntry& entry, const Filters& __restrict filters, const Filters& __restrict exclusions) {
|
2023-02-01 15:22:08 +00:00
|
|
|
return !matches(entry, exclusions, false) && matches(entry, filters, true);
|
2023-01-28 14:11:46 +00:00
|
|
|
}
|
2023-01-31 14:40:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
void from_json(const nlohmann::json& j, FilterKey& key) {
|
|
|
|
const std::string& str = j.get_ref<const nlohmann::json::string_t&>();
|
|
|
|
|
|
|
|
if (str == "BUFFER") key = FilterKey::Buffer;
|
|
|
|
else if (str == "USER") key = FilterKey::User;
|
|
|
|
else if (str == "PID") key = FilterKey::PID;
|
|
|
|
else if (str == "TID") key = FilterKey::TID;
|
|
|
|
else if (str == "PRIORITY") key = FilterKey::Priority;
|
|
|
|
else if (str == "TAG") key = FilterKey::Tag;
|
|
|
|
else if (str == "MESSAGE") key = FilterKey::Message;
|
|
|
|
else throw std::invalid_argument(std::string("Unknown filter key: ") + str);
|
|
|
|
}
|
|
|
|
|
|
|
|
void to_json(nlohmann::json& j, const FilterKey& key) {
|
|
|
|
switch (key) {
|
|
|
|
case FilterKey::Buffer: j = "BUFFER"; break;
|
|
|
|
case FilterKey::User: j = "USER"; break;
|
|
|
|
case FilterKey::PID: j = "PID"; break;
|
|
|
|
case FilterKey::TID: j = "TID"; break;
|
|
|
|
case FilterKey::Priority: j = "PRIORITY"; break;
|
|
|
|
case FilterKey::Tag: j = "TAG"; break;
|
|
|
|
case FilterKey::Message: j = "MESSAGE"; break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void from_json(const nlohmann::json& j, GroupFilter::Type& type) {
|
|
|
|
const std::string& str = j.get_ref<const nlohmann::json::string_t&>();
|
|
|
|
|
|
|
|
if (str == "ALL") type = GroupFilter::Type::All;
|
|
|
|
else if (str == "ANY") type = GroupFilter::Type::Any;
|
|
|
|
else if (str == "ONE") type = GroupFilter::Type::One;
|
|
|
|
else if (str == "NONE") type = GroupFilter::Type::None;
|
|
|
|
else throw std::invalid_argument(std::string("Unknown group filter type: ") + str);
|
|
|
|
}
|
|
|
|
|
|
|
|
void to_json(nlohmann::json& j, const GroupFilter::Type& type) {
|
|
|
|
switch (type) {
|
|
|
|
case GroupFilter::Type::All: j = "ALL"; break;
|
|
|
|
case GroupFilter::Type::Any: j = "ANY"; break;
|
|
|
|
case GroupFilter::Type::One: j = "ONE"; break;
|
|
|
|
case GroupFilter::Type::None: j = "NONE"; break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void from_json(const nlohmann::json& j, std::unique_ptr<Filter>& filter) {
|
|
|
|
const std::string& filter_type = j.at("type").get_ref<const nlohmann::json::string_t&>();
|
|
|
|
bool disabled = j.at("disabled").get<bool>();
|
|
|
|
|
|
|
|
if (filter_type == "INTEGER") {
|
|
|
|
filter = std::make_unique<IntegerFilter>(j.at("key").get<FilterKey>(), j.at("other").get<size_t>(), j.at("inverted").get<bool>(), disabled);
|
|
|
|
} else if (filter_type == "STRING") {
|
|
|
|
filter = std::make_unique<StringFilter>(
|
|
|
|
j.at("key").get<FilterKey>(), j.at("other").get<std::string>(), j.at("inverted").get<bool>(), j.at("exact_match").get<bool>(), disabled);
|
|
|
|
} else if (filter_type == "BUFFER") {
|
|
|
|
unsigned int wanted = 0;
|
|
|
|
nlohmann::json wanted_json = j.at("wanted");
|
|
|
|
if (!wanted_json.is_array()) {
|
|
|
|
throw std::invalid_argument("Wanted buffer items is not an array");
|
|
|
|
}
|
|
|
|
for (nlohmann::json::const_iterator it = wanted_json.cbegin(); it != wanted_json.cend(); it++) {
|
|
|
|
wanted |= static_cast<unsigned int>(it->get<Buffer>());
|
|
|
|
}
|
|
|
|
|
|
|
|
filter = std::make_unique<BufferFilter>(wanted, disabled);
|
|
|
|
} else if (filter_type == "PRIORITY") {
|
|
|
|
unsigned int wanted = 0;
|
|
|
|
nlohmann::json wanted_json = j.at("wanted");
|
|
|
|
if (!wanted_json.is_array()) {
|
|
|
|
throw std::invalid_argument("Wanted priority items is not an array");
|
|
|
|
}
|
|
|
|
for (nlohmann::json::const_iterator it = wanted_json.cbegin(); it != wanted_json.cend(); it++) {
|
|
|
|
wanted |= static_cast<unsigned int>(it->get<Priority>());
|
|
|
|
}
|
|
|
|
|
|
|
|
filter = std::make_unique<PriorityFilter>(wanted, disabled);
|
|
|
|
} else if (filter_type == "GROUP") {
|
|
|
|
filter = std::make_unique<GroupFilter>(
|
|
|
|
j.at("filters").get<std::vector<std::unique_ptr<Filter>>>(), j.at("group_type").get<GroupFilter::Type>(), disabled);
|
|
|
|
} else {
|
|
|
|
throw std::invalid_argument(std::string("Unknown filter type: ") + filter_type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void to_json(nlohmann::json& j, const std::unique_ptr<Filter>& filter) {
|
|
|
|
j["disabled"] = filter->disabled();
|
|
|
|
|
|
|
|
if (const IntegerFilter* ifilter = dynamic_cast<const IntegerFilter*>(filter.get())) {
|
|
|
|
j["type"] = "INTEGER";
|
|
|
|
j["key"] = ifilter->key;
|
|
|
|
j["other"] = ifilter->other;
|
|
|
|
j["inverted"] = ifilter->inverted;
|
|
|
|
} else if (const StringFilter* sfilter = dynamic_cast<const StringFilter*>(filter.get())) {
|
|
|
|
j["type"] = "STRING";
|
|
|
|
j["key"] = sfilter->key;
|
|
|
|
j["other"] = sfilter->other;
|
|
|
|
j["inverted"] = sfilter->inverted;
|
|
|
|
j["exact_match"] = sfilter->exact_match;
|
|
|
|
} else if (const BufferFilter* bfilter = dynamic_cast<const BufferFilter*>(filter.get())) {
|
|
|
|
j["type"] = "BUFFER";
|
|
|
|
j["wanted"] = nlohmann::json::array();
|
|
|
|
if (bfilter->wanted & static_cast<unsigned int>(Buffer::Unknown)) {
|
|
|
|
j["wanted"].emplace_back(Buffer::Unknown);
|
|
|
|
}
|
|
|
|
if (bfilter->wanted & static_cast<unsigned int>(Buffer::Main)) {
|
|
|
|
j["wanted"].emplace_back(Buffer::Main);
|
|
|
|
}
|
|
|
|
if (bfilter->wanted & static_cast<unsigned int>(Buffer::System)) {
|
|
|
|
j["wanted"].emplace_back(Buffer::System);
|
|
|
|
}
|
|
|
|
if (bfilter->wanted & static_cast<unsigned int>(Buffer::Radio)) {
|
|
|
|
j["wanted"].emplace_back(Buffer::Radio);
|
|
|
|
}
|
|
|
|
if (bfilter->wanted & static_cast<unsigned int>(Buffer::Events)) {
|
|
|
|
j["wanted"].emplace_back(Buffer::Events);
|
|
|
|
}
|
|
|
|
if (bfilter->wanted & static_cast<unsigned int>(Buffer::Crash)) {
|
|
|
|
j["wanted"].emplace_back(Buffer::Crash);
|
|
|
|
}
|
|
|
|
} else if (const PriorityFilter* pfilter = dynamic_cast<const PriorityFilter*>(filter.get())) {
|
|
|
|
j["type"] = "PRIORITY";
|
|
|
|
j["wanted"] = nlohmann::json::array();
|
|
|
|
if (pfilter->wanted & static_cast<unsigned int>(Priority::Unknown)) {
|
|
|
|
j["wanted"].emplace_back(Priority::Unknown);
|
|
|
|
}
|
|
|
|
if (pfilter->wanted & static_cast<unsigned int>(Priority::Verbose)) {
|
|
|
|
j["wanted"].emplace_back(Priority::Verbose);
|
|
|
|
}
|
|
|
|
if (pfilter->wanted & static_cast<unsigned int>(Priority::Debug)) {
|
|
|
|
j["wanted"].emplace_back(Priority::Debug);
|
|
|
|
}
|
|
|
|
if (pfilter->wanted & static_cast<unsigned int>(Priority::Info)) {
|
|
|
|
j["wanted"].emplace_back(Priority::Info);
|
|
|
|
}
|
|
|
|
if (pfilter->wanted & static_cast<unsigned int>(Priority::Warn)) {
|
|
|
|
j["wanted"].emplace_back(Priority::Warn);
|
|
|
|
}
|
|
|
|
if (pfilter->wanted & static_cast<unsigned int>(Priority::Error)) {
|
|
|
|
j["wanted"].emplace_back(Priority::Error);
|
|
|
|
}
|
|
|
|
if (pfilter->wanted & static_cast<unsigned int>(Priority::Fatal)) {
|
|
|
|
j["wanted"].emplace_back(Priority::Fatal);
|
|
|
|
}
|
|
|
|
} else if (const GroupFilter* gfilter = dynamic_cast<const GroupFilter*>(filter.get())) {
|
|
|
|
j["type"] = "GROUP";
|
|
|
|
j["group_type"] = gfilter->type;
|
|
|
|
j["filters"] = nlohmann::json::array();
|
|
|
|
for (const std::unique_ptr<Filter>& i : gfilter->filters) {
|
|
|
|
j["filters"].emplace_back(i);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw std::invalid_argument("Cannot serialize unknown filter");
|
|
|
|
}
|
|
|
|
}
|