use crate::error::{Error, Result};
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum TlsRecordType {
ChangeCipherSpec,
Alert,
Handshake,
ApplicationData,
Other(u8),
}
impl TlsRecordType {
#[must_use]
pub const fn from_u8(v: u8) -> Self {
match v {
20 => Self::ChangeCipherSpec,
21 => Self::Alert,
22 => Self::Handshake,
23 => Self::ApplicationData,
other => Self::Other(other),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TlsRecord<'a> {
pub record_type: TlsRecordType,
pub legacy_version: u16,
pub payload: &'a [u8],
}
pub const MAX_RECORD_PAYLOAD: usize = 16_384;
pub const RECORD_HEADER_LEN: usize = 5;
pub fn parse_records(buf: &[u8]) -> Result<(Vec<TlsRecord<'_>>, usize)> {
let mut records = Vec::new();
let mut cursor = 0;
while buf.len() - cursor >= RECORD_HEADER_LEN {
let header = &buf[cursor..cursor + RECORD_HEADER_LEN];
let record_type = TlsRecordType::from_u8(header[0]);
let legacy_version = u16::from_be_bytes([header[1], header[2]]);
let length = usize::from(u16::from_be_bytes([header[3], header[4]]));
if length > MAX_RECORD_PAYLOAD {
return Err(Error::Parse(format!(
"record payload length {length} exceeds maximum {MAX_RECORD_PAYLOAD}"
)));
}
let record_end = cursor + RECORD_HEADER_LEN + length;
if record_end > buf.len() {
break;
}
records.push(TlsRecord {
record_type,
legacy_version,
payload: &buf[cursor + RECORD_HEADER_LEN..record_end],
});
cursor = record_end;
}
Ok((records, cursor))
}
#[cfg(test)]
mod tests {
use super::*;
fn record(kind: u8, payload: &[u8]) -> Vec<u8> {
let mut out = vec![kind, 0x03, 0x03];
out.extend_from_slice(&u16::try_from(payload.len()).unwrap().to_be_bytes());
out.extend_from_slice(payload);
out
}
#[test]
fn returns_empty_on_empty_input() {
let (records, consumed) = parse_records(&[]).unwrap();
assert!(records.is_empty());
assert_eq!(consumed, 0);
}
#[test]
fn parses_single_record() {
let bytes = record(22, &[0xaa, 0xbb, 0xcc]);
let (records, consumed) = parse_records(&bytes).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].record_type, TlsRecordType::Handshake);
assert_eq!(records[0].payload, &[0xaa, 0xbb, 0xcc]);
assert_eq!(consumed, bytes.len());
}
#[test]
fn leaves_incomplete_trailer() {
let mut bytes = record(23, &[1, 2, 3, 4]);
bytes.extend_from_slice(&[22, 0x03, 0x03]);
let (records, consumed) = parse_records(&bytes).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(consumed, 9);
}
#[test]
fn leaves_incomplete_payload() {
let mut bytes = record(23, &[1, 2, 3, 4]);
bytes.extend_from_slice(&[22, 0x03, 0x03, 0x00, 0x0a, 0xff, 0xff]);
let (records, consumed) = parse_records(&bytes).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(consumed, 9);
}
#[test]
fn rejects_oversize_record() {
let bytes = [22, 0x03, 0x03, 0xff, 0xff];
let err = parse_records(&bytes).unwrap_err();
assert!(matches!(err, Error::Parse(_)));
}
#[test]
fn never_panics_on_random_input() {
for len in 0u16..64 {
let bytes: Vec<u8> = (0..len)
.map(|i| u8::try_from(i & 0xff).unwrap_or(0))
.collect();
let _ = parse_records(&bytes);
}
}
}