Skip to content

Commit 5096906

Browse files
Merge pull request #27 from SuperDARNCanada/develop
Release: v0.4.0
2 parents 0fa5ba0 + 94b6431 commit 5096906

File tree

7 files changed

+798
-182
lines changed

7 files changed

+798
-182
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dmap"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
edition = "2021"
55
rust-version = "1.63.0"
66

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "darn-dmap"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
requires-python = ">=3.8"
99
authors = [
1010
{ name = "Remington Rohel" }

src/compression.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use std::io::{Chain, Cursor, Error, Read};
2+
3+
/// Detects bz2 compression on the input `stream`. Returns a reader
4+
/// which includes all data from `stream`.
5+
pub(crate) fn detect_bz2<T>(mut stream: T) -> Result<(bool, Chain<Cursor<[u8; 3]>, T>), Error>
6+
where
7+
T: for<'a> Read,
8+
{
9+
// Read the first 3 bytes to detect bz2 compression
10+
let mut buffer = [0u8; 3];
11+
stream.read_exact(&mut buffer)?;
12+
13+
// valid bz2 blocks start with "BZh", which is 425a68 in hex.
14+
let is_bz2 = buffer == [0x42, 0x5a, 0x68];
15+
let full_stream = Cursor::new(buffer).chain(stream);
16+
Ok((is_bz2, full_stream))
17+
}
18+
19+
#[cfg(test)]
20+
mod tests {
21+
use super::*;
22+
use bzip2::{read::BzDecoder, read::BzEncoder, Compression};
23+
24+
#[test]
25+
fn bz2_detection() -> Result<(), Error> {
26+
let data = "Hello world".as_bytes();
27+
let compressor = BzEncoder::new(data, Compression::best());
28+
29+
let (result, stream) = detect_bz2(compressor)?;
30+
assert_eq!(result, true);
31+
let mut returned_stream = vec![];
32+
let mut decompressed = BzDecoder::new(stream);
33+
let _ = decompressed.read_to_end(&mut returned_stream);
34+
assert_eq!(returned_stream, b"Hello world");
35+
36+
let data = "Hello world".as_bytes();
37+
let (result, mut stream) = detect_bz2(data)?;
38+
assert_eq!(result, false);
39+
let mut returned_stream = vec![];
40+
let _ = stream.read_to_end(&mut returned_stream);
41+
assert_eq!(returned_stream, b"Hello world");
42+
43+
Ok(())
44+
}
45+
}

src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ pub enum DmapError {
1414
#[error("{0}")]
1515
Io(#[from] std::io::Error),
1616

17+
/// Error casting between Dmap types.
18+
#[error("{0}")]
19+
BadCast(#[from] std::num::TryFromIntError),
20+
1721
/// Invalid key for a DMAP type. Valid keys are defined [here](https://github.com/SuperDARN/rst/blob/main/codebase/general/src.lib/dmap.1.25/include/dmap.h)
1822
#[error("{0}")]
1923
InvalidKey(i8),
@@ -31,6 +35,10 @@ pub enum DmapError {
3135
#[error("{0}")]
3236
InvalidVector(String),
3337

38+
/// Bytes cannot be interpreted as a DMAP field.
39+
#[error("{0}")]
40+
InvalidField(String),
41+
3442
/// Errors when reading in multiple records
3543
#[error("First error: {1}\nRecords with errors: {0:?}")]
3644
BadRecords(Vec<usize>, String),

src/lib.rs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@
55
//! For more information about DMAP files, see [RST](https://radar-software-toolkit-rst.readthedocs.io/en/latest/)
66
//! or [pyDARNio](https://pydarnio.readthedocs.io/en/latest/).
77
8+
pub mod compression;
89
pub mod error;
910
pub mod formats;
1011
pub mod record;
1112
pub mod types;
1213

13-
use crate::error::DmapError;
14-
use crate::formats::dmap::DmapRecord;
15-
use crate::formats::fitacf::FitacfRecord;
16-
use crate::formats::grid::GridRecord;
17-
use crate::formats::iqdat::IqdatRecord;
18-
use crate::formats::map::MapRecord;
19-
use crate::formats::rawacf::RawacfRecord;
20-
use crate::formats::snd::SndRecord;
21-
use crate::record::Record;
14+
pub use crate::error::DmapError;
15+
pub use crate::formats::dmap::DmapRecord;
16+
pub use crate::formats::fitacf::FitacfRecord;
17+
pub use crate::formats::grid::GridRecord;
18+
pub use crate::formats::iqdat::IqdatRecord;
19+
pub use crate::formats::map::MapRecord;
20+
pub use crate::formats::rawacf::RawacfRecord;
21+
pub use crate::formats::snd::SndRecord;
22+
pub use crate::record::Record;
2223
use crate::types::DmapField;
2324
use bzip2::read::BzEncoder;
2425
use bzip2::Compression;
@@ -234,7 +235,14 @@ macro_rules! read_py {
234235
}
235236
}
236237

237-
read_py!(iqdat, "read_iqdat", "read_iqdat_lax", "read_iqdat_bytes", "read_iqdat_bytes_lax", "sniff_iqdat");
238+
read_py!(
239+
iqdat,
240+
"read_iqdat",
241+
"read_iqdat_lax",
242+
"read_iqdat_bytes",
243+
"read_iqdat_bytes_lax",
244+
"sniff_iqdat"
245+
);
238246
read_py!(
239247
rawacf,
240248
"read_rawacf",
@@ -251,10 +259,38 @@ read_py!(
251259
"read_fitacf_bytes_lax",
252260
"sniff_fitacf"
253261
);
254-
read_py!(grid, "read_grid", "read_grid_lax", "read_grid_bytes", "read_grid_bytes_lax", "sniff_grid");
255-
read_py!(map, "read_map", "read_map_lax", "read_map_bytes", "read_map_bytes_lax", "sniff_map");
256-
read_py!(snd, "read_snd", "read_snd_lax", "read_snd_bytes", "read_snd_bytes_lax", "sniff_snd");
257-
read_py!(dmap, "read_dmap", "read_dmap_lax", "read_dmap_bytes", "read_dmap_bytes_lax", "sniff_dmap");
262+
read_py!(
263+
grid,
264+
"read_grid",
265+
"read_grid_lax",
266+
"read_grid_bytes",
267+
"read_grid_bytes_lax",
268+
"sniff_grid"
269+
);
270+
read_py!(
271+
map,
272+
"read_map",
273+
"read_map_lax",
274+
"read_map_bytes",
275+
"read_map_bytes_lax",
276+
"sniff_map"
277+
);
278+
read_py!(
279+
snd,
280+
"read_snd",
281+
"read_snd_lax",
282+
"read_snd_bytes",
283+
"read_snd_bytes_lax",
284+
"sniff_snd"
285+
);
286+
read_py!(
287+
dmap,
288+
"read_dmap",
289+
"read_dmap_lax",
290+
"read_dmap_bytes",
291+
"read_dmap_bytes_lax",
292+
"sniff_dmap"
293+
);
258294

259295
/// Checks that a list of dictionaries contains DMAP records, then appends to outfile.
260296
///

src/record.rs

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! Defines the `Record` trait, which contains the shared behaviour that all
22
//! DMAP records must have.
33
4+
use crate::compression::detect_bz2;
45
use crate::error::DmapError;
56
use crate::types::{parse_scalar, parse_vector, read_data, DmapField, DmapType, DmapVec, Fields};
67
use bzip2::read::BzDecoder;
78
use indexmap::IndexMap;
89
use rayon::prelude::*;
9-
use std::ffi::OsStr;
1010
use std::fmt::Debug;
1111
use std::fs::File;
1212
use std::io::{Cursor, Read};
@@ -26,12 +26,19 @@ pub trait Record<'a>:
2626
Self: Sized,
2727
Self: Send,
2828
{
29-
let mut buffer = [0; 8]; // record size should be an i32 of the data
30-
let read_result = dmap_data.read(&mut buffer[..])?;
31-
if read_result < buffer.len() {
32-
return Err(DmapError::CorruptStream("Unable to read size of first record"))
29+
let mut stream: Box<dyn Read>;
30+
let (is_bz2, chunk) = detect_bz2(&mut dmap_data)?;
31+
if is_bz2 {
32+
stream = Box::new(BzDecoder::new(chunk));
33+
} else {
34+
stream = Box::new(chunk);
3335
}
3436

37+
let mut buffer = [0; 8]; // record size should be an i32 of the data
38+
stream
39+
.read_exact(&mut buffer)
40+
.map_err(|_| DmapError::CorruptStream("Unable to read size of first record"))?;
41+
3542
let rec_size = i32::from_le_bytes(buffer[4..8].try_into().unwrap()) as usize; // advance 4 bytes, skipping the "code" field
3643
if rec_size <= 0 {
3744
return Err(DmapError::InvalidRecord(format!(
@@ -42,7 +49,7 @@ pub trait Record<'a>:
4249

4350
let mut rec = vec![0; rec_size];
4451
rec[0..8].clone_from_slice(&buffer[..]);
45-
dmap_data.read_exact(&mut rec[8..])?;
52+
stream.read_exact(&mut rec[8..])?;
4653
let first_rec = Self::parse_record(&mut Cursor::new(rec))?;
4754

4855
Ok(first_rec)
@@ -57,12 +64,19 @@ pub trait Record<'a>:
5764
Self: Send,
5865
{
5966
let mut buffer: Vec<u8> = vec![];
60-
dmap_data.read_to_end(&mut buffer)?;
67+
let (is_bz2, mut chunk) = detect_bz2(&mut dmap_data)?;
68+
if is_bz2 {
69+
let mut stream = BzDecoder::new(chunk);
70+
stream.read_to_end(&mut buffer)?;
71+
} else {
72+
chunk.read_to_end(&mut buffer)?;
73+
}
6174

6275
let mut slices: Vec<_> = vec![];
6376
let mut rec_start: usize = 0;
6477
let mut rec_size: usize;
6578
let mut rec_end: usize;
79+
6680
while ((rec_start + 2 * i32::size()) as u64) < buffer.len() as u64 {
6781
rec_size = i32::from_le_bytes(buffer[rec_start + 4..rec_start + 8].try_into().unwrap())
6882
as usize; // advance 4 bytes, skipping the "code" field
@@ -123,7 +137,13 @@ pub trait Record<'a>:
123137
Self: Send,
124138
{
125139
let mut buffer: Vec<u8> = vec![];
126-
dmap_data.read_to_end(&mut buffer)?;
140+
let (is_bz2, mut chunk) = detect_bz2(&mut dmap_data)?;
141+
if is_bz2 {
142+
let mut stream = BzDecoder::new(chunk);
143+
stream.read_to_end(&mut buffer)?;
144+
} else {
145+
chunk.read_to_end(&mut buffer)?;
146+
}
127147

128148
let mut dmap_records: Vec<Self> = vec![];
129149
let mut bad_byte: Option<usize> = None;
@@ -173,13 +193,7 @@ pub trait Record<'a>:
173193
Self: Send,
174194
{
175195
let file = File::open(infile)?;
176-
match infile.extension() {
177-
Some(ext) if ext == OsStr::new("bz2") => {
178-
let compressor = BzDecoder::new(file);
179-
Self::read_records(compressor)
180-
}
181-
_ => Self::read_records(file),
182-
}
196+
Self::read_records(file)
183197
}
184198

185199
/// Read a DMAP file of type `Self`.
@@ -192,13 +206,7 @@ pub trait Record<'a>:
192206
Self: Send,
193207
{
194208
let file = File::open(infile)?;
195-
match infile.extension() {
196-
Some(ext) if ext == OsStr::new("bz2") => {
197-
let compressor = BzDecoder::new(file);
198-
Self::read_records_lax(compressor)
199-
}
200-
_ => Self::read_records_lax(file),
201-
}
209+
Self::read_records_lax(file)
202210
}
203211

204212
/// Reads the first record of a DMAP file of type `Self`.
@@ -208,13 +216,7 @@ pub trait Record<'a>:
208216
Self: Send,
209217
{
210218
let file = File::open(infile)?;
211-
match infile.extension() {
212-
Some(ext) if ext == OsStr::new("bz2") => {
213-
let compressor = BzDecoder::new(file);
214-
Self::read_first_record(compressor)
215-
}
216-
_ => Self::read_first_record(file),
217-
}
219+
Self::read_first_record(file)
218220
}
219221

220222
/// Reads a record from `cursor`.
@@ -648,6 +650,45 @@ macro_rules! create_record_type {
648650
Self::coerce::<[< $format:camel Record>]>(value, &$fields)
649651
}
650652
}
653+
654+
#[cfg(test)]
655+
mod tests {
656+
use super::*;
657+
use std::path::PathBuf;
658+
659+
/// Creates a test to ensure that the record is still able to be read, even when missing
660+
/// some of the optional fields.
661+
#[test]
662+
fn test_missing_optional_fields() -> Result<(), DmapError> {
663+
let filename: PathBuf = PathBuf::from(format!("tests/test_files/test.{}", stringify!($format)));
664+
let data = [< $format:camel Record >]::sniff_file(&filename).expect("Unable to sniff file");
665+
let recs = data.inner();
666+
667+
for field in $fields.scalars_optional.iter().chain($fields.vectors_optional.iter()) {
668+
let mut cloned_rec = recs.clone();
669+
let _ = cloned_rec.shift_remove(field.0);
670+
let _ = [< $format:camel Record >]::try_from(&mut cloned_rec)?;
671+
}
672+
Ok(())
673+
}
674+
675+
/// Creates a test to ensure that the record is not able to be read when missing
676+
/// some of the required fields.
677+
#[test]
678+
fn test_missing_required_fields() -> Result<(), DmapError> {
679+
let filename: PathBuf = PathBuf::from(format!("tests/test_files/test.{}", stringify!($format)));
680+
let data = [< $format:camel Record >]::sniff_file(&filename).expect("Unable to sniff file");
681+
let recs = data.inner();
682+
683+
for field in $fields.scalars_required.iter().chain($fields.vectors_required.iter()) {
684+
let mut cloned_rec = recs.clone();
685+
let _ = cloned_rec.shift_remove(field.0);
686+
let res = [< $format:camel Record >]::try_from(&mut cloned_rec);
687+
assert!(res.is_err());
688+
}
689+
Ok(())
690+
}
691+
}
651692
}
652693
}
653694
}

0 commit comments

Comments
 (0)