qubit_codec_text/charset/unicode_bom.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8use crate::{
9 ByteOrder,
10 Charset,
11};
12
13/// Unicode byte order marks supported by this crate.
14///
15/// `detect` recognizes BOMs only from the bytes supplied to the call. Streaming
16/// callers should buffer up to four bytes, or read until EOF, before deciding
17/// that no longer BOM can be present.
18///
19/// # Examples
20///
21/// ```rust
22/// use qubit_codec_text::{
23/// ByteOrder,
24/// Charset,
25/// UnicodeBom,
26/// };
27///
28/// let bom = UnicodeBom::detect(&[0xff, 0xfe, 0x00, 0x00]);
29/// assert_eq!(Some(UnicodeBom::Utf32LittleEndian), bom);
30///
31/// let bom = bom.expect("UTF-32LE BOM");
32/// assert_eq!(Charset::UTF_32LE, bom.charset());
33/// assert_eq!(Some(ByteOrder::LittleEndian), bom.byte_order());
34/// assert_eq!(4, bom.byte_len());
35/// ```
36#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
37pub enum UnicodeBom {
38 /// UTF-8 byte order mark.
39 Utf8,
40
41 /// UTF-16 big-endian byte order mark.
42 Utf16BigEndian,
43
44 /// UTF-16 little-endian byte order mark.
45 Utf16LittleEndian,
46
47 /// UTF-32 big-endian byte order mark.
48 Utf32BigEndian,
49
50 /// UTF-32 little-endian byte order mark.
51 Utf32LittleEndian,
52}
53
54impl UnicodeBom {
55 /// Detects a Unicode byte order mark at the beginning of `bytes`.
56 ///
57 /// # Parameters
58 ///
59 /// - `bytes`: The byte buffer to inspect.
60 ///
61 /// # Returns
62 ///
63 /// Returns the detected BOM, or `None` if no supported BOM prefix is
64 /// present.
65 ///
66 /// UTF-32 BOMs are checked before UTF-16 BOMs so that overlapping prefixes
67 /// such as `FF FE 00 00` are classified as UTF-32 little-endian when all
68 /// four bytes are available.
69 pub fn detect(bytes: &[u8]) -> Option<Self> {
70 if bytes.starts_with(&[0x00, 0x00, 0xfe, 0xff]) {
71 Some(Self::Utf32BigEndian)
72 } else if bytes.starts_with(&[0xff, 0xfe, 0x00, 0x00]) {
73 Some(Self::Utf32LittleEndian)
74 } else if bytes.starts_with(&[0xef, 0xbb, 0xbf]) {
75 Some(Self::Utf8)
76 } else if bytes.starts_with(&[0xfe, 0xff]) {
77 Some(Self::Utf16BigEndian)
78 } else if bytes.starts_with(&[0xff, 0xfe]) {
79 Some(Self::Utf16LittleEndian)
80 } else {
81 None
82 }
83 }
84
85 /// Returns the bytes that represent this BOM.
86 ///
87 /// # Returns
88 ///
89 /// Returns a static byte slice containing the BOM bytes.
90 #[inline(always)]
91 pub const fn bytes(self) -> &'static [u8] {
92 match self {
93 Self::Utf8 => &[0xef, 0xbb, 0xbf],
94 Self::Utf16BigEndian => &[0xfe, 0xff],
95 Self::Utf16LittleEndian => &[0xff, 0xfe],
96 Self::Utf32BigEndian => &[0x00, 0x00, 0xfe, 0xff],
97 Self::Utf32LittleEndian => &[0xff, 0xfe, 0x00, 0x00],
98 }
99 }
100
101 /// Returns the byte length of this BOM.
102 ///
103 /// # Returns
104 ///
105 /// Returns the number of bytes in this BOM.
106 #[inline(always)]
107 pub const fn byte_len(self) -> usize {
108 match self {
109 Self::Utf8 => 3,
110 Self::Utf16BigEndian | Self::Utf16LittleEndian => 2,
111 Self::Utf32BigEndian | Self::Utf32LittleEndian => 4,
112 }
113 }
114
115 /// Returns the charset indicated by this BOM.
116 ///
117 /// # Returns
118 ///
119 /// Returns the corresponding [`Charset`], including fixed byte order for
120 /// UTF-16 and UTF-32 BOMs.
121 #[inline(always)]
122 pub const fn charset(self) -> Charset {
123 match self {
124 Self::Utf8 => Charset::UTF_8,
125 Self::Utf16BigEndian => Charset::UTF_16BE,
126 Self::Utf16LittleEndian => Charset::UTF_16LE,
127 Self::Utf32BigEndian => Charset::UTF_32BE,
128 Self::Utf32LittleEndian => Charset::UTF_32LE,
129 }
130 }
131
132 /// Returns the byte order indicated by this BOM when applicable.
133 ///
134 /// # Returns
135 ///
136 /// Returns `Some(ByteOrder)` for UTF-16 and UTF-32 BOMs. Returns `None` for
137 /// UTF-8 because byte order does not apply.
138 #[inline(always)]
139 pub const fn byte_order(self) -> Option<ByteOrder> {
140 match self {
141 Self::Utf8 => None,
142 Self::Utf16BigEndian | Self::Utf32BigEndian => {
143 Some(ByteOrder::BigEndian)
144 }
145 Self::Utf16LittleEndian | Self::Utf32LittleEndian => {
146 Some(ByteOrder::LittleEndian)
147 }
148 }
149 }
150}