#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
#[cfg(all(doctest, feature = "alloc", feature = "whatwg"))]
#[doc = include_str!("../README.md")]
struct Readme;
mod ascii;
#[cfg(feature = "big5")]
mod big5;
#[cfg(feature = "big5")]
#[allow(non_camel_case_types)]
mod big5_1984;
mod code_page;
mod decoder;
mod encoder;
#[cfg(feature = "gb18030")]
mod euc_cn;
#[cfg(feature = "euc-jp")]
mod euc_jp;
#[cfg(feature = "euc-jp")]
mod euc_jp_1997;
#[cfg(feature = "euc-kr")]
mod euc_kr;
#[cfg(feature = "full-byte")]
mod full_byte;
#[cfg(feature = "gb18030")]
mod gb18030;
mod identity;
#[cfg(any(
feature = "big5",
feature = "euc-jp",
feature = "euc-kr",
feature = "gb18030",
feature = "iso-2022-jp",
feature = "shift-jis"
))]
mod index;
#[cfg(feature = "iso-2022-cn")]
mod iso_2022_cn;
#[cfg(feature = "iso-2022-jp")]
mod iso_2022_jp;
#[cfg(feature = "iso-2022-jp")]
mod iso_2022_jp_1468;
#[cfg(feature = "iso-2022-jp-2")]
mod iso_2022_jp_2;
#[cfg(feature = "iso-2022-kr")]
mod iso_2022_kr;
#[cfg(any(feature = "euc-jp", feature = "iso-2022-jp", feature = "shift-jis"))]
mod jis0208_1997;
mod options;
mod replacement;
mod result;
#[cfg(feature = "scsu")]
mod scsu;
#[cfg(feature = "shift-jis")]
mod shift_jis;
#[cfg(feature = "shift-jis")]
#[allow(non_camel_case_types)]
mod shift_jis_1997;
#[cfg(feature = "half-byte")]
mod single_byte;
mod sink;
#[cfg(feature = "translit")]
mod translit;
mod tables;
#[cfg(test)]
mod tests;
mod utf_16;
#[cfg(feature = "unicode-extras")]
mod utf_32;
#[cfg(feature = "unicode-extras")]
mod utf_7;
mod utf_8;
mod variant;
mod x_user_defined;
mod encodings;
mod extra_encodings;
#[cfg(feature = "serde")]
mod serde_impl;
#[cfg(feature = "alloc")]
use alloc::{borrow::Cow, string::String, vec::Vec};
pub use crate::decoder::{DECODER_MIN_BUFFER, Decoder, MalformedError};
pub use crate::encoder::{ENCODER_MIN_BUFFER, Encoder, UnmappableError};
pub use crate::encodings::*;
pub use crate::extra_encodings::*;
pub use crate::options::{Bom, DecodeOptions, EncodeOptions, Malformed, Tally, Unmappable};
pub use crate::result::{DecoderResult, EncoderResult};
use crate::code_page::CODE_PAGES;
use crate::tables::extra_labels::EXTRA_CODE_PAGES;
#[cfg(feature = "whatwg-aliases")]
use crate::tables::labels::WHATWG_LABELS;
use crate::tables::labels::{ALL_ENCODINGS, LABELS, Label};
use crate::variant::VariantEncoding;
const MAX_LABEL_LEN: usize = 48;
pub struct Encoding {
name: &'static str,
variant: VariantEncoding,
}
impl Encoding {
pub(crate) const fn new(name: &'static str, variant: VariantEncoding) -> Encoding {
Encoding { name, variant }
}
pub(crate) fn variant(&self) -> VariantEncoding {
self.variant
}
pub(crate) fn bom(&self) -> Option<&'static [u8]> {
match self.variant {
VariantEncoding::Utf8 => Some(&[0xEF, 0xBB, 0xBF]),
VariantEncoding::Utf16Be => Some(&[0xFE, 0xFF]),
VariantEncoding::Utf16Le => Some(&[0xFF, 0xFE]),
_ => None,
}
}
pub fn name(&self) -> &'static str {
self.name
}
pub fn all() -> &'static [&'static Encoding] {
ALL_ENCODINGS
}
pub fn labels(&'static self) -> impl Iterator<Item = &'static str> {
LABELS
.iter()
.filter(move |entry| entry.encoding == self)
.map(|entry| entry.text)
}
pub fn for_label(label: &[u8]) -> Option<&'static Encoding> {
Encoding::look_up_in(LABELS, label).map(|entry| entry.encoding)
}
fn look_up_in(table: &'static [Label], label: &[u8]) -> Option<&'static Label> {
let trimmed = trim_ascii_whitespace(label);
if trimmed.is_empty() || trimmed.len() > MAX_LABEL_LEN {
return None;
}
let mut lowercased = [0u8; MAX_LABEL_LEN];
for (slot, byte) in lowercased.iter_mut().zip(trimmed) {
*slot = byte.to_ascii_lowercase();
}
let needle = &lowercased[..trimmed.len()];
table
.binary_search_by(|entry| entry.text.as_bytes().cmp(needle))
.ok()
.map(|i| &table[i])
}
#[cfg(feature = "whatwg-aliases")]
pub fn for_whatwg_label(label: &[u8]) -> Option<&'static Encoding> {
Encoding::look_up_in(WHATWG_LABELS, label).map(|entry| entry.encoding)
}
#[cfg(feature = "whatwg-aliases")]
pub fn for_whatwg_label_no_replacement(label: &[u8]) -> Option<&'static Encoding> {
match Encoding::for_whatwg_label(label) {
Some(encoding) if encoding.variant == VariantEncoding::Replacement => None,
other => other,
}
}
pub fn is_whatwg(&self) -> bool {
LABELS
.iter()
.any(|entry| entry.whatwg && entry.encoding == self)
}
pub fn for_label_no_replacement(label: &[u8]) -> Option<&'static Encoding> {
match Encoding::for_label(label) {
Some(encoding) if encoding.variant == VariantEncoding::Replacement => None,
other => other,
}
}
pub fn for_windows_code_page(code_page: u32) -> Option<&'static Encoding> {
for table in [CODE_PAGES, EXTRA_CODE_PAGES] {
if let Ok(i) = table.binary_search_by_key(&code_page, |entry| entry.number) {
return Some(table[i].encoding);
}
}
None
}
pub fn for_cp(code_page: u32) -> Option<&'static Encoding> {
Encoding::for_windows_code_page(code_page)
}
pub fn for_windows_code_page_no_replacement(code_page: u32) -> Option<&'static Encoding> {
match Encoding::for_windows_code_page(code_page) {
Some(encoding) if encoding.variant == VariantEncoding::Replacement => None,
other => other,
}
}
pub fn windows_code_page(&self) -> Option<u32> {
CODE_PAGES
.iter()
.chain(EXTRA_CODE_PAGES)
.find(|entry| entry.canonical && entry.encoding == self)
.map(|entry| entry.number)
}
pub fn for_bom(buffer: &[u8]) -> Option<(&'static Encoding, usize)> {
if buffer.starts_with(&[0xEF, 0xBB, 0xBF]) {
Some((UTF_8, 3))
} else if buffer.starts_with(&[0xFE, 0xFF]) {
Some((UTF_16BE, 2))
} else if buffer.starts_with(&[0xFF, 0xFE]) {
Some((UTF_16LE, 2))
} else {
None
}
}
pub fn output_encoding(&'static self) -> &'static Encoding {
match self.variant {
VariantEncoding::Replacement | VariantEncoding::Utf16Be | VariantEncoding::Utf16Le => {
UTF_8
}
_ => self,
}
}
pub fn is_single_byte(&self) -> bool {
self.variant.is_single_byte()
}
pub fn is_ascii_compatible(&self) -> bool {
self.variant.is_ascii_compatible()
}
pub fn new_decoder(&'static self) -> Decoder {
Decoder::new(self, DecodeOptions::new())
}
pub fn new_decoder_with(&'static self, options: DecodeOptions) -> Decoder {
Decoder::new(self, options)
}
pub fn new_encoder(&'static self) -> Encoder {
Encoder::new(self.output_encoding(), EncodeOptions::new())
}
pub fn new_encoder_with(&'static self, options: EncodeOptions) -> Encoder {
Encoder::new(self.output_encoding(), options)
}
#[cfg(feature = "alloc")]
pub fn decode<'a>(&'static self, bytes: &'a [u8]) -> (Cow<'a, str>, &'static Encoding, Tally) {
self.decode_with(bytes, DecodeOptions::new())
}
#[cfg(feature = "alloc")]
pub fn decode_with<'a>(
&'static self,
bytes: &'a [u8],
options: DecodeOptions,
) -> (Cow<'a, str>, &'static Encoding, Tally) {
match self.try_decode(bytes, options) {
Ok((text, encoding, tally)) => (text, encoding, tally),
Err((text, encoding, _)) => (Cow::Owned(text), encoding, Tally { errors: 1 }),
}
}
#[cfg(feature = "alloc")]
#[allow(clippy::type_complexity)]
pub fn try_decode<'a>(
&'static self,
bytes: &'a [u8],
options: DecodeOptions,
) -> Result<(Cow<'a, str>, &'static Encoding, Tally), (String, &'static Encoding, MalformedError)>
{
let (encoding, skip) = match options.bom {
Bom::Sniff => Encoding::for_bom(bytes).unwrap_or((self, 0)),
Bom::Remove => match self.bom() {
Some(bom) if bytes.starts_with(bom) => (self, bom.len()),
_ => (self, 0),
},
Bom::Ignore => (self, 0),
};
let bytes = &bytes[skip..];
if let Some(borrowed) = encoding.borrow_as_str(bytes) {
return Ok((Cow::Borrowed(borrowed), encoding, Tally::default()));
}
let mut text = String::with_capacity(bytes.len());
let mut decoder = encoding.new_decoder_with(DecodeOptions {
bom: Bom::Ignore,
..options
});
match decoder.decode_to_string(bytes, &mut text, true) {
Ok(()) => Ok((Cow::Owned(text), encoding, decoder.tally())),
Err(e) => Err((text, encoding, e)),
}
}
#[cfg(feature = "alloc")]
#[allow(clippy::type_complexity)]
pub fn encode<'a>(
&'static self,
string: &'a str,
) -> Result<(Cow<'a, [u8]>, &'static Encoding, Tally), UnmappableError> {
self.encode_with(string, EncodeOptions::new())
}
#[cfg(feature = "alloc")]
#[allow(clippy::type_complexity)]
pub fn encode_with<'a>(
&'static self,
string: &'a str,
options: EncodeOptions,
) -> Result<(Cow<'a, [u8]>, &'static Encoding, Tally), UnmappableError> {
let output = self.output_encoding();
if options.unmappable != Unmappable::Html
&& options.unmappable != Unmappable::JsonEscape
&& output.borrow_as_str(string.as_bytes()).is_some()
{
return Ok((Cow::Borrowed(string.as_bytes()), output, Tally::default()));
}
let mut bytes = Vec::with_capacity(string.len());
let mut encoder = output.new_encoder_with(options);
encoder.encode_from_str(string, &mut bytes, true)?;
Ok((Cow::Owned(bytes), output, encoder.tally()))
}
#[cfg(feature = "alloc")]
pub fn encode_html_form<'a>(
&'static self,
string: &'a str,
) -> (Cow<'a, [u8]>, &'static Encoding, Tally) {
let output = self.output_encoding();
if output.borrow_as_str(string.as_bytes()).is_some() {
return (Cow::Borrowed(string.as_bytes()), output, Tally::default());
}
let mut bytes = Vec::with_capacity(string.len());
let mut encoder = Encoder::new_html_form(output);
encoder
.encode_from_str(string, &mut bytes, true)
.expect("numeric character references never fail");
(Cow::Owned(bytes), output, encoder.tally())
}
#[cfg(feature = "alloc")]
fn borrow_as_str<'a>(&self, bytes: &'a [u8]) -> Option<&'a str> {
let decodes_verbatim = match self.variant {
VariantEncoding::Utf8 => true,
_ if self.variant.is_ascii_compatible() => ascii::is_ascii(bytes),
_ => false,
};
if decodes_verbatim {
core::str::from_utf8(bytes).ok()
} else {
None
}
}
}
fn trim_ascii_whitespace(label: &[u8]) -> &[u8] {
let is_space = |b: u8| matches!(b, 0x09 | 0x0A | 0x0C | 0x0D | 0x20);
let start = label.iter().position(|&b| !is_space(b));
match start {
None => &[],
Some(start) => {
let end = label.iter().rposition(|&b| !is_space(b)).unwrap_or(start);
&label[start..=end]
}
}
}
impl PartialEq for Encoding {
fn eq(&self, other: &Encoding) -> bool {
core::ptr::eq(self, other) || self.name == other.name
}
}
impl Eq for Encoding {}
impl core::hash::Hash for Encoding {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl core::fmt::Debug for Encoding {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Encoding({})", self.name)
}
}
impl core::fmt::Display for Encoding {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.name)
}
}