Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3934196
Error on bad rec sizes in files
RemingtonRohel Feb 28, 2025
642bd79
Created reading functions that are lax w.r.t. corrupt records.
RemingtonRohel Jun 19, 2025
dadcbd0
Used macro_rules to reduce similar code blocks.
RemingtonRohel Jun 24, 2025
9f0575d
Updated tests.rs to generate with macro_rules!
RemingtonRohel Jun 24, 2025
3969b0c
Adding macros to lib.rs
RemingtonRohel Jun 24, 2025
54eb455
Some larger refactoring with macro_rules.
RemingtonRohel Jun 25, 2025
2c37669
Added a few unit tests to types.rs
RemingtonRohel Jun 25, 2025
26e7642
Ran `cargo fmt`
RemingtonRohel Jun 25, 2025
b3365bd
Added new functions to read from / write to bytes via Python API.
RemingtonRohel Jul 3, 2025
d3d25be
Merge pull request #20 from SuperDARNCanada/lax_read
RemingtonRohel Jul 3, 2025
33120f7
Version number bump
RemingtonRohel Jul 3, 2025
59539c4
Run all OS builds in CI for PR
RemingtonRohel Jul 3, 2025
e397d5e
Updated CI.yml with new `maturin-ci generate` command.
RemingtonRohel Jul 7, 2025
a0d7e25
Disable free-threaded Python 3.13 support.
RemingtonRohel Jul 7, 2025
03ce438
Return Python bytes for `write_dmap_bytes`
RemingtonRohel Jul 7, 2025
2236bea
Added lax reading from bytes for Python API.
RemingtonRohel Jul 9, 2025
5dd20e7
Version bump and CI.yml update to fix uploading to PyPI
RemingtonRohel Jul 9, 2025
fd1b44c
Merge branch 'main' into develop
RemingtonRohel Jul 9, 2025
f926c8a
Added ability to "sniff" first record of files. (#24)
RemingtonRohel Aug 19, 2025
86e073d
Version bump to 0.3.0
RemingtonRohel Aug 19, 2025
17b8d0f
Merge branch 'main' into develop
RemingtonRohel Aug 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dmap"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
rust-version = "1.63.0"

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "darn-dmap"
version = "0.2.1"
version = "0.3.0"
requires-python = ">=3.8"
authors = [
{ name = "Remington Rohel" }
Expand Down
38 changes: 30 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ read_rust!(dmap);
/// Generates two functions: `read_[type]` and `read_[type]_lax`, for strict and lax
/// reading, respectively.
macro_rules! read_py {
($name:ident, $py_name:literal, $lax_name:literal, $bytes_name:literal, $lax_bytes_name:literal) => {
($name:ident, $py_name:literal, $lax_name:literal, $bytes_name:literal, $lax_bytes_name:literal, $sniff_name:literal) => {
paste! {
#[doc = "Reads a `" $name:upper "` file, returning a list of dictionaries containing the fields." ]
#[pyfunction]
Expand Down Expand Up @@ -219,29 +219,42 @@ macro_rules! read_py {
result.1,
))
}

#[doc = "Reads a `" $name:upper "` file, returning the first record." ]
#[pyfunction]
#[pyo3(name = $sniff_name)]
#[pyo3(text_signature = "(infile: str, /)")]
fn [< sniff_ $name _py >](infile: PathBuf) -> PyResult<IndexMap<String, DmapField>> {
Ok([< $name:camel Record >]::sniff_file(&infile)
.map_err(PyErr::from)?
.inner()
)
}
}
}
}

read_py!(iqdat, "read_iqdat", "read_iqdat_lax", "read_iqdat_bytes", "read_iqdat_bytes_lax");
read_py!(iqdat, "read_iqdat", "read_iqdat_lax", "read_iqdat_bytes", "read_iqdat_bytes_lax", "sniff_iqdat");
read_py!(
rawacf,
"read_rawacf",
"read_rawacf_lax",
"read_rawacf_bytes",
"read_rawacf_bytes_lax"
"read_rawacf_bytes_lax",
"sniff_rawacf"
);
read_py!(
fitacf,
"read_fitacf",
"read_fitacf_lax",
"read_fitacf_bytes",
"read_fitacf_bytes_lax"
"read_fitacf_bytes_lax",
"sniff_fitacf"
);
read_py!(grid, "read_grid", "read_grid_lax", "read_grid_bytes", "read_grid_bytes_lax");
read_py!(map, "read_map", "read_map_lax", "read_map_bytes", "read_map_bytes_lax");
read_py!(snd, "read_snd", "read_snd_lax", "read_snd_bytes", "read_snd_bytes_lax");
read_py!(dmap, "read_dmap", "read_dmap_lax", "read_dmap_bytes", "read_dmap_bytes_lax");
read_py!(grid, "read_grid", "read_grid_lax", "read_grid_bytes", "read_grid_bytes_lax", "sniff_grid");
read_py!(map, "read_map", "read_map_lax", "read_map_bytes", "read_map_bytes_lax", "sniff_map");
read_py!(snd, "read_snd", "read_snd_lax", "read_snd_bytes", "read_snd_bytes_lax", "sniff_snd");
read_py!(dmap, "read_dmap", "read_dmap_lax", "read_dmap_bytes", "read_dmap_bytes_lax", "sniff_dmap");

/// Checks that a list of dictionaries contains DMAP records, then appends to outfile.
///
Expand Down Expand Up @@ -359,5 +372,14 @@ fn dmap(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(write_grid_bytes_py, m)?)?;
m.add_function(wrap_pyfunction!(write_map_bytes_py, m)?)?;

// Sniff the first record
m.add_function(wrap_pyfunction!(sniff_dmap_py, m)?)?;
m.add_function(wrap_pyfunction!(sniff_iqdat_py, m)?)?;
m.add_function(wrap_pyfunction!(sniff_rawacf_py, m)?)?;
m.add_function(wrap_pyfunction!(sniff_fitacf_py, m)?)?;
m.add_function(wrap_pyfunction!(sniff_snd_py, m)?)?;
m.add_function(wrap_pyfunction!(sniff_grid_py, m)?)?;
m.add_function(wrap_pyfunction!(sniff_map_py, m)?)?;

Ok(())
}
46 changes: 46 additions & 0 deletions src/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@ pub trait Record<'a>:
/// Gets the underlying data of `self`.
fn inner(self) -> IndexMap<String, DmapField>;

