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. If you construct
35/// one manually via the public field, you are responsible for maintaining the invariant;
36/// methods on this type will panic in debug builds if it is violated.
37///
38/// Use [`ManufacturerId::from_ascii`] for a checked construction path.
39///
40/// Available in all build configurations including bare `no_std`. The `Display` impl renders
41/// the three-character string directly, so `format!("{}", id)` and `id.to_string()` both work
42/// wherever a `Display` bound is satisfied.
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct ManufacturerId(pub [u8; 3]);
46
47impl ManufacturerId {
48    /// Constructs a `ManufacturerId` from three raw bytes, returning `None` if any byte is
49    /// not an ASCII uppercase letter (`A`–`Z`).
50    pub fn from_ascii(bytes: [u8; 3]) -> Option<Self> {
51        if bytes.iter().all(|&b| b.is_ascii_uppercase()) {
52            Some(Self(bytes))
53        } else {
54            None
55        }
56    }
57
58    /// Returns the ID as a `&str` slice.
59    ///
60    /// Panics in debug builds if the stored bytes are not ASCII uppercase letters, which
61    /// would indicate the type invariant was violated at construction time.
62    pub fn as_str(&self) -> &str {
63        debug_assert!(
64            self.0.iter().all(|&b| b.is_ascii_uppercase()),
65            "ManufacturerId invariant violated: bytes must be ASCII uppercase A-Z, got {:?}",
66            self.0
67        );
68        // Safety: ASCII uppercase bytes are always valid UTF-8.
69        core::str::from_utf8(&self.0).expect("ManufacturerId bytes must be ASCII uppercase A-Z")
70    }
71}
72
73impl core::fmt::Display for ManufacturerId {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        f.write_str(self.as_str())
76    }
77}
78
79/// A monitor descriptor string, decoded from one of the 18-byte descriptor slots in the
80/// EDID base block (`0xFC` monitor name, `0xFF` serial number, `0xFE` unspecified text).
81///
82/// The text payload occupies bytes 5–17 of the descriptor (13 bytes), terminated by `0x0A`
83/// and padded with spaces. The `as_str()` method strips both the terminator and trailing
84/// spaces, returning a clean `&str`.
85///
86/// Available in all build configurations including bare `no_std`. `Deref<Target = str>`
87/// is implemented so `Option<MonitorString>::as_deref()` returns `Option<&str>` directly.
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct MonitorString(pub [u8; 13]);
91
92impl MonitorString {
93    /// Returns the string content with the `0x0A` terminator and trailing spaces stripped.
94    ///
95    /// Returns an empty `&str` if the payload is all padding or not valid UTF-8.
96    pub fn as_str(&self) -> &str {
97        let bytes = &self.0;
98        let end = bytes.iter().position(|&b| b == 0x0A).unwrap_or(bytes.len());
99        let trimmed = match bytes[..end].iter().rposition(|&b| b != b' ') {
100            Some(i) => &bytes[..=i],
101            None => &[][..],
102        };
103        core::str::from_utf8(trimmed).unwrap_or("")
104    }
105}
106
107impl core::fmt::Display for MonitorString {
108    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109        f.write_str(self.as_str())
110    }
111}
112
113impl core::ops::Deref for MonitorString {
114    type Target = str;
115    fn deref(&self) -> &str {
116        self.as_str()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    #[cfg(any(feature = "alloc", feature = "std"))]
124    use crate::alloc::string::ToString;
125
126    // --- ManufacturerId ---
127
128    #[test]
129    fn manufacturer_id_valid_ascii_uppercase() {
130        let id = ManufacturerId::from_ascii(*b"DEL").unwrap();
131        assert_eq!(id.as_str(), "DEL");
132    }
133
134    #[test]
135    fn manufacturer_id_rejects_lowercase() {
136        assert!(ManufacturerId::from_ascii(*b"del").is_none());
137    }
138
139    #[test]
140    fn manufacturer_id_rejects_digit() {
141        assert!(ManufacturerId::from_ascii(*b"D3L").is_none());
142    }
143
144    #[test]
145    #[cfg(any(feature = "alloc", feature = "std"))]
146    fn manufacturer_id_display() {
147        let id = ManufacturerId::from_ascii(*b"SAM").unwrap();
148        assert_eq!(id.to_string(), "SAM");
149    }
150
151    // --- MonitorString ---
152
153    #[test]
154    fn monitor_string_strips_terminator_and_spaces() {
155        let mut buf = [b' '; 13];
156        let name = b"DELL U2722D";
157        buf[..name.len()].copy_from_slice(name);
158        buf[name.len()] = 0x0A;
159        assert_eq!(MonitorString(buf).as_str(), "DELL U2722D");
160    }
161
162    #[test]
163    fn monitor_string_no_terminator_strips_trailing_spaces() {
164        let mut buf = [b' '; 13];
165        buf[..3].copy_from_slice(b"ABC");
166        assert_eq!(MonitorString(buf).as_str(), "ABC");
167    }
168
169    #[test]
170    fn monitor_string_all_padding_gives_empty() {
171        assert_eq!(MonitorString([b' '; 13]).as_str(), "");
172    }
173
174    #[test]
175    fn monitor_string_deref() {
176        let mut buf = [b' '; 13];
177        buf[..3].copy_from_slice(b"LEN");
178        buf[3] = 0x0A;
179        let ms = MonitorString(buf);
180        let s: &str = &ms;
181        assert_eq!(s, "LEN");
182    }
183
184    #[test]
185    #[cfg(any(feature = "alloc", feature = "std"))]
186    fn monitor_string_display() {
187        let mut buf = [b' '; 13];
188        buf[..3].copy_from_slice(b"GSM");
189        buf[3] = 0x0A;
190        assert_eq!(MonitorString(buf).to_string(), "GSM");
191    }
192}