1use crate::error::{Error, Result};
4use crate::parameter;
5use crate::util::{grib_i32, grib_i8};
6
7#[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#[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#[derive(Debug, Clone, PartialEq)]
75pub struct ProductDefinition {
76 pub parameter_category: u8,
77 pub parameter_number: u8,
78 pub template: ProductDefinitionTemplate,
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub enum ProductDefinitionTemplate {
84 AnalysisOrForecast(AnalysisOrForecastTemplate),
85}
86
87#[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(§ion_bytes[22..28]),
202 second_surface: parse_surface(§ion_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(§ion_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(§ion).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(§ion).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(§ion).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(§ion).unwrap_err();
322 assert!(matches!(err, Error::InvalidSection { section: 4, .. }));
323 }
324}