Skip to main content

serializer/
utf8.rs

1//! UTF-8 validation utilities for dx-serializer
2//!
3//! This module provides UTF-8 validation functions that return detailed
4//! error information including byte offsets for invalid sequences.
5
6use crate::error::{DxError, Result};
7
8/// Validate that a byte slice is valid UTF-8
9///
10/// Returns the validated string slice on success, or a `DxError::Utf8Error`
11/// with the byte offset of the first invalid sequence on failure.
12///
13/// # Examples
14///
15/// ```
16/// use serializer::utf8::validate_utf8;
17///
18/// // Valid UTF-8
19/// let valid = b"Hello, World!";
20/// assert!(validate_utf8(valid).is_ok());
21///
22/// // Invalid UTF-8 (invalid continuation byte)
23/// let invalid = &[0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x80]; // "Hello" + invalid byte
24/// let err = validate_utf8(invalid).unwrap_err();
25/// // Error contains offset 5 (position of invalid byte)
26/// ```
27pub fn validate_utf8(bytes: &[u8]) -> Result<&str> {
28    match std::str::from_utf8(bytes) {
29        Ok(s) => Ok(s),
30        Err(e) => Err(DxError::Utf8Error {
31            offset: e.valid_up_to(),
32        }),
33    }
34}
35
36/// Validate UTF-8 and return owned String
37///
38/// Converts a byte vector to String, returning detailed error information
39/// if the bytes are not valid UTF-8.
40pub fn validate_utf8_owned(bytes: Vec<u8>) -> Result<String> {
41    match String::from_utf8(bytes) {
42        Ok(s) => Ok(s),
43        Err(e) => Err(DxError::Utf8Error {
44            offset: e.utf8_error().valid_up_to(),
45        }),
46    }
47}
48
49/// Validate UTF-8 with detailed error information
50///
51/// Returns additional context about the invalid sequence including
52/// the invalid byte value and expected continuation information.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Utf8ValidationError {
55    /// Byte offset where the error occurred
56    pub offset: usize,
57    /// The invalid byte value (if available)
58    pub invalid_byte: Option<u8>,
59    /// Human-readable description of the error
60    pub description: String,
61}
62
63impl Utf8ValidationError {
64    /// Create a new validation error
65    pub fn new(offset: usize, invalid_byte: Option<u8>, description: impl Into<String>) -> Self {
66        Self {
67            offset,
68            invalid_byte,
69            description: description.into(),
70        }
71    }
72}
73
74/// Validate UTF-8 with detailed error information
75///
76/// This function provides more detailed error information than `validate_utf8`,
77/// including the specific invalid byte and a description of why it's invalid.
78#[allow(unsafe_code)]
79pub fn validate_utf8_detailed(bytes: &[u8]) -> std::result::Result<&str, Utf8ValidationError> {
80    let mut i = 0;
81    while i < bytes.len() {
82        let byte = bytes[i];
83
84        // Determine expected sequence length from first byte
85        let seq_len = if byte & 0x80 == 0 {
86            // ASCII: 0xxxxxxx
87            1
88        } else if byte & 0xE0 == 0xC0 {
89            // 2-byte: 110xxxxx
90            2
91        } else if byte & 0xF0 == 0xE0 {
92            // 3-byte: 1110xxxx
93            3
94        } else if byte & 0xF8 == 0xF0 {
95            // 4-byte: 11110xxx
96            4
97        } else if byte & 0xC0 == 0x80 {
98            // Unexpected continuation byte
99            return Err(Utf8ValidationError::new(
100                i,
101                Some(byte),
102                format!(
103                    "Unexpected continuation byte 0x{:02X} at position {}",
104                    byte, i
105                ),
106            ));
107        } else {
108            // Invalid start byte
109            return Err(Utf8ValidationError::new(
110                i,
111                Some(byte),
112                format!("Invalid UTF-8 start byte 0x{:02X} at position {}", byte, i),
113            ));
114        };
115
116        // Check if we have enough bytes
117        if i + seq_len > bytes.len() {
118            return Err(Utf8ValidationError::new(
119                i,
120                Some(byte),
121                format!(
122                    "Incomplete UTF-8 sequence at position {}: expected {} bytes, got {}",
123                    i,
124                    seq_len,
125                    bytes.len() - i
126                ),
127            ));
128        }
129
130        // Validate continuation bytes
131        for j in 1..seq_len {
132            let cont_byte = bytes[i + j];
133            if cont_byte & 0xC0 != 0x80 {
134                return Err(Utf8ValidationError::new(
135                    i + j,
136                    Some(cont_byte),
137                    format!(
138                        "Invalid continuation byte 0x{:02X} at position {} (expected 10xxxxxx)",
139                        cont_byte,
140                        i + j
141                    ),
142                ));
143            }
144        }
145
146        // Additional validation for overlong encodings and invalid code points
147        if seq_len == 2 {
148            // 2-byte sequences must encode values >= 0x80
149            let code_point = ((byte as u32 & 0x1F) << 6) | (bytes[i + 1] as u32 & 0x3F);
150            if code_point < 0x80 {
151                return Err(Utf8ValidationError::new(
152                    i,
153                    Some(byte),
154                    format!(
155                        "Overlong encoding at position {}: 2-byte sequence for code point U+{:04X}",
156                        i, code_point
157                    ),
158                ));
159            }
160        } else if seq_len == 3 {
161            // 3-byte sequences must encode values >= 0x800 and not be surrogates
162            let code_point = ((byte as u32 & 0x0F) << 12)
163                | ((bytes[i + 1] as u32 & 0x3F) << 6)
164                | (bytes[i + 2] as u32 & 0x3F);
165            if code_point < 0x800 {
166                return Err(Utf8ValidationError::new(
167                    i,
168                    Some(byte),
169                    format!(
170                        "Overlong encoding at position {}: 3-byte sequence for code point U+{:04X}",
171                        i, code_point
172                    ),
173                ));
174            }
175            if (0xD800..=0xDFFF).contains(&code_point) {
176                return Err(Utf8ValidationError::new(
177                    i,
178                    Some(byte),
179                    format!(
180                        "Invalid surrogate code point U+{:04X} at position {}",
181                        code_point, i
182                    ),
183                ));
184            }
185        } else if seq_len == 4 {
186            // 4-byte sequences must encode values >= 0x10000 and <= 0x10FFFF
187            let code_point = ((byte as u32 & 0x07) << 18)
188                | ((bytes[i + 1] as u32 & 0x3F) << 12)
189                | ((bytes[i + 2] as u32 & 0x3F) << 6)
190                | (bytes[i + 3] as u32 & 0x3F);
191            if code_point < 0x10000 {
192                return Err(Utf8ValidationError::new(
193                    i,
194                    Some(byte),
195                    format!(
196                        "Overlong encoding at position {}: 4-byte sequence for code point U+{:04X}",
197                        i, code_point
198                    ),
199                ));
200            }
201            if code_point > 0x10FFFF {
202                return Err(Utf8ValidationError::new(
203                    i,
204                    Some(byte),
205                    format!(
206                        "Code point U+{:04X} exceeds maximum U+10FFFF at position {}",
207                        code_point, i
208                    ),
209                ));
210            }
211        }
212
213        i += seq_len;
214    }
215
216    // All validation passed, safe to convert
217    // SAFETY: the manual scanner above validates start bytes, continuation
218    // bytes, overlong encodings, surrogate ranges, and maximum code point.
219    Ok(unsafe { std::str::from_utf8_unchecked(bytes) })
220}
221
222/// Validate a string value during parsing
223///
224/// This is the main entry point for UTF-8 validation during parsing.
225/// It validates the input and returns a DxError with byte offset on failure.
226pub fn validate_string_input(bytes: &[u8], base_offset: usize) -> Result<&str> {
227    match std::str::from_utf8(bytes) {
228        Ok(s) => Ok(s),
229        Err(e) => Err(DxError::Utf8Error {
230            offset: base_offset + e.valid_up_to(),
231        }),
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_valid_ascii() {
241        let input = b"Hello, World!";
242        assert_eq!(validate_utf8(input).unwrap(), "Hello, World!");
243    }
244
245    #[test]
246    fn test_valid_utf8_multibyte() {
247        // "Hello, 世界!" in UTF-8
248        let input = "Hello, 世界!".as_bytes();
249        assert_eq!(validate_utf8(input).unwrap(), "Hello, 世界!");
250    }
251
252    #[test]
253    fn test_valid_utf8_emoji() {
254        // Emoji (4-byte sequence)
255        let input = "Hello 🌍!".as_bytes();
256        assert_eq!(validate_utf8(input).unwrap(), "Hello 🌍!");
257    }
258
259    #[test]
260    fn test_invalid_continuation_byte() {
261        // Invalid continuation byte at position 5
262        let input = &[0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x80]; // "Hello" + 0x80
263        let err = validate_utf8(input).unwrap_err();
264        if let DxError::Utf8Error { offset } = err {
265            assert_eq!(offset, 5);
266        } else {
267            panic!("Expected Utf8Error");
268        }
269    }
270
271    #[test]
272    fn test_invalid_start_byte() {
273        // Invalid start byte 0xFF
274        let input = &[0x48, 0x65, 0xFF, 0x6c, 0x6f]; // "He" + 0xFF + "lo"
275        let err = validate_utf8(input).unwrap_err();
276        if let DxError::Utf8Error { offset } = err {
277            assert_eq!(offset, 2);
278        } else {
279            panic!("Expected Utf8Error");
280        }
281    }
282
283    #[test]
284    fn test_incomplete_sequence() {
285        // Incomplete 2-byte sequence at end
286        let input = &[0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xC2]; // "Hello" + start of 2-byte
287        let err = validate_utf8(input).unwrap_err();
288        if let DxError::Utf8Error { offset } = err {
289            assert_eq!(offset, 5);
290        } else {
291            panic!("Expected Utf8Error");
292        }
293    }
294
295    #[test]
296    fn test_detailed_validation_overlong() {
297        // Overlong encoding of ASCII 'A' (0x41) as 2-byte sequence
298        let input = &[0xC1, 0x81]; // Should be just 0x41
299        let err = validate_utf8_detailed(input).unwrap_err();
300        assert_eq!(err.offset, 0);
301        assert!(err.description.contains("Overlong"));
302    }
303
304    #[test]
305    fn test_detailed_validation_surrogate() {
306        // UTF-16 surrogate (U+D800) encoded as UTF-8
307        let input = &[0xED, 0xA0, 0x80]; // U+D800 (invalid in UTF-8)
308        let err = validate_utf8_detailed(input).unwrap_err();
309        assert_eq!(err.offset, 0);
310        assert!(err.description.contains("surrogate"));
311    }
312
313    #[test]
314    fn test_detailed_validation_too_large() {
315        // Code point > U+10FFFF
316        let input = &[0xF4, 0x90, 0x80, 0x80]; // U+110000 (invalid)
317        let err = validate_utf8_detailed(input).unwrap_err();
318        assert_eq!(err.offset, 0);
319        assert!(err.description.contains("exceeds"));
320    }
321
322    #[test]
323    fn test_validate_string_input_with_offset() {
324        // Invalid byte at position 2 in the slice, but base_offset is 10
325        let input = &[0x48, 0x65, 0xFF]; // "He" + invalid
326        let err = validate_string_input(input, 10).unwrap_err();
327        if let DxError::Utf8Error { offset } = err {
328            assert_eq!(offset, 12); // 10 + 2
329        } else {
330            panic!("Expected Utf8Error");
331        }
332    }
333
334    #[test]
335    fn test_empty_input() {
336        assert_eq!(validate_utf8(b"").unwrap(), "");
337    }
338
339    #[test]
340    fn test_validate_utf8_owned() {
341        let valid = vec![0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello"
342        assert_eq!(validate_utf8_owned(valid).unwrap(), "Hello");
343
344        let invalid = vec![0x48, 0x65, 0xFF]; // "He" + invalid
345        let err = validate_utf8_owned(invalid).unwrap_err();
346        if let DxError::Utf8Error { offset } = err {
347            assert_eq!(offset, 2);
348        } else {
349            panic!("Expected Utf8Error");
350        }
351    }
352}