Skip to content

Commit 33033be

Browse files
committed
Preserve unsupported products and missing fixed-surface values
1 parent 41c885d commit 33033be

8 files changed

Lines changed: 152 additions & 52 deletions

File tree

grib-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ pub use parameter::{
2929
};
3030
pub use product::{
3131
AnalysisOrForecastTemplate, EnsembleStatisticalProcessTemplate, FixedSurface, Identification,
32-
IndividualEnsembleForecastTemplate, ProductDefinition, ProductDefinitionTemplate,
32+
IndividualEnsembleForecastTemplate, ProductDefinition, ProductDefinitionTemplate, ScaledValue,
3333
StatisticalProcessTemplate, StatisticalTimeRange,
3434
};

grib-core/src/product.rs

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,40 @@ impl Identification {
7070
#[derive(Debug, Clone, PartialEq)]
7171
pub struct FixedSurface {
7272
pub surface_type: u8,
73+
/// The numeric level, or `None` when either WMO numeric component is
74+
/// encoded with its missing-value sentinel.
75+
pub value: Option<ScaledValue>,
76+
}
77+
78+
/// The scale/value pair used to encode a fixed-surface numeric level.
79+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80+
pub struct ScaledValue {
7381
pub scale_factor: i16,
7482
pub scaled_value: i32,
7583
}
7684

7785
impl FixedSurface {
78-
pub fn scaled_value_f64(&self) -> f64 {
79-
let factor = 10.0_f64.powi(-(self.scale_factor as i32));
80-
self.scaled_value as f64 * factor
86+
pub const fn with_value(surface_type: u8, scale_factor: i16, scaled_value: i32) -> Self {
87+
Self {
88+
surface_type,
89+
value: Some(ScaledValue {
90+
scale_factor,
91+
scaled_value,
92+
}),
93+
}
94+
}
95+
96+
pub const fn without_value(surface_type: u8) -> Self {
97+
Self {
98+
surface_type,
99+
value: None,
100+
}
101+
}
102+
103+
pub fn scaled_value_f64(&self) -> Option<f64> {
104+
let value = self.value?;
105+
let factor = 10.0_f64.powi(-i32::from(value.scale_factor));
106+
Some(f64::from(value.scaled_value) * factor)
81107
}
82108
}
83109

@@ -91,11 +117,19 @@ pub struct ProductDefinition {
91117

92118
/// Typed GRIB2 Product Definition templates.
93119
#[derive(Debug, Clone, PartialEq)]
120+
#[non_exhaustive]
94121
pub enum ProductDefinitionTemplate {
95122
AnalysisOrForecast(AnalysisOrForecastTemplate),
96123
IndividualEnsembleForecast(IndividualEnsembleForecastTemplate),
97124
StatisticalProcess(StatisticalProcessTemplate),
98125
EnsembleStatisticalProcess(EnsembleStatisticalProcessTemplate),
126+
/// A well-framed Section 4 whose template is not interpreted by this
127+
/// version of the library. `raw` contains the template-specific bytes
128+
/// following the common parameter category and number.
129+
Unsupported {
130+
number: u16,
131+
raw: Vec<u8>,
132+
},
99133
}
100134

101135
/// Product Definition Template 4.0: analysis or forecast at a horizontal level.
@@ -186,23 +220,27 @@ impl ProductDefinition {
186220
}
187221

188222
pub fn generating_process(&self) -> Option<u8> {
189-
Some(self.template.base().generating_process)
223+
self.template.base().map(|base| base.generating_process)
190224
}
191225

192226
pub fn forecast_time_unit(&self) -> Option<u8> {
193-
Some(self.template.base().forecast_time_unit)
227+
self.template.base().map(|base| base.forecast_time_unit)
194228
}
195229

196230
pub fn forecast_time(&self) -> Option<u32> {
197-
Some(self.template.base().forecast_time)
231+
self.template.base().map(|base| base.forecast_time)
198232
}
199233

200234
pub fn first_surface(&self) -> Option<&FixedSurface> {
201-
self.template.base().first_surface.as_ref()
235+
self.template
236+
.base()
237+
.and_then(|base| base.first_surface.as_ref())
202238
}
203239

204240
pub fn second_surface(&self) -> Option<&FixedSurface> {
205-
self.template.base().second_surface.as_ref()
241+
self.template
242+
.base()
243+
.and_then(|base| base.second_surface.as_ref())
206244
}
207245

208246
pub fn end_of_overall_time_interval(&self) -> Option<ReferenceTime> {
@@ -225,7 +263,17 @@ impl ProductDefinitionTemplate {
225263
11 => Ok(Self::EnsembleStatisticalProcess(
226264
EnsembleStatisticalProcessTemplate::parse(section_bytes)?,
227265
)),
228-
other => Err(Error::UnsupportedProductTemplate(other)),
266+
number => {
267+
let raw_len = section_bytes.len() - 11;
268+
let mut raw = Vec::new();
269+
raw.try_reserve(raw_len).map_err(|err| {
270+
Error::Other(format!(
271+
"failed to reserve {raw_len} unsupported product-template bytes: {err}"
272+
))
273+
})?;
274+
raw.extend_from_slice(&section_bytes[11..]);
275+
Ok(Self::Unsupported { number, raw })
276+
}
229277
}
230278
}
231279

@@ -235,16 +283,18 @@ impl ProductDefinitionTemplate {
235283
Self::IndividualEnsembleForecast(_) => 1,
236284
Self::StatisticalProcess(_) => 8,
237285
Self::EnsembleStatisticalProcess(_) => 11,
286+
Self::Unsupported { number, .. } => *number,
238287
}
239288
}
240289

241-
fn base(&self) -> &AnalysisOrForecastTemplate {
242-
match self {
290+
fn base(&self) -> Option<&AnalysisOrForecastTemplate> {
291+
Some(match self {
243292
Self::AnalysisOrForecast(template) => template,
244293
Self::IndividualEnsembleForecast(template) => &template.base,
245294
Self::StatisticalProcess(template) => &template.base,
246295
Self::EnsembleStatisticalProcess(template) => &template.ensemble.base,
247-
}
296+
Self::Unsupported { .. } => return None,
297+
})
248298
}
249299

250300
fn end_of_overall_time_interval(&self) -> Option<ReferenceTime> {
@@ -254,6 +304,7 @@ impl ProductDefinitionTemplate {
254304
Some(template.end_of_overall_time_interval)
255305
}
256306
Self::AnalysisOrForecast(_) | Self::IndividualEnsembleForecast(_) => None,
307+
Self::Unsupported { .. } => None,
257308
}
258309
}
259310
}
@@ -398,10 +449,18 @@ fn parse_surface(section_bytes: &[u8]) -> Option<FixedSurface> {
398449
return None;
399450
}
400451

452+
let value = if section_bytes[1] == 0xff || section_bytes[2..6] == [0xff; 4] {
453+
None
454+
} else {
455+
Some(ScaledValue {
456+
scale_factor: grib_i8(section_bytes[1]),
457+
scaled_value: grib_i32(&section_bytes[2..6])?,
458+
})
459+
};
460+
401461
Some(FixedSurface {
402462
surface_type,
403-
scale_factor: grib_i8(section_bytes[1]),
404-
scaled_value: grib_i32(&section_bytes[2..6])?,
463+
value,
405464
})
406465
}
407466

