///|
pub(all) struct CoapUri {
host : String
port : Int
path : Array[Bytes]
query : Array[Bytes]
} derive(Eq, Debug)
///|
pub extend CoapUri with Eq::{equal, not_equal}
///|
pub extend CoapUri with @debug.Debug::{to_repr}
///|
pub fn percent_decode(text : String) -> Result[Bytes, Failure] {
let input = @utf8.encode(text)
let output : Array[Byte] = []
let mut cursor = 0
while cursor < input.length() {
if input[cursor] == b'%' {
if cursor > input.length() - 3 {
return Err(Syntax(cursor, "truncated percent escape"))
}
let high = hex_digit(input[cursor + 1].to_int())
let low = hex_digit(input[cursor + 2].to_int())
if high < 0 || low < 0 {
return Err(Syntax(cursor, "invalid percent escape"))
}
output.push((high * 16 + low).to_byte())
cursor = cursor + 3
} else {
let value = input[cursor].to_int()
if value < 32 || value == 127 || value == 32 {
return Err(Syntax(cursor, "unescaped whitespace or control character"))
}
output.push(input[cursor])
cursor = cursor + 1
}
}
Ok(Bytes::from_array(output))
}
///|
pub fn percent_encode(bytes : Bytes) -> String {
let digits = "0123456789ABCDEF"
let result : Array[Char] = []
for byte in bytes {
let value = byte.to_int()
if (value >= 65 && value <= 90) ||
(value >= 97 && value <= 122) ||
(value >= 48 && value <= 57) ||
value == 45 ||
value == 46 ||
value == 95 ||
value == 126 {
result.push(value.unsafe_to_char())
} else {
result.push('%')
result.push(digits.get_char(value >> 4).unwrap())
result.push(digits.get_char(value & 15).unwrap())
}
}
String::from_array(result)
}
///|
fn parse_port(text : String) -> Result[Int, Failure] {
if text.is_empty() {
return Err(Invalid("empty URI port"))
}
let mut value = 0
for character in text.iter() {
let digit = character.to_int() - 48
if digit < 0 || digit > 9 {
return Err(Invalid("URI port must be decimal"))
}
if value > (65535 - digit) / 10 {
return Err(Invalid("URI port exceeds 65535"))
}
value = value * 10 + digit
}
if value == 0 {
return Err(Invalid("URI port must be nonzero"))
}
Ok(value)
}
///|
fn split_uri_segments(
text : String,
separator : String,
) -> Result[Array[Bytes], Failure] {
let result : Array[Bytes] = []
for segment in text.split(separator) {
let value = match percent_decode(segment.to_owned()) {
Ok(value) => value
Err(error) => return Err(error)
}
if value.length() > 255 {
return Err(Capacity("URI segment exceeds option length"))
}
match decode_utf8(value) {
Err(error) => return Err(error)
Ok(_) => ()
}
result.push(value)
}
Ok(result)
}
///|
pub fn CoapUri::parse(
text : String,
limits : Limits,
) -> Result[CoapUri, Failure] {
match limits.validate() {
Err(error) => return Err(error)
Ok(_) => ()
}
if @utf8.encode(text).length() > limits.max_uri_bytes {
return Err(Capacity("URI exceeds configured limit"))
}
if !text.has_prefix("coap://") {
return Err(Unsupported("only coap:// URIs are supported"))
}
if text.contains("#") {
return Err(Invalid("CoAP request URI must not contain a fragment"))
}
let mut authority_end = text.length()
for i = 7; i < text.length(); i = i + 1 {
if text[i].to_int() == 47 || text[i].to_int() == 63 {
authority_end = i
break
}
}
let authority = text[7:authority_end].to_owned()
if authority.is_empty() || authority.contains("@") {
return Err(Invalid("empty host or unsupported userinfo"))
}
let mut host = ""
let mut port = 5683
if authority.has_prefix("[") {
let closing = match authority.find("]") {
Some(index) => index
None => return Err(Invalid("unclosed IPv6 address"))
}
host = authority[1:closing].to_owned()
if !host.contains(":") || host.contains("%") {
return Err(Invalid("invalid or scoped IPv6 literal"))
}
host = match canonical_ipv6(host) {
Err(e) => return Err(e)
Ok(v) => v
}
let suffix = authority[closing + 1:].to_owned()
if !suffix.is_empty() {
if !suffix.has_prefix(":") {
return Err(Invalid("unexpected bytes after IPv6 address"))
}
port = match parse_port(suffix[1:].to_owned()) {
Ok(value) => value
Err(error) => return Err(error)
}
}
} else {
match authority.split_once(":") {
None => host = authority
Some((name, port_text)) => {
host = name.to_owned()
port = match parse_port(port_text.to_owned()) {
Ok(value) => value
Err(error) => return Err(error)
}
}
}
}
if host.is_empty() || @utf8.encode(host).length() > 255 {
return Err(Invalid("invalid host length"))
}
for character in host.iter() {
let value = character.to_int()
if value <= 32 ||
value >= 127 ||
value == 47 ||
value == 63 ||
value == 91 ||
value == 93 ||
value == 37 {
return Err(Invalid("host must be an ASCII name or IP literal"))
}
}
let remainder = text[authority_end:].to_owned()
let (path_text, query_text) = match remainder.split_once("?") {
None => (remainder, None)
Some((path, query)) => (path.to_owned(), Some(query.to_owned()))
}
let path = if path_text.is_empty() || path_text == "/" {
[]
} else {
if !path_text.has_prefix("/") {
return Err(Invalid("absolute URI path must start with slash"))
}
match split_uri_segments(path_text[1:].to_owned(), "/") {
Ok(value) => value
Err(error) => return Err(error)
}
}
let normalized : Array[Bytes] = []
for i = 0; i < path.length(); i = i + 1 {
let part = path[i]
if part == b".." {
if !normalized.is_empty() {
normalized.remove(normalized.length() - 1) |> ignore
}
if i == path.length() - 1 && !normalized.is_empty() {
normalized.push(b"")
}
} else if part == b"." {
if i == path.length() - 1 && !normalized.is_empty() {
normalized.push(b"")
}
} else {
normalized.push(part)
}
}
let path = normalized
let query = match query_text {
None => []
Some(query) =>
match split_uri_segments(query, "&") {
Ok(value) => value
Err(error) => return Err(error)
}
}
if path.length() + query.length() + 2 > limits.max_options {
return Err(Capacity("URI produces too many options"))
}
Ok({ host: host.to_lower(), port, path, query, })
}
///|
pub fn CoapUri::to_options(self : CoapUri) -> Array[CoapOption] {
let options : Array[CoapOption] = [option_text(3, self.host)]
if self.port != 5683 {
options.push(option_uint(7, self.port.to_int64()).unwrap())
}
for value in self.path {
options.push({ number: 11, value, })
}
for value in self.query {
options.push({ number: 15, value, })
}
options
}
///|
pub fn CoapUri::to_string(self : CoapUri) -> String {
let authority = if self.host.contains(":") {
"[" + self.host + "]"
} else {
self.host
}
let mut text = "coap://" + authority
if self.port != 5683 {
text = text + ":" + self.port.to_string()
}
text = text + "/"
for i = 0; i < self.path.length(); i = i + 1 {
if i > 0 {
text = text + "/"
}
text = text + percent_encode(self.path[i])
}
for i = 0; i < self.query.length(); i = i + 1 {
text = text + (if i == 0 { "?" } else { "&" })
text = text + percent_encode(self.query[i])
}
text
}
///|
pub fn Request::from_uri(
verb : Method,
text : String,
limits : Limits,
) -> Result[(CoapUri, Request), Failure] {
match CoapUri::parse(text, limits) {
Err(error) => Err(error)
Ok(uri) =>
Ok(
(
uri,
{ verb, options: uri.to_options(), payload: b"", confirmable: true, },
),
)
}
}