blob: 4fbf6700e08b06cdae3b3966bd46a73677b3bed3 [file] [log] [blame]
// Copyright 2019 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "libipp/ipp_parser.h"
#include <set>
#include "libipp/frame.h"
#include "libipp/ipp_encoding.h"
namespace ipp {
namespace {
// This parameter defines how deep can be a package with recursive collections.
// A collection placed directly in attributes group has level 1, each
// sub-collection belonging directly to it has level 2 etc..
constexpr int kMaxCollectionLevel = 16;
// Decodes 1-, 2- or 4-bytes integers (two's-complement binary encoding).
// Returns false if (data.size() != BytesCount) or (out == nullptr).
template <size_t BytesCount>
bool LoadInteger(const std::vector<uint8_t>& data, int32_t* out) {
if ((data.size() != BytesCount) || (out == nullptr))
return false;
const uint8_t* ptr = data.data();
ParseSignedInteger<BytesCount>(&ptr, out);
return true;
}
// Reads simple string from buf.
std::string LoadString(const std::vector<uint8_t>& buf) {
return std::string(buf.data(), buf.data() + buf.size());
}
// Reads textWithLanguage/nameWithLanguage (see [rfc8010], section 3.9) from
// buf. Returns false if given content is invalid or (out == nullptr).
bool LoadStringWithLanguage(const std::vector<uint8_t>& buf,
ipp::StringWithLanguage* out) {
// The shortest possible value has 4 bytes: 2 times 2-bytes zero.
if ((buf.size() < 4) || (out == nullptr))
return false;
const uint8_t* ptr = buf.data();
size_t length;
if (!ParseUnsignedInteger<2>(&ptr, &length))
return false;
if (buf.size() < 4 + length)
return false;
out->language.assign(ptr, ptr + length);
ptr += length;
if (!ParseUnsignedInteger<2>(&ptr, &length))
return false;
if (buf.size() != 4 + out->language.size() + length)
return false;
out->value.assign(ptr, ptr + length);
return true;
}
// Reads dateTime (see [rfc8010]) from buf.
// Fails when binary representation has invalid size or (out == nullptr).
bool LoadDateTime(const std::vector<uint8_t>& buf, ipp::DateTime* out) {
if ((buf.size() != 11) || (out == nullptr))
return false;
const uint8_t* ptr = buf.data();
return (ParseUnsignedInteger<2>(&ptr, &out->year) &&
ParseUnsignedInteger<1>(&ptr, &out->month) &&
ParseUnsignedInteger<1>(&ptr, &out->day) &&
ParseUnsignedInteger<1>(&ptr, &out->hour) &&
ParseUnsignedInteger<1>(&ptr, &out->minutes) &&
ParseUnsignedInteger<1>(&ptr, &out->seconds) &&
ParseUnsignedInteger<1>(&ptr, &out->deci_seconds) &&
ParseUnsignedInteger<1>(&ptr, &out->UTC_direction) &&
ParseUnsignedInteger<1>(&ptr, &out->UTC_hours) &&
ParseUnsignedInteger<1>(&ptr, &out->UTC_minutes));
}
// Reads resolution (see [rfc8010]) from buf.
// Fails when binary representation has invalid size or (out == nullptr).
bool LoadResolution(const std::vector<uint8_t>& buf, ipp::Resolution* out) {
if ((buf.size() != 9) || (out == nullptr))
return false;
const uint8_t* ptr = buf.data();
ParseSignedInteger<4>(&ptr, &out->xres);
ParseSignedInteger<4>(&ptr, &out->yres);
int8_t units;
ParseSignedInteger<1>(&ptr, &units);
out->units = static_cast<Resolution::Units>(units);
return true;
}
// Reads rangeOfInteger (see [rfc8010]) from buf.
// Fails when binary representation has invalid size or (out == nullptr).
bool LoadRangeOfInteger(const std::vector<uint8_t>& buf,
ipp::RangeOfInteger* out) {
if ((buf.size() != 8) || (out == nullptr))
return false;
const uint8_t* ptr = buf.data();
ParseSignedInteger<4>(&ptr, &out->min_value);
ParseSignedInteger<4>(&ptr, &out->max_value);
return true;
}
// Scope guard to control the context path. Constructor adds new a element to
// the path while destructor removes it from the path.
class ContextPathGuard {
public:
ContextPathGuard(AttrPath* path, uint16_t index) : path_(path) {
path_->PushBack(index, "");
}
~ContextPathGuard() { path_->PopBack(); }
private:
ContextPathGuard(const ContextPathGuard&) = delete;
ContextPathGuard(ContextPathGuard&&) = delete;
ContextPathGuard& operator=(const ContextPathGuard&) = delete;
ContextPathGuard& operator=(ContextPathGuard&&) = delete;
AttrPath* path_;
};
// Return true if source type can be used in attribute of target type.
bool IsConvertibleTo(const ipp::ValueTag source, const ipp::ValueTag target) {
if (source == target)
return true;
if (source == ipp::ValueTag::integer &&
target == ipp::ValueTag::rangeOfInteger)
return true;
if (source == ipp::ValueTag::integer && target == ipp::ValueTag::enum_)
return true;
if (source == ipp::ValueTag::nameWithoutLanguage &&
target == ipp::ValueTag::nameWithLanguage)
return true;
if (source == ipp::ValueTag::textWithoutLanguage &&
target == ipp::ValueTag::textWithLanguage)
return true;
return false;
}
// Parses two bytes from `ptr` and move it forward by 2 bytes.
uint16_t ParseUInt16(const uint8_t*& ptr) {
uint16_t val = *ptr;
val <<= 8;
val += *++ptr;
++ptr;
return val;
}
} // namespace
void Parser::LogParserError(ParserCode error_code, const uint8_t* ptr) {
ssize_t buf_offset = -1;
if (ptr != nullptr && ptr >= buffer_begin_ && ptr <= buffer_end_) {
// Current position in the buffer.
buf_offset = ptr - buffer_begin_;
}
log_->AddParserError({parser_context_, error_code, buf_offset});
}
void Parser::LogParserErrors(const std::vector<ParserCode>& error_codes) {
for (ParserCode error_code : error_codes) {
LogParserError(error_code);
}
}
// Temporary representation of an attribute's value parsed from TNVs.
struct RawValue {
// original tag (IsValid(tag))
ValueTag tag;
// original data, empty when (tag == collection), content not verified
std::vector<uint8_t> data;
// (not nullptr) <=> (tag == collection)
std::unique_ptr<RawCollection> collection;
// create as standard value
RawValue(ValueTag tag, const std::vector<uint8_t>& data)
: tag(tag), data(data) {}
// create as collection
explicit RawValue(RawCollection* coll)
: tag(ValueTag::collection), collection(coll) {}
};
struct RawCollection;
// Temporary representation of an attribute parsed from TNVs.
struct RawAttribute {
// verified (non-empty)
std::string name;
// parsed values (see RawValue)
std::vector<RawValue> values;
explicit RawAttribute(const std::string& name) : name(name) {}
};
// Temporary representation of a collection parsed from TNVs.
struct RawCollection {
// parsed attributes (may have duplicate names)
std::vector<RawAttribute> attributes;
};
// Parse a value of type `attr_type` from `raw_value` to `output` when possible.
// Returns true <=> parsing was successful. All spotted errors are added to
// `errors`.
template <typename ApiType>
bool LoadAttrValue(ValueTag attr_type,
const RawValue& raw_value,
ApiType& output,
std::vector<ParserCode>& errors);
template <>
bool LoadAttrValue<std::string>(ValueTag attr_type,
const RawValue& raw_value,
std::string& output,
std::vector<ParserCode>& errors) {
if (!IsString(raw_value.tag) && raw_value.tag != ValueTag::octetString) {
errors.push_back(ParserCode::kValueMismatchTagOmitted);
return false;
}
output = LoadString(raw_value.data);
if (attr_type != raw_value.tag)
errors.push_back(ParserCode::kValueMismatchTagConverted);
return true;
}
template <>
bool LoadAttrValue<int32_t>(ValueTag attr_type,
const RawValue& raw_value,
int32_t& output,
std::vector<ParserCode>& errors) {
switch (raw_value.tag) {
case ValueTag::boolean: {
if (!LoadInteger<1>(raw_value.data, &output)) {
errors.push_back(ParserCode::kValueInvalidSize);
return false;
}
if (attr_type != ValueTag::boolean) {
errors.push_back(ParserCode::kValueMismatchTagConverted);
}
if (output < 0 || output > 1) {
output = 1;
errors.push_back(ParserCode::kBooleanValueOutOfRange);
}
return true;
}
case ValueTag::integer:
case ValueTag::enum_: {
if (!LoadInteger<4>(raw_value.data, &output)) {
errors.push_back(ParserCode::kValueInvalidSize);
return false;
}
if (attr_type != raw_value.tag) {
errors.push_back(ParserCode::kValueMismatchTagConverted);
}
return true;
}
default:
errors.push_back(ParserCode::kValueMismatchTagOmitted);
return false;
}
}
template <>
bool LoadAttrValue<DateTime>(ValueTag attr_type,
const RawValue& raw_value,
DateTime& output,
std::vector<ParserCode>& errors) {
if (raw_value.tag != ValueTag::dateTime) {
errors.push_back(ParserCode::kValueMismatchTagOmitted);
return false;
}
if (!LoadDateTime(raw_value.data, &output)) {
errors.push_back(ParserCode::kValueInvalidSize);
return false;
}
return true;
}
template <>
bool LoadAttrValue<Resolution>(ValueTag attr_type,
const RawValue& raw_value,
Resolution& output,
std::vector<ParserCode>& errors) {
if (raw_value.tag != ValueTag::resolution) {
errors.push_back(ParserCode::kValueMismatchTagOmitted);
return false;
}
if (!LoadResolution(raw_value.data, &output)) {
errors.push_back(ParserCode::kValueInvalidSize);
return false;
}
return true;
}
template <>
bool LoadAttrValue<RangeOfInteger>(ValueTag attr_type,
const RawValue& raw_value,
RangeOfInteger& output,
std::vector<ParserCode>& errors) {
if (raw_value.tag == ValueTag::integer) {
if (!LoadInteger<4>(raw_value.data, &output.min_value)) {
errors.push_back(ParserCode::kValueInvalidSize);
return false;
}
output.max_value = output.min_value;
return true;
}
if (raw_value.tag != ValueTag::rangeOfInteger) {
errors.push_back(ParserCode::kValueMismatchTagOmitted);
return false;
}
if (!LoadRangeOfInteger(raw_value.data, &output)) {
errors.push_back(ParserCode::kValueInvalidSize);
return false;
}
return true;
}
template <>
bool LoadAttrValue<StringWithLanguage>(ValueTag attr_type,
const RawValue& raw_value,
StringWithLanguage& output,
std::vector<ParserCode>& errors) {
if (raw_value.tag == ValueTag::nameWithLanguage ||
raw_value.tag == ValueTag::textWithLanguage) {
if (!LoadStringWithLanguage(raw_value.data, &output)) {
errors.push_back(ParserCode::kValueInvalidSize);
return false;
}
if (raw_value.tag != attr_type) {
errors.push_back(ParserCode::kValueMismatchTagConverted);
}
return true;
}
if (IsString(raw_value.tag)) {
output.language.clear();
output.value = LoadString(raw_value.data);
if (raw_value.tag == ValueTag::nameWithoutLanguage &&
attr_type != ValueTag::nameWithLanguage) {
errors.push_back(ParserCode::kValueMismatchTagConverted);
}
if (raw_value.tag == ValueTag::textWithoutLanguage &&
attr_type != ValueTag::textWithLanguage) {
errors.push_back(ParserCode::kValueMismatchTagConverted);
}
return true;
}
errors.push_back(ParserCode::kValueMismatchTagOmitted);
return false;
}
// Parse an attribute of type `attr_type` from `raw_attr` and add it to `coll`
// when possible. Return a list of parser errors. `coll` must not be nullptr.
template <typename ApiType>
std::vector<ParserCode> LoadAttrValues(Collection* coll,
ValueTag attr_type,
const RawAttribute& raw_attr) {
std::vector<ParserCode> errors;
std::vector<ApiType> vals;
vals.reserve(raw_attr.values.size());
for (const RawValue& raw_value : raw_attr.values) {
ApiType val;
if (LoadAttrValue<ApiType>(attr_type, raw_value, val, errors)) {
vals.push_back(std::move(val));
}
}
if (vals.empty()) {
errors.push_back(ParserCode::kAttributeNoValues);
} else {
const Code err = coll->AddAttr(raw_attr.name, attr_type, vals);
if (err != Code::kOK) {
errors.push_back(ParserCode::kErrorWhenAddingAttribute);
}
}
return errors;
}
bool Parser::SaveFrameToPackage(Frame* package) {
for (size_t i = 0; i < frame_->groups_tags_.size(); ++i) {
GroupTag gn = frame_->groups_tags_[i];
parser_context_ = AttrPath(gn);
parser_context_.PushBack(package->Groups(gn).size(), "");
CollsView::iterator coll;
Code err = package->AddGroup(gn, coll);
if (err != Code::kOK) {
LogParserError(ParserCode::kErrorWhenAddingGroup);
continue;
}
RawCollection raw_coll;
if (!ParseRawGroup(&(frame_->groups_content_[i]), &raw_coll)) {
return false;
}
DecodeCollection(&raw_coll, &*coll);
}
package->SetData(std::move(frame_->data_));
return true;
}
bool Parser::ReadFrameFromBuffer(const uint8_t* ptr,
const uint8_t* const buf_end) {
parser_context_ = AttrPath(AttrPath::kHeader);
buffer_begin_ = ptr;
buffer_end_ = buf_end;
if (buf_end - ptr < 9) {
LogParserError(ParserCode::kUnexpectedEndOfFrame, ptr);
return false;
}
frame_->version_ = ParseUInt16(ptr);
ParseSignedInteger<2>(&ptr, &frame_->operation_id_or_status_code_);
ParseSignedInteger<4>(&ptr, &frame_->request_id_);
while (*ptr != end_of_attributes_tag) {
GroupTag group_tag = static_cast<GroupTag>(*ptr);
parser_context_ = AttrPath(group_tag);
if (!IsValid(group_tag)) {
// begin-attribute-group-tag was expected.
LogParserError(ParserCode::kGroupTagWasExpected, ptr);
return false;
}
if (frame_->groups_tags_.size() >= kMaxCountOfAttributeGroups) {
LogParserError(ParserCode::kLimitOnGroupsCountExceeded, ptr);
return false;
}
frame_->groups_tags_.push_back(group_tag);
frame_->groups_content_.resize(frame_->groups_tags_.size());
++ptr;
if (!ReadTNVsFromBuffer(&ptr, buf_end, &(frame_->groups_content_.back())))
return false;
if (ptr >= buf_end) {
// begin-attribute-group-tag or end-of-attributes-tag was expected.
LogParserError(ParserCode::kUnexpectedEndOfFrame);
return false;
}
}
++ptr;
frame_->data_.assign(ptr, buf_end);
ptr = buf_end;
return true;
}
// Parses TNVs from given buffer until the end of the buffer is reached or next
// begin-attribute-group-tag is spotted. The pointer ptr is shifted accordingly.
// returns true <=> (ptr == buf_end) or (*ptr is begin-attribute-group-tag)
// returns false <=> parsing error occurs, ptr points to incorrect field
// Parsed TNVs are added to the end of |tnvs|. If |tnvs| is nullptr then no
// output is saved but parsing occurs as usual.
bool Parser::ReadTNVsFromBuffer(const uint8_t** ptr2,
const uint8_t* const buf_end,
std::list<TagNameValue>* tnvs) {
const uint8_t*& ptr = *ptr2;
while ((ptr < buf_end) && (*ptr > max_begin_attribute_group_tag)) {
TagNameValue tnv;
if (buf_end - ptr < 5) {
// Expected at least 1-byte tag, 2-bytes name-length and 2-bytes
// value-length.
LogParserError(ParserCode::kUnexpectedEndOfFrame, ptr);
return false;
}
// *ptr is a ValueTag because it was already checked in while (...)
tnv.tag = *ptr;
++ptr;
int length = 0;
if (!ParseUnsignedInteger<2>(&ptr, &length)) {
LogParserError(ParserCode::kNegativeNameLengthInTNV, ptr);
return false;
}
if (buf_end - ptr < length + 2) {
// Expected at least `length`-bytes name and 2-bytes value-length.
LogParserError(ParserCode::kUnexpectedEndOfFrame, ptr);
return false;
}
tnv.name.assign(ptr, ptr + length);
ptr += length;
if (!ParseUnsignedInteger<2>(&ptr, &length)) {
LogParserError(ParserCode::kNegativeValueLengthInTNV, ptr);
return false;
}
if (buf_end - ptr < length) {
LogParserError(ParserCode::kUnexpectedEndOfFrame, ptr);
return false;
}
tnv.value.assign(ptr, ptr + length);
ptr += length;
if (tnvs != nullptr)
tnvs->push_back(std::move(tnv));
}
return true;
}
void Parser::ResetContent() {
buffer_begin_ = nullptr;
buffer_end_ = nullptr;
parser_context_ = AttrPath(AttrPath::kHeader);
}
// Parses single attribute's value and add it to |attr|. |tnv| is the first TNV
// with the value, |tnvs| contains all following TNVs. Both |tnvs| and |attr|
// cannot be nullptr. |coll_level| denotes how "deep" is the collection that
// contains the attribute; attributes defined directly in the attributes group
// have level 0. Returns false <=> critical parsing error was spotted.
// See section 3.5.2 from rfc8010 for details.
bool Parser::ParseRawValue(int coll_level,
const TagNameValue& tnv,
std::list<TagNameValue>* tnvs,
RawAttribute* attr) {
// Is it correct attribute's value tag? If not then fail.
if (tnv.tag == endCollection_value_tag ||
tnv.tag == memberAttrName_value_tag) {
LogParserError(ParserCode::kTNVWithUnexpectedValueTag);
return false;
}
// Is it a collection ?
if (tnv.tag == begCollection_value_tag) {
ContextPathGuard path_update(&parser_context_, attr->values.size());
if (!tnv.value.empty()) {
LogParserError(ParserCode::kEmptyValueExpectedInTNV);
return false;
}
std::unique_ptr<RawCollection> coll(new RawCollection);
if (!ParseRawCollection(coll_level + 1, tnvs, coll.get()))
return false;
attr->values.emplace_back(coll.release());
return true;
}
ValueTag type = static_cast<ValueTag>(tnv.tag);
if (!IsValid(type)) {
// unknown attribute's syntax
LogParserError(ParserCode::kUnsupportedValueTag);
return true;
}
attr->values.emplace_back(type, tnv.value);
return true;
}
// Parses single collections from given TNVs. |coll_level| denotes how "deep"
// the collection is; collections defined directly in the attributes group
// have level 1. Both |tnvs| and |coll| cannot be nullptr.
// Returns false <=> critical parsing error was spotted.
bool Parser::ParseRawCollection(int coll_level,
std::list<TagNameValue>* tnvs,
RawCollection* coll) {
if (coll_level > kMaxCollectionLevel) {
LogParserError(ParserCode::kLimitOnCollectionsLevelExceeded);
return false;
}
while (true) {
if (tnvs->empty()) {
LogParserError(ParserCode::kUnexpectedEndOfGroup);
return false;
}
TagNameValue tnv = tnvs->front();
tnvs->pop_front();
// exit if the end of the collection was reached
if (tnv.tag == endCollection_value_tag) {
if (!tnv.name.empty()) {
LogParserError(ParserCode::kEmptyNameExpectedInTNV);
return false;
}
if (!tnv.value.empty()) {
LogParserError(ParserCode::kEmptyValueExpectedInTNV);
return false;
}
return true;
}
// still here, so we parse an attribute (collection's member)
if (tnv.tag != memberAttrName_value_tag) {
LogParserError(ParserCode::kTNVWithUnexpectedValueTag);
return false;
}
// parse name & create attribute
const std::string name = LoadString(tnv.value);
parser_context_.Back().attribute_name = name;
if (!tnv.name.empty()) {
LogParserError(ParserCode::kEmptyNameExpectedInTNV);
return false;
}
if (name.empty()) {
LogParserErrors({ParserCode::kAttributeNameIsEmpty});
return false;
}
coll->attributes.emplace_back(name);
RawAttribute* attr = &coll->attributes.back();
// parse tag
if (tnvs->empty()) {
LogParserError(ParserCode::kUnexpectedEndOfGroup);
return false;
}
// parse all values
while (!tnvs->empty() && tnvs->front().tag != endCollection_value_tag &&
tnvs->front().tag != memberAttrName_value_tag) {
tnv = tnvs->front();
tnvs->pop_front();
if (!tnv.name.empty()) {
LogParserError(ParserCode::kEmptyNameExpectedInTNV);
return false;
}
if (!ParseRawValue(coll_level, tnv, tnvs, attr))
return false;
}
}
}
// Parses attributes group from given TNVs and saves it to |coll|. Both |tnvs|
// and |coll| cannot be nullptr. Returns false <=> critical parsing error was
// spotted.
bool Parser::ParseRawGroup(std::list<TagNameValue>* tnvs, RawCollection* coll) {
while (!tnvs->empty()) {
TagNameValue tnv = tnvs->front();
tnvs->pop_front();
// parse name & create attribute
const std::string name = LoadString(tnv.name);
parser_context_.Back().attribute_name = name;
if (name.empty()) {
LogParserErrors({ParserCode::kAttributeNameIsEmpty});
return false;
}
coll->attributes.emplace_back(name);
RawAttribute* attr = &coll->attributes.back();
// parse all values
while (true) {
// parse value
if (!ParseRawValue(0 /*collection level*/, tnv, tnvs, attr))
return false;
// go to the next value or attribute
if (tnvs->empty() || !tnvs->front().name.empty())
break; // end of the attribute
// next value
tnv = tnvs->front();
tnvs->pop_front();
}
}
return true;
}
// Converts a collection/group saved in |raw_coll| to |coll|. Both |raw_coll|
// and |coll| must not be nullptr.
void Parser::DecodeCollection(RawCollection* raw_coll, Collection* coll) {
for (RawAttribute& raw_attr : raw_coll->attributes) {
// Tries to match the attribute to existing one by name.
parser_context_.Back().attribute_name = raw_attr.name;
if (coll->GetAttr(raw_attr.name) != coll->end()) {
// The attribute exists.
LogParserErrors({ParserCode::kAttributeNameConflict});
continue;
}
if (raw_attr.values.empty()) {
LogParserErrors({ParserCode::kAttributeNoValues});
continue;
}
// Tries to detect an attribute's type.
ValueTag detected_type = raw_attr.values.front().tag;
for (auto& raw_val : raw_attr.values)
if (IsConvertibleTo(detected_type, raw_val.tag))
detected_type = raw_val.tag;
// Is it an attribute with Ouf-Of-Band value? Then set it and finish.
if (IsOutOfBand(detected_type)) {
if (raw_attr.values.size() > 1) {
LogParserError(ParserCode::kOutOfBandAttributeWithManyValues);
}
if (!raw_attr.values.front().data.empty()) {
LogParserError(ParserCode::kOutOfBandValueWithNonEmptyData);
}
const Code err = coll->AddAttr(raw_attr.name, detected_type);
if (err != Code::kOK) {
LogParserErrors({ParserCode::kErrorWhenAddingAttribute});
}
continue;
}
// It is a collection?
if (detected_type == ValueTag::collection) {
std::vector<ParserCode> errors;
std::vector<RawCollection*> raw_colls;
raw_colls.reserve(raw_attr.values.size());
for (const RawValue& raw_value : raw_attr.values) {
if (raw_value.collection) {
raw_colls.push_back(raw_value.collection.get());
} else {
errors.push_back(ParserCode::kValueMismatchTagOmitted);
}
}
CollsView colls;
Code err = coll->AddAttr(raw_attr.name, raw_colls.size(), colls);
if (err == Code::kOK) {
for (size_t i = 0; i < colls.size(); ++i) {
ContextPathGuard path_update(&parser_context_, i);
DecodeCollection(raw_colls[i], &colls[i]);
}
} else {
errors.push_back(ParserCode::kErrorWhenAddingAttribute);
}
LogParserErrors(errors);
continue;
}
// It is an attribute with standard values. Parse the values and create
// a new attribute.
if (IsInteger(detected_type)) {
LogParserErrors(LoadAttrValues<int32_t>(coll, detected_type, raw_attr));
continue;
}
if (IsString(detected_type) || detected_type == ValueTag::octetString) {
LogParserErrors(
LoadAttrValues<std::string>(coll, detected_type, raw_attr));
continue;
}
switch (detected_type) {
case ValueTag::dateTime:
LogParserErrors(
LoadAttrValues<DateTime>(coll, detected_type, raw_attr));
break;
case ValueTag::resolution:
LogParserErrors(
LoadAttrValues<Resolution>(coll, detected_type, raw_attr));
break;
case ValueTag::rangeOfInteger:
LogParserErrors(
LoadAttrValues<RangeOfInteger>(coll, detected_type, raw_attr));
break;
case ValueTag::nameWithLanguage:
case ValueTag::textWithLanguage:
LogParserErrors(
LoadAttrValues<StringWithLanguage>(coll, detected_type, raw_attr));
break;
default:
LogParserErrors({ParserCode::kErrorWhenAddingAttribute});
break;
}
}
}
} // namespace ipp