From b98475037c3bcc3013cd1062ba6ac4404d6012d6 Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Sun, 8 Feb 2026 22:48:48 +0100 Subject: [PATCH] [fix](trx-server): use raw bytes for APRS position parsing The APRS info field is raw AX.25 bytes, not valid UTF-8. from_utf8_lossy inserts multi-byte replacement characters, causing panics when the position parser sliced by byte index. Switch parse_aprs_position, parse_aprs_compressed, parse_aprs_lat, and parse_aprs_lon to operate on &[u8] instead of &str. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Stanislaw Grams --- src/trx-server/src/decode/aprs.rs | 78 ++++++++++++++----------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/trx-server/src/decode/aprs.rs b/src/trx-server/src/decode/aprs.rs index 58b485e..5560e7e 100644 --- a/src/trx-server/src/decode/aprs.rs +++ b/src/trx-server/src/decode/aprs.rs @@ -384,10 +384,11 @@ fn parse_aprs(ax25: &Ax25Frame) -> AprsPacket { .map(|d| format_call(d)) .collect::>() .join(","); - let info_str = String::from_utf8_lossy(&ax25.info).to_string(); + let info = &ax25.info; + let info_str = String::from_utf8_lossy(info).to_string(); - let packet_type = if !info_str.is_empty() { - match info_str.as_bytes()[0] { + let packet_type = if !info.is_empty() { + match info[0] { b'!' | b'=' | b'/' | b'@' => "Position", b':' => "Message", b'>' => "Status", @@ -407,7 +408,7 @@ fn parse_aprs(ax25: &Ax25Frame) -> AprsPacket { let mut symbol_code = None; if packet_type == "Position" { - if let Some(pos) = parse_aprs_position(&info_str) { + if let Some(pos) = parse_aprs_position(info) { lat = Some(pos.0); lon = Some(pos.1); symbol_table = Some(pos.2.to_string()); @@ -429,61 +430,56 @@ fn parse_aprs(ax25: &Ax25Frame) -> AprsPacket { } } -fn parse_aprs_position(info_str: &str) -> Option<(f64, f64, char, char)> { - if info_str.is_empty() { +fn parse_aprs_position(info: &[u8]) -> Option<(f64, f64, char, char)> { + if info.is_empty() { return None; } - let bytes = info_str.as_bytes(); - let dt = bytes[0]; + let dt = info[0]; - let pos_str = match dt { - b'!' | b'=' => &info_str[1..], + let pos = match dt { + b'!' | b'=' => &info[1..], b'/' | b'@' => { - if info_str.len() < 9 { + if info.len() < 9 { return None; } - &info_str[8..] + &info[8..] } _ => return None, }; - if pos_str.is_empty() { + if pos.is_empty() { return None; } - let first = pos_str.as_bytes()[0]; - if first < b'0' || first > b'9' { - return parse_aprs_compressed(pos_str); + if pos[0] < b'0' || pos[0] > b'9' { + return parse_aprs_compressed(pos); } // Uncompressed: DDMM.MMN/DDDMM.MMEsYYY - if pos_str.len() < 19 { + if pos.len() < 19 { return None; } - let lat_str = &pos_str[..8]; - let sym_table = pos_str.as_bytes()[8] as char; - let lon_str = &pos_str[9..18]; - let sym_code = pos_str.as_bytes()[18] as char; + let sym_table = pos[8] as char; + let sym_code = pos[18] as char; - let lat = parse_aprs_lat(lat_str)?; - let lon = parse_aprs_lon(lon_str)?; + let lat = parse_aprs_lat(&pos[..8])?; + let lon = parse_aprs_lon(&pos[9..18])?; Some((lat, lon, sym_table, sym_code)) } -fn parse_aprs_compressed(pos_str: &str) -> Option<(f64, f64, char, char)> { - if pos_str.len() < 10 { +fn parse_aprs_compressed(pos: &[u8]) -> Option<(f64, f64, char, char)> { + if pos.len() < 10 { return None; } - let bytes = pos_str.as_bytes(); - let sym_table = bytes[0] as char; + let sym_table = pos[0] as char; let mut lat_val: u32 = 0; let mut lon_val: u32 = 0; for i in 0..4 { - let lc = bytes[1 + i] as i32 - 33; - let xc = bytes[5 + i] as i32 - 33; + let lc = pos[1 + i] as i32 - 33; + let xc = pos[5 + i] as i32 - 33; if lc < 0 || lc > 90 || xc < 0 || xc > 90 { return None; } @@ -498,22 +494,21 @@ fn parse_aprs_compressed(pos_str: &str) -> Option<(f64, f64, char, char)> { return None; } - let sym_code = bytes[9] as char; + let sym_code = pos[9] as char; let lat = (lat * 1e6).round() / 1e6; let lon = (lon * 1e6).round() / 1e6; Some((lat, lon, sym_table, sym_code)) } -fn parse_aprs_lat(s: &str) -> Option { - if s.len() < 8 { +fn parse_aprs_lat(b: &[u8]) -> Option { + if b.len() < 8 { return None; } - let deg: f64 = s[..2].parse().ok()?; - let min: f64 = s[2..7].parse().ok()?; - let ns = s.as_bytes()[7]; + let deg: f64 = std::str::from_utf8(&b[..2]).ok()?.parse().ok()?; + let min: f64 = std::str::from_utf8(&b[2..7]).ok()?.parse().ok()?; let mut lat = deg + min / 60.0; - match ns { + match b[7] { b'S' | b's' => lat = -lat, b'N' | b'n' => {} _ => return None, @@ -521,15 +516,14 @@ fn parse_aprs_lat(s: &str) -> Option { Some((lat * 1e6).round() / 1e6) } -fn parse_aprs_lon(s: &str) -> Option { - if s.len() < 9 { +fn parse_aprs_lon(b: &[u8]) -> Option { + if b.len() < 9 { return None; } - let deg: f64 = s[..3].parse().ok()?; - let min: f64 = s[3..8].parse().ok()?; - let ew = s.as_bytes()[8]; + let deg: f64 = std::str::from_utf8(&b[..3]).ok()?.parse().ok()?; + let min: f64 = std::str::from_utf8(&b[3..8]).ok()?.parse().ok()?; let mut lon = deg + min / 60.0; - match ew { + match b[8] { b'W' | b'w' => lon = -lon, b'E' | b'e' => {} _ => return None,