1#[cfg(feature = "std")]
2use prettytable::{format, row, Table};
3use wireless_mbus_link_layer::WirelessFrame;
4
5use crate::user_data;
6use crate::MbusError;
7use wired_mbus_link_layer as frames;
8use wireless_mbus_link_layer;
9
10#[cfg_attr(
11 feature = "serde",
12 derive(serde::Serialize),
13 serde(bound(deserialize = "'de: 'a"))
14)]
15#[derive(Debug)]
16pub struct MbusData<'a, F> {
17 pub frame: F,
18 pub user_data: Option<user_data::UserDataBlock<'a>>,
19 pub data_records: Option<user_data::DataRecords<'a>>,
20}
21
22impl<'a> TryFrom<&'a [u8]> for MbusData<'a, frames::WiredFrame<'a>> {
23 type Error = MbusError;
24
25 fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
26 let frame = frames::WiredFrame::try_from(data)?;
27 let mut user_data = None;
28 let mut data_records = None;
29 match &frame {
30 frames::WiredFrame::LongFrame { data, .. } => {
31 if let Ok(x) = user_data::UserDataBlock::try_from(*data) {
32 match &x {
33 user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
34 variable_data_block,
35 ..
36 }
37 | user_data::UserDataBlock::VariableDataStructureWithShortTplHeader {
38 variable_data_block,
39 ..
40 }
41 | user_data::UserDataBlock::VariableDataStructureWithoutTplHeader {
42 variable_data_block,
43 ..
44 } => {
45 data_records = Some((*variable_data_block).into());
46 }
47 _ => {}
48 }
49 user_data = Some(x);
50 }
51 }
52 frames::WiredFrame::SingleCharacter { .. } => (),
53 frames::WiredFrame::ShortFrame { .. } => (),
54 frames::WiredFrame::ControlFrame { .. } => (),
55 _ => (),
56 };
57
58 Ok(MbusData {
59 frame,
60 user_data,
61 data_records,
62 })
63 }
64}
65
66impl<'a> TryFrom<&'a [u8]> for MbusData<'a, WirelessFrame<'a>> {
67 type Error = MbusError;
68
69 fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
70 let frame = wireless_mbus_link_layer::WirelessFrame::try_from(data)?;
71 let mut user_data = None;
72 let mut data_records = None;
73 let wireless_mbus_link_layer::WirelessFrame { data, .. } = &frame;
75
76 if let Ok(user_data_block) = user_data::UserDataBlock::try_from(*data) {
77 match &user_data_block {
78 user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
79 variable_data_block,
80 ..
81 } => {
82 data_records = Some((*variable_data_block).into());
83 }
84 user_data::UserDataBlock::VariableDataStructureWithShortTplHeader {
85 variable_data_block,
86 ..
87 } => {
88 data_records = Some((*variable_data_block).into());
89 }
90 user_data::UserDataBlock::VariableDataStructureWithoutTplHeader {
91 variable_data_block,
92 ..
93 } => {
94 data_records = Some((*variable_data_block).into());
95 }
96 _ => {}
97 }
98 user_data = Some(user_data_block);
99 }
100
101 Ok(MbusData {
102 frame,
103 user_data,
104 data_records,
105 })
106 }
107}
108
109#[cfg(feature = "std")]
110fn clean_and_convert(input: &str) -> Vec<u8> {
111 use core::str;
112 let input = input.trim();
113 let cleaned_data: String = input.replace("0x", "").replace([' ', ',', 'x'], "");
114
115 cleaned_data
116 .as_bytes()
117 .chunks(2)
118 .map(|chunk| {
119 let byte_str = str::from_utf8(chunk).unwrap_or_default();
120 u8::from_str_radix(byte_str, 16).unwrap_or_default()
121 })
122 .collect()
123}
124
125#[cfg(feature = "std")]
126#[must_use]
127pub fn serialize_mbus_data(data: &str, format: &str, key: Option<&[u8; 16]>) -> String {
128 match format {
129 "json" => parse_to_json(data, key),
130 "yaml" => parse_to_yaml(data, key),
131 "csv" => parse_to_csv(data, key),
132 "xml" => parse_to_xml(data),
133 "mermaid" => parse_to_mermaid(data, key),
134 "annotated" => parse_to_annotated(data),
135 "hexview" => parse_to_hexview(data, key),
136 "annotated-text" => parse_to_annotated_text(data),
137 _ => parse_to_table(data, key),
138 }
139}
140
141#[cfg(feature = "std")]
148#[derive(serde::Serialize)]
149struct FrameSummary {
150 frame_type: &'static str,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 function: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 address: Option<String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 identification_number: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 manufacturer: Option<ManufacturerSummary>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 access_number: Option<u8>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 status: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 security_mode: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 version: Option<u8>,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 device_type: Option<String>,
169 encrypted: bool,
170 decrypted: bool,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 encrypted_payload_hex: Option<String>,
173 records: Vec<RecordSummary>,
174}
175
176#[cfg(feature = "std")]
177#[derive(serde::Serialize)]
178struct ManufacturerSummary {
179 code: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 name: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 website: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 description: Option<String>,
186}
187
188#[cfg(feature = "std")]
189#[derive(serde::Serialize)]
190struct RecordSummary {
191 display: String,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 value: Option<f64>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 exponent: Option<isize>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 unit: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 quantity: Option<String>,
201 data_information: String,
202 header_hex: String,
203 data_hex: String,
204}
205
206#[cfg(feature = "std")]
207#[derive(serde::Serialize)]
208struct SummarySection<'a> {
209 summary: &'a FrameSummary,
210}
211
212#[cfg(feature = "std")]
213struct ParsedOutput {
214 summary: FrameSummary,
215 json: serde_json::Value,
216 yaml: String,
217}
218
219#[cfg(feature = "std")]
220impl FrameSummary {
221 fn new(frame_type: &'static str) -> Self {
222 Self {
223 frame_type,
224 function: None,
225 address: None,
226 identification_number: None,
227 manufacturer: None,
228 access_number: None,
229 status: None,
230 security_mode: None,
231 version: None,
232 device_type: None,
233 encrypted: false,
234 decrypted: false,
235 encrypted_payload_hex: None,
236 records: Vec::new(),
237 }
238 }
239}
240
241#[cfg(feature = "std")]
242fn manufacturer_summary(code: String) -> ManufacturerSummary {
243 let info = crate::manufacturers::lookup_manufacturer(&code);
244 ManufacturerSummary {
245 code,
246 name: info.as_ref().map(|i| i.name.to_string()),
247 website: info.as_ref().map(|i| i.website.to_string()),
248 description: info.as_ref().map(|i| i.description.to_string()),
249 }
250}
251
252#[cfg(feature = "std")]
253fn hex_string(data: &[u8]) -> String {
254 data.iter().map(|b| format!("{:02X}", b)).collect()
255}
256
257#[cfg(feature = "std")]
258fn user_data_is_encrypted(user_data: Option<&user_data::UserDataBlock>) -> bool {
259 use user_data::UserDataBlock;
260 match user_data {
261 Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
262 long_tpl_header, ..
263 }) => long_tpl_header.is_encrypted(),
264 Some(UserDataBlock::VariableDataStructureWithShortTplHeader {
265 short_tpl_header, ..
266 }) => short_tpl_header.is_encrypted(),
267 _ => false,
268 }
269}
270
271#[cfg(feature = "std")]
272fn variable_data_block<'a>(user_data: Option<&user_data::UserDataBlock<'a>>) -> Option<&'a [u8]> {
273 use user_data::UserDataBlock;
274 match user_data {
275 Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
276 variable_data_block,
277 ..
278 })
279 | Some(UserDataBlock::VariableDataStructureWithShortTplHeader {
280 variable_data_block,
281 ..
282 })
283 | Some(UserDataBlock::VariableDataStructureWithoutTplHeader {
284 variable_data_block,
285 ..
286 }) => Some(variable_data_block),
287 _ => None,
288 }
289}
290
291#[cfg(feature = "std")]
292fn record_summaries(records: &user_data::DataRecords) -> Vec<RecordSummary> {
293 use user_data::data_information::DataType;
294
295 records
296 .clone()
297 .flatten()
298 .map(|record| {
299 let value_information = record
300 .data_record_header
301 .processed_data_record_header
302 .value_information
303 .as_ref();
304 let value_information_display = match value_information {
305 Some(x) => format!("{}", x),
306 None => ")".to_string(),
307 };
308 let data_information = match record
309 .data_record_header
310 .processed_data_record_header
311 .data_information
312 {
313 Some(ref x) => format!("{}", x),
314 None => "None".to_string(),
315 };
316 let value = match record.data.value {
317 Some(DataType::Number(n)) | Some(DataType::LossyNumber(n)) => Some(n),
318 _ => None,
319 };
320 let unit = value_information
321 .map(|vi| vi.units.iter().map(ToString::to_string).collect::<String>())
322 .filter(|s| !s.is_empty());
323 let quantity = value_information
324 .map(|vi| {
325 vi.labels
326 .iter()
327 .map(|label| format!("{:?}", label))
328 .collect::<Vec<_>>()
329 .join(", ")
330 })
331 .filter(|s| !s.is_empty());
332 RecordSummary {
333 display: format!("({}{}", record.data, value_information_display),
334 value,
335 exponent: value_information.map(|vi| vi.decimal_scale_exponent),
336 unit,
337 quantity,
338 data_information,
339 header_hex: record.data_record_header_hex(),
340 data_hex: record.data_hex(),
341 }
342 })
343 .collect()
344}
345
346#[cfg(feature = "std")]
347fn apply_user_data_summary(
348 summary: &mut FrameSummary,
349 user_data: Option<&user_data::UserDataBlock>,
350) {
351 use user_data::UserDataBlock;
352 match user_data {
353 Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
354 long_tpl_header, ..
355 }) => {
356 summary.identification_number = Some(long_tpl_header.identification_number.to_string());
357 summary.manufacturer = Some(manufacturer_summary(
358 long_tpl_header
359 .manufacturer
360 .as_ref()
361 .map_or_else(|e| format!("Error: {}", e), |m| m.to_string()),
362 ));
363 summary.access_number = Some(long_tpl_header.short_tpl_header.access_number);
364 summary.status = Some(long_tpl_header.short_tpl_header.status.to_string());
365 summary.security_mode = Some(
366 long_tpl_header
367 .short_tpl_header
368 .configuration_field
369 .security_mode()
370 .to_string(),
371 );
372 summary.version = Some(long_tpl_header.version);
373 summary.device_type = Some(long_tpl_header.device_type.to_string());
374 }
375 Some(UserDataBlock::VariableDataStructureWithShortTplHeader {
376 short_tpl_header, ..
377 }) => {
378 summary.access_number = Some(short_tpl_header.access_number);
379 summary.status = Some(short_tpl_header.status.to_string());
380 summary.security_mode = Some(
381 short_tpl_header
382 .configuration_field
383 .security_mode()
384 .to_string(),
385 );
386 }
387 Some(UserDataBlock::FixedDataStructure {
388 identification_number,
389 access_number,
390 status,
391 ..
392 }) => {
393 summary.identification_number = Some(identification_number.to_string());
394 summary.access_number = Some(*access_number);
395 summary.status = Some(status.to_string());
396 }
397 _ => {}
398 }
399}
400
401#[cfg(feature = "std")]
402fn finalize_summary(
403 summary: &mut FrameSummary,
404 data_records: Option<&user_data::DataRecords>,
405 user_data: Option<&user_data::UserDataBlock>,
406 encrypted: bool,
407 decrypted: bool,
408) {
409 summary.encrypted = encrypted;
410 summary.decrypted = decrypted;
411 if encrypted && !decrypted {
412 summary.encrypted_payload_hex = variable_data_block(user_data).map(hex_string);
413 } else if let Some(records) = data_records {
414 summary.records = record_summaries(records);
415 }
416}
417
418#[cfg(feature = "std")]
419fn summarize_wired(
420 parsed: &MbusData<frames::WiredFrame>,
421 encrypted: bool,
422 decrypted: bool,
423) -> FrameSummary {
424 let mut summary = match &parsed.frame {
425 frames::WiredFrame::LongFrame { .. } => FrameSummary::new("LongFrame"),
426 frames::WiredFrame::ShortFrame { .. } => FrameSummary::new("ShortFrame"),
427 frames::WiredFrame::ControlFrame { .. } => FrameSummary::new("ControlFrame"),
428 frames::WiredFrame::SingleCharacter { .. } => FrameSummary::new("SingleCharacter"),
429 _ => FrameSummary::new("Unknown"),
430 };
431 match &parsed.frame {
432 frames::WiredFrame::LongFrame {
433 function, address, ..
434 }
435 | frames::WiredFrame::ControlFrame {
436 function, address, ..
437 }
438 | frames::WiredFrame::ShortFrame { function, address } => {
439 summary.function = Some(function.to_string());
440 summary.address = Some(address.to_string());
441 }
442 _ => {}
443 }
444 apply_user_data_summary(&mut summary, parsed.user_data.as_ref());
445 finalize_summary(
446 &mut summary,
447 parsed.data_records.as_ref(),
448 parsed.user_data.as_ref(),
449 encrypted,
450 decrypted,
451 );
452 summary
453}
454
455#[cfg(feature = "std")]
456fn summarize_wireless(
457 parsed: &MbusData<WirelessFrame>,
458 encrypted: bool,
459 decrypted: bool,
460) -> FrameSummary {
461 let manufacturer_id = &parsed.frame.manufacturer_id;
462 let mut summary = FrameSummary::new("Wireless");
463 summary.function = Some(parsed.frame.function.to_string());
464 summary.identification_number = Some(manufacturer_id.identification_number.to_string());
465 summary.manufacturer = Some(manufacturer_summary(
466 manufacturer_id.manufacturer_code.to_string(),
467 ));
468 summary.version = Some(manufacturer_id.version);
469 summary.device_type = Some(manufacturer_id.device_type.to_string());
470 apply_user_data_summary(&mut summary, parsed.user_data.as_ref());
472 finalize_summary(
473 &mut summary,
474 parsed.data_records.as_ref(),
475 parsed.user_data.as_ref(),
476 encrypted,
477 decrypted,
478 );
479 summary
480}
481
482#[cfg(feature = "std")]
483fn build_output<T: serde::Serialize>(parsed: &T, summary: FrameSummary) -> ParsedOutput {
484 let mut json = serde_json::to_value(parsed).unwrap_or_default();
485 if let serde_json::Value::Object(ref mut map) = json {
486 map.insert(
487 "summary".to_string(),
488 serde_json::to_value(&summary).unwrap_or_default(),
489 );
490 }
491
492 let mut yaml = serde_yaml::to_string(parsed).unwrap_or_default();
493 yaml.push_str(
494 &serde_yaml::to_string(&SummarySection { summary: &summary }).unwrap_or_default(),
495 );
496
497 ParsedOutput {
498 summary,
499 json,
500 yaml,
501 }
502}
503
504#[cfg(feature = "std")]
508#[cfg_attr(not(feature = "decryption"), allow(unused_mut))]
509fn parse_frame_output(input: &str, key: Option<&[u8; 16]>) -> Option<ParsedOutput> {
510 let data = clean_and_convert(input);
511 #[cfg(feature = "decryption")]
512 let mut decrypted_buffer = [0u8; 256];
513 #[cfg(not(feature = "decryption"))]
514 let _ = key;
515
516 if let Ok(mut parsed) = MbusData::<frames::WiredFrame>::try_from(data.as_slice()) {
518 let encrypted = user_data_is_encrypted(parsed.user_data.as_ref());
519 let mut decrypted = false;
520 #[cfg(feature = "decryption")]
521 if encrypted {
522 if let (Some(key_bytes), Some(user_data)) = (key, parsed.user_data.as_ref()) {
523 if let Some(len) = decrypt_variable_data_with_key(
524 user_data,
525 None,
526 key_bytes,
527 &mut decrypted_buffer,
528 ) {
529 if let (
530 Some(decrypted_data),
531 Some(user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
532 long_tpl_header,
533 ..
534 }),
535 ) = (decrypted_buffer.get(..len), &parsed.user_data)
536 {
537 parsed.data_records = Some(user_data::DataRecords::new(
538 decrypted_data,
539 Some(long_tpl_header),
540 ));
541 decrypted = true;
542 }
543 }
544 }
545 }
546 let summary = summarize_wired(&parsed, encrypted, decrypted);
547 return Some(build_output(&parsed, summary));
548 }
549
550 let mut crc_buf = [0u8; 512];
552 let wireless_data =
553 wireless_mbus_link_layer::strip_format_a_crcs(&data, &mut crc_buf).unwrap_or(&data);
554 if let Ok(mut parsed) =
555 MbusData::<wireless_mbus_link_layer::WirelessFrame>::try_from(wireless_data)
556 {
557 let encrypted = user_data_is_encrypted(parsed.user_data.as_ref());
558 let mut decrypted = false;
559 #[cfg(feature = "decryption")]
560 if encrypted {
561 if let (Some(key_bytes), Some(user_data)) = (key, parsed.user_data.as_ref()) {
562 if let Some(len) = decrypt_variable_data_with_key(
563 user_data,
564 Some(&parsed.frame.manufacturer_id),
565 key_bytes,
566 &mut decrypted_buffer,
567 ) {
568 if let Some(decrypted_data) = decrypted_buffer.get(..len) {
569 let long_header = match &parsed.user_data {
570 Some(
571 user_data::UserDataBlock::VariableDataStructureWithLongTplHeader {
572 long_tpl_header,
573 ..
574 },
575 ) => Some(long_tpl_header),
576 _ => None,
577 };
578 parsed.data_records =
579 Some(user_data::DataRecords::new(decrypted_data, long_header));
580 decrypted = true;
581 }
582 }
583 }
584 }
585 let summary = summarize_wireless(&parsed, encrypted, decrypted);
586 return Some(build_output(&parsed, summary));
587 }
588
589 None
590}
591
592#[cfg(feature = "std")]
593#[must_use]
594pub fn parse_to_json(input: &str, key: Option<&[u8; 16]>) -> String {
595 match parse_frame_output(input, key) {
596 Some(output) => serde_json::to_string_pretty(&output.json).unwrap_or_default(),
597 None => "{}".to_string(),
598 }
599}
600
601#[cfg(feature = "std")]
602#[must_use]
603fn parse_to_yaml(input: &str, key: Option<&[u8; 16]>) -> String {
604 match parse_frame_output(input, key) {
605 Some(output) => output.yaml,
606 None => "---\nerror: Could not parse data\n".to_string(),
607 }
608}
609
610#[cfg(feature = "std")]
611#[must_use]
612fn parse_to_table(input: &str, key: Option<&[u8; 16]>) -> String {
613 match parse_frame_output(input, key) {
614 Some(output) => render_table(&output.summary, key.is_some()),
615 None => "Error: Could not parse data as wired or wireless M-Bus".to_string(),
616 }
617}
618
619#[cfg(feature = "std")]
620fn render_table(summary: &FrameSummary, key_provided: bool) -> String {
621 let mut out = String::new();
622 let title = match summary.frame_type {
623 "LongFrame" => "Long Frame",
624 "ShortFrame" => "Short Frame",
625 "ControlFrame" => "Control Frame",
626 "SingleCharacter" => "Single Character Frame",
627 "Wireless" => "Wireless Frame",
628 other => other,
629 };
630 out.push_str(title);
631 out.push('\n');
632
633 if let (Some(function), Some(address)) = (&summary.function, &summary.address) {
635 let mut table = Table::new();
636 table.set_format(*format::consts::FORMAT_BOX_CHARS);
637 table.set_titles(row!["Function", "Address"]);
638 table.add_row(row![function, address]);
639 out.push_str(&table.to_string());
640 }
641
642 let mut info_table = Table::new();
643 info_table.set_format(*format::consts::FORMAT_BOX_CHARS);
644 info_table.set_titles(row!["Field", "Value"]);
645 if summary.address.is_none() {
646 if let Some(function) = &summary.function {
647 info_table.add_row(row!["Function", function]);
648 }
649 }
650 if let Some(identification_number) = &summary.identification_number {
651 info_table.add_row(row!["Identification Number", identification_number]);
652 }
653 if let Some(manufacturer) = &summary.manufacturer {
654 info_table.add_row(row!["Manufacturer", manufacturer.code]);
655 if let Some(name) = &manufacturer.name {
656 info_table.add_row(row!["Manufacturer Name", name]);
657 }
658 if let Some(website) = &manufacturer.website {
659 info_table.add_row(row!["Website", website]);
660 }
661 if let Some(description) = &manufacturer.description {
662 info_table.add_row(row!["Description", description]);
663 }
664 }
665 if let Some(access_number) = summary.access_number {
666 info_table.add_row(row!["Access Number", access_number]);
667 }
668 if let Some(status) = &summary.status {
669 info_table.add_row(row!["Status", status]);
670 }
671 if let Some(security_mode) = &summary.security_mode {
672 info_table.add_row(row!["Security Mode", security_mode]);
673 }
674 if let Some(version) = summary.version {
675 info_table.add_row(row!["Version", version]);
676 }
677 if let Some(device_type) = &summary.device_type {
678 info_table.add_row(row!["Device Type", device_type]);
679 }
680 if !info_table.is_empty() {
681 out.push_str(&info_table.to_string());
682 }
683
684 if summary.encrypted && !summary.decrypted {
685 if key_provided {
686 out.push_str("Decryption failed\n");
687 }
688 if let Some(payload_hex) = &summary.encrypted_payload_hex {
689 out.push_str("Encrypted Payload : ");
690 out.push_str(payload_hex);
691 out.push('\n');
692 }
693 return out;
694 }
695
696 if summary.decrypted {
697 out.push_str("Decrypted successfully\n");
698 }
699
700 if matches!(summary.frame_type, "LongFrame" | "Wireless") || !summary.records.is_empty() {
701 let mut value_table = Table::new();
702 value_table.set_format(*format::consts::FORMAT_BOX_CHARS);
703 value_table.set_titles(row!["Value", "Data Information", "Header Hex", "Data Hex"]);
704 for record in &summary.records {
705 value_table.add_row(row![
706 record.display,
707 record.data_information,
708 record.header_hex,
709 record.data_hex
710 ]);
711 }
712 out.push_str(&value_table.to_string());
713 }
714
715 out
716}
717
718#[cfg(feature = "std")]
719#[must_use]
720pub fn parse_to_csv(input: &str, key: Option<&[u8; 16]>) -> String {
721 use prettytable::csv;
722
723 let mut writer = csv::Writer::from_writer(vec![]);
724
725 let Some(output) = parse_frame_output(input, key) else {
726 writer
727 .write_record(["Error"])
728 .map_err(|_| ())
729 .unwrap_or_default();
730 writer
731 .write_record(["Error parsing data as wired or wireless M-Bus"])
732 .map_err(|_| ())
733 .unwrap_or_default();
734 let csv_data = writer.into_inner().unwrap_or_default();
735 return String::from_utf8(csv_data)
736 .unwrap_or_else(|_| "Error converting CSV data to string".to_string());
737 };
738
739 let summary = &output.summary;
740
741 let mut headers = vec![
742 "FrameType".to_string(),
743 "Function".to_string(),
744 "Address".to_string(),
745 "Identification Number".to_string(),
746 "Manufacturer".to_string(),
747 "Manufacturer Name".to_string(),
748 "Access Number".to_string(),
749 "Status".to_string(),
750 "Security Mode".to_string(),
751 "Version".to_string(),
752 "Device Type".to_string(),
753 ];
754 for i in 1..=summary.records.len() {
755 headers.push(format!("DataPoint{}_Value", i));
756 headers.push(format!("DataPoint{}_Info", i));
757 }
758 writer
759 .write_record(&headers)
760 .map_err(|_| ())
761 .unwrap_or_default();
762
763 let mut row = vec![
764 summary.frame_type.to_string(),
765 summary.function.clone().unwrap_or_default(),
766 summary.address.clone().unwrap_or_default(),
767 summary.identification_number.clone().unwrap_or_default(),
768 summary
769 .manufacturer
770 .as_ref()
771 .map(|m| m.code.clone())
772 .unwrap_or_default(),
773 summary
774 .manufacturer
775 .as_ref()
776 .and_then(|m| m.name.clone())
777 .unwrap_or_default(),
778 summary
779 .access_number
780 .map(|n| n.to_string())
781 .unwrap_or_default(),
782 summary.status.clone().unwrap_or_default(),
783 summary.security_mode.clone().unwrap_or_default(),
784 summary.version.map(|v| v.to_string()).unwrap_or_default(),
785 summary.device_type.clone().unwrap_or_default(),
786 ];
787 for record in &summary.records {
788 row.push(record.display.clone());
789 row.push(record.data_information.clone());
790 }
791 writer
792 .write_record(&row)
793 .map_err(|_| ())
794 .unwrap_or_default();
795
796 let csv_data = writer.into_inner().unwrap_or_default();
797 String::from_utf8(csv_data)
798 .unwrap_or_else(|_| "Error converting CSV data to string".to_string())
799}
800
801#[cfg(feature = "std")]
802#[must_use]
803pub fn parse_to_mermaid(input: &str, _key: Option<&[u8; 16]>) -> String {
804 use user_data::UserDataBlock;
805
806 const MAX_PER_ROW: usize = 4;
807 const RECORD_COLORS: &[(&str, &str)] = &[
809 ("#1565c0", "#fff"),
810 ("#2e7d32", "#fff"),
811 ("#e65100", "#fff"),
812 ("#6a1b9a", "#fff"),
813 ("#c62828", "#fff"),
814 ("#00695c", "#fff"),
815 ("#f9a825", "#000"),
816 ("#4527a0", "#fff"),
817 ];
818
819 let data = clean_and_convert(input);
820
821 if let Ok(parsed_data) = MbusData::<frames::WiredFrame>::try_from(data.as_slice()) {
823 let mut out = String::from("flowchart TD\n");
824 let mut styles = String::new();
825
826 match parsed_data.frame {
827 frames::WiredFrame::LongFrame {
828 function,
829 address,
830 data: _,
831 } => {
832 out.push_str(" subgraph FRAME_SG[\"Frame Header\"]\n");
834 out.push_str("");
835 out.push_str(&format!(
836 " FTYPE[\"Long Frame\"]\n FUNC[\"Function: {}\"]\n ADDR[\"Address: {}\"]\n",
837 mermaid_escape(&format!("{}", function)),
838 mermaid_escape(&format!("{}", address))
839 ));
840 let (chains, pads) =
841 mermaid_centered_chains(&["FTYPE", "FUNC", "ADDR"], MAX_PER_ROW, "FP");
842 out.push_str(&chains);
843 out.push_str(" end\n");
844 styles.push_str(&pads);
845 styles.push_str(" style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
846 styles.push_str(" style FTYPE fill:#2980b9,color:#fff,stroke:#1a5276\n");
847 styles.push_str(" style FUNC fill:#2980b9,color:#fff,stroke:#1a5276\n");
848 styles.push_str(" style ADDR fill:#2980b9,color:#fff,stroke:#1a5276\n");
849
850 if let Some(UserDataBlock::VariableDataStructureWithLongTplHeader {
851 long_tpl_header,
852 ..
853 }) = &parsed_data.user_data
854 {
855 let mfr = long_tpl_header
856 .manufacturer
857 .as_ref()
858 .map_or_else(|e| format!("Error: {}", e), |m| format!("{}", m));
859
860 let mfr_info = crate::manufacturers::lookup_manufacturer(&mfr);
861 out.push_str(" subgraph DEV_SG[\"Device Info\"]\n");
862 out.push_str("");
863 out.push_str(&format!(
864 " DEV1[\"ID: {}\"]\n",
865 mermaid_escape(&format!("{}", long_tpl_header.identification_number))
866 ));
867 out.push_str(&format!(
868 " DEV2[\"Manufacturer: {}\"]\n",
869 mermaid_escape(&mfr)
870 ));
871 out.push_str(&format!(
872 " DEV3[\"Version: {}\"]\n",
873 long_tpl_header.version
874 ));
875 out.push_str(&format!(
876 " DEV4[\"Device Type: {}\"]\n",
877 mermaid_escape(&format!("{:?}", long_tpl_header.device_type))
878 ));
879 out.push_str(&format!(
880 " DEV5[\"Access Number: {}\"]\n",
881 long_tpl_header.short_tpl_header.access_number
882 ));
883 out.push_str(&format!(
884 " DEV6[\"Status: {}\"]\n",
885 mermaid_escape(&format!("{}", long_tpl_header.short_tpl_header.status))
886 ));
887 let mut dev_node_count = 6usize;
888 if let Some(ref info) = mfr_info {
889 dev_node_count += 1;
890 out.push_str(&format!(
891 " DEV{}[\"Name: {}\"]\n",
892 dev_node_count,
893 mermaid_escape(info.name)
894 ));
895 dev_node_count += 1;
896 out.push_str(&format!(
897 " DEV{}[\"Website: {}\"]\n",
898 dev_node_count,
899 mermaid_escape(info.website)
900 ));
901 dev_node_count += 1;
902 out.push_str(&format!(
903 " DEV{}[\"{}\"]\n",
904 dev_node_count,
905 mermaid_escape(info.description)
906 ));
907 }
908 let dev_ids: Vec<String> =
909 (1..=dev_node_count).map(|i| format!("DEV{}", i)).collect();
910 let dev_id_refs: Vec<&str> = dev_ids.iter().map(|s| s.as_str()).collect();
911 let (chains, pads) = mermaid_centered_chains(&dev_id_refs, MAX_PER_ROW, "DP");
912 out.push_str(&chains);
913 out.push_str(" end\n");
914 styles.push_str(&pads);
915 styles.push_str(" style DEV_SG fill:#1e8449,color:#fff,stroke:#145a32\n");
916 for i in 1..=dev_node_count {
917 styles.push_str(&format!(
918 " style DEV{} fill:#27ae60,color:#fff,stroke:#145a32\n",
919 i
920 ));
921 }
922
923 out.push_str(" FRAME_SG --> DEV_SG\n");
924 }
925
926 if let Some(data_records) = parsed_data.data_records {
927 out.push_str(" subgraph REC_SG[\"Data Records\"]\n");
928 out.push_str("");
929 let records: Vec<_> = data_records.flatten().collect();
930 for (i, record) in records.iter().enumerate() {
931 let value_information = match record
932 .data_record_header
933 .processed_data_record_header
934 .value_information
935 {
936 Some(ref x) => format!("{}", x),
937 None => String::new(),
938 };
939 let label = format!("({}{}", record.data, value_information);
940 out.push_str(&format!(" R{}[\"{}\"]\n", i, mermaid_escape(&label)));
941 let (fill, text) = RECORD_COLORS
942 .get(i % RECORD_COLORS.len())
943 .copied()
944 .unwrap_or(("#888", "#fff"));
945 styles.push_str(&format!(
946 " style R{} fill:{},color:{},stroke:#333\n",
947 i, fill, text
948 ));
949 }
950 let ids: Vec<String> = (0..records.len()).map(|i| format!("R{}", i)).collect();
951 let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();
952 let (chains, pads) = mermaid_centered_chains(&id_refs, MAX_PER_ROW, "RP");
953 out.push_str(&chains);
954 out.push_str(" end\n");
955 styles.push_str(" style REC_SG fill:#6c3483,color:#fff,stroke:#4a235a\n");
956 styles.push_str(&pads);
957 out.push_str(" DEV_SG --> REC_SG\n");
958 }
959 }
960 frames::WiredFrame::ShortFrame { function, address } => {
961 out.push_str(" subgraph FRAME_SG[\"Short Frame\"]\n");
962 out.push_str(&format!(
963 " FUNC[\"Function: {}\"]\n ADDR[\"Address: {}\"]\n",
964 mermaid_escape(&format!("{}", function)),
965 mermaid_escape(&format!("{}", address))
966 ));
967 out.push_str(" end\n");
968 styles.push_str(" style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
969 }
970 frames::WiredFrame::SingleCharacter { character } => {
971 out.push_str(&format!(
972 " FRAME[\"Single Character: 0x{:02X}\"]\n",
973 character
974 ));
975 styles.push_str(" style FRAME fill:#2e86c1,color:#fff,stroke:#1a5276\n");
976 }
977 frames::WiredFrame::ControlFrame {
978 function, address, ..
979 } => {
980 out.push_str(" subgraph FRAME_SG[\"Control Frame\"]\n");
981 out.push_str(&format!(
982 " FUNC[\"Function: {}\"]\n ADDR[\"Address: {}\"]\n",
983 mermaid_escape(&format!("{}", function)),
984 mermaid_escape(&format!("{}", address))
985 ));
986 out.push_str(" end\n");
987 styles.push_str(" style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
988 }
989 _ => {
990 out.push_str(" FRAME[\"Unknown Frame\"]\n");
991 }
992 }
993 out.push_str(&styles);
994 return out;
995 }
996
997 let mut crc_buf = [0u8; 512];
999 let wireless_data =
1000 wireless_mbus_link_layer::strip_format_a_crcs(&data, &mut crc_buf).unwrap_or(&data);
1001 if let Ok(parsed_data) =
1002 MbusData::<wireless_mbus_link_layer::WirelessFrame>::try_from(wireless_data)
1003 {
1004 let wireless_mbus_link_layer::WirelessFrame {
1005 function,
1006 manufacturer_id,
1007 data: _,
1008 } = &parsed_data.frame;
1009
1010 let mut out = String::from("flowchart TD\n");
1011 let mut styles = String::new();
1012
1013 let wmfr_str = format!("{}", manufacturer_id.manufacturer_code);
1014 let wmfr_info = crate::manufacturers::lookup_manufacturer(&wmfr_str);
1015 out.push_str(" subgraph FRAME_SG[\"Wireless Frame\"]\n");
1016 out.push_str(&format!(" FUNC[\"Function: {:?}\"]\n", function));
1017 out.push_str(&format!(
1018 " MFR[\"Manufacturer: {}\"]\n",
1019 mermaid_escape(&wmfr_str)
1020 ));
1021 out.push_str(&format!(
1022 " ID[\"ID: {:?}\"]\n",
1023 manufacturer_id.identification_number
1024 ));
1025 out.push_str(&format!(
1026 " DEVT[\"Device Type: {:?}\"]\n",
1027 manufacturer_id.device_type
1028 ));
1029 out.push_str(&format!(
1030 " VER[\"Version: {:?}\"]\n",
1031 manufacturer_id.version
1032 ));
1033 let mut wframe_nodes: Vec<&str> = vec!["FUNC", "MFR", "ID", "DEVT", "VER"];
1034 if let Some(ref info) = wmfr_info {
1035 out.push_str(&format!(
1036 " MFRNAME[\"Name: {}\"]\n",
1037 mermaid_escape(info.name)
1038 ));
1039 out.push_str(&format!(
1040 " MFRWEB[\"Website: {}\"]\n",
1041 mermaid_escape(info.website)
1042 ));
1043 out.push_str(&format!(
1044 " MFRDESC[\"{}\"]\n",
1045 mermaid_escape(info.description)
1046 ));
1047 wframe_nodes.extend_from_slice(&["MFRNAME", "MFRWEB", "MFRDESC"]);
1048 }
1049 out.push_str(" end\n");
1050 styles.push_str(" style FRAME_SG fill:#2e86c1,color:#fff,stroke:#1a5276\n");
1051 for node in &wframe_nodes {
1052 styles.push_str(&format!(
1053 " style {} fill:#2980b9,color:#fff,stroke:#1a5276\n",
1054 node
1055 ));
1056 }
1057
1058 if let Some(data_records) = parsed_data.data_records {
1059 out.push_str(" subgraph REC_SG[\"Data Records\"]\n");
1060 let records: Vec<_> = data_records.flatten().collect();
1061 for (i, record) in records.iter().enumerate() {
1062 let value_information = match record
1063 .data_record_header
1064 .processed_data_record_header
1065 .value_information
1066 {
1067 Some(ref x) => format!("{}", x),
1068 None => String::new(),
1069 };
1070 let label = format!("({}{}", record.data, value_information);
1071 out.push_str(&format!(" R{}[\"{}\"]\n", i, mermaid_escape(&label)));
1072 let (fill, text) = RECORD_COLORS
1073 .get(i % RECORD_COLORS.len())
1074 .copied()
1075 .unwrap_or(("#888", "#fff"));
1076 styles.push_str(&format!(
1077 " style R{} fill:{},color:{},stroke:#333\n",
1078 i, fill, text
1079 ));
1080 }
1081 out.push_str(" end\n");
1082 styles.push_str(" style REC_SG fill:#6c3483,color:#fff,stroke:#4a235a\n");
1083 out.push_str(" FRAME_SG --> REC_SG\n");
1084 }
1085
1086 out.push_str(&styles);
1087 return out;
1088 }
1089
1090 "flowchart TD\n ERR[\"Error: Could not parse data\"]\n".to_string()
1091}
1092
1093#[cfg(feature = "std")]
1097fn mermaid_centered_chains(ids: &[&str], max_per_row: usize, pad_prefix: &str) -> (String, String) {
1098 let mut body = String::new();
1099 let mut styles = String::new();
1100 let mut pad_idx = 0usize;
1101 for chunk in ids.chunks(max_per_row) {
1102 let row: Vec<String> = if chunk.len() == max_per_row {
1103 chunk.iter().map(|s| s.to_string()).collect()
1104 } else {
1105 let padding = max_per_row - chunk.len();
1106 let left = padding / 2;
1107 let right = padding - left;
1108 let mut row: Vec<String> = Vec::new();
1109 for _ in 0..left {
1110 let id = format!("{}P{}", pad_prefix, pad_idx);
1111 body.push_str(&format!(" {}[\" \"]\n", id));
1112 styles.push_str(&format!(
1113 " style {} fill:none,stroke:none,color:none\n",
1114 id
1115 ));
1116 row.push(id);
1117 pad_idx += 1;
1118 }
1119 row.extend(chunk.iter().map(|s| s.to_string()));
1120 for _ in 0..right {
1121 let id = format!("{}P{}", pad_prefix, pad_idx);
1122 body.push_str(&format!(" {}[\" \"]\n", id));
1123 styles.push_str(&format!(
1124 " style {} fill:none,stroke:none,color:none\n",
1125 id
1126 ));
1127 row.push(id);
1128 pad_idx += 1;
1129 }
1130 row
1131 };
1132 for pair in row.windows(2) {
1134 if let (Some(a), Some(b)) = (pair.first(), pair.get(1)) {
1135 body.push_str(&format!(" {}~~~{}\n", a, b));
1136 }
1137 }
1138 }
1139 (body, styles)
1140}
1141
1142#[cfg(feature = "std")]
1143#[must_use]
1144fn parse_to_xml(input: &str) -> String {
1145 let data = clean_and_convert(input);
1146 crate::rscada_xml::render_from_bytes(&data)
1147}
1148
1149#[cfg(feature = "std")]
1150#[must_use]
1151fn parse_to_annotated(input: &str) -> String {
1152 let data = clean_and_convert(input);
1153 annotated_segments_to_json(&data)
1154}
1155
1156#[cfg(feature = "std")]
1157#[must_use]
1158fn parse_to_hexview(input: &str, key: Option<&[u8; 16]>) -> String {
1159 let data = clean_and_convert(input);
1160
1161 #[cfg(feature = "decryption")]
1162 if let Some(key_bytes) = key {
1163 if let Some(display_data) = decrypted_hexview_data(&data, key_bytes) {
1164 return match crate::annotate::annotate_frame(&display_data) {
1165 Ok(mut segments) => {
1166 replace_encrypted_payload_segments(&mut segments, &display_data);
1167 serde_json::to_string_pretty(&serde_json::json!({
1168 "bytes": display_data,
1169 "segments": segments,
1170 "decrypted": true,
1171 }))
1172 .unwrap_or_default()
1173 }
1174 Err(e) => format!("{{\"error\": \"{}\"}}", e),
1175 };
1176 }
1177 }
1178
1179 #[cfg(not(feature = "decryption"))]
1180 let _ = key;
1181
1182 annotated_segments_to_json(&data)
1183}
1184
1185#[cfg(feature = "std")]
1186#[must_use]
1187fn annotated_segments_to_json(data: &[u8]) -> String {
1188 match crate::annotate::annotate_frame(data) {
1189 Ok(segments) => serde_json::to_string_pretty(&segments).unwrap_or_default(),
1190 Err(e) => format!("{{\"error\": \"{}\"}}", e),
1191 }
1192}
1193
1194#[cfg(all(feature = "std", feature = "decryption"))]
1195fn decrypted_hexview_data(data: &[u8], key: &[u8; 16]) -> Option<Vec<u8>> {
1196 decrypted_wired_hexview_data(data, key).or_else(|| decrypted_wireless_hexview_data(data, key))
1197}
1198
1199#[cfg(all(feature = "std", feature = "decryption"))]
1200fn replace_encrypted_payload_segments(
1201 segments: &mut Vec<crate::annotate::ByteSegment>,
1202 display_data: &[u8],
1203) {
1204 let mut rewritten = Vec::with_capacity(segments.len());
1205
1206 for segment in segments.drain(..) {
1207 if segment.kind == crate::annotate::SegmentKind::EncryptedPayload {
1208 let before_len = rewritten.len();
1209 if let Some(data) = display_data.get(segment.start..segment.end) {
1210 crate::annotate::annotate_data_records(&mut rewritten, segment.start, data);
1211 }
1212 if rewritten.len() == before_len {
1213 rewritten.push(segment);
1214 }
1215 } else {
1216 rewritten.push(segment);
1217 }
1218 }
1219
1220 *segments = rewritten;
1221}
1222
1223#[cfg(all(feature = "std", feature = "decryption"))]
1224fn decrypted_wired_hexview_data(data: &[u8], key: &[u8; 16]) -> Option<Vec<u8>> {
1225 let parsed_data = MbusData::<frames::WiredFrame>::try_from(data).ok()?;
1226 let user_data = parsed_data.user_data.as_ref()?;
1227 let variable_data_len = user_data.variable_data_len();
1228 if variable_data_len == 0 {
1229 return None;
1230 }
1231
1232 let mut decrypted_buffer = [0u8; 256];
1233 let decrypted_len =
1234 decrypt_variable_data_with_key(user_data, None, key, &mut decrypted_buffer)?;
1235
1236 let user_data_len = match parsed_data.frame {
1237 frames::WiredFrame::LongFrame {
1238 data: user_data_slice,
1239 ..
1240 }
1241 | frames::WiredFrame::ControlFrame {
1242 data: user_data_slice,
1243 ..
1244 } => user_data_slice.len(),
1245 _ => return None,
1246 };
1247
1248 let payload_start = 6 + user_data_len.checked_sub(variable_data_len)?;
1249 let payload_end = payload_start + variable_data_len;
1250 let mut display_data = data.to_vec();
1251 let decrypted_data = decrypted_buffer.get(..decrypted_len)?;
1252 display_data.splice(payload_start..payload_end, decrypted_data.iter().copied());
1253
1254 let length = display_data.len().checked_sub(6)?;
1255 if length > u8::MAX as usize || display_data.len() < 6 {
1256 return None;
1257 }
1258 *display_data.get_mut(1)? = length as u8;
1259 *display_data.get_mut(2)? = length as u8;
1260
1261 let checksum_index = display_data.len().checked_sub(2)?;
1262 let checksum = display_data
1263 .get(4..checksum_index)?
1264 .iter()
1265 .fold(0u8, |acc, byte| acc.wrapping_add(*byte));
1266 *display_data.get_mut(checksum_index)? = checksum;
1267
1268 Some(display_data)
1269}
1270
1271#[cfg(all(feature = "std", feature = "decryption"))]
1272fn decrypted_wireless_hexview_data(data: &[u8], key: &[u8; 16]) -> Option<Vec<u8>> {
1273 let mut crc_buf = [0u8; 512];
1274 let wireless_data =
1275 wireless_mbus_link_layer::strip_format_a_crcs(data, &mut crc_buf).unwrap_or(data);
1276 let parsed_data =
1277 MbusData::<wireless_mbus_link_layer::WirelessFrame>::try_from(wireless_data).ok()?;
1278 let user_data = parsed_data.user_data.as_ref()?;
1279 let variable_data_len = user_data.variable_data_len();
1280 if variable_data_len == 0 {
1281 return None;
1282 }
1283
1284 let mut decrypted_buffer = [0u8; 256];
1285 let manufacturer_id = &parsed_data.frame.manufacturer_id;
1286 let decrypted_len = decrypt_variable_data_with_key(
1287 user_data,
1288 Some(manufacturer_id),
1289 key,
1290 &mut decrypted_buffer,
1291 )?;
1292
1293 let app_data_len = parsed_data.frame.data.len();
1294 let app_start = wireless_data.len().checked_sub(app_data_len)?;
1295 let payload_start = app_start + app_data_len.checked_sub(variable_data_len)?;
1296 let payload_end = payload_start + variable_data_len;
1297
1298 let mut display_data = wireless_data.to_vec();
1299 let decrypted_data = decrypted_buffer.get(..decrypted_len)?;
1300 display_data.splice(payload_start..payload_end, decrypted_data.iter().copied());
1301 if let Some(length) = display_data.len().checked_sub(1) {
1302 if length <= u8::MAX as usize {
1303 if let Some(length_byte) = display_data.get_mut(0) {
1304 *length_byte = length as u8;
1305 }
1306 }
1307 }
1308
1309 Some(display_data)
1310}
1311
1312#[cfg(all(feature = "std", feature = "decryption"))]
1313fn decrypt_variable_data_with_key(
1314 user_data: &user_data::UserDataBlock<'_>,
1315 wireless_manufacturer_id: Option<&wireless_mbus_link_layer::ManufacturerId>,
1316 key: &[u8; 16],
1317 output: &mut [u8],
1318) -> Option<usize> {
1319 use user_data::UserDataBlock;
1320
1321 let mut provider = crate::decryption::StaticKeyProvider::<1>::new();
1322 match user_data {
1323 UserDataBlock::VariableDataStructureWithLongTplHeader {
1324 long_tpl_header, ..
1325 } => {
1326 if !long_tpl_header.is_encrypted() {
1327 return None;
1328 }
1329 let manufacturer = long_tpl_header.manufacturer.as_ref().ok()?;
1330 provider
1331 .add_key(
1332 manufacturer.to_id(),
1333 long_tpl_header.identification_number.number,
1334 *key,
1335 )
1336 .ok()?;
1337 user_data.decrypt_variable_data(&provider, output).ok()
1338 }
1339 UserDataBlock::VariableDataStructureWithShortTplHeader {
1340 short_tpl_header, ..
1341 } => {
1342 if !short_tpl_header.is_encrypted() {
1343 return None;
1344 }
1345 let manufacturer_id = wireless_manufacturer_id?;
1346 provider
1347 .add_key(
1348 manufacturer_id.manufacturer_code.to_id(),
1349 manufacturer_id.identification_number.number,
1350 *key,
1351 )
1352 .ok()?;
1353 user_data
1354 .decrypt_variable_data_with_context(
1355 &provider,
1356 manufacturer_id.manufacturer_code,
1357 manufacturer_id.identification_number.number,
1358 manufacturer_id.version,
1359 manufacturer_id.device_type,
1360 output,
1361 )
1362 .ok()
1363 }
1364 _ => None,
1365 }
1366}
1367
1368#[cfg(feature = "std")]
1369#[must_use]
1370fn parse_to_annotated_text(input: &str) -> String {
1371 let data = clean_and_convert(input);
1372 match crate::annotate::annotate_and_render(&data) {
1373 Ok(text) => text,
1374 Err(e) => format!("Error: {}", e),
1375 }
1376}
1377
1378#[cfg(feature = "std")]
1379fn mermaid_escape(s: &str) -> String {
1380 s.replace('"', "#quot;")
1381 .replace('[', "#91;")
1382 .replace(']', "#93;")
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387
1388 #[cfg(feature = "std")]
1389 #[test]
1390 fn test_csv_converter() {
1391 use super::parse_to_csv;
1392 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1393 let csv_output: String = parse_to_csv(input, None);
1394 println!("{}", csv_output);
1395 let yaml_output: String = super::parse_to_yaml(input, None);
1396 println!("{}", yaml_output);
1397 let json_output: String = super::parse_to_json(input, None);
1398 println!("{}", json_output);
1399 let table_output: String = super::parse_to_table(input, None);
1400 println!("{}", table_output);
1401 }
1402
1403 #[cfg(feature = "std")]
1404 #[test]
1405 fn test_csv_expected_output() {
1406 use super::parse_to_csv;
1407 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1408 let csv_output = parse_to_csv(input, None);
1409
1410 let expected = "FrameType,Function,Address,Identification Number,Manufacturer,Manufacturer Name,Access Number,Status,Security Mode,Version,Device Type,DataPoint1_Value,DataPoint1_Info,DataPoint2_Value,DataPoint2_Info,DataPoint3_Value,DataPoint3_Info,DataPoint4_Value,DataPoint4_Info,DataPoint5_Value,DataPoint5_Info,DataPoint6_Value,DataPoint6_Info,DataPoint7_Value,DataPoint7_Info,DataPoint8_Value,DataPoint8_Info,DataPoint9_Value,DataPoint9_Info,DataPoint10_Value,DataPoint10_Info\nLongFrame,\"RspUd (ACD: false, DFC: false)\",Primary (1),02205100,SLB,Schlumberger Industries,0,\"Permanent error, Manufacturer specific 3\",No encryption used,2,Heat Meter (Return),(0)e4[Wh](Energy),\"0,Inst,32-bit Integer\",(3)e-1[m³](Volume),\"0,Inst,BCD 8-digit\",(0)e3[W](Power),\"0,Inst,BCD 6-digit\",(0)e-3[m³h⁻¹](VolumeFlow),\"0,Inst,BCD 6-digit\",(1288)e-1[°C](FlowTemperature),\"0,Inst,BCD 4-digit\",(516)e-1[°C](ReturnTemperature),\"0,Inst,BCD 4-digit\",(7723)e-2[°K](TemperatureDifference),\"0,Inst,BCD 6-digit\",(12/Jan/12)(Date),\"0,Inst,Date Type G\",(3383)[day](OperatingTime),\"0,Inst,16-bit Integer\",\"(Manufacturer Specific: [96, 0])\",\"0,Inst,Special Functions (ManufacturerSpecific)\"\n";
1411
1412 assert_eq!(csv_output, expected);
1413 }
1414
1415 #[cfg(feature = "std")]
1416 #[test]
1417 fn test_wireless_csv_contains_header_info() {
1418 let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1422 let csv_output = super::parse_to_csv(input, None);
1423
1424 let mut lines = csv_output.lines();
1425 let header: Vec<&str> = lines.next().expect("header row").split(',').collect();
1426 let row = lines.next().expect("data row");
1427 assert!(header.starts_with(&[
1429 "FrameType",
1430 "Function",
1431 "Address",
1432 "Identification Number",
1433 "Manufacturer",
1434 "Manufacturer Name",
1435 "Access Number",
1436 "Status",
1437 "Security Mode",
1438 "Version",
1439 "Device Type"
1440 ]));
1441 assert!(row.starts_with("Wireless,SndNk,,12345678,ELS,Elster Group,42,No Error(s),"));
1442 assert!(row.contains("AES-CBC-128; IV ≠ 0"));
1443 assert!(row.contains(",51,Gas Meter"));
1444 }
1445
1446 #[cfg(feature = "std")]
1447 #[test]
1448 fn test_wireless_encrypted_csv_without_key_has_no_garbage_records() {
1449 let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1450 let csv_output = super::parse_to_csv(input, None);
1451 assert!(!csv_output.contains("DataPoint1_Value"));
1452 assert!(csv_output.contains("12345678"));
1454 }
1455
1456 #[cfg(all(feature = "std", feature = "decryption"))]
1457 #[test]
1458 fn test_wireless_csv_with_key_contains_records() {
1459 let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1460 let key = [
1461 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1462 0x0F, 0x11,
1463 ];
1464 let csv_output = super::parse_to_csv(input, Some(&key));
1465 assert!(csv_output.contains("12345678"));
1466 assert!(csv_output.contains("(2850427)e-2[m³](Volume)"));
1467 }
1468
1469 #[cfg(feature = "std")]
1470 #[test]
1471 fn test_json_and_yaml_contain_summary() {
1472 let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1473 let json_output = super::parse_to_json(input, None);
1474 let parsed: serde_json::Value =
1475 serde_json::from_str(&json_output).expect("json output should be valid");
1476 let summary = parsed.get("summary").expect("json should contain summary");
1477 assert_eq!(
1478 summary.get("frame_type").and_then(|v| v.as_str()),
1479 Some("Wireless")
1480 );
1481 assert_eq!(
1482 summary
1483 .get("identification_number")
1484 .and_then(|v| v.as_str()),
1485 Some("12345678")
1486 );
1487 assert_eq!(
1488 summary
1489 .get("manufacturer")
1490 .and_then(|m| m.get("code"))
1491 .and_then(|v| v.as_str()),
1492 Some("ELS")
1493 );
1494 assert_eq!(
1495 summary.get("security_mode").and_then(|v| v.as_str()),
1496 Some("AES-CBC-128; IV ≠ 0")
1497 );
1498 assert_eq!(
1499 summary.get("status").and_then(|v| v.as_str()),
1500 Some("No Error(s)")
1501 );
1502 assert_eq!(
1503 summary.get("encrypted").and_then(|v| v.as_bool()),
1504 Some(true)
1505 );
1506
1507 let yaml_output = super::parse_to_yaml(input, None);
1508 assert!(yaml_output.contains("summary:"));
1509 assert!(yaml_output.contains("frame_type: Wireless"));
1510 assert!(yaml_output.contains("security_mode: AES-CBC-128; IV ≠ 0"));
1511 }
1512
1513 #[cfg(feature = "std")]
1514 #[test]
1515 fn test_all_formats_agree_on_header_fields() {
1516 let inputs = [
1518 "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16",
1520 "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A",
1522 "1444AE0C7856341201078C2027780B134365877AC5",
1524 ];
1525 for input in inputs {
1526 let output = super::parse_frame_output(input, None).expect("frame should parse");
1527 let summary = &output.summary;
1528 let table = super::parse_to_table(input, None);
1529 let csv = super::parse_to_csv(input, None);
1530 let json = super::parse_to_json(input, None);
1531 let yaml = super::parse_to_yaml(input, None);
1532 for value in [
1533 summary.identification_number.clone(),
1534 summary.manufacturer.as_ref().map(|m| m.code.clone()),
1535 summary.status.clone(),
1536 summary.security_mode.clone(),
1537 summary.device_type.clone(),
1538 ]
1539 .into_iter()
1540 .flatten()
1541 {
1542 for (format, output) in [
1543 ("table", &table),
1544 ("csv", &csv),
1545 ("json", &json),
1546 ("yaml", &yaml),
1547 ] {
1548 assert!(
1549 output.contains(&value),
1550 "{format} output is missing header value {value:?} for input {input}"
1551 );
1552 }
1553 }
1554
1555 let parsed: serde_json::Value =
1558 serde_json::from_str(&json).expect("json output should be valid");
1559 let json_records = parsed
1560 .get("summary")
1561 .and_then(|s| s.get("records"))
1562 .and_then(|r| r.as_array())
1563 .expect("json summary should contain records");
1564 assert_eq!(json_records.len(), summary.records.len());
1565 for (json_record, record) in json_records.iter().zip(&summary.records) {
1566 assert_eq!(
1567 json_record.get("display").and_then(|v| v.as_str()),
1568 Some(record.display.as_str()),
1569 "json record display mismatch for input {input}"
1570 );
1571 assert_eq!(
1572 json_record.get("value").and_then(|v| v.as_f64()),
1573 record.value,
1574 "json record value mismatch for input {input}"
1575 );
1576 assert_eq!(
1577 json_record
1578 .get("exponent")
1579 .and_then(|v| v.as_i64())
1580 .map(|v| v as isize),
1581 record.exponent,
1582 "json record exponent mismatch for input {input}"
1583 );
1584 assert_eq!(
1585 json_record.get("unit").and_then(|v| v.as_str()),
1586 record.unit.as_deref(),
1587 "json record unit mismatch for input {input}"
1588 );
1589 assert_eq!(
1590 json_record.get("quantity").and_then(|v| v.as_str()),
1591 record.quantity.as_deref(),
1592 "json record quantity mismatch for input {input}"
1593 );
1594 if let Some(unit) = &record.unit {
1597 assert!(
1598 record.display.contains(unit.as_str()),
1599 "display {:?} should contain unit {unit:?}",
1600 record.display
1601 );
1602 }
1603 if let Some(exponent) = record.exponent.filter(|e| *e != 0) {
1604 assert!(
1605 record.display.contains(&format!("e{exponent}")),
1606 "display {:?} should contain exponent {exponent}",
1607 record.display
1608 );
1609 }
1610 }
1611 }
1612 }
1613
1614 #[cfg(feature = "std")]
1615 #[test]
1616 fn test_json_records_contain_structured_values() {
1617 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1618 let json_output = super::parse_to_json(input, None);
1619 let parsed: serde_json::Value =
1620 serde_json::from_str(&json_output).expect("json output should be valid");
1621 let records = parsed
1622 .get("summary")
1623 .and_then(|s| s.get("records"))
1624 .and_then(|r| r.as_array())
1625 .expect("summary should contain records");
1626
1627 let volume = records
1629 .iter()
1630 .find(|r| r.get("quantity").and_then(|v| v.as_str()) == Some("Volume"))
1631 .expect("volume record");
1632 assert_eq!(volume.get("value").and_then(|v| v.as_f64()), Some(3.0));
1633 assert_eq!(volume.get("exponent").and_then(|v| v.as_i64()), Some(-1));
1634 assert_eq!(volume.get("unit").and_then(|v| v.as_str()), Some("m³"));
1635 assert_eq!(
1636 volume.get("display").and_then(|v| v.as_str()),
1637 Some("(3)e-1[m³](Volume)")
1638 );
1639
1640 let date = records
1642 .iter()
1643 .find(|r| r.get("quantity").and_then(|v| v.as_str()) == Some("Date"))
1644 .expect("date record");
1645 assert!(date.get("value").is_none());
1646 assert_eq!(
1647 date.get("display").and_then(|v| v.as_str()),
1648 Some("(12/Jan/12)(Date)")
1649 );
1650
1651 let manufacturer_specific = records
1654 .iter()
1655 .find(|r| {
1656 r.get("data_information")
1657 .and_then(|v| v.as_str())
1658 .is_some_and(|s| s.contains("ManufacturerSpecific"))
1659 })
1660 .expect("manufacturer specific record");
1661 assert!(manufacturer_specific.get("value").is_none());
1662 assert!(manufacturer_specific.get("exponent").is_none());
1663 assert!(manufacturer_specific.get("unit").is_none());
1664 assert!(manufacturer_specific.get("quantity").is_none());
1665 }
1666
1667 #[cfg(feature = "std")]
1668 #[test]
1669 fn test_json_and_yaml_have_no_manufacturer_info() {
1670 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1671 let json_output = super::parse_to_json(input, None);
1672 let parsed: serde_json::Value =
1673 serde_json::from_str(&json_output).expect("json output should be valid");
1674 assert!(parsed.get("manufacturer_info").is_none());
1675 assert_eq!(
1677 parsed
1678 .get("summary")
1679 .and_then(|s| s.get("manufacturer"))
1680 .and_then(|m| m.get("name"))
1681 .and_then(|v| v.as_str()),
1682 Some("Schlumberger Industries")
1683 );
1684
1685 let yaml_output = super::parse_to_yaml(input, None);
1686 assert!(!yaml_output.contains("manufacturer_info:"));
1687 assert!(yaml_output.contains("name: Schlumberger Industries"));
1688 }
1689
1690 #[cfg(feature = "std")]
1691 #[test]
1692 fn test_byte_payloads_serialize_as_hex_strings() {
1693 let wireless_input = "1444AE0C7856341201078C2027780B134365877AC5";
1695 let json_output = super::parse_to_json(wireless_input, None);
1696 let parsed: serde_json::Value =
1697 serde_json::from_str(&json_output).expect("json output should be valid");
1698 let frame_data = parsed
1699 .get("frame")
1700 .and_then(|f| f.get("data"))
1701 .expect("wireless frame should contain data");
1702 let hex = frame_data.as_str().expect("frame data should be a string");
1703 assert!(!hex.is_empty());
1704 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
1705 assert_eq!(hex, hex.to_uppercase());
1706
1707 let wired_input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1709 let json_output = super::parse_to_json(wired_input, None);
1710 let parsed: serde_json::Value =
1711 serde_json::from_str(&json_output).expect("json output should be valid");
1712 let records = parsed
1713 .get("data_records")
1714 .and_then(|r| r.as_array())
1715 .expect("data records");
1716 let first_raw_bytes = records
1717 .first()
1718 .and_then(|r| r.get("raw_bytes"))
1719 .and_then(|v| v.as_str())
1720 .expect("raw_bytes should be a hex string");
1721 assert_eq!(first_raw_bytes, "040700000000");
1722
1723 assert!(json_output.contains("\"ManufacturerSpecific\": \"6000\""));
1725 }
1726
1727 #[cfg(feature = "std")]
1728 #[test]
1729 fn test_yaml_expected_output() {
1730 use super::parse_to_yaml;
1731 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1732 let yaml_output = parse_to_yaml(input, None);
1733
1734 let expected_start = "frame: !LongFrame\n function: !RspUd\n acd: false\n dfc: false\n address: !Primary 1\nuser_data: !VariableDataStructureWithLongTplHeader\n";
1736
1737 assert!(yaml_output.starts_with(expected_start));
1738 assert!(yaml_output.contains("device_type: HeatMeterReturn"));
1740 assert!(yaml_output.contains("identification_number:"));
1741 assert!(yaml_output.contains("status: PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3"));
1742 }
1743
1744 #[cfg(feature = "std")]
1745 #[test]
1746 fn test_json_expected_output() {
1747 use super::parse_to_json;
1748 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1749 let json_output = parse_to_json(input, None);
1750
1751 assert!(json_output.contains("\"Ok\""));
1753 assert!(json_output.contains("\"LongFrame\""));
1754 assert!(json_output.contains("\"RspUd\""));
1755 assert!(json_output.contains("\"number\": 2205100"));
1756 assert!(json_output.contains("\"device_type\": \"HeatMeterReturn\""));
1757 assert!(json_output.contains("\"status\": \"PERMANENT_ERROR | MANUFACTURER_SPECIFIC_3\""));
1758
1759 let json_parsed = serde_json::from_str::<serde_json::Value>(&json_output);
1761 assert!(json_parsed.is_ok());
1762 }
1763
1764 #[cfg(feature = "std")]
1765 #[test]
1766 fn test_mermaid_expected_output() {
1767 use super::parse_to_mermaid;
1768 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1769 let mermaid_output = parse_to_mermaid(input, None);
1770
1771 assert!(mermaid_output.starts_with("flowchart TD\n"));
1772 assert!(mermaid_output.contains("Long Frame"));
1773 assert!(mermaid_output.contains("Device Info"));
1774 assert!(mermaid_output.contains("Data Records"));
1775 assert!(mermaid_output.contains("02205100"));
1776 assert!(mermaid_output.contains("SLB"));
1777 }
1778
1779 #[cfg(feature = "std")]
1780 #[test]
1781 fn test_table_expected_output() {
1782 use super::parse_to_table;
1783 let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16";
1784 let table_output = parse_to_table(input, None);
1785
1786 assert!(table_output.starts_with("Long Frame"));
1788
1789 assert!(table_output.contains("RspUd (ACD: false, DFC: false)"));
1791 assert!(table_output.contains("Primary (1)"));
1792 assert!(table_output.contains("Identification Number"));
1793 assert!(table_output.contains("02205100"));
1794 assert!(table_output.contains("SLB"));
1795
1796 assert!(table_output.contains("(0)e4[Wh]"));
1798 assert!(table_output.contains("(3)e-1[m³](Volume)"));
1799 assert!(table_output.contains("(1288)e-1[°C](FlowTemperature)"));
1800 assert!(table_output.contains("(12/Jan/12)(Date)"));
1801 assert!(table_output.contains("(3383)[day]"));
1802 }
1803
1804 #[cfg(all(feature = "std", feature = "decryption"))]
1805 #[test]
1806 fn decrypted_utf8_text_is_not_rendered_as_latin1() {
1807 let input = "2E44931578563412330333637A2A00202557FB8016CA78E1243700B52E981E1918233AFE5E826DD0D4AD7854C697E7C8EB";
1808 let key = [
1809 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1810 0x0F, 0x11,
1811 ];
1812
1813 for format in ["table", "json", "yaml", "csv"] {
1814 let output = super::serialize_mbus_data(input, format, Some(&key));
1815 assert!(
1816 output.contains("m³"),
1817 "{format} output should contain the decoded UTF-8 text: {output}"
1818 );
1819 assert!(
1820 !output.contains("m³"),
1821 "{format} output contains mojibake: {output}"
1822 );
1823 }
1824 }
1825
1826 #[cfg(feature = "std")]
1827 #[test]
1828 fn test_annotated_output() {
1829 let input = "68 4D 4D 68 08 01 72 01 00 00 00 96 15 01 00 18 00 00 00 0C 78 56 00 00 00 01 FD 1B 00 02 FC 03 48 52 25 74 44 0D 22 FC 03 48 52 25 74 F1 0C 12 FC 03 48 52 25 74 63 11 02 65 B4 09 22 65 86 09 12 65 B7 09 01 72 00 72 65 00 00 B2 01 65 00 00 1F B3 16";
1830 let output = super::serialize_mbus_data(input, "annotated", None);
1831
1832 let parsed: serde_json::Value = serde_json::from_str(&output).unwrap_or_else(|e| {
1834 panic!(
1835 "annotated output should be valid JSON: {}\nOutput: {}",
1836 e, output
1837 )
1838 });
1839
1840 assert!(parsed.is_array(), "annotated output should be a JSON array");
1842 let segments = parsed.as_array().expect("array");
1843
1844 assert!(!segments.is_empty());
1846
1847 assert_eq!(segments[0].get("start").and_then(|v| v.as_u64()), Some(0));
1849
1850 let last = segments.last().expect("non-empty");
1852 assert_eq!(last.get("end").and_then(|v| v.as_u64()), Some(83));
1853
1854 for window in segments.windows(2) {
1856 let end = window[0].get("end").and_then(|v| v.as_u64());
1857 let start = window[1].get("start").and_then(|v| v.as_u64());
1858 assert_eq!(end, start, "segments should be contiguous");
1859 }
1860 }
1861
1862 #[cfg(feature = "std")]
1863 #[test]
1864 fn test_hexview_output_for_ci_78_frame_is_annotated_json() {
1865 let input = "1444AE0C7856341201078C2027780B134365877AC5";
1866 let output = super::serialize_mbus_data(input, "hexview", None);
1867
1868 let parsed: serde_json::Value = serde_json::from_str(&output).unwrap_or_else(|e| {
1869 panic!(
1870 "hexview output should be valid annotated JSON: {}\nOutput: {}",
1871 e, output
1872 )
1873 });
1874 let segments = parsed
1875 .as_array()
1876 .expect("hexview output should be a JSON array");
1877
1878 assert!(segments.iter().any(|seg| {
1879 seg.get("kind").and_then(|v| v.as_str()) == Some("CiField")
1880 && seg
1881 .get("detail")
1882 .and_then(|v| v.as_str())
1883 .is_some_and(|detail| detail.contains("0x78"))
1884 }));
1885 assert!(segments.iter().any(|seg| {
1886 seg.get("kind").and_then(|v| v.as_str()) == Some("DataPayload")
1887 && seg.get("detail").and_then(|v| v.as_str()) == Some("876543")
1888 }));
1889 assert!(!segments.iter().any(|seg| {
1890 seg.get("kind").and_then(|v| v.as_str()) == Some("Unknown")
1891 || seg
1892 .get("detail")
1893 .and_then(|v| v.as_str())
1894 .is_some_and(|detail| detail.contains("Unparseable data record bytes"))
1895 }));
1896 }
1897
1898 #[cfg(all(feature = "std", feature = "decryption"))]
1899 #[test]
1900 fn test_hexview_with_key_displays_decrypted_wireless_payload() {
1901 let input = "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A";
1902 let key = [
1903 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1904 0x0F, 0x11,
1905 ];
1906 let output = super::serialize_mbus_data(input, "hexview", Some(&key));
1907
1908 let parsed: serde_json::Value = serde_json::from_str(&output).unwrap_or_else(|e| {
1909 panic!(
1910 "decrypted hexview output should be valid JSON: {}\nOutput: {}",
1911 e, output
1912 )
1913 });
1914 assert_eq!(
1915 parsed.get("decrypted").and_then(|v| v.as_bool()),
1916 Some(true)
1917 );
1918
1919 let bytes = parsed
1920 .get("bytes")
1921 .and_then(|v| v.as_array())
1922 .expect("decrypted hexview should include display bytes");
1923 let display_hex = bytes
1924 .iter()
1925 .map(|byte| format!("{:02X}", byte.as_u64().unwrap_or_default()))
1926 .collect::<String>();
1927 assert!(
1928 display_hex.contains("0C1427048502046D32371F1502FD170000"),
1929 "display bytes should contain decrypted record bytes: {display_hex}"
1930 );
1931
1932 let segments = parsed
1933 .get("segments")
1934 .and_then(|v| v.as_array())
1935 .expect("decrypted hexview should include annotation segments");
1936 assert!(segments.iter().any(|seg| {
1937 seg.get("kind").and_then(|v| v.as_str()) == Some("DataPayload")
1938 && seg
1939 .get("detail")
1940 .and_then(|v| v.as_str())
1941 .is_some_and(|detail| detail.contains("2850427"))
1942 }));
1943 assert!(
1944 !segments
1945 .iter()
1946 .any(|seg| seg.get("kind").and_then(|v| v.as_str()) == Some("EncryptedPayload")),
1947 "decrypted hexview should annotate decrypted records, not ciphertext payloads"
1948 );
1949 }
1950}