Skip to content

Commit 6a905c0

Browse files
committed
Validate GRIB reference timestamps
1 parent 9d1f70c commit 6a905c0

5 files changed

Lines changed: 198 additions & 26 deletions

File tree

grib-core/src/grib1.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ impl ProductDefinition {
5151
parameter_number: section_bytes[8],
5252
level_type: section_bytes[9],
5353
level_value: u16::from_be_bytes(section_bytes[10..12].try_into().unwrap()),
54-
reference_time: parse_reference_time(section_bytes),
54+
reference_time: parse_reference_time(section_bytes)?,
5555
forecast_time_unit: section_bytes[17],
5656
p1: section_bytes[18],
5757
p2: section_bytes[19],
@@ -179,22 +179,24 @@ impl BinaryDataSection {
179179
}
180180
}
181181

182-
fn parse_reference_time(section_bytes: &[u8]) -> ReferenceTime {
182+
fn parse_reference_time(section_bytes: &[u8]) -> Result<ReferenceTime> {
183183
let century = section_bytes[24];
184184
let year_of_century = u16::from(section_bytes[12]);
185185
let year = match century {
186186
0 => year_of_century,
187187
c => (u16::from(c) - 1) * 100 + year_of_century,
188188
};
189189

190-
ReferenceTime {
190+
let reference_time = ReferenceTime {
191191
year,
192192
month: section_bytes[13],
193193
day: section_bytes[14],
194194
hour: section_bytes[15],
195195
minute: section_bytes[16],
196196
second: 0,
197-
}
197+
};
198+
reference_time.validate_in_section(1)?;
199+
Ok(reference_time)
198200
}
199201

200202
fn parse_latlon_grid(section_bytes: &[u8]) -> GridDefinition {
@@ -228,6 +230,7 @@ fn parse_latlon_grid(section_bytes: &[u8]) -> GridDefinition {
228230
#[cfg(test)]
229231
mod tests {
230232
use super::ProductDefinition;
233+
use crate::error::Error;
231234
use crate::metadata::ReferenceTime;
232235

233236
#[test]
@@ -263,4 +266,49 @@ mod tests {
263266

264267
assert_eq!(product.forecast_time(), Some(300));
265268
}
269+
270+
#[test]
271+
fn rejects_invalid_product_definition_reference_time() {
272+
let mut section = valid_product_definition_section();
273+
section[13] = 2;
274+
section[14] = 29;
275+
let err = ProductDefinition::parse(&section).unwrap_err();
276+
assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
277+
assert!(err.to_string().contains("invalid reference timestamp"));
278+
279+
let mut section = valid_product_definition_section();
280+
section[15] = 24;
281+
let err = ProductDefinition::parse(&section).unwrap_err();
282+
assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
283+
284+
let mut section = valid_product_definition_section();
285+
section[16] = 60;
286+
let err = ProductDefinition::parse(&section).unwrap_err();
287+
assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
288+
}
289+
290+
fn valid_product_definition_section() -> Vec<u8> {
291+
let mut section = vec![0u8; 28];
292+
section[..3].copy_from_slice(&[0, 0, 28]);
293+
section[3] = 2;
294+
section[4] = 7;
295+
section[5] = 255;
296+
section[6] = 0;
297+
section[7] = 0b1000_0000;
298+
section[8] = 11;
299+
section[9] = 100;
300+
section[10..12].copy_from_slice(&850u16.to_be_bytes());
301+
section[12] = 26;
302+
section[13] = 3;
303+
section[14] = 20;
304+
section[15] = 12;
305+
section[16] = 0;
306+
section[17] = 1;
307+
section[18] = 0;
308+
section[19] = 0;
309+
section[20] = 0;
310+
section[24] = 21;
311+
section[25] = 0;
312+
section
313+
}
266314
}

grib-core/src/metadata.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Edition-independent field metadata.
22
3+
use crate::error::{Error, Result};
4+
35
use std::borrow::Cow;
46

57
/// Semantic forecast-time units shared across GRIB editions.
@@ -101,6 +103,25 @@ pub struct ReferenceTime {
101103
}
102104

103105
impl ReferenceTime {
106+
/// Return whether this timestamp has valid calendar and time-of-day fields.
107+
pub fn is_valid(&self) -> bool {
108+
self.seconds_since_epoch().is_some()
109+
}
110+
111+
pub(crate) fn validate_in_section(&self, section: u8) -> Result<()> {
112+
if self.is_valid() {
113+
return Ok(());
114+
}
115+
116+
Err(Error::InvalidSection {
117+
section,
118+
reason: format!(
119+
"invalid reference timestamp {:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
120+
self.year, self.month, self.day, self.hour, self.minute, self.second
121+
),
122+
})
123+
}
124+
104125
/// Add a GRIB forecast lead using a semantic forecast-time unit.
105126
///
106127
/// Returns `None` for calendar-dependent units or invalid timestamps.
@@ -390,6 +411,47 @@ mod tests {
390411
.is_none());
391412
}
392413

414+
#[test]
415+
fn validates_calendar_and_time_ranges() {
416+
assert!(ReferenceTime {
417+
year: 2024,
418+
month: 2,
419+
day: 29,
420+
hour: 23,
421+
minute: 59,
422+
second: 59,
423+
}
424+
.is_valid());
425+
426+
assert!(!ReferenceTime {
427+
year: 2026,
428+
month: 2,
429+
day: 29,
430+
hour: 12,
431+
minute: 0,
432+
second: 0,
433+
}
434+
.is_valid());
435+
assert!(!ReferenceTime {
436+
year: 2026,
437+
month: 13,
438+
day: 1,
439+
hour: 0,
440+
minute: 0,
441+
second: 0,
442+
}
443+
.is_valid());
444+
assert!(!ReferenceTime {
445+
year: 2026,
446+
month: 3,
447+
day: 20,
448+
hour: 24,
449+
minute: 0,
450+
second: 0,
451+
}
452+
.is_valid());
453+
}
454+
393455
#[test]
394456
fn decodes_edition_specific_forecast_units() {
395457
assert_eq!(

grib-core/src/product.rs

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! GRIB2 metadata carried by Sections 1 and 4.
22
33
use crate::error::{Error, Result};
4+
use crate::metadata::ReferenceTime;
45
use crate::parameter;
56
use crate::util::{grib_i32, grib_i8};
67

@@ -37,18 +38,28 @@ impl Identification {
3738
});
3839
}
3940

41+
let reference_time = ReferenceTime {
42+
year: u16::from_be_bytes(section_bytes[12..14].try_into().unwrap()),
43+
month: section_bytes[14],
44+
day: section_bytes[15],
45+
hour: section_bytes[16],
46+
minute: section_bytes[17],
47+
second: section_bytes[18],
48+
};
49+
reference_time.validate_in_section(1)?;
50+
4051
Ok(Self {
4152
center_id: u16::from_be_bytes(section_bytes[5..7].try_into().unwrap()),
4253
subcenter_id: u16::from_be_bytes(section_bytes[7..9].try_into().unwrap()),
4354
master_table_version: section_bytes[9],
4455
local_table_version: section_bytes[10],
4556
significance_of_reference_time: section_bytes[11],
46-
reference_year: u16::from_be_bytes(section_bytes[12..14].try_into().unwrap()),
47-
reference_month: section_bytes[14],
48-
reference_day: section_bytes[15],
49-
reference_hour: section_bytes[16],
50-
reference_minute: section_bytes[17],
51-
reference_second: section_bytes[18],
57+
reference_year: reference_time.year,
58+
reference_month: reference_time.month,
59+
reference_day: reference_time.day,
60+
reference_hour: reference_time.hour,
61+
reference_minute: reference_time.minute,
62+
reference_second: reference_time.second,
5263
production_status: section_bytes[19],
5364
processed_data_type: section_bytes[20],
5465
})
@@ -239,29 +250,29 @@ mod tests {
239250

240251
#[test]
241252
fn parses_identification_section() {
242-
let mut section = vec![0u8; 21];
243-
section[..4].copy_from_slice(&(21u32).to_be_bytes());
244-
section[4] = 1;
245-
section[5..7].copy_from_slice(&7u16.to_be_bytes());
246-
section[7..9].copy_from_slice(&14u16.to_be_bytes());
247-
section[9] = 35;
248-
section[10] = 1;
249-
section[11] = 1;
250-
section[12..14].copy_from_slice(&2026u16.to_be_bytes());
251-
section[14] = 3;
252-
section[15] = 20;
253-
section[16] = 12;
254-
section[17] = 30;
255-
section[18] = 45;
256-
section[19] = 0;
257-
section[20] = 1;
253+
let section = valid_identification_section();
258254

259255
let id = Identification::parse(&section).unwrap();
260256
assert_eq!(id.center_id, 7);
261257
assert_eq!(id.reference_year, 2026);
262258
assert_eq!(id.reference_hour, 12);
263259
}
264260

261+
#[test]
262+
fn rejects_invalid_identification_reference_time() {
263+
let mut section = valid_identification_section();
264+
section[14] = 2;
265+
section[15] = 29;
266+
let err = Identification::parse(&section).unwrap_err();
267+
assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
268+
assert!(err.to_string().contains("invalid reference timestamp"));
269+
270+
let mut section = valid_identification_section();
271+
section[18] = 60;
272+
let err = Identification::parse(&section).unwrap_err();
273+
assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
274+
}
275+
265276
#[test]
266277
fn parses_product_definition_template_zero_fields() {
267278
let mut section = vec![0u8; 34];
@@ -321,4 +332,24 @@ mod tests {
321332
let err = ProductDefinition::parse(&section).unwrap_err();
322333
assert!(matches!(err, Error::InvalidSection { section: 4, .. }));
323334
}
335+
336+
fn valid_identification_section() -> Vec<u8> {
337+
let mut section = vec![0u8; 21];
338+
section[..4].copy_from_slice(&(21u32).to_be_bytes());
339+
section[4] = 1;
340+
section[5..7].copy_from_slice(&7u16.to_be_bytes());
341+
section[7..9].copy_from_slice(&14u16.to_be_bytes());
342+
section[9] = 35;
343+
section[10] = 1;
344+
section[11] = 1;
345+
section[12..14].copy_from_slice(&2026u16.to_be_bytes());
346+
section[14] = 3;
347+
section[15] = 20;
348+
section[16] = 12;
349+
section[17] = 30;
350+
section[18] = 45;
351+
section[19] = 0;
352+
section[20] = 1;
353+
section
354+
}
324355
}

grib-reader/src/grib1.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ mod tests {
149149
let mut pds = vec![0u8; 28];
150150
pds[..3].copy_from_slice(&[0, 0, 28]);
151151
pds[7] = 0b1000_0000;
152+
pds[12] = 26;
153+
pds[13] = 3;
154+
pds[14] = 20;
155+
pds[15] = 12;
152156
pds[24] = 21;
153157
message.extend_from_slice(&pds);
154158
let mut gds = vec![0u8; 32];
@@ -173,6 +177,10 @@ mod tests {
173177
let mut pds = vec![0u8; 28];
174178
pds[..3].copy_from_slice(&[0, 0, 28]);
175179
pds[7] = 0b1000_0000;
180+
pds[12] = 26;
181+
pds[13] = 3;
182+
pds[14] = 20;
183+
pds[15] = 12;
176184
pds[24] = 21;
177185
message.extend_from_slice(&pds);
178186
let mut gds = vec![0u8; 32];

grib-reader/tests/corruption.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,29 @@ fn rejects_impossibly_short_reported_message_length() {
6868
assert!(matches!(err, Error::InvalidMessage(_)));
6969
}
7070

71+
#[test]
72+
fn rejects_invalid_grib2_reference_timestamp() {
73+
let mut message = build_grib2_message(&[1, 2, 3, 4]);
74+
let section1_offset = 16;
75+
message[section1_offset + 18] = 60;
76+
77+
let err = expect_err(message);
78+
assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
79+
assert!(err.to_string().contains("invalid reference timestamp"));
80+
}
81+
82+
#[test]
83+
fn rejects_invalid_grib1_reference_timestamp() {
84+
let mut message = build_grib1_message(&[1, 2, 3, 4]);
85+
let pds_offset = 8;
86+
message[pds_offset + 13] = 2;
87+
message[pds_offset + 14] = 29;
88+
89+
let err = expect_err(message);
90+
assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
91+
assert!(err.to_string().contains("invalid reference timestamp"));
92+
}
93+
7194
#[test]
7295
fn strict_open_reports_validly_framed_unsupported_edition() {
7396
let err = expect_err(build_valid_unsupported_edition_message(3));

0 commit comments

Comments
 (0)