Skip to main content

grib_core/
product.rs

1//! GRIB2 metadata carried by Sections 1 and 4.
2
3use crate::error::{Error, Result};
4use crate::parameter;
5use crate::util::{grib_i32, grib_i8};
6
7/// Section 1: Identification Section.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Identification {
10    pub center_id: u16,
11    pub subcenter_id: u16,
12    pub master_table_version: u8,
13    pub local_table_version: u8,
14    pub significance_of_reference_time: u8,
15    pub reference_year: u16,
16    pub reference_month: u8,
17    pub reference_day: u8,
18    pub reference_hour: u8,
19    pub reference_minute: u8,
20    pub reference_second: u8,
21    pub production_status: u8,
22    pub processed_data_type: u8,
23}
24
25impl Identification {
26    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
27        if section_bytes.len() < 21 {
28            return Err(Error::InvalidSection {
29                section: 1,
30                reason: format!("expected at least 21 bytes, got {}", section_bytes.len()),
31            });
32        }
33        if section_bytes[4] != 1 {
34            return Err(Error::InvalidSection {
35                section: section_bytes[4],
36                reason: "not an identification section".into(),
37            });
38        }
39
40        Ok(Self {
41            center_id: u16::from_be_bytes(section_bytes[5..7].try_into().unwrap()),
42            subcenter_id: u16::from_be_bytes(section_bytes[7..9].try_into().unwrap()),
43            master_table_version: section_bytes[9],
44            local_table_version: section_bytes[10],
45            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],
52            production_status: section_bytes[19],
53            processed_data_type: section_bytes[20],
54        })
55    }
56}
57
58/// A fixed surface from Product Definition templates.
59#[derive(Debug, Clone, PartialEq)]
60pub struct FixedSurface {
61    pub surface_type: u8,
62    pub scale_factor: i16,
63    pub scaled_value: i32,
64}
65
66impl FixedSurface {
67    pub fn scaled_value_f64(&self) -> f64 {
68        let factor = 10.0_f64.powi(-(self.scale_factor as i32));
69        self.scaled_value as f64 * factor
70    }
71}
72
73/// Section 4: Product Definition Section.
74#[derive(Debug, Clone, PartialEq)]
75pub struct ProductDefinition {
76    pub parameter_category: u8,
77    pub parameter_number: u8,
78    pub template: ProductDefinitionTemplate,
79}
80
81/// Typed GRIB2 Product Definition templates.
82#[derive(Debug, Clone, PartialEq)]
83pub enum ProductDefinitionTemplate {
84    AnalysisOrForecast(AnalysisOrForecastTemplate),
85}
86
87/// Product Definition Template 4.0: analysis or forecast at a horizontal level.
88#[derive(Debug, Clone, PartialEq)]
89pub struct AnalysisOrForecastTemplate {
90    pub generating_process: u8,
91    pub forecast_time_unit: u8,
92    pub forecast_time: u32,
93    pub first_surface: Option<FixedSurface>,
94    pub second_surface: Option<FixedSurface>,
95}
96
97impl ProductDefinition {
98    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
99        if section_bytes.len() < 11 {
100            return Err(Error::InvalidSection {
101                section: 4,
102                reason: format!("expected at least 11 bytes, got {}", section_bytes.len()),
103            });
104        }
105        if section_bytes[4] != 4 {
106            return Err(Error::InvalidSection {
107                section: section_bytes[4],
108                reason: "not a product definition section".into(),
109            });
110        }
111
112        let template = u16::from_be_bytes(section_bytes[7..9].try_into().unwrap());
113        let parameter_category = section_bytes[9];
114        let parameter_number = section_bytes[10];
115
116        Ok(Self {
117            parameter_category,
118            parameter_number,
119            template: ProductDefinitionTemplate::parse(template, section_bytes)?,
120        })
121    }
122
123    pub fn parameter_name(&self, discipline: u8) -> &'static str {
124        parameter::parameter_name(discipline, self.parameter_category, self.parameter_number)
125    }
126
127    pub fn parameter_description(&self, discipline: u8) -> &'static str {
128        parameter::parameter_description(discipline, self.parameter_category, self.parameter_number)
129    }
130
131    pub fn template_number(&self) -> u16 {
132        self.template.number()
133    }
134
135    pub fn generating_process(&self) -> Option<u8> {
136        match &self.template {
137            ProductDefinitionTemplate::AnalysisOrForecast(template) => {
138                Some(template.generating_process)
139            }
140        }
141    }
142
143    pub fn forecast_time_unit(&self) -> Option<u8> {
144        match &self.template {
145            ProductDefinitionTemplate::AnalysisOrForecast(template) => {
146                Some(template.forecast_time_unit)
147            }
148        }
149    }
150
151    pub fn forecast_time(&self) -> Option<u32> {
152        match &self.template {
153            ProductDefinitionTemplate::AnalysisOrForecast(template) => Some(template.forecast_time),
154        }
155    }
156
157    pub fn first_surface(&self) -> Option<&FixedSurface> {
158        match &self.template {
159            ProductDefinitionTemplate::AnalysisOrForecast(template) => {
160                template.first_surface.as_ref()
161            }
162        }
163    }
164
165    pub fn second_surface(&self) -> Option<&FixedSurface> {
166        match &self.template {
167            ProductDefinitionTemplate::AnalysisOrForecast(template) => {
168                template.second_surface.as_ref()
169            }
170        }
171    }
172}
173
174impl ProductDefinitionTemplate {
175    pub fn parse(template: u16, section_bytes: &[u8]) -> Result<Self> {
176        match template {
177            0 => Ok(Self::AnalysisOrForecast(AnalysisOrForecastTemplate::parse(
178                section_bytes,
179            )?)),
180            other => Err(Error::UnsupportedProductTemplate(other)),
181        }
182    }
183
184    pub const fn number(&self) -> u16 {
185        match self {
186            Self::AnalysisOrForecast(_) => 0,
187        }
188    }
189}
190
191impl AnalysisOrForecastTemplate {
192    const MINIMUM_LENGTH: usize = 34;
193
194    fn parse(section_bytes: &[u8]) -> Result<Self> {
195        require_len(section_bytes, Self::MINIMUM_LENGTH, "template 4.0")?;
196
197        Ok(Self {
198            generating_process: section_bytes[11],
199            forecast_time_unit: section_bytes[17],
200            forecast_time: u32::from_be_bytes(section_bytes[18..22].try_into().unwrap()),
201            first_surface: parse_surface(&section_bytes[22..28]),
202            second_surface: parse_surface(&section_bytes[28..34]),
203        })
204    }
205}
206
207fn require_len(section_bytes: &[u8], min_len: usize, context: &str) -> Result<()> {
208    if section_bytes.len() < min_len {
209        return Err(Error::InvalidSection {
210            section: 4,
211            reason: format!(
212                "{context} requires at least {min_len} bytes, got {}",
213                section_bytes.len()
214            ),
215        });
216    }
217    Ok(())
218}
219
220fn parse_surface(section_bytes: &[u8]) -> Option<FixedSurface> {
221    let surface_type = section_bytes[0];
222    if surface_type == 255 {
223        return None;
224    }
225
226    Some(FixedSurface {
227        surface_type,
228        scale_factor: grib_i8(section_bytes[1]),
229        scaled_value: grib_i32(&section_bytes[2..6])?,
230    })
231}
232
233#[cfg(test)]
234mod tests {
235    use super::{
236        AnalysisOrForecastTemplate, Identification, ProductDefinition, ProductDefinitionTemplate,
237    };
238    use crate::error::Error;
239
240    #[test]
241    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;
258
259        let id = Identification::parse(&section).unwrap();
260        assert_eq!(id.center_id, 7);
261        assert_eq!(id.reference_year, 2026);
262        assert_eq!(id.reference_hour, 12);
263    }
264
265    #[test]
266    fn parses_product_definition_template_zero_fields() {
267        let mut section = vec![0u8; 34];
268        section[..4].copy_from_slice(&(34u32).to_be_bytes());
269        section[4] = 4;
270        section[7..9].copy_from_slice(&0u16.to_be_bytes());
271        section[9] = 2;
272        section[10] = 3;
273        section[11] = 2;
274        section[17] = 1;
275        section[18..22].copy_from_slice(&6u32.to_be_bytes());
276        section[22] = 103;
277        section[23] = 0;
278        section[24..28].copy_from_slice(&850u32.to_be_bytes());
279        section[28] = 255;
280
281        let product = ProductDefinition::parse(&section).unwrap();
282        assert_eq!(product.parameter_category, 2);
283        assert_eq!(product.parameter_number, 3);
284        assert_eq!(product.template_number(), 0);
285        assert_eq!(product.forecast_time(), Some(6));
286        assert_eq!(product.first_surface().unwrap().scaled_value_f64(), 850.0);
287        assert_eq!(
288            product.template,
289            ProductDefinitionTemplate::AnalysisOrForecast(AnalysisOrForecastTemplate {
290                generating_process: 2,
291                forecast_time_unit: 1,
292                forecast_time: 6,
293                first_surface: product.first_surface().cloned(),
294                second_surface: None,
295            })
296        );
297    }
298
299    #[test]
300    fn rejects_unsupported_product_definition_templates() {
301        let mut section = vec![0u8; 34];
302        section[..4].copy_from_slice(&(34u32).to_be_bytes());
303        section[4] = 4;
304        section[7..9].copy_from_slice(&8u16.to_be_bytes());
305        section[9] = 2;
306        section[10] = 3;
307
308        let err = ProductDefinition::parse(&section).unwrap_err();
309        assert!(matches!(err, Error::UnsupportedProductTemplate(8)));
310    }
311
312    #[test]
313    fn rejects_truncated_template_zero_sections() {
314        let mut section = vec![0u8; 33];
315        section[..4].copy_from_slice(&(33u32).to_be_bytes());
316        section[4] = 4;
317        section[7..9].copy_from_slice(&0u16.to_be_bytes());
318        section[9] = 2;
319        section[10] = 3;
320
321        let err = ProductDefinition::parse(&section).unwrap_err();
322        assert!(matches!(err, Error::InvalidSection { section: 4, .. }));
323    }
324}