1use kernel::{AxCode, AxError};
17use serde::{Deserialize, Serialize};
18
19const UTF8_MARK: [u8; 3] = [0xEF, 0xBB, 0xBF];
20const UTF16_LE_MARK: [u8; 2] = [0xFF, 0xFE];
21const UTF16_BE_MARK: [u8; 2] = [0xFE, 0xFF];
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27pub enum Encoding {
28 Utf8,
30 Utf8Bom,
32 Utf16Le,
34 Utf16Be,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Reading {
41 Text(Encoding),
43 Opaque,
46}
47
48impl Reading {
49 pub fn of(bytes: &[u8]) -> Reading {
51 let encoding = Encoding::of_mark(bytes);
52 if encoding.spells(bytes) {
53 Reading::Text(encoding)
54 } else {
55 Reading::Opaque
56 }
57 }
58}
59
60impl Encoding {
61 pub fn of_mark(front: &[u8]) -> Encoding {
65 if front.starts_with(&UTF8_MARK) {
66 Encoding::Utf8Bom
67 } else if front.starts_with(&UTF16_LE_MARK) {
68 Encoding::Utf16Le
69 } else if front.starts_with(&UTF16_BE_MARK) {
70 Encoding::Utf16Be
71 } else {
72 Encoding::Utf8
73 }
74 }
75
76 fn spells(self, bytes: &[u8]) -> bool {
79 match self {
80 Encoding::Utf8 => !bytes.contains(&0) && std::str::from_utf8(bytes).is_ok(),
81 Encoding::Utf8Bom => std::str::from_utf8(bytes).is_ok(),
82 Encoding::Utf16Le | Encoding::Utf16Be => {
83 let (units, rest) = bytes.as_chunks::<2>();
84 rest.is_empty() && char::decode_utf16(self.units(units)).all(|unit| unit.is_ok())
85 }
86 }
87 }
88
89 pub fn decode(self, bytes: &[u8]) -> Result<String, AxError> {
98 let text = match self {
99 Encoding::Utf8 | Encoding::Utf8Bom => std::str::from_utf8(bytes)
100 .ok()
101 .filter(|text| matches!(self, Encoding::Utf8Bom) || !text.contains('\0'))
102 .map(str::to_owned),
103 Encoding::Utf16Le | Encoding::Utf16Be => {
104 let (units, rest) = bytes.as_chunks::<2>();
105 rest.is_empty()
106 .then(|| char::decode_utf16(self.units(units)).collect::<Result<String, _>>())
107 .and_then(Result::ok)
108 }
109 };
110 text.ok_or_else(|| {
111 AxError::failure(
112 AxCode::InvalidArgs,
113 "read a document window",
114 format!("{} bytes that are not {self:?} text", bytes.len()),
115 )
116 .with_recovery("open the document again: its answer says whether it reads as text")
117 })
118 }
119
120 pub(crate) fn begins_at(self, bytes: &[u8], index: usize, origin: u64) -> bool {
125 match self {
126 Encoding::Utf8 | Encoding::Utf8Bom => {
127 bytes.get(index).is_some_and(|byte| byte & 0xC0 != 0x80)
128 }
129 Encoding::Utf16Le | Encoding::Utf16Be => {
130 let at = origin.saturating_add(crate::span::offset(index));
131 let unit = bytes
132 .get(index..)
133 .and_then(<[u8]>::first_chunk::<2>)
134 .map(|pair| self.unit(*pair));
135 at.is_multiple_of(2) && unit.is_some_and(|unit| !(0xDC00..=0xDFFF).contains(&unit))
136 }
137 }
138 }
139
140 pub(crate) fn encode(self, text: &str) -> Vec<u8> {
144 match self {
145 Encoding::Utf8 | Encoding::Utf8Bom => text.as_bytes().to_vec(),
146 Encoding::Utf16Le => text.encode_utf16().flat_map(u16::to_le_bytes).collect(),
147 Encoding::Utf16Be => text.encode_utf16().flat_map(u16::to_be_bytes).collect(),
148 }
149 }
150
151 fn units(self, pairs: &[[u8; 2]]) -> impl Iterator<Item = u16> {
152 pairs.iter().map(move |pair| self.unit(*pair))
153 }
154
155 fn unit(self, pair: [u8; 2]) -> u16 {
156 match self {
157 Encoding::Utf16Be => u16::from_be_bytes(pair),
158 Encoding::Utf8 | Encoding::Utf8Bom | Encoding::Utf16Le => u16::from_le_bytes(pair),
159 }
160 }
161}
162
163#[cfg(test)]
164#[allow(
165 clippy::unwrap_used,
166 clippy::expect_used,
167 clippy::panic,
168 clippy::indexing_slicing,
169 reason = "test code"
170)]
171mod tests;