/// Reads from `dmap_data` and parses into `Vec<Self>`.
///
/// Returns `DmapError` if `dmap_data` cannot be read or contains invalid data.
fn read_first_record(mut dmap_data: impl Read) -> Result<Self, DmapError>
where
Self: Sized,
Self: Send,
{
let mut buffer = [0; 8]; // record size should be an i32 of the data
let read_result = dmap_data.read(&mut buffer[..])?;
if read_result < buffer.len() {
return Err(DmapError::CorruptStream("Unable to read size of first record"))
}

let rec_size = i32::from_le_bytes(buffer[4..8].try_into().unwrap()) as usize; // advance 4 bytes, skipping the "code" field
if rec_size <= 0 {
return Err(DmapError::InvalidRecord(format!(
"Record 0 starting at byte 0 has non-positive size {} <= 0",
rec_size
)));
}

let mut rec = vec![0; rec_size];
rec[0..8].clone_from_slice(&buffer[..]);
dmap_data.read_exact(&mut rec[8..])?;
let first_rec = Self::parse_record(&mut Cursor::new(rec))?;

Ok(first_rec)
}

/// Reads from `dmap_data` and parses into `Vec<Self>`.
///
/// Returns `DmapError` if `dmap_data` cannot be read or contains invalid data.
Expand Down Expand Up @@ -171,6 +201,22 @@ pub trait Record<'a>:
}
}

/// Reads the first record of a DMAP file of type `Self`.
fn sniff_file(infile: &PathBuf) -> Result<Self, DmapError>
where
Self: Sized,
Self: Send,
{
let file = File::open(infile)?;
match infile.extension() {
Some(ext) if ext == OsStr::new("bz2") => {
let compressor = BzDecoder::new(file);
Self::read_first_record(compressor)
}
_ => Self::read_first_record(file),
}
}

/// Reads a record from `cursor`.
fn parse_record(cursor: &mut Cursor<Vec<u8>>) -> Result<Self, DmapError>
where
Expand Down
8 changes: 8 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ macro_rules! make_test {
// Clean up tempfile
remove_file(&tempfile).expect("Unable to delete tempfile");
}

#[test]
fn [< test_ $record_type _sniff >] () {
let filename: PathBuf = PathBuf::from(format!("tests/test_files/test.{}", stringify!($record_type)));
let data = [< $record_type:camel Record >]::sniff_file(&filename).expect("Unable to sniff file");
let all_recs = [< $record_type:camel Record >]::read_file(&filename).expect("Unable to read file");
assert_eq!(data, all_recs[0])
}
}
};
}
Expand Down
Loading