Skip to main content

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}