opentdf_protocol/nanotdf/
resource_locator.rs1use crate::binary::{BinaryRead, BinaryWrite, read_u8, write_u8};
7use std::io::{self, Read, Write};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum Protocol {
13 Http = 0x0,
15 Https = 0x1,
17 SharedResourceDirectory = 0xF,
19}
20
21impl Protocol {
22 pub fn from_bits(bits: u8) -> io::Result<Self> {
24 match bits & 0x0F {
25 0x0 => Ok(Protocol::Http),
26 0x1 => Ok(Protocol::Https),
27 0xF => Ok(Protocol::SharedResourceDirectory),
28 _ => Err(io::Error::new(
29 io::ErrorKind::InvalidData,
30 format!("Unknown protocol: 0x{:X}", bits),
31 )),
32 }
33 }
34
35 pub fn to_bits(self) -> u8 {
37 self as u8
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum IdentifierType {
44 None,
46 TwoByte,
48 EightByte,
50 ThirtyTwoByte,
52}
53
54impl IdentifierType {
55 pub fn from_bits(bits: u8) -> io::Result<Self> {
57 match (bits >> 4) & 0x0F {
58 0x0 => Ok(IdentifierType::None),
59 0x1 => Ok(IdentifierType::TwoByte),
60 0x2 => Ok(IdentifierType::EightByte),
61 0x3 => Ok(IdentifierType::ThirtyTwoByte),
62 _ => Err(io::Error::new(
63 io::ErrorKind::InvalidData,
64 format!("Unknown identifier type: 0x{:X}", bits >> 4),
65 )),
66 }
67 }
68
69 pub fn to_bits(self) -> u8 {
71 let value = match self {
72 IdentifierType::None => 0x0,
73 IdentifierType::TwoByte => 0x1,
74 IdentifierType::EightByte => 0x2,
75 IdentifierType::ThirtyTwoByte => 0x3,
76 };
77 value << 4
78 }
79
80 pub fn byte_length(self) -> usize {
82 match self {
83 IdentifierType::None => 0,
84 IdentifierType::TwoByte => 2,
85 IdentifierType::EightByte => 8,
86 IdentifierType::ThirtyTwoByte => 32,
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ResourceLocator {
105 pub protocol: Protocol,
107 pub identifier: Option<Vec<u8>>,
109 pub body: Vec<u8>,
111}
112
113impl ResourceLocator {
114 pub fn new(protocol: Protocol, body: impl Into<Vec<u8>>) -> Self {
116 Self {
117 protocol,
118 identifier: None,
119 body: body.into(),
120 }
121 }
122
123 pub fn with_identifier(mut self, identifier: Vec<u8>) -> io::Result<Self> {
125 match identifier.len() {
127 0 => {
128 self.identifier = None;
129 }
130 2 | 8 | 32 => {
131 self.identifier = Some(identifier);
132 }
133 _ => {
134 return Err(io::Error::new(
135 io::ErrorKind::InvalidInput,
136 format!("Invalid identifier length: {}", identifier.len()),
137 ));
138 }
139 }
140 Ok(self)
141 }
142
143 pub fn identifier_type(&self) -> IdentifierType {
145 match &self.identifier {
146 None => IdentifierType::None,
147 Some(id) => match id.len() {
148 2 => IdentifierType::TwoByte,
149 8 => IdentifierType::EightByte,
150 32 => IdentifierType::ThirtyTwoByte,
151 _ => IdentifierType::None, },
153 }
154 }
155
156 pub fn from_url(url: &str) -> io::Result<Self> {
158 if let Some(rest) = url.strip_prefix("http://") {
159 Ok(Self::new(Protocol::Http, rest.as_bytes()))
160 } else if let Some(rest) = url.strip_prefix("https://") {
161 Ok(Self::new(Protocol::Https, rest.as_bytes()))
162 } else {
163 Err(io::Error::new(
164 io::ErrorKind::InvalidInput,
165 "URL must start with http:// or https://",
166 ))
167 }
168 }
169
170 pub fn to_url(&self) -> io::Result<String> {
172 let protocol_str = match self.protocol {
173 Protocol::Http => "http://",
174 Protocol::Https => "https://",
175 Protocol::SharedResourceDirectory => {
176 return Err(io::Error::new(
177 io::ErrorKind::InvalidData,
178 "Shared Resource Directory cannot be converted to URL",
179 ));
180 }
181 };
182
183 let body_str = String::from_utf8(self.body.clone()).map_err(|_| {
184 io::Error::new(io::ErrorKind::InvalidData, "Body contains invalid UTF-8")
185 })?;
186
187 Ok(format!("{}{}", protocol_str, body_str))
188 }
189}
190
191impl BinaryRead for ResourceLocator {
192 fn read_from<R: Read>(reader: &mut R) -> io::Result<Self> {
193 let header = read_u8(reader)?;
195
196 let protocol = Protocol::from_bits(header)?;
198 let identifier_type = IdentifierType::from_bits(header)?;
199
200 let body_len = read_u8(reader)? as usize;
202
203 let mut body = vec![0u8; body_len];
205 reader.read_exact(&mut body)?;
206
207 let identifier = if identifier_type.byte_length() > 0 {
209 let mut id = vec![0u8; identifier_type.byte_length()];
210 reader.read_exact(&mut id)?;
211 Some(id)
212 } else {
213 None
214 };
215
216 Ok(Self {
217 protocol,
218 identifier,
219 body,
220 })
221 }
222}
223
224impl BinaryWrite for ResourceLocator {
225 fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
226 if self.body.len() > 255 {
228 return Err(io::Error::new(
229 io::ErrorKind::InvalidInput,
230 format!("Body too long: {} bytes (max 255)", self.body.len()),
231 ));
232 }
233
234 let header = self.identifier_type().to_bits() | self.protocol.to_bits();
236 write_u8(writer, header)?;
237
238 write_u8(writer, self.body.len() as u8)?;
240
241 writer.write_all(&self.body)?;
243
244 if let Some(ref id) = self.identifier {
246 writer.write_all(id)?;
247 }
248
249 Ok(())
250 }
251
252 fn serialized_size(&self) -> usize {
253 1 + 1 + self.body.len() +
256 self.identifier.as_ref().map(|id| id.len()).unwrap_or(0)
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use std::io::Cursor;
264
265 #[test]
266 fn test_resource_locator_http() {
267 let locator = ResourceLocator::from_url("http://kas.example.com").unwrap();
268 assert_eq!(locator.protocol, Protocol::Http);
269 assert_eq!(locator.body, b"kas.example.com");
270 assert_eq!(locator.identifier, None);
271 }
272
273 #[test]
274 fn test_resource_locator_https() {
275 let locator = ResourceLocator::from_url("https://kas.example.com/kas").unwrap();
276 assert_eq!(locator.protocol, Protocol::Https);
277 assert_eq!(locator.body, b"kas.example.com/kas");
278 }
279
280 #[test]
281 fn test_resource_locator_roundtrip() {
282 let original = ResourceLocator::new(Protocol::Https, b"kas.virtru.com".to_vec());
283
284 let mut buf = Vec::new();
285 original.write_to(&mut buf).unwrap();
286
287 let mut cursor = Cursor::new(buf);
288 let decoded = ResourceLocator::read_from(&mut cursor).unwrap();
289
290 assert_eq!(original, decoded);
291 }
292
293 #[test]
294 fn test_resource_locator_with_identifier() {
295 let original = ResourceLocator::new(Protocol::Https, b"kas.example.com".to_vec())
296 .with_identifier(vec![0x01, 0x02])
297 .unwrap();
298
299 assert_eq!(original.identifier_type(), IdentifierType::TwoByte);
300
301 let mut buf = Vec::new();
302 original.write_to(&mut buf).unwrap();
303
304 let mut cursor = Cursor::new(buf);
305 let decoded = ResourceLocator::read_from(&mut cursor).unwrap();
306
307 assert_eq!(original, decoded);
308 }
309
310 #[test]
311 fn test_to_url() {
312 let locator = ResourceLocator::new(Protocol::Https, b"kas.example.com/kas".to_vec());
313 assert_eq!(locator.to_url().unwrap(), "https://kas.example.com/kas");
314 }
315}