1use super::SdCardError;
26use crate::types::channel::FlashChannel;
27
28pub const HEADER_SIZE: usize = 0x100;
30
31pub const MAX_CHANNELS: usize = 1000;
33
34const CHANNEL_ENTRY_SIZE: usize = FlashChannel::BYTE_SIZE; const CHANNEL_NAME_SIZE: usize = 16;
39
40const CHANNEL_FLAGS_OFFSET: usize = HEADER_SIZE + 0x2000;
47
48const CHANNEL_DATA_OFFSET: usize = HEADER_SIZE + 0x4000;
54
55const CHANNEL_NAME_OFFSET: usize = HEADER_SIZE + 0x10000;
61
62const CHANNEL_FLAGS_SIZE: usize = 4;
64
65const KNOWN_MODELS: &[&str] = &["Data For TH-D75A", "Data For TH-D75E", "Data For TH-D75"];
67
68#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ConfigHeader {
74 pub model: String,
78
79 pub version_bytes: [u8; 4],
83
84 pub raw: [u8; HEADER_SIZE],
89}
90
91#[derive(Debug, Clone)]
95pub struct RadioConfig {
96 pub header: ConfigHeader,
98
99 pub channels: Vec<ChannelEntry>,
105
106 pub raw_image: Vec<u8>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct ChannelEntry {
117 pub number: u16,
119
120 pub name: String,
122
123 pub flash: FlashChannel,
130
131 pub used: bool,
136
137 pub lockout: bool,
139}
140
141pub fn parse_config(data: &[u8]) -> Result<RadioConfig, SdCardError> {
149 let min_size = CHANNEL_NAME_OFFSET + (MAX_CHANNELS * CHANNEL_NAME_SIZE);
151 if data.len() < min_size {
152 return Err(SdCardError::FileTooSmall {
153 expected: min_size,
154 actual: data.len(),
155 });
156 }
157
158 let mut raw_header = [0u8; HEADER_SIZE];
160 raw_header.copy_from_slice(&data[..HEADER_SIZE]);
161
162 let model = extract_null_terminated(&raw_header[..16]);
163 if !KNOWN_MODELS.contains(&model.as_str()) {
164 return Err(SdCardError::InvalidModelString { found: model });
165 }
166
167 let mut version_bytes = [0u8; 4];
168 version_bytes.copy_from_slice(&raw_header[0x14..0x18]);
169
170 let header = ConfigHeader {
171 model,
172 version_bytes,
173 raw: raw_header,
174 };
175
176 let mut channels = Vec::with_capacity(MAX_CHANNELS);
178
179 for i in 0..MAX_CHANNELS {
180 let ch_offset = CHANNEL_DATA_OFFSET + (i * CHANNEL_ENTRY_SIZE);
181 let name_offset = CHANNEL_NAME_OFFSET + (i * CHANNEL_NAME_SIZE);
182 let flags_offset = CHANNEL_FLAGS_OFFSET + (i * CHANNEL_FLAGS_SIZE);
183
184 let ch_end = ch_offset + CHANNEL_ENTRY_SIZE;
188 #[allow(clippy::cast_possible_truncation)]
189 let ch_index = i as u16; let (used, flash) = if ch_end <= data.len() {
192 let ch_bytes = &data[ch_offset..ch_end];
193 let rx_freq = u32::from_le_bytes([ch_bytes[0], ch_bytes[1], ch_bytes[2], ch_bytes[3]]);
194 let is_used = rx_freq != 0 && rx_freq != 0xFFFF_FFFF;
195 let ch = FlashChannel::from_bytes(ch_bytes).map_err(|e| SdCardError::ChannelParse {
196 index: ch_index,
197 detail: e.to_string(),
198 })?;
199 (is_used, ch)
200 } else {
201 (false, FlashChannel::default())
202 };
203
204 let name = if name_offset + CHANNEL_NAME_SIZE <= data.len() {
206 extract_null_terminated(&data[name_offset..name_offset + CHANNEL_NAME_SIZE])
207 } else {
208 String::new()
209 };
210
211 let lockout = if flags_offset + CHANNEL_FLAGS_SIZE <= data.len() {
213 data[flags_offset] & 0x01 != 0
214 } else {
215 false
216 };
217
218 channels.push(ChannelEntry {
219 number: ch_index,
220 name,
221 flash,
222 used,
223 lockout,
224 });
225 }
226
227 let raw_image = data[HEADER_SIZE..].to_vec();
229
230 Ok(RadioConfig {
231 header,
232 channels,
233 raw_image,
234 })
235}
236
237#[must_use]
243pub fn write_config(config: &RadioConfig) -> Vec<u8> {
244 let image_size = config.raw_image.len();
245 let total_size = HEADER_SIZE + image_size;
246 let mut out = vec![0u8; total_size];
247
248 out[..HEADER_SIZE].copy_from_slice(&config.header.raw);
250
251 out[HEADER_SIZE..].copy_from_slice(&config.raw_image);
253
254 for entry in &config.channels {
256 let i = entry.number as usize;
257 if i >= MAX_CHANNELS {
258 continue;
259 }
260
261 let ch_offset = CHANNEL_DATA_OFFSET + (i * CHANNEL_ENTRY_SIZE);
263 let ch_end = ch_offset + CHANNEL_ENTRY_SIZE;
264 if ch_end <= out.len() {
265 let bytes = entry.flash.to_bytes();
266 out[ch_offset..ch_end].copy_from_slice(&bytes);
267 }
268
269 let name_offset = CHANNEL_NAME_OFFSET + (i * CHANNEL_NAME_SIZE);
271 let name_end = name_offset + CHANNEL_NAME_SIZE;
272 if name_end <= out.len() {
273 let mut name_buf = [0u8; CHANNEL_NAME_SIZE];
274 let src = entry.name.as_bytes();
275 let copy_len = src.len().min(CHANNEL_NAME_SIZE);
276 name_buf[..copy_len].copy_from_slice(&src[..copy_len]);
277 out[name_offset..name_end].copy_from_slice(&name_buf);
278 }
279
280 let flags_offset = CHANNEL_FLAGS_OFFSET + (i * CHANNEL_FLAGS_SIZE);
282 if flags_offset + CHANNEL_FLAGS_SIZE <= out.len() {
283 if entry.lockout {
285 out[flags_offset] |= 0x01;
286 } else {
287 out[flags_offset] &= !0x01;
288 }
289 }
290 }
291
292 out
293}
294
295fn extract_null_terminated(bytes: &[u8]) -> String {
297 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
298 String::from_utf8_lossy(&bytes[..end]).into_owned()
299}
300
301pub fn make_header(model: &str, version_bytes: [u8; 4]) -> Result<ConfigHeader, SdCardError> {
310 if !KNOWN_MODELS.contains(&model) {
311 return Err(SdCardError::InvalidModelString {
312 found: model.to_owned(),
313 });
314 }
315
316 let mut raw = [0u8; HEADER_SIZE];
317 let model_bytes = model.as_bytes();
318 let copy_len = model_bytes.len().min(16);
319 raw[..copy_len].copy_from_slice(&model_bytes[..copy_len]);
320 raw[0x14..0x18].copy_from_slice(&version_bytes);
321
322 Ok(ConfigHeader {
323 model: model.to_owned(),
324 version_bytes,
325 raw,
326 })
327}
328
329#[must_use]
331pub fn empty_channel(number: u16) -> ChannelEntry {
332 ChannelEntry {
333 number,
334 name: String::new(),
335 flash: FlashChannel::default(),
336 used: false,
337 lockout: false,
338 }
339}
340
341#[must_use]
346pub fn make_channel(number: u16, name: &str, flash: FlashChannel) -> ChannelEntry {
347 let used = flash.rx_frequency.as_hz() != 0;
348 ChannelEntry {
349 number,
350 name: name.to_owned(),
351 flash,
352 used,
353 lockout: false,
354 }
355}
356
357pub fn write_d75(
369 image: &crate::memory::MemoryImage,
370 header: &ConfigHeader,
371) -> Result<Vec<u8>, SdCardError> {
372 if !KNOWN_MODELS.contains(&header.model.as_str()) {
374 return Err(SdCardError::InvalidModelString {
375 found: header.model.clone(),
376 });
377 }
378
379 let raw = image.as_raw();
380
381 let min_body = CHANNEL_NAME_OFFSET - HEADER_SIZE + (MAX_CHANNELS * CHANNEL_NAME_SIZE);
384 if raw.len() < min_body {
385 return Err(SdCardError::FileTooSmall {
386 expected: min_body,
387 actual: raw.len(),
388 });
389 }
390
391 let mut out = Vec::with_capacity(HEADER_SIZE + raw.len());
392 out.extend_from_slice(&header.raw);
393 out.extend_from_slice(raw);
394 Ok(out)
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::types::frequency::Frequency;
401
402 #[test]
403 fn extract_null_terminated_basic() {
404 let mut buf = [0u8; 16];
405 buf[..5].copy_from_slice(b"hello");
406 assert_eq!(extract_null_terminated(&buf), "hello");
407 }
408
409 #[test]
410 fn extract_null_terminated_full() {
411 let buf = *b"abcdefghijklmnop";
412 assert_eq!(extract_null_terminated(&buf), "abcdefghijklmnop");
413 }
414
415 #[test]
416 fn make_header_valid() {
417 let hdr = make_header("Data For TH-D75A", [0x95, 0xC4, 0x8F, 0x42]).unwrap();
418 assert_eq!(hdr.model, "Data For TH-D75A");
419 assert_eq!(hdr.version_bytes, [0x95, 0xC4, 0x8F, 0x42]);
420 assert_eq!(hdr.raw.len(), HEADER_SIZE);
421 }
422
423 #[test]
424 fn make_header_invalid_model() {
425 let err = make_header("Data For TH-D74A", [0; 4]).unwrap_err();
426 assert!(matches!(err, SdCardError::InvalidModelString { .. }));
427 }
428
429 #[test]
430 fn empty_channel_defaults() {
431 let ch = empty_channel(42);
432 assert_eq!(ch.number, 42);
433 assert!(!ch.used);
434 assert!(!ch.lockout);
435 assert_eq!(ch.name, "");
436 }
437
438 #[test]
439 fn make_channel_marks_used() {
440 let flash = FlashChannel {
441 rx_frequency: Frequency::new(145_000_000),
442 ..FlashChannel::default()
443 };
444 let ch = make_channel(0, "2M RPT", flash);
445 assert!(ch.used);
446 assert_eq!(ch.name, "2M RPT");
447 }
448
449 #[test]
450 fn make_channel_zero_freq_unused() {
451 let ch = make_channel(0, "empty", FlashChannel::default());
452 assert!(!ch.used);
453 }
454
455 #[test]
456 fn write_d75_round_trip() {
457 use crate::memory::MemoryImage;
458 use crate::protocol::programming;
459
460 let header = make_header("Data For TH-D75A", [0x95, 0xC4, 0x8F, 0x42]).unwrap();
461 let raw = vec![0u8; programming::TOTAL_SIZE];
462 let image = MemoryImage::from_raw(raw).unwrap();
463
464 let d75_bytes = write_d75(&image, &header).unwrap();
466
467 assert_eq!(d75_bytes.len(), HEADER_SIZE + programming::TOTAL_SIZE);
469 assert_eq!(&d75_bytes[..HEADER_SIZE], &header.raw);
470 assert_eq!(&d75_bytes[HEADER_SIZE..], image.as_raw());
471
472 let parsed = parse_config(&d75_bytes).unwrap();
474 assert_eq!(parsed.header.model, "Data For TH-D75A");
475 assert_eq!(parsed.header.version_bytes, [0x95, 0xC4, 0x8F, 0x42]);
476 assert_eq!(parsed.raw_image.len(), d75_bytes.len() - HEADER_SIZE);
477 }
478
479 #[test]
480 fn write_d75_invalid_model_rejected() {
481 use crate::memory::MemoryImage;
482 use crate::protocol::programming;
483
484 let mut raw_header = [0u8; HEADER_SIZE];
485 raw_header[..17].copy_from_slice(b"Data For TH-D74A\0");
486 let header = ConfigHeader {
487 model: "Data For TH-D74A".to_owned(),
488 version_bytes: [0; 4],
489 raw: raw_header,
490 };
491 let raw = vec![0u8; programming::TOTAL_SIZE];
492 let image = MemoryImage::from_raw(raw).unwrap();
493
494 let err = write_d75(&image, &header).unwrap_err();
495 assert!(matches!(err, SdCardError::InvalidModelString { .. }));
496 }
497
498 #[test]
499 fn write_d75_preserves_channel_data() {
500 use crate::memory::MemoryImage;
501 use crate::protocol::programming;
502
503 let header = make_header("Data For TH-D75A", [0x95, 0xC4, 0x8F, 0x42]).unwrap();
504
505 let mut raw = vec![0u8; programming::TOTAL_SIZE];
507 if raw.len() > 0x4000 {
509 raw[0x4000] = 0xAB;
510 }
511 let image = MemoryImage::from_raw(raw).unwrap();
512
513 let d75_bytes = write_d75(&image, &header).unwrap();
514
515 assert_eq!(d75_bytes[HEADER_SIZE + 0x4000], 0xAB);
517 }
518
519 #[test]
520 fn parse_config_channel_parse_error() {
521 use crate::protocol::programming;
522
523 let header = make_header("Data For TH-D75A", [0x95, 0xC4, 0x8F, 0x42]).unwrap();
524
525 let mut d75_data = vec![0u8; HEADER_SIZE + programming::TOTAL_SIZE];
527 d75_data[..HEADER_SIZE].copy_from_slice(&header.raw);
528
529 let ch0_offset = CHANNEL_DATA_OFFSET;
532 d75_data[ch0_offset..ch0_offset + 4].copy_from_slice(&[0x01, 0x00, 0x00, 0x00]);
533 d75_data[ch0_offset + 0x08] = 0xF0;
536
537 let err = parse_config(&d75_data).unwrap_err();
538 assert!(
539 matches!(err, SdCardError::ChannelParse { index: 0, .. }),
540 "expected ChannelParse for index 0, got {err:?}"
541 );
542 }
543}