kenwood_thd75/sdcard/
callsign_list.rs1use super::SdCardError;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct CallsignEntry {
20 pub callsign: String,
22}
23
24pub fn parse_callsign_list(data: &[u8]) -> Result<Vec<CallsignEntry>, SdCardError> {
35 let text = decode_utf16le_bom(data)?;
36 let mut entries = Vec::new();
37
38 for (line_idx, line) in text.lines().enumerate() {
39 if line_idx == 0 || line.trim().is_empty() {
41 continue;
42 }
43
44 let line_num = line_idx + 1;
45 let cols: Vec<&str> = line.split('\t').collect();
46 if cols.is_empty() {
47 return Err(SdCardError::ColumnCount {
48 line: line_num,
49 expected: 1,
50 actual: 0,
51 });
52 }
53
54 let callsign = cols[0].to_owned();
55
56 if callsign.trim() == "CQCQCQ" {
58 continue;
59 }
60
61 entries.push(CallsignEntry { callsign });
62 }
63
64 Ok(entries)
65}
66
67#[must_use]
71pub fn write_callsign_list(entries: &[CallsignEntry]) -> Vec<u8> {
72 let mut text = String::new();
73
74 text.push_str("Callsign\r\n");
76
77 for entry in entries {
79 text.push_str(&entry.callsign);
80 text.push_str("\r\n");
81 }
82
83 encode_utf16le_bom(&text)
84}
85
86fn decode_utf16le_bom(data: &[u8]) -> Result<String, SdCardError> {
88 if data.len() < 2 {
89 return Err(SdCardError::MissingBom);
90 }
91 if data[0] != 0xFF || data[1] != 0xFE {
92 return Err(SdCardError::MissingBom);
93 }
94
95 let payload = &data[2..];
96 if !payload.len().is_multiple_of(2) {
97 return Err(SdCardError::InvalidUtf16Length { len: payload.len() });
98 }
99
100 let code_units: Vec<u16> = payload
101 .chunks_exact(2)
102 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
103 .collect();
104
105 String::from_utf16(&code_units).map_err(|e| SdCardError::Utf16Decode {
106 detail: e.to_string(),
107 })
108}
109
110fn encode_utf16le_bom(text: &str) -> Vec<u8> {
112 let mut out = Vec::with_capacity(2 + text.len() * 2);
113 out.push(0xFF);
115 out.push(0xFE);
116 for unit in text.encode_utf16() {
118 let bytes = unit.to_le_bytes();
119 out.push(bytes[0]);
120 out.push(bytes[1]);
121 }
122 out
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn parse_empty_list() {
131 let data = encode_utf16le_bom("Callsign\r\n");
132 let entries = parse_callsign_list(&data).unwrap();
133 assert!(entries.is_empty());
134 }
135
136 #[test]
137 fn parse_filters_cqcqcq() {
138 let data = encode_utf16le_bom("Callsign\r\nCQCQCQ \r\nW4CDR \r\n");
139 let entries = parse_callsign_list(&data).unwrap();
140 assert_eq!(entries.len(), 1);
142 assert_eq!(entries[0].callsign, "W4CDR ");
143 }
144}