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