mod common;
use std::io::{Cursor, Write};
use common::{
build_grib1_large_message_convention, build_grib1_message, build_grib1_message_with_bitmap,
build_grib1_second_order_general_message, build_grib1_second_order_row_by_row_message,
build_grib2_albers_equal_area_message, build_grib2_complex_packing_message,
build_grib2_complex_packing_message_with_missing, build_grib2_lambert_alternating_message,
build_grib2_lambert_message, build_grib2_mercator_message, build_grib2_message,
build_grib2_message_with_forecast, build_grib2_multifield_message,
build_grib2_polar_stereographic_alternating_message, build_grib2_polar_stereographic_message,
build_grib2_run_length_message, build_grib2_spatial_differencing_message,
};
use grib_reader::{
Error, ForecastTimeUnit, GribFile, GridDefinition, LocalParameterEntry, ParameterTableSource,
ProductDefinitionTemplate,
};
#[test]
fn open_grib2_from_file_and_decode() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("sample.grib2");
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(&build_grib2_message(&[1, 2, 3, 4])).unwrap();
let opened = GribFile::open(&path).unwrap();
assert_eq!(opened.message_count(), 1);
let field = opened.message(0).unwrap();
assert_eq!(field.edition(), 2);
assert_eq!(field.parameter_name(), "TMP");
assert_eq!(field.reference_time().year, 2026);
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0]
);
assert_eq!(
field
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>(),
vec![1.0, 2.0, 3.0, 4.0]
);
assert_eq!(
field
.read_data_as_f32()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>(),
vec![1.0_f32, 2.0, 3.0, 4.0]
);
}
#[test]
fn open_grib2_from_reader_and_decode() {
let bytes = build_grib2_message(&[1, 2, 3, 4]);
let opened = GribFile::builder().from_reader(Cursor::new(bytes)).unwrap();
assert_eq!(opened.message_count(), 1);
assert_eq!(
opened.message(0).unwrap().read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0]
);
}
#[test]
fn decodes_run_length_packing_message() {
let opened = GribFile::from_bytes(build_grib2_run_length_message()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.metadata().data_representation.template_number(), 200);
let values = field.read_flat_data_as_f64().unwrap();
assert_eq!(values[..3], [10.0, 10.0, 10.0]);
assert!(values[3].is_nan());
assert_eq!(values[4..], [20.0, 20.0]);
}
#[test]
fn open_grib1_from_file_and_decode() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("sample.grib1");
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(&build_grib1_message(&[5, 6, 7, 8])).unwrap();
let opened = GribFile::open(&path).unwrap();
assert_eq!(opened.message_count(), 1);
let field = opened.message(0).unwrap();
assert_eq!(field.edition(), 1);
assert_eq!(field.parameter_name(), "TMP");
assert_eq!(field.center_id(), 7);
assert!(field.grib1_product_definition().is_some());
assert_eq!(
field
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>(),
vec![5.0, 6.0, 7.0, 8.0]
);
}
#[test]
fn decodes_grib1_second_order_row_and_secondary_bitmap_groups() {
for bytes in [
build_grib1_second_order_row_by_row_message(),
build_grib1_second_order_general_message(),
] {
let opened = GribFile::from_bytes(bytes).unwrap();
assert_eq!(
opened.message(0).unwrap().read_flat_data_as_f64().unwrap(),
vec![10.0, 11.0, 12.0, 20.0, 22.0, 23.0]
);
}
}
#[test]
fn opens_grib1_large_message_convention_and_resolves_section4_length() {
let opened = GribFile::from_bytes(build_grib1_large_message_convention()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.metadata().message_length, 84);
assert_eq!(field.read_flat_data_as_f64().unwrap(), [1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn decode_into_reuses_caller_buffers_for_f32_and_f64() {
let opened = GribFile::from_bytes(build_grib2_message(&[1, 2, 3, 4])).unwrap();
let field = opened.message(0).unwrap();
let mut as_f32 = [0.0_f32; 4];
field.decode_into(&mut as_f32).unwrap();
assert_eq!(as_f32, [1.0, 2.0, 3.0, 4.0]);
let mut as_f64 = [0.0_f64; 4];
field.decode_into(&mut as_f64).unwrap();
assert_eq!(as_f64, [1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn open_grib2_lambert_conformal_field_and_decode_flat_data() {
let opened = GribFile::from_bytes(build_grib2_lambert_message()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.grid_shape(), (3, 2));
assert_eq!(field.latitudes().unwrap(), None);
assert_eq!(field.longitudes().unwrap(), None);
assert_eq!(
field.projected_x_coordinates().unwrap().unwrap(),
vec![0.0, 2_539.703, 5_079.406]
);
assert_eq!(
field.projected_y_coordinates().unwrap().unwrap(),
vec![-0.0, -2_539.703]
);
match field.grid_definition() {
GridDefinition::LambertConformal(grid) => {
assert_eq!(grid.core.number_of_points, 6);
assert_eq!(grid.core.shape_of_earth, 1);
assert_eq!(grid.core.scaled_value_radius, 6_371_200);
assert_eq!(grid.core.nx, 3);
assert_eq!(grid.core.ny, 2);
assert_eq!(grid.core.lat_first, 12_190_000);
assert_eq!(grid.core.lon_first, 226_541_000);
assert_eq!(grid.lat_d, 25_000_000);
assert_eq!(grid.lon_v, 265_000_000);
assert_eq!(grid.core.dx, 2_539_703);
assert_eq!(grid.core.dy, 2_539_703);
assert_eq!(grid.latin1, 25_000_000);
assert_eq!(grid.latin2, 25_000_000);
}
other => panic!("expected Lambert conformal grid, got {other:?}"),
}
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
let array = field.read_data_as_f64().unwrap();
assert_eq!(array.shape(), &[2, 3]);
assert_eq!(
array.iter().copied().collect::<Vec<_>>(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
}
#[test]
fn over_limit_grid_opens_but_decode_enforces_configured_point_limit() {
let opened = GribFile::builder()
.max_decoded_points(3)
.from_bytes(build_grib2_message(&[1, 2, 3, 4]))
.unwrap();
assert_eq!(opened.message_count(), 1);
let err = opened.message(0).unwrap().read_data_as_f64().unwrap_err();
assert!(matches!(
err,
Error::LimitExceeded {
what: "decoded grid points",
requested: 4,
limit: 3,
}
));
}
#[test]
fn projected_coordinate_helpers_enforce_axis_limit() {
let opened = GribFile::builder()
.max_axis_points(2)
.from_bytes(build_grib2_lambert_message())
.unwrap();
let field = opened.message(0).unwrap();
let err = field.projected_x_coordinates().unwrap_err();
assert!(matches!(
err,
Error::LimitExceeded {
what: "projected x axis",
requested: 3,
limit: 2,
}
));
}
#[test]
fn open_rejects_projected_grid_point_count_mismatch() {
let mut bytes = build_grib2_lambert_message();
let grid_offset = 16 + 21;
bytes[grid_offset + 6..grid_offset + 10].copy_from_slice(&5u32.to_be_bytes());
let err = match GribFile::from_bytes(bytes) {
Ok(_) => panic!("expected projected grid point count mismatch"),
Err(err) => err,
};
assert!(matches!(err, Error::InvalidSection { section: 3, .. }));
assert!(err.to_string().contains("declares 5 points"));
}
#[test]
fn open_grib2_lambert_conformal_normalizes_alternating_scan_rows() {
let opened = GribFile::from_bytes(build_grib2_lambert_alternating_message()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
}
#[test]
fn open_grib2_mercator_field_and_decode_flat_data() {
let opened = GribFile::from_bytes(build_grib2_mercator_message()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.grid_shape(), (3, 2));
assert_eq!(field.latitudes().unwrap(), None);
assert_eq!(field.longitudes().unwrap(), None);
assert_eq!(
field.projected_x_coordinates().unwrap().unwrap(),
vec![0.0, 1_000.0, 2_000.0]
);
assert_eq!(
field.projected_y_coordinates().unwrap().unwrap(),
vec![-0.0, -2_000.0]
);
match field.grid_definition() {
GridDefinition::Mercator(grid) => {
assert_eq!(grid.core.number_of_points, 6);
assert_eq!(grid.core.shape_of_earth, 6);
assert_eq!(grid.core.nx, 3);
assert_eq!(grid.core.ny, 2);
assert_eq!(grid.core.lat_first, -20_000_000);
assert_eq!(grid.core.lon_first, -100_000_000);
assert_eq!(grid.lat_d, 0);
assert_eq!(grid.lat_last, 20_000_000);
assert_eq!(grid.lon_last, -98_000_000);
assert_eq!(grid.orientation_of_grid, 0);
assert_eq!(grid.core.dx, 1_000_000);
assert_eq!(grid.core.dy, 2_000_000);
}
other => panic!("expected Mercator grid, got {other:?}"),
}
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
}
#[test]
fn open_grib2_albers_equal_area_field_and_decode_flat_data() {
let opened = GribFile::from_bytes(build_grib2_albers_equal_area_message()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.grid_shape(), (3, 2));
assert_eq!(field.latitudes().unwrap(), None);
assert_eq!(field.longitudes().unwrap(), None);
assert_eq!(
field.projected_x_coordinates().unwrap().unwrap(),
vec![0.0, 4_000.0, 8_000.0]
);
assert_eq!(
field.projected_y_coordinates().unwrap().unwrap(),
vec![-0.0, -5_000.0]
);
match field.grid_definition() {
GridDefinition::AlbersEqualArea(grid) => {
assert_eq!(grid.core.number_of_points, 6);
assert_eq!(grid.core.shape_of_earth, 6);
assert_eq!(grid.core.nx, 3);
assert_eq!(grid.core.ny, 2);
assert_eq!(grid.core.lat_first, 23_000_000);
assert_eq!(grid.core.lon_first, 240_000_000);
assert_eq!(grid.lat_d, 25_000_000);
assert_eq!(grid.lon_v, 265_000_000);
assert_eq!(grid.core.dx, 4_000_000);
assert_eq!(grid.core.dy, 5_000_000);
assert_eq!(grid.latin1, 29_500_000);
assert_eq!(grid.latin2, 45_500_000);
assert_eq!(grid.lat_southern_pole, -90_000_000);
assert_eq!(grid.lon_southern_pole, 0);
}
other => panic!("expected Albers equal-area grid, got {other:?}"),
}
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
}
#[test]
fn open_grib2_polar_stereographic_field_and_decode_flat_data() {
let opened = GribFile::from_bytes(build_grib2_polar_stereographic_message()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.grid_shape(), (3, 2));
assert_eq!(field.latitudes().unwrap(), None);
assert_eq!(field.longitudes().unwrap(), None);
assert_eq!(
field.projected_x_coordinates().unwrap().unwrap(),
vec![0.0, 3_000.0, 6_000.0]
);
assert_eq!(
field.projected_y_coordinates().unwrap().unwrap(),
vec![-0.0, -3_000.0]
);
match field.grid_definition() {
GridDefinition::PolarStereographic(grid) => {
assert_eq!(grid.core.number_of_points, 6);
assert_eq!(grid.core.shape_of_earth, 6);
assert_eq!(grid.core.nx, 3);
assert_eq!(grid.core.ny, 2);
assert_eq!(grid.core.lat_first, 41_612_949);
assert_eq!(grid.core.lon_first, 185_117_126);
assert_eq!(grid.lat_d, 60_000_000);
assert_eq!(grid.lon_v, 225_000_000);
assert_eq!(grid.core.dx, 3_000_000);
assert_eq!(grid.core.dy, 3_000_000);
assert_eq!(grid.projection_center_flag, 0);
}
other => panic!("expected polar stereographic grid, got {other:?}"),
}
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
let array = field.read_data_as_f64().unwrap();
assert_eq!(array.shape(), &[2, 3]);
assert_eq!(
array.iter().copied().collect::<Vec<_>>(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
}
#[test]
fn open_grib2_polar_stereographic_normalizes_alternating_scan_rows() {
let opened =
GribFile::from_bytes(build_grib2_polar_stereographic_alternating_message()).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
);
}
#[test]
fn open_grib2_simple_field_applies_decimal_scale_to_reference_and_values() {
let mut bytes = build_grib2_message(&[0, 10, 20, 30]);
let section5_offset = 16 + 21 + 72 + 34;
bytes[section5_offset + 11..section5_offset + 15].copy_from_slice(&10f32.to_be_bytes());
bytes[section5_offset + 15..section5_offset + 17].copy_from_slice(&0i16.to_be_bytes());
bytes[section5_offset + 17..section5_offset + 19].copy_from_slice(&1i16.to_be_bytes());
let opened = GribFile::from_bytes(bytes).unwrap();
let decoded = opened
.message(0)
.unwrap()
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>();
assert!((decoded[0] - 1.0).abs() < 1e-12);
assert!((decoded[1] - 2.0).abs() < 1e-12);
assert!((decoded[2] - 3.0).abs() < 1e-12);
assert!((decoded[3] - 4.0).abs() < 1e-12);
}
#[test]
fn computes_valid_time_from_forecast_lead() {
let opened =
GribFile::from_bytes(build_grib2_message_with_forecast(&[1, 2, 3, 4], 18)).unwrap();
let field = opened.message(0).unwrap();
let valid = field.valid_time().unwrap();
assert_eq!(field.forecast_time_unit(), Some(1));
assert_eq!(
field.forecast_time_unit_kind(),
Some(ForecastTimeUnit::Hour)
);
assert_eq!(field.forecast_time(), Some(18));
assert_eq!(valid.year, 2026);
assert_eq!(valid.month, 3);
assert_eq!(valid.day, 21);
assert_eq!(valid.hour, 6);
assert_eq!(valid.minute, 0);
assert_eq!(valid.second, 0);
}
#[test]
fn computes_grib1_indicator_ten_valid_time_from_u16_lead() {
let mut bytes = build_grib1_message(&[5, 6, 7, 8]);
let pds_offset = 8;
bytes[pds_offset + 18] = 0x01;
bytes[pds_offset + 19] = 0x2c;
bytes[pds_offset + 20] = 10;
let opened = GribFile::from_bytes(bytes).unwrap();
let field = opened.message(0).unwrap();
let valid = field.valid_time().unwrap();
assert_eq!(field.forecast_time_unit(), Some(1));
assert_eq!(
field.forecast_time_unit_kind(),
Some(ForecastTimeUnit::Hour)
);
assert_eq!(field.forecast_time(), Some(300));
assert_eq!(valid.year, 2026);
assert_eq!(valid.month, 4);
assert_eq!(valid.day, 2);
assert_eq!(valid.hour, 0);
assert_eq!(valid.minute, 0);
assert_eq!(valid.second, 0);
}
#[test]
fn computes_grib1_accumulation_valid_time_from_end_of_period() {
let mut bytes = build_grib1_message(&[5, 6, 7, 8]);
let pds_offset = 8;
bytes[pds_offset + 18] = 6;
bytes[pds_offset + 19] = 12;
bytes[pds_offset + 20] = 4;
let opened = GribFile::from_bytes(bytes).unwrap();
let field = opened.message(0).unwrap();
let valid = field.valid_time().unwrap();
assert_eq!(field.forecast_time(), None);
assert_eq!(valid.year, 2026);
assert_eq!(valid.month, 3);
assert_eq!(valid.day, 21);
assert_eq!(valid.hour, 0);
}
#[test]
fn computes_grib1_quarter_hour_valid_time() {
let mut bytes = build_grib1_message(&[5, 6, 7, 8]);
let pds_offset = 8;
bytes[pds_offset + 17] = 13;
bytes[pds_offset + 18] = 2;
bytes[pds_offset + 20] = 0;
let opened = GribFile::from_bytes(bytes).unwrap();
let field = opened.message(0).unwrap();
let valid = field.valid_time().unwrap();
assert_eq!(field.forecast_time_unit(), Some(13));
assert_eq!(
field.forecast_time_unit_kind(),
Some(ForecastTimeUnit::QuarterHour)
);
assert_eq!(field.forecast_time(), Some(2));
assert_eq!(valid.hour, 12);
assert_eq!(valid.minute, 30);
assert_eq!(valid.second, 0);
}
#[test]
fn computes_grib2_second_valid_time() {
let mut bytes = build_grib2_message_with_forecast(&[1, 2, 3, 4], 30);
let product_offset = 16 + 21 + 72;
bytes[product_offset + 17] = 13;
let opened = GribFile::from_bytes(bytes).unwrap();
let field = opened.message(0).unwrap();
let valid = field.valid_time().unwrap();
assert_eq!(field.forecast_time_unit(), Some(13));
assert_eq!(
field.forecast_time_unit_kind(),
Some(ForecastTimeUnit::Second)
);
assert_eq!(field.forecast_time(), Some(30));
assert_eq!(valid.hour, 12);
assert_eq!(valid.minute, 0);
assert_eq!(valid.second, 30);
}
#[test]
fn opens_grib2_individual_ensemble_product_template() {
let opened = GribFile::from_bytes(replace_grib2_product_section(
build_grib2_message(&[1, 2, 3, 4]),
product_section_template_one(),
))
.unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.product_definition().unwrap().template_number(), 1);
match &field.product_definition().unwrap().template {
ProductDefinitionTemplate::IndividualEnsembleForecast(template) => {
assert_eq!(template.type_of_ensemble_forecast, 1);
assert_eq!(template.perturbation_number, 2);
assert_eq!(template.number_of_forecasts_in_ensemble, 20);
}
other => panic!("expected template 4.1, got {other:?}"),
}
assert_eq!(
field.read_flat_data_as_f64().unwrap(),
vec![1.0, 2.0, 3.0, 4.0]
);
}
#[test]
fn opens_grib2_statistical_process_product_template() {
let opened = GribFile::from_bytes(replace_grib2_product_section(
build_grib2_message(&[1, 2, 3, 4]),
product_section_template_eight(),
))
.unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.product_definition().unwrap().template_number(), 8);
let valid = field.valid_time().unwrap();
assert_eq!(valid.year, 2026);
assert_eq!(valid.month, 3);
assert_eq!(valid.day, 20);
assert_eq!(valid.hour, 18);
match &field.product_definition().unwrap().template {
ProductDefinitionTemplate::StatisticalProcess(template) => {
assert_eq!(template.interval.time_ranges.len(), 1);
assert_eq!(
template.interval.time_ranges[0].type_of_statistical_processing,
1
);
assert_eq!(template.interval.time_ranges[0].time_range_length, 6);
}
other => panic!("expected template 4.8, got {other:?}"),
}
}
#[test]
fn opens_grib2_ensemble_statistical_process_product_template() {
let opened = GribFile::from_bytes(replace_grib2_product_section(
build_grib2_message(&[1, 2, 3, 4]),
product_section_template_eleven(),
))
.unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.product_definition().unwrap().template_number(), 11);
let valid = field.valid_time().unwrap();
assert_eq!(valid.year, 2026);
assert_eq!(valid.month, 3);
assert_eq!(valid.day, 20);
assert_eq!(valid.hour, 18);
match &field.product_definition().unwrap().template {
ProductDefinitionTemplate::EnsembleStatisticalProcess(template) => {
assert_eq!(template.ensemble.perturbation_number, 3);
assert_eq!(template.interval.time_ranges.len(), 1);
}
other => panic!("expected template 4.11, got {other:?}"),
}
}
#[test]
fn iterates_multifield_grib2_message() {
let opened = GribFile::from_bytes(build_grib2_multifield_message()).unwrap();
let names = opened
.messages()
.map(|message| message.parameter_name().to_owned())
.collect::<Vec<_>>();
assert_eq!(names, vec!["TMP", "POT"]);
}
#[test]
fn ncep_refc_resolves_from_builtin_local_table() {
let mut bytes = build_grib2_message(&[1, 2, 3, 4]);
set_grib2_identification(&mut bytes, 7, 0, 1);
set_grib2_product_parameter(&mut bytes, 16, 196);
let opened = GribFile::from_bytes(bytes).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.parameter_name(), "REFC");
assert_eq!(
field.parameter_description(),
"Maximum/Composite radar reflectivity"
);
assert_eq!(
field.parameter().source,
ParameterTableSource::Local {
center_id: 7,
subcenter_id: 0,
local_table_version: 1,
}
);
}
#[test]
fn unknown_local_grib2_parameter_remains_unknown_local() {
let mut bytes = build_grib2_message(&[1, 2, 3, 4]);
set_grib2_identification(&mut bytes, 8, 0, 1);
set_grib2_product_parameter(&mut bytes, 16, 196);
let opened = GribFile::from_bytes(bytes).unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.parameter_name(), "unknown");
assert_eq!(field.parameter_description(), "Unknown parameter");
assert_eq!(
field.parameter().source,
ParameterTableSource::UnknownLocal {
center_id: 8,
subcenter_id: 0,
local_table_version: 1,
}
);
}
#[test]
fn caller_local_entries_resolve_unknown_center_local_parameters() {
let mut bytes = build_grib2_message(&[1, 2, 3, 4]);
set_grib2_identification(&mut bytes, 8, 0, 1);
set_grib2_product_parameter(&mut bytes, 16, 196);
let local_parameters = [LocalParameterEntry {
center_id: 8,
subcenter_id: Some(0),
local_table_version: Some(1),
discipline: 0,
category: 16,
number: 196,
short_name: "LREFC",
description: "Local composite reflectivity",
}];
let opened = GribFile::builder()
.local_parameters(&local_parameters)
.from_bytes(bytes)
.unwrap();
let field = opened.message(0).unwrap();
assert_eq!(field.parameter_name(), "LREFC");
assert_eq!(
field.parameter_description(),
"Local composite reflectivity"
);
assert_eq!(
field.parameter().source,
ParameterTableSource::Local {
center_id: 8,
subcenter_id: 0,
local_table_version: 1,
}
);
}
#[test]
fn tolerant_open_skips_malformed_candidates() {
let mut bytes = b"junkGRIB\x00\x00\x00\x02not-a-real-message".to_vec();
bytes.extend_from_slice(&build_grib2_message(&[9, 8, 7, 6]));
let opened = GribFile::builder().strict(false).from_bytes(bytes).unwrap();
assert_eq!(opened.message_count(), 1);
assert_eq!(
opened
.message(0)
.unwrap()
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>(),
vec![9.0, 8.0, 7.0, 6.0]
);
}
#[test]
fn unsupported_product_template_does_not_poison_file_open() {
let mut bytes = build_grib2_message(&[1, 2, 3, 4]);
let product_offset = 16 + 21 + 72;
bytes[product_offset + 7..product_offset + 9].copy_from_slice(&99u16.to_be_bytes());
bytes.extend_from_slice(&build_grib2_message(&[9, 8, 7, 6]));
let opened = GribFile::builder().strict(false).from_bytes(bytes).unwrap();
assert_eq!(opened.message_count(), 2);
let first = opened.message(0).unwrap();
assert_eq!(first.product_definition().unwrap().template_number(), 99);
assert_eq!(first.forecast_time(), None);
assert!(matches!(
&first.product_definition().unwrap().template,
ProductDefinitionTemplate::Unsupported { number: 99, .. }
));
assert_eq!(
opened.message(1).unwrap().read_flat_data_as_f64().unwrap(),
vec![9.0, 8.0, 7.0, 6.0]
);
}
#[test]
fn open_grib1_bitmap_field_ignores_padding_bits() {
let opened = GribFile::from_bytes(build_grib1_message_with_bitmap(
&[9, 7],
3,
1,
Some(&[0b1011_1111]),
))
.unwrap();
let decoded = opened
.message(0)
.unwrap()
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>();
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0], 9.0);
assert!(decoded[1].is_nan());
assert_eq!(decoded[2], 7.0);
}
#[test]
fn open_grib2_complex_packing_field_and_decode() {
let opened = GribFile::from_bytes(build_grib2_complex_packing_message()).unwrap();
let decoded = opened
.message(0)
.unwrap()
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>();
assert_eq!(decoded, vec![3.0, 4.0, 5.0, 9.0]);
}
#[test]
fn open_grib2_complex_packing_field_with_missing_values() {
let opened = GribFile::from_bytes(build_grib2_complex_packing_message_with_missing()).unwrap();
let decoded = opened
.message(0)
.unwrap()
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>();
assert_eq!(decoded[0], 7.0);
assert!(decoded[1].is_nan());
assert_eq!(decoded[2], 9.0);
assert!(decoded[3].is_nan());
}
#[test]
fn open_grib2_spatial_differencing_field_and_decode() {
let opened = GribFile::from_bytes(build_grib2_spatial_differencing_message()).unwrap();
let decoded = opened
.message(0)
.unwrap()
.read_data_as_f64()
.unwrap()
.iter()
.copied()
.collect::<Vec<_>>();
assert_eq!(decoded, vec![10.0, 12.0, 15.0, 19.0]);
}
fn replace_grib2_product_section(mut bytes: Vec<u8>, section: Vec<u8>) -> Vec<u8> {
let offset = grib2_section_offset(&bytes, 4);
let old_len = u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize;
bytes.splice(offset..offset + old_len, section);
let total_len = bytes.len() as u64;
bytes[8..16].copy_from_slice(&total_len.to_be_bytes());
bytes
}
fn base_product_section() -> Vec<u8> {
let bytes = build_grib2_message(&[1, 2, 3, 4]);
let offset = grib2_section_offset(&bytes, 4);
let len = u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize;
bytes[offset..offset + len].to_vec()
}
fn product_section_template_one() -> Vec<u8> {
let mut section = base_product_section();
section.resize(37, 0);
section[..4].copy_from_slice(&37u32.to_be_bytes());
section[7..9].copy_from_slice(&1u16.to_be_bytes());
section[34] = 1;
section[35] = 2;
section[36] = 20;
section
}
fn product_section_template_eight() -> Vec<u8> {
let mut section = base_product_section();
section.resize(58, 0);
section[..4].copy_from_slice(&58u32.to_be_bytes());
section[7..9].copy_from_slice(&8u16.to_be_bytes());
section[34..36].copy_from_slice(&2026u16.to_be_bytes());
section[36] = 3;
section[37] = 20;
section[38] = 18;
section[41] = 1;
section[46] = 1;
section[47] = 2;
section[48] = 1;
section[49..53].copy_from_slice(&6u32.to_be_bytes());
section[53] = 255;
section
}
fn product_section_template_eleven() -> Vec<u8> {
let mut section = product_section_template_eight();
section.resize(61, 0);
section[..4].copy_from_slice(&61u32.to_be_bytes());
section[7..9].copy_from_slice(&11u16.to_be_bytes());
section.copy_within(34..58, 37);
section[34] = 1;
section[35] = 3;
section[36] = 20;
section
}
fn set_grib2_identification(
bytes: &mut [u8],
center_id: u16,
subcenter_id: u16,
local_table_version: u8,
) {
let offset = grib2_section_offset(bytes, 1);
bytes[offset + 5..offset + 7].copy_from_slice(¢er_id.to_be_bytes());
bytes[offset + 7..offset + 9].copy_from_slice(&subcenter_id.to_be_bytes());
bytes[offset + 10] = local_table_version;
}
fn set_grib2_product_parameter(bytes: &mut [u8], category: u8, number: u8) {
let offset = grib2_section_offset(bytes, 4);
bytes[offset + 9] = category;
bytes[offset + 10] = number;
}
fn grib2_section_offset(bytes: &[u8], section_number: u8) -> usize {
let mut offset = 16;
while offset + 5 <= bytes.len().saturating_sub(4) {
let length = u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize;
assert!(length >= 5, "invalid section length {length}");
if bytes[offset + 4] == section_number {
return offset;
}
offset += length;
}
panic!("section {section_number} not found");
}