Skip to main content

opentdf_protocol/nanotdf/
resource_locator.rs

1//! Resource Locator for NanoTDF
2//!
3//! The Resource Locator provides a compact way to reference external resources
4//! like KAS endpoints and remote policies.
5
6use crate::binary::{BinaryRead, BinaryWrite, read_u8, write_u8};
7use std::io::{self, Read, Write};
8
9/// Protocol type for resource location
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum Protocol {
13    /// HTTP protocol
14    Http = 0x0,
15    /// HTTPS protocol
16    Https = 0x1,
17    /// Shared Resource Directory (experimental)
18    SharedResourceDirectory = 0xF,
19}
20
21impl Protocol {
22    /// Parse protocol from 4-bit value
23    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    /// Convert to 4-bit value
36    pub fn to_bits(self) -> u8 {
37        self as u8
38    }
39}
40
41/// Identifier type and length
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum IdentifierType {
44    /// No identifier
45    None,
46    /// 2-byte identifier
47    TwoByte,
48    /// 8-byte identifier
49    EightByte,
50    /// 32-byte identifier
51    ThirtyTwoByte,
52}
53
54impl IdentifierType {
55    /// Parse identifier type from 4-bit value
56    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    /// Convert to 4-bit value (upper nibble)
70    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    /// Get the byte length of this identifier type
81    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/// Resource Locator - compact reference to external resources
92///
93/// Structure:
94/// ```text
95/// ┌────────────────┬──────────────┬────────────┬─────────────────────┐
96/// │ Protocol (1B)  │ Body Len (1B)│ Body (var) │ Identifier (0-32B)  │
97/// └────────────────┴──────────────┴────────────┴─────────────────────┘
98/// ```
99///
100/// The Protocol byte contains:
101/// - Bits 7-4: Identifier type
102/// - Bits 3-0: Protocol enum
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ResourceLocator {
105    /// Protocol type
106    pub protocol: Protocol,
107    /// Identifier type and optional value
108    pub identifier: Option<Vec<u8>>,
109    /// Resource body (e.g., "kas.example.com/kas" for HTTPS)
110    pub body: Vec<u8>,
111}
112
113impl ResourceLocator {
114    /// Create a new resource locator
115    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    /// Set the identifier
124    pub fn with_identifier(mut self, identifier: Vec<u8>) -> io::Result<Self> {
125        // Validate identifier length
126        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    /// Get the identifier type
144    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, // Should never happen due to validation
152            },
153        }
154    }
155
156    /// Create from URL string (convenience method)
157    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    /// Convert to URL string (convenience method)
171    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        // Read protocol header (1 byte)
194        let header = read_u8(reader)?;
195
196        // Parse protocol and identifier type
197        let protocol = Protocol::from_bits(header)?;
198        let identifier_type = IdentifierType::from_bits(header)?;
199
200        // Read body length (1 byte)
201        let body_len = read_u8(reader)? as usize;
202
203        // Read body
204        let mut body = vec![0u8; body_len];
205        reader.read_exact(&mut body)?;
206
207        // Read identifier if present
208        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        // Validate body length
227        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        // Write protocol header (protocol + identifier type)
235        let header = self.identifier_type().to_bits() | self.protocol.to_bits();
236        write_u8(writer, header)?;
237
238        // Write body length
239        write_u8(writer, self.body.len() as u8)?;
240
241        // Write body
242        writer.write_all(&self.body)?;
243
244        // Write identifier if present
245        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 + // Protocol header
254        1 + // Body length
255        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}