Skip to main content

gpg_inspector_lib/packet/
literal_data.rs

1//! Literal Data packet parsing (tag 11).
2//!
3//! Literal Data packets contain the actual message content that is not
4//! to be further interpreted. This includes the data format, optional
5//! filename, modification date, and the literal data itself.
6//!
7//! RFC 4880 Section 5.9
8
9use chrono::{DateTime, TimeZone, Utc};
10
11use crate::error::Result;
12use crate::stream::ByteStream;
13
14use super::Field;
15
16/// Data format indicator for Literal Data packets.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DataFormat {
19    /// Binary data ('b', 0x62).
20    Binary,
21    /// Text data ('t', 0x74) - may need line ending conversion.
22    Text,
23    /// UTF-8 text ('u', 0x75) - text known to be UTF-8.
24    Utf8,
25    /// MIME format ('m', 0x6D) - RFC 9580.
26    Mime,
27    /// Local mode ('l', 0x6C) - deprecated.
28    Local,
29    /// Unknown format.
30    Unknown(u8),
31}
32
33impl DataFormat {
34    fn from_u8(b: u8) -> Self {
35        match b {
36            0x62 => DataFormat::Binary,
37            0x74 => DataFormat::Text,
38            0x75 => DataFormat::Utf8,
39            0x6D => DataFormat::Mime,
40            0x6C => DataFormat::Local,
41            _ => DataFormat::Unknown(b),
42        }
43    }
44
45    fn description(&self) -> String {
46        match self {
47            DataFormat::Binary => "b (Binary)".to_string(),
48            DataFormat::Text => "t (Text)".to_string(),
49            DataFormat::Utf8 => "u (UTF-8 text)".to_string(),
50            DataFormat::Mime => "m (MIME)".to_string(),
51            DataFormat::Local => "l (Local - deprecated)".to_string(),
52            DataFormat::Unknown(b) => format!("{:#04x} (Unknown)", b),
53        }
54    }
55}
56
57/// Parsed Literal Data packet.
58#[derive(Debug, Clone)]
59pub struct LiteralDataPacket {
60    /// Data format indicator.
61    pub format: DataFormat,
62    /// Filename (may be empty).
63    pub filename: String,
64    /// Modification date (Unix timestamp, 0 = unspecified).
65    pub date: u32,
66    /// The literal data content.
67    pub data: Vec<u8>,
68}
69
70/// Parses a Literal Data packet body.
71pub fn parse_literal_data(
72    stream: &mut ByteStream,
73    fields: &mut Vec<Field>,
74    body_offset: usize,
75) -> Result<LiteralDataPacket> {
76    let format_start = body_offset + stream.pos();
77    let format_byte = stream.octet()?;
78    let format = DataFormat::from_u8(format_byte);
79    fields.push(Field::field(
80        "Format",
81        format.description(),
82        (format_start, format_start + 1),
83    ));
84
85    let filename_len_start = body_offset + stream.pos();
86    let filename_len = stream.octet()? as usize;
87    fields.push(Field::field(
88        "Filename Length",
89        filename_len.to_string(),
90        (filename_len_start, filename_len_start + 1),
91    ));
92
93    let filename = if filename_len > 0 {
94        let filename_start = body_offset + stream.pos();
95        let filename_bytes = stream.bytes(filename_len)?;
96        let filename_str = String::from_utf8_lossy(&filename_bytes).to_string();
97
98        // Check for special "_CONSOLE" name
99        let display_name = if filename_str == "_CONSOLE" {
100            format!("{} (for your eyes only)", filename_str)
101        } else {
102            filename_str.clone()
103        };
104
105        fields.push(Field::field(
106            "Filename",
107            display_name,
108            (filename_start, filename_start + filename_len),
109        ));
110        filename_str
111    } else {
112        String::new()
113    };
114
115    let date_start = body_offset + stream.pos();
116    let date = stream.uint32()?;
117    let date_display = if date == 0 {
118        "0 (unspecified)".to_string()
119    } else {
120        let datetime: DateTime<Utc> = Utc.timestamp_opt(date as i64, 0).unwrap();
121        datetime.format("%Y-%m-%d %H:%M:%S UTC").to_string()
122    };
123    fields.push(Field::field(
124        "Date",
125        date_display,
126        (date_start, date_start + 4),
127    ));
128
129    let data_start = body_offset + stream.pos();
130    let data = stream.rest();
131    let data_end = body_offset + stream.pos();
132
133    if !data.is_empty() {
134        // Show a preview for text formats, byte count for binary
135        let data_desc = match format {
136            DataFormat::Text | DataFormat::Utf8 => {
137                let preview = String::from_utf8_lossy(&data);
138                if preview.len() > 64 {
139                    format!(
140                        "{} bytes: \"{}...\"",
141                        data.len(),
142                        &preview[..60].replace('\n', "\\n")
143                    )
144                } else {
145                    format!("{} bytes: \"{}\"", data.len(), preview.replace('\n', "\\n"))
146                }
147            }
148            _ => format!("{} bytes", data.len()),
149        };
150
151        fields.push(Field::field("Data", data_desc, (data_start, data_end)));
152    }
153
154    Ok(LiteralDataPacket {
155        format,
156        filename,
157        date,
158        data,
159    })
160}