| 1 | //! Presentation helpers shared by handlers and templates. |
| 2 | |
| 3 | pub mod highlight; |
| 4 | pub mod markdown; |
| 5 | pub mod time; |
| 6 | |
| 7 | use std::fmt::Write; |
| 8 | |
| 9 | /// Percent-encode a slash-separated path for use in a URL, keeping `/`. |
| 10 | pub 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). |
| 15 | pub 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). |
| 28 | pub 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. |
| 49 | pub fn escape_html(out: &mut String, text: &str) { |
| 50 | for c in text.chars() { |
| 51 | match c { |
| 52 | '&' => out.push_str("&"), |
| 53 | '<' => out.push_str("<"), |
| 54 | '>' => out.push_str(">"), |
| 55 | '"' => out.push_str("""), |
| 56 | '\'' => out.push_str("'"), |
| 57 | c => out.push(c), |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /// "12.4 KB" (binary multiples, conventional labels). |
| 63 | pub 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)] |
| 82 | mod 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, "<a href="x">&'"); |
| 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 | } |