agentmux_srv\backend\utilfn/
nullenc.rs

1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5
6use std::collections::HashMap;
7
8// ---- Constants ----
9
10const NULL_ENCODE_ESC_BYTE: u8 = b'\\';
11const NULL_ENCODE_SEP_BYTE: u8 = b'|';
12const NULL_ENCODE_EQ_BYTE: u8 = b'=';
13const NULL_ENCODE_ZERO_BYTE_ESC: u8 = b'0';
14const NULL_ENCODE_ESC_BYTE_ESC: u8 = b'\\';
15const NULL_ENCODE_SEP_BYTE_ESC: u8 = b's';
16const NULL_ENCODE_EQ_BYTE_ESC: u8 = b'e';
17
18// ---- Encoding / Decoding ----
19
20/// Encode a string, escaping null bytes, backslashes, separators (`|`), and equals (`=`).
21///
22/// - `\0` → `\0`
23/// - `\` → `\\`
24/// - `|` → `\s`
25/// - `=` → `\e`
26pub fn null_encode_str(s: &str) -> Vec<u8> {
27    let bytes = s.as_bytes();
28    // Fast path: if no special bytes, return as-is
29    if !bytes.iter().any(|&b| b == 0 || b == NULL_ENCODE_ESC_BYTE || b == NULL_ENCODE_SEP_BYTE || b == NULL_ENCODE_EQ_BYTE) {
30        return bytes.to_vec();
31    }
32    let mut rtn = Vec::with_capacity(bytes.len() + 8);
33    for &b in bytes {
34        match b {
35            0 => {
36                rtn.push(NULL_ENCODE_ESC_BYTE);
37                rtn.push(NULL_ENCODE_ZERO_BYTE_ESC);
38            }
39            b if b == NULL_ENCODE_ESC_BYTE => {
40                rtn.push(NULL_ENCODE_ESC_BYTE);
41                rtn.push(NULL_ENCODE_ESC_BYTE_ESC);
42            }
43            b if b == NULL_ENCODE_SEP_BYTE => {
44                rtn.push(NULL_ENCODE_ESC_BYTE);
45                rtn.push(NULL_ENCODE_SEP_BYTE_ESC);
46            }
47            b if b == NULL_ENCODE_EQ_BYTE => {
48                rtn.push(NULL_ENCODE_ESC_BYTE);
49                rtn.push(NULL_ENCODE_EQ_BYTE_ESC);
50            }
51            _ => rtn.push(b),
52        }
53    }
54    rtn
55}
56
57/// Decode a null-encoded byte slice back to a string.
58pub fn null_decode_str(barr: &[u8]) -> Result<String, String> {
59    if !barr.contains(&NULL_ENCODE_ESC_BYTE) {
60        return Ok(String::from_utf8_lossy(barr).into_owned());
61    }
62    let mut rtn = Vec::with_capacity(barr.len());
63    let mut i = 0;
64    while i < barr.len() {
65        let cur = barr[i];
66        if cur == NULL_ENCODE_ESC_BYTE {
67            i += 1;
68            if i >= barr.len() {
69                return Err("invalid null encoding: escape at end of string".to_string());
70            }
71            let next = barr[i];
72            match next {
73                NULL_ENCODE_ZERO_BYTE_ESC => rtn.push(0),
74                NULL_ENCODE_ESC_BYTE_ESC => rtn.push(NULL_ENCODE_ESC_BYTE),
75                NULL_ENCODE_SEP_BYTE_ESC => rtn.push(NULL_ENCODE_SEP_BYTE),
76                NULL_ENCODE_EQ_BYTE_ESC => rtn.push(NULL_ENCODE_EQ_BYTE),
77                _ => return Err(format!("invalid null encoding: {}", next)),
78            }
79        } else {
80            rtn.push(cur);
81        }
82        i += 1;
83    }
84    Ok(String::from_utf8_lossy(&rtn).into_owned())
85}
86
87/// Encode a map of strings using null encoding, sorted by key.
88/// Format: `key1=val1|key2=val2`
89pub fn encode_string_map(m: &HashMap<String, String>) -> Vec<u8> {
90    let mut keys: Vec<&String> = m.keys().collect();
91    keys.sort();
92    let mut buf = Vec::new();
93    for (idx, key) in keys.iter().enumerate() {
94        let val = &m[*key];
95        buf.extend_from_slice(&null_encode_str(key));
96        buf.push(NULL_ENCODE_EQ_BYTE);
97        buf.extend_from_slice(&null_encode_str(val));
98        if idx < keys.len() - 1 {
99            buf.push(NULL_ENCODE_SEP_BYTE);
100        }
101    }
102    buf
103}
104
105/// Decode a null-encoded byte slice into a map of strings.
106pub fn decode_string_map(barr: &[u8]) -> Result<HashMap<String, String>, String> {
107    if barr.is_empty() {
108        return Ok(HashMap::new());
109    }
110    let mut rtn = HashMap::new();
111    for part in split_bytes(barr, NULL_ENCODE_SEP_BYTE) {
112        let kv: Vec<&[u8]> = splitn_bytes(part, NULL_ENCODE_EQ_BYTE, 2);
113        if kv.len() != 2 {
114            return Err(format!("invalid null encoding: {}", String::from_utf8_lossy(part)));
115        }
116        let key = null_decode_str(kv[0])?;
117        let val = null_decode_str(kv[1])?;
118        rtn.insert(key, val);
119    }
120    Ok(rtn)
121}
122
123/// Encode a string array using null encoding.
124/// Format: `elem1|elem2|elem3`
125pub fn encode_string_array(arr: &[String]) -> Vec<u8> {
126    let mut buf = Vec::new();
127    for (idx, s) in arr.iter().enumerate() {
128        buf.extend_from_slice(&null_encode_str(s));
129        if idx < arr.len() - 1 {
130            buf.push(NULL_ENCODE_SEP_BYTE);
131        }
132    }
133    buf
134}
135
136/// Decode a null-encoded byte slice into a string array.
137pub fn decode_string_array(barr: &[u8]) -> Result<Vec<String>, String> {
138    if barr.is_empty() {
139        return Ok(Vec::new());
140    }
141    let mut rtn = Vec::new();
142    for part in split_bytes(barr, NULL_ENCODE_SEP_BYTE) {
143        rtn.push(null_decode_str(part)?);
144    }
145    Ok(rtn)
146}
147
148/// Check if an encoded string array has the given first value.
149pub fn encoded_string_array_has_first_val(encoded: &[u8], first_key: &str) -> bool {
150    let first_key_bytes = null_encode_str(first_key);
151    if !encoded.starts_with(&first_key_bytes) {
152        return false;
153    }
154    encoded.len() == first_key_bytes.len() || encoded[first_key_bytes.len()] == NULL_ENCODE_SEP_BYTE
155}
156
157/// Get the first value from an encoded string array without decoding the whole array.
158pub fn encoded_string_array_get_first_val(encoded: &[u8]) -> String {
159    let sep_idx = encoded.iter().position(|&b| b == NULL_ENCODE_SEP_BYTE);
160    let slice = match sep_idx {
161        Some(idx) => &encoded[..idx],
162        None => encoded,
163    };
164    null_decode_str(slice).unwrap_or_default()
165}
166
167// ---- Internal helpers ----
168
169fn split_bytes(data: &[u8], delim: u8) -> Vec<&[u8]> {
170    let mut parts = Vec::new();
171    let mut start = 0;
172    for (i, &b) in data.iter().enumerate() {
173        if b == delim {
174            parts.push(&data[start..i]);
175            start = i + 1;
176        }
177    }
178    parts.push(&data[start..]);
179    parts
180}
181
182fn splitn_bytes(data: &[u8], delim: u8, n: usize) -> Vec<&[u8]> {
183    let mut parts = Vec::new();
184    let mut start = 0;
185    for (i, &b) in data.iter().enumerate() {
186        if b == delim && parts.len() < n - 1 {
187            parts.push(&data[start..i]);
188            start = i + 1;
189        }
190    }
191    parts.push(&data[start..]);
192    parts
193}