1use cue_sdk_sys as ffi;
2use failure::_core::str::Utf8Error;
3
4use super::{
5 channels_from_ffi, Channel, ChannelsFromFfiError, DeviceCapabilities, DeviceId, DeviceLayout,
6 DeviceType,
7};
8use crate::internal::try_c_char_ptr_to_str;
9
10#[derive(Debug, Clone, Fail, PartialEq)]
12pub enum CueDeviceInfoFromFfiError {
13 #[fail(
14 display = "Expected to create a CueDevice from a valid pointer, but received a null pointer instead."
15 )]
16 NullPointer,
17 #[fail(display = "Failed to generate on field: {}, error: {}", field, error)]
18 StringConversionError {
19 field: String,
20 #[cause]
21 error: Utf8Error,
22 },
23 #[fail(display = "Unexpected null pointer on field: {}", _0)]
24 NullPointerField(String),
25 #[fail(display = "Invalid (negative) number of leds: {}", _0)]
26 InvalidLedsCount(i32),
27 #[fail(display = "Error with channels: {}", _0)]
28 ChannelsError(ChannelsFromFfiError),
29}
30
31impl CueDeviceInfo {
32 pub(crate) fn from_ffi(
33 device_info: *mut ffi::CorsairDeviceInfo,
34 ) -> Result<Self, CueDeviceInfoFromFfiError> {
35 if device_info.is_null() {
36 return Err(CueDeviceInfoFromFfiError::NullPointer);
37 }
38
39 let info = unsafe { *device_info };
40 let id = DeviceId::from_ffi(info.deviceId).map_err(|e| {
41 CueDeviceInfoFromFfiError::StringConversionError {
42 field: "deviceId".to_owned(),
43 error: e.0,
44 }
45 })?;
46
47 let device_type = DeviceType::from_ffi(info.type_);
48
49 let model = try_c_char_ptr_to_str(info.model)
50 .map_err(|e| CueDeviceInfoFromFfiError::StringConversionError {
51 field: "model".to_string(),
52 error: e,
53 })?
54 .ok_or_else(|| CueDeviceInfoFromFfiError::NullPointerField("model".to_string()))?
55 .to_string();
56
57 let layout = DeviceLayout::from_ffi_values(info.physicalLayout, info.logicalLayout);
58
59 if info.ledsCount < 0 {
60 return Err(CueDeviceInfoFromFfiError::InvalidLedsCount(info.ledsCount));
61 }
62
63 let leds_count = info.ledsCount as u32;
64
65 let channels = channels_from_ffi(info.channels)
66 .map_err(|e| CueDeviceInfoFromFfiError::ChannelsError(e))?;
67
68 let capabilities = DeviceCapabilities::from_ffi(info.capsMask);
69
70 Ok(CueDeviceInfo {
71 id,
72 capabilities,
73 channels,
74 leds_count,
75 device_type,
76 layout,
77 model,
78 })
79 }
80}
81
82#[derive(Debug, Clone, PartialEq)]
85pub struct CueDeviceInfo {
86 pub id: DeviceId,
87 pub device_type: Option<DeviceType>,
88 pub model: String,
89 pub layout: Option<DeviceLayout>,
90 pub capabilities: DeviceCapabilities,
91 pub leds_count: u32,
92 pub channels: Vec<Channel>,
93}
94
95#[cfg(test)]
96mod tests {
97 use cue_sdk_sys as ffi;
98
99 use std::ffi::CString;
100 use std::ptr;
101
102 use super::{CueDeviceInfo, CueDeviceInfoFromFfiError};
103 use crate::device::DeviceId;
104 use crate::device::{
105 Channel, DeviceCapabilities, DeviceLayout, DeviceType, LogicalLayout, PhysicalLayout,
106 };
107 use std::os::raw::c_char;
108
109 const DEVICE_ID: [c_char; 128] = [
110 0x11, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20,
111 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20,
112 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30,
113 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30,
114 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20,
115 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20,
116 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30,
117 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30,
118 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50,
119 ];
120
121 #[test]
122 fn device_from_ffi_null_ptr() {
123 let result = CueDeviceInfo::from_ffi(ptr::null_mut());
124 assert_eq!(result.unwrap_err(), CueDeviceInfoFromFfiError::NullPointer);
125 }
126
127 #[test]
128 fn device_from_ffi_model_null_ptr() {
129 let channels_info = ffi::CorsairChannelsInfo {
130 channelsCount: 0,
131 channels: ptr::null_mut(),
132 };
133
134 let mut info = ffi::CorsairDeviceInfo {
135 type_: ffi::CorsairDeviceType_CDT_Cooler,
136 model: ptr::null(),
137 physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
138 logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
139 capsMask: 0,
140 ledsCount: 20,
141 channels: channels_info,
142 deviceId: DEVICE_ID,
143 };
144
145 let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
146
147 let result = CueDeviceInfo::from_ffi(info_ptr);
148 assert!(
149 matches!(result.unwrap_err(), CueDeviceInfoFromFfiError::NullPointerField(field) if field == "model")
150 )
151 }
152
153 #[test]
154 fn device_from_ffi_model_invalid_utf8() {
155 let invalid_utf8 = CString::new([0xC0, 0xC0, 0xC0, 0xC0]).unwrap();
156
157 let channels_info = ffi::CorsairChannelsInfo {
158 channelsCount: 0,
159 channels: ptr::null_mut(),
160 };
161
162 let mut info = ffi::CorsairDeviceInfo {
163 type_: ffi::CorsairDeviceType_CDT_Cooler,
164 model: invalid_utf8.as_ptr(),
165 physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
166 logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
167 capsMask: 0,
168 ledsCount: 20,
169 channels: channels_info,
170 deviceId: DEVICE_ID,
171 };
172
173 let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
174
175 let result = CueDeviceInfo::from_ffi(info_ptr);
176 assert!(
177 matches!(result.unwrap_err(), CueDeviceInfoFromFfiError::StringConversionError {field, ..} if field == "model")
178 )
179 }
180
181 #[test]
182 fn device_from_ffi_invalid_leds_count() {
183 let cool_device_model = CString::new("some-cool-device-model").unwrap();
184
185 let channels_info = ffi::CorsairChannelsInfo {
186 channelsCount: 0,
187 channels: ptr::null_mut(),
188 };
189
190 let mut info = ffi::CorsairDeviceInfo {
191 type_: ffi::CorsairDeviceType_CDT_Cooler,
192 model: cool_device_model.as_ptr(),
193 physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
194 logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
195 capsMask: 0,
196 ledsCount: -1,
197 channels: channels_info,
198 deviceId: DEVICE_ID,
199 };
200
201 let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
202
203 let result = CueDeviceInfo::from_ffi(info_ptr);
204 assert_eq!(
205 result.unwrap_err(),
206 CueDeviceInfoFromFfiError::InvalidLedsCount(-1)
207 );
208 }
209
210 #[test]
211 fn device_from_ffi_invalid_channels() {
212 let cool_device_model = CString::new("some-cool-device-model").unwrap();
213
214 let channels_info = ffi::CorsairChannelsInfo {
215 channelsCount: -1,
216 channels: ptr::null_mut(),
217 };
218
219 let mut info = ffi::CorsairDeviceInfo {
220 type_: ffi::CorsairDeviceType_CDT_Cooler,
221 model: cool_device_model.as_ptr(),
222 physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
223 logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
224 capsMask: 0,
225 ledsCount: 23,
226 channels: channels_info,
227 deviceId: DEVICE_ID,
228 };
229
230 let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
231
232 let result = CueDeviceInfo::from_ffi(info_ptr);
233 assert!(matches!(
234 result.unwrap_err(),
235 CueDeviceInfoFromFfiError::ChannelsError(_)
236 ));
237 }
238
239 #[test]
240 fn device_from_ffi_all_valid() {
241 let cool_device_model = CString::new("some-cool-device-model").unwrap();
242
243 let mut channel_devices = [
244 ffi::CorsairChannelDeviceInfo {
245 type_: ffi::CorsairChannelDeviceType_CCDT_QL_Fan,
246 deviceLedCount: 6,
247 },
248 ffi::CorsairChannelDeviceInfo {
249 type_: ffi::CorsairChannelDeviceType_CCDT_Strip,
250 deviceLedCount: 6,
251 },
252 ];
253
254 let mut channels = [
255 ffi::CorsairChannelInfo {
256 totalLedsCount: 2,
257 devicesCount: 0,
258 devices: ptr::null_mut(),
259 },
260 ffi::CorsairChannelInfo {
261 totalLedsCount: 23,
262 devicesCount: 0,
263 devices: channel_devices.as_mut_ptr(),
264 },
265 ];
266
267 let channels_info = ffi::CorsairChannelsInfo {
268 channelsCount: 2,
269 channels: channels.as_mut_ptr(),
270 };
271
272 let mut info = ffi::CorsairDeviceInfo {
273 type_: ffi::CorsairDeviceType_CDT_Cooler,
274 model: cool_device_model.as_ptr(),
275 physicalLayout: ffi::CorsairPhysicalLayout_CPL_JP,
276 logicalLayout: ffi::CorsairLogicalLayout_CLL_JP,
277 capsMask: 0,
278 ledsCount: 32,
279 channels: channels_info,
280 deviceId: DEVICE_ID,
281 };
282
283 let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
284
285 let result = CueDeviceInfo::from_ffi(info_ptr);
286 assert_eq!(result.unwrap(), CueDeviceInfo {
287 id: DeviceId("\u{11}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P".to_string()),
288 device_type: Some(DeviceType::Cooler),
289 model: "some-cool-device-model".to_string(),
290 layout: Some(DeviceLayout::Keyboard {
291 physical_layout: PhysicalLayout::KeyboardJp,
292 logical_layout: LogicalLayout::KeyboardJp
293 }),
294 capabilities: DeviceCapabilities {
295 lighting: false,
296 property_lookup: false
297 },
298 leds_count: 32,
299 channels: vec![
300 Channel {
301 total_led_count: 2,
302 devices: vec![]
303 },
304 Channel {
305 total_led_count: 23,
306 devices: vec![]
307 }
308 ]
309 });
310 }
311}