1use dbus::Path;
13use dbus::blocking::LocalConnection;
14use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
15use std::collections::HashMap;
16use std::time::Duration;
17
18const SERVICE: &str = "org.freedesktop.ColorManager";
20const MANAGER: &str = "/org/freedesktop/ColorManager";
21const ASK: Duration = Duration::from_millis(400);
23const SETUP: Duration = Duration::from_secs(1);
25
26pub fn checksums(output: &str) -> Option<Vec<String>> {
32 checksums_on(&crate::bus::system_address(), output)
33}
34
35pub fn checksums_on(address: &str, output: &str) -> Option<Vec<String>> {
37 let bus = LocalConnection::from(crate::bus::connect_within(address, SETUP).ok()?);
38 let found: Result<(Path<'static>,), dbus::Error> = bus.with_proxy(SERVICE, MANAGER, ASK).method_call(SERVICE, "FindDeviceByProperty", ("XRANDR_name", output));
39 let device = match found {
40 Ok((device,)) => device,
41 Err(e) if e.name().is_some_and(|name| name.starts_with(SERVICE)) => return Some(Vec::new()),
43 Err(_) => return None,
44 };
45 let profiles: Vec<Path<'static>> = bus.with_proxy(SERVICE, device, ASK).get("org.freedesktop.ColorManager.Device", "Profiles").unwrap_or_default();
46 let mut sums = Vec::new();
47 for profile in profiles {
48 let profile = bus.with_proxy(SERVICE, profile, ASK);
49 if let Ok(id) = profile.get::<String>("org.freedesktop.ColorManager.Profile", "ProfileId")
50 && let Some(sum) = id.strip_prefix("icc-")
51 {
52 sums.push(sum.to_lowercase());
53 }
54 if let Ok(metadata) = profile.get::<HashMap<String, String>>("org.freedesktop.ColorManager.Profile", "Metadata")
55 && let Some(sum) = metadata.get("FILE_checksum").filter(|sum| !sum.is_empty())
56 {
57 sums.push(sum.to_lowercase());
58 }
59 }
60 Some(sums)
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
74 fn colord_says_which_profiles_an_output_has() {
75 use dbus::arg::{RefArg, Variant};
76 use dbus::blocking::stdintf::org_freedesktop_dbus::RequestNameReply;
77 use dbus::channel::{MatchingReceiver, Sender as _};
78 use dbus::message::MatchRule;
79 let _alone = crate::bus::NOTIFICATION_SERVER.lock().unwrap_or_else(|e| e.into_inner());
80 let Some(address) = crate::bus::session_address() else {
81 eprintln!("No bus of the test's own (dbus-run-session): skipped.");
82 return;
83 };
84 let look = LocalConnection::from(crate::bus::connect(&address).unwrap());
85 let owned = |name: &str| -> bool { look.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", ASK).method_call("org.freedesktop.DBus", "NameHasOwner", (name,)).is_ok_and(|(owned,): (bool,)| owned) };
86 if owned("org.freedesktop.Notifications") || owned(SERVICE) {
87 eprintln!("A desktop's bus, or a colord on it already: skipped.");
88 return;
89 }
90 assert_eq!(checksums_on(&address, "eDP-1"), None);
92 let (ready, started) = std::sync::mpsc::channel::<bool>();
93 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
94 let stopped = std::sync::Arc::clone(&stop);
95 let served = address.clone();
96 std::thread::spawn(move || {
97 let Some(bus) = crate::bus::connect(&served).ok().map(LocalConnection::from).filter(|bus| matches!(bus.request_name(SERVICE, false, false, true), Ok(RequestNameReply::PrimaryOwner))) else {
98 let _ = ready.send(false);
99 return;
100 };
101 bus.start_receive(
102 MatchRule::new_method_call(),
103 Box::new(|call: dbus::Message, bus: &LocalConnection| {
104 let path = call.path().map(|p| p.to_string()).unwrap_or_default();
105 let reply = match (call.member().as_deref(), path.as_str()) {
106 (Some("FindDeviceByProperty"), MANAGER) => {
107 let (key, output): (String, String) = call.read2().unwrap();
108 if key == "XRANDR_name" && output == "eDP-1" {
109 call.method_return().append1(Path::from("/org/freedesktop/ColorManager/devices/xrandr_eDP_1"))
110 } else {
111 call.error(&"org.freedesktop.ColorManager.NotFound".into(), c"no such device")
112 }
113 }
114 (Some("Get"), path) => {
115 let (_interface, name): (String, String) = call.read2().unwrap();
116 let value: Box<dyn RefArg> = match (path, name.as_str()) {
117 ("/org/freedesktop/ColorManager/devices/xrandr_eDP_1", "Profiles") => Box::new(vec![Path::from("/org/freedesktop/ColorManager/profiles/a"), Path::from("/org/freedesktop/ColorManager/profiles/b")]),
118 ("/org/freedesktop/ColorManager/profiles/a", "ProfileId") => Box::new("icc-0123456789ABCDEF0123456789ABCDEF".to_string()),
119 ("/org/freedesktop/ColorManager/profiles/a", "Metadata") => Box::new(HashMap::from([("FILE_checksum".to_string(), "FEDCBA9876543210FEDCBA9876543210".to_string())])),
120 ("/org/freedesktop/ColorManager/profiles/b", "ProfileId") => Box::new("custom-made".to_string()),
121 ("/org/freedesktop/ColorManager/profiles/b", "Metadata") => Box::new(HashMap::<String, String>::new()),
122 _ => return true,
123 };
124 call.method_return().append1(Variant(value))
125 }
126 _ => return true,
127 };
128 let _ = bus.send(reply);
129 true
130 }),
131 );
132 let _ = ready.send(true);
133 let until = std::time::Instant::now() + Duration::from_secs(20);
134 while !stopped.load(std::sync::atomic::Ordering::Relaxed) && std::time::Instant::now() < until {
135 let _ = bus.process(Duration::from_millis(50));
136 }
137 });
138 if !started.recv_timeout(Duration::from_secs(5)).unwrap_or(false) {
139 eprintln!("colord's name taken on this bus: skipped.");
140 return;
141 }
142 assert_eq!(checksums_on(&address, "eDP-1"), Some(vec!["0123456789abcdef0123456789abcdef".to_string(), "fedcba9876543210fedcba9876543210".to_string()]));
143 assert_eq!(checksums_on(&address, "HDMI-1"), Some(Vec::new()), "an output colord does not know: none");
144 stop.store(true, std::sync::atomic::Ordering::Relaxed);
145 }
146}