Skip to main content

display_types/
manufacture.rs

1/// Manufacture date or model year, decoded from EDID base block bytes 16–17.
2///
3/// | Byte 16 | Meaning                                              |
4/// |---------|------------------------------------------------------|
5/// | `0x00`  | Week unspecified; byte 17 is the manufacture year.  |
6/// | `0x01`–`0x36` | Week of manufacture (1–54).               |
7/// | `0xFF`  | Byte 17 is a model year, not a manufacture year.    |
8///
9/// Year is encoded as `byte_17 + 1990`.
10#[non_exhaustive]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ManufactureDate {
14    /// The display was manufactured in the given year.
15    /// `week` is `None` if byte 16 was `0x00` (week unspecified).
16    Manufactured {
17        /// Week of manufacture (1–54), if specified.
18        week: Option<u8>,
19        /// Year of manufacture.
20        year: u16,
21    },
22    /// The year identifies a model year rather than a manufacture date.
23    ModelYear(u16),
24}
25
26/// A three-character PNP manufacturer identifier, decoded from EDID base block bytes `0x08`–`0x09`.
27///
28/// Each character is an ASCII uppercase letter (A–Z). Valid IDs are registered with the IANA
29/// PNP registry. Well-known examples: `GSM` (LG), `SAM` (Samsung), `DEL` (Dell).
30///
31/// # Invariant
32///
33/// All three bytes must be ASCII uppercase letters (`b'A'`–`b'Z'`, i.e. `0x41`–`0x5A`).
34/// The library only constructs this type after validating that constraint, and
35/// deserialization (with the `serde` feature) rejects bytes that violate it. If you construct
36/// one manually via the public field, you are responsible for maintaining the invariant:
37/// [`as_str`][Self::as_str] panics in debug builds and returns `""` in release builds if it
38/// is violated, and the `Display` impl renders the raw bytes escaped (e.g. `\xff\x00\x00`).
39///
40/// Use [`ManufacturerId::from_ascii`] or [`TryFrom<[u8; 3]>`][TryFrom] for a checked
41/// construction path.
42///
43/// Available in all build configurations including bare `no_std`. The `Display` impl renders
44/// the three-character string directly, so `format!("{}", id)` and `id.to_string()` both work
45/// wherever a `Display` bound is satisfied.
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[cfg_attr(feature = "serde", serde(try_from = "[u8; 3]", into = "[u8; 3]"))]
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct ManufacturerId(pub [u8; 3]);
50
51impl ManufacturerId {
52    /// Constructs a `ManufacturerId` from three raw bytes, returning `None` if any byte is
53    /// not an ASCII uppercase letter (`A`–`Z`).
54    pub fn from_ascii(bytes: [u8; 3]) -> Option<Self> {
55        if bytes.iter().all(|&b| b.is_ascii_uppercase()) {
56            Some(Self(bytes))
57        } else {
58            None
59        }
60    }
61
62    /// Returns the ID as a `&str` slice.
63    ///
64    /// If the stored bytes are not ASCII uppercase letters (the type invariant was violated
65    /// at construction time), panics in debug builds and returns `""` in release builds.
66    pub fn as_str(&self) -> &str {
67        let valid = self.is_valid();
68        debug_assert!(
69            valid,
70            "ManufacturerId invariant violated: bytes must be ASCII uppercase A-Z, got {:?}",
71            self.0
72        );
73        if valid {
74            // ASCII uppercase bytes are always valid UTF-8.
75            core::str::from_utf8(&self.0).unwrap_or("")
76        } else {
77            ""
78        }
79    }
80
81    fn is_valid(&self) -> bool {
82        self.0.iter().all(u8::is_ascii_uppercase)
83    }
84}
85
86impl TryFrom<[u8; 3]> for ManufacturerId {
87    type Error = &'static str;
88
89    /// Checked construction; equivalent to [`ManufacturerId::from_ascii`].
90    fn try_from(bytes: [u8; 3]) -> Result<Self, Self::Error> {
91        Self::from_ascii(bytes).ok_or("ManufacturerId bytes must be ASCII uppercase A-Z")
92    }
93}
94
95impl From<ManufacturerId> for [u8; 3] {
96    fn from(id: ManufacturerId) -> Self {
97        id.0
98    }
99}
100
101impl core::fmt::Display for ManufacturerId {
102    /// Renders the three-letter ID. Bytes that violate the type invariant are rendered
103    /// escaped (e.g. `\xff\x00\x00`) rather than panicking.
104    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
105        if self.is_valid() {
106            f.write_str(self.as_str())
107        } else {
108            write!(f, "{}", self.0.escape_ascii())
109        }
110    }
111}
112
113/// A monitor descriptor string, decoded from one of the 18-byte descriptor slots in the
114/// EDID base block (`0xFC` monitor name, `0xFF` serial number, `0xFE` unspecified text).
115///
116/// The text payload occupies bytes 5–17 of the descriptor (13 bytes), terminated by `0x0A`
117/// and padded with spaces. The `as_str()` method strips both the terminator and trailing
118/// spaces, returning a clean `&str`.
119///
120/// Available in all build configurations including bare `no_std`. `Deref<Target = str>`
121/// is implemented so `Option<MonitorString>::as_deref()` returns `Option<&str>` directly.
122#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct MonitorString(pub [u8; 13]);
125
126impl MonitorString {
127    /// Returns the string content with the `0x0A` terminator and trailing spaces stripped.
128    ///
129    /// Returns an empty `&str` if the payload is all padding or not valid UTF-8.
130    pub fn as_str(&self) -> &str {
131        let bytes = &self.0;
132        let end = bytes.iter().position(|&b| b == 0x0A).unwrap_or(bytes.len());
133        let trimmed = match bytes[..end].iter().rposition(|&b| b != b' ') {
134            Some(i) => &bytes[..=i],
135            None => &[][..],
136        };
137        core::str::from_utf8(trimmed).unwrap_or("")
138    }
139}
140
141impl core::fmt::Display for MonitorString {
142    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
143        f.write_str(self.as_str())
144    }
145}
146
147impl core::ops::Deref for MonitorString {
148    type Target = str;
149    fn deref(&self) -> &str {
150        self.as_str()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    #[cfg(any(feature = "alloc", feature = "std"))]
158    use crate::alloc::string::ToString;
159
160    // --- ManufacturerId ---
161
162    #[test]
163    fn manufacturer_id_valid_ascii_uppercase() {
164        let id = ManufacturerId::from_ascii(*b"DEL").unwrap();
165        assert_eq!(id.as_str(), "DEL");
166    }
167
168    #[test]
169    fn manufacturer_id_rejects_lowercase() {
170        assert!(ManufacturerId::from_ascii(*b"del").is_none());
171    }
172
173    #[test]
174    fn manufacturer_id_rejects_digit() {
175        assert!(ManufacturerId::from_ascii(*b"D3L").is_none());
176    }
177
178    #[test]
179    #[cfg(any(feature = "alloc", feature = "std"))]
180    fn manufacturer_id_display() {
181        let id = ManufacturerId::from_ascii(*b"SAM").unwrap();
182        assert_eq!(id.to_string(), "SAM");
183    }
184
185    #[test]
186    fn manufacturer_id_try_from_checks_invariant() {
187        assert_eq!(
188            ManufacturerId::try_from(*b"GSM"),
189            Ok(ManufacturerId(*b"GSM"))
190        );
191        assert!(ManufacturerId::try_from([0xFF, 0x00, 0x00]).is_err());
192    }
193
194    #[test]
195    #[cfg(any(feature = "alloc", feature = "std"))]
196    fn manufacturer_id_display_escapes_invalid_bytes() {
197        // Reachable only by bypassing the invariant via the public field. Display must
198        // render the real bytes without panicking, in debug and release builds alike.
199        assert_eq!(
200            ManufacturerId([0xFF, 0x00, 0x00]).to_string(),
201            r"\xff\x00\x00"
202        );
203        assert_eq!(ManufacturerId(*b"del").to_string(), "del");
204    }
205
206    #[test]
207    #[cfg(not(debug_assertions))]
208    fn manufacturer_id_as_str_invalid_bytes_is_empty_in_release() {
209        assert_eq!(ManufacturerId([0xFF, 0x00, 0x00]).as_str(), "");
210    }
211
212    #[test]
213    #[cfg(debug_assertions)]
214    #[should_panic(expected = "ManufacturerId invariant violated")]
215    fn manufacturer_id_as_str_invalid_bytes_panics_in_debug() {
216        let _ = ManufacturerId([0xFF, 0x00, 0x00]).as_str();
217    }
218
219    #[test]
220    #[cfg(all(feature = "serde", any(feature = "alloc", feature = "std")))]
221    fn manufacturer_id_serde_wire_format_is_byte_array() {
222        let id = ManufacturerId(*b"DEL");
223        let json = serde_json::to_string(&id).unwrap();
224        assert_eq!(json, "[68,69,76]");
225        assert_eq!(serde_json::from_str::<ManufacturerId>(&json).unwrap(), id);
226    }
227
228    #[test]
229    #[cfg(feature = "serde")]
230    fn manufacturer_id_deserialize_rejects_invalid_bytes() {
231        // Previously deserialized successfully and then panicked in `Display`.
232        assert!(serde_json::from_str::<ManufacturerId>("[255,0,0]").is_err());
233        assert!(serde_json::from_str::<ManufacturerId>("[100,101,108]").is_err()); // "del"
234    }
235
236    // --- MonitorString ---
237
238    #[test]
239    fn monitor_string_strips_terminator_and_spaces() {
240        let mut buf = [b' '; 13];
241        let name = b"DELL U2722D";
242        buf[..name.len()].copy_from_slice(name);
243        buf[name.len()] = 0x0A;
244        assert_eq!(MonitorString(buf).as_str(), "DELL U2722D");
245    }
246
247    #[test]
248    fn monitor_string_no_terminator_strips_trailing_spaces() {
249        let mut buf = [b' '; 13];
250        buf[..3].copy_from_slice(b"ABC");
251        assert_eq!(MonitorString(buf).as_str(), "ABC");
252    }
253
254    #[test]
255    fn monitor_string_all_padding_gives_empty() {
256        assert_eq!(MonitorString([b' '; 13]).as_str(), "");
257    }
258
259    #[test]
260    fn monitor_string_deref() {
261        let mut buf = [b' '; 13];
262        buf[..3].copy_from_slice(b"LEN");
263        buf[3] = 0x0A;
264        let ms = MonitorString(buf);
265        let s: &str = &ms;
266        assert_eq!(s, "LEN");
267    }
268
269    #[test]
270    #[cfg(any(feature = "alloc", feature = "std"))]
271    fn monitor_string_display() {
272        let mut buf = [b' '; 13];
273        buf[..3].copy_from_slice(b"GSM");
274        buf[3] = 0x0A;
275        assert_eq!(MonitorString(buf).to_string(), "GSM");
276    }
277}