1use crate::error::HostLinkError;
2use std::fmt;
3
4const DEVICE_TYPES_PARSE_ORDER: &[&str] = &[
5 "MR", "LR", "CR", "VB", "DM", "EM", "FM", "ZF", "TM", "TC", "TS", "CC", "CS", "AT", "CM", "VM",
6 "R", "B", "W", "Z", "T", "C", "X", "Y", "M", "L", "D", "E", "F",
7];
8const FORCE_DEVICE_TYPES: &[&str] = &["R", "B", "MR", "LR", "CR", "T", "C", "VB"];
9const MBS_DEVICE_TYPES: &[&str] = &[
10 "R", "B", "MR", "LR", "CR", "T", "C", "VB", "X", "Y", "M", "L",
11];
12const MWS_DEVICE_TYPES: &[&str] = &[
13 "R", "B", "MR", "LR", "CR", "VB", "X", "Y", "DM", "EM", "FM", "W", "TM", "Z", "TC", "TS", "CC",
14 "CS", "CM", "VM",
15];
16const RDC_DEVICE_TYPES: &[&str] = &[
17 "R", "B", "MR", "LR", "CR", "DM", "EM", "FM", "ZF", "W", "TM", "Z", "T", "C", "CM", "X", "Y",
18 "M", "L", "D", "E", "F",
19];
20const WR_DEVICE_TYPES: &[&str] = &[
21 "R", "B", "MR", "LR", "CR", "VB", "DM", "EM", "FM", "ZF", "W", "TM", "Z", "T", "TC", "TS", "C",
22 "CC", "CS", "CM", "VM", "X", "Y", "M", "L", "D", "E", "F",
23];
24const WS_DEVICE_TYPES: &[&str] = &["T", "C"];
25
26#[derive(Debug, Clone, Copy)]
27struct DeviceRange {
28 lo: u32,
29 hi: u32,
30 base: u32,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct KvDeviceAddress {
35 pub device_type: String,
36 pub number: u32,
37 pub suffix: String,
38}
39
40impl KvDeviceAddress {
41 pub fn to_text(&self) -> Result<String, HostLinkError> {
42 let range = device_range(&self.device_type).ok_or_else(|| {
43 HostLinkError::protocol(format!("Unsupported device type: {}", self.device_type))
44 })?;
45 let number = if uses_bit_bank_address(&self.device_type) {
46 format_bit_bank_number(self.number)
47 } else if uses_xym_bit_address(&self.device_type) {
48 format_xym_bit_number(self.number)
49 } else if range.base == 16 {
50 format!("{:X}", self.number)
51 } else {
52 self.number.to_string()
53 };
54 Ok(format!("{}{}{}", self.device_type, number, self.suffix))
55 }
56}
57
58impl fmt::Display for KvDeviceAddress {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self.to_text() {
61 Ok(text) => write!(f, "{text}"),
62 Err(_) => write!(f, "{}{}{}", self.device_type, self.number, self.suffix),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct KvLogicalAddress {
69 pub base_address: KvDeviceAddress,
70 pub data_type: String,
71 pub bit_index: Option<u8>,
72}
73
74impl KvLogicalAddress {
75 pub fn is_bit_in_word(&self) -> bool {
76 self.bit_index.is_some()
77 }
78
79 pub fn to_text(&self) -> Result<String, HostLinkError> {
80 let mut base = self.base_address.clone();
81 base.suffix.clear();
82 let base_text = base.to_text()?;
83 if let Some(bit_index) = self.bit_index {
84 return Ok(format!("{base_text}.{bit_index:X}"));
85 }
86
87 if self.data_type == logical_default_dtype_by_device_type(&self.base_address.device_type) {
88 Ok(base_text)
89 } else {
90 Ok(format!("{base_text}:{}", self.data_type))
91 }
92 }
93}
94
95pub struct HostLinkAddress;
96
97impl HostLinkAddress {
98 pub fn parse(text: &str) -> Result<KvDeviceAddress, HostLinkError> {
99 parse_device(text)
100 }
101
102 pub fn try_parse(text: &str) -> Option<KvDeviceAddress> {
103 parse_device(text).ok()
104 }
105
106 pub fn format(address: &KvDeviceAddress) -> Result<String, HostLinkError> {
107 address.to_text()
108 }
109
110 pub fn normalize(text: &str) -> Result<String, HostLinkError> {
111 if let Ok(address) = parse_device(text) {
112 return address.to_text();
113 }
114
115 parse_logical_address(text)?.to_text()
116 }
117
118 pub fn parse_logical(text: &str) -> Result<KvLogicalAddress, HostLinkError> {
119 parse_logical_address(text)
120 }
121
122 pub fn try_parse_logical(text: &str) -> Option<KvLogicalAddress> {
123 parse_logical_address(text).ok()
124 }
125
126 pub fn normalize_logical(text: &str) -> Result<String, HostLinkError> {
127 parse_logical_address(text)?.to_text()
128 }
129}
130
131pub(crate) fn model_name_for_code(code: &str) -> &str {
132 match code {
133 "134" => "KV-N24nn",
134 "133" => "KV-N40nn",
135 "132" => "KV-N60nn",
136 "128" => "KV-NC32T",
137 "63" => "KV-X550",
138 "61" => "KV-X530",
139 "60" => "KV-X520",
140 "62" => "KV-X500",
141 "59" => "KV-X310",
142 "58" => "KV-8000A",
143 "57" => "KV-8000",
144 "55" => "KV-7500",
145 "54" => "KV-7300",
146 "53" => "KV-5500",
147 "52" => "KV-5000",
148 "51" => "KV-3000",
149 "50" => "KV-1000",
150 "49" => "KV-700 (With expansion memory)",
151 "48" => "KV-700 (No expansion memory)",
152 _ => "Unknown",
153 }
154}
155
156pub(crate) fn force_device_types() -> &'static [&'static str] {
157 FORCE_DEVICE_TYPES
158}
159
160pub(crate) fn mbs_device_types() -> &'static [&'static str] {
161 MBS_DEVICE_TYPES
162}
163
164pub(crate) fn mws_device_types() -> &'static [&'static str] {
165 MWS_DEVICE_TYPES
166}
167
168pub(crate) fn rdc_device_types() -> &'static [&'static str] {
169 RDC_DEVICE_TYPES
170}
171
172pub(crate) fn wr_device_types() -> &'static [&'static str] {
173 WR_DEVICE_TYPES
174}
175
176pub(crate) fn ws_device_types() -> &'static [&'static str] {
177 WS_DEVICE_TYPES
178}
179
180pub(crate) fn default_format_by_device_type(device_type: &str) -> &'static str {
181 match device_type {
182 "R" | "B" | "MR" | "LR" | "CR" | "VB" | "X" | "Y" | "M" | "L" => "",
183 "DM" | "EM" | "FM" | "ZF" | "W" | "TM" | "Z" | "CM" | "VM" | "D" | "E" | "F" => ".U",
184 "AT" => ".D",
185 "T" | "TC" | "TS" | "C" | "CC" | "CS" => ".D",
186 _ => "",
187 }
188}
189
190pub(crate) fn logical_default_dtype_by_device_type(device_type: &str) -> &'static str {
191 default_format_by_device_type(device_type).trim_start_matches('.')
192}
193
194pub(crate) fn is_direct_bit_device_type(device_type: &str) -> bool {
195 matches!(
196 device_type,
197 "R" | "B" | "MR" | "LR" | "CR" | "VB" | "X" | "Y" | "M" | "L"
198 )
199}
200
201pub(crate) fn uses_bit_bank_address(device_type: &str) -> bool {
202 matches!(device_type, "R" | "MR" | "LR" | "CR")
203}
204
205fn uses_xym_bit_address(device_type: &str) -> bool {
206 matches!(device_type, "X" | "Y")
207}
208
209fn is_valid_bit_bank_number(number: u32) -> bool {
210 number % 100 <= 15
211}
212
213pub(crate) fn bit_bank_logical_number(number: u32) -> u32 {
214 (number / 100) * 16 + (number % 100)
215}
216
217fn bit_bank_number_from_logical(number: u32) -> u32 {
218 (number / 16) * 100 + (number % 16)
219}
220
221fn format_bit_bank_number(number: u32) -> String {
222 let bank = number / 100;
223 let bit = number % 100;
224 format!("{bank}{bit:02}")
225}
226
227fn format_xym_bit_number(number: u32) -> String {
228 let bank = number / 16;
229 let bit = number % 16;
230 format!("{bank}{bit:X}")
231}
232
233pub(crate) fn is_optimizable_read_named_device_type(device_type: &str) -> bool {
234 default_format_by_device_type(device_type) == ".U"
235}
236
237pub(crate) fn offset_device(
238 start: &KvDeviceAddress,
239 word_offset: u32,
240) -> Result<String, HostLinkError> {
241 let mut next = start.clone();
242 next.number = if uses_bit_bank_address(&next.device_type) {
243 let logical = bit_bank_logical_number(next.number)
244 .checked_add(word_offset)
245 .ok_or_else(|| HostLinkError::protocol("Device offset overflow"))?;
246 bit_bank_number_from_logical(logical)
247 } else {
248 next.number
249 .checked_add(word_offset)
250 .ok_or_else(|| HostLinkError::protocol("Device offset overflow"))?
251 };
252 next.suffix.clear();
253 next.to_text()
254}
255
256pub(crate) fn parse_named_address_parts(
257 address: &str,
258) -> Result<(String, String, Option<u8>), HostLinkError> {
259 let logical = parse_logical_address(address)?;
260 let mut base = logical.base_address;
261 base.suffix.clear();
262 Ok((base.to_text()?, logical.data_type, logical.bit_index))
263}
264
265pub fn normalize_suffix(suffix: impl AsRef<str>) -> Result<String, HostLinkError> {
266 let suffix = suffix.as_ref();
267 if suffix.is_empty() {
268 return Ok(String::new());
269 }
270
271 let mut normalized = suffix.trim().to_ascii_uppercase();
272 if !normalized.starts_with('.') {
273 normalized.insert(0, '.');
274 }
275
276 match normalized.as_str() {
277 ".U" | ".S" | ".D" | ".L" | ".H" => Ok(normalized),
278 _ => Err(HostLinkError::protocol(format!(
279 "Unsupported data format suffix: {suffix}"
280 ))),
281 }
282}
283
284pub fn parse_device(text: &str) -> Result<KvDeviceAddress, HostLinkError> {
285 parse_device_internal(text, true)
286}
287
288fn parse_device_internal(
289 text: &str,
290 allow_omitted_type: bool,
291) -> Result<KvDeviceAddress, HostLinkError> {
292 let raw = text.trim().to_ascii_uppercase();
293 if raw.is_empty() {
294 return Err(HostLinkError::protocol("Device string must not be empty"));
295 }
296
297 let (core, suffix) = extract_suffix(&raw)?;
298 let (device_type, number_text) = if let Some(device_type) = DEVICE_TYPES_PARSE_ORDER
299 .iter()
300 .find(|candidate| core.starts_with(**candidate))
301 {
302 (
303 (*device_type).to_owned(),
304 core[device_type.len()..].to_owned(),
305 )
306 } else if allow_omitted_type && core.bytes().all(|byte| byte.is_ascii_digit()) {
307 ("R".to_owned(), core.to_owned())
308 } else {
309 return Err(HostLinkError::protocol(format!(
310 "Invalid device string '{text}'. Valid device types: {}.",
311 DEVICE_TYPES_PARSE_ORDER.join(", ")
312 )));
313 };
314
315 if number_text.is_empty() || !number_text.bytes().all(|byte| byte.is_ascii_hexdigit()) {
316 return Err(HostLinkError::protocol(format!(
317 "Invalid device number for {device_type}: {number_text}"
318 )));
319 }
320
321 let range = device_range(&device_type).ok_or_else(|| {
322 HostLinkError::protocol(format!("Unsupported device type: {device_type}"))
323 })?;
324
325 let number = if uses_xym_bit_address(&device_type) {
326 parse_xym_bit_number(&device_type, &number_text)?
327 } else {
328 u32::from_str_radix(&number_text, range.base).map_err(|_| {
329 HostLinkError::protocol(format!(
330 "Invalid device number for {device_type}: {number_text}"
331 ))
332 })?
333 };
334 if number < range.lo || number > range.hi {
335 return Err(HostLinkError::protocol(format!(
336 "Device number out of range: {device_type}{number_text} (allowed: {}..{})",
337 format_device_number(&device_type, range.lo),
338 format_device_number(&device_type, range.hi)
339 )));
340 }
341 if uses_bit_bank_address(&device_type) && !is_valid_bit_bank_number(number) {
342 return Err(HostLinkError::protocol(format!(
343 "Invalid bit-bank device number: {device_type}{number_text} (lower two digits must be 00..15)"
344 )));
345 }
346
347 Ok(KvDeviceAddress {
348 device_type,
349 number,
350 suffix,
351 })
352}
353
354pub fn parse_logical_address(text: &str) -> Result<KvLogicalAddress, HostLinkError> {
355 let raw = text.trim();
356 if raw.is_empty() {
357 return Err(HostLinkError::protocol("Address must not be empty"));
358 }
359
360 if let Some(colon_index) = raw.find(':') {
361 let base = parse_device(&raw[..colon_index])?;
362 let mut base = base;
363 base.suffix.clear();
364 return Ok(KvLogicalAddress {
365 base_address: base,
366 data_type: normalize_dtype(&raw[colon_index + 1..])?,
367 bit_index: None,
368 });
369 }
370
371 if let Some(dot_index) = raw.rfind('.') {
372 if let Ok(bit_index) = u8::from_str_radix(&raw[dot_index + 1..], 16) {
373 if bit_index <= 15 {
374 let mut base = parse_device(&raw[..dot_index])?;
375 base.suffix.clear();
376 return Ok(KvLogicalAddress {
377 base_address: base,
378 data_type: "BIT_IN_WORD".to_owned(),
379 bit_index: Some(bit_index),
380 });
381 }
382 }
383 }
384
385 let mut base = parse_device(raw)?;
386 let data_type = if base.suffix.is_empty() {
387 logical_default_dtype_by_device_type(&base.device_type).to_owned()
388 } else {
389 normalize_dtype(&base.suffix)?
390 };
391 base.suffix.clear();
392 Ok(KvLogicalAddress {
393 base_address: base,
394 data_type,
395 bit_index: None,
396 })
397}
398
399pub fn resolve_effective_format(device_type: &str, suffix: &str) -> String {
400 if suffix.is_empty() {
401 default_format_by_device_type(device_type).to_owned()
402 } else {
403 suffix.to_owned()
404 }
405}
406
407pub fn validate_device_type(
408 command: &str,
409 device_type: &str,
410 allowed_types: &[&str],
411) -> Result<(), HostLinkError> {
412 if allowed_types.contains(&device_type) {
413 Ok(())
414 } else {
415 Err(HostLinkError::protocol(format!(
416 "Command '{command}' does not support device type '{device_type}'. Supported types: {}.",
417 allowed_types.join(", ")
418 )))
419 }
420}
421
422pub fn validate_device_count(
423 device_type: &str,
424 effective_format: &str,
425 count: usize,
426) -> Result<(), HostLinkError> {
427 let is_32_bit = matches!(effective_format, ".D" | ".L");
428 let (lo, hi) = match device_type {
429 "TM" => (1, if is_32_bit { 256 } else { 512 }),
430 "Z" => (1, 12),
431 "AT" => (1, 8),
432 "T" | "TC" | "TS" | "C" | "CC" | "CS" => (1, 120),
433 _ => (1, if is_32_bit { 500 } else { 1000 }),
434 };
435
436 if !(lo..=hi).contains(&count) {
437 return Err(HostLinkError::protocol(format!(
438 "Count {count} is out of range for device type '{device_type}' with format '{effective_format}' (allowed: {lo}..{hi})."
439 )));
440 }
441
442 Ok(())
443}
444
445pub fn validate_device_span(
446 device_type: &str,
447 start_number: u32,
448 effective_format: &str,
449 count: usize,
450) -> Result<(), HostLinkError> {
451 let range = device_range(device_type).ok_or_else(|| {
452 HostLinkError::protocol(format!("Unsupported device type: {device_type}"))
453 })?;
454 if count == 0 {
455 return Err(HostLinkError::protocol(
456 "count out of range: 0 (allowed: 1..)",
457 ));
458 }
459
460 let word_width = if device_type == "AT" {
461 1u32
462 } else if matches!(effective_format, ".D" | ".L") {
463 2u32
464 } else {
465 1u32
466 };
467 let start_span_number = if uses_bit_bank_address(device_type) {
468 bit_bank_logical_number(start_number)
469 } else {
470 start_number
471 };
472 let hi_span_number = if uses_bit_bank_address(device_type) {
473 bit_bank_logical_number(range.hi)
474 } else {
475 range.hi
476 };
477 let end_span_number = start_span_number
478 .checked_add((count as u32).saturating_mul(word_width))
479 .and_then(|value| value.checked_sub(1))
480 .ok_or_else(|| HostLinkError::protocol("Device span overflow"))?;
481
482 if start_number < range.lo || start_number > range.hi || end_span_number > hi_span_number {
483 let start_text = format_device_number(device_type, start_number);
484 let end_number = if uses_bit_bank_address(device_type) {
485 bit_bank_number_from_logical(end_span_number)
486 } else {
487 end_span_number
488 };
489 let end_text = format_device_number(device_type, end_number);
490 return Err(HostLinkError::protocol(format!(
491 "Device span out of range: {device_type}{start_text}..{device_type}{end_text} with format '{effective_format}'"
492 )));
493 }
494
495 Ok(())
496}
497
498pub fn validate_expansion_buffer_count(
499 effective_format: &str,
500 count: usize,
501) -> Result<(), HostLinkError> {
502 let hi = if matches!(effective_format, ".D" | ".L") {
503 500
504 } else {
505 1000
506 };
507 if !(1..=hi).contains(&count) {
508 return Err(HostLinkError::protocol(format!(
509 "Count {count} is out of range for expansion buffer format '{effective_format}' (allowed: 1..{hi})."
510 )));
511 }
512 Ok(())
513}
514
515pub fn validate_expansion_buffer_span(
516 address: u32,
517 effective_format: &str,
518 count: usize,
519) -> Result<(), HostLinkError> {
520 if count == 0 {
521 return Err(HostLinkError::protocol(
522 "count out of range: 0 (allowed: 1..)",
523 ));
524 }
525
526 let word_width = if matches!(effective_format, ".D" | ".L") {
527 2u32
528 } else {
529 1u32
530 };
531 let end_address = address
532 .checked_add((count as u32).saturating_mul(word_width))
533 .and_then(|value| value.checked_sub(1))
534 .ok_or_else(|| HostLinkError::protocol("Expansion buffer span overflow"))?;
535 if address > 59_999 || end_address > 59_999 {
536 return Err(HostLinkError::protocol(format!(
537 "Expansion buffer span out of range: {address}..{end_address} with format '{effective_format}'"
538 )));
539 }
540 Ok(())
541}
542
543fn normalize_dtype(text: &str) -> Result<String, HostLinkError> {
544 match text
545 .trim()
546 .trim_start_matches('.')
547 .to_ascii_uppercase()
548 .as_str()
549 {
550 "U" => Ok("U".to_owned()),
551 "S" => Ok("S".to_owned()),
552 "D" => Ok("D".to_owned()),
553 "L" => Ok("L".to_owned()),
554 "F" => Ok("F".to_owned()),
555 "COMMENT" => Ok("COMMENT".to_owned()),
556 _ => Err(HostLinkError::protocol(format!(
557 "Unsupported logical data type '{text}'."
558 ))),
559 }
560}
561
562fn extract_suffix(raw: &str) -> Result<(&str, String), HostLinkError> {
563 if raw.len() >= 2 && raw.as_bytes()[raw.len() - 2] == b'.' {
564 let suffix = normalize_suffix(&raw[raw.len() - 2..])?;
565 Ok((&raw[..raw.len() - 2], suffix))
566 } else {
567 Ok((raw, String::new()))
568 }
569}
570
571fn format_device_number(device_type: &str, value: u32) -> String {
572 if uses_bit_bank_address(device_type) {
573 return format_bit_bank_number(value);
574 }
575 if uses_xym_bit_address(device_type) {
576 return format_xym_bit_number(value);
577 }
578
579 let Some(range) = device_range(device_type) else {
580 return value.to_string();
581 };
582 if range.base == 16 {
583 format!("{value:X}")
584 } else {
585 value.to_string()
586 }
587}
588
589fn parse_xym_bit_number(device_type: &str, number_text: &str) -> Result<u32, HostLinkError> {
590 let bank_text = if number_text.len() == 1 {
591 "0"
592 } else {
593 &number_text[..number_text.len() - 1]
594 };
595 if !bank_text.bytes().all(|byte| byte.is_ascii_digit()) {
596 return Err(HostLinkError::protocol(format!(
597 "Invalid X/Y device number: {device_type}{number_text} (bank digits must be decimal and bit digit must be 0..F)"
598 )));
599 }
600
601 let bank = bank_text.parse::<u32>().map_err(|_| {
602 HostLinkError::protocol(format!(
603 "Invalid device number for {device_type}: {number_text}"
604 ))
605 })?;
606 let bit = u32::from_str_radix(&number_text[number_text.len() - 1..], 16).map_err(|_| {
607 HostLinkError::protocol(format!(
608 "Invalid device number for {device_type}: {number_text}"
609 ))
610 })?;
611 bank.checked_mul(16)
612 .and_then(|value| value.checked_add(bit))
613 .ok_or_else(|| {
614 HostLinkError::protocol(format!(
615 "Invalid device number for {device_type}: {number_text}"
616 ))
617 })
618}
619
620fn device_range(device_type: &str) -> Option<DeviceRange> {
621 let range = match device_type {
622 "R" => DeviceRange {
623 lo: 0,
624 hi: 199_915,
625 base: 10,
626 },
627 "B" => DeviceRange {
628 lo: 0,
629 hi: 0x7FFF,
630 base: 16,
631 },
632 "MR" => DeviceRange {
633 lo: 0,
634 hi: 399_915,
635 base: 10,
636 },
637 "LR" => DeviceRange {
638 lo: 0,
639 hi: 99_915,
640 base: 10,
641 },
642 "CR" => DeviceRange {
643 lo: 0,
644 hi: 7_915,
645 base: 10,
646 },
647 "VB" => DeviceRange {
648 lo: 0,
649 hi: 0xF9FF,
650 base: 16,
651 },
652 "DM" => DeviceRange {
653 lo: 0,
654 hi: 65_534,
655 base: 10,
656 },
657 "EM" => DeviceRange {
658 lo: 0,
659 hi: 65_534,
660 base: 10,
661 },
662 "FM" => DeviceRange {
663 lo: 0,
664 hi: 32_767,
665 base: 10,
666 },
667 "ZF" => DeviceRange {
668 lo: 0,
669 hi: 524_287,
670 base: 10,
671 },
672 "W" => DeviceRange {
673 lo: 0,
674 hi: 0x7FFF,
675 base: 16,
676 },
677 "TM" => DeviceRange {
678 lo: 0,
679 hi: 511,
680 base: 10,
681 },
682 "Z" => DeviceRange {
683 lo: 1,
684 hi: 12,
685 base: 10,
686 },
687 "T" | "TC" | "TS" | "C" | "CC" | "CS" => DeviceRange {
688 lo: 0,
689 hi: 3_999,
690 base: 10,
691 },
692 "AT" => DeviceRange {
693 lo: 0,
694 hi: 7,
695 base: 10,
696 },
697 "CM" => DeviceRange {
698 lo: 0,
699 hi: 7_599,
700 base: 10,
701 },
702 "VM" => DeviceRange {
703 lo: 0,
704 hi: 589_823,
705 base: 10,
706 },
707 "X" => DeviceRange {
708 lo: 0,
709 hi: 1_999 * 16 + 15,
710 base: 10,
711 },
712 "Y" => DeviceRange {
713 lo: 0,
714 hi: 1_999 * 16 + 15,
715 base: 10,
716 },
717 "M" => DeviceRange {
718 lo: 0,
719 hi: 63_999,
720 base: 10,
721 },
722 "L" => DeviceRange {
723 lo: 0,
724 hi: 15_999,
725 base: 10,
726 },
727 "D" | "E" => DeviceRange {
728 lo: 0,
729 hi: 65_534,
730 base: 10,
731 },
732 "F" => DeviceRange {
733 lo: 0,
734 hi: 32_767,
735 base: 10,
736 },
737 _ => return None,
738 };
739 Some(range)
740}
741
742#[cfg(test)]
743mod tests {
744 use super::{
745 HostLinkAddress, offset_device, parse_device, parse_logical_address, validate_device_span,
746 wr_device_types,
747 };
748
749 #[test]
750 fn parse_device_normalizes_hex_suffix_and_number() {
751 let address = parse_device("w1a.h").unwrap();
752 assert_eq!(address.device_type, "W");
753 assert_eq!(address.number, 0x1A);
754 assert_eq!(address.suffix, ".H");
755 assert_eq!(address.to_text().unwrap(), "W1A.H");
756 }
757
758 #[test]
759 fn parse_logical_bit_index_uses_hex_notation() {
760 let logical = parse_logical_address("dm100.a").unwrap();
761 assert_eq!(logical.to_text().unwrap(), "DM100.A");
762 assert_eq!(logical.bit_index, Some(10));
763 }
764
765 #[test]
766 fn normalize_plain_address_keeps_default_r_omission_rule() {
767 assert_eq!(HostLinkAddress::normalize("100").unwrap(), "R100");
768 }
769
770 #[test]
771 fn parse_logical_comment_address_round_trips() {
772 let logical = parse_logical_address("dm100:comment").unwrap();
773 assert_eq!(logical.to_text().unwrap(), "DM100:COMMENT");
774 assert_eq!(logical.data_type, "COMMENT");
775 }
776
777 #[test]
778 fn parse_logical_direct_bit_defaults_to_bool_read() {
779 let logical = parse_logical_address("cr0").unwrap();
780 assert_eq!(logical.to_text().unwrap(), "CR000");
781 assert_eq!(logical.data_type, "");
782 }
783
784 #[test]
785 fn parse_device_rejects_invalid_bit_bank_numbers() {
786 assert!(parse_device("R016").is_err());
787 assert!(parse_device("MR116").is_err());
788 assert!(parse_device("LR99916").is_err());
789 assert!(parse_device("CR7916").is_err());
790 }
791
792 #[test]
793 fn parse_device_accepts_valid_bit_bank_boundaries() {
794 assert_eq!(parse_device("R0").unwrap().to_text().unwrap(), "R000");
795 assert_eq!(parse_device("R1").unwrap().to_text().unwrap(), "R001");
796 assert_eq!(parse_device("R015").unwrap().to_text().unwrap(), "R015");
797 assert_eq!(parse_device("R100").unwrap().to_text().unwrap(), "R100");
798 assert_eq!(parse_device("MR115").unwrap().to_text().unwrap(), "MR115");
799 assert_eq!(parse_device("CR0").unwrap().to_text().unwrap(), "CR000");
800 assert_eq!(parse_device("CR7915").unwrap().to_text().unwrap(), "CR7915");
801 }
802
803 #[test]
804 fn bit_bank_offsets_cross_bank_boundaries_by_bit_position() {
805 let start = parse_device("CR3614").unwrap();
806 assert_eq!(offset_device(&start, 0).unwrap(), "CR3614");
807 assert_eq!(offset_device(&start, 1).unwrap(), "CR3615");
808 assert_eq!(offset_device(&start, 2).unwrap(), "CR3700");
809 assert_eq!(offset_device(&start, 18).unwrap(), "CR3800");
810 }
811
812 #[test]
813 fn validate_device_span_uses_bit_bank_point_count() {
814 validate_device_span("CR", 7900, "", 16).unwrap();
815 assert!(validate_device_span("CR", 7900, "", 17).is_err());
816 }
817
818 #[test]
819 fn validate_device_span_treats_at_32bit_as_device_points() {
820 validate_device_span("AT", 7, ".D", 1).unwrap();
821 validate_device_span("AT", 0, ".D", 8).unwrap();
822 assert!(validate_device_span("AT", 1, ".D", 8).is_err());
823 }
824
825 #[test]
826 fn parse_device_accepts_high_xym_m_addresses() {
827 assert_eq!(parse_device("M63872").unwrap().to_text().unwrap(), "M63872");
828 assert!(parse_device("M64000").is_err());
829 }
830
831 #[test]
832 fn parse_device_uses_decimal_bank_hex_bit_for_xym_bits() {
833 let address = parse_device("X390").unwrap();
834 assert_eq!(address.device_type, "X");
835 assert_eq!(address.number, 39 * 16);
836 assert_eq!(address.to_text().unwrap(), "X390");
837
838 assert_eq!(
839 parse_device("X3F0").unwrap_err().to_string(),
840 "Invalid X/Y device number: X3F0 (bank digits must be decimal and bit digit must be 0..F)"
841 );
842 assert_eq!(parse_device("X1999F").unwrap().to_text().unwrap(), "X1999F");
843 assert!(parse_device("X20000").is_err());
844 assert_eq!(parse_device("Y1999F").unwrap().to_text().unwrap(), "Y1999F");
845 assert!(parse_device("Y20000").is_err());
846 }
847
848 #[test]
849 fn validate_device_span_allows_xym_m_upper_bound() {
850 validate_device_span("X", 1_999 * 16 + 15, "", 1).unwrap();
851 assert!(validate_device_span("X", 1_999 * 16 + 15, "", 2).is_err());
852 validate_device_span("Y", 1_999 * 16 + 15, "", 1).unwrap();
853 assert!(validate_device_span("Y", 1_999 * 16 + 15, "", 2).is_err());
854 validate_device_span("M", 63_998, "", 1).unwrap();
855 validate_device_span("M", 63_998, "", 2).unwrap();
856 assert!(validate_device_span("M", 63_999, "", 2).is_err());
857 assert!(validate_device_span("L", 16_000, "", 1).is_err());
858 }
859
860 #[test]
861 fn parse_logical_suffix_preserves_explicit_type() {
862 let logical = parse_logical_address("dm100.s").unwrap();
863 assert_eq!(logical.to_text().unwrap(), "DM100:S");
864 assert_eq!(logical.data_type, "S");
865 }
866
867 #[test]
868 fn parse_logical_counter_defaults_to_dword_read() {
869 let logical = parse_logical_address("t0").unwrap();
870 assert_eq!(logical.to_text().unwrap(), "T0");
871 assert_eq!(logical.data_type, "D");
872 }
873
874 #[test]
875 fn parse_logical_at_defaults_to_dword_read() {
876 let logical = parse_logical_address("at7").unwrap();
877 assert_eq!(logical.to_text().unwrap(), "AT7");
878 assert_eq!(logical.data_type, "D");
879 }
880
881 #[test]
882 fn wr_device_types_exclude_at() {
883 assert!(!wr_device_types().contains(&"AT"));
884 assert!(wr_device_types().contains(&"DM"));
885 assert!(wr_device_types().contains(&"TS"));
886 }
887}