Skip to main content

m_bus_parser/
mbus_data.rs

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