Skip to main content

sim_kernel/catalog/
key.rs

1use crate::{Error, Result, Symbol};
2
3/// Build a deterministic ASCII catalog key from a namespace and path parts.
4pub fn catalog_key(namespace: &str, parts: &[&str]) -> Symbol {
5    let mut key = String::new();
6    push_escaped(&mut key, namespace);
7    for part in parts {
8        key.push('/');
9        push_escaped(&mut key, part);
10    }
11    Symbol::new(key)
12}
13
14/// Split a deterministic catalog key back into decoded path components.
15pub fn split_catalog_key(symbol: &Symbol) -> Result<Vec<String>> {
16    symbol
17        .as_qualified_str()
18        .split('/')
19        .map(decode_part)
20        .collect()
21}
22
23fn push_escaped(out: &mut String, text: &str) {
24    for byte in text.bytes() {
25        match byte {
26            b'%' => out.push_str("%25"),
27            b'/' => out.push_str("%2F"),
28            0x00..=0x1f | 0x7f..=0xff => push_hex_escape(out, byte),
29            _ => out.push(char::from(byte)),
30        }
31    }
32}
33
34fn push_hex_escape(out: &mut String, byte: u8) {
35    const HEX: &[u8; 16] = b"0123456789ABCDEF";
36    out.push('%');
37    out.push(char::from(HEX[usize::from(byte >> 4)]));
38    out.push(char::from(HEX[usize::from(byte & 0x0f)]));
39}
40
41fn decode_part(text: &str) -> Result<String> {
42    let bytes = text.as_bytes();
43    let mut decoded = Vec::with_capacity(bytes.len());
44    let mut index = 0;
45    while index < bytes.len() {
46        match bytes[index] {
47            b'%' => {
48                let hi = bytes
49                    .get(index + 1)
50                    .copied()
51                    .and_then(hex_value)
52                    .ok_or_else(malformed_escape)?;
53                let lo = bytes
54                    .get(index + 2)
55                    .copied()
56                    .and_then(hex_value)
57                    .ok_or_else(malformed_escape)?;
58                decoded.push((hi << 4) | lo);
59                index += 3;
60            }
61            byte @ (0x00..=0x1f | 0x7f..=0xff) => {
62                return Err(catalog_key_schema_error(format!(
63                    "catalog key contains unescaped byte 0x{byte:02X}"
64                )));
65            }
66            byte => {
67                decoded.push(byte);
68                index += 1;
69            }
70        }
71    }
72    String::from_utf8(decoded)
73        .map_err(|_| catalog_key_schema_error("catalog key escape is not valid UTF-8"))
74}
75
76fn hex_value(byte: u8) -> Option<u8> {
77    match byte {
78        b'0'..=b'9' => Some(byte - b'0'),
79        b'a'..=b'f' => Some(byte - b'a' + 10),
80        b'A'..=b'F' => Some(byte - b'A' + 10),
81        _ => None,
82    }
83}
84
85fn malformed_escape() -> Error {
86    catalog_key_schema_error("malformed catalog key escape")
87}
88
89fn catalog_key_schema_error(message: impl Into<String>) -> Error {
90    Error::CatalogSchema {
91        table: Symbol::new("catalog/key"),
92        message: message.into(),
93    }
94}