Krolyxon
Commit 55c1225

ferreniumsrcrendermod.rs

115 lines · 3.2 KB · Rust

1//! Presentation helpers shared by handlers and templates.
2
3pub mod highlight;
4pub mod markdown;
5pub mod time;
6
7use std::fmt::Write;
8
9/// Percent-encode a slash-separated path for use in a URL, keeping `/`.
10pub fn encode_path(path: &str) -> String {
11 encode_bytes(path.as_bytes())
12}
13
14/// [`encode_path`] for paths that may not be UTF-8 (git tree paths).
15pub fn encode_bytes(path: &[u8]) -> String {
16 let mut out = String::with_capacity(path.len());
17 for &b in path {
18 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
19 out.push(b as char);
20 } else {
21 let _ = write!(out, "%{b:02X}");
22 }
23 }
24 out
25}
26
27/// Decode `%XX` escapes (invalid escapes are kept literally).
28pub fn percent_decode(s: &str) -> Vec<u8> {
29 let bytes = s.as_bytes();
30 let mut out = Vec::with_capacity(bytes.len());
31 let mut i = 0;
32 while i < bytes.len() {
33 let hex = |b: u8| (b as char).to_digit(16);
34 if bytes[i] == b'%'
35 && let (Some(&h), Some(&l)) = (bytes.get(i + 1), bytes.get(i + 2))
36 && let (Some(h), Some(l)) = (hex(h), hex(l))
37 {
38 out.push((h * 16 + l) as u8);
39 i += 3;
40 } else {
41 out.push(bytes[i]);
42 i += 1;
43 }
44 }
45 out
46}
47
48/// Append `text` to `out` with `&<>"'` escaped.
49pub fn escape_html(out: &mut String, text: &str) {
50 for c in text.chars() {
51 match c {
52 '&' => out.push_str("&amp;"),
53 '<' => out.push_str("&lt;"),
54 '>' => out.push_str("&gt;"),
55 '"' => out.push_str("&quot;"),
56 '\'' => out.push_str("&#39;"),
57 c => out.push(c),
58 }
59 }
60}
61
62/// "12.4 KB" (binary multiples, conventional labels).
63pub fn human_size(bytes: u64) -> String {
64 const UNITS: [&str; 4] = ["KB", "MB", "GB", "TB"];
65 if bytes < 1024 {
66 return format!("{bytes} B");
67 }
68 let mut value = bytes as f64 / 1024.0;
69 let mut unit = 0;
70 while value >= 1024.0 && unit < UNITS.len() - 1 {
71 value /= 1024.0;
72 unit += 1;
73 }
74 if value >= 100.0 {
75 format!("{value:.0} {}", UNITS[unit])
76 } else {
77 format!("{value:.1} {}", UNITS[unit])
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn percent_decoding() {
87 assert_eq!(percent_decode("a%20b%2Fc"), b"a b/c");
88 assert_eq!(percent_decode("%C3%BC"), "ü".as_bytes());
89 assert_eq!(percent_decode("100%"), b"100%");
90 assert_eq!(percent_decode("%zz%4"), b"%zz%4");
91 }
92
93 #[test]
94 fn sizes() {
95 assert_eq!(human_size(0), "0 B");
96 assert_eq!(human_size(1023), "1023 B");
97 assert_eq!(human_size(12_698), "12.4 KB");
98 assert_eq!(human_size(300 * 1024), "300 KB");
99 assert_eq!(human_size(5 * 1024 * 1024), "5.0 MB");
100 }
101
102 #[test]
103 fn escaping() {
104 let mut s = String::new();
105 escape_html(&mut s, "<a href=\"x\">&'");
106 assert_eq!(s, "&lt;a href=&quot;x&quot;&gt;&amp;&#39;");
107 }
108
109 #[test]
110 fn encodes_reserved_but_keeps_slashes() {
111 assert_eq!(encode_path("forks/foo.rs"), "forks/foo.rs");
112 assert_eq!(encode_path("a b#?%"), "a%20b%23%3F%25");
113 assert_eq!(encode_path("ünï"), "%C3%BCn%C3%AF");
114 }
115}