Skip to main content

qrcode_generator/
url.rs

1use alloc::string::String;
2
3use url::{Position, Url};
4
5use crate::ToQRText;
6
7// The WHATWG special schemes are the ones whose host is parsed and compared case-insensitively.
8fn is_special_scheme(scheme: &str) -> bool {
9    matches!(scheme, "ftp" | "file" | "http" | "https" | "ws" | "wss")
10}
11
12fn uppercase_percent_escapes(text: &str) -> String {
13    let bytes = text.as_bytes();
14    let mut normalized = String::with_capacity(text.len());
15    let mut copied = 0;
16    let mut index = 0;
17
18    // Only complete percent-encoded octets can be changed without changing URL data.
19    while index + 2 < bytes.len() {
20        if bytes[index] == b'%'
21            && bytes[index + 1].is_ascii_hexdigit()
22            && bytes[index + 2].is_ascii_hexdigit()
23        {
24            normalized.push_str(&text[copied..index]);
25            normalized.push('%');
26            normalized.push(char::from(bytes[index + 1].to_ascii_uppercase()));
27            normalized.push(char::from(bytes[index + 2].to_ascii_uppercase()));
28
29            index += 3;
30            copied = index;
31        } else {
32            index += 1;
33        }
34    }
35
36    normalized.push_str(&text[copied..]);
37    normalized
38}
39
40impl ToQRText for Url {
41    fn to_qr_text(&self) -> String {
42        let serialized = self.as_str();
43        let scheme_end = self[..Position::AfterScheme].len();
44        let path_start = self[..Position::BeforePath].len();
45        let path_end = self[..Position::AfterPath].len();
46        let omit_root_path = matches!(self.scheme(), "http" | "https") && self.path() == "/";
47        let mut normalized = String::with_capacity(serialized.len());
48
49        normalized.push_str(&serialized[..scheme_end].to_ascii_uppercase());
50
51        // A special scheme has a case-insensitive host, but user information and path data may not be.
52        if self.has_host() {
53            let host_start = self[..Position::BeforeHost].len();
54            let host_end = self[..Position::AfterHost].len();
55            let host = &serialized[host_start..host_end];
56
57            normalized.push_str(&serialized[scheme_end..host_start]);
58
59            // Only special schemes fold host case; an opaque host of any other scheme keeps its case.
60            if is_special_scheme(self.scheme()) {
61                normalized.push_str(&host.to_ascii_uppercase());
62            } else {
63                normalized.push_str(host);
64            }
65
66            normalized.push_str(&serialized[host_end..path_start]);
67        } else {
68            normalized.push_str(&serialized[scheme_end..path_start]);
69        }
70
71        if !omit_root_path {
72            normalized.push_str(&serialized[path_start..path_end]);
73        }
74
75        normalized.push_str(&serialized[path_end..]);
76        uppercase_percent_escapes(&normalized)
77    }
78}