1use base64::Engine;
2
3const RAW_TXT_RDATA_PREFIX: &str = "bindizr:txt-rdata:v1:";
4
5#[derive(Debug, PartialEq, Eq)]
6pub enum DecodedTxtValue {
7 String(String),
8 Segments(Vec<String>),
9}
10
11pub fn encode_raw_txt_rdata(rdata: &[u8]) -> String {
12 format!(
13 "{}{}",
14 RAW_TXT_RDATA_PREFIX,
15 base64::engine::general_purpose::STANDARD.encode(rdata)
16 )
17}
18
19pub fn encode_txt_segments<'a, I>(segments: I) -> Result<String, String>
20where
21 I: IntoIterator<Item = &'a str>,
22{
23 let mut rdata = Vec::new();
24 let mut has_segments = false;
25 for segment in segments {
26 has_segments = true;
27 let bytes = segment.as_bytes();
28 if bytes.len() > 255 {
29 return Err("TXT character-string must be 255 bytes or less".to_string());
30 }
31 rdata.push(bytes.len() as u8);
32 rdata.extend_from_slice(bytes);
33 }
34 if !has_segments {
35 return Err("TXT record must contain at least one character-string".to_string());
36 }
37 Ok(encode_raw_txt_rdata(&rdata))
38}
39
40pub fn encode_txt_string(value: &str) -> String {
41 let mut rdata = Vec::new();
42 let mut chunk_start = 0usize;
43 let mut chunk_len = 0usize;
44
45 for (idx, ch) in value.char_indices() {
46 let char_len = ch.len_utf8();
47 if chunk_len + char_len > 255 {
48 rdata.push(chunk_len as u8);
49 rdata.extend_from_slice(&value.as_bytes()[chunk_start..idx]);
50 chunk_start = idx;
51 chunk_len = 0;
52 }
53 chunk_len += char_len;
54 }
55
56 rdata.push(chunk_len as u8);
57 rdata.extend_from_slice(&value.as_bytes()[chunk_start..]);
58 encode_raw_txt_rdata(&rdata)
59}
60
61pub fn decode_raw_txt_rdata(value: &str) -> Option<Vec<u8>> {
62 let encoded = value.strip_prefix(RAW_TXT_RDATA_PREFIX)?;
63 base64::engine::general_purpose::STANDARD
64 .decode(encoded)
65 .ok()
66 .filter(|rdata| is_valid_txt_rdata(rdata))
67}
68
69pub fn decode_raw_txt_value(value: &str) -> Option<DecodedTxtValue> {
70 let rdata = decode_raw_txt_rdata(value)?;
71 if rdata.is_empty() {
72 return None;
73 }
74
75 let mut pos = 0usize;
76 let mut segments = Vec::new();
77
78 while pos < rdata.len() {
79 let chunk_len = rdata[pos] as usize;
80 pos += 1;
81 let chunk = std::str::from_utf8(&rdata[pos..pos + chunk_len]).ok()?;
82 segments.push(chunk.to_string());
83 pos += chunk_len;
84 }
85
86 match segments.as_slice() {
87 [single] => Some(DecodedTxtValue::String(single.clone())),
88 _ => Some(DecodedTxtValue::Segments(segments)),
89 }
90}
91
92fn is_valid_txt_rdata(rdata: &[u8]) -> bool {
93 let mut pos = 0usize;
94 while pos < rdata.len() {
95 let chunk_len = rdata[pos] as usize;
96 pos += 1;
97 if pos + chunk_len > rdata.len() {
98 return false;
99 }
100 pos += chunk_len;
101 }
102 true
103}
104
105#[cfg(test)]
106mod tests {
107 use super::{decode_raw_txt_rdata, encode_raw_txt_rdata};
108
109 #[test]
110 fn raw_txt_rdata_round_trips() {
111 let rdata = [2, b'a', b'b', 1, b'c'];
112 let encoded = encode_raw_txt_rdata(&rdata);
113
114 assert_eq!(decode_raw_txt_rdata(&encoded), Some(rdata.to_vec()));
115 }
116
117 #[test]
118 fn txt_segments_encode_to_reversible_json_value() {
119 let encoded = super::encode_txt_segments(["a", "bc"]).unwrap();
120
121 assert_eq!(
122 super::decode_raw_txt_value(&encoded),
123 Some(super::DecodedTxtValue::Segments(vec![
124 "a".to_string(),
125 "bc".to_string()
126 ]))
127 );
128 }
129
130 #[test]
131 fn txt_segments_reject_empty_lists() {
132 assert_eq!(
133 super::encode_txt_segments(std::iter::empty()).unwrap_err(),
134 "TXT record must contain at least one character-string"
135 );
136 }
137
138 #[test]
139 fn txt_value_rejects_zero_segment_rdata() {
140 let encoded = encode_raw_txt_rdata(&[]);
141
142 assert_eq!(super::decode_raw_txt_value(&encoded), None);
143 }
144
145 #[test]
146 fn txt_segments_allow_single_empty_segment() {
147 let encoded = super::encode_txt_segments([""]).unwrap();
148
149 assert_eq!(decode_raw_txt_rdata(&encoded), Some(vec![0]));
150 assert_eq!(
151 super::decode_raw_txt_value(&encoded),
152 Some(super::DecodedTxtValue::String(String::new()))
153 );
154 }
155
156 #[test]
157 fn txt_string_auto_splits_long_values() {
158 let value = "a".repeat(300);
159 let encoded = super::encode_txt_string(&value);
160
161 assert_eq!(
162 decode_raw_txt_rdata(&encoded),
163 Some({
164 let mut rdata = Vec::new();
165 rdata.push(255);
166 rdata.extend(std::iter::repeat_n(b'a', 255));
167 rdata.push(45);
168 rdata.extend(std::iter::repeat_n(b'a', 45));
169 rdata
170 })
171 );
172 assert_eq!(
173 super::decode_raw_txt_value(&encoded),
174 Some(super::DecodedTxtValue::Segments(vec![
175 "a".repeat(255),
176 "a".repeat(45)
177 ]))
178 );
179 }
180
181 #[test]
182 fn txt_string_splits_on_utf8_boundaries() {
183 let value = format!("{}{}", "a".repeat(254), "é");
184 let encoded = super::encode_txt_string(&value);
185
186 assert_eq!(
187 super::decode_raw_txt_value(&encoded),
188 Some(super::DecodedTxtValue::Segments(vec![
189 "a".repeat(254),
190 "é".to_string()
191 ]))
192 );
193 }
194
195 #[test]
196 fn invalid_raw_txt_rdata_prefix_is_ignored() {
197 assert_eq!(decode_raw_txt_rdata("bindizr:txt-rdata:v1:A2Fi"), None);
198 }
199}