Skip to main content

qrcode_core/
mode.rs

1//! Type-level QR encoding modes.
2//!
3//! This module complements the runtime [`Mode`] enum with marker
4//! types that can be used in generic APIs such as
5//! [`Bits::push_mode_data`](crate::bits::Bits::push_mode_data).
6
7use crate::Mode;
8
9/// A type-level QR encoding mode.
10pub trait EncodingMode {
11    /// Runtime mode represented by this marker type.
12    const MODE: Mode;
13
14    /// Returns whether `data` is valid for this mode.
15    fn validate(data: &[u8]) -> bool;
16
17    /// Returns the first invalid byte for this mode, if any.
18    fn invalid_character(data: &[u8]) -> Option<(usize, u8)>;
19
20    /// Returns the number of logical characters represented by `data`.
21    ///
22    /// This differs from `data.len()` for Kanji mode, where every character is
23    /// represented by two Shift-JIS bytes.
24    fn character_count(data: &[u8]) -> usize {
25        data.len()
26    }
27
28    /// Approximate payload bits per logical character for this mode.
29    fn bits_per_character() -> f64;
30}
31
32/// Type-level numeric mode marker.
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
34pub struct NumericMode;
35
36impl EncodingMode for NumericMode {
37    const MODE: Mode = Mode::Numeric;
38
39    fn validate(data: &[u8]) -> bool {
40        data.iter().all(u8::is_ascii_digit)
41    }
42
43    fn invalid_character(data: &[u8]) -> Option<(usize, u8)> {
44        data.iter().copied().enumerate().find(|(_, byte)| !byte.is_ascii_digit())
45    }
46
47    fn bits_per_character() -> f64 {
48        10.0 / 3.0
49    }
50}
51
52/// Type-level alphanumeric mode marker.
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54pub struct AlphanumericMode;
55
56impl EncodingMode for AlphanumericMode {
57    const MODE: Mode = Mode::Alphanumeric;
58
59    fn validate(data: &[u8]) -> bool {
60        data.iter().copied().all(is_alphanumeric)
61    }
62
63    fn invalid_character(data: &[u8]) -> Option<(usize, u8)> {
64        data.iter().copied().enumerate().find(|(_, byte)| !is_alphanumeric(*byte))
65    }
66
67    fn bits_per_character() -> f64 {
68        11.0 / 2.0
69    }
70}
71
72/// Type-level byte mode marker.
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
74pub struct ByteMode;
75
76impl EncodingMode for ByteMode {
77    const MODE: Mode = Mode::Byte;
78
79    fn validate(_data: &[u8]) -> bool {
80        true
81    }
82
83    fn invalid_character(_data: &[u8]) -> Option<(usize, u8)> {
84        None
85    }
86
87    fn bits_per_character() -> f64 {
88        8.0
89    }
90}
91
92/// Type-level Shift-JIS Kanji mode marker.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub struct KanjiMode;
95
96impl EncodingMode for KanjiMode {
97    const MODE: Mode = Mode::Kanji;
98
99    fn validate(data: &[u8]) -> bool {
100        Self::invalid_character(data).is_none()
101    }
102
103    fn invalid_character(data: &[u8]) -> Option<(usize, u8)> {
104        for (index, chunk) in data.chunks(2).enumerate() {
105            if chunk.len() != 2 {
106                return Some((index * 2, chunk[0]));
107            }
108            let hi = chunk[0];
109            let lo = chunk[1];
110            let codepoint = u16::from(hi) << 8 | u16::from(lo);
111            if !is_kanji_codepoint(codepoint) {
112                return Some((index * 2, hi));
113            }
114        }
115        None
116    }
117
118    fn character_count(data: &[u8]) -> usize {
119        data.len() / 2
120    }
121
122    fn bits_per_character() -> f64 {
123        13.0
124    }
125}
126
127fn is_alphanumeric(byte: u8) -> bool {
128    matches!(
129        byte,
130        b'0'..=b'9' | b'A'..=b'Z' | b' ' | b'$' | b'%' | b'*' | b'+' | b'-' | b'.' | b'/' | b':'
131    )
132}
133
134fn is_kanji_codepoint(codepoint: u16) -> bool {
135    (0x8140..=0x9ffc).contains(&codepoint) || (0xe040..=0xebbf).contains(&codepoint)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::{AlphanumericMode, ByteMode, EncodingMode, KanjiMode, NumericMode};
141    use crate::Mode;
142
143    #[test]
144    fn numeric_mode_validates_ascii_digits() {
145        assert!(NumericMode::validate(b"0123456789"));
146        assert_eq!(NumericMode::invalid_character(b"12a"), Some((2, b'a')));
147        assert_eq!(NumericMode::MODE, Mode::Numeric);
148    }
149
150    #[test]
151    fn alphanumeric_mode_validates_qr_alphabet() {
152        assert!(AlphanumericMode::validate(b"HELLO WORLD-42"));
153        assert_eq!(AlphanumericMode::invalid_character(b"hello"), Some((0, b'h')));
154        assert_eq!(AlphanumericMode::MODE, Mode::Alphanumeric);
155    }
156
157    #[test]
158    fn byte_mode_accepts_any_bytes() {
159        assert!(ByteMode::validate(&[0, b'a', 255]));
160        assert_eq!(ByteMode::invalid_character(&[0, b'a', 255]), None);
161        assert_eq!(ByteMode::MODE, Mode::Byte);
162    }
163
164    #[test]
165    fn kanji_mode_validates_shift_jis_pairs() {
166        assert!(KanjiMode::validate(b"\x93\x5f\xe4\xaa"));
167        assert_eq!(KanjiMode::character_count(b"\x93\x5f\xe4\xaa"), 2);
168        assert_eq!(KanjiMode::invalid_character(b"\x93"), Some((0, 0x93)));
169        assert_eq!(KanjiMode::invalid_character(b"\x01\x02"), Some((0, 0x01)));
170        assert_eq!(KanjiMode::MODE, Mode::Kanji);
171    }
172}