1use std::borrow::Cow;
5use std::hash::{DefaultHasher, Hasher};
6use std::sync::{Arc, OnceLock};
7
8use crate::codecs::{self, DecodeError, Errors};
9use crate::encoding::{self, encoding_indication};
10use crate::{Error, TOO_BIG_SEQUENCE, pyfloat, unicode};
11
12#[derive(Clone, Debug)]
44pub struct CharsetMatch {
45 payload: Arc<[u8]>,
46 encoding: Cow<'static, str>,
47 chaos: f64,
48 has_sig_or_bom: bool,
49 languages: Arc<[(Cow<'static, str>, f64)]>,
50 decoded: OnceLock<Arc<String>>,
51 preemptive_declaration: Option<Cow<'static, str>>,
52 submatches: Vec<CharsetMatch>,
53 fingerprint: OnceLock<u64>,
54 char_count: OnceLock<usize>,
55 alphabets: OnceLock<Vec<&'static str>>,
56}
57
58impl CharsetMatch {
59 pub fn new(
65 payload: impl Into<Arc<[u8]>>,
66 encoding: impl Into<Cow<'static, str>>,
67 chaos: f64,
68 has_sig_or_bom: bool,
69 languages: impl Into<Arc<[(Cow<'static, str>, f64)]>>,
70 decoded: Option<String>,
71 preemptive_declaration: Option<Cow<'static, str>>,
72 ) -> Self {
73 let decoded_cell = OnceLock::new();
74 if let Some(text) = decoded {
75 let _ = decoded_cell.set(Arc::new(text));
76 }
77 Self {
78 payload: payload.into(),
79 encoding: encoding.into(),
80 chaos,
81 has_sig_or_bom,
82 languages: languages.into(),
83 decoded: decoded_cell,
84 preemptive_declaration,
85 submatches: Vec::new(),
86 fingerprint: OnceLock::new(),
87 char_count: OnceLock::new(),
88 alphabets: OnceLock::new(),
89 }
90 }
91
92 pub fn encoding(&self) -> &str {
94 &self.encoding
95 }
96
97 pub fn encoding_aliases(&self) -> Vec<&'static str> {
99 encoding::aliases(&self.encoding)
100 }
101
102 pub fn chaos(&self) -> f64 {
104 self.chaos
105 }
106
107 pub fn percent_chaos(&self) -> f64 {
109 pyfloat::round(self.chaos * 100.0, 3)
110 }
111
112 pub fn coherence(&self) -> f64 {
114 self.languages.first().map_or(0.0, |value| value.1)
115 }
116
117 pub fn percent_coherence(&self) -> f64 {
119 pyfloat::round(self.coherence() * 100.0, 3)
120 }
121
122 pub fn has_sig_or_bom(&self) -> bool {
124 self.has_sig_or_bom
125 }
126
127 pub fn language_ratios(&self) -> &[(Cow<'static, str>, f64)] {
129 &self.languages
130 }
131
132 pub fn languages(&self) -> Vec<&str> {
134 self.languages.iter().map(|item| &*item.0).collect()
135 }
136
137 pub fn language(&self) -> &str {
139 if let Some((language, _)) = self.languages.first() {
140 return language;
141 }
142 if self.could_be_from("ascii") {
143 return "English";
144 }
145 let languages = encoding::target_languages(&self.encoding);
146 match languages.first() {
147 Some(first) if !languages.contains(&"Latin Based") => first,
148 _ => "Unknown",
149 }
150 }
151
152 pub fn raw(&self) -> &[u8] {
154 &self.payload
155 }
156
157 pub fn payload(&self) -> &Arc<[u8]> {
159 &self.payload
160 }
161
162 pub fn preemptive_declaration(&self) -> Option<&str> {
164 self.preemptive_declaration.as_deref()
165 }
166
167 pub fn decoded(&self) -> Result<&str, DecodeError> {
174 if let Some(text) = self.decoded.get() {
175 return Ok(text);
176 }
177 let mut text = codecs::decode(&self.payload, &self.encoding, Errors::Strict)?;
178 if self.has_sig_or_bom && self.encoding == "utf_7" && text.starts_with('\u{feff}') {
179 text.remove(0);
180 }
181 Ok(self.decoded.get_or_init(|| Arc::new(text)))
182 }
183
184 pub fn cached_decoded(&self) -> Option<&str> {
186 self.decoded.get().map(|text| text.as_str())
187 }
188
189 pub fn set_decoded(&mut self, text: Option<String>) {
191 self.decoded = OnceLock::new();
192 if let Some(text) = text {
193 let _ = self.decoded.set(Arc::new(text));
194 }
195 }
196
197 pub fn fingerprint(&self) -> Result<u64, DecodeError> {
203 if let Some(value) = self.fingerprint.get() {
204 return Ok(*value);
205 }
206 let mut hasher = DefaultHasher::new();
207 hasher.write(self.decoded()?.as_bytes());
208 Ok(*self.fingerprint.get_or_init(|| hasher.finish()))
209 }
210
211 pub fn multi_byte_usage(&self) -> Result<f64, DecodeError> {
217 if self.payload.is_empty() {
218 return Ok(0.0);
219 }
220 let count = if let Some(count) = self.char_count.get() {
221 *count
222 } else {
223 let text = self.decoded()?;
224 *self.char_count.get_or_init(|| text.chars().count())
225 };
226 Ok(1.0 - count as f64 / self.payload.len() as f64)
227 }
228
229 pub fn alphabets(&self) -> Result<&[&'static str], DecodeError> {
235 if let Some(ranges) = self.alphabets.get() {
236 return Ok(ranges);
237 }
238 let mut seen = vec![false; unicode::ranges().len()];
239 for character in self.decoded()?.chars() {
240 let index = unicode::props(character).range;
241 if index != unicode::NO_RANGE {
242 seen[index as usize] = true;
243 }
244 }
245 let mut ranges: Vec<&'static str> = unicode::ranges()
246 .iter()
247 .zip(seen)
248 .filter_map(|(range, seen)| seen.then_some(range.name))
249 .collect();
250 ranges.sort_unstable();
251 Ok(self.alphabets.get_or_init(|| ranges))
252 }
253
254 pub fn submatches(&self) -> &[CharsetMatch] {
262 &self.submatches
263 }
264
265 pub fn take_submatches(&mut self) -> Vec<CharsetMatch> {
267 std::mem::take(&mut self.submatches)
268 }
269
270 pub fn has_submatch(&self) -> bool {
272 !self.submatches.is_empty()
273 }
274
275 fn could_be_from(&self, encoding: &str) -> bool {
277 self.encoding == encoding || self.submatches.iter().any(|item| item.encoding == encoding)
278 }
279
280 pub fn could_be_from_charset(&self) -> Vec<&str> {
282 std::iter::once(&*self.encoding)
283 .chain(self.submatches.iter().map(|item| &*item.encoding))
284 .collect()
285 }
286
287 pub fn same_as(&self, other: &CharsetMatch) -> Result<bool, DecodeError> {
293 Ok(self.encoding == other.encoding && self.fingerprint()? == other.fingerprint()?)
294 }
295
296 pub fn add_submatch(&mut self, mut other: CharsetMatch) -> Result<bool, DecodeError> {
303 if self.same_as(&other)? {
304 return Ok(false);
305 }
306 other.set_decoded(None);
307 self.submatches.push(other);
308 Ok(true)
309 }
310
311 #[expect(
318 clippy::float_cmp,
319 reason = "only identical chaos values denote the same result"
320 )]
321 pub fn is_duplicate_of(&self, existing: &CharsetMatch) -> Result<bool, DecodeError> {
322 if self.payload.len() >= TOO_BIG_SEQUENCE || existing.chaos != self.chaos {
323 return Ok(false);
324 }
325 Ok(existing.decoded()? == self.decoded()?)
327 }
328
329 pub fn ranks_before(&self, other: &CharsetMatch) -> bool {
332 let chaos_difference = (self.chaos - other.chaos).abs();
333 let coherence_difference = (self.coherence() - other.coherence()).abs();
334 if chaos_difference < 0.005 && coherence_difference > 0.02 {
335 return self.coherence() > other.coherence();
336 }
337 if chaos_difference < 0.005 && coherence_difference <= 0.02 {
338 if self.payload.len() >= TOO_BIG_SEQUENCE {
339 return self.chaos < other.chaos;
340 }
341 return self.multi_byte_usage().unwrap_or(0.0)
342 > other.multi_byte_usage().unwrap_or(0.0);
343 }
344 self.chaos < other.chaos
345 }
346
347 pub fn output_text(&self, encoding: &str) -> Result<String, OutputError> {
355 let mut decoded = self.decoded().map_err(OutputError::Decode)?.to_owned();
356 let declared_non_utf8 = self.preemptive_declaration.as_ref().is_some_and(|value| {
357 !matches!(value.to_lowercase().as_str(), "utf-8" | "utf8" | "utf_8")
358 });
359 if declared_non_utf8 {
360 let prefix_end = decoded
361 .char_indices()
362 .nth(8192)
363 .map_or(decoded.len(), |(offset, _)| offset);
364 let found = encoding_indication()
365 .captures(&decoded[..prefix_end])
366 .and_then(|captures| Some((captures.get(0)?.range(), captures.get(1)?.range())));
367 if let Some((full, group)) = found {
368 let replacement = encoding::iana_name(encoding, true)
369 .map_err(OutputError::Encoding)?
370 .replace('_', "-");
371 let patched = decoded[full.clone()].replace(&decoded[group], &replacement);
372 decoded = format!(
373 "{}{}{}",
374 &decoded[..full.start],
375 patched,
376 &decoded[full.end..]
377 );
378 }
379 }
380 Ok(decoded)
381 }
382
383 pub fn output(&self, encoding: &str) -> Result<Vec<u8>, OutputError> {
397 let text = self.output_text(encoding)?;
398 codecs::encode(&text, encoding).ok_or_else(|| OutputError::Unsupported(encoding.to_owned()))
399 }
400}
401
402#[derive(Clone, Debug, PartialEq, Eq)]
412#[non_exhaustive]
413pub enum OutputError {
414 Decode(DecodeError),
416 Encoding(Error),
418 Unsupported(String),
420}
421
422impl std::fmt::Display for OutputError {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 match self {
425 OutputError::Decode(error) => error.fmt(f),
426 OutputError::Encoding(error) => error.fmt(f),
427 OutputError::Unsupported(name) => write!(f, "no native encoder for '{name}'"),
428 }
429 }
430}
431
432impl std::error::Error for OutputError {}
433
434pub fn sort_by_rank<T>(items: &mut [T], mut lt: impl FnMut(&T, &T) -> bool) {
439 let n = items.len();
440 if n < 2 {
441 return;
442 }
443 let mut run = 1;
445 if n < 64 {
446 run = 2;
447 if lt(&items[1], &items[0]) {
448 while run < n && lt(&items[run], &items[run - 1]) {
449 run += 1;
450 }
451 items[..run].reverse();
452 } else {
453 while run < n && !lt(&items[run], &items[run - 1]) {
454 run += 1;
455 }
456 }
457 }
458 for start in run..n {
459 let mut left = 0;
460 let mut right = start;
461 while left < right {
462 let middle = left + ((right - left) >> 1);
463 if lt(&items[start], &items[middle]) {
464 right = middle;
465 } else {
466 left = middle + 1;
467 }
468 }
469 items[left..=start].rotate_right(1);
470 }
471}
472
473#[derive(Clone, Debug, Default)]
496pub struct CharsetMatches {
497 results: Vec<CharsetMatch>,
498}
499
500impl CharsetMatches {
501 pub fn new(mut results: Vec<CharsetMatch>) -> Self {
503 sort_by_rank(&mut results, CharsetMatch::ranks_before);
504 Self { results }
505 }
506
507 pub(crate) fn from_sorted(results: Vec<CharsetMatch>) -> Self {
509 Self { results }
510 }
511
512 pub fn push(&mut self, item: CharsetMatch) {
515 for existing in &mut self.results {
516 if item.is_duplicate_of(existing).unwrap_or(false) {
517 let mut item = item;
518 item.set_decoded(None);
519 existing.submatches.push(item);
520 return;
521 }
522 }
523 self.results.push(item);
524 sort_by_rank(&mut self.results, CharsetMatch::ranks_before);
525 }
526
527 #[must_use]
529 pub fn best(&self) -> Option<&CharsetMatch> {
530 self.results.first()
531 }
532
533 #[must_use]
535 pub fn len(&self) -> usize {
536 self.results.len()
537 }
538
539 #[must_use]
541 pub fn is_empty(&self) -> bool {
542 self.results.is_empty()
543 }
544
545 #[must_use]
547 pub fn get(&self, index: usize) -> Option<&CharsetMatch> {
548 self.results.get(index)
549 }
550
551 #[must_use]
553 pub fn get_by_encoding(&self, encoding: &str) -> Option<&CharsetMatch> {
554 let name = encoding::iana_name(encoding, false).ok()?;
555 self.results.iter().find(|item| item.could_be_from(&name))
556 }
557
558 pub fn iter(&self) -> std::slice::Iter<'_, CharsetMatch> {
560 self.results.iter()
561 }
562
563 #[must_use]
565 pub fn into_vec(self) -> Vec<CharsetMatch> {
566 self.results
567 }
568}
569
570impl IntoIterator for CharsetMatches {
571 type Item = CharsetMatch;
572 type IntoIter = std::vec::IntoIter<CharsetMatch>;
573
574 fn into_iter(self) -> Self::IntoIter {
575 self.results.into_iter()
576 }
577}
578
579impl<'a> IntoIterator for &'a CharsetMatches {
580 type Item = &'a CharsetMatch;
581 type IntoIter = std::slice::Iter<'a, CharsetMatch>;
582
583 fn into_iter(self) -> Self::IntoIter {
584 self.results.iter()
585 }
586}
587
588pub(crate) struct PendingMatches {
591 results: Vec<CharsetMatch>,
592 sorted: bool,
593}
594
595impl PendingMatches {
596 pub(crate) fn new() -> Self {
597 Self {
598 results: Vec::new(),
599 sorted: true,
600 }
601 }
602
603 pub(crate) fn len(&self) -> usize {
604 self.results.len()
605 }
606
607 #[cfg_attr(feature = "hotpath", hotpath::measure)]
608 pub(crate) fn push(&mut self, item: CharsetMatch) {
609 for existing in &mut self.results {
610 if item.is_duplicate_of(existing).unwrap_or(false) {
611 let mut item = item;
612 item.set_decoded(None);
613 existing.submatches.push(item);
614 return;
615 }
616 }
617 self.results.push(item);
618 self.sorted = false;
619 }
620
621 fn sort(&mut self) {
622 if !self.sorted {
623 sort_by_rank(&mut self.results, CharsetMatch::ranks_before);
624 self.sorted = true;
625 }
626 }
627
628 pub(crate) fn best(&mut self) -> Option<&CharsetMatch> {
629 self.sort();
630 self.results.first()
631 }
632
633 pub(crate) fn take_best(mut self) -> Option<CharsetMatch> {
634 self.sort();
635 self.results.into_iter().next()
636 }
637
638 pub(crate) fn into_matches(mut self) -> CharsetMatches {
639 self.sort();
640 CharsetMatches::from_sorted(self.results)
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647
648 fn sample(encoding: &'static str, chaos: f64, text: &str) -> CharsetMatch {
649 CharsetMatch::new(
650 text.as_bytes().to_vec(),
651 encoding,
652 chaos,
653 false,
654 Vec::new(),
655 None,
656 None,
657 )
658 }
659
660 #[test]
661 fn ranking_and_merging() {
662 let mut matches = CharsetMatches::default();
663 matches.push(sample("utf_8", 0.3, "hello"));
664 matches.push(sample("ascii", 0.1, "hello"));
665 matches.push(sample("latin_1", 0.1, "hello"));
666 assert_eq!(matches.len(), 2);
667 let best = matches.best().unwrap();
668 assert_eq!(best.encoding(), "ascii");
669 assert_eq!(best.could_be_from_charset(), ["ascii", "latin_1"]);
670 assert_eq!(best.language(), "English");
671 assert!(matches.get_by_encoding("latin-1").is_some());
672 }
673
674 #[test]
675 fn short_sort_matches_cpython() {
676 let mut values = vec![3, 1, 2, 5, 4];
677 sort_by_rank(&mut values, |a, b| a < b);
678 assert_eq!(values, [1, 2, 3, 4, 5]);
679 let mut long: Vec<u32> = (0..100).rev().collect();
680 sort_by_rank(&mut long, |a, b| a < b);
681 assert!(long.windows(2).all(|pair| pair[0] <= pair[1]));
682 }
683}