This crate parses Amateur Data Interchange Format (ADIF) data using asynchronous streams.
ADIF is a standard data format used by ham radio operators to exchange information logged about past contacts. This crate provides a few ways to parse ADIF data in Rust on top of tokio.
Add the dependency:
cargo add TBDThen start reading ADIF from any object that implements the AsyncRead trait:
use adif::RecordStream;
use futures::StreamExt;
use tokio::{fs::File, io::BufReader};
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("examples/sample.adif").await?;
let reader = BufReader::new(file);
let mut stream = RecordStream::new(reader, true);
while let Some(result) = stream.next().await {
let record = result?;
if let Some(call) = record.get("call") {
println!("call: {}", call.as_str());
}
}
Ok(())
}See examples or documentation for further information.
Because there is some variety to the ADIF fields generated by various programs, the data reader usually needs to engage in some interpretation of the data based on its source.
This parser is intended to be maximally flexible and provide the user with parsed ADIF data from any source at any level with or without preprocessing.
Input is read from a stream. File input is not assumed. Reading an entire file is not assumed; it's fine to start in the middle. Trailing data in the form of a partial tag or record can be ignored or return an error. Leading text is ignored.
The code in this crate strives to be panic-free, extremely safe, and
lightweight in terms of memory usage. (There is a single line of unsafe
code in [CiStr::new] that transmutes references from a &str to a
transparent wrapper type.)
The [TagStream] provides the lowest level of output: individual ADIF tags and their associated values. Values are parsed and are strongly typed, although in the absence of type specifiers (which is common), data can be coerced to the desired type when accessed.
The [RecordStream] provides higher level output by aggregating fields into records, each representing one contact. Records may then be indexed into by key.
A number of data normalizers are provided in the [filter] module that can be stacked on top of a [RecordStream] to automatically transform records as they are read. They are intended to be generally useful at smoothing some of ADIF's roughest edges, but they are not necessarily every single transformation an application might desire. The user can, however, write additional normalizers to implement additional transformations not heretofore envisioned by the author.
As a convenience tool, the crate also contains a [CabrilloSink] to output records as a contest log in Cabrillo format. Reading Cabrillo format is not supported.
Test coverage of this crate is 100% as of 2025-11-27.
Every single function, every single line, every single expression, and every single character in the entire crate is executed by at least one test.
Every single branch in every single function is tested by at least one
concrete instantiation. (For function Foo<T>::bar, there exists at
least one T for which the tests test every possible path through bar.)
These facts are verified by both cargo llvm-cov and codecov.io (badge
at top) across over 4500 code regions. The nightly toolchain may be
used to verify branch coverage.
I reviewed your flight plan. Not one error in a million keystrokes. Phenomenal. [Gattaca, 1997]
Additionally, each of the property-based tests has run over one million times without error.
Although performance is not a primary concern, some benchmarking of this crate has been performed to measure parsing performance on a single core using synthetic data with 13 fields per record.
- Apple M3 Pro processor (2023) — 544,000 contacts per second
- Intel Core i5 processor (2021) — 252,000 contacts per second
Sidney Cammeresi, AB9BH
THE AUTHOR HAS MADE AND MAKES NO REPRESENTATION OR WARRANTY WHATSOEVER, EITHER EXPRESS OR IMPLIED, THAT THIS CRATE IS NOT SIGNIFICANTLY AND RIDICULOUSLY OVER-ENGINEERED.