#![cfg(feature = "tnc")]
use yodel::SampleRate;
use yodel::aprs::{
Addressee, AprsError, AprsPacket, Capabilities, CompressedCs, CompressionType, DataExtension,
Item, Latitude, Longitude, Message, MessageContent, Object, Position, PositionCs,
PositionTimestamped, PositionWeather, PositionlessWeather, Status, Symbol, Telemetry,
Timestamp, WeatherReport,
};
use yodel::ax25::{Address, UiFrame};
use yodel::geo::Ambiguity;
use yodel::tnc::{DefaultTncReceiver, TncConfig, TncReceiver, TncTransmitter};
use yodel::units::{Humidity, Pressure, Rainfall, Speed, Temperature};
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Lcg(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
fn next_u8(&mut self) -> u8 {
(self.next_u64() >> 56) as u8
}
fn below(&mut self, bound: usize) -> usize {
(self.next_u64() >> 33) as usize % bound
}
fn bytes(&mut self, max_len: usize) -> Vec<u8> {
let len = self.below(max_len + 1);
(0..len).map(|_| self.next_u8()).collect()
}
}
const DISPATCH_DTI_COUNT: usize = 11;
const MIC_E_DTI_COUNT: usize = 2;
const MIC_E_DTIS: [u8; MIC_E_DTI_COUNT] = [b'`', b'\''];
const DTIS: [u8; DISPATCH_DTI_COUNT + MIC_E_DTI_COUNT] = [
b'!', b'=', b'/', b'@', b':', b'>', b'_', b'T', b';', b')', b'<', b'`', b'\'',
];
const VARIANT_COUNT: usize = 11;
fn variant_index(packet: &AprsPacket<'_>) -> Option<usize> {
match *packet {
AprsPacket::Position(_) => Some(0),
AprsPacket::PositionCs(_) => Some(1),
AprsPacket::PositionTimestamped(_) => Some(2),
AprsPacket::PositionWeather(_) => Some(3),
AprsPacket::Weather(_) => Some(4),
AprsPacket::Telemetry(_) => Some(5),
AprsPacket::Object(_) => Some(6),
AprsPacket::Item(_) => Some(7),
AprsPacket::Status(_) => Some(8),
AprsPacket::Message(_) => Some(9),
AprsPacket::Capabilities(_) => Some(10),
_ => None,
}
}
#[test]
fn dti_table_and_corpus_cover_every_aprs_packet_variant() {
let dispatched: Vec<u8> = (0..=u8::MAX)
.filter(|&byte| {
!matches!(
AprsPacket::parse(&[byte]),
Err(AprsError::InvalidDataType { .. })
)
})
.collect();
let as_chars: Vec<char> = dispatched.iter().map(|&b| b as char).collect();
assert_eq!(
dispatched.len(),
DISPATCH_DTI_COUNT,
"AprsPacket::parse dispatches on {as_chars:?}, which is not {DISPATCH_DTI_COUNT} \
identifiers: update DISPATCH_DTI_COUNT and DTIS together"
);
for &dti in &dispatched {
assert!(
DTIS.contains(&dti),
"DTIS is missing the dispatched identifier {:?}, so the fuzz sweep never reaches \
its branch",
dti as char
);
}
for &dti in &MIC_E_DTIS {
assert!(DTIS.contains(&dti), "DTIS lost a Mic-E identifier");
assert!(
!dispatched.contains(&dti),
"{:?} is now a dispatched identifier, not a rejected Mic-E one",
dti as char
);
}
assert_eq!(DTIS.len(), DISPATCH_DTI_COUNT + MIC_E_DTI_COUNT);
for (i, &a) in DTIS.iter().enumerate() {
assert!(
!DTIS[i + 1..].contains(&a),
"DTIS lists {:?} twice, inflating the case count without adding a branch",
a as char
);
}
let corpus = corpus();
assert!(
corpus.len() >= MIN_CORPUS_CASES,
"corpus shrank to {}, below the {MIN_CORPUS_CASES}-encoding floor",
corpus.len()
);
let mut seen = [false; VARIANT_COUNT];
for encoded in &corpus {
let parsed = AprsPacket::parse(encoded).expect("every corpus encoding must parse");
let index = variant_index(&parsed).expect(
"the corpus produced an AprsPacket variant this file does not know: extend \
variant_index, VARIANT_COUNT and the corpus together",
);
seen[index] = true;
}
for (index, hit) in seen.iter().enumerate() {
assert!(
*hit,
"no corpus encoding parses as AprsPacket variant #{index}, so truncation and \
corruption never reach it"
);
}
}
#[test]
fn fuzz_aprs_packet_parse_random() {
let mut rng = Lcg::new(0xA905_2024_0001);
for _ in 0..3000 {
let input = rng.bytes(100);
let _ = AprsPacket::parse(&input);
}
for &dti in &DTIS {
for _ in 0..1050 {
let mut input = vec![dti];
input.extend_from_slice(&rng.bytes(99));
let _ = AprsPacket::parse(&input);
}
}
}
#[test]
fn fuzz_aprs_subparsers_random() {
const EXT_SYMBOLS: [Symbol; 2] = [Symbol::CAR, Symbol::from_wire(b'/', b'_')];
let mut rng = Lcg::new(0xA905_2024_0002);
for _ in 0..2000 {
let input = rng.bytes(100);
let _ = Position::parse(&input);
let _ = PositionCs::parse(&input);
let _ = PositionTimestamped::parse(&input);
let _ = PositionWeather::parse(&input);
let _ = PositionlessWeather::parse(&input);
let _ = Telemetry::parse(&input);
let _ = Object::parse(&input);
let _ = Item::parse(&input);
let _ = Status::parse(&input);
let _ = Message::parse(&input);
let _ = Capabilities::parse(&input);
for symbol in EXT_SYMBOLS {
let _ = DataExtension::parse(&input, symbol);
}
if !input.is_empty() {
let _ = Timestamp::parse(&input, rng.below(input.len()));
}
let _ = Timestamp::parse(&input, 0);
}
for &dti in &DTIS {
for _ in 0..200 {
let mut input = vec![dti];
input.extend_from_slice(&rng.bytes(60));
let _ = Position::parse(&input);
let _ = PositionCs::parse(&input);
let _ = PositionTimestamped::parse(&input);
let _ = PositionWeather::parse(&input);
let _ = PositionlessWeather::parse(&input);
let _ = Telemetry::parse(&input);
let _ = Object::parse(&input);
let _ = Item::parse(&input);
let _ = Status::parse(&input);
let _ = Message::parse(&input);
let _ = Capabilities::parse(&input);
for symbol in EXT_SYMBOLS {
let _ = DataExtension::parse(&input, symbol);
}
}
}
}
#[cfg(feature = "micE")]
#[test]
fn fuzz_mic_e_decode_random() {
use yodel::aprs::mic_e;
let mut rng = Lcg::new(0xA905_2024_0003);
for _ in 0..3000 {
let dest = rng.bytes(10);
let info = rng.bytes(60);
let _ = mic_e::decode(&dest, &info);
}
for _ in 0..3000 {
let dest: Vec<u8> = (0..6).map(|_| 0x20 + rng.next_u8() % 0x5F).collect();
let mut info = vec![if rng.next_u8() & 1 == 0 { b'`' } else { b'\'' }];
let len = rng.below(40);
info.extend((0..len).map(|_| {
if rng.next_u8() & 3 == 0 {
rng.next_u8()
} else {
0x20 + rng.next_u8() % 0x5F
}
}));
let _ = mic_e::decode(&dest, &info);
}
}
#[cfg(feature = "kiss")]
#[test]
fn fuzz_kiss_deframer_and_command() {
use yodel::kiss::{FEND, FESC, KissCommand, KissDeframer, TFEND, TFESC};
for byte in 0..=255u8 {
let _ = KissCommand::from_byte(byte);
}
let mut rng = Lcg::new(0xA905_2024_0004);
let mut deframer = KissDeframer::<64>::new();
for _ in 0..20_000 {
let _ = deframer.push(rng.next_u8());
}
let specials = [FEND, FESC, TFEND, TFESC, 0x00, 0xFF];
for _ in 0..200 {
let mut d = KissDeframer::<32>::new();
for _ in 0..rng.below(200) {
let byte = if rng.next_u8() & 1 == 0 {
specials[rng.below(specials.len())]
} else {
rng.next_u8()
};
let _ = d.push(byte);
}
}
}
#[test]
fn fuzz_ax25_frame_parse_random() {
let mut rng = Lcg::new(0xA905_2024_0005);
for _ in 0..4000 {
let input = rng.bytes(100);
let _ = UiFrame::parse(&input);
if input.len() >= 7 {
let field: &[u8; 7] = input[..7].try_into().unwrap();
let _ = Address::decode(field);
}
}
for _ in 0..2000 {
let mut input: Vec<u8> = (0..14)
.map(|_| (0x41u8 << 1) | (rng.next_u8() & 1))
.collect();
input.extend_from_slice(&rng.bytes(60));
let _ = UiFrame::parse(&input);
}
}
#[test]
fn fuzz_hdlc_deframer_random_bits() {
use yodel::Bit;
use yodel::ax25::HdlcDeframer;
let mut rng = Lcg::new(0xA905_2024_0006);
let mut deframer = HdlcDeframer::<64>::new();
for _ in 0..50_000 {
let bit = if rng.next_u8() & 1 == 1 {
Bit::One
} else {
Bit::Zero
};
let _ = deframer.push(bit);
}
for _ in 0..500 {
for i in 0..8 {
let bit = if (0x7Eu8 >> i) & 1 == 1 {
Bit::One
} else {
Bit::Zero
};
let _ = deframer.push(bit);
}
for _ in 0..rng.below(64) {
let bit = if rng.next_u8() & 1 == 1 {
Bit::One
} else {
Bit::Zero
};
let _ = deframer.push(bit);
}
}
}
fn lat(v: i64) -> Latitude {
Latitude::new(v * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE).unwrap()
}
fn lon(v: i64) -> Longitude {
Longitude::new(v * yodel::geo::UNITS_PER_HUNDREDTH_MINUTE).unwrap()
}
const MIN_CORPUS_CASES: usize = VARIANT_COUNT + 2;
fn corpus() -> Vec<Vec<u8>> {
let packets: [AprsPacket<'static>; MIN_CORPUS_CASES] = [
AprsPacket::Position(Position {
ambiguity: Ambiguity::EXACT,
latitude: lat(49 * 6000 + 350),
longitude: lon(-(72 * 6000 + 175)),
symbol: Symbol::HOUSE,
messaging: false,
compressed: false,
extension: None,
comment: b"fuzz corpus",
}),
AprsPacket::Position(Position {
ambiguity: Ambiguity::EXACT,
latitude: lat(-6001),
longitude: lon(6001),
symbol: Symbol::from_wire(b'\\', b'O'),
messaging: true,
compressed: true,
extension: None,
comment: b"cmp",
}),
AprsPacket::Status(Status {
text: b"fuzz status",
}),
AprsPacket::Message(Message {
addressee: Addressee::new(b"N1CALL").unwrap(),
content: MessageContent::Text {
text: b"hello fuzz",
id: Some(b"7"),
},
}),
AprsPacket::Weather(PositionlessWeather {
month: 6,
day: 15,
hour: 12,
minute: 30,
weather: WeatherReport {
wind_direction: Some(220),
wind_speed: Some(Speed::from_mph(4)),
gust: Some(Speed::from_mph(5)),
temperature: Some(Temperature::from_fahrenheit(77)),
rain_1h: Some(Rainfall::from_hundredths_inch(0)),
rain_24h: Some(Rainfall::from_hundredths_inch(0)),
rain_midnight: Some(Rainfall::from_hundredths_inch(0)),
humidity: Some(Humidity::new(50).expect("in range")),
barometric_pressure: Some(Pressure::from_tenths_hpa(9900)),
luminosity: None,
snowfall: None,
},
rest: b"",
}),
AprsPacket::Telemetry(Telemetry {
seq: 5,
analog: Telemetry::integer_channels([199, 0, 255, 73, 123]),
digital: Some([false, true, true, false, true, false, false, true]),
rest: b"",
}),
AprsPacket::Object(Object {
ambiguity: Ambiguity::EXACT,
name: b"LEADER",
live: true,
timestamp: Timestamp::DhmZulu {
day: 9,
hour: 23,
minute: 45,
},
latitude: lat(49 * 6000 + 350),
longitude: lon(-(72 * 6000 + 175)),
symbol: Symbol::CAR,
compressed: false,
comment: b"088/036",
}),
AprsPacket::Item(Item {
ambiguity: Ambiguity::EXACT,
name: b"AID#2",
live: true,
latitude: lat(6000),
longitude: lon(-6000),
symbol: Symbol::from_wire(b'/', b'8'),
compressed: false,
comment: b"first aid",
}),
AprsPacket::PositionCs(PositionCs {
position: Position {
ambiguity: Ambiguity::EXACT,
latitude: lat(49 * 6000 + 350),
longitude: lon(-(72 * 6000 + 175)),
symbol: Symbol::from_wire(b'/', b'>'),
messaging: true,
compressed: true,
extension: None,
comment: b"rng",
},
cs: CompressedCs::RadioRange { miles: 20 },
compression_type: CompressionType::default(),
}),
AprsPacket::PositionCs(PositionCs {
position: Position {
ambiguity: Ambiguity::EXACT,
latitude: lat(49 * 6000 + 350),
longitude: lon(-(72 * 6000 + 175)),
symbol: Symbol::from_wire(b'/', b'>'),
messaging: false,
compressed: true,
extension: None,
comment: b"alt",
},
cs: CompressedCs::Altitude { feet: 363 },
compression_type: CompressionType::default(),
}),
AprsPacket::PositionTimestamped(PositionTimestamped {
timestamp: Timestamp::Hms {
hour: 23,
minute: 45,
second: 17,
},
position: Position {
ambiguity: Ambiguity::EXACT,
latitude: lat(49 * 6000 + 350),
longitude: lon(-(72 * 6000 + 175)),
symbol: Symbol::CAR,
messaging: true,
compressed: false,
extension: None,
comment: b"stamped",
},
cs: CompressedCs::NoData,
compression_type: CompressionType::default(),
}),
AprsPacket::PositionWeather(PositionWeather {
ambiguity: Ambiguity::EXACT,
latitude: lat(49 * 6000 + 350),
longitude: lon(-(72 * 6000 + 175)),
symbol: Symbol::from_wire(b'/', b'_'),
messaging: false,
timestamp: None,
weather: WeatherReport {
wind_direction: Some(220),
wind_speed: Some(Speed::from_knots(4)),
temperature: Some(Temperature::from_fahrenheit(77)),
humidity: Some(Humidity::new(50).expect("in range")),
barometric_pressure: Some(Pressure::from_tenths_hpa(9900)),
..WeatherReport::default()
},
rest: b"",
}),
AprsPacket::Capabilities(Capabilities {
body: b"IGATE,MSG_CNT=13,LOC_CNT=54",
}),
];
packets
.iter()
.map(|p| {
let mut buf = [0u8; 256];
let len = p.build(&mut buf).unwrap();
buf[..len].to_vec()
})
.collect()
}
#[test]
fn fuzz_truncated_valid_encodings() {
for encoded in corpus() {
for cut in 0..encoded.len() {
let _ = AprsPacket::parse(&encoded[..cut]);
}
assert!(AprsPacket::parse(&encoded).is_ok());
}
}
#[test]
fn fuzz_corrupted_valid_encodings() {
let mut rng = Lcg::new(0xA905_2024_0007);
let corpus = corpus();
for _ in 0..3000 {
let mut bytes = corpus[rng.below(corpus.len())].clone();
for _ in 0..(1 + rng.below(4)) {
let at = rng.below(bytes.len());
if rng.next_u8() & 1 == 0 {
bytes[at] = rng.next_u8();
} else {
bytes[at] ^= 1 << rng.below(8);
}
}
if let Ok(parsed) = AprsPacket::parse(&bytes) {
let mut buf = [0u8; 300];
let len = parsed
.build(&mut buf)
.expect("a parsed packet must re-encode");
let reparsed = AprsPacket::parse(&buf[..len]).expect("re-encoding must re-parse");
let mut buf2 = [0u8; 300];
let len2 = reparsed
.build(&mut buf2)
.expect("a re-parsed packet must re-encode");
assert_eq!(
&buf2[..len2],
&buf[..len],
"re-encode/re-parse must reach a fixed point"
);
}
}
}
#[test]
fn fuzz_ax25_truncation_and_corruption() {
use yodel::ax25::crc16_x25;
let sr = SampleRate::new(11_025).unwrap();
let cfg = TncConfig::bell_202(sr).unwrap();
let tx = TncTransmitter::new(cfg);
let mut frame_buf = [0u8; 330];
let len = tx
.build_frame_raw(
Address::new(b"APRS", 0).unwrap(),
Address::new(b"N0CALL", 7).unwrap(),
&[Address::new(b"WIDE1", 1).unwrap()],
b"!4903.50N/07201.75W-fuzz",
&mut frame_buf,
)
.unwrap();
let mut with_fcs = frame_buf[..len].to_vec();
with_fcs.extend_from_slice(&crc16_x25(&frame_buf[..len]).to_le_bytes());
for cut in 0..=with_fcs.len() {
let _ = UiFrame::parse(&with_fcs[..cut]);
}
let mut rng = Lcg::new(0xA905_2024_0008);
for _ in 0..3000 {
let mut bytes = with_fcs.clone();
for _ in 0..(1 + rng.below(4)) {
let at = rng.below(bytes.len());
bytes[at] ^= 1 << rng.below(8);
}
let _ = UiFrame::parse(&bytes);
}
}
#[test]
fn fuzz_tnc_receiver_pcm() {
let sr = SampleRate::new(11_025).unwrap();
let cfg = TncConfig::bell_202(sr).unwrap();
let mut rng = Lcg::new(0xA905_2024_0009);
let mut rx: DefaultTncReceiver = TncReceiver::new(cfg).unwrap();
let mut prev = rx.stats();
for _ in 0..60_000 {
let sample = rng.next_u64() as i16;
let _ = rx.push_i16(sample);
let now = rx.stats();
assert!(now.frames_ok >= prev.frames_ok, "counter went backwards");
assert!(now.fcs_errors >= prev.fcs_errors, "counter went backwards");
assert!(now.oversize >= prev.oversize, "counter went backwards");
assert!(now.malformed >= prev.malformed, "counter went backwards");
prev = now;
}
for pattern in 0..4u8 {
let mut rx: DefaultTncReceiver = TncReceiver::new(cfg).unwrap();
for i in 0..20_000u32 {
let sample = match pattern {
0 => i16::MAX,
1 => i16::MIN,
2 => {
if i & 1 == 0 {
i16::MAX
} else {
i16::MIN
}
}
_ => 0,
};
let _ = rx.push_i16(sample);
}
}
let mut rx: DefaultTncReceiver = TncReceiver::new(cfg).unwrap();
for i in 0..20_000u32 {
let sample = match i % 7 {
0 => f32::NAN,
1 => f32::INFINITY,
2 => f32::NEG_INFINITY,
3 => 1.0e9,
_ => (rng.next_u64() >> 40) as f32 / 8_388_608.0 - 1.0,
};
let _ = rx.push_f32(sample);
}
}