use crate::binary::{BinaryRead, BinaryWrite, read_u8, write_u8};
use std::io::{self, Read, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Protocol {
Http = 0x0,
Https = 0x1,
SharedResourceDirectory = 0xF,
}
impl Protocol {
pub fn from_bits(bits: u8) -> io::Result<Self> {
match bits & 0x0F {
0x0 => Ok(Protocol::Http),
0x1 => Ok(Protocol::Https),
0xF => Ok(Protocol::SharedResourceDirectory),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unknown protocol: 0x{:X}", bits),
)),
}
}
pub fn to_bits(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdentifierType {
None,
TwoByte,
EightByte,
ThirtyTwoByte,
}
impl IdentifierType {
pub fn from_bits(bits: u8) -> io::Result<Self> {
match (bits >> 4) & 0x0F {
0x0 => Ok(IdentifierType::None),
0x1 => Ok(IdentifierType::TwoByte),
0x2 => Ok(IdentifierType::EightByte),
0x3 => Ok(IdentifierType::ThirtyTwoByte),
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Unknown identifier type: 0x{:X}", bits >> 4),
)),
}
}
pub fn to_bits(self) -> u8 {
let value = match self {
IdentifierType::None => 0x0,
IdentifierType::TwoByte => 0x1,
IdentifierType::EightByte => 0x2,
IdentifierType::ThirtyTwoByte => 0x3,
};
value << 4
}
pub fn byte_length(self) -> usize {
match self {
IdentifierType::None => 0,
IdentifierType::TwoByte => 2,
IdentifierType::EightByte => 8,
IdentifierType::ThirtyTwoByte => 32,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceLocator {
pub protocol: Protocol,
pub identifier: Option<Vec<u8>>,
pub body: Vec<u8>,
}
impl ResourceLocator {
pub fn new(protocol: Protocol, body: impl Into<Vec<u8>>) -> Self {
Self {
protocol,
identifier: None,
body: body.into(),
}
}
pub fn with_identifier(mut self, identifier: Vec<u8>) -> io::Result<Self> {
match identifier.len() {
0 => {
self.identifier = None;
}
2 | 8 | 32 => {
self.identifier = Some(identifier);
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid identifier length: {}", identifier.len()),
));
}
}
Ok(self)
}
pub fn identifier_type(&self) -> IdentifierType {
match &self.identifier {
None => IdentifierType::None,
Some(id) => match id.len() {
2 => IdentifierType::TwoByte,
8 => IdentifierType::EightByte,
32 => IdentifierType::ThirtyTwoByte,
_ => IdentifierType::None, },
}
}
pub fn from_url(url: &str) -> io::Result<Self> {
if let Some(rest) = url.strip_prefix("http://") {
Ok(Self::new(Protocol::Http, rest.as_bytes()))
} else if let Some(rest) = url.strip_prefix("https://") {
Ok(Self::new(Protocol::Https, rest.as_bytes()))
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"URL must start with http:// or https://",
))
}
}
pub fn to_url(&self) -> io::Result<String> {
let protocol_str = match self.protocol {
Protocol::Http => "http://",
Protocol::Https => "https://",
Protocol::SharedResourceDirectory => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Shared Resource Directory cannot be converted to URL",
));
}
};
let body_str = String::from_utf8(self.body.clone()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "Body contains invalid UTF-8")
})?;
Ok(format!("{}{}", protocol_str, body_str))
}
}
impl BinaryRead for ResourceLocator {
fn read_from<R: Read>(reader: &mut R) -> io::Result<Self> {
let header = read_u8(reader)?;
let protocol = Protocol::from_bits(header)?;
let identifier_type = IdentifierType::from_bits(header)?;
let body_len = read_u8(reader)? as usize;
let mut body = vec![0u8; body_len];
reader.read_exact(&mut body)?;
let identifier = if identifier_type.byte_length() > 0 {
let mut id = vec![0u8; identifier_type.byte_length()];
reader.read_exact(&mut id)?;
Some(id)
} else {
None
};
Ok(Self {
protocol,
identifier,
body,
})
}
}
impl BinaryWrite for ResourceLocator {
fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
if self.body.len() > 255 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Body too long: {} bytes (max 255)", self.body.len()),
));
}
let header = self.identifier_type().to_bits() | self.protocol.to_bits();
write_u8(writer, header)?;
write_u8(writer, self.body.len() as u8)?;
writer.write_all(&self.body)?;
if let Some(ref id) = self.identifier {
writer.write_all(id)?;
}
Ok(())
}
fn serialized_size(&self) -> usize {
1 + 1 + self.body.len() +
self.identifier.as_ref().map(|id| id.len()).unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_resource_locator_http() {
let locator = ResourceLocator::from_url("http://kas.example.com").unwrap();
assert_eq!(locator.protocol, Protocol::Http);
assert_eq!(locator.body, b"kas.example.com");
assert_eq!(locator.identifier, None);
}
#[test]
fn test_resource_locator_https() {
let locator = ResourceLocator::from_url("https://kas.example.com/kas").unwrap();
assert_eq!(locator.protocol, Protocol::Https);
assert_eq!(locator.body, b"kas.example.com/kas");
}
#[test]
fn test_resource_locator_roundtrip() {
let original = ResourceLocator::new(Protocol::Https, b"kas.virtru.com".to_vec());
let mut buf = Vec::new();
original.write_to(&mut buf).unwrap();
let mut cursor = Cursor::new(buf);
let decoded = ResourceLocator::read_from(&mut cursor).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn test_resource_locator_with_identifier() {
let original = ResourceLocator::new(Protocol::Https, b"kas.example.com".to_vec())
.with_identifier(vec![0x01, 0x02])
.unwrap();
assert_eq!(original.identifier_type(), IdentifierType::TwoByte);
let mut buf = Vec::new();
original.write_to(&mut buf).unwrap();
let mut cursor = Cursor::new(buf);
let decoded = ResourceLocator::read_from(&mut cursor).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn test_to_url() {
let locator = ResourceLocator::new(Protocol::Https, b"kas.example.com/kas".to_vec());
assert_eq!(locator.to_url().unwrap(), "https://kas.example.com/kas");
}
}