| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "validator.h" |
| |
| #include <limits> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "attribute.h" |
| #include "errors.h" |
| #include "frame.h" |
| |
| namespace ipp { |
| namespace { |
| |
| constexpr std::string_view kAllowedCharsInKeyword = "-_."; |
| constexpr std::string_view kAllowedCharsInUri = ":/?#[]@!$&'()*+,;=-._~%"; |
| constexpr std::string_view kAllowedCharsInUriScheme = "+-."; |
| constexpr std::string_view kAllowedCharsInNaturalLanguage = "-"; |
| |
| bool IsLowercaseLetter(char c) { |
| return c >= 'a' && c <= 'z'; |
| } |
| |
| bool IsUppercaseLetter(char c) { |
| return c >= 'A' && c <= 'Z'; |
| } |
| |
| bool IsDigit(char c) { |
| return c >= '1' && c <= '9'; |
| } |
| |
| // Helper struct for string validation. |
| struct StringValidator { |
| // The input string to validate. |
| std::string_view value; |
| // Output set of error codes. |
| std::set<ValidatorCode> codes; |
| // Validates the string length. |
| void CheckLength(size_t max_length, bool empty_string_allowed = false) { |
| if (value.empty()) { |
| if (!empty_string_allowed) { |
| codes.insert(ValidatorCode::kStringEmpty); |
| } |
| return; |
| } |
| if (value.size() > max_length) { |
| codes.insert(ValidatorCode::kStringTooLong); |
| } |
| } |
| // Checks if the string starts from lowercase letter. It does nothing if |
| // the input string is empty. |
| void CheckFirstLetterIsLowercase() { |
| if (value.empty() || IsLowercaseLetter(value.front())) |
| return; |
| codes.insert(ValidatorCode::kStringMustStartLowercaseLetter); |
| } |
| // Checks if the input string consists only of letters, digits and |
| // characters from `allowed_chars`. |
| void CheckLettersDigits(std::string_view allowed_chars, |
| bool uppercase_letters_allowed = false) { |
| for (char c : value) { |
| if (IsLowercaseLetter(c)) |
| continue; |
| if (uppercase_letters_allowed && IsUppercaseLetter(c)) |
| continue; |
| if (IsDigit(c)) |
| continue; |
| if (allowed_chars.find(c) == std::string_view::npos) { |
| codes.insert(ValidatorCode::kStringInvalidCharacter); |
| break; |
| } |
| } |
| } |
| // Checks if the input string consists only of printable characters. |
| void CheckPrintable(bool uppercase_letters_allowed = false) { |
| for (char c : value) { |
| if (c < 0x20 || c > 0x7e) { |
| codes.insert(ValidatorCode::kStringInvalidCharacter); |
| break; |
| } |
| if (!uppercase_letters_allowed && IsUppercaseLetter(c)) { |
| codes.insert(ValidatorCode::kStringInvalidCharacter); |
| break; |
| } |
| } |
| } |
| }; |
| |
| // `year` must be > 0. |
| bool IsLeapYear(uint16_t year) { |
| if (year % 4) |
| return false; |
| // Is divisible by 4. |
| if (year % 100) |
| return true; |
| // Is divisible by 4 and 100. |
| return (year % 400 == 0); |
| } |
| |
| // Validate 'text' value based on: |
| // * rfc8011, section 5.1.2. |
| std::set<ValidatorCode> validateTextWithoutLanguage(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfText, /*empty_string_allowed=*/true); |
| return validator.codes; |
| } |
| |
| // Validate 'name' value based on: |
| // * rfc8011, section 5.1.3. |
| std::set<ValidatorCode> validateNameWithoutLanguage(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfName, /*empty_string_allowed=*/true); |
| return validator.codes; |
| } |
| |
| // Validate 'keyword' value based on: |
| // * rfc8011, section 5.1.4. |
| // * rfc8011 errata |
| std::set<ValidatorCode> validateKeyword(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfKeyword); |
| validator.CheckLettersDigits(kAllowedCharsInKeyword, |
| /*uppercase_letters_allowed=*/true); |
| return validator.codes; |
| } |
| |
| // Validate 'uri' value based on: |
| // * rfc8011, section 5.1.6; |
| // * rfc3986, section 2. |
| std::set<ValidatorCode> validateUri(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfUri); |
| validator.CheckLettersDigits(kAllowedCharsInUri, |
| /*uppercase_letters_allowed=*/true); |
| return validator.codes; |
| } |
| |
| // Validate 'uriScheme' value based on: |
| // * rfc8011, section 5.1.7; |
| // * rfc3986, section 3.1. |
| std::set<ValidatorCode> validateUriScheme(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfUriScheme); |
| validator.CheckFirstLetterIsLowercase(); |
| validator.CheckLettersDigits(kAllowedCharsInUriScheme); |
| return validator.codes; |
| } |
| |
| // Validate 'charset' value based on: |
| // * rfc8011, section 5.1.8; |
| // * https://www.iana.org/assignments/character-sets/character-sets.xhtml. |
| std::set<ValidatorCode> validateCharset(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfCharset); |
| validator.CheckPrintable(); |
| return validator.codes; |
| } |
| |
| // Validate 'naturalLanguage' value based on: |
| // * rfc8011, section 5.1.9; |
| // * rfc5646, section 2.1. |
| std::set<ValidatorCode> validateNaturalLanguage(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfNaturalLanguage); |
| validator.CheckLettersDigits(kAllowedCharsInNaturalLanguage); |
| return validator.codes; |
| } |
| |
| // Validate 'mimeMediaType' value based on: |
| // * rfc8011, section 5.1.10; |
| // * https://www.iana.org/assignments/media-types/media-types.xhtml. |
| std::set<ValidatorCode> validateMimeMediaType(std::string_view value) { |
| StringValidator validator = {value}; |
| validator.CheckLength(kMaxLengthOfMimeMediaType); |
| validator.CheckPrintable(/*uppercase_letters_allowed=*/true); |
| return validator.codes; |
| } |
| |
| // Validate 'octetString' value based on: |
| // * rfc8011, section 5.1.11. |
| std::set<ValidatorCode> validateOctetString(std::string_view value) { |
| std::set<ValidatorCode> codes; |
| if (value.size() > kMaxLengthOfOctetString) |
| codes.insert(ValidatorCode::kStringTooLong); |
| return codes; |
| } |
| |
| // Validate 'dateTime' value based on: |
| // * rfc8011, section 5.1.15; |
| // * DateAndTime defined in rfc2579, section 2; |
| // * also enforces 1970 <= year <= 2100. |
| std::set<ValidatorCode> validateDateTime(const DateTime& value) { |
| std::set<ValidatorCode> codes; |
| |
| // Verify the date. |
| if (value.year < 1970 || value.year > 2100 || value.month < 1 || |
| value.month > 12 || value.day < 1) { |
| codes.insert(ValidatorCode::kDateTimeInvalidDate); |
| } else { |
| uint8_t max_day = 31; |
| switch (value.month) { |
| case 2: |
| if (IsLeapYear(value.year)) { |
| max_day = 29; |
| } else { |
| max_day = 28; |
| } |
| break; |
| case 4: // FALLTHROUGH |
| case 6: // FALLTHROUGH |
| case 9: // FALLTHROUGH |
| case 11: |
| max_day = 30; |
| break; |
| } |
| if (value.day > max_day) { |
| codes.insert(ValidatorCode::kDateTimeInvalidDate); |
| } |
| } |
| |
| // Verify the time of day (seconds == 60 means leap second). |
| if (value.hour > 23 || value.minutes > 59 || value.seconds > 60 || |
| value.deci_seconds > 9) { |
| codes.insert(ValidatorCode::kDateTimeInvalidTimeOfDay); |
| } |
| |
| // Verify the timezone (daylight saving time in New Zealand is +13). |
| if ((value.UTC_direction != '-' && value.UTC_direction != '+') || |
| value.UTC_hours > 13 || value.UTC_minutes > 59) { |
| codes.insert(ValidatorCode::kDateTimeInvalidZone); |
| } |
| |
| return codes; |
| } |
| |
| // Validate 'resolution' value based on: |
| // * rfc8011, section 5.1.16. |
| std::set<ValidatorCode> validateResolution(Resolution value) { |
| std::set<ValidatorCode> codes; |
| if (value.units != Resolution::Units::kDotsPerCentimeter && |
| value.units != Resolution::Units::kDotsPerInch) |
| codes.insert(ValidatorCode::kResolutionInvalidUnit); |
| if (value.xres < 1 || value.yres < 1) |
| codes.insert(ValidatorCode::kResolutionInvalidDimension); |
| return codes; |
| } |
| |
| // Validate 'rangeOfInteger' value based on: |
| // * rfc8011, section 5.1.14. |
| std::set<ValidatorCode> validateRangeOfInteger(RangeOfInteger value) { |
| std::set<ValidatorCode> codes; |
| if (value.min_value > value.max_value) |
| codes.insert(ValidatorCode::kRangeOfIntegerMaxLessMin); |
| return codes; |
| } |
| |
| // Validate 'textWithLanguage' value based on: |
| // * rfc8011, section 5.1.2.2. |
| std::set<ValidatorCode> validateTextWithLanguage( |
| const StringWithLanguage& value) { |
| std::set<ValidatorCode> codes = validateTextWithoutLanguage(value.value); |
| if (!value.language.empty()) { |
| if (!validateNaturalLanguage(value.language).empty()) |
| codes.insert(ValidatorCode::kStringWithLangInvalidLanguage); |
| } |
| return codes; |
| } |
| |
| // Validate 'nameWithLanguage' value based on: |
| // * rfc8011, section 5.1.3.2. |
| std::set<ValidatorCode> validateNameWithLanguage( |
| const StringWithLanguage& value) { |
| std::set<ValidatorCode> codes = validateNameWithoutLanguage(value.value); |
| if (!value.language.empty()) { |
| if (!validateNaturalLanguage(value.language).empty()) |
| codes.insert(ValidatorCode::kStringWithLangInvalidLanguage); |
| } |
| return codes; |
| } |
| |
| // Validate a single value in `attribute`. `attribute` must not be nullptr and |
| // `value_index` must be a valid index. |
| std::set<ValidatorCode> ValidateValue(const Attribute* attribute, |
| size_t value_index) { |
| if (IsString(attribute->Tag())) { |
| std::string values_str; |
| attribute->GetValue(value_index, values_str); |
| switch (attribute->Tag()) { |
| case ValueTag::textWithoutLanguage: |
| return validateTextWithoutLanguage(values_str); |
| case ValueTag::nameWithoutLanguage: |
| return validateNameWithoutLanguage(values_str); |
| case ValueTag::keyword: |
| return validateKeyword(values_str); |
| case ValueTag::uri: |
| return validateUri(values_str); |
| case ValueTag::uriScheme: |
| return validateUriScheme(values_str); |
| case ValueTag::charset: |
| return validateCharset(values_str); |
| case ValueTag::naturalLanguage: |
| return validateNaturalLanguage(values_str); |
| case ValueTag::mimeMediaType: |
| return validateMimeMediaType(values_str); |
| default: |
| // There are no validation rules for other strings. |
| return {}; |
| } |
| } |
| switch (attribute->Tag()) { |
| case ValueTag::octetString: { |
| std::string value; |
| attribute->GetValue(value_index, value); |
| return validateOctetString(value); |
| } |
| case ValueTag::dateTime: { |
| DateTime value; |
| attribute->GetValue(value_index, value); |
| return validateDateTime(value); |
| } |
| case ValueTag::resolution: { |
| Resolution value; |
| attribute->GetValue(value_index, value); |
| return validateResolution(value); |
| } |
| case ValueTag::rangeOfInteger: { |
| RangeOfInteger value; |
| attribute->GetValue(value_index, value); |
| return validateRangeOfInteger(value); |
| } |
| case ValueTag::textWithLanguage: { |
| StringWithLanguage value; |
| attribute->GetValue(value_index, value); |
| return validateTextWithLanguage(value); |
| } |
| case ValueTag::nameWithLanguage: { |
| StringWithLanguage value; |
| attribute->GetValue(value_index, value); |
| return validateNameWithLanguage(value); |
| } |
| default: |
| // Other types does not need validation. |
| return {}; |
| } |
| } |
| |
| struct ValidationResult { |
| bool no_errors = true; |
| bool keep_going = true; |
| }; |
| |
| ValidationResult operator&&(ValidationResult vr1, ValidationResult vr2) { |
| ValidationResult result; |
| result.keep_going = vr1.keep_going && vr2.keep_going; |
| result.no_errors = vr1.no_errors && vr2.no_errors; |
| return result; |
| } |
| |
| ValidationResult ValidateCollections(ConstCollsView colls, |
| ValidatorLog& log, |
| AttrPath& path); |
| |
| ValidationResult ValidateAttribute(const Attribute* attr, |
| ValidatorLog& log, |
| AttrPath& path) { |
| ValidationResult result; |
| std::set<ValidatorCode> name_errors = validateKeyword(attr->Name()); |
| if (!name_errors.empty()) { |
| result.no_errors = false; |
| result.keep_going = |
| log.AddValidatorError({path, AttrError(std::move(name_errors))}); |
| if (!result.keep_going) |
| return result; |
| } |
| const size_t values_count = attr->Size(); |
| if (attr->Tag() == ValueTag::collection) { |
| result = result && ValidateCollections(attr->Colls(), log, path); |
| } else { |
| for (size_t i = 0; i < values_count; ++i) { |
| std::set<ValidatorCode> value_errors = ValidateValue(attr, i); |
| if (!value_errors.empty()) { |
| result.no_errors = false; |
| result.keep_going = log.AddValidatorError( |
| {path, AttrError(i, std::move(value_errors))}); |
| if (!result.keep_going) |
| return result; |
| } |
| } |
| } |
| return result; |
| } |
| |
| ValidationResult ValidateCollections(ConstCollsView colls, |
| ValidatorLog& log, |
| AttrPath& path) { |
| ValidationResult result; |
| uint16_t icoll = 0; |
| for (const Collection& coll : colls) { |
| for (const Attribute& attr : coll) { |
| path.PushBack(icoll, attr.Name()); |
| result = result && ValidateAttribute(&attr, log, path); |
| path.PopBack(); |
| if (!result.keep_going) |
| return result; |
| } |
| ++icoll; |
| } |
| return result; |
| } |
| |
| // Checks the values saved in the header of `frame`. |
| ValidationResult ValidateHeader(const Frame& frame, ValidatorLog& log) { |
| std::vector<std::string> invalid_fields; |
| invalid_fields.reserve(4); |
| uint8_t ver_major = static_cast<uint16_t>(frame.VersionNumber()) >> 8; |
| uint8_t ver_minor = static_cast<uint16_t>(frame.VersionNumber()) & 0xffu; |
| if (ver_major < 1 || ver_major > 9) { |
| invalid_fields.push_back("major-version-number"); |
| } |
| if (ver_minor > 9) { |
| invalid_fields.push_back("minor-version-number"); |
| } |
| if (frame.OperationIdOrStatusCode() < 0) { |
| invalid_fields.push_back("operation-id or status-code"); |
| } |
| if (frame.RequestId() < 1) { |
| invalid_fields.push_back("request-id"); |
| } |
| ValidationResult result; |
| result.no_errors = invalid_fields.empty(); |
| AttrError error = AttrError(0, {ValidatorCode::kIntegerOutOfRange}); |
| AttrPath path = AttrPath(AttrPath::kHeader); |
| for (const std::string& name : invalid_fields) { |
| path.PushBack(0, name); |
| result.keep_going = log.AddValidatorError({path, error}); |
| if (!result.keep_going) { |
| break; |
| } |
| path.PopBack(); |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| std::string_view ToStrView(ValidatorCode code) { |
| switch (code) { |
| case ValidatorCode::kStringEmpty: |
| return "StringEmpty"; |
| case ValidatorCode::kStringTooLong: |
| return "StringTooLong"; |
| case ValidatorCode::kStringMustStartLowercaseLetter: |
| return "StringMustStartLowercaseLetter"; |
| case ValidatorCode::kStringInvalidCharacter: |
| return "StringInvalidCharacter"; |
| case ValidatorCode::kStringWithLangInvalidLanguage: |
| return "StringWithLangInvalidLanguage"; |
| case ValidatorCode::kDateTimeInvalidDate: |
| return "DateTimeInvalidDate"; |
| case ValidatorCode::kDateTimeInvalidTimeOfDay: |
| return "DateTimeInvalidTimeOfDay"; |
| case ValidatorCode::kDateTimeInvalidZone: |
| return "DateTimeInvalidZone"; |
| case ValidatorCode::kResolutionInvalidUnit: |
| return "ResolutionInvalidUnit"; |
| case ValidatorCode::kResolutionInvalidDimension: |
| return "ResolutionInvalidDimension"; |
| case ValidatorCode::kRangeOfIntegerMaxLessMin: |
| return "RangeOfIntegerMaxLessMin"; |
| case ValidatorCode::kIntegerOutOfRange: |
| return "IntegerOutOfRange"; |
| } |
| } |
| |
| std::vector<ValidatorCode> AttrError::ErrorsAsVector() const { |
| return std::vector<ValidatorCode>(errors_.begin(), errors_.end()); |
| } |
| |
| std::string ToString(const ValidatorError& error) { |
| std::string msg = error.path.AsString(); |
| if (error.error.IsInTheName()) { |
| msg += "; name; "; |
| } else { |
| msg += "; "; |
| msg += std::to_string(error.error.Index()); |
| msg += "; "; |
| } |
| const std::vector<ValidatorCode> codes = error.error.ErrorsAsVector(); |
| for (ValidatorCode code : codes) { |
| if (msg.back() != '\t') { |
| msg += ","; |
| } |
| msg += ToStrView(code); |
| } |
| return msg; |
| } |
| |
| bool SimpleValidatorLog::AddValidatorError(const ValidatorError& error) { |
| if (entries_.size() < max_entries_count_) { |
| entries_.emplace_back(error); |
| } |
| return (entries_.size() < max_entries_count_); |
| } |
| |
| bool Validate(const Frame& frame, ValidatorLog& log) { |
| ValidationResult result = ValidateHeader(frame, log); |
| if (!result.keep_going) |
| return result.no_errors; |
| for (GroupTag group_tag : kGroupTags) { |
| size_t coll_index = 0; |
| for (const Collection& coll : frame.Groups(group_tag)) { |
| AttrPath path(group_tag); |
| for (const Attribute& attr : coll) { |
| path.PushBack(coll_index, attr.Name()); |
| result = result && ValidateAttribute(&attr, log, path); |
| path.PopBack(); |
| if (!result.keep_going) |
| return result.no_errors; |
| } |
| ++coll_index; |
| } |
| } |
| return result.no_errors; |
| } |
| |
| } // namespace ipp |