/src/wireshark/wiretap/peak-trc.c
Line | Count | Source |
1 | | /* peak_trc.c |
2 | | * |
3 | | * Wiretap Library |
4 | | * Copyright (c) 1998 by Gilbert Ramirez <gram@alumni.rice.edu> |
5 | | * |
6 | | * Support for TRC log file format generated by several CAN tools from PEAK Gmbh |
7 | | * Copyright (c) 2024 by Miklos Marton <martonmiklosqdev@gmail.com> |
8 | | * |
9 | | * File format description: |
10 | | * https://www.peak-system.com/produktcd/Pdf/English/PEAK_CAN_TRC_File_Format.pdf |
11 | | * |
12 | | * SPDX-License-Identifier: GPL-2.0-or-later |
13 | | */ |
14 | | |
15 | | #include "config.h" |
16 | 0 | #define WS_LOG_DOMAIN "peak-trc" |
17 | | #include <wireshark.h> |
18 | | #include <file_wrappers.h> |
19 | | #include <epan/dissectors/packet-socketcan.h> |
20 | | #include <wctype.h> |
21 | | #include <string.h> |
22 | | #include <stdlib.h> |
23 | | #include <errno.h> |
24 | | #include <time.h> |
25 | | #include "peak-trc.h" |
26 | | #include <wsutil/strtoi.h> |
27 | | #include "socketcan.h" |
28 | | |
29 | 0 | #define PEAK_TRC_MAX_LINE_SIZE 4096 // J1939 logs could contain long lines |
30 | | |
31 | | typedef enum { |
32 | | FileVersion, |
33 | | StartTime, |
34 | | ColumnHeader, |
35 | | Comment, |
36 | | Data |
37 | | } peak_trc_parse_state_t; |
38 | | |
39 | | typedef enum { |
40 | | Col_BusNumber, // 0 |
41 | | Col_BusNumber_v1, // 1 |
42 | | Col_Direction, // 2 |
43 | | Col_CanId, // 3 |
44 | | Col_DLC_l, // 4 l (lowercase āLā) Data Length(0 - 64) |
45 | | Col_DLC_L, // 5 L Data Length Code (CAN: 0..8; CAN FD: 0..15) |
46 | | Col_MessageNumber, // 6 |
47 | | Col_TimeOffset_v1_0, // 7 |
48 | | Col_TimeOffset, // 8 |
49 | | Col_Reserved, // 9 |
50 | | Col_MessageType, // 10 |
51 | | Col_ErrorOrRtr_v1p0, // 11 |
52 | | // keep it to the last one to get the |
53 | | Col_Data_v1_0, // 12 |
54 | | Col_Data, // 13 |
55 | | Col_Invalid // 14 |
56 | | } peak_trc_column_type_t; |
57 | | |
58 | | typedef struct { |
59 | | peak_trc_column_type_t type; |
60 | | char code; |
61 | | char* regex; |
62 | | } peak_trc_column_map_t; |
63 | | |
64 | | typedef struct { |
65 | | time_t trace_start; |
66 | | time_t trace_start_nsec; |
67 | | |
68 | | uint8_t column_positions[Col_Invalid]; |
69 | | |
70 | | uint8_t file_version_major, file_version_minor; |
71 | | GRegex* data_matcher; |
72 | | } peak_trc_state_t; |
73 | | |
74 | | static int peak_trc_file_type_subtype = -1; |
75 | | |
76 | | static const peak_trc_column_map_t colmap[] = { |
77 | | {Col_BusNumber, 'B', "\\s*(([0-9]*)|-)"}, |
78 | | /* This isn't a real column type, so give it a "type" that shouldn't be used */ |
79 | | {Col_BusNumber_v1, 1, "\\s*([0-9]*)"}, |
80 | | {Col_Direction, 'd', "\\s*(Rx|Tx|Warng|Error)"}, |
81 | | /* This isn't a real column type, so give it a "type" that shouldn't be used */ |
82 | | {Col_Data_v1_0, 2, "\\s?(((--|[0-9A-F]{2})\\s?)*)"}, |
83 | | {Col_Data, 'D', "\\s?((((--|[0-9A-F]{2})\\s?))*)"}, |
84 | | {Col_CanId, 'I', "\\s*([0-9A-F]*)"}, |
85 | | {Col_DLC_l, 'l', "\\s*([0-9]*)"}, |
86 | | {Col_DLC_L, 'L', "\\s*([0-9]*)"}, /* https://www.peak-system.com/documentation/API/PCAN-Basic.Net/html/3ac7b0e3-f9df-c30e-d4fe-bcb63e5295bb.htm */ |
87 | | {Col_MessageNumber, 'N', "\\s*([0-9]*)\\)"}, |
88 | | {Col_TimeOffset, 'O', "\\s*([0-9]*\\.[0-9]*)"}, |
89 | | /* This isn't a real column type, so give it a "type" that shouldn't be used */ |
90 | | {Col_TimeOffset_v1_0, 3, "\\s*([0-9]*)"}, |
91 | | {Col_Reserved, 'R', "\\s*(-)"}, |
92 | | {Col_MessageType, 'T', "\\s*([A-Z]{2})"}, |
93 | | /* This isn't a real column, so give it a "type" that shouldn't be used */ |
94 | | {Col_ErrorOrRtr_v1p0, 4, "\\s*(ERROR|RTR)?"}, |
95 | | {Col_Invalid, 0, ""}, |
96 | | }; |
97 | | |
98 | | void register_peak_trc(void); |
99 | | |
100 | | static wtap_open_return_val |
101 | | peak_trc_parse(wtap* wth, peak_trc_state_t* state, int64_t* offset, int* err, char** err_info) |
102 | 0 | { |
103 | 0 | int64_t seek_off = 0; |
104 | 0 | bool found_timestamp = false; |
105 | 0 | char line_buffer[PEAK_TRC_MAX_LINE_SIZE]; |
106 | | |
107 | | /* 1.0 file format does not have version info or column line, |
108 | | * so assume that until told otherwise |
109 | | */ |
110 | 0 | int major = 1; |
111 | 0 | int minor = 0; |
112 | |
|
113 | 0 | ws_debug("%s: Trying peak_trc file decoder\n", G_STRFUNC); |
114 | | |
115 | | /* Initial start time until we find it in the header */ |
116 | 0 | state->trace_start = time(NULL); |
117 | 0 | state->trace_start_nsec = 0; |
118 | |
|
119 | 0 | state->file_version_major = 1; |
120 | 0 | state->file_version_minor = 0; |
121 | | |
122 | | /* Initialize columns to "invalid" */ |
123 | 0 | for (int i = 0; i < Col_Invalid; i++) |
124 | 0 | state->column_positions[i] = Col_Invalid; |
125 | | |
126 | | /* Bail if the end of the file is here and it hasn't been determined it's a PEAK file */ |
127 | 0 | while (!file_eof(wth->fh)) |
128 | 0 | { |
129 | 0 | seek_off = file_tell(wth->fh); |
130 | |
|
131 | 0 | ws_debug("%s: Starting parser at offset %" PRIi64 "\n", G_STRFUNC, seek_off); |
132 | |
|
133 | 0 | if (file_gets(line_buffer, PEAK_TRC_MAX_LINE_SIZE, wth->fh) == NULL) |
134 | 0 | { |
135 | | /* Error reading file, bail out */ |
136 | 0 | *err = file_error(wth->fh, err_info); |
137 | 0 | if (*err != 0 && *err != WTAP_ERR_SHORT_READ) |
138 | 0 | return WTAP_OPEN_ERROR; |
139 | | |
140 | 0 | return WTAP_OPEN_NOT_MINE; |
141 | 0 | } |
142 | | |
143 | 0 | g_strstrip(line_buffer); |
144 | 0 | size_t line_len = strlen(line_buffer); |
145 | | |
146 | | /* Ignore empty lines */ |
147 | 0 | if (line_len == 0) |
148 | 0 | continue; |
149 | | |
150 | | /* The intention here is to get through all of the "header" which consists of |
151 | | * "commented" lines (notated with a ;) |
152 | | * Once the header is complete, parsing can be confident it's a PEAK file |
153 | | */ |
154 | 0 | if (line_buffer[0] != ';') |
155 | 0 | { |
156 | | /* Two possibilities: |
157 | | * 1. This is the start of the actual packet data and header parsing is complete |
158 | | * 2. This isn't a PEAK file |
159 | | * |
160 | | * Determine this by if the timestamp has been found |
161 | | */ |
162 | 0 | if (!found_timestamp) |
163 | 0 | return WTAP_OPEN_NOT_MINE; |
164 | | |
165 | | /* Valid PEAK file */ |
166 | 0 | *offset = seek_off; |
167 | | |
168 | | /* Save off the data found in the header */ |
169 | 0 | state->file_version_major = (uint8_t)major; |
170 | 0 | state->file_version_minor = (uint8_t)minor; |
171 | | |
172 | | /* File version 1.x doesn't explicitly provide column order information */ |
173 | 0 | if (state->file_version_major == 1) |
174 | 0 | { |
175 | 0 | state->column_positions[Col_MessageNumber] = 1; |
176 | |
|
177 | 0 | switch(state->file_version_minor) |
178 | 0 | { |
179 | 0 | case 0: |
180 | 0 | state->column_positions[Col_TimeOffset_v1_0] = 2; |
181 | 0 | state->column_positions[Col_CanId] = 3; |
182 | 0 | state->column_positions[Col_DLC_l] = 4; |
183 | 0 | state->column_positions[Col_ErrorOrRtr_v1p0] = 5; |
184 | 0 | state->column_positions[Col_Data_v1_0] = 6; |
185 | 0 | break; |
186 | 0 | case 1: |
187 | 0 | state->column_positions[Col_TimeOffset] = 2; |
188 | 0 | state->column_positions[Col_Direction] = 3; |
189 | 0 | state->column_positions[Col_CanId] = 4; |
190 | 0 | state->column_positions[Col_DLC_l] = 5; |
191 | 0 | state->column_positions[Col_ErrorOrRtr_v1p0] = 6; |
192 | 0 | state->column_positions[Col_Data] = 7; |
193 | 0 | break; |
194 | 0 | case 2: |
195 | 0 | state->column_positions[Col_TimeOffset] = 2; |
196 | 0 | state->column_positions[Col_BusNumber_v1] = 3; |
197 | 0 | state->column_positions[Col_Direction] = 4; |
198 | 0 | state->column_positions[Col_CanId] = 5; |
199 | 0 | state->column_positions[Col_DLC_l] = 6; |
200 | 0 | state->column_positions[Col_ErrorOrRtr_v1p0] = 7; |
201 | 0 | state->column_positions[Col_Data] = 8; |
202 | 0 | break; |
203 | 0 | default: |
204 | 0 | state->column_positions[Col_TimeOffset] = 2; |
205 | 0 | state->column_positions[Col_BusNumber_v1] = 3; |
206 | 0 | state->column_positions[Col_Direction] = 4; |
207 | 0 | state->column_positions[Col_CanId] = 5; |
208 | 0 | state->column_positions[Col_Reserved] = 6; |
209 | 0 | state->column_positions[Col_DLC_l] = 7; |
210 | 0 | state->column_positions[Col_ErrorOrRtr_v1p0] = 8; |
211 | 0 | state->column_positions[Col_Data] = 9; |
212 | 0 | break; |
213 | 0 | } |
214 | | |
215 | 0 | GString* regex_str = g_string_new(NULL); |
216 | 0 | for (int column_index = 0; column_index < Col_Invalid; column_index++) |
217 | 0 | { |
218 | 0 | for (int col_type = Col_BusNumber; col_type < Col_Invalid; col_type++) |
219 | 0 | { |
220 | 0 | if (state->column_positions[col_type] == column_index) |
221 | 0 | { |
222 | 0 | for (const peak_trc_column_map_t * item = colmap; item->type != Col_Invalid; item++) |
223 | 0 | { |
224 | 0 | if ((int)item->type == col_type) |
225 | 0 | { |
226 | 0 | g_string_append(regex_str, item->regex); |
227 | 0 | break; |
228 | 0 | } |
229 | 0 | } |
230 | 0 | } |
231 | 0 | } |
232 | 0 | } |
233 | |
|
234 | 0 | state->data_matcher = g_regex_new(regex_str->str, (GRegexCompileFlags)(G_REGEX_OPTIMIZE | G_REGEX_RAW), (GRegexMatchFlags)0, NULL); |
235 | 0 | g_string_free(regex_str, TRUE); |
236 | 0 | } |
237 | | |
238 | 0 | return WTAP_OPEN_MINE; |
239 | 0 | } |
240 | | |
241 | 0 | if (g_str_has_prefix(line_buffer, ";$FILEVERSION=")) |
242 | 0 | { |
243 | | /* Parse ";$FILEVERSION=major.minor" */ |
244 | 0 | const char *p = line_buffer + strlen(";$FILEVERSION="); |
245 | 0 | const char *end; |
246 | 0 | if (!ws_strtoi(p, &end, &major) || *end != '.') |
247 | 0 | return WTAP_OPEN_NOT_MINE; |
248 | 0 | p = end + 1; |
249 | 0 | if (!ws_strtoi(p, NULL, &minor)) |
250 | 0 | return WTAP_OPEN_NOT_MINE; |
251 | 0 | } |
252 | 0 | else if (g_str_has_prefix(line_buffer, ";$STARTTIME=")) |
253 | 0 | { |
254 | | /* $STARTTIME keyword to store the absolute start time of the trace file: |
255 | | Format: Floating point, decimal separator is a point. |
256 | | Value: Integral part = Number of days that have passed since 30. December 1899. |
257 | | Fractional Part = Fraction of a 24-hour day that has elapsed, resolution is 1 millisecond. */ |
258 | |
|
259 | 0 | char** parts = g_strsplit(line_buffer, "=", 2); |
260 | 0 | if (parts[1] == NULL) |
261 | 0 | { |
262 | | /* At this point, the file is probably a PEAK file that is poorly formatted, so bail with an error */ |
263 | 0 | *err_info = ws_strdup("peak_trc: unable to parse start time"); |
264 | 0 | g_strfreev(parts); |
265 | 0 | return WTAP_OPEN_ERROR; |
266 | 0 | } |
267 | | |
268 | | /* Convert to a more "standard" timestamp */ |
269 | 0 | double days_since_epoch = g_strtod(parts[1], NULL); |
270 | 0 | days_since_epoch -= 25569.0; |
271 | 0 | time_t ts = 0; |
272 | 0 | struct tm* start_tm = gmtime(&ts); |
273 | 0 | if (start_tm == NULL) // Extremely unlikely |
274 | 0 | { |
275 | 0 | *err_info = ws_strdup("peak_trc: unable to convert start time"); |
276 | 0 | g_strfreev(parts); |
277 | 0 | return WTAP_OPEN_ERROR; |
278 | 0 | } |
279 | 0 | start_tm->tm_sec += (int)(days_since_epoch * 24 * 3600); |
280 | 0 | state->trace_start = mktime(start_tm); |
281 | 0 | state->trace_start_nsec = 0; |
282 | |
|
283 | 0 | found_timestamp = true; |
284 | |
|
285 | 0 | g_strfreev(parts); |
286 | 0 | } |
287 | 0 | else if (g_str_has_prefix(line_buffer, ";$COLUMNS=")) |
288 | 0 | { |
289 | 0 | char** parts = g_strsplit(line_buffer, "=", 2); |
290 | 0 | if (parts[1] == NULL) |
291 | 0 | { |
292 | | /* At this point, the file is probably a PEAK file that is poorly formatted, so bail with an error */ |
293 | 0 | *err_info = ws_strdup("peak_trc: unable to parse column definitions"); |
294 | 0 | g_strfreev(parts); |
295 | 0 | return WTAP_OPEN_ERROR; |
296 | 0 | } |
297 | | |
298 | 0 | char** columns = g_strsplit(parts[1], ",", 0); |
299 | 0 | int column_index = 0; |
300 | 0 | for (char** iter = columns; *iter != NULL; iter++) |
301 | 0 | { |
302 | 0 | size_t col_length = strlen(*iter); |
303 | 0 | if (col_length > 1) { |
304 | 0 | *err_info = ws_strdup_printf("peak_trc: unknown column definition in the $COLUMNS line: '%s'", *iter); |
305 | 0 | g_strfreev(columns); |
306 | 0 | g_strfreev(parts); |
307 | 0 | return WTAP_OPEN_ERROR; |
308 | 0 | } |
309 | 0 | if (col_length == 0) |
310 | 0 | { |
311 | | //Done ? |
312 | 0 | break; |
313 | 0 | } |
314 | | |
315 | | /* Assign the column value if found */ |
316 | 0 | for (const peak_trc_column_map_t* item = colmap; item->type != Col_Invalid; item++) |
317 | 0 | { |
318 | 0 | if ((*iter)[0] == item->code) |
319 | 0 | { |
320 | | // column type matched |
321 | 0 | state->column_positions[item->type] = column_index; |
322 | 0 | break; |
323 | 0 | } |
324 | 0 | } |
325 | 0 | column_index++; |
326 | 0 | } |
327 | | |
328 | 0 | g_strfreev(columns); |
329 | 0 | g_strfreev(parts); |
330 | 0 | } |
331 | 0 | else if (line_len >= 2) |
332 | 0 | { |
333 | | /* The rest of the "keywords" are separated by whitespace after the initial ';' */ |
334 | 0 | char* keyword = &line_buffer[1]; |
335 | 0 | while (g_ascii_isspace(*keyword)) |
336 | 0 | keyword++; |
337 | |
|
338 | 0 | if (g_str_has_prefix(keyword, "Start time:")) |
339 | 0 | { |
340 | | /* Use the $STARTTIME if it has already been found */ |
341 | 0 | if (!found_timestamp) |
342 | 0 | { |
343 | 0 | int day, month, year, hour, minute, second, millisecond; |
344 | | /* Parse "Start time: month.day.year hour:minute:second.millisecond" */ |
345 | 0 | { |
346 | 0 | const char *p = keyword + strlen("Start time: "); |
347 | 0 | const char *end; |
348 | 0 | bool ok = ws_strtoi(p, &end, &month) && *end == '.' && |
349 | 0 | ws_strtoi(end+1, &end, &day) && *end == '.' && |
350 | 0 | ws_strtoi(end+1, &end, &year) && *end == ' ' && |
351 | 0 | ws_strtoi(end+1, &end, &hour) && *end == ':' && |
352 | 0 | ws_strtoi(end+1, &end, &minute) && *end == ':' && |
353 | 0 | ws_strtoi(end+1, &end, &second) && *end == '.' && |
354 | 0 | ws_strtoi(end+1, NULL, &millisecond); |
355 | 0 | if (!ok) |
356 | 0 | { |
357 | 0 | *err_info = ws_strdup("peak_trc: unable to parse start time"); |
358 | 0 | return WTAP_OPEN_ERROR; |
359 | 0 | } |
360 | 0 | } |
361 | | |
362 | 0 | struct tm t; |
363 | 0 | t.tm_sec = second; |
364 | 0 | t.tm_min = minute; |
365 | 0 | t.tm_hour = hour; |
366 | 0 | t.tm_mday = day; |
367 | 0 | t.tm_mon = month-1; |
368 | 0 | t.tm_year = year-1900; |
369 | 0 | t.tm_wday = 0; |
370 | 0 | t.tm_yday = 0; |
371 | 0 | t.tm_isdst = -1; |
372 | |
|
373 | 0 | state->trace_start = mktime(&t); |
374 | 0 | state->trace_start_nsec = millisecond*1000000; |
375 | |
|
376 | 0 | found_timestamp = true; |
377 | 0 | } |
378 | 0 | } |
379 | 0 | } |
380 | 0 | } |
381 | | |
382 | 0 | return WTAP_OPEN_NOT_MINE; |
383 | 0 | } |
384 | | |
385 | | static bool peak_trc_read_packet_v1(wtap* wth, peak_trc_state_t* state, wtap_can_msg_t* peak_msg, char* line_buffer) |
386 | 0 | { |
387 | | // pre 2.0 packets use regexp with fixed column order with a regexp |
388 | 0 | GMatchInfo* match_info = NULL; |
389 | 0 | bool found = g_regex_match(state->data_matcher, line_buffer, 0, &match_info); |
390 | 0 | int column_count = g_match_info_get_match_count(match_info); |
391 | 0 | if (!found || column_count < 5) |
392 | 0 | { |
393 | | /* Not a valid v1 packet */ |
394 | 0 | g_match_info_free(match_info); |
395 | 0 | return false; |
396 | 0 | } |
397 | | |
398 | 0 | for (int col = Col_BusNumber; col < Col_Invalid; col++) |
399 | 0 | { |
400 | 0 | if (state->column_positions[col] >= column_count || state->column_positions[col] == Col_Invalid) |
401 | 0 | continue; |
402 | | |
403 | 0 | char* column_text = g_match_info_fetch(match_info, state->column_positions[col]); |
404 | 0 | if (column_text == NULL) |
405 | 0 | { |
406 | 0 | g_match_info_free(match_info); |
407 | 0 | return false; |
408 | 0 | } |
409 | | |
410 | 0 | switch (col) |
411 | 0 | { |
412 | 0 | case Col_Direction: |
413 | 0 | if (strcmp(column_text, "Warng") == 0) |
414 | 0 | { |
415 | | // Warning lines cannot be interpreted in Wireshark needs to be skipped |
416 | 0 | g_free(column_text); |
417 | 0 | g_match_info_free(match_info); |
418 | 0 | return false; |
419 | 0 | } |
420 | 0 | if (strcmp(column_text, "Error") == 0) |
421 | 0 | { |
422 | 0 | peak_msg->type = MSG_TYPE_ERR; |
423 | 0 | } |
424 | 0 | break; |
425 | 0 | case Col_CanId: |
426 | 0 | peak_msg->id = (uint32_t)g_ascii_strtoull(column_text, NULL, 16); |
427 | 0 | break; |
428 | 0 | case Col_ErrorOrRtr_v1p0: |
429 | 0 | if (state->file_version_minor == 1 && strcmp(column_text, "RTR") == 0) |
430 | 0 | { |
431 | 0 | peak_msg->type = MSG_TYPE_STD_RTR; |
432 | 0 | } |
433 | 0 | break; |
434 | 0 | case Col_TimeOffset: |
435 | 0 | case Col_TimeOffset_v1_0: |
436 | 0 | { |
437 | 0 | double tsd = g_ascii_strtod(column_text, NULL) / 1000.0; // parse to seconds |
438 | 0 | tsd += state->trace_start; |
439 | 0 | peak_msg->ts.secs = (uint64_t)tsd; |
440 | 0 | peak_msg->ts.nsecs = (int)((tsd - (uint64_t)tsd) * 1000000000); |
441 | 0 | } |
442 | 0 | break; |
443 | 0 | case Col_DLC_l: |
444 | 0 | peak_msg->data.length = (uint8_t)g_ascii_strtoull(column_text, NULL, 10); |
445 | 0 | break; |
446 | 0 | case Col_BusNumber_v1: |
447 | 0 | { |
448 | 0 | char* interface_name = wmem_strdup_printf(NULL, "interface%s", column_text); |
449 | 0 | peak_msg->interface_id = wtap_socketcan_find_or_create_new_interface(wth, interface_name); |
450 | 0 | wmem_free(NULL, interface_name); |
451 | 0 | break; |
452 | 0 | } |
453 | | |
454 | 0 | case Col_Data: |
455 | 0 | case Col_Data_v1_0: |
456 | 0 | { |
457 | 0 | char* err_or_rtr = g_match_info_fetch(match_info, state->column_positions[Col_ErrorOrRtr_v1p0]); |
458 | 0 | char** bytes = g_strsplit(g_strstrip(column_text), " ", CAN_MAX_DLEN); |
459 | | // 1.0 format has an ERROR prefix in the data bytes, 1.1 has a separate columns for message type |
460 | 0 | if ((g_str_has_prefix(err_or_rtr, "ERROR")) || |
461 | 0 | (state->file_version_minor == 1 && peak_msg->type == MSG_TYPE_ERR)) |
462 | 0 | { |
463 | 0 | peak_msg->type = MSG_TYPE_ERR; |
464 | | |
465 | | /* For LINUX_CAN_ERR the length has to be CAN_MAX_DLEN */ |
466 | 0 | peak_msg->data.length = CAN_MAX_DLEN; |
467 | | /* Clear the data of the message to populate it with SocketCAN meta data */ |
468 | 0 | memset(peak_msg->data.data, 0, CAN_MAX_DLEN); |
469 | |
|
470 | 0 | uint32_t old_id = peak_msg->id; |
471 | 0 | peak_msg->id = 0; |
472 | | // ID: Type of Error Frame |
473 | 0 | switch (old_id) |
474 | 0 | { |
475 | 0 | case 1: // 1 = Bit Error |
476 | 0 | peak_msg->id |= CAN_ERR_PROT_BIT; |
477 | 0 | break; |
478 | 0 | case 2: // 2 = Form Error |
479 | 0 | peak_msg->id |= CAN_ERR_PROT_FORM; |
480 | 0 | break; |
481 | 0 | case 4: // 4 = Stuff Error |
482 | 0 | peak_msg->id |= CAN_ERR_PROT_STUFF; |
483 | 0 | break; |
484 | 0 | } |
485 | | |
486 | | // There should be 4 bytes of data in an error message. |
487 | | // The first two have information that can become metadata |
488 | | // XXX - Should we reject the message entirely if there's fewer |
489 | | // than 2, or 4, bytes of data? |
490 | | //Translate PEAK error data into SocketCAN meta data for supported error types |
491 | 0 | if (bytes && g_strv_length(bytes) >= 2 && (peak_msg->id | (CAN_ERR_PROT_FORM& CAN_ERR_PROT_STUFF)) != 0) |
492 | 0 | { |
493 | | // Data Byte 0: Direction |
494 | 0 | uint8_t byte0 = (uint8_t)g_ascii_strtoull(bytes[0], NULL, 16); |
495 | 0 | if (byte0 < 2) |
496 | 0 | { |
497 | | //0 = Error occurred while sending |
498 | | //1 = Error occurred while receiving. |
499 | 0 | peak_msg->data.data[2] |= CAN_ERR_PROT_TX; |
500 | 0 | } |
501 | | |
502 | | // Data Byte 1: Current Position in Bit Stream |
503 | 0 | uint8_t bit_pos = (uint8_t)g_ascii_strtoull(bytes[1], NULL, 16); |
504 | 0 | if (bit_pos == 28) |
505 | 0 | { |
506 | 0 | peak_msg->data.data[2] |= CAN_ERR_PROT_OVERLOAD; |
507 | 0 | } |
508 | 0 | else |
509 | 0 | { |
510 | | // magically SocketCAN and PEAK error bit positions are identical with some exceptions |
511 | 0 | peak_msg->data.data[3] = bit_pos; |
512 | 0 | if (peak_msg->data.data[3] == 23) |
513 | 0 | peak_msg->data.data[3] = 0; // Error position not present in SocketCAN |
514 | 0 | } |
515 | 0 | } |
516 | 0 | } |
517 | 0 | else if (g_str_has_prefix(err_or_rtr, "RTR") || (state->file_version_minor == 1 && peak_msg->type == MSG_TYPE_STD_RTR)) |
518 | 0 | { |
519 | 0 | peak_msg->type = MSG_TYPE_STD_RTR; |
520 | 0 | peak_msg->data.length = 0; |
521 | 0 | } |
522 | 0 | else |
523 | 0 | { |
524 | 0 | peak_msg->type = MSG_TYPE_STD; |
525 | 0 | if (bytes) |
526 | 0 | { |
527 | 0 | for (int byte_i = 0; ((byte_i < CAN_MAX_DLEN) && (bytes[byte_i] != 0)); byte_i++) |
528 | 0 | peak_msg->data.data[byte_i] = (uint8_t)g_ascii_strtoull(bytes[byte_i], NULL, 16); |
529 | 0 | } |
530 | 0 | } |
531 | 0 | g_strfreev(bytes); |
532 | 0 | g_free(err_or_rtr); |
533 | 0 | break; |
534 | 0 | } |
535 | 0 | } |
536 | 0 | g_free(column_text); |
537 | 0 | } |
538 | | |
539 | | #ifdef WS_DEBUG |
540 | | for (int i = 0; i < column_count; i++) { |
541 | | char *match_text = g_match_info_fetch(match_info, i); |
542 | | ws_debug("%d: %s\n", i, match_text); |
543 | | g_free(match_text); |
544 | | } |
545 | | #endif |
546 | | |
547 | 0 | g_match_info_free(match_info); |
548 | 0 | return true; |
549 | 0 | } |
550 | | |
551 | | uint8_t |
552 | | dlc_to_data_length(uint8_t dlc) |
553 | 0 | { |
554 | 0 | if (dlc <= 8) |
555 | 0 | return dlc; |
556 | | |
557 | 0 | switch (dlc) |
558 | 0 | { |
559 | 0 | case 9: return 12; |
560 | 0 | case 10: return 16; |
561 | 0 | case 11: return 20; |
562 | 0 | case 12: return 24; |
563 | 0 | case 13: return 32; |
564 | 0 | case 14: return 48; |
565 | 0 | case 15: return 64; |
566 | 0 | default: return 0; |
567 | 0 | } |
568 | 0 | } |
569 | | |
570 | | static bool peak_trc_read_packet_v2(wtap* wth, peak_trc_state_t* state, wtap_can_msg_t* peak_msg, char* line_buffer) |
571 | 0 | { |
572 | 0 | int i = 0; |
573 | 0 | int column_i = 0; |
574 | 0 | int column_start = 0; |
575 | 0 | bool last_char_is_ws = true; |
576 | 0 | peak_trc_column_type_t current_column = Col_Invalid; |
577 | 0 | while (line_buffer[i] != 0) |
578 | 0 | { |
579 | 0 | bool is_whitespace = g_ascii_isspace(line_buffer[i]); |
580 | 0 | if (current_column == Col_Data) |
581 | 0 | { |
582 | | // data always the last and could contain spaces |
583 | 0 | is_whitespace = (line_buffer[i] == '\r' || line_buffer[i] == '\n'); |
584 | 0 | } |
585 | |
|
586 | 0 | if (is_whitespace) |
587 | 0 | { |
588 | 0 | if (!last_char_is_ws) |
589 | 0 | { |
590 | | // column closed -> process data |
591 | 0 | char* column_text = g_utf8_make_valid(&line_buffer[column_start], i - column_start); |
592 | |
|
593 | 0 | ws_debug("Column %d: %s", current_column, column_text); |
594 | |
|
595 | 0 | switch (current_column) { |
596 | 0 | case Col_BusNumber: |
597 | 0 | { |
598 | 0 | char* interface_name = wmem_strdup_printf(NULL, "interface%s", column_text); |
599 | 0 | peak_msg->interface_id = wtap_socketcan_find_or_create_new_interface(wth, interface_name); |
600 | 0 | wmem_free(NULL, interface_name); |
601 | 0 | break; |
602 | 0 | } |
603 | 0 | case Col_Direction: |
604 | 0 | break; |
605 | 0 | case Col_Data: { |
606 | 0 | if (peak_msg->type == MSG_TYPE_ERR) |
607 | 0 | { |
608 | | // FIXME fill data and msg id |
609 | 0 | } |
610 | 0 | else |
611 | 0 | { |
612 | 0 | char** bytes = g_strsplit(g_strstrip(column_text), " ", CANFD_MAX_DLEN); |
613 | 0 | int byte_i = 0; |
614 | 0 | while (byte_i < CANFD_MAX_DLEN) { |
615 | 0 | if (bytes[byte_i] == 0) |
616 | 0 | break; |
617 | 0 | peak_msg->data.data[byte_i] = (uint8_t)g_ascii_strtoull(bytes[byte_i], NULL, 16); |
618 | 0 | byte_i++; |
619 | 0 | } |
620 | 0 | g_strfreev(bytes); |
621 | 0 | } |
622 | 0 | break; |
623 | 0 | } |
624 | 0 | case Col_CanId: |
625 | 0 | peak_msg->id = (unsigned)g_ascii_strtoull(column_text, NULL, 16); |
626 | 0 | break; |
627 | 0 | case Col_DLC_l: |
628 | 0 | peak_msg->data.length = (unsigned)g_ascii_strtoull(column_text, NULL, 10); |
629 | 0 | break; |
630 | 0 | case Col_DLC_L: |
631 | 0 | peak_msg->data.length = dlc_to_data_length((unsigned)g_ascii_strtoull(column_text, NULL, 10)); |
632 | 0 | break; |
633 | 0 | case Col_TimeOffset: |
634 | 0 | { |
635 | 0 | double tsd = g_ascii_strtod(column_text, NULL) / 1000.0; // parse to seconds |
636 | 0 | tsd += state->trace_start; |
637 | 0 | peak_msg->ts.secs = (uint64_t)tsd; |
638 | 0 | peak_msg->ts.nsecs = (int)((tsd - (uint64_t)tsd) * 1000000000); |
639 | 0 | break; |
640 | 0 | } |
641 | 0 | case Col_MessageType: |
642 | 0 | if (strcmp(column_text, "DT") == 0) |
643 | 0 | { |
644 | 0 | peak_msg->type = MSG_TYPE_STD; |
645 | 0 | } |
646 | 0 | else if (strcmp(column_text, "FD") == 0) |
647 | 0 | { |
648 | 0 | peak_msg->type = MSG_TYPE_STD_FD; |
649 | 0 | } |
650 | 0 | else if (strcmp(column_text, "FB") == 0) |
651 | 0 | { |
652 | | // CAN FD data frame with BRS bit set (Bit Rate Switch). |
653 | 0 | peak_msg->type = MSG_TYPE_STD_FD; // TODO |
654 | 0 | } |
655 | 0 | else if (strcmp(column_text, "FE") == 0) |
656 | 0 | { |
657 | | // CAN FD data frame with ESI bit set (Error State Indicator). |
658 | 0 | peak_msg->type = MSG_TYPE_ERR; |
659 | 0 | } |
660 | 0 | else if (strcmp(column_text, "BI") == 0) |
661 | 0 | { |
662 | | // CAN FD data frame with both BRS and ESI bits set. |
663 | 0 | peak_msg->type = MSG_TYPE_ERR; |
664 | 0 | } |
665 | 0 | else if (strcmp(column_text, "RR") == 0) |
666 | 0 | { |
667 | 0 | peak_msg->type = MSG_TYPE_STD_RTR; |
668 | 0 | } |
669 | 0 | else if (strcmp(column_text, "ST") == 0) |
670 | 0 | { |
671 | | // TODO: Hardware Status change. |
672 | | // Currently not supported in Wireshark |
673 | 0 | g_free(column_text); |
674 | 0 | return false; |
675 | 0 | } |
676 | 0 | else if (strcmp(column_text, "ER") == 0) |
677 | 0 | { |
678 | 0 | peak_msg->type = MSG_TYPE_ERR; |
679 | 0 | } |
680 | 0 | else if (strcmp(column_text, "EC") == 0) |
681 | 0 | { |
682 | | // TODO: Error Counter change |
683 | | // Currently not supported in Wireshark |
684 | 0 | g_free(column_text); |
685 | 0 | return false; |
686 | 0 | } |
687 | 0 | else if (strcmp(column_text, "EV") == 0) |
688 | 0 | { |
689 | | // Event. User-defined text, begins directly after bus specifier |
690 | | // TODO add support for this event type |
691 | 0 | g_free(column_text); |
692 | 0 | return false; |
693 | 0 | } |
694 | 0 | break; |
695 | 0 | case Col_MessageNumber: |
696 | 0 | case Col_Reserved: |
697 | 0 | case Col_Invalid: |
698 | 0 | break; |
699 | | /* Version 1 column types ignored */ |
700 | 0 | case Col_ErrorOrRtr_v1p0: |
701 | 0 | case Col_BusNumber_v1: |
702 | 0 | case Col_TimeOffset_v1_0: |
703 | 0 | case Col_Data_v1_0: |
704 | 0 | break; |
705 | 0 | } |
706 | 0 | g_free(column_text); |
707 | 0 | } |
708 | 0 | } |
709 | 0 | else |
710 | 0 | { |
711 | | // non whitespace read |
712 | 0 | if (last_char_is_ws) |
713 | 0 | { |
714 | 0 | for (int lut_i = 0; lut_i < Col_Invalid; lut_i++) |
715 | 0 | { |
716 | 0 | if (state->column_positions[lut_i] == column_i) |
717 | 0 | { |
718 | 0 | current_column = lut_i; |
719 | 0 | break; |
720 | 0 | } |
721 | 0 | } |
722 | 0 | column_i++; |
723 | 0 | column_start = i; |
724 | 0 | } |
725 | 0 | } |
726 | 0 | last_char_is_ws = is_whitespace; |
727 | 0 | i++; |
728 | 0 | } |
729 | | |
730 | 0 | return true; |
731 | 0 | } |
732 | | |
733 | | static bool |
734 | | peak_trc_read_packet(wtap *wth, FILE_T fh, peak_trc_state_t* state, |
735 | | wtap_rec *rec, int *err, char **err_info, |
736 | | int64_t *data_offset) |
737 | 0 | { |
738 | 0 | wtap_can_msg_t peak_msg = {0}; |
739 | 0 | char line_buffer[PEAK_TRC_MAX_LINE_SIZE]; |
740 | |
|
741 | 0 | while (!file_eof(fh)) |
742 | 0 | { |
743 | 0 | if (data_offset) |
744 | 0 | *data_offset = file_tell(fh); |
745 | | |
746 | | /* Read a line */ |
747 | 0 | if (file_gets(line_buffer, PEAK_TRC_MAX_LINE_SIZE, fh) == NULL) |
748 | 0 | { |
749 | 0 | *err = file_error(fh, err_info); |
750 | 0 | return false; |
751 | 0 | } |
752 | | |
753 | | /* Ignore empty or commented lines */ |
754 | 0 | if ((strlen(line_buffer) == 0) || |
755 | 0 | (line_buffer[0] == ';')) |
756 | 0 | continue; |
757 | | |
758 | | //Start with unknown interface ID |
759 | 0 | peak_msg.interface_id = WTAP_SOCKETCAN_INVALID_INTERFACE_ID; |
760 | |
|
761 | 0 | if (state->file_version_major >= 2) |
762 | 0 | { |
763 | 0 | if (!peak_trc_read_packet_v2(wth, state, &peak_msg, line_buffer)) |
764 | 0 | continue; |
765 | 0 | } |
766 | 0 | else |
767 | 0 | { |
768 | 0 | if (!peak_trc_read_packet_v1(wth, state, &peak_msg, line_buffer)) |
769 | 0 | continue; |
770 | 0 | } |
771 | | |
772 | 0 | if (peak_msg.id > 0x7FF) |
773 | 0 | { |
774 | | //Translate from 11-bit to 32-bit IDs |
775 | 0 | switch (peak_msg.type) |
776 | 0 | { |
777 | 0 | case MSG_TYPE_STD_FD: |
778 | 0 | peak_msg.type = MSG_TYPE_EXT_FD; |
779 | 0 | break; |
780 | 0 | case MSG_TYPE_STD: |
781 | 0 | peak_msg.type = MSG_TYPE_EXT; |
782 | 0 | break; |
783 | 0 | case MSG_TYPE_STD_RTR: |
784 | 0 | peak_msg.type = MSG_TYPE_EXT_RTR; |
785 | 0 | break; |
786 | 0 | default: |
787 | | //Ignore other types |
788 | 0 | break; |
789 | 0 | } |
790 | 0 | } |
791 | | |
792 | 0 | return wtap_socketcan_gen_packet(wth, rec, &peak_msg, "peak", err, err_info); |
793 | 0 | } |
794 | | |
795 | 0 | return false; |
796 | 0 | } |
797 | | |
798 | | static bool |
799 | | peak_trc_read(wtap *wth, wtap_rec *rec, int *err, char **err_info, |
800 | | int64_t *data_offset) |
801 | 0 | { |
802 | 0 | peak_trc_state_t* state = (peak_trc_state_t*)wtap_socketcan_get_private_data(wth); |
803 | |
|
804 | 0 | ws_debug("%s: Try reading at offset %" PRIi64 "\n", G_STRFUNC, file_tell(wth->fh)); |
805 | |
|
806 | 0 | return peak_trc_read_packet(wth, wth->fh, state, rec, err, err_info, data_offset); |
807 | 0 | } |
808 | | |
809 | | static bool peak_trc_seek_read(wtap *wth, int64_t seek_off, wtap_rec *rec, int *err, char **err_info) |
810 | 0 | { |
811 | 0 | ws_debug("%s: Read at offset %" PRIi64 "\n", G_STRFUNC, seek_off); |
812 | |
|
813 | 0 | peak_trc_state_t* state = (peak_trc_state_t*)wtap_socketcan_get_private_data(wth); |
814 | |
|
815 | 0 | if (file_seek(wth->random_fh, seek_off, SEEK_SET, err) == -1) |
816 | 0 | { |
817 | 0 | *err = errno; |
818 | 0 | *err_info = g_strdup(g_strerror(errno)); |
819 | |
|
820 | 0 | return false; |
821 | 0 | } |
822 | | |
823 | 0 | return peak_trc_read_packet(wth, wth->random_fh, state, rec, err, err_info, NULL); |
824 | 0 | } |
825 | | |
826 | | static void |
827 | | clean_trc_state(peak_trc_state_t* state) |
828 | 0 | { |
829 | 0 | if (state != NULL) |
830 | 0 | { |
831 | 0 | if (state->data_matcher != NULL) |
832 | 0 | g_regex_unref(state->data_matcher); |
833 | |
|
834 | 0 | g_free(state); |
835 | 0 | } |
836 | 0 | } |
837 | | |
838 | | static void |
839 | | peak_trc_close(void* tap_data) |
840 | 0 | { |
841 | 0 | clean_trc_state((peak_trc_state_t*)tap_data); |
842 | 0 | } |
843 | | |
844 | | wtap_open_return_val |
845 | | peak_trc_open(wtap* wth, int* err, char** err_info) |
846 | 0 | { |
847 | 0 | int64_t data_offset = 0; |
848 | 0 | peak_trc_state_t* trc_state = g_new0(peak_trc_state_t, 1); |
849 | | |
850 | | /* wth->priv stores a pointer to the general file properties. |
851 | | * It it updated when the header data is parsed */ |
852 | 0 | wth->priv = trc_state; |
853 | |
|
854 | 0 | wtap_open_return_val open_val = peak_trc_parse(wth, trc_state, &data_offset, err, err_info); |
855 | 0 | if (open_val != WTAP_OPEN_MINE) |
856 | 0 | { |
857 | 0 | clean_trc_state(trc_state); |
858 | 0 | wth->priv = NULL; |
859 | 0 | return open_val; |
860 | 0 | } |
861 | | |
862 | 0 | ws_debug("%s: This is our file\n", G_STRFUNC); |
863 | | |
864 | | /* Go to the start of the real packet data since header is now done */ |
865 | 0 | if (file_seek(wth->fh, data_offset, SEEK_SET, err) == -1) |
866 | 0 | { |
867 | 0 | *err = errno; |
868 | 0 | *err_info = g_strdup(g_strerror(errno)); |
869 | 0 | return WTAP_OPEN_ERROR; |
870 | 0 | } |
871 | | |
872 | 0 | wtap_set_as_socketcan(wth, peak_trc_file_type_subtype, WTAP_TSPREC_USEC, trc_state, peak_trc_close); |
873 | 0 | wth->subtype_read = peak_trc_read; |
874 | 0 | wth->subtype_seek_read = peak_trc_seek_read; |
875 | |
|
876 | 0 | return WTAP_OPEN_MINE; |
877 | 0 | } |
878 | | |
879 | | static const struct supported_block_type peak_trc_blocks_supported[] = { |
880 | | /* |
881 | | * We support packet blocks, with no comments or other options. |
882 | | */ |
883 | | { WTAP_BLOCK_PACKET, MULTIPLE_BLOCKS_SUPPORTED, NO_OPTIONS_SUPPORTED } |
884 | | }; |
885 | | |
886 | | static const struct file_type_subtype_info peak_trc_info = { |
887 | | "PEAK CAN TRC file", "peak-trc", "trc", NULL, |
888 | | false, BLOCKS_SUPPORTED(peak_trc_blocks_supported), |
889 | | NULL, NULL, NULL |
890 | | }; |
891 | | |
892 | | void register_peak_trc(void) |
893 | 16 | { |
894 | 16 | peak_trc_file_type_subtype = wtap_register_file_type_subtype(&peak_trc_info); |
895 | 16 | } |
896 | | |
897 | | /* |
898 | | * Editor modelines - https://www.wireshark.org/tools/modelines.html |
899 | | * |
900 | | * Local variables: |
901 | | * c-basic-offset: 4 |
902 | | * tab-width: 8 |
903 | | * indent-tabs-mode: nil |
904 | | * End: |
905 | | * |
906 | | * vi: set shiftwidth=4 tabstop=8 expandtab: |
907 | | * :indentSize=4:tabSize=8:noTabs=true: |
908 | | */ |