///|
fn is_etag_char(c : Char) -> Bool {
let code = c.to_int()
code == 33 || (code >= 35 && code <= 126) || code >= 128
}
///|
fn parse_opaque_tag(text : String, item : Int) -> Result[String, ParseError] {
if text.length() < 2 ||
text.get_char(0) != Some('"') ||
text.get_char(text.length() - 1) != Some('"') {
return Err(InvalidParameter(item, text))
}
let inner = text[1:text.length() - 1].to_owned()
for c in inner.iter() {
if !is_etag_char(c) {
return Err(InvalidToken(item, "etag", text))
}
}
Ok(inner)
}
///|
pub fn parse_etag(value : StringView) -> Result[EntityTag, ParseError] {
let raw = clean(value)
if raw.has_prefix("W/") || raw.has_prefix("w/") {
let rest = raw[2:].to_owned()
match parse_opaque_tag(rest, 0) {
Ok(tag) => Ok({ weak: true, tag, raw })
Err(err) => Err(err)
}
} else {
match parse_opaque_tag(raw, 0) {
Ok(tag) => Ok({ weak: false, tag, raw })
Err(err) => Err(err)
}
}
}
///|
pub fn parse_etag_list(
value : StringView,
) -> Result[Array[EntityTag], ParseError] {
let text = clean(value)
let tags : Array[EntityTag] = []
if text.is_empty() {
return Ok(tags)
}
if text == "*" {
return Ok([{ weak: false, tag: "*", raw: "*" }])
}
let parts = split_quoted(text, ',')
for i in 0.. tags.push(tag)
Err(_) => return Err(InvalidParameter(i, part))
}
}
Ok(tags)
}
///|
pub fn format_etag(tag : EntityTag) -> String {
if tag.raw == "*" {
return "*"
}
let buf = StringBuilder()
if tag.weak {
buf.write_string("W/")
}
buf.write_char('"')
buf.write_string(tag.tag)
buf.write_char('"')
buf.to_string()
}
///|
pub fn strong_etag_match(left : EntityTag, right : EntityTag) -> Bool {
!left.weak && !right.weak && left.tag == right.tag
}
///|
pub fn weak_etag_match(left : EntityTag, right : EntityTag) -> Bool {
left.tag == right.tag
}
///|
pub fn etag_list_matches(
header_value : StringView,
current : EntityTag,
strong? : Bool = false,
) -> Result[Bool, ParseError] {
let tags = match parse_etag_list(header_value) {
Ok(tags) => tags
Err(err) => return Err(err)
}
for tag in tags {
if tag.raw == "*" {
return Ok(true)
}
if strong {
if strong_etag_match(tag, current) {
return Ok(true)
}
} else if weak_etag_match(tag, current) {
return Ok(true)
}
}
Ok(false)
}
///|
pub fn evaluate_if_none_match(
http_method : StringView,
header_value : StringView,
current : EntityTag,
) -> Result[ConditionalDecision, ParseError] {
let matched = match etag_list_matches(header_value, current) {
Ok(value) => value
Err(err) => return Err(err)
}
if !matched {
return Ok({
matched: false,
status: 200,
reason: "if-none-match did not match current entity tag",
})
}
let normalized_method = clean(http_method).to_upper()
if normalized_method == "GET" || normalized_method == "HEAD" {
Ok({
matched: true,
status: 304,
reason: "if-none-match matched for safe retrieval method",
})
} else {
Ok({
matched: true,
status: 412,
reason: "if-none-match matched for state-changing method",
})
}
}
///|
pub fn evaluate_if_match(
header_value : StringView,
current : EntityTag,
) -> Result[ConditionalDecision, ParseError] {
let matched = match etag_list_matches(header_value, current, strong=true) {
Ok(value) => value
Err(err) => return Err(err)
}
if matched {
Ok({
matched: true,
status: 200,
reason: "if-match matched current entity tag",
})
} else {
Ok({
matched: false,
status: 412,
reason: "if-match did not match current entity tag",
})
}
}
///|
pub fn format_conditional_decision(decision : ConditionalDecision) -> String {
let buf = StringBuilder()
buf.write_string("matched=")
buf.write_string(decision.matched.to_string())
buf.write_string(", status=")
buf.write_string(decision.status.to_string())
buf.write_string(", ")
buf.write_string(decision.reason)
buf.to_string()
}