use crate::error::{DxError, Result};
pub fn validate_utf8(bytes: &[u8]) -> Result<&str> {
match std::str::from_utf8(bytes) {
Ok(s) => Ok(s),
Err(e) => Err(DxError::Utf8Error {
offset: e.valid_up_to(),
}),
}
}
pub fn validate_utf8_owned(bytes: Vec<u8>) -> Result<String> {
match String::from_utf8(bytes) {
Ok(s) => Ok(s),
Err(e) => Err(DxError::Utf8Error {
offset: e.utf8_error().valid_up_to(),
}),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Utf8ValidationError {
pub offset: usize,
pub invalid_byte: Option<u8>,
pub description: String,
}
impl Utf8ValidationError {
pub fn new(offset: usize, invalid_byte: Option<u8>, description: impl Into<String>) -> Self {
Self {
offset,
invalid_byte,
description: description.into(),
}
}
}
#[allow(unsafe_code)]
pub fn validate_utf8_detailed(bytes: &[u8]) -> std::result::Result<&str, Utf8ValidationError> {
let mut i = 0;
while i < bytes.len() {
let byte = bytes[i];
let seq_len = if byte & 0x80 == 0 {
1
} else if byte & 0xE0 == 0xC0 {
2
} else if byte & 0xF0 == 0xE0 {
3
} else if byte & 0xF8 == 0xF0 {
4
} else if byte & 0xC0 == 0x80 {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!(
"Unexpected continuation byte 0x{:02X} at position {}",
byte, i
),
));
} else {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!("Invalid UTF-8 start byte 0x{:02X} at position {}", byte, i),
));
};
if i + seq_len > bytes.len() {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!(
"Incomplete UTF-8 sequence at position {}: expected {} bytes, got {}",
i,
seq_len,
bytes.len() - i
),
));
}
for j in 1..seq_len {
let cont_byte = bytes[i + j];
if cont_byte & 0xC0 != 0x80 {
return Err(Utf8ValidationError::new(
i + j,
Some(cont_byte),
format!(
"Invalid continuation byte 0x{:02X} at position {} (expected 10xxxxxx)",
cont_byte,
i + j
),
));
}
}
if seq_len == 2 {
let code_point = ((byte as u32 & 0x1F) << 6) | (bytes[i + 1] as u32 & 0x3F);
if code_point < 0x80 {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!(
"Overlong encoding at position {}: 2-byte sequence for code point U+{:04X}",
i, code_point
),
));
}
} else if seq_len == 3 {
let code_point = ((byte as u32 & 0x0F) << 12)
| ((bytes[i + 1] as u32 & 0x3F) << 6)
| (bytes[i + 2] as u32 & 0x3F);
if code_point < 0x800 {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!(
"Overlong encoding at position {}: 3-byte sequence for code point U+{:04X}",
i, code_point
),
));
}
if (0xD800..=0xDFFF).contains(&code_point) {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!(
"Invalid surrogate code point U+{:04X} at position {}",
code_point, i
),
));
}
} else if seq_len == 4 {
let code_point = ((byte as u32 & 0x07) << 18)
| ((bytes[i + 1] as u32 & 0x3F) << 12)
| ((bytes[i + 2] as u32 & 0x3F) << 6)
| (bytes[i + 3] as u32 & 0x3F);
if code_point < 0x10000 {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!(
"Overlong encoding at position {}: 4-byte sequence for code point U+{:04X}",
i, code_point
),
));
}
if code_point > 0x10FFFF {
return Err(Utf8ValidationError::new(
i,
Some(byte),
format!(
"Code point U+{:04X} exceeds maximum U+10FFFF at position {}",
code_point, i
),
));
}
}
i += seq_len;
}
Ok(unsafe { std::str::from_utf8_unchecked(bytes) })
}
pub fn validate_string_input(bytes: &[u8], base_offset: usize) -> Result<&str> {
match std::str::from_utf8(bytes) {
Ok(s) => Ok(s),
Err(e) => Err(DxError::Utf8Error {
offset: base_offset + e.valid_up_to(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_ascii() {
let input = b"Hello, World!";
assert_eq!(validate_utf8(input).unwrap(), "Hello, World!");
}
#[test]
fn test_valid_utf8_multibyte() {
let input = "Hello, 世界!".as_bytes();
assert_eq!(validate_utf8(input).unwrap(), "Hello, 世界!");
}
#[test]
fn test_valid_utf8_emoji() {
let input = "Hello 🌍!".as_bytes();
assert_eq!(validate_utf8(input).unwrap(), "Hello 🌍!");
}
#[test]
fn test_invalid_continuation_byte() {
let input = &[0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x80]; let err = validate_utf8(input).unwrap_err();
if let DxError::Utf8Error { offset } = err {
assert_eq!(offset, 5);
} else {
panic!("Expected Utf8Error");
}
}
#[test]
fn test_invalid_start_byte() {
let input = &[0x48, 0x65, 0xFF, 0x6c, 0x6f]; let err = validate_utf8(input).unwrap_err();
if let DxError::Utf8Error { offset } = err {
assert_eq!(offset, 2);
} else {
panic!("Expected Utf8Error");
}
}
#[test]
fn test_incomplete_sequence() {
let input = &[0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xC2]; let err = validate_utf8(input).unwrap_err();
if let DxError::Utf8Error { offset } = err {
assert_eq!(offset, 5);
} else {
panic!("Expected Utf8Error");
}
}
#[test]
fn test_detailed_validation_overlong() {
let input = &[0xC1, 0x81]; let err = validate_utf8_detailed(input).unwrap_err();
assert_eq!(err.offset, 0);
assert!(err.description.contains("Overlong"));
}
#[test]
fn test_detailed_validation_surrogate() {
let input = &[0xED, 0xA0, 0x80]; let err = validate_utf8_detailed(input).unwrap_err();
assert_eq!(err.offset, 0);
assert!(err.description.contains("surrogate"));
}
#[test]
fn test_detailed_validation_too_large() {
let input = &[0xF4, 0x90, 0x80, 0x80]; let err = validate_utf8_detailed(input).unwrap_err();
assert_eq!(err.offset, 0);
assert!(err.description.contains("exceeds"));
}
#[test]
fn test_validate_string_input_with_offset() {
let input = &[0x48, 0x65, 0xFF]; let err = validate_string_input(input, 10).unwrap_err();
if let DxError::Utf8Error { offset } = err {
assert_eq!(offset, 12); } else {
panic!("Expected Utf8Error");
}
}
#[test]
fn test_empty_input() {
assert_eq!(validate_utf8(b"").unwrap(), "");
}
#[test]
fn test_validate_utf8_owned() {
let valid = vec![0x48, 0x65, 0x6c, 0x6c, 0x6f]; assert_eq!(validate_utf8_owned(valid).unwrap(), "Hello");
let invalid = vec![0x48, 0x65, 0xFF]; let err = validate_utf8_owned(invalid).unwrap_err();
if let DxError::Utf8Error { offset } = err {
assert_eq!(offset, 2);
} else {
panic!("Expected Utf8Error");
}
}
}