Skip to main content

rgpre/import/
cpf.rs

1//! Reader and writer for REDGUARD.CPF console preferences.
2use crate::{Result, error::Error};
3
4/// Size of a complete version 2.0 console preference file.
5pub const CPF_FILE_SIZE: usize = 40;
6
7/// Raw persisted settings; interpretation and loading defaults belong to the host.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct CpfFile {
10    /// Font selector, normally zero or one.
11    pub font: u32,
12    /// Stored shadow setting, normally zero or one.
13    pub shadow: u32,
14    /// GUI color indices in edit, report, screen and paper order.
15    pub colors: [u32; 4],
16    /// Hexadecimal dump line count; the engine clamps values below one on load.
17    pub hex_lines: i32,
18    /// Zero enables paper artwork; one selects a solid background.
19    pub paper_disabled: u32,
20    /// Palette entry for the solid background.
21    pub background: u8,
22    /// Screen limit with eight fractional bits.
23    pub screen_limit: i32,
24}
25
26/// Decode exactly one version 2.0 file without normalizing any stored values.
27pub fn parse_cpf_file(data: &[u8]) -> Result<CpfFile> {
28    if data.len() != CPF_FILE_SIZE {
29        return Err(Error::Parse(format!(
30            "CPF file must be exactly {CPF_FILE_SIZE} bytes, got {}",
31            data.len()
32        )));
33    }
34    if &data[..3] != b"2.0" {
35        return Err(Error::Parse("CPF signature must be 2.0".into()));
36    }
37    let word = |offset| {
38        u32::from_le_bytes([
39            data[offset],
40            data[offset + 1],
41            data[offset + 2],
42            data[offset + 3],
43        ])
44    };
45    Ok(CpfFile {
46        font: word(3),
47        shadow: word(7),
48        colors: [word(11), word(15), word(19), word(23)],
49        hex_lines: word(27) as i32,
50        paper_disabled: word(31),
51        background: data[35],
52        screen_limit: word(36) as i32,
53    })
54}
55
56/// Encode every stored field, retaining raw values that a host may not support.
57#[must_use]
58pub fn encode_cpf_file(file: &CpfFile) -> [u8; CPF_FILE_SIZE] {
59    let mut bytes = [0; CPF_FILE_SIZE];
60    bytes[..3].copy_from_slice(b"2.0");
61    for (offset, value) in [
62        (3, file.font),
63        (7, file.shadow),
64        (11, file.colors[0]),
65        (15, file.colors[1]),
66        (19, file.colors[2]),
67        (23, file.colors[3]),
68        (27, file.hex_lines as u32),
69        (31, file.paper_disabled),
70        (36, file.screen_limit as u32),
71    ] {
72        bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
73    }
74    bytes[35] = file.background;
75    bytes
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    const BYTES: [u8; CPF_FILE_SIZE] = [
83        b'2', b'.', b'0', 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0,
84        255, 255, 255, 255, 1, 0, 0, 0, 199, 0, 44, 1, 0,
85    ];
86
87    #[test]
88    fn reads_and_writes_the_documented_layout_without_normalizing() {
89        let expected = CpfFile {
90            font: 1,
91            shadow: 2,
92            colors: [3, 13, 9, 15],
93            hex_lines: -1,
94            paper_disabled: 1,
95            background: 199,
96            screen_limit: 300 * 256,
97        };
98        assert_eq!(parse_cpf_file(&BYTES).unwrap(), expected);
99        assert_eq!(encode_cpf_file(&expected), BYTES);
100    }
101
102    #[test]
103    fn rejects_incomplete_trailing_and_wrong_version_data() {
104        for length in 0..CPF_FILE_SIZE {
105            assert!(parse_cpf_file(&BYTES[..length]).is_err());
106        }
107        let mut trailing = BYTES.to_vec();
108        trailing.push(0);
109        assert!(parse_cpf_file(&trailing).is_err());
110        let mut invalid = BYTES;
111        invalid[0] = b'3';
112        assert!(parse_cpf_file(&invalid).is_err());
113    }
114}