Skip to main content

neco_fuzzy/
encode.rs

1use core::fmt;
2
3pub const PREPARED_CANDIDATE_FORMAT_VERSION: u16 = 1;
4pub const PREPARED_CANDIDATE_ALGORITHM_VERSION: u16 = 2;
5pub const PREPARED_CANDIDATE_HEADER_LEN: usize = 16;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct PreparedCandidateHeader {
9    pub format_version: u16,
10    pub algorithm_version: u16,
11    pub fingerprint: u64,
12    pub text_len: u32,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum EncodeError {
17    BufferTooSmall { required: usize },
18    TextTooLong { len: usize },
19}
20
21impl fmt::Display for EncodeError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Self::BufferTooSmall { required } => {
25                write!(
26                    f,
27                    "buffer too small for prepared candidate archive: need {required} bytes"
28                )
29            }
30            Self::TextTooLong { len } => {
31                write!(
32                    f,
33                    "prepared candidate text length {len} exceeds u32 archive header"
34                )
35            }
36        }
37    }
38}
39
40impl std::error::Error for EncodeError {}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum DecodeError {
44    Truncated { required: usize, actual: usize },
45    UnsupportedFormatVersion { expected: u16, actual: u16 },
46    UnsupportedAlgorithmVersion { expected: u16, actual: u16 },
47    FingerprintMismatch { expected: u64, actual: u64 },
48    PlatformLengthOverflow { text_len: u32 },
49    InvalidUtf8,
50}
51
52impl fmt::Display for DecodeError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Truncated { required, actual } => {
56                write!(
57                    f,
58                    "prepared candidate archive truncated: need {required} bytes, got {actual}"
59                )
60            }
61            Self::UnsupportedFormatVersion { expected, actual } => {
62                write!(
63                    f,
64                    "unsupported prepared candidate format version: expected {expected}, got {actual}"
65                )
66            }
67            Self::UnsupportedAlgorithmVersion { expected, actual } => {
68                write!(
69                    f,
70                    "unsupported prepared candidate algorithm version: expected {expected}, got {actual}"
71                )
72            }
73            Self::FingerprintMismatch { expected, actual } => {
74                write!(
75                    f,
76                    "prepared candidate fingerprint mismatch: expected {expected}, got {actual}"
77                )
78            }
79            Self::PlatformLengthOverflow { text_len } => {
80                write!(
81                    f,
82                    "prepared candidate archive length {text_len} does not fit on this platform"
83                )
84            }
85            Self::InvalidUtf8 => write!(f, "prepared candidate archive contains invalid utf-8"),
86        }
87    }
88}
89
90impl std::error::Error for DecodeError {}
91
92pub(crate) fn checked_archive_text_len(len: usize) -> Result<u32, EncodeError> {
93    u32::try_from(len).map_err(|_| EncodeError::TextTooLong { len })
94}
95
96pub(crate) fn decode_header(bytes: &[u8]) -> Result<PreparedCandidateHeader, DecodeError> {
97    if bytes.len() < PREPARED_CANDIDATE_HEADER_LEN {
98        return Err(DecodeError::Truncated {
99            required: PREPARED_CANDIDATE_HEADER_LEN,
100            actual: bytes.len(),
101        });
102    }
103
104    let format_version = u16::from_le_bytes([bytes[0], bytes[1]]);
105    let algorithm_version = u16::from_le_bytes([bytes[2], bytes[3]]);
106    let fingerprint = u64::from_le_bytes([
107        bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11],
108    ]);
109    let text_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
110    Ok(PreparedCandidateHeader {
111        format_version,
112        algorithm_version,
113        fingerprint,
114        text_len,
115    })
116}
117
118pub(crate) fn encode_candidate(
119    text: &str,
120    fingerprint: u64,
121    out: &mut [u8],
122) -> Result<usize, EncodeError> {
123    let required = encoded_len(text.len());
124    if out.len() < required {
125        return Err(EncodeError::BufferTooSmall { required });
126    }
127
128    let header = PreparedCandidateHeader {
129        format_version: PREPARED_CANDIDATE_FORMAT_VERSION,
130        algorithm_version: PREPARED_CANDIDATE_ALGORITHM_VERSION,
131        fingerprint,
132        text_len: checked_archive_text_len(text.len())?,
133    };
134    out[0..2].copy_from_slice(&header.format_version.to_le_bytes());
135    out[2..4].copy_from_slice(&header.algorithm_version.to_le_bytes());
136    out[4..12].copy_from_slice(&header.fingerprint.to_le_bytes());
137    out[12..16].copy_from_slice(&header.text_len.to_le_bytes());
138    out[16..required].copy_from_slice(text.as_bytes());
139    Ok(required)
140}
141
142pub(crate) fn encoded_len(text_len: usize) -> usize {
143    PREPARED_CANDIDATE_HEADER_LEN + text_len
144}
145
146pub(crate) fn decode_candidate_text(
147    bytes: &[u8],
148) -> Result<(&str, PreparedCandidateHeader), DecodeError> {
149    let header = decode_header(bytes)?;
150    if header.format_version != PREPARED_CANDIDATE_FORMAT_VERSION {
151        return Err(DecodeError::UnsupportedFormatVersion {
152            expected: PREPARED_CANDIDATE_FORMAT_VERSION,
153            actual: header.format_version,
154        });
155    }
156    if header.algorithm_version != PREPARED_CANDIDATE_ALGORITHM_VERSION {
157        return Err(DecodeError::UnsupportedAlgorithmVersion {
158            expected: PREPARED_CANDIDATE_ALGORITHM_VERSION,
159            actual: header.algorithm_version,
160        });
161    }
162
163    let text_len =
164        usize::try_from(header.text_len).map_err(|_| DecodeError::PlatformLengthOverflow {
165            text_len: header.text_len,
166        })?;
167    let required = PREPARED_CANDIDATE_HEADER_LEN + text_len;
168    if bytes.len() < required {
169        return Err(DecodeError::Truncated {
170            required,
171            actual: bytes.len(),
172        });
173    }
174
175    let text_bytes = &bytes[PREPARED_CANDIDATE_HEADER_LEN..required];
176    let text = core::str::from_utf8(text_bytes).map_err(|_| DecodeError::InvalidUtf8)?;
177    Ok((text, header))
178}