/src/fwupd/libfwupdplugin/fu-hid-descriptor.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2023 Richard Hughes <richard@hughsie.com> |
3 | | * |
4 | | * SPDX-License-Identifier: LGPL-2.1-or-later |
5 | | */ |
6 | | |
7 | 156k | #define G_LOG_DOMAIN "FuHidDevice" |
8 | | |
9 | | #include "config.h" |
10 | | |
11 | | #include "fu-byte-array.h" |
12 | | #include "fu-common.h" |
13 | | #include "fu-hid-descriptor.h" |
14 | | #include "fu-hid-report-item.h" |
15 | | #include "fu-hid-struct.h" |
16 | | #include "fu-input-stream.h" |
17 | | |
18 | | /** |
19 | | * FuHidDescriptor: |
20 | | * |
21 | | * A HID descriptor. |
22 | | * |
23 | | * Each report is a image of this firmware object and each report has children of #FuHidReportItem. |
24 | | * |
25 | | * Documented: https://www.usb.org/sites/default/files/hid1_11.pdf |
26 | | * |
27 | | * See also: [class@FuFirmware] |
28 | | */ |
29 | | |
30 | 1.44k | G_DEFINE_TYPE(FuHidDescriptor, fu_hid_descriptor, FU_TYPE_FIRMWARE) |
31 | 1.44k | |
32 | 148k | #define FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX 1024 |
33 | 33.6k | #define FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX 16 |
34 | 148k | #define FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX 1024 |
35 | 40.6k | #define FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX 64 |
36 | | |
37 | | static guint |
38 | | fu_hid_descriptor_count_table_dupes(GPtrArray *table, FuHidReportItem *item) |
39 | 74.2k | { |
40 | 74.2k | guint cnt = 0; |
41 | 21.0M | for (guint i = 0; i < table->len; i++) { |
42 | 20.9M | FuHidReportItem *item_tmp = g_ptr_array_index(table, i); |
43 | 20.9M | if (fu_hid_report_item_get_kind(item) == fu_hid_report_item_get_kind(item_tmp) && |
44 | 20.9M | fu_hid_report_item_get_value(item) == fu_hid_report_item_get_value(item_tmp) && |
45 | 10.7M | fu_firmware_get_idx(FU_FIRMWARE(item)) == |
46 | 10.7M | fu_firmware_get_idx(FU_FIRMWARE(item_tmp))) |
47 | 960k | cnt++; |
48 | 20.9M | } |
49 | 74.2k | return cnt; |
50 | 74.2k | } |
51 | | |
52 | | static gboolean |
53 | | fu_hid_descriptor_parse(FuFirmware *firmware, |
54 | | GInputStream *stream, |
55 | | FuFirmwareParseFlags flags, |
56 | | GError **error) |
57 | 1.20k | { |
58 | 1.20k | gsize offset = 0; |
59 | 1.20k | gsize streamsz = 0; |
60 | 1.20k | g_autoptr(GPtrArray) table_state = |
61 | 1.20k | g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); |
62 | 1.20k | g_autoptr(GPtrArray) table_local = |
63 | 1.20k | g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); |
64 | 1.20k | if (!fu_input_stream_size(stream, &streamsz, error)) |
65 | 0 | return FALSE; |
66 | 149k | while (offset < streamsz) { |
67 | 148k | g_autofree gchar *itemstr = NULL; |
68 | 148k | g_autoptr(FuHidReportItem) item = fu_hid_report_item_new(); |
69 | | |
70 | | /* sanity check */ |
71 | 148k | if (table_state->len > FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX) { |
72 | 4 | g_set_error(error, |
73 | 4 | FWUPD_ERROR, |
74 | 4 | FWUPD_ERROR_INVALID_DATA, |
75 | 4 | "HID table state too large, limit is %u", |
76 | 4 | (guint)FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX); |
77 | 4 | return FALSE; |
78 | 4 | } |
79 | 148k | if (table_local->len > FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX) { |
80 | 1 | g_set_error(error, |
81 | 1 | FWUPD_ERROR, |
82 | 1 | FWUPD_ERROR_INVALID_DATA, |
83 | 1 | "HID table state too large, limit is %u", |
84 | 1 | (guint)FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX); |
85 | 1 | return FALSE; |
86 | 1 | } |
87 | | |
88 | 148k | if (!fu_firmware_parse_stream(FU_FIRMWARE(item), stream, offset, flags, error)) |
89 | 128 | return FALSE; |
90 | 148k | if (!fu_size_checked_inc(&offset, fu_firmware_get_size(FU_FIRMWARE(item)), error)) |
91 | 0 | return FALSE; |
92 | | |
93 | | /* only for debugging */ |
94 | 148k | itemstr = fu_firmware_to_string(FU_FIRMWARE(item)); |
95 | 148k | g_debug("add to table-state: %s", itemstr); |
96 | | |
97 | | /* if there is a sane number of duplicate tokens then add to table */ |
98 | 148k | if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_GLOBAL) { |
99 | 40.6k | if (fu_hid_descriptor_count_table_dupes(table_state, item) > |
100 | 40.6k | FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX) { |
101 | 9 | g_set_error( |
102 | 9 | error, |
103 | 9 | FWUPD_ERROR, |
104 | 9 | FWUPD_ERROR_INVALID_DATA, |
105 | 9 | "table invalid @0x%x, too many duplicate global %s tokens", |
106 | 9 | (guint)offset, |
107 | 9 | fu_firmware_get_id(FU_FIRMWARE(item))); |
108 | 9 | return FALSE; |
109 | 9 | } |
110 | 40.6k | g_ptr_array_add(table_state, g_object_ref(item)); |
111 | 108k | } else if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_LOCAL || |
112 | 78.2k | fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) { |
113 | 33.6k | if (fu_hid_descriptor_count_table_dupes(table_local, item) > |
114 | 33.6k | FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX) { |
115 | 14 | g_set_error( |
116 | 14 | error, |
117 | 14 | FWUPD_ERROR, |
118 | 14 | FWUPD_ERROR_INVALID_DATA, |
119 | 14 | "table invalid @0x%x, too many duplicate %s %s:0x%x tokens", |
120 | 14 | (guint)offset, |
121 | 14 | fu_hid_item_kind_to_string(fu_hid_report_item_get_kind(item)), |
122 | 14 | fu_firmware_get_id(FU_FIRMWARE(item)), |
123 | 14 | fu_hid_report_item_get_value(item)); |
124 | 14 | return FALSE; |
125 | 14 | } |
126 | 33.6k | g_ptr_array_add(table_local, g_object_ref(item)); |
127 | 33.6k | } |
128 | | |
129 | | /* add report */ |
130 | 148k | if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) { |
131 | 3.77k | g_autoptr(FuHidReport) report = fu_hid_report_new(); |
132 | | |
133 | | /* copy the table state to the new report */ |
134 | 181k | for (guint i = 0; i < table_state->len; i++) { |
135 | 178k | FuHidReportItem *item_tmp = g_ptr_array_index(table_state, i); |
136 | 178k | if (!fu_firmware_add_image(FU_FIRMWARE(report), |
137 | 178k | FU_FIRMWARE(item_tmp), |
138 | 178k | error)) |
139 | 0 | return FALSE; |
140 | 178k | } |
141 | 33.7k | for (guint i = 0; i < table_local->len; i++) { |
142 | 29.9k | FuHidReportItem *item_tmp = g_ptr_array_index(table_local, i); |
143 | 29.9k | if (!fu_firmware_add_image(FU_FIRMWARE(report), |
144 | 29.9k | FU_FIRMWARE(item_tmp), |
145 | 29.9k | error)) |
146 | 0 | return FALSE; |
147 | 29.9k | } |
148 | 3.77k | if (!fu_firmware_add_image(firmware, FU_FIRMWARE(report), error)) |
149 | 88 | return FALSE; |
150 | | |
151 | | /* remove all the local items */ |
152 | 3.68k | g_ptr_array_set_size(table_local, 0); |
153 | 3.68k | } |
154 | 148k | } |
155 | | |
156 | | /* success */ |
157 | 963 | return TRUE; |
158 | 1.20k | } |
159 | | |
160 | | static gboolean |
161 | | fu_hid_descriptor_write_report_item(FuFirmware *report_item, |
162 | | GByteArray *buf, |
163 | | GHashTable *globals, |
164 | | GError **error) |
165 | 13.7k | { |
166 | 13.7k | g_autoptr(GBytes) fw = NULL; |
167 | | |
168 | | /* dedupe any globals */ |
169 | 13.7k | if (fu_hid_report_item_get_kind(FU_HID_REPORT_ITEM(report_item)) == |
170 | 13.7k | FU_HID_ITEM_KIND_GLOBAL) { |
171 | 9.76k | guint8 tag = fu_firmware_get_idx(report_item); |
172 | 9.76k | FuFirmware *report_item_tmp = g_hash_table_lookup(globals, GUINT_TO_POINTER(tag)); |
173 | 9.76k | if (report_item_tmp != NULL && |
174 | 7.79k | fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item)) == |
175 | 7.79k | fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item_tmp))) { |
176 | 7.21k | g_debug("skipping duplicate global tag 0x%x", tag); |
177 | 7.21k | return TRUE; |
178 | 7.21k | } |
179 | 2.55k | g_hash_table_insert(globals, GUINT_TO_POINTER(tag), report_item); |
180 | 2.55k | } |
181 | 6.51k | fw = fu_firmware_write(report_item, error); |
182 | 6.51k | if (fw == NULL) |
183 | 0 | return FALSE; |
184 | 6.51k | fu_byte_array_append_bytes(buf, fw); |
185 | | |
186 | | /* success */ |
187 | 6.51k | return TRUE; |
188 | 6.51k | } |
189 | | |
190 | | static gboolean |
191 | | fu_hid_descriptor_write_report(FuFirmware *report, |
192 | | GByteArray *buf, |
193 | | GHashTable *globals, |
194 | | GError **error) |
195 | 2.48k | { |
196 | 2.48k | g_autoptr(GPtrArray) report_items = fu_firmware_get_images(report); |
197 | | |
198 | | /* for each item */ |
199 | 16.2k | for (guint i = 0; i < report_items->len; i++) { |
200 | 13.7k | FuFirmware *report_item = g_ptr_array_index(report_items, i); |
201 | 13.7k | if (!fu_hid_descriptor_write_report_item(report_item, buf, globals, error)) |
202 | 0 | return FALSE; |
203 | 13.7k | } |
204 | | |
205 | | /* success */ |
206 | 2.48k | return TRUE; |
207 | 2.48k | } |
208 | | |
209 | | static GByteArray * |
210 | | fu_hid_descriptor_write(FuFirmware *firmware, GError **error) |
211 | 963 | { |
212 | 963 | g_autoptr(GByteArray) buf = g_byte_array_new(); |
213 | 963 | g_autoptr(GHashTable) globals = g_hash_table_new(g_direct_hash, g_direct_equal); |
214 | 963 | g_autoptr(GPtrArray) reports = fu_firmware_get_images(firmware); |
215 | | |
216 | | /* for each report */ |
217 | 3.44k | for (guint i = 0; i < reports->len; i++) { |
218 | 2.48k | FuFirmware *report = g_ptr_array_index(reports, i); |
219 | 2.48k | if (!fu_hid_descriptor_write_report(report, buf, globals, error)) |
220 | 0 | return NULL; |
221 | 2.48k | } |
222 | | |
223 | | /* success */ |
224 | 963 | return g_steal_pointer(&buf); |
225 | 963 | } |
226 | | |
227 | | typedef struct { |
228 | | const gchar *id; |
229 | | guint32 value; |
230 | | } FuHidDescriptorCondition; |
231 | | |
232 | | /** |
233 | | * fu_hid_descriptor_find_report: |
234 | | * @self: a #FuHidDescriptor |
235 | | * @error: (nullable): optional return location for an error |
236 | | * @...: pairs of string-integer values, ending with %NULL |
237 | | * |
238 | | * Finds the first HID report that matches all the report attributes. |
239 | | * |
240 | | * Returns: (transfer full): A #FuHidReport, or %NULL if not found. |
241 | | * |
242 | | * Since: 1.9.4 |
243 | | **/ |
244 | | FuHidReport * |
245 | | fu_hid_descriptor_find_report(FuHidDescriptor *self, GError **error, ...) |
246 | 0 | { |
247 | 0 | va_list args; |
248 | 0 | g_autoptr(GPtrArray) conditions = g_ptr_array_new_with_free_func(g_free); |
249 | 0 | g_autoptr(GPtrArray) reports = fu_firmware_get_images(FU_FIRMWARE(self)); |
250 | |
|
251 | 0 | g_return_val_if_fail(FU_IS_HID_DESCRIPTOR(self), NULL); |
252 | 0 | g_return_val_if_fail(error == NULL || *error == NULL, NULL); |
253 | | |
254 | | /* parse varargs */ |
255 | 0 | va_start(args, error); |
256 | 0 | for (guint i = 0; i < 1000; i++) { |
257 | 0 | g_autofree FuHidDescriptorCondition *cond = g_new0(FuHidDescriptorCondition, 1); |
258 | 0 | cond->id = va_arg(args, const gchar *); |
259 | 0 | if (cond->id == NULL) |
260 | 0 | break; |
261 | 0 | cond->value = va_arg(args, guint32); |
262 | 0 | g_ptr_array_add(conditions, g_steal_pointer(&cond)); |
263 | 0 | } |
264 | 0 | va_end(args); |
265 | | |
266 | | /* return the first report that matches *all* conditions */ |
267 | 0 | for (guint i = 0; i < reports->len; i++) { |
268 | 0 | FuHidReport *report = g_ptr_array_index(reports, i); |
269 | 0 | gboolean matched = TRUE; |
270 | 0 | for (guint j = 0; j < conditions->len; j++) { |
271 | 0 | FuHidDescriptorCondition *cond = g_ptr_array_index(conditions, j); |
272 | 0 | g_autoptr(FuFirmware) item = |
273 | 0 | fu_firmware_get_image_by_id(FU_FIRMWARE(report), cond->id, NULL); |
274 | 0 | if (item == NULL) { |
275 | 0 | matched = FALSE; |
276 | 0 | break; |
277 | 0 | } |
278 | 0 | if (fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item)) != cond->value) { |
279 | 0 | matched = FALSE; |
280 | 0 | break; |
281 | 0 | } |
282 | 0 | } |
283 | 0 | if (matched) |
284 | 0 | return g_object_ref(report); |
285 | 0 | } |
286 | 0 | g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no report found"); |
287 | 0 | return NULL; |
288 | 0 | } |
289 | | |
290 | | static void |
291 | | fu_hid_descriptor_init(FuHidDescriptor *self) |
292 | 1.44k | { |
293 | 1.44k | fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); |
294 | 1.44k | fu_firmware_set_size_max(FU_FIRMWARE(self), 64 * FU_KB); |
295 | 1.44k | #ifdef HAVE_FUZZER |
296 | 1.44k | fu_firmware_set_images_max(FU_FIRMWARE(self), 10); |
297 | | #else |
298 | | fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); |
299 | | #endif |
300 | 1.44k | fu_firmware_add_image_gtype(FU_FIRMWARE(self), FU_TYPE_HID_REPORT); |
301 | 1.44k | } |
302 | | |
303 | | static void |
304 | | fu_hid_descriptor_class_init(FuHidDescriptorClass *klass) |
305 | 1 | { |
306 | 1 | FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); |
307 | 1 | firmware_class->parse = fu_hid_descriptor_parse; |
308 | 1 | firmware_class->write = fu_hid_descriptor_write; |
309 | 1 | } |
310 | | |
311 | | /** |
312 | | * fu_hid_descriptor_new: |
313 | | * |
314 | | * Creates a new #FuFirmware to parse a HID descriptor |
315 | | * |
316 | | * Since: 1.9.4 |
317 | | **/ |
318 | | FuFirmware * |
319 | | fu_hid_descriptor_new(void) |
320 | 0 | { |
321 | 0 | return FU_FIRMWARE(g_object_new(FU_TYPE_HID_DESCRIPTOR, NULL)); |
322 | 0 | } |