kenwood_thd75/sdcard/
repeater_list.rs1use super::SdCardError;
16
17const EXPECTED_COLUMNS: usize = 8;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct RepeaterEntry {
23 pub group_name: String,
25 pub name: String,
27 pub sub_name: String,
29 pub callsign_rpt1: String,
31 pub callsign_rpt2: String,
33 pub frequency: u32,
35 pub duplex: String,
37 pub offset: u32,
39}
40
41pub fn parse_repeater_list(data: &[u8]) -> Result<Vec<RepeaterEntry>, SdCardError> {
51 let text = decode_utf16le_bom(data)?;
52 let mut entries = Vec::new();
53
54 for (line_idx, line) in text.lines().enumerate() {
55 if line_idx == 0 || line.trim().is_empty() {
57 continue;
58 }
59
60 let line_num = line_idx + 1;
61 let cols: Vec<&str> = line.split('\t').collect();
62 if cols.len() < EXPECTED_COLUMNS {
63 return Err(SdCardError::ColumnCount {
64 line: line_num,
65 expected: EXPECTED_COLUMNS,
66 actual: cols.len(),
67 });
68 }
69
70 let frequency = parse_frequency_mhz(cols[5], line_num, "Frequency")?;
71 let offset = parse_frequency_mhz(cols[7], line_num, "Offset")?;
72
73 entries.push(RepeaterEntry {
74 group_name: cols[0].to_owned(),
75 name: cols[1].to_owned(),
76 sub_name: cols[2].to_owned(),
77 callsign_rpt1: cols[3].to_owned(),
78 callsign_rpt2: cols[4].to_owned(),
79 frequency,
80 duplex: cols[6].to_owned(),
81 offset,
82 });
83 }
84
85 Ok(entries)
86}
87
88#[must_use]
92pub fn write_repeater_list(entries: &[RepeaterEntry]) -> Vec<u8> {
93 let mut text = String::new();
94
95 text.push_str(
97 "Group Name\tName\tSub Name\tRepeater Call Sign\t\
98 Gateway Call Sign\tFrequency\tDup\tOffset\r\n",
99 );
100
101 for entry in entries {
103 text.push_str(&entry.group_name);
104 text.push('\t');
105 text.push_str(&entry.name);
106 text.push('\t');
107 text.push_str(&entry.sub_name);
108 text.push('\t');
109 text.push_str(&entry.callsign_rpt1);
110 text.push('\t');
111 text.push_str(&entry.callsign_rpt2);
112 text.push('\t');
113 text.push_str(&format_frequency_mhz(entry.frequency));
114 text.push('\t');
115 text.push_str(&entry.duplex);
116 text.push('\t');
117 text.push_str(&format_frequency_mhz(entry.offset));
118 text.push_str("\r\n");
119 }
120
121 encode_utf16le_bom(&text)
122}
123
124fn parse_frequency_mhz(s: &str, line: usize, column: &str) -> Result<u32, SdCardError> {
126 let trimmed = s.trim();
127 if trimmed.is_empty() {
128 return Ok(0);
129 }
130 let mhz: f64 = trimmed.parse().map_err(|_| SdCardError::InvalidField {
131 line,
132 column: column.to_owned(),
133 detail: format!("invalid frequency: {trimmed:?}"),
134 })?;
135 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
137 let hz = (mhz * 1_000_000.0).round() as u32;
138 Ok(hz)
139}
140
141fn format_frequency_mhz(hz: u32) -> String {
143 let mhz = f64::from(hz) / 1_000_000.0;
144 format!("{mhz:.6}")
145}
146
147fn decode_utf16le_bom(data: &[u8]) -> Result<String, SdCardError> {
149 if data.len() < 2 {
150 return Err(SdCardError::MissingBom);
151 }
152 if data[0] != 0xFF || data[1] != 0xFE {
153 return Err(SdCardError::MissingBom);
154 }
155
156 let payload = &data[2..];
157 if !payload.len().is_multiple_of(2) {
158 return Err(SdCardError::InvalidUtf16Length { len: payload.len() });
159 }
160
161 let code_units: Vec<u16> = payload
162 .chunks_exact(2)
163 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
164 .collect();
165
166 String::from_utf16(&code_units).map_err(|e| SdCardError::Utf16Decode {
167 detail: e.to_string(),
168 })
169}
170
171fn encode_utf16le_bom(text: &str) -> Vec<u8> {
173 let mut out = Vec::with_capacity(2 + text.len() * 2);
174 out.push(0xFF);
176 out.push(0xFE);
177 for unit in text.encode_utf16() {
179 let bytes = unit.to_le_bytes();
180 out.push(bytes[0]);
181 out.push(bytes[1]);
182 }
183 out
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn decode_utf16le_bom_basic() {
192 let text = "hello";
193 let encoded = encode_utf16le_bom(text);
194 let decoded = decode_utf16le_bom(&encoded).unwrap();
195 assert_eq!(decoded, "hello");
196 }
197
198 #[test]
199 fn decode_utf16le_missing_bom() {
200 let err = decode_utf16le_bom(&[0x00, 0x00]).unwrap_err();
201 assert!(matches!(err, SdCardError::MissingBom));
202 }
203
204 #[test]
205 fn decode_utf16le_odd_length() {
206 let err = decode_utf16le_bom(&[0xFF, 0xFE, 0x41]).unwrap_err();
207 assert!(matches!(err, SdCardError::InvalidUtf16Length { .. }));
208 }
209
210 #[test]
211 fn format_frequency_round_trip() {
212 let hz = 145_000_000u32;
213 let s = format_frequency_mhz(hz);
214 assert_eq!(s, "145.000000");
215 let back = parse_frequency_mhz(&s, 1, "test").unwrap();
216 assert_eq!(back, hz);
217 }
218
219 #[test]
220 fn parse_frequency_empty() {
221 let hz = parse_frequency_mhz("", 1, "test").unwrap();
222 assert_eq!(hz, 0);
223 }
224}