charset_norm/lib.rs
1//! Universal character encoding detector.
2//!
3//! `charset-norm` is a Rust implementation of
4//! [charset_normalizer](https://github.com/jawah/charset_normalizer). Give it
5//! bytes of unknown origin and it tells you which encodings they were
6//! plausibly written in, which language the text is in, and how confident it
7//! is. It knows 98 code pages, from UTF-8 and the Windows/ISO-8859 families to
8//! the CJK and ISO-2022 encodings.
9//!
10//! Decoding reproduces `CPython`'s codecs byte for byte, so verdicts agree with
11//! the Python package.
12//!
13//! # Quick start
14//!
15//! ```
16//! let results = charset_norm::from_bytes("Ça va très bien, merci.".as_bytes());
17//! let best = results.best().expect("text payload");
18//! assert_eq!(best.encoding(), "utf_8");
19//! assert_eq!(best.decoded().unwrap(), "Ça va très bien, merci.");
20//! ```
21//!
22//! [`from_bytes`] returns [`CharsetMatches`], the plausible encodings best
23//! first. It is empty when nothing fits, which usually means the payload is
24//! binary. Each [`CharsetMatch`] exposes the encoding, the decoded text, the
25//! detected languages and the scores behind the verdict:
26//!
27//! ```
28//! use charset_norm::codecs;
29//!
30//! let text = "Всеки човек има право на образование. Образованието трябва да бъде безплатно.";
31//! let payload = codecs::encode(text, "cp1251").unwrap();
32//!
33//! let results = charset_norm::from_bytes(&payload);
34//! let best = results.best().unwrap();
35//! assert_eq!(best.encoding(), "cp1251");
36//! assert!(best.chaos() < 0.1);
37//!
38//! // Languages come ranked by coherence. On one short sentence, Cyrillic
39//! // languages are hard to tell apart, so look at the whole list.
40//! assert!(best.languages().contains(&"Bulgarian"));
41//!
42//! // Transcode to UTF-8 (in-document charset declarations are rewritten too).
43//! assert_eq!(best.output("utf_8").unwrap(), text.as_bytes());
44//! ```
45//!
46//! Files are read with [`from_path`]; [`is_binary`] answers the text vs.
47//! binary question alone.
48//!
49//! # Tuning detection
50//!
51//! [`from_bytes_with`] takes [`DetectionOptions`] to restrict the candidate
52//! encodings, change how much of the payload is sampled, or set how noisy a
53//! decoding may look before it is rejected. The defaults match the Python
54//! package.
55//!
56//! ```
57//! use charset_norm::{DetectionOptions, NoLogger, codecs, from_bytes_with};
58//!
59//! let payload = codecs::encode("Příliš žluťoučký kůň úpěl ďábelské ódy.", "cp1250").unwrap();
60//! let options = DetectionOptions {
61//! cp_isolation: vec!["cp1250".into(), "cp1252".into()],
62//! ..DetectionOptions::default()
63//! };
64//! let results = from_bytes_with(&payload, &options, &NoLogger);
65//! assert_eq!(results.best().unwrap().encoding(), "cp1250");
66//! ```
67//!
68//! # How it works
69//!
70//! Candidates are tried in order: the encoding announced by a byte order
71//! mark, the one declared in the document (`<meta charset>`, `# coding:`),
72//! ASCII, UTF-8, then every other code page. For each one, the detector:
73//!
74//! 1. Skips it when the payload cannot be decoded with it, or when a
75//! near-identical code page (see [`encoding::is_cp_similar`]) was already
76//! rejected.
77//! 2. Decodes [`steps`](DetectionOptions::steps) chunks of
78//! [`chunk_size`](DetectionOptions::chunk_size) bytes spread over the
79//! payload.
80//! 3. Scores each chunk's **chaos** with [`mess::mess_ratio`]: suspicious
81//! character successions, unexpected accents, unprintable characters and
82//! so on. Candidates whose mean chaos reaches
83//! [`threshold`](DetectionOptions::threshold) are dropped.
84//! 4. Scores the survivors' **coherence** with
85//! [`coherence::coherence_ratio`]: how well letter frequencies match known
86//! languages.
87//! 5. Ranks the matches: lower chaos first, then higher coherence. Encodings
88//! that produce the very same text become
89//! [submatches](CharsetMatch::submatches) of one another.
90//!
91//! Detection stops early once a preferred candidate (declared, ASCII or
92//! UTF-8) decodes cleanly. When no candidate survives, ASCII, UTF-8 or a
93//! declared encoding may still
94//! be returned as a fallback (see
95//! [`enable_fallback`](DetectionOptions::enable_fallback)).
96//!
97//! # Modules
98//!
99//! The top-level functions cover most needs. The building blocks are public
100//! for tools that want them:
101//!
102//! | Module | Contents |
103//! |--------|----------|
104//! | [`codecs`] | `CPython`-compatible decoders and encoders |
105//! | [`encoding`] | Code page names and aliases, BOMs, declared charsets, code page languages |
106//! | [`mess`] | Chaos (noise) scoring of decoded text |
107//! | [`coherence`] | Language detection from letter frequencies |
108//! | [`unicode`] | Character properties (`unicodedata` semantics) |
109//! | [`chunks`] | Sampling of decoded chunks, as the detector does it |
110//! | [`log`] | Diagnostics emitted while detecting |
111//!
112//! # Encoding names
113//!
114//! Encodings are reported by their canonical `CPython` names: `"utf_8"`,
115//! `"cp1252"`, `"shift_jis"`, `"iso8859_5"`. Wherever a name is accepted,
116//! aliases such as `"UTF-8"`, `"windows-1252"` or `"latin-1"` work too; see
117//! [`encoding::iana_name`] and [`encoding::supported_encodings`].
118//!
119//! # Diagnostics
120//!
121//! Detection reports what it tries and why it rejects candidates through a
122//! [`Logger`]. [`NoLogger`] discards everything; implement the trait to
123//! collect messages yourself, or enable the `log` feature and pass
124//! `log::LogCrate` to forward them to the [`log`](https://docs.rs/log)
125//! crate.
126//!
127//! # Feature flags
128//!
129//! - `log` (off by default): adds `log::LogCrate`, a [`Logger`] that forwards
130//! diagnostics to the `log` crate under the `charset_norm` target.
131//!
132//! # Minimum supported Rust version
133//!
134//! Rust 1.98.
135
136#![cfg_attr(docsrs, feature(doc_cfg))]
137#![forbid(unsafe_code)]
138#![warn(missing_docs)]
139
140pub mod chunks;
141pub mod codecs;
142pub mod coherence;
143mod detect;
144pub mod encoding;
145mod error;
146pub mod log;
147mod matches;
148pub mod mess;
149mod pyfloat;
150mod stackfmt;
151mod tables;
152pub mod unicode;
153
154pub use detect::{DetectionOptions, detect, from_bytes, from_bytes_with, from_path, is_binary};
155pub use error::Error;
156pub use log::{Level, Logger, NoLogger};
157pub use matches::{CharsetMatch, CharsetMatches, OutputError, sort_by_rank};
158
159/// Payloads shorter than this many bytes are considered too small for
160/// reliable detection.
161///
162/// They are still analysed, but as a single chunk, and verdicts on so little
163/// text are best treated as guesses.
164pub const TOO_SMALL_SEQUENCE: usize = 32;
165
166/// Payloads of at least this many bytes are sampled rather than fully decoded.
167///
168/// For such payloads, [`CharsetMatch::decoded`] decodes on demand instead of
169/// keeping the text detection produced, and duplicate matches are no longer
170/// folded into submatches.
171pub const TOO_BIG_SEQUENCE: usize = 10_000_000;