use super::hash_table::CallsignHashTable;
const C1: &[u8] = b" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/";
const C2: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const C3: &[u8] = b"0123456789";
const C4: &[u8] = b" ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const C38: &[u8] = b" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/";
const FREE_TEXT: &[u8] = b" 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?";
const NTOKENS: u32 = 2_063_592;
const MAX22: u32 = 4_194_304;
const MAX_GRID4: u32 = 32_400;
fn read_bits(msg: &[u8; 77], start: usize, len: usize) -> u32 {
let mut n = 0u32;
for i in start..start + len {
n = (n << 1) | (msg[i] & 1) as u32;
}
n
}
fn read_bits_u64(msg: &[u8; 77], start: usize, len: usize) -> u64 {
let mut n = 0u64;
for i in start..start + len {
n = (n << 1) | (msg[i] & 1) as u64;
}
n
}
fn unpack28(n28: u32) -> String {
if n28 < NTOKENS {
return match n28 {
0 => "DE".to_string(),
1 => "QRZ".to_string(),
2 => "CQ".to_string(),
3..=1002 => format!("CQ {:03}", n28 - 3),
_ => {
let n = n28 - 1003;
let i1 = (n / (27 * 27 * 27)) as usize;
let n = n % (27 * 27 * 27);
let i2 = (n / (27 * 27)) as usize;
let n = n % (27 * 27);
let i3 = (n / 27) as usize;
let i4 = (n % 27) as usize;
if i1 >= C4.len() || i2 >= C4.len() || i3 >= C4.len() || i4 >= C4.len() {
return "<?>".to_string();
}
let suffix: String = [C4[i1], C4[i2], C4[i3], C4[i4]]
.iter()
.map(|&b| b as char)
.collect();
format!("CQ {}", suffix.trim())
}
};
}
let n = n28 - NTOKENS;
if n < MAX22 {
return "<...>".to_string();
}
let n = n - MAX22;
let i1 = (n / (36 * 10 * 27 * 27 * 27)) as usize;
let n = n % (36 * 10 * 27 * 27 * 27);
let i2 = (n / (10 * 27 * 27 * 27)) as usize;
let n = n % (10 * 27 * 27 * 27);
let i3 = (n / (27 * 27 * 27)) as usize;
let n = n % (27 * 27 * 27);
let i4 = (n / (27 * 27)) as usize;
let n = n % (27 * 27);
let i5 = (n / 27) as usize;
let i6 = (n % 27) as usize;
if i1 >= C1.len()
|| i2 >= C2.len()
|| i3 >= C3.len()
|| i4 >= C4.len()
|| i5 >= C4.len()
|| i6 >= C4.len()
{
return "?????".to_string();
}
let s: String = [C1[i1], C2[i2], C3[i3], C4[i4], C4[i5], C4[i6]]
.iter()
.map(|&b| b as char)
.collect();
s.trim().to_string()
}
fn unpack28_h(n28: u32, ht: &CallsignHashTable) -> String {
if n28 >= NTOKENS {
let n = n28 - NTOKENS;
if n < MAX22 {
if let Some(resolved) = ht.lookup22(n) {
return resolved;
}
return "<...>".to_string();
}
}
unpack28(n28)
}
fn resolve_hash12(n12: u32, ht: &CallsignHashTable) -> String {
if let Some(call) = ht.lookup12(n12) {
format!("<{}>", call)
} else {
"<...>".to_string()
}
}
fn to_grid4(n: u32) -> Option<String> {
if n > MAX_GRID4 {
return None;
}
let j1 = n / (18 * 10 * 10);
let n = n % (18 * 10 * 10);
let j2 = n / (10 * 10);
let n = n % (10 * 10);
let j3 = n / 10;
let j4 = n % 10;
if j1 > 17 || j2 > 17 {
return None;
}
Some(format!(
"{}{}{}{}",
(b'A' + j1 as u8) as char,
(b'A' + j2 as u8) as char,
(b'0' + j3 as u8) as char,
(b'0' + j4 as u8) as char,
))
}
fn unpack_free_text(msg: &[u8; 77]) -> String {
let mut n = 0u128;
for i in 0..71 {
n = (n << 1) | (msg[i] & 1) as u128;
}
let mut chars = [b' '; 13];
for i in (0..13).rev() {
chars[i] = FREE_TEXT[(n % 42) as usize];
n /= 42;
}
String::from_utf8(chars.to_vec())
.unwrap_or_default()
.trim()
.to_string()
}
pub fn unpack77(msg: &[u8; 77]) -> Option<String> {
let n3 = read_bits(msg, 71, 3);
let i3 = read_bits(msg, 74, 3);
match i3 {
0 => match n3 {
0 => {
let text = unpack_free_text(msg);
if text.is_empty() { None } else { Some(text) }
}
1 => {
let n28a = read_bits(msg, 0, 28);
let n28b = read_bits(msg, 28, 28);
let n5 = read_bits(msg, 66, 5);
let irpt = 2 * n5 as i32 - 30;
let crpt = if irpt >= 0 {
format!("+{:02}", irpt)
} else {
format!("{:03}", irpt)
};
let c1 = unpack28(n28a);
let c2 = unpack28(n28b);
Some(format!("{} RR73; {} <...> {}", c1, c2, crpt))
}
3 | 4 => {
let c1 = unpack28(read_bits(msg, 0, 28));
let c2 = unpack28(read_bits(msg, 28, 28));
Some(format!("{} {} [FD]", c1, c2))
}
_ => None,
},
1 | 2 => {
let n28a = read_bits(msg, 0, 28);
let ipa = msg[28] & 1;
let n28b = read_bits(msg, 29, 28);
let ipb = msg[57] & 1;
let ir = msg[58] & 1;
let igrid = read_bits(msg, 59, 15);
let mut c1 = unpack28(n28a);
let mut c2 = unpack28(n28b);
if ipa == 1 && !c1.starts_with('<') && !c1.starts_with("CQ") {
c1.push_str(if i3 == 1 { "/R" } else { "/P" });
}
if ipb == 1 && !c2.starts_with('<') {
c2.push_str(if i3 == 1 { "/R" } else { "/P" });
}
let report = if igrid <= MAX_GRID4 {
let grid = to_grid4(igrid)?;
if ir == 0 { grid } else { format!("R {}", grid) }
} else {
let irpt = igrid - MAX_GRID4;
match irpt {
1 => String::new(),
2 => "RRR".to_string(),
3 => "RR73".to_string(),
4 => "73".to_string(),
n => {
let mut isnr = n as i32 - 35;
if isnr > 50 {
isnr -= 101;
}
let sign = if isnr >= 0 { "+" } else { "" };
if ir == 1 {
format!("R{}{:02}", sign, isnr)
} else {
format!("{}{:02}", sign, isnr)
}
}
}
};
if report.is_empty() {
Some(format!("{} {}", c1, c2))
} else {
Some(format!("{} {} {}", c1, c2, report))
}
}
3 => {
let _itu = msg[0] & 1;
let n28a = read_bits(msg, 1, 28);
let n28b = read_bits(msg, 29, 28);
let c1 = unpack28(n28a);
let c2 = unpack28(n28b);
Some(format!("{} {} [RTTY]", c1, c2))
}
4 => {
let n58 = read_bits_u64(msg, 12, 58);
let iflip = msg[70] & 1;
let nrpt = read_bits(msg, 71, 2);
let icq = msg[73] & 1;
let mut n = n58;
let mut buf = [b' '; 11];
for i in (0..11).rev() {
buf[i] = C38[(n % 38) as usize];
n /= 38;
}
let nonstd = String::from_utf8(buf.to_vec())
.unwrap_or_default()
.trim()
.to_string();
if icq == 1 {
return Some(format!("CQ {}", nonstd));
}
let (c1, c2) = if iflip == 0 {
("<...>".to_string(), nonstd)
} else {
(nonstd, "<...>".to_string())
};
match nrpt {
0 => Some(format!("{} {}", c1, c2)),
1 => Some(format!("{} {} RRR", c1, c2)),
2 => Some(format!("{} {} RR73", c1, c2)),
3 => Some(format!("{} {} 73", c1, c2)),
_ => None,
}
}
_ => None,
}
}
pub fn unpack77_with_hash(msg: &[u8; 77], ht: &CallsignHashTable) -> Option<String> {
let n3 = read_bits(msg, 71, 3);
let i3 = read_bits(msg, 74, 3);
match i3 {
0 => match n3 {
0 => {
let text = unpack_free_text(msg);
if text.is_empty() { None } else { Some(text) }
}
1 => {
let n28a = read_bits(msg, 0, 28);
let n28b = read_bits(msg, 28, 28);
let n10 = read_bits(msg, 56, 10);
let n5 = read_bits(msg, 66, 5);
let irpt = 2 * n5 as i32 - 30;
let crpt = if irpt >= 0 {
format!("+{:02}", irpt)
} else {
format!("{:03}", irpt)
};
let c1 = unpack28_h(n28a, ht);
let c2 = unpack28_h(n28b, ht);
let c3 = if let Some(call) = ht.lookup10(n10) {
format!("<{}>", call)
} else {
"<...>".to_string()
};
Some(format!("{} RR73; {} {} {}", c1, c2, c3, crpt))
}
3 | 4 => {
let c1 = unpack28_h(read_bits(msg, 0, 28), ht);
let c2 = unpack28_h(read_bits(msg, 28, 28), ht);
Some(format!("{} {} [FD]", c1, c2))
}
_ => None,
},
1 | 2 => {
let n28a = read_bits(msg, 0, 28);
let ipa = msg[28] & 1;
let n28b = read_bits(msg, 29, 28);
let ipb = msg[57] & 1;
let ir = msg[58] & 1;
let igrid = read_bits(msg, 59, 15);
let mut c1 = unpack28_h(n28a, ht);
let mut c2 = unpack28_h(n28b, ht);
if ipa == 1 && !c1.starts_with('<') && !c1.starts_with("CQ") {
c1.push_str(if i3 == 1 { "/R" } else { "/P" });
}
if ipb == 1 && !c2.starts_with('<') {
c2.push_str(if i3 == 1 { "/R" } else { "/P" });
}
let report = if igrid <= MAX_GRID4 {
let grid = to_grid4(igrid)?;
if ir == 0 { grid } else { format!("R {}", grid) }
} else {
let irpt = igrid - MAX_GRID4;
match irpt {
1 => String::new(),
2 => "RRR".to_string(),
3 => "RR73".to_string(),
4 => "73".to_string(),
n => {
let mut isnr = n as i32 - 35;
if isnr > 50 {
isnr -= 101;
}
let sign = if isnr >= 0 { "+" } else { "" };
if ir == 1 {
format!("R{}{:02}", sign, isnr)
} else {
format!("{}{:02}", sign, isnr)
}
}
}
};
if report.is_empty() {
Some(format!("{} {}", c1, c2))
} else {
Some(format!("{} {} {}", c1, c2, report))
}
}
3 => {
let _itu = msg[0] & 1;
let n28a = read_bits(msg, 1, 28);
let n28b = read_bits(msg, 29, 28);
let c1 = unpack28_h(n28a, ht);
let c2 = unpack28_h(n28b, ht);
Some(format!("{} {} [RTTY]", c1, c2))
}
4 => {
let n12 = read_bits(msg, 0, 12);
let n58 = read_bits_u64(msg, 12, 58);
let iflip = msg[70] & 1;
let nrpt = read_bits(msg, 71, 2);
let icq = msg[73] & 1;
let mut n = n58;
let mut buf = [b' '; 11];
for i in (0..11).rev() {
buf[i] = C38[(n % 38) as usize];
n /= 38;
}
let nonstd = String::from_utf8(buf.to_vec())
.unwrap_or_default()
.trim()
.to_string();
if icq == 1 {
return Some(format!("CQ {}", nonstd));
}
let hashed = resolve_hash12(n12, ht);
let (c1, c2) = if iflip == 0 {
(hashed, nonstd)
} else {
(nonstd, hashed)
};
match nrpt {
0 => Some(format!("{} {}", c1, c2)),
1 => Some(format!("{} {} RRR", c1, c2)),
2 => Some(format!("{} {} RR73", c1, c2)),
3 => Some(format!("{} {} 73", c1, c2)),
_ => None,
}
}
_ => None,
}
}
pub fn is_standard_callsign(call: &str) -> bool {
let call = call.trim();
let base = if call.ends_with("/R") || call.ends_with("/P") {
&call[..call.len() - 2]
} else {
call
};
let b = base.as_bytes();
if b.is_empty() || b.len() > 6 {
return false;
}
let mut split = None;
for i in (0..b.len()).rev() {
if b[i].is_ascii_digit() {
if b[i + 1..].iter().all(|&c| c.is_ascii_uppercase()) {
split = Some(i);
break;
}
}
}
let split = match split {
Some(s) => s,
None => return false,
};
let part1 = &b[..split];
let part2 = &b[split..];
if part2.is_empty() || !part2[0].is_ascii_digit() {
return false;
}
if part2.len() > 4 {
return false;
}
if !part2[1..].iter().all(|c| c.is_ascii_uppercase()) {
return false;
}
match part1.len() {
0 => true, 1 => part1[0].is_ascii_uppercase() || part1[0].is_ascii_digit(),
2 => {
let (a, b) = (part1[0], part1[1]);
(a.is_ascii_uppercase() && b.is_ascii_uppercase()) || (a.is_ascii_uppercase() && b.is_ascii_digit()) || (a.is_ascii_digit() && b.is_ascii_uppercase()) }
_ => false,
}
}
fn is_base_callsign(s: &str) -> bool {
let b = s.as_bytes();
if b.len() < 2 || b.len() > 7 {
return false;
}
let mut split = None;
for i in (0..b.len()).rev() {
if b[i].is_ascii_digit() && b[i + 1..].iter().all(|c| c.is_ascii_uppercase()) {
split = Some(i);
break;
}
}
let split = match split {
Some(s) if s + 1 < b.len() => s, _ => return false,
};
let prefix = &b[..split];
let suffix = &b[split + 1..];
if prefix.is_empty() || prefix.len() > 3 {
return false;
}
if !prefix.iter().all(|c| c.is_ascii_alphanumeric()) {
return false;
}
if !prefix.iter().any(|c| c.is_ascii_alphabetic()) {
return false;
}
suffix.len() <= 4 && suffix.iter().all(|c| c.is_ascii_uppercase())
}
pub fn is_valid_callsign(call: &str) -> bool {
if is_standard_callsign(call) {
return true;
}
let parts: Vec<&str> = call.split('/').collect();
match parts.len() {
1 => is_base_callsign(parts[0]),
2 => {
let (a, b) = (parts[0], parts[1]);
let a_base = is_base_callsign(a);
let b_base = is_base_callsign(b);
let a_mod = !a.is_empty()
&& a.len() <= 3
&& a.as_bytes().iter().all(|c| c.is_ascii_alphanumeric());
let b_mod = !b.is_empty()
&& b.len() <= 3
&& b.as_bytes().iter().all(|c| c.is_ascii_alphanumeric());
(a_base && b_mod) || (a_mod && b_base) || (a_base && b_base)
}
_ => false,
}
}
pub fn is_plausible_message(text: &str) -> bool {
let words: Vec<&str> = text.split_whitespace().collect();
if words.is_empty() {
return false;
}
if text.contains("[FD]") || text.contains("[RTTY]") || text.contains("RR73;") {
return true;
}
for (idx, &w) in words.iter().enumerate() {
if matches!(
w,
"CQ" | "DE" | "QRZ" | "RRR" | "RR73" | "73" | "R" | "" | "DX"
) {
continue;
}
if w.starts_with("CQ") {
continue;
}
if idx == 1 && words[0] == "CQ" && w.len() <= 4 && w.bytes().all(|b| b.is_ascii_uppercase())
{
continue;
}
if w.starts_with('<') && w.ends_with('>') {
continue;
}
if w.starts_with("R+") || w.starts_with("R-") {
continue;
}
if (w.starts_with('+') || w.starts_with('-')) && w[1..].parse::<i32>().is_ok() {
continue;
}
if w.len() == 4 {
let b = w.as_bytes();
if b[0].is_ascii_uppercase()
&& b[1].is_ascii_uppercase()
&& b[2].is_ascii_digit()
&& b[3].is_ascii_digit()
{
continue;
}
}
if !is_valid_callsign(w) {
return false;
}
}
true
}
fn write_bits(msg: &mut [u8; 77], start: usize, len: usize, val: u32) {
for i in 0..len {
msg[start + i] = ((val >> (len - 1 - i)) & 1) as u8;
}
}
pub fn pack28(call: &str) -> Option<u32> {
let call = call.trim();
match call {
"DE" => return Some(0),
"QRZ" => return Some(1),
"CQ" => return Some(2),
_ => {}
}
if let Some(suffix) = call.strip_prefix("CQ ") {
let suffix = suffix.trim();
if !suffix.is_empty() {
if let Ok(n) = suffix.parse::<u32>()
&& n <= 999
{
return Some(3 + n);
}
let sb = suffix.as_bytes();
if sb.len() <= 4 && sb.iter().all(|c| c.is_ascii_uppercase()) {
let mut buf = [b' '; 4];
for (i, &b) in sb.iter().enumerate() {
buf[i] = b;
}
let i1 = C4.iter().position(|&c| c == buf[0])?;
let i2 = C4.iter().position(|&c| c == buf[1])?;
let i3 = C4.iter().position(|&c| c == buf[2])?;
let i4 = C4.iter().position(|&c| c == buf[3])?;
return Some(1003 + ((i1 * 27 + i2) * 27 + i3) as u32 * 27 + i4 as u32);
}
return None; }
}
let bytes = call.as_bytes();
if bytes.is_empty() || bytes.len() > 6 {
return None;
}
let mut buf = [b' '; 6];
if bytes.len() >= 3 && bytes[2].is_ascii_digit() {
for (i, &b) in bytes.iter().enumerate().take(6) {
buf[i] = b.to_ascii_uppercase();
}
} else if bytes.len() >= 2 && bytes[1].is_ascii_digit() {
buf[0] = b' ';
for (i, &b) in bytes.iter().enumerate() {
if i + 1 < 6 {
buf[i + 1] = b.to_ascii_uppercase();
}
}
} else {
return None; }
if !buf[2].is_ascii_digit() {
return None;
}
let i1 = C1.iter().position(|&c| c == buf[0])?;
let i2 = C2.iter().position(|&c| c == buf[1])?;
let i3 = C3.iter().position(|&c| c == buf[2])?;
let i4 = C4.iter().position(|&c| c == buf[3])?;
let i5 = C4.iter().position(|&c| c == buf[4])?;
let i6 = C4.iter().position(|&c| c == buf[5])?;
let n = ((((i1 as u32 * 36 + i2 as u32) * 10 + i3 as u32) * 27 + i4 as u32) * 27 + i5 as u32)
* 27
+ i6 as u32;
Some(NTOKENS + MAX22 + n)
}
pub fn pack_grid4(grid: &str) -> Option<u32> {
let g = grid.as_bytes();
if g.len() != 4 {
return None;
}
let j1 = g[0].to_ascii_uppercase().wrapping_sub(b'A') as u32;
let j2 = g[1].to_ascii_uppercase().wrapping_sub(b'A') as u32;
let j3 = g[2].wrapping_sub(b'0') as u32;
let j4 = g[3].wrapping_sub(b'0') as u32;
if j1 > 17 || j2 > 17 || j3 > 9 || j4 > 9 {
return None;
}
Some(((j1 * 18 + j2) * 10 + j3) * 10 + j4)
}
pub fn pack77_type1(call1: &str, call2: &str, grid: &str) -> Option<[u8; 77]> {
let n28a = pack28(call1)?;
let n28b = pack28(call2)?;
let igrid = pack_grid4(grid)?;
let mut msg = [0u8; 77];
write_bits(&mut msg, 0, 28, n28a); write_bits(&mut msg, 29, 28, n28b); write_bits(&mut msg, 59, 15, igrid); write_bits(&mut msg, 74, 3, 1); Some(msg)
}
pub fn pack77(call1: &str, call2: &str, report: &str) -> Option<[u8; 77]> {
let n28a = pack28(call1)?;
let n28b = pack28(call2)?;
let report = report.trim();
let (igrid, ir): (u32, u8) = if report.is_empty() {
(MAX_GRID4 + 1, 0)
} else if report == "RRR" {
(MAX_GRID4 + 2, 0)
} else if report == "RR73" {
(MAX_GRID4 + 3, 0)
} else if report == "73" {
(MAX_GRID4 + 4, 0)
} else if report.len() == 4 && pack_grid4(report).is_some() {
(pack_grid4(report).unwrap(), 0)
} else {
let (r_prefix, num_str) = if let Some(s) = report.strip_prefix('R') {
(1u8, s)
} else {
(0u8, report)
};
let snr: i32 = num_str.parse().ok()?;
if !(-50..=49).contains(&snr) {
return None;
}
let mut isnr = snr + 35;
if isnr < 0 {
isnr += 101;
}
(MAX_GRID4 + isnr as u32, r_prefix)
};
let mut msg = [0u8; 77];
write_bits(&mut msg, 0, 28, n28a);
write_bits(&mut msg, 29, 28, n28b);
msg[58] = ir; write_bits(&mut msg, 59, 15, igrid);
write_bits(&mut msg, 74, 3, 1); Some(msg)
}
fn write_bits_u64(msg: &mut [u8; 77], start: usize, len: usize, val: u64) {
for i in 0..len {
msg[start + i] = ((val >> (len - 1 - i)) & 1) as u8;
}
}
pub fn pack77_type4(nonstd: &str, std_call: &str, report: &str, is_cq: bool) -> Option<[u8; 77]> {
let nonstd = nonstd.trim().to_ascii_uppercase();
let nb = nonstd.as_bytes();
if nb.is_empty() || nb.len() > 11 {
return None;
}
if !nb.iter().all(|c| C38.contains(c)) {
return None;
}
let mut n58: u64 = 0;
let mut padded = [b' '; 11];
let offset = 11 - nb.len();
for (i, &b) in nb.iter().enumerate() {
padded[offset + i] = b;
}
for &ch in &padded {
let idx = C38.iter().position(|&c| c == ch)?;
n58 = n58 * 38 + idx as u64;
}
let n12 = if is_cq {
0u32 } else {
use super::hash_table::ihashcall;
ihashcall(std_call, 12)
};
let nrpt: u32 = match report.trim() {
"" => 0,
"RRR" => 1,
"RR73" => 2,
"73" => 3,
_ => return None,
};
let iflip: u8 = if is_cq || pack28(std_call).is_some() {
0
} else {
1
};
let icq: u8 = if is_cq { 1 } else { 0 };
let mut msg = [0u8; 77];
write_bits(&mut msg, 0, 12, n12); write_bits_u64(&mut msg, 12, 58, n58); msg[70] = iflip; write_bits(&mut msg, 71, 2, nrpt); msg[73] = icq; write_bits(&mut msg, 74, 3, 4); Some(msg)
}
pub fn pack77_free_text(text: &str) -> Option<[u8; 77]> {
let text = text.to_ascii_uppercase();
let bytes = text.as_bytes();
if bytes.is_empty() || bytes.len() > 13 {
return None;
}
let mut padded = [b' '; 13];
for (i, &b) in bytes.iter().enumerate() {
padded[i] = b;
}
let mut n: u128 = 0;
for &ch in &padded {
let idx = FREE_TEXT.iter().position(|&c| c == ch)? as u128;
n = n * 42 + idx;
}
let mut msg = [0u8; 77];
for i in 0..71 {
msg[i] = ((n >> (70 - i)) & 1) as u8;
}
Some(msg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unpack28_does_not_panic_for_extended_range() {
for n28 in [1003u32, 532443, 532444, 1_000_000, NTOKENS - 1] {
let _ = unpack28(n28);
}
}
fn hex_to_msg77(hex: &str) -> [u8; 77] {
assert_eq!(hex.len(), 20, "need exactly 20 hex chars (10 bytes)");
let bytes: Vec<u8> = (0..10)
.map(|i| u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).unwrap())
.collect();
let mut msg = [0u8; 77];
for (j, bit) in msg.iter_mut().enumerate() {
*bit = (bytes[j / 8] >> (7 - j % 8)) & 1;
}
msg
}
#[test]
fn decode_cq_r7iw_ln35() {
let msg = hex_to_msg77("0000002059654a94a3c8");
let text = unpack77(&msg).expect("should decode");
assert_eq!(text, "CQ R7IW LN35");
}
#[test]
fn decode_cq_dx_r6wa_ln32() {
let msg = hex_to_msg77("000046f059519f14a308");
let text = unpack77(&msg).expect("should decode");
assert_eq!(text, "CQ DX R6WA LN32");
}
#[test]
fn silence_bits_returns_none_or_empty() {
let msg = [0u8; 77];
assert!(unpack77(&msg).is_none());
}
#[test]
fn pack28_roundtrip() {
for call in &["JQ1QSO", "3Y0Z", "R7IW", "JA1ABC", "W1AW", "VK2RG"] {
let n = pack28(call).unwrap_or_else(|| panic!("pack28 failed for {call}"));
let decoded = unpack28(n);
assert_eq!(
decoded,
call.trim(),
"roundtrip mismatch for {call}: got {decoded}"
);
}
assert_eq!(pack28("CQ"), Some(2));
assert_eq!(pack28("DE"), Some(0));
assert_eq!(pack28("QRZ"), Some(1));
for cq in &["CQ POTA", "CQ SOTA", "CQ DX", "CQ NA", "CQ EU"] {
let n = pack28(cq).unwrap_or_else(|| panic!("pack28 failed for {cq}"));
let decoded = unpack28(n);
assert_eq!(decoded, *cq, "CQ suffix roundtrip mismatch for {cq}");
}
let n = pack28("CQ 001").unwrap();
assert_eq!(unpack28(n), "CQ 001");
let n = pack28("CQ 999").unwrap();
assert_eq!(unpack28(n), "CQ 999");
}
#[test]
fn pack77_type1_roundtrip() {
let msg = pack77_type1("CQ", "3Y0Z", "JD34").expect("pack failed");
let text = unpack77(&msg).expect("unpack failed");
assert_eq!(text, "CQ 3Y0Z JD34");
let msg2 = pack77_type1("CQ", "JQ1QSO", "PM95").expect("pack failed");
let text2 = unpack77(&msg2).expect("unpack failed");
assert_eq!(text2, "CQ JQ1QSO PM95");
}
#[test]
fn standard_callsign_valid() {
assert!(is_standard_callsign("JA1ABC"));
assert!(is_standard_callsign("3Y0Z"));
assert!(is_standard_callsign("W1AW"));
assert!(is_standard_callsign("VK2RG"));
assert!(is_standard_callsign("R7IW"));
assert!(is_standard_callsign("JQ1QSO"));
assert!(is_standard_callsign("TA6CQ"));
assert!(is_standard_callsign("JA1ABC/P"));
assert!(is_standard_callsign("JM1VWQ/R"));
}
#[test]
fn standard_callsign_invalid() {
assert!(!is_standard_callsign("NFW/0811"));
assert!(!is_standard_callsign("791JLI"));
assert!(!is_standard_callsign(""));
assert!(!is_standard_callsign("ABCDEFG"));
assert!(!is_standard_callsign("123"));
}
#[test]
fn standard_callsign_edge_cases() {
assert!(is_standard_callsign("SY2XHO")); assert!(is_standard_callsign("8I9NIH")); }
#[test]
fn valid_callsign_standard() {
assert!(is_valid_callsign("JA1ABC"));
assert!(is_valid_callsign("3Y0Z"));
assert!(is_valid_callsign("W1AW"));
assert!(is_valid_callsign("W1AW/P"));
assert!(is_valid_callsign("JM1VWQ/R"));
assert!(is_valid_callsign("W1A")); }
#[test]
fn valid_callsign_nonstandard() {
assert!(is_valid_callsign("JL1NIE/1")); assert!(is_valid_callsign("JL1NIE/P")); assert!(is_valid_callsign("F/JA1ABC")); assert!(is_valid_callsign("ZS6/JA1ABC")); assert!(is_valid_callsign("JR9ECD/P")); assert!(is_valid_callsign("3DA0WPX")); assert!(is_valid_callsign("JA1ABC/QRP")); }
#[test]
fn valid_callsign_rejected() {
assert!(!is_valid_callsign("NFW/0811")); assert!(!is_valid_callsign("ABCDEF")); assert!(!is_valid_callsign(""));
assert!(!is_valid_callsign("A")); assert!(!is_valid_callsign("HELLO+WORLD")); assert!(!is_valid_callsign("123")); assert!(!is_valid_callsign("//////")); }
#[test]
fn plausible_message_standard() {
assert!(is_plausible_message("CQ JA1ABC PM95"));
assert!(is_plausible_message("CQ DX R6WA LN32"));
assert!(is_plausible_message("JA1ABC 3Y0Z -12"));
assert!(is_plausible_message("JA1ABC 3Y0Z RRR"));
assert!(is_plausible_message("JA1ABC 3Y0Z 73"));
assert!(is_plausible_message("CQ 3Y0Z JD34"));
assert!(is_plausible_message("OH3NIV ZS6S R-12"));
}
#[test]
fn plausible_message_nonstandard() {
assert!(is_plausible_message("JR1UJX/P JH1GIN PM96"));
assert!(is_plausible_message("<...> JH4IUV/P RR73"));
assert!(is_plausible_message("CQ JR9ECD/P"));
assert!(is_plausible_message("F/JA1ABC 3Y0Z -12"));
assert!(is_plausible_message("CQ SOTA JL1NIE/1"));
assert!(is_plausible_message("<...> JA1ABC -12"));
assert!(is_plausible_message("JA1ABC <...> RRR"));
assert!(is_plausible_message("CQ POTA JA1ABC PM95"));
assert!(is_plausible_message("CQ NA W1AW FN31"));
assert!(is_plausible_message("CQ SOTA JL1NIE/P"));
assert!(is_plausible_message("JA1ABC 3Y0Z [FD]"));
}
#[test]
fn plausible_message_rejected() {
assert!(!is_plausible_message("NFW/0811 73"));
assert!(!is_plausible_message("ABCDEF GHIJKL"));
assert!(!is_plausible_message(""));
}
#[test]
fn pack77_type4_roundtrip() {
let msg = pack77_type4("JL1NIE/P", "", "", true).expect("pack failed");
let text = unpack77(&msg).expect("unpack failed");
assert_eq!(text, "CQ JL1NIE/P");
let msg = pack77_type4("JL1NIE/1", "JA1ABC", "", false).expect("pack failed");
let text = unpack77(&msg).expect("unpack failed");
assert!(
text.contains("JL1NIE/1"),
"should contain non-std call: {text}"
);
assert!(
text.contains("<...>"),
"should contain hash placeholder: {text}"
);
let msg = pack77_type4("JR9ECD/P", "W1AW", "73", false).expect("pack failed");
let text = unpack77(&msg).expect("unpack failed");
assert!(text.contains("JR9ECD/P"), "got: {text}");
assert!(text.contains("73"), "got: {text}");
let msg = pack77_type4("F/JA1ABC", "W1AW", "RR73", false).expect("pack failed");
let text = unpack77(&msg).expect("unpack failed");
assert!(text.contains("F/JA1ABC"), "got: {text}");
assert!(text.contains("RR73"), "got: {text}");
}
#[test]
fn type4_hash_register_then_resolve() {
let mut ht = CallsignHashTable::new();
ht.insert("JA1ABC");
let msg = pack77_type4("JL1NIE/1", "JA1ABC", "", false).expect("pack failed");
let text_no_ht = unpack77(&msg).expect("unpack failed");
assert!(
text_no_ht.contains("<...>"),
"without hash table: {text_no_ht}"
);
assert!(
text_no_ht.contains("JL1NIE/1"),
"without hash table: {text_no_ht}"
);
let text_ht = unpack77_with_hash(&msg, &ht).expect("unpack failed");
assert!(
text_ht.contains("<JA1ABC>"),
"with hash table should resolve: {text_ht}"
);
assert!(text_ht.contains("JL1NIE/1"), "with hash table: {text_ht}");
assert!(
is_plausible_message(&text_ht),
"resolved message should be plausible: {text_ht}"
);
}
#[test]
fn pack77_type4_cq_with_pack77() {
let msg = pack77_type4("JL1NIE/1", "", "", true).expect("pack failed");
let text = unpack77(&msg).expect("unpack failed");
assert_eq!(text, "CQ JL1NIE/1");
assert!(is_plausible_message(&text));
}
#[test]
fn pack77_free_text_roundtrip() {
let msg = pack77_free_text("JA/TK-001").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JA/TK-001");
let msg = pack77_free_text("JP-1001").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JP-1001");
let msg = pack77_free_text("JCC 100110").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JCC 100110");
let msg = pack77_free_text("HELLO FT8 WLD").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "HELLO FT8 WLD");
assert!(pack77_free_text("ABCDEFGHIJKLMN").is_none());
assert!(pack77_free_text("HELLO!").is_none()); }
#[test]
fn pack77_report_roundtrip() {
let msg = pack77("CQ", "JA1ABC", "PM95").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "CQ JA1ABC PM95");
let msg = pack77("JA1ABC", "3Y0Z", "-12").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JA1ABC 3Y0Z -12");
let msg = pack77("JA1ABC", "3Y0Z", "+05").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JA1ABC 3Y0Z +05");
let msg = pack77("3Y0Z", "JA1ABC", "R-12").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "3Y0Z JA1ABC R-12");
let msg = pack77("JA1ABC", "3Y0Z", "RRR").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JA1ABC 3Y0Z RRR");
let msg = pack77("JA1ABC", "3Y0Z", "RR73").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JA1ABC 3Y0Z RR73");
let msg = pack77("3Y0Z", "JA1ABC", "73").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "3Y0Z JA1ABC 73");
let msg = pack77("JA1ABC", "3Y0Z", "").unwrap();
assert_eq!(unpack77(&msg).unwrap(), "JA1ABC 3Y0Z");
}
}