use crate::errors::AmfError;
use crate::traits::{Marshall, MarshallLength, Unmarshall};
use std::borrow::Borrow;
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AmfUtf8<const LBW: usize> {
inner: String,
}
impl<const LBW: usize> AmfUtf8<LBW> {
pub fn new(inner: String) -> Result<Self, AmfError> {
debug_assert!(LBW == 2 || LBW == 4);
let len = inner.len();
if (LBW == 2 && len > u16::MAX as usize) || (LBW == 4 && len > u32::MAX as usize) {
return Err(AmfError::StringTooLong { max: LBW, got: len });
}
Ok(Self {
inner: inner.to_string(),
})
}
pub fn new_from_str(inner: &str) -> Result<Self, AmfError> {
Self::new(inner.to_string())
}
}
impl<const LBW: usize> Marshall for AmfUtf8<LBW> {
fn marshall(&self) -> Result<Vec<u8>, AmfError> {
debug_assert!(LBW == 2 || LBW == 4);
let mut vec = Vec::with_capacity(self.marshall_length());
if LBW == 2 {
vec.extend_from_slice((self.inner.len() as u16).to_be_bytes().as_slice())
} else if LBW == 4 {
vec.extend_from_slice((self.inner.len() as u32).to_be_bytes().as_slice())
} else {
return Err(AmfError::Custom("Invalid length byte width".to_string()));
}
vec.extend_from_slice(self.inner.as_bytes());
Ok(vec)
}
}
impl<const LBW: usize> MarshallLength for AmfUtf8<LBW> {
fn marshall_length(&self) -> usize {
debug_assert!(LBW == 2 || LBW == 4);
LBW + self.inner.len()
}
}
impl<const LBW: usize> Unmarshall for AmfUtf8<LBW> {
fn unmarshall(buf: &[u8]) -> Result<(Self, usize), AmfError> {
debug_assert!(LBW == 2 || LBW == 4);
let length;
if LBW == 2 {
if buf.len() < 2 {
return Err(AmfError::BufferTooSmall {
want: 2,
got: buf.len(),
});
}
length = u16::from_be_bytes(buf[0..2].try_into().unwrap()) as usize;
} else if LBW == 4 {
if buf.len() < 4 {
return Err(AmfError::BufferTooSmall {
want: 4,
got: buf.len(),
});
}
length = u32::from_be_bytes(buf[0..4].try_into().unwrap()) as usize;
} else {
return Err(AmfError::Custom("Invalid length byte width".to_string()));
}
let start = LBW;
let end = start + length;
if buf.len() < end {
return Err(AmfError::BufferTooSmall {
want: end,
got: buf.len(),
});
}
let value = std::str::from_utf8(&buf[start..end]).map_err(|e| AmfError::InvalidUtf8(e))?;
Ok((
Self {
inner: value.to_string(),
},
end,
))
}
}
impl<const LBW: usize> TryFrom<&[u8]> for AmfUtf8<LBW> {
type Error = AmfError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::unmarshall(value).map(|(v, _)| v)
}
}
impl<const LBW: usize> TryFrom<Vec<u8>> for AmfUtf8<LBW> {
type Error = AmfError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice())
}
}
impl<const LBW: usize> TryFrom<AmfUtf8<LBW>> for Vec<u8> {
type Error = AmfError;
fn try_from(value: AmfUtf8<LBW>) -> Result<Self, Self::Error> {
value.marshall()
}
}
impl<const LBW: usize> TryFrom<String> for AmfUtf8<LBW> {
type Error = AmfError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl<const LBW: usize> TryFrom<AmfUtf8<LBW>> for String {
type Error = AmfError;
fn try_from(value: AmfUtf8<LBW>) -> Result<Self, Self::Error> {
Ok(value.inner)
}
}
impl<const LBW: usize> TryFrom<&str> for AmfUtf8<LBW> {
type Error = AmfError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new_from_str(value)
}
}
impl<const LBW: usize> AsRef<str> for AmfUtf8<LBW> {
fn as_ref(&self) -> &str {
self.inner.as_ref()
}
}
impl<const LBW: usize> Deref for AmfUtf8<LBW> {
type Target = str;
fn deref(&self) -> &Self::Target {
Self::as_ref(self)
}
}
impl<const LBW: usize> Borrow<str> for AmfUtf8<LBW> {
fn borrow(&self) -> &str {
Self::as_ref(self)
}
}
impl<const LBW: usize> Display for AmfUtf8<LBW> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
impl<const LBW: usize> Default for AmfUtf8<LBW> {
fn default() -> Self {
Self::new_from_str("").unwrap()
}
}
pub type Utf8 = AmfUtf8<2>;
pub type Utf8Long = AmfUtf8<4>;
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::{Marshall, MarshallLength, Unmarshall};
use std::hash::{DefaultHasher, Hash, Hasher};
#[test]
fn new_valid_utf8_w2() {
let s = "a".repeat(u16::MAX as usize);
let amf_str = AmfUtf8::<2>::new_from_str(&s).unwrap();
assert_eq!(amf_str.inner, s);
}
#[test]
fn new_too_long_utf8_w2() {
let s = "a".repeat(u16::MAX as usize + 1);
assert!(matches!(
AmfUtf8::<2>::new_from_str(&s),
Err(AmfError::StringTooLong { max: 2, got: _ })
));
}
#[test]
fn new_valid_utf8_w4() {
let s = "a".repeat(1000); let amf_str = AmfUtf8::<4>::new_from_str(&s).unwrap();
assert_eq!(amf_str.inner, s);
}
#[test]
fn try_into_bytes_w2() {
let amf_str = AmfUtf8::<2>::new_from_str("hello").unwrap();
let bytes = amf_str.marshall().unwrap();
assert_eq!(bytes, &[0x00, 0x05, b'h', b'e', b'l', b'l', b'o']);
}
#[test]
fn try_into_bytes_w4() {
let amf_str = AmfUtf8::<4>::new_from_str("world").unwrap();
let bytes = amf_str.marshall().unwrap();
assert_eq!(
bytes,
&[0x00, 0x00, 0x00, 0x05, b'w', b'o', b'r', b'l', b'd']
);
}
#[test]
fn try_from_bytes_w2() {
let data = [0x00, 0x05, b'h', b'e', b'l', b'l', b'o'];
let (amf_str, consumed) = AmfUtf8::<2>::unmarshall(&data).unwrap();
assert_eq!(amf_str.inner, "hello");
assert_eq!(consumed, 7);
}
#[test]
fn try_from_bytes_w4() {
let data = [0x00, 0x00, 0x00, 0x05, b'w', b'o', b'r', b'l', b'd'];
let (amf_str, consumed) = AmfUtf8::<4>::unmarshall(&data).unwrap();
assert_eq!(amf_str.inner, "world");
assert_eq!(consumed, 9);
}
#[test]
fn length_calculation() {
let amf_str = AmfUtf8::<2>::new_from_str("abc").unwrap();
assert_eq!(amf_str.marshall_length(), 2 + 3);
let amf_str = AmfUtf8::<4>::new_from_str("abcde").unwrap();
assert_eq!(amf_str.marshall_length(), 4 + 5); }
#[test]
fn try_from_slice() {
let data = [0x00, 0x03, b'f', b'o', b'o'];
let amf_str: AmfUtf8<2> = data[..].try_into().unwrap();
assert_eq!(amf_str.inner, "foo");
}
#[test]
fn deref_and_as_ref() {
let amf_str = AmfUtf8::<2>::new_from_str("bar").unwrap();
assert_eq!(&*amf_str, "bar");
assert_eq!(amf_str.as_ref(), "bar");
}
#[test]
fn display_format() {
let amf_str = AmfUtf8::<2>::new_from_str("test").unwrap();
assert_eq!(format!("{}", amf_str), "test");
}
fn calculate_hash<T: Hash>(t: &T) -> u64 {
let mut hasher = DefaultHasher::new();
t.hash(&mut hasher);
hasher.finish()
}
#[test]
fn clone_preserves_equality() {
let original = AmfUtf8::<2>::new_from_str("hello").unwrap();
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn eq_and_neq_behaviour() {
let a = AmfUtf8::<4>::new_from_str("rust").unwrap();
let b_same = AmfUtf8::<4>::new_from_str("rust").unwrap();
let c_diff = AmfUtf8::<4>::new_from_str("Rust").unwrap();
assert_eq!(a, b_same);
assert_ne!(a, c_diff);
}
#[test]
fn equal_values_have_same_hash() {
let x = AmfUtf8::<2>::new_from_str("hash_me").unwrap();
let y = AmfUtf8::<2>::new_from_str("hash_me").unwrap();
let hx = calculate_hash(&x);
let hy = calculate_hash(&y);
assert_eq!(hx, hy, "Equal values should produce the same hash");
}
#[test]
fn different_values_have_different_hash() {
let x = AmfUtf8::<2>::new_from_str("foo").unwrap();
let y = AmfUtf8::<2>::new_from_str("bar").unwrap();
let hx = calculate_hash(&x);
let hy = calculate_hash(&y);
assert_ne!(hx, hy, "Different values should produce different hashes");
}
#[test]
fn clone_preserves_hash() {
let original = AmfUtf8::<4>::new_from_str("clone_hash").unwrap();
let cloned = original.clone();
let h1 = calculate_hash(&original);
let h2 = calculate_hash(&cloned);
assert_eq!(
h1, h2,
"Cloned instance should have the same hash as original"
);
}
}