@@ -447,7 +506,10 @@ mod tests {
447506
assert_eq!(product.parameter_number, 3);
448507
assert_eq!(product.template_number(), 0);
449508
assert_eq!(product.forecast_time(), Some(6));
450-
assert_eq!(product.first_surface().unwrap().scaled_value_f64(), 850.0);
509+
assert_eq!(
510+
product.first_surface().unwrap().scaled_value_f64(),
511+
Some(850.0)
512+
);
451513
assert_eq!(
452514
product.template,
453515
ProductDefinitionTemplate::AnalysisOrForecast(AnalysisOrForecastTemplate {
@@ -557,16 +619,38 @@ mod tests {
557619
}
558620

559621
#[test]
560-
fn rejects_unsupported_product_definition_templates() {
622+
fn preserves_unsupported_product_definition_templates() {
561623
let mut section = vec![0u8; 34];
562624
section[..4].copy_from_slice(&(34u32).to_be_bytes());
563625
section[4] = 4;
564626
section[7..9].copy_from_slice(&99u16.to_be_bytes());
565627
section[9] = 2;
566628
section[10] = 3;
567629

568-
let err = ProductDefinition::parse(&section).unwrap_err();
569-
assert!(matches!(err, Error::UnsupportedProductTemplate(99)));
630+
section[11..].copy_from_slice(&[0x5a; 23]);
631+
632+
let product = ProductDefinition::parse(&section).unwrap();
633+
assert_eq!(product.template_number(), 99);
634+
assert_eq!(product.forecast_time(), None);
635+
assert_eq!(product.first_surface(), None);
636+
assert!(matches!(
637+
product.template,
638+
ProductDefinitionTemplate::Unsupported { number: 99, ref raw }
639+
if raw == &[0x5a; 23]
640+
));
641+
}
642+
643+
#[test]
644+
fn preserves_surface_type_when_numeric_level_is_missing() {
645+
let mut section = product_section_template_zero();
646+
section[23] = 0xff;
647+
section[24..28].copy_from_slice(&[0xff; 4]);
648+
649+
let product = ProductDefinition::parse(&section).unwrap();
650+
let surface = product.first_surface().unwrap();
651+
assert_eq!(surface.surface_type, 103);
652+
assert_eq!(surface.value, None);
653+
assert_eq!(surface.scaled_value_f64(), None);
570654
}
571655

572656
#[test]

grib-reader/fuzz/fuzz_targets/fuzz_grib_writer_inputs.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,7 @@ fn product(input: &mut Input<'_>) -> ProductDefinition {
220220
generating_process: 2,
221221
forecast_time_unit: 1,
222222
forecast_time: u32::from(input.u8()),
223-
first_surface: Some(FixedSurface {
224-
surface_type: 103,
225-
scale_factor: 0,
226-
scaled_value: 850,
227-
}),
223+
first_surface: Some(FixedSurface::with_value(103, 0, 850)),
228224
second_surface: None,
229225
}),
230226
}

grib-reader/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub use parameter::{
5757
};
5858
pub use product::{
5959
AnalysisOrForecastTemplate, EnsembleStatisticalProcessTemplate, FixedSurface, Identification,
60-
IndividualEnsembleForecastTemplate, ProductDefinition, ProductDefinitionTemplate,
60+
IndividualEnsembleForecastTemplate, ProductDefinition, ProductDefinitionTemplate, ScaledValue,
6161
StatisticalProcessTemplate, StatisticalTimeRange,
6262
};
6363

grib-reader/src/product.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pub use grib_core::product::{
22
AnalysisOrForecastTemplate, EnsembleStatisticalProcessTemplate, FixedSurface, Identification,
3-
IndividualEnsembleForecastTemplate, ProductDefinition, ProductDefinitionTemplate,
3+
IndividualEnsembleForecastTemplate, ProductDefinition, ProductDefinitionTemplate, ScaledValue,
44
StatisticalProcessTemplate, StatisticalTimeRange,
55
};

grib-reader/tests/integration.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -570,24 +570,33 @@ fn tolerant_open_skips_malformed_candidates() {
570570
}
571571

572572
#[test]
573-
fn tolerant_open_still_reports_unsupported_messages() {
573+
fn unsupported_product_template_does_not_poison_file_open() {
574574
let mut bytes = build_grib2_message(&[1, 2, 3, 4]);
575575
let product_offset = 16 + 21 + 72;
576576
bytes[product_offset + 7..product_offset + 9].copy_from_slice(&99u16.to_be_bytes());
577577
bytes.extend_from_slice(&build_grib2_message(&[9, 8, 7, 6]));
578578

579-
let err = match GribFile::from_bytes_with_options(
579+
let opened = GribFile::from_bytes_with_options(
580580
bytes,
581581
OpenOptions {
582582
strict: false,
583583
..OpenOptions::default()
584584
},
585-
) {
586-
Ok(_) => panic!("expected unsupported product template error"),
587-
Err(err) => err,
588-
};
585+
)
586+
.unwrap();
589587

590-
assert!(matches!(err, Error::UnsupportedProductTemplate(99)));
588+
assert_eq!(opened.message_count(), 2);
589+
let first = opened.message(0).unwrap();
590+
assert_eq!(first.product_definition().unwrap().template_number(), 99);
591+
assert_eq!(first.forecast_time(), None);
592+
assert!(matches!(
593+
&first.product_definition().unwrap().template,
594+
ProductDefinitionTemplate::Unsupported { number: 99, .. }
595+
));
596+
assert_eq!(
597+
opened.message(1).unwrap().read_flat_data_as_f64().unwrap(),
598+
vec![9.0, 8.0, 7.0, 6.0]
599+
);
591600
}
592601

593602
#[test]

grib-writer/src/lib.rs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2051,6 +2051,10 @@ fn write_product_section(out: &mut Vec<u8>, product: &ProductDefinition) -> Resu
20512051
write_u32_be(out, template.number_of_missing_in_statistical_process)?;
20522052
write_statistical_time_ranges(out, &template.time_ranges)
20532053
}
2054+
ProductDefinitionTemplate::Unsupported { number, .. } => {
2055+
Err(Error::UnsupportedProductTemplate(*number))
2056+
}
2057+
template => Err(Error::UnsupportedProductTemplate(template.number())),
20542058
}
20552059
}
20562060

@@ -2148,15 +2152,26 @@ fn write_surface(out: &mut Vec<u8>, surface: Option<&FixedSurface>) -> Result<()
21482152
match surface {
21492153
Some(surface) => {
21502154
write_u8_be(out, surface.surface_type)?;
2151-
write_u8_be(
2152-
out,
2153-
encode_wmo_i8(surface.scale_factor).ok_or_else(|| {
2154-
Error::Other("fixed-surface scale factor does not fit GRIB signed i8".into())
2155-
})?,
2156-
)?;
2157-
out.extend_from_slice(&encode_wmo_i32(surface.scaled_value).ok_or_else(|| {
2158-
Error::Other("fixed-surface scaled value does not fit GRIB signed i32".into())
2159-
})?);
2155+
match surface.value {
2156+
Some(value) => {
2157+
write_u8_be(
2158+
out,
2159+
encode_wmo_i8(value.scale_factor).ok_or_else(|| {
2160+
Error::Other(
2161+
"fixed-surface scale factor does not fit GRIB signed i8".into(),
2162+
)
2163+
})?,
2164+
)?;
2165+
out.extend_from_slice(&encode_wmo_i32(value.scaled_value).ok_or_else(
2166+
|| {
2167+
Error::Other(
2168+
"fixed-surface scaled value does not fit GRIB signed i32".into(),
2169+
)
2170+
},
2171+
)?);
2172+
}
2173+
None => out.extend_from_slice(&[0xff; 5]),
2174+
}
21602175
Ok(())
21612176
}
21622177
None => {
@@ -2420,6 +2435,10 @@ fn validate_supported_product(product: &ProductDefinition) -> Result<()> {
24202435
checked_time_range_count(template.time_ranges.len())?;
24212436
validate_reference_time(template.end_of_overall_time_interval)
24222437
}
2438+
ProductDefinitionTemplate::Unsupported { number, .. } => {
2439+
Err(Error::UnsupportedProductTemplate(*number))
2440+
}
2441+
template => Err(Error::UnsupportedProductTemplate(template.number())),
24232442
}
24242443
}
24252444

@@ -2648,11 +2667,7 @@ mod tests {
26482667
generating_process: 2,
26492668
forecast_time_unit: 1,
26502669
forecast_time: 6,
2651-
first_surface: Some(FixedSurface {
2652-
surface_type: 103,
2653-
scale_factor: 0,
2654-
scaled_value: 850,
2655-
}),
2670+
first_surface: Some(FixedSurface::with_value(103, 0, 850)),
26562671
second_surface: None,
26572672
}
26582673
}

grib-writer/tests/common/mod.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -396,11 +396,7 @@ pub fn product(parameter_category: u8, parameter_number: u8) -> ProductDefinitio
396396
generating_process: 2,
397397
forecast_time_unit: 1,
398398
forecast_time: 6,
399-
first_surface: Some(FixedSurface {
400-
surface_type: 103,
401-
scale_factor: 0,
402-
scaled_value: 850,
403-
}),
399+
first_surface: Some(FixedSurface::with_value(103, 0, 850)),
404400
second_surface: None,
405401
}),
406402
}

0 commit comments

Comments
 (0)