#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextClass {
Binary,
Text,
Latin1,
Extended,
}
pub(crate) const fn text_char_class(b: u8) -> TextClass {
match b {
0x07..=0x0D | 0x1B | 0x20..=0x7E | 0x85 => TextClass::Text,
0x80..=0x9F => TextClass::Extended,
0xA0..=0xFF => TextClass::Latin1,
_ => TextClass::Binary,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Utf8Look {
Invalid,
Control,
Ascii,
Multibyte,
}
const fn utf8_lead_shape(b: u8) -> Option<(usize, u8, u8)> {
match b {
0xC2..=0xDF => Some((1, 0x80, 0xBF)),
0xE0 => Some((2, 0xA0, 0xBF)),
0xE1..=0xEC | 0xEE..=0xEF => Some((2, 0x80, 0xBF)),
0xED => Some((2, 0x80, 0x9F)),
0xF0 => Some((3, 0x90, 0xBF)),
0xF1..=0xF3 => Some((3, 0x80, 0xBF)),
0xF4 => Some((3, 0x80, 0x8F)),
_ => None,
}
}
pub(crate) fn looks_utf8(buf: &[u8]) -> Utf8Look {
let finish = |ctrl: bool, gotone: bool| match (ctrl, gotone) {
(true, _) => Utf8Look::Control,
(false, true) => Utf8Look::Multibyte,
(false, false) => Utf8Look::Ascii,
};
let mut gotone = false;
let mut ctrl = false;
let mut i = 0;
while let Some(&b) = buf.get(i) {
i += 1;
if b & 0x80 == 0 {
if text_char_class(b) != TextClass::Text {
ctrl = true;
}
continue;
}
if b & 0x40 == 0 {
return Utf8Look::Invalid;
}
let Some((following, lo, hi)) = utf8_lead_shape(b) else {
return Utf8Look::Invalid;
};
for n in 0..following {
let Some(&c) = buf.get(i) else {
return finish(ctrl, gotone);
};
i += 1;
if (n == 0 && !(lo..=hi).contains(&c)) || c & 0xC0 != 0x80 {
return Utf8Look::Invalid;
}
}
gotone = true;
}
finish(ctrl, gotone)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextEncoding {
Empty,
Ascii,
Utf8,
Latin1,
Extended,
Data,
}
impl TextEncoding {
pub(crate) const fn label(self) -> &'static str {
match self {
Self::Empty => "empty",
Self::Ascii => "ASCII text",
Self::Utf8 => "Unicode text, UTF-8 text",
Self::Latin1 => "ISO-8859 text",
Self::Extended => "Non-ISO extended-ASCII text",
Self::Data => "data",
}
}
pub(crate) const fn is_text(self) -> bool {
!matches!(self, Self::Empty | Self::Data)
}
}
#[must_use]
pub(crate) fn classify(buffer: &[u8]) -> TextEncoding {
if buffer.is_empty() {
return TextEncoding::Empty;
}
if buffer
.iter()
.all(|&b| text_char_class(b) == TextClass::Text)
{
return TextEncoding::Ascii;
}
if looks_utf8(buffer) == Utf8Look::Multibyte {
return TextEncoding::Utf8;
}
if buffer
.iter()
.all(|&b| matches!(text_char_class(b), TextClass::Text | TextClass::Latin1))
{
return TextEncoding::Latin1;
}
if buffer
.iter()
.all(|&b| text_char_class(b) != TextClass::Binary)
{
return TextEncoding::Extended;
}
TextEncoding::Data
}
#[must_use]
pub fn classify_fallback(buffer: &[u8]) -> &'static str {
classify(buffer).label()
}
const MAXLINELEN: usize = 300;
const BYTES_MAX: usize = 7 * 1_048_576;
const ENCODING_MAX: usize = 65_536;
#[derive(Debug, Clone, Copy)]
pub(crate) struct TextWindow<'a> {
pub(crate) scan: &'a [u8],
pub(crate) hint: &'a [u8],
}
fn trim_nuls(buf: &[u8]) -> &[u8] {
let mut len = buf.len();
while len > 1 && buf.get(len - 1) == Some(&0) {
len -= 1;
}
if !len.is_multiple_of(2) && buf.len().is_multiple_of(2) {
len += 1;
}
buf.get(..len).unwrap_or(buf)
}
#[must_use]
pub(crate) fn text_window(buffer: &[u8]) -> TextWindow<'_> {
let read = buffer.get(..BYTES_MAX).unwrap_or(buffer);
let trimmed = trim_nuls(read);
TextWindow {
scan: trimmed.get(..ENCODING_MAX).unwrap_or(trimmed),
hint: read.get(..ENCODING_MAX).unwrap_or(read),
}
}
fn complete_utf8_prefix(buf: &[u8]) -> &[u8] {
match std::str::from_utf8(buf) {
Ok(_) => buf,
Err(e) => buf.get(..e.valid_up_to()).unwrap_or(buf),
}
}
pub(crate) fn text_pass_buffer(scan: &[u8], class: TextEncoding) -> std::borrow::Cow<'_, [u8]> {
use std::borrow::Cow;
match class {
TextEncoding::Utf8 => Cow::Borrowed(complete_utf8_prefix(scan)),
TextEncoding::Ascii | TextEncoding::Latin1 | TextEncoding::Extended
if !scan.iter().all(u8::is_ascii) =>
{
let mut out = Vec::with_capacity(scan.len() * 2);
for &b in scan {
if b < 0x80 {
out.push(b);
} else {
out.push(0xC0 | (b >> 6));
out.push(0x80 | (b & 0x3F));
}
}
Cow::Owned(out)
}
TextEncoding::Ascii
| TextEncoding::Latin1
| TextEncoding::Extended
| TextEncoding::Empty
| TextEncoding::Data => Cow::Borrowed(scan),
}
}
const NEL: u32 = 0x85;
pub(crate) fn text_qualifiers(text: &[u8]) -> String {
if looks_utf8(text) == Utf8Look::Multibyte
&& let Ok(s) = std::str::from_utf8(complete_utf8_prefix(text))
{
return scan_qualifiers(s.chars().map(u32::from));
}
scan_qualifiers(text.iter().map(|&b| u32::from(b)))
}
fn scan_qualifiers(code_points: impl Iterator<Item = u32>) -> String {
let (mut n_crlf, mut n_cr, mut n_lf, mut n_nel) = (0_usize, 0_usize, 0_usize, 0_usize);
let (mut has_escapes, mut has_backspace, mut seen_cr) = (false, false, false);
let mut longest = 0_usize;
let mut line_start = 0_usize;
for (i, c) in code_points.enumerate() {
if c == u32::from(b'\n') {
if seen_cr {
n_crlf += 1;
} else {
n_lf += 1;
}
line_start = i + 1;
} else if seen_cr {
n_cr += 1;
}
seen_cr = c == u32::from(b'\r');
if seen_cr {
line_start = i + 1;
}
if c == NEL {
n_nel += 1;
line_start = i + 1;
}
let line_len = i + 1 - line_start;
if line_len > MAXLINELEN {
longest = longest.max(line_len);
}
has_escapes |= c == 0x1B;
has_backspace |= c == 0x08;
}
let mut out = String::new();
if longest > 0 {
out.push_str(", with very long lines (");
out.push_str(&longest.to_string());
out.push(')');
}
let none = n_crlf == 0 && n_cr == 0 && n_nel == 0 && n_lf == 0;
if none || n_crlf != 0 || n_cr != 0 || n_nel != 0 {
let kinds: Vec<&str> = [(n_crlf, "CRLF"), (n_cr, "CR"), (n_lf, "LF"), (n_nel, "NEL")]
.iter()
.filter(|(n, _)| *n != 0)
.map(|&(_, name)| name)
.collect();
out.push_str(", with ");
out.push_str(&if none {
"no".to_string()
} else {
kinds.join(", ")
});
out.push_str(" line terminators");
}
if has_escapes {
out.push_str(", with escape sequences");
}
if has_backspace {
out.push_str(", with overstriking");
}
out
}
pub(crate) fn append_text_class(desc: &str, class: &str, qualifiers: &str) -> String {
let mut head = if desc.is_empty() {
class.to_string()
} else if let Some(base) = desc.strip_suffix(" text") {
format!("{base}, {class}")
} else if let Some(base) = desc.strip_suffix(" text executable") {
format!("{base}, {class} executable")
} else {
format!("{desc}, {class}")
};
head.push_str(qualifiers);
head
}
#[cfg(test)]
mod tests;