1use crate::{Error, Result};
13
14pub const MAX_KEY: usize = 1024;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct Key<'a>(&'a str);
20
21impl<'a> Key<'a> {
22 pub fn parse(s: &'a str) -> Result<Self> {
31 check(s)?;
32 Ok(Key(s))
33 }
34
35 #[must_use]
37 pub fn as_str(&self) -> &'a str {
38 self.0
39 }
40}
41
42pub(crate) fn check_prefix(s: &str) -> Result<()> {
44 if s.is_empty() {
45 return Ok(());
46 }
47 check(s.strip_suffix('/').unwrap_or(s))
48}
49
50fn check(s: &str) -> Result<()> {
51 let bad = |reason| Err(Error::InvalidKey { reason });
52 if s.is_empty() {
53 return bad("empty");
54 }
55 if s.len() > MAX_KEY {
56 return bad("longer than 1024 bytes");
57 }
58 for seg in s.split('/') {
59 if seg.is_empty() {
60 return bad("empty segment");
61 }
62 if seg.starts_with('.') {
63 return bad("segment starts with a dot");
66 }
67 if !seg.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-')) {
68 return bad("characters outside [A-Za-z0-9._-/]");
69 }
70 if seg.ends_with('.') {
71 return bad("segment ends with a dot");
72 }
73 if is_device_name(seg) {
74 return bad("reserved device name");
75 }
76 }
77 Ok(())
78}
79
80fn is_device_name(seg: &str) -> bool {
83 let stem = seg.split('.').next().unwrap_or(seg);
84 let upper = stem.to_ascii_uppercase();
85 matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL")
86 || (upper.len() == 4
87 && (upper.starts_with("COM") || upper.starts_with("LPT"))
88 && upper.as_bytes()[3].is_ascii_digit())
89}
90
91pub fn encode_component(s: &str, out: &mut String) {
95 const HEX: &[u8; 16] = b"0123456789ABCDEF";
96 for b in s.bytes() {
97 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
98 out.push(char::from(b));
99 } else {
100 out.push('%');
101 out.push(char::from(HEX[usize::from(b >> 4)]));
102 out.push(char::from(HEX[usize::from(b & 0xF)]));
103 }
104 }
105}
106
107#[doc(hidden)]
110pub fn fuzz(input: &[u8]) {
111 let Ok(s) = std::str::from_utf8(input) else { return };
112 let prefix_ok = check_prefix(s).is_ok();
113 if let Ok(k) = Key::parse(s) {
114 assert!(prefix_ok, "every key is a valid prefix");
115 let k = k.as_str();
116 assert!(!k.starts_with('/') && !k.ends_with('/') && !k.contains("//"));
117 assert!(k.split('/').all(|seg| seg != "." && seg != ".." && !seg.starts_with('.')));
118 assert!(k.bytes().all(|b| b.is_ascii_graphic() && b != b'\\'));
119 {
120 let mut enc = String::new();
121 encode_component(k, &mut enc);
122 assert!(enc.bytes().all(
123 |b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'%')
124 ));
125 assert!(!enc.contains('/'));
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn grammar() {
136 for ok in [
137 "a",
138 "a/b",
139 "HEAD",
140 "bases/00000001.base",
141 "wal/0001/000000000001.seg",
142 "x-y_z.1",
143 "console",
144 "com",
145 "COMX",
146 ] {
147 assert!(Key::parse(ok).is_ok(), "{ok}");
148 }
149 for bad in [
150 "", "/a", "a/", "a//b", ".", "..", "a/../b", "a/./b", ".hidden", "a/.tmp", "a b",
151 "a\\b", "รค", "a\0b", "C:/x", "a?b", "a.", "x/NUL", "con.txt", "com1", "LPT9.log",
152 ] {
153 assert!(Key::parse(bad).is_err(), "{bad:?}");
154 }
155 assert!(Key::parse(&"a".repeat(MAX_KEY)).is_ok());
156 assert!(Key::parse(&"a".repeat(MAX_KEY + 1)).is_err());
157 assert!(
158 check_prefix("").is_ok()
159 && check_prefix("wal/").is_ok()
160 && check_prefix("wal/000").is_ok()
161 );
162 assert!(check_prefix("/").is_err() && check_prefix("../").is_err());
163 }
164
165 #[test]
166 fn encoding() {
167 let mut s = String::new();
168 encode_component("wal/0001/a-b_c.seg", &mut s);
169 assert_eq!(s, "wal%2F0001%2Fa-b_c.seg");
170 }
171}