///|
pub(all) struct PrinterUri {
scheme : String
host : String
port : Int32
path : String
ipv6 : Bool
} derive(Eq, Debug)
///|
pub extend PrinterUri with Eq::{equal, not_equal}
///|
pub extend PrinterUri with @debug.Debug::{to_repr}
///|
fn parse_decimal(text : String, maximum : Int32) -> Int32 raise IppError {
if text.is_empty() {
raise Invalid("empty integer")
}
let mut value = 0
for char in text.to_array() {
let digit = char.to_int() - 48
if digit < 0 || digit > 9 {
raise Invalid("invalid integer")
}
if value > (maximum - digit) / 10 {
raise Invalid("integer exceeds maximum \{maximum}")
}
value = value * 10 + digit
}
value
}
///|
fn string_slice(text : String, start : Int32, end : Int32) -> String {
text.exact_view(start~, end~).to_owned()
}
///|
fn valid_ipv4_literal(text : String) -> Bool {
let parts = text.split(".").to_array()
if parts.length() != 4 {
return false
}
for part in parts {
if part.is_empty() ||
part.length() > 3 ||
(part.length() > 1 && part[0] == '0') {
return false
}
let mut number = 0
for char in part {
let digit = char.to_int() - 48
if digit < 0 || digit > 9 {
return false
}
number = number * 10 + digit
}
if number > 255 {
return false
}
}
true
}
///|
fn ipv6_units(text : String, allow_ipv4 : Bool) -> Int32? {
if text.is_empty() {
return Some(0)
}
let parts = text.split(":").to_array()
let mut count = 0
for index, part in parts {
if part.contains(".") {
if !allow_ipv4 ||
index != parts.length() - 1 ||
!valid_ipv4_literal(part.to_owned()) {
return None
}
count += 2
} else {
if part.is_empty() || part.length() > 4 {
return None
}
let word = part.to_owned()
for i in 0.. Bool {
match host.split_once("::") {
Some((left, right)) => {
if right.contains("::") {
return false
}
match
(ipv6_units(left.to_owned(), false), ipv6_units(right.to_owned(), true)) {
(Some(a), Some(b)) => a + b < 8
_ => false
}
}
None => ipv6_units(host, true) == Some(8)
}
}
///|
fn valid_dns_host(host : String) -> Bool {
let name = if host.has_suffix(".") {
string_slice(host, 0, host.length() - 1)
} else {
host
}
if name.is_empty() || name.length() > 253 {
return false
}
for label in name.split(".") {
if label.is_empty() ||
label.length() > 63 ||
label.has_prefix("-") ||
label.has_suffix("-") {
return false
}
for char in label {
let n = char.to_int()
if !((n >= 48 && n <= 57) ||
(n >= 65 && n <= 90) ||
(n >= 97 && n <= 122) ||
n == 45) {
return false
}
}
}
true
}
///|
fn valid_path_character(char : UInt16) -> Bool {
let n = char.to_int()
(n >= 48 && n <= 57) ||
(n >= 65 && n <= 90) ||
(n >= 97 && n <= 122) ||
"-._~!$&'()*+,;=:@/".contains(char.to_char().unwrap().to_string())
}
///|
pub fn PrinterUri::parse(text : String) -> PrinterUri raise IppError {
if text.length() > 1023 {
raise Invalid("printer URI exceeds 1023 characters")
}
for char in text.to_array() {
if char.to_int() <= 32 || char.to_int() >= 127 {
raise Invalid("URI requires escaped ASCII characters")
}
}
let (scheme_view, remainder_view) = match text.split_once("://") {
Some(parts) => parts
None => raise Invalid("URI must include scheme and authority")
}
let scheme = scheme_view.to_owned().to_lower()
if scheme != "ipp" && scheme != "ipps" {
raise Invalid("printer URI scheme must be ipp or ipps")
}
let remainder = remainder_view.to_owned()
let slash = match remainder.find("/") {
Some(index) => index
None => remainder.length()
}
let authority = string_slice(remainder, 0, slash)
let path = if slash == remainder.length() {
"/"
} else {
string_slice(remainder, slash, remainder.length())
}
if authority.is_empty() || authority.contains("@") {
raise Invalid("empty host or URI userinfo")
}
if path.contains("#") || path.contains("?") || path.contains("\\") {
raise Invalid(
"printer URI path must not contain query, fragment or backslash",
)
}
let mut host = ""
let mut port = 631
let ipv6 = authority.has_prefix("[")
if ipv6 {
let closing = match authority.find("]") {
Some(index) => index
None => raise Invalid("unterminated IPv6 host")
}
host = string_slice(authority, 1, closing)
if !valid_ipv6_literal(host) {
raise Invalid("invalid IPv6 host")
}
let suffix = string_slice(authority, closing + 1, authority.length())
if suffix != "" {
if !suffix.has_prefix(":") {
raise Invalid("unexpected data after IPv6 host")
}
port = parse_decimal(string_slice(suffix, 1, suffix.length()), 65535)
}
} else {
match authority.split_once(":") {
Some((hostname, port_text)) => {
host = hostname.to_owned()
port = parse_decimal(port_text.to_owned(), 65535)
}
None => host = authority
}
if !valid_dns_host(host) {
raise Invalid("invalid hostname")
}
}
if host.is_empty() || port < 1 {
raise Invalid("host and port must not be empty")
}
let mut i = 0
while i < path.length() {
if path[i].to_int() == 37 {
if i + 2 >= path.length() ||
hex_nibble(path[i + 1]) is None ||
hex_nibble(path[i + 2]) is None {
raise Invalid("invalid percent escape")
}
i += 3
} else {
if !valid_path_character(path[i]) {
raise Invalid("URI path contains an unescaped reserved character")
}
i += 1
}
}
let result : PrinterUri = { scheme, host: host.to_lower(), port, path, ipv6, }
if result.text().length() > 1023 {
raise Invalid("normalized printer URI exceeds 1023 characters")
}
result
}
///|
pub fn PrinterUri::authority(self : PrinterUri) -> String {
let host = if self.ipv6 { "[\{self.host}]" } else { self.host }
"\{host}:\{self.port}"
}
///|
pub fn PrinterUri::text(self : PrinterUri) -> String {
"\{self.scheme}://\{self.authority()}\{self.path}"
}
///|
pub fn PrinterUri::http_url(self : PrinterUri) -> String {
let scheme = if self.scheme == "ipps" { "https" } else { "http" }
"\{scheme}://\{self.authority()}\{self.path}"
}
///|
pub fn PrinterUri::secure(self : PrinterUri) -> Bool {
self.scheme == "ipps"
}