input_pipeline/mouse_model_database/
db.rs
1use super::{data, data_import_from_chromiumos};
6use fidl_fuchsia_input_report as fidl_input_report;
7use lazy_static::lazy_static;
8use std::collections::HashMap;
9
10#[derive(Clone, Debug, PartialEq)]
15pub(crate) struct MouseModel {
16 pub(crate) identifier: &'static str,
17 pub(crate) vendor_id: &'static str,
18 pub(crate) product_id: &'static str,
19 pub(crate) counts_per_mm: u32,
21}
22
23const MM_PER_INCH: f32 = 25.4;
24pub(crate) const DEFAULT_COUNTS_PER_MM: u32 = (1000.0 / MM_PER_INCH) as u32;
25
26impl MouseModel {
27 fn new(
28 vendor_id: &'static str,
29 product_id: &'static str,
30 counts_per_inch: u32,
31 identifier: &'static str,
32 ) -> Self {
33 MouseModel {
34 identifier,
35 vendor_id,
36 product_id,
37 counts_per_mm: ((counts_per_inch as f32) / MM_PER_INCH) as u32,
38 }
39 }
40}
41
42struct VendorProducts {
50 default_model: Option<MouseModel>,
51 patterns: Vec<(glob::Pattern, MouseModel)>,
52 exact_models: HashMap<String, MouseModel>,
53}
54
55impl VendorProducts {
56 fn new() -> Self {
57 Self { default_model: None, patterns: vec![], exact_models: HashMap::new() }
58 }
59
60 fn add(&mut self, model: MouseModel) {
61 if model.product_id == "*" {
62 assert_eq!(self.default_model, None);
66 self.default_model = Some(model);
67 } else if model.product_id.ends_with("*") {
68 let pattern =
71 glob::Pattern::new(model.product_id).expect("product id is not a valid glob");
72 self.patterns.push((pattern, model));
73 } else {
74 self.exact_models.insert(model.product_id.to_string(), model);
75 }
76 }
77
78 fn get(&self, product_id: u32) -> Option<MouseModel> {
79 let pid = to_hex(product_id);
80 if let Some(m) = self.exact_models.get(&pid) {
81 return Some(m.clone());
82 }
83
84 if let Some((_, m)) = self.patterns.iter().find(|(p, _)| p.matches(&pid)) {
85 return Some(m.clone());
86 }
87
88 self.default_model.clone()
89 }
90}
91
92lazy_static! {
93 static ref DB: HashMap<String, VendorProducts> = {
94 let mut models: Vec<MouseModel> = Vec::new();
95 for m in data::MODELS {
96 models.push(MouseModel::new(m.0, m.1, m.2, m.3));
97 }
98 for m in data_import_from_chromiumos::MODELS {
99 models.push(MouseModel::new(m.0, m.1, m.2, m.3));
100 }
101
102 let mut db: HashMap<String, VendorProducts> = HashMap::new();
103
104 for m in models {
105 let vendor_id = m.vendor_id.to_owned();
106 match db.get_mut(&vendor_id) {
107 Some(v) => {
108 v.add(m);
109 }
110 None => {
111 let mut v = VendorProducts::new();
112 v.add(m);
113 db.insert(vendor_id, v);
114 }
115 }
116 }
117
118 db
119 };
120}
121
122const DEFAULT_MODEL: MouseModel = MouseModel {
124 identifier: "default mouse",
125 vendor_id: "*",
126 product_id: "*",
127 counts_per_mm: DEFAULT_COUNTS_PER_MM,
128};
129
130pub(crate) fn get_mouse_model(
131 device_info: Option<fidl_input_report::DeviceInformation>,
132) -> MouseModel {
133 match device_info {
134 None => DEFAULT_MODEL.clone(),
135 Some(device_info) => {
136 let vid = to_hex(device_info.vendor_id.unwrap_or_default());
137 match DB.get(&vid) {
138 Some(v) => match v.get(device_info.product_id.unwrap_or_default()) {
139 Some(m) => m,
140 None => DEFAULT_MODEL.clone(),
141 },
142 None => DEFAULT_MODEL.clone(),
143 }
144 }
145 }
146}
147
148fn to_hex(id: u32) -> String {
150 format!("{:04x}", id)
151}
152
153#[cfg(test)]
154mod test {
155 use super::super::{data, data_import_from_chromiumos};
156 use super::*;
157 use regex::Regex;
158 use std::collections::HashSet;
159 use test_case::test_case;
160
161 #[test_case("*", "*", 1000, "default mouse" =>
162 MouseModel {
163 vendor_id: "*",
164 identifier: "default mouse",
165 product_id: "*",
166 counts_per_mm: DEFAULT_COUNTS_PER_MM,
167 }; "default mouse")]
168 #[test_case("0001", "*", 1000, "any mouse of vendor" =>
169 MouseModel {
170 vendor_id: "0001",
171 identifier: "any mouse of vendor",
172 product_id: "*",
173 counts_per_mm: DEFAULT_COUNTS_PER_MM,
174 }; "any mouse of vendor")]
175 #[test_case("0001", "001*", 1000, "pattern product_id" =>
176 MouseModel {
177 vendor_id: "0001",
178 identifier: "pattern product_id",
179 product_id: "001*",
180 counts_per_mm: DEFAULT_COUNTS_PER_MM,
181 }; "pattern product_id")]
182 #[test_case("0001", "0002", 1000, "exact model" =>
183 MouseModel {
184 vendor_id: "0001",
185 identifier: "exact model",
186 product_id: "0002",
187 counts_per_mm: DEFAULT_COUNTS_PER_MM,
188 }; "exact model")]
189 #[fuchsia::test]
190 fn test_mouse_model_new(
191 vendor_id: &'static str,
192 product_id: &'static str,
193 cpi: u32,
194 identifier: &'static str,
195 ) -> MouseModel {
196 MouseModel::new(vendor_id, product_id, cpi, identifier)
197 }
198
199 #[test_case(0x046d, 0xc24c =>
200 MouseModel::new("046d", "c24c", 4000, "Logitech G400s")
201 ; "Known mouse")]
202 #[test_case(0x046d, 0xc401 =>
203 MouseModel::new("046d", "c40*", 600, "Logitech Trackballs*")
204 ; "pattern match")]
205 #[test_case(0x05ac, 0x0000 =>
206 MouseModel::new("05ac", "*", 373, "Apple mice (other)")
207 ; "any match")]
208 #[test_case(0x046d, 0x0aaf =>
209 MouseModel::new("*", "*", 1000, "default mouse")
210 ; "Unknown device: this is a microphone")]
211 #[fuchsia::test]
212 fn test_get_mouse_model(vendor_id: u32, product_id: u32) -> MouseModel {
213 get_mouse_model(Some(fidl_input_report::DeviceInformation {
214 vendor_id: Some(vendor_id),
215 product_id: Some(product_id),
216 version: Some(0),
217 polling_rate: Some(0),
218 ..Default::default()
219 }))
220 }
221
222 #[fuchsia::test]
223 fn test_get_mouse_model_none() {
224 pretty_assertions::assert_eq!(get_mouse_model(None), DEFAULT_MODEL);
225 }
226
227 #[fuchsia::test]
228 fn no_duplicated_mouse_model() {
229 let mut models: HashSet<(&'static str, &'static str)> = HashSet::new();
230 let mut check_duplication =
231 |filename: &'static str, list: &[(&'static str, &'static str, u32, &'static str)]| {
232 for m in list {
233 let new_inserted = models.insert((m.0, m.1));
234 if !new_inserted {
235 panic!(
236 "found duplicated mouse model in {}: vendor: {}, product: {}",
237 filename, m.0, m.1
238 );
239 }
240 }
241 };
242
243 check_duplication("data_import_from_chromiumos", &data_import_from_chromiumos::MODELS);
244 check_duplication("data", &data::MODELS);
245 }
246
247 #[test_case(&data_import_from_chromiumos::MODELS; "data_import_from_chromiumos")]
248 #[test_case(&data::MODELS; "data")]
249 #[fuchsia::test]
250 fn validate_vendor_id_product_id(models: &[(&'static str, &'static str, u32, &'static str)]) {
251 let vendor_id_re = Regex::new(r"^[0-9a-f]{4}$").unwrap();
252 let product_id_re = Regex::new(r"^[0-9a-f]{3}[0-9a-f\*]$").unwrap();
253 for m in models {
254 assert!(vendor_id_re.is_match(m.0), "vendor id should be 4 low case hex digit");
255 if m.1 != "*" {
256 assert!(
257 product_id_re.is_match(m.1),
258 r#"product id should be "* only" or "3 low case hex digit with ending *" or "4 low case hex digit""#
259 );
260 }
261 }
262 }
263}