Skip to main content

m_bus_parser/
mbus_data.rs

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    /// Application-layer parsing is deliberately non-fatal: callers can still
21    /// inspect a valid link-layer frame and report a partial decode.
22    #[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        // Extract application layer data from wireless frame
84        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/// Normalized, human-readable interpretation of a parsed frame.
167///
168/// This is the single source of truth for the table and CSV renderers and is
169/// embedded as `summary` in the JSON and YAML outputs, so every output format
170/// exposes the same header information regardless of whether it comes from a
171/// long TPL header, a short TPL header or the wireless link layer.
172#[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    /// Human-readable rendering (same string the table and CSV outputs use).
217    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    // A long TPL header overrides the link-layer addressing where present.
496    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/// Parses a frame (wired first, wireless as fallback), applies decryption when
530/// a key is provided, and produces the shared output model every text format
531/// renders from.
532#[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    // Try wired first
542    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    // If wired fails, try wireless - strip Format A CRCs if present
576    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    // Wired frames carry link-layer function and address in their own table.
659    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    // Colors for data record nodes (fill, text), cycling through the palette
833    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    // Try wired first
847    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                // Frame header subgraph
858                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    // Try wireless
1023    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/// Returns (body, styles) where body contains padding node declarations + chain lines,
1119/// and styles contains the invisible styles for padding nodes.
1120/// Pads each incomplete row symmetrically so nodes appear centred.
1121#[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        // Emit individual pairs instead of a chain to maximise mermaid compatibility
1158        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        // Wireless frame with a short TPL header: the identification number,
1482        // manufacturer, version and device type come from the link layer and
1483        // must not be missing from the CSV output.
1484        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        // Parse the row respecting quoting via a simple check of key fields
1491        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        // Header info must still be present even though the payload is encrypted
1516        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        // The same header information must be present in every output format.
1580        let inputs = [
1581            // wired long frame
1582            "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            // wireless with short TPL header
1584            "2E44931578563412330333637A2A0020255923C95AAA26D1B2E7493BC2AD013EC4A6F6D3529B520EDFF0EA6DEFC955B29D6D69EBF3EC8A",
1585            // wireless without TPL header (ELL)
1586            "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            // The structured record fields in the JSON summary must agree with
1619            // the normalized summary the table and CSV render from.
1620            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                // The display string the table/CSV formats use must embed the
1658                // same structured information.
1659                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        // "(3)e-1[m³](Volume)" becomes structured fields.
1691        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        // A date record has no numeric value but keeps its display string.
1704        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        // A manufacturer-specific record has neither value information nor a
1715        // numeric value.
1716        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        // summary.manufacturer carries the same information instead.
1739        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        // Wireless frame: frame.data must be a compact uppercase hex string.
1757        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        // Data records: raw_bytes must be hex strings as well.
1771        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        // Manufacturer-specific data is a hex string instead of a byte array.
1787        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        // First line of YAML output to test against - we'll test just the beginning to avoid a massive string
1798        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        // Additional checks for specific content in the YAML
1802        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        // Testing specific content in JSON
1815        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        // Verify JSON structure is valid
1823        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        // First section of the table output
1850        assert!(table_output.starts_with("Long Frame"));
1851
1852        // Key content pieces to verify
1853        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        // Data point verifications
1860        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        // Should be valid JSON
1896        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        // Should be an array
1904        assert!(parsed.is_array(), "annotated output should be a JSON array");
1905        let segments = parsed.as_array().expect("array");
1906
1907        // Should cover all 83 bytes
1908        assert!(!segments.is_empty());
1909
1910        // First segment should start at 0
1911        assert_eq!(segments[0].get("start").and_then(|v| v.as_u64()), Some(0));
1912
1913        // Last segment should end at 83
1914        let last = segments.last().expect("non-empty");
1915        assert_eq!(last.get("end").and_then(|v| v.as_u64()), Some(83));
1916
1917        // Check contiguity
1918        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}