///|
/// Errors produced when a value fails RSS-spec validation,
/// mirroring `rss::validation::ValidationError`.
pub(all) suberror ValidationError {
/// A wrapped lower-level parse failure (URL, date, integer…).
Invalid(String)
/// A spec rule was violated.
Validation(String)
} derive(Debug, Eq)
///|
pub impl Show for ValidationError with fn output(self, logger) -> Unit {
match self {
Invalid(msg) => logger.write_string(msg)
Validation(msg) => logger.write_string(msg)
}
}
///|
fn fail_validation(msg : String) -> ValidationError {
Validation(msg)
}
///|
/// Lenient URL syntax check in the spirit of the `url` crate:
/// `scheme:` followed by a non-hierarchical or hierarchical part.
/// Requires `scheme://rest` with a reasonable scheme and non-empty rest.
fn is_valid_url(s : String) -> Bool {
let mut colon = -1
let limit = s.length() - 3
let mut i = 0
while i <= limit {
if s[i] == (':' : UInt16) && substring_eq(s, i, "://") {
colon = i
break
}
i += 1
}
if colon < 0 {
return false
}
if colon == 0 {
return false
}
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
let first = s[0].to_int()
let is_alpha = fn(c : Int) -> Bool {
(c >= 97 && c <= 122) || (c >= 65 && c <= 90)
}
let is_scheme_char = fn(c : Int) -> Bool {
is_alpha(c) || (c >= 48 && c <= 57) || c == 43 || c == 45 || c == 46
}
if !is_alpha(first) {
return false
}
for i in 1.. colon + 3
}
///|
fn validate_url(s : String, what~ : String) -> Unit raise ValidationError {
if !is_valid_url(s) {
raise Invalid("\{what}: invalid url \"\{s}\"")
}
}
///|
let months : Array[String] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]
///|
let weekdays : Array[String] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
///|
/// Parse an RFC 822 / RFC 2822 date-time, returning its components.
/// Accepts the common forms used by feeds:
/// `[Weekday,] DD Mon YYYY HH:MM[:SS] Zone`.
pub fn parse_rfc2822(s : String) -> Bool {
let parts : Array[String] = []
let mut current = StringBuilder()
let mut had_content = false
for c in s {
if c == ' ' || c == '\t' || c == ',' {
if had_content {
parts.push(current.to_string())
current = StringBuilder()
had_content = false
}
} else {
current.write_char(c)
had_content = true
}
}
if had_content {
parts.push(current.to_string())
}
let mut i = 0
// Optional trailing-comment "(...)" sections and weekday names.
while i < parts.length() {
let p = parts[i]
let mut is_weekday = false
for wd in weekdays {
if p.length() >= 3 && substring_eq(p, 0, wd) {
is_weekday = true
break
}
}
if is_weekday {
i += 1
} else {
break
}
}
guard i < parts.length() else { return false }
// Day: 1-2 digits.
let day = parts[i]
guard day.length() >= 1 && day.length() <= 2 else { return false }
for c in day {
guard c >= '0' && c <= '9' else { return false }
}
let day_num = digits_to_int(day)
guard day_num >= 1 && day_num <= 31 else { return false }
i += 1
guard i < parts.length() else { return false }
// Month name.
let mon = parts[i]
guard mon.length() == 3 else { return false }
let mut found_month = false
for m in months {
if m == mon {
found_month = true
break
}
}
guard found_month else { return false }
i += 1
guard i < parts.length() else { return false }
// Year: 2 or 4 digits.
let year = parts[i]
guard year.length() == 2 || year.length() == 4 else { return false }
for c in year {
guard c >= '0' && c <= '9' else { return false }
}
i += 1
guard i < parts.length() else { return false }
// Time HH:MM[:SS].
let time = parts[i]
let tparts = split_char(time, ':')
guard tparts.length() == 2 || tparts.length() == 3 else { return false }
for tp in tparts {
guard tp.length() == 2 else { return false }
for c in tp {
guard c >= '0' && c <= '9' else { return false }
}
}
i += 1
// Optional zone.
if i < parts.length() {
let zone = parts[i]
if !(zone is "UT" ||
zone is "GMT" ||
zone is "UTC" ||
zone is "EST" ||
zone is "EDT" ||
zone is "CST" ||
zone is "CDT" ||
zone is "MST" ||
zone is "MDT" ||
zone is "PST" ||
zone is "PDT") {
// numeric +/-HHMM
guard zone.length() == 5 else { return false }
let z0 = zone[0].to_int()
guard z0 == 43 || z0 == 45 else { return false }
for k in 1..<5 {
let zc = zone[k].to_int()
guard zc >= 48 && zc <= 57 else { return false }
}
}
}
true
}
///|
fn digits_to_int(s : String) -> Int {
let mut v = 0
for c in s {
if c >= '0' && c <= '9' {
v = v * 10 + (c.to_int() - '0'.to_int())
}
}
v
}
///|
fn split_char(s : String, sep : Char) -> Array[String] {
let out : Array[String] = []
let mut start = 0
for i in 0.. Bool {
if s.length() == 0 {
return false
}
for c in s {
guard c >= '0' && c <= '9' else { return false }
}
true
}
///|
fn is_mime_type(s : String) -> Bool {
let parts = split_char(s, '/')
if parts.length() != 2 {
return false
}
for p in parts {
if p.length() == 0 {
return false
}
for c in p {
let ci = c.to_int()
let ok = (ci >= 97 && ci <= 122) ||
(ci >= 65 && ci <= 90) ||
(ci >= 48 && ci <= 57) ||
ci == 33 ||
ci == 35 ||
ci == 36 ||
ci == 38 ||
ci == 45 ||
ci == 43 ||
ci == 46 ||
ci == 94 ||
ci == 95 ||
ci == 96 ||
ci == 124 ||
ci == 126
guard ok else { return false }
}
}
true
}
///|
/// Validate this category against the RSS spec.
pub fn Category::validate(self : Category) -> Unit raise ValidationError {
if self.domain is Some(domain) {
validate_url(domain, what="category domain")
}
}
///|
/// Validate this cloud against the RSS spec.
pub fn Cloud::validate(self : Cloud) -> Unit raise ValidationError {
guard is_positive_int(self.port) else {
raise fail_validation("Cloud port must be greater than 0")
}
validate_url(self.domain, what="cloud domain")
if !(self.protocol is "xml-rpc" ||
self.protocol is "soap" ||
self.protocol is "http-post") {
raise fail_validation("Unknown cloud protocol: \{self.protocol}")
}
}
///|
/// Validate this enclosure against the RSS spec.
pub fn Enclosure::validate(self : Enclosure) -> Unit raise ValidationError {
validate_url(self.url, what="enclosure url")
if !is_mime_type(self.mime_type) {
raise Invalid("enclosure mime_type: invalid mime type")
}
let length = int_or_fail(self.length)
guard length > 0 else {
raise fail_validation("Enclosure length is not greater than 0")
}
}
///|
/// Validate this text input against the RSS spec.
pub fn TextInput::validate(self : TextInput) -> Unit raise ValidationError {
validate_url(self.link, what="textInput link")
}
///|
/// Validate this image against the RSS spec.
pub fn Image::validate(self : Image) -> Unit raise ValidationError {
validate_url(self.link, what="image link")
validate_url(self.url, what="image url")
if self.width is Some(width) {
let w = int_or_fail(width)
guard w >= 0 && w <= 144 else {
raise fail_validation("Image width is not between 0 and 144")
}
}
if self.height is Some(height) {
let h = int_or_fail(height)
guard h >= 0 && h <= 144 else {
raise fail_validation("Image height is not between 0 and 144")
}
}
}
///|
fn int_or_fail(s : String) -> Int raise ValidationError {
@strconv.parse_int(s) catch {
_ => raise Invalid("invalid integer \"\{s}\"")
}
}
///|
/// Validate this source against the RSS spec.
pub fn Source::validate(self : Source) -> Unit raise ValidationError {
validate_url(self.url, what="source url")
}
///|
/// Validate this item against the RSS spec.
pub fn Item::validate(self : Item) -> Unit raise ValidationError {
if self.link is Some(link) {
validate_url(link, what="item link")
}
if self.comments is Some(comments) {
validate_url(comments, what="item comments")
}
if self.enclosure is Some(enclosure) {
enclosure.validate()
}
if self.pub_date is Some(pub_date) {
guard parse_rfc2822(pub_date) else {
raise Invalid("item pub_date: invalid RFC2822 string")
}
}
if self.source is Some(source) {
source.validate()
}
}
///|
/// Validate this channel against the RSS specification.
pub fn Channel::validate(self : Channel) -> Unit raise ValidationError {
validate_url(self.link, what="channel link")
for category in self.categories {
category.validate()
}
if self.cloud is Some(cloud) {
cloud.validate()
}
if self.docs is Some(docs) {
validate_url(docs, what="channel docs")
}
if self.image is Some(image) {
image.validate()
}
for item in self.items {
item.validate()
}
if self.last_build_date is Some(date) {
guard parse_rfc2822(date) else {
raise Invalid("channel last_build_date: invalid RFC2822 string")
}
}
if self.pub_date is Some(date) {
guard parse_rfc2822(date) else {
raise Invalid("channel pub_date: invalid RFC2822 string")
}
}
for hour in self.skip_hours {
let h = int_or_fail(hour)
guard h >= 0 && h <= 23 else {
raise fail_validation("Channel skip hour is not between 0 and 23")
}
}
for day in self.skip_days {
if !(day is "Monday" ||
day is "Tuesday" ||
day is "Wednesday" ||
day is "Thursday" ||
day is "Friday" ||
day is "Saturday" ||
day is "Sunday") {
raise fail_validation("Unknown skip day: \{day}")
}
}
if self.text_input is Some(text_input) {
text_input.validate()
}
if self.ttl is Some(ttl) {
let t = int_or_fail(ttl)
guard t > 0 else {
raise fail_validation("Channel TTL is not greater than 0")
}
}
}