216 lines
8.0 KiB
Rust
216 lines
8.0 KiB
Rust
//! Human-readable EDID identities. Serial-less displays remain tied to a qualified connector.
|
|
//! Duplicate serials are qualified too; a remembered collision stays qualified after unplug.
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct Identity {
|
|
pub make: String,
|
|
pub model: String,
|
|
pub serial: Option<String>,
|
|
}
|
|
|
|
fn escaped(value: &str) -> String {
|
|
value
|
|
.bytes()
|
|
.map(|byte| {
|
|
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.') {
|
|
char::from(byte).to_string()
|
|
} else {
|
|
format!("%{byte:02X}")
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
impl Identity {
|
|
pub fn from_edid(edid: &[u8]) -> Option<Self> {
|
|
if edid.len() < 128
|
|
|| edid[..8] != [0, 255, 255, 255, 255, 255, 255, 0]
|
|
|| edid[..128].iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte)) != 0
|
|
{
|
|
return None;
|
|
}
|
|
let manufacturer = u16::from_be_bytes([edid[8], edid[9]]);
|
|
let mut make = String::new();
|
|
for shift in [10, 5, 0] {
|
|
let letter = ((manufacturer >> shift) & 31) as u8;
|
|
if !(1..=26).contains(&letter) {
|
|
return None;
|
|
}
|
|
make.push(char::from(b'A' + letter - 1));
|
|
}
|
|
let product = u16::from_le_bytes([edid[10], edid[11]]);
|
|
let numeric_serial = u32::from_le_bytes(edid[12..16].try_into().ok()?);
|
|
let mut model = format!("{product:04X}");
|
|
let mut serial = None;
|
|
for descriptor in edid[54..126].chunks_exact(18) {
|
|
if descriptor[..3] != [0, 0, 0] {
|
|
continue;
|
|
}
|
|
let text = String::from_utf8_lossy(&descriptor[5..18])
|
|
.trim_matches(|c: char| c.is_whitespace() || c == '\0')
|
|
.to_owned();
|
|
if text.is_empty() {
|
|
continue;
|
|
}
|
|
match descriptor[3] {
|
|
0xfc => model = format!("{product:04X}-{}", escaped(&text)),
|
|
0xff if text != "0" => serial = Some(escaped(&text)),
|
|
_ => {}
|
|
}
|
|
}
|
|
if serial.is_none() && numeric_serial != 0 && numeric_serial != u32::MAX {
|
|
serial = Some(format!("{numeric_serial:08X}"));
|
|
}
|
|
Some(Self { make, model, serial })
|
|
}
|
|
|
|
pub fn base(&self) -> String {
|
|
format!(
|
|
"edid:{}:{}:{}",
|
|
self.make,
|
|
self.model,
|
|
self.serial.as_deref().unwrap_or("no-serial")
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct IdentityRegistry {
|
|
ambiguous: HashSet<String>,
|
|
/// Retains the first monitor's identity if a later collision is discovered.
|
|
known: HashMap<String, String>,
|
|
}
|
|
|
|
impl IdentityRegistry {
|
|
pub fn resolve(&mut self, monitors: &[(String, Option<Identity>)]) -> Vec<String> {
|
|
let mut counts = HashMap::new();
|
|
for (_, identity) in monitors {
|
|
if let Some(identity) = identity {
|
|
*counts.entry(identity.base()).or_insert(0) += 1;
|
|
}
|
|
}
|
|
self.ambiguous
|
|
.extend(counts.into_iter().filter(|(_, count)| *count > 1).map(|(base, _)| base));
|
|
// Prefer already-known identities, but never permit duplicate runtime IDs.
|
|
let mut used = HashSet::new();
|
|
let mut ordered = monitors.iter().enumerate().collect::<Vec<_>>();
|
|
ordered.sort_by_key(|(_, (key, _))| (!self.known.contains_key(key), key));
|
|
let mut result = vec![String::new(); monitors.len()];
|
|
for (index, (key, identity)) in ordered {
|
|
let base = identity.as_ref().map(Identity::base);
|
|
let qualified = match &base {
|
|
Some(base) => format!("{base}@{}", escaped(key)),
|
|
None => format!("drm:{}", escaped(key)),
|
|
};
|
|
let preferred = self
|
|
.known
|
|
.get(key)
|
|
.filter(|known| {
|
|
base.as_ref().is_some_and(|base| *known == base || *known == &qualified)
|
|
|| (base.is_none() && *known == &qualified)
|
|
})
|
|
.cloned()
|
|
.unwrap_or_else(|| {
|
|
if identity.as_ref().is_some_and(|identity| identity.serial.is_some())
|
|
&& !self.ambiguous.contains(base.as_ref().unwrap())
|
|
{
|
|
base.clone().unwrap()
|
|
} else {
|
|
qualified.clone()
|
|
}
|
|
});
|
|
let resolved = if used.insert(preferred.clone()) {
|
|
preferred
|
|
} else {
|
|
qualified
|
|
};
|
|
assert!(
|
|
result.iter().all(|other| other != &resolved),
|
|
"qualified hardware connector must be unique"
|
|
);
|
|
self.known.insert(key.clone(), resolved.clone());
|
|
result[index] = resolved;
|
|
}
|
|
result
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn identity(serial: Option<&str>) -> Identity {
|
|
Identity {
|
|
make: "DEL".into(),
|
|
model: "1234-monitor".into(),
|
|
serial: serial.map(String::from),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn replacement_on_same_connector_invalidates_cached_identity() {
|
|
let mut registry = IdentityRegistry::default();
|
|
let key = "gpu/DP-1".to_owned();
|
|
let dell = identity(Some("123"));
|
|
let mut lg = identity(Some("456"));
|
|
lg.make = "GSM".into();
|
|
let old = registry.resolve(&[(key.clone(), Some(dell.clone()))]);
|
|
let new = registry.resolve(&[(key.clone(), Some(lg))]);
|
|
assert_ne!(old, new);
|
|
assert!(new[0].starts_with("edid:GSM:"));
|
|
assert_eq!(registry.resolve(&[(key.clone(), Some(dell))]), old);
|
|
assert!(registry.resolve(&[(key, None)])[0].starts_with("drm:"));
|
|
}
|
|
|
|
#[test]
|
|
fn serial_survives_connector_change() {
|
|
let mut registry = IdentityRegistry::default();
|
|
let first = registry.resolve(&[("gpu/DP-1".into(), Some(identity(Some("123"))))]);
|
|
let next = registry.resolve(&[("gpu/DP-2".into(), Some(identity(Some("123"))))]);
|
|
assert_eq!(first, next);
|
|
assert_eq!(first[0], "edid:DEL:1234-monitor:123");
|
|
}
|
|
|
|
#[test]
|
|
fn identical_without_serial_are_distinct_and_reconnect_stably() {
|
|
for serial in [None, Some("duplicated")] {
|
|
let mut registry = IdentityRegistry::default();
|
|
let inputs = [
|
|
("gpu/DP-1".into(), Some(identity(serial))),
|
|
("gpu/DP-2".into(), Some(identity(serial))),
|
|
];
|
|
let resolved = registry.resolve(&inputs);
|
|
assert_ne!(resolved[0], resolved[1]);
|
|
assert_eq!(registry.resolve(&inputs[..1])[0], resolved[0]);
|
|
assert_eq!(registry.resolve(&inputs), resolved);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn collision_after_first_connection_does_not_change_existing_identity() {
|
|
let mut registry = IdentityRegistry::default();
|
|
let a = ("gpu/DP-1".into(), Some(identity(Some("duplicate"))));
|
|
let b = ("gpu/DP-2".into(), Some(identity(Some("duplicate"))));
|
|
let first = registry.resolve(std::slice::from_ref(&a));
|
|
let both = registry.resolve(&[a.clone(), b]);
|
|
assert_eq!(both[0], first[0]);
|
|
assert_ne!(both[0], both[1]);
|
|
assert_eq!(registry.resolve(&[a]), first);
|
|
}
|
|
|
|
#[test]
|
|
fn edid_decodes_make_product_and_numeric_serial_and_checks_checksum() {
|
|
let mut edid = [0u8; 128];
|
|
edid[..8].copy_from_slice(&[0, 255, 255, 255, 255, 255, 255, 0]);
|
|
let del = (4u16 << 10) | (5 << 5) | 12;
|
|
edid[8..10].copy_from_slice(&del.to_be_bytes());
|
|
edid[10..12].copy_from_slice(&0x1234u16.to_le_bytes());
|
|
edid[12..16].copy_from_slice(&42u32.to_le_bytes());
|
|
edid[127] = 0u8.wrapping_sub(edid.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte)));
|
|
assert_eq!(Identity::from_edid(&edid).unwrap().base(), "edid:DEL:1234:0000002A");
|
|
edid[127] ^= 1;
|
|
assert!(Identity::from_edid(&edid).is_none());
|
|
}
|
|
}
|