Skip to main content

grib_core/
grib1.rs

1//! GRIB Edition 1 shared metadata and section models.
2
3use crate::binary::{decode_ibm_f32, decode_wmo_i16, decode_wmo_i24};
4use crate::bit::read_bit;
5use crate::data::{
6    DataRepresentation, Grib1SecondOrderLayout, Grib1SecondOrderPackingParams, ScaledPackingParams,
7};
8use crate::error::{Error, Result};
9use crate::grid::{GridDefinition, LatLonGrid, MISSING_INCREMENT};
10use crate::metadata::{Parameter, ReferenceTime};
11use crate::parameter;
12
13/// GRIB1 product definition metadata.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ProductDefinition {
16    pub table_version: u8,
17    pub center_id: u8,
18    pub generating_process_id: u8,
19    pub grid_id: u8,
20    pub has_grid_definition: bool,
21    pub has_bitmap: bool,
22    pub parameter_number: u8,
23    pub level_type: u8,
24    pub level_value: u16,
25    pub reference_time: ReferenceTime,
26    pub forecast_time_unit: u8,
27    pub p1: u8,
28    pub p2: u8,
29    pub time_range_indicator: u8,
30    pub average_count: u16,
31    pub missing_count: u8,
32    pub century: u8,
33    pub subcenter_id: u8,
34    pub decimal_scale: i16,
35}
36
37impl ProductDefinition {
38    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
39        if section_bytes.len() < 28 {
40            return Err(Error::InvalidSection {
41                section: 1,
42                reason: format!("expected at least 28 bytes, got {}", section_bytes.len()),
43            });
44        }
45
46        Ok(Self {
47            table_version: section_bytes[3],
48            center_id: section_bytes[4],
49            generating_process_id: section_bytes[5],
50            grid_id: section_bytes[6],
51            has_grid_definition: section_bytes[7] & 0b1000_0000 != 0,
52            has_bitmap: section_bytes[7] & 0b0100_0000 != 0,
53            parameter_number: section_bytes[8],
54            level_type: section_bytes[9],
55            level_value: u16::from_be_bytes(section_bytes[10..12].try_into().unwrap()),
56            reference_time: parse_reference_time(section_bytes)?,
57            forecast_time_unit: section_bytes[17],
58            p1: section_bytes[18],
59            p2: section_bytes[19],
60            time_range_indicator: section_bytes[20],
61            average_count: u16::from_be_bytes(section_bytes[21..23].try_into().unwrap()),
62            missing_count: section_bytes[23],
63            century: section_bytes[24],
64            subcenter_id: section_bytes[25],
65            decimal_scale: decode_wmo_i16(&section_bytes[26..28]).unwrap(),
66        })
67    }
68
69    pub fn parameter(&self) -> Parameter {
70        let short_name = parameter::grib1_parameter_name(self.table_version, self.parameter_number);
71        let description =
72            parameter::grib1_parameter_description(self.table_version, self.parameter_number);
73        Parameter::new_grib1(
74            self.table_version,
75            self.parameter_number,
76            short_name,
77            description,
78        )
79    }
80
81    pub fn forecast_time(&self) -> Option<u32> {
82        match self.time_range_indicator {
83            0 | 1 => Some(u32::from(self.p1)),
84            10 => Some(u32::from(u16::from_be_bytes([self.p1, self.p2]))),
85            _ => None,
86        }
87    }
88
89    /// Offset from the reference time at which the product is valid, in
90    /// `forecast_time_unit` units.
91    ///
92    /// Ranges, averages, accumulations, and differences (time range
93    /// indicators 2 through 5) are valid at the end of their period, `P2`.
94    pub fn valid_time_offset(&self) -> Option<u32> {
95        match self.time_range_indicator {
96            2..=5 => Some(u32::from(self.p2)),
97            _ => self.forecast_time(),
98        }
99    }
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub struct GridDescription {
104    pub nv: u8,
105    pub pv_or_pl: u8,
106    pub data_representation_type: u8,
107    pub grid: GridDefinition,
108}
109
110impl GridDescription {
111    pub fn parse(section_bytes: &[u8]) -> Result<Self> {
112        if section_bytes.len() < 32 {
113            return Err(Error::InvalidSection {
114                section: 2,
115                reason: format!("expected at least 32 bytes, got {}", section_bytes.len()),
116            });
117        }
118
119        let data_representation_type = section_bytes[5];
120        let grid = match data_representation_type {
121            0 => parse_latlon_grid(section_bytes)?,
122            other => GridDefinition::Unsupported(u16::from(other)),
123        };
124
125        Ok(Self {
126            nv: section_bytes[3],
127            pv_or_pl: section_bytes[4],
128            data_representation_type,
129            grid,
130        })
131    }
132}
133
134#[derive(Debug, Clone, PartialEq)]
135pub struct BinaryDataSection {
136    pub flags: u8,
137    pub unused_bits: u8,
138    pub binary_scale: i16,
139    pub reference_value: f32,
140    pub bits_per_value: u8,
141}
142
143impl BinaryDataSection {
144    pub fn parse(
145        section_bytes: &[u8],
146        decimal_scale: i16,
147        encoded_values: usize,
148    ) -> Result<(Self, DataRepresentation)> {
149        if section_bytes.len() < 11 {
150            return Err(Error::InvalidSection {
151                section: 4,
152                reason: format!("expected at least 11 bytes, got {}", section_bytes.len()),
153            });
154        }
155
156        let data_flag = section_bytes[3];
157        let flags = data_flag >> 4;
158        if flags & 0b1000 != 0 {
159            return Err(Error::UnsupportedDataTemplate(1004));
160        }
161
162        let binary_scale = decode_wmo_i16(&section_bytes[4..6]).unwrap();
163        let reference_value = decode_ibm_f32(section_bytes[6..10].try_into().unwrap());
164        let bits_per_value = section_bytes[10];
165        if bits_per_value > 64 {
166            return Err(Error::UnsupportedPackingWidth(bits_per_value));
167        }
168        let packing = ScaledPackingParams {
169            encoded_values,
170            reference_value,
171            binary_scale,
172            decimal_scale,
173            bits_per_value,
174            original_field_type: u8::from(flags & 0b0010 != 0),
175        };
176        let representation = if flags & 0b0100 != 0 {
177            parse_second_order_packing(section_bytes, packing, data_flag & 0x0f)?
178        } else {
179            if flags & 0b0001 != 0 {
180                return Err(Error::UnsupportedDataTemplate(1007));
181            }
182            DataRepresentation::SimplePacking(packing)
183        };
184
185        Ok((
186            Self {
187                flags,
188                unused_bits: data_flag & 0x0f,
189                binary_scale,
190                reference_value,
191                bits_per_value,
192            },
193            representation,
194        ))
195    }
196}
197
198fn parse_second_order_packing(
199    section: &[u8],
200    packing: ScaledPackingParams,
201    unused_bits: u8,
202) -> Result<DataRepresentation> {
203    if section.len() < 22 {
204        return Err(invalid_second_order(format!(
205            "requires at least 22 bytes, got {}",
206            section.len()
207        )));
208    }
209
210    let n1 = usize::from(u16::from_be_bytes(section[11..13].try_into().unwrap()));
211    let extended_flags = section[13];
212    let n2 = usize::from(u16::from_be_bytes(section[14..16].try_into().unwrap()));
213    let coded_group_count = usize::from(u16::from_be_bytes(section[16..18].try_into().unwrap()));
214    let encoded_values = usize::from(u16::from_be_bytes(section[18..20].try_into().unwrap()));
215    if section[20] != 0 {
216        return Err(invalid_second_order(format!(
217            "reserved octet 21 must be zero, got {}",
218            section[20]
219        )));
220    }
221    let group_count = coded_group_count;
222
223    if encoded_values != packing.encoded_values {
224        return Err(Error::DataLengthMismatch {
225            expected: packing.encoded_values,
226            actual: encoded_values,
227        });
228    }
229    if extended_flags & 0x80 != 0 {
230        return Err(invalid_second_order("reserved extended flag bit 1 is set"));
231    }
232    if extended_flags & 0x40 != 0 {
233        return Err(invalid_second_order(
234            "matrix-valued grid points are not supported",
235        ));
236    }
237    if extended_flags & 0x0f != 0 {
238        return Err(invalid_second_order(format!(
239            "non-WMO extended ordering or spatial differencing flags are set: 0x{:02x}",
240            extended_flags & 0x0f
241        )));
242    }
243
244    let has_secondary_bitmap = extended_flags & 0x20 != 0;
245    let has_variable_widths = extended_flags & 0x10 != 0;
246    let layout = match (has_secondary_bitmap, has_variable_widths) {
247        (false, true) => Grib1SecondOrderLayout::RowByRow,
248        (true, false) => Grib1SecondOrderLayout::ConstantWidth,
249        (true, true) => Grib1SecondOrderLayout::VariableWidth,
250        (false, false) => {
251            return Err(invalid_second_order(
252                "constant-width groups require a secondary bitmap",
253            ));
254        }
255    };
256
257    if encoded_values > 0 && group_count == 0 {
258        return Err(invalid_second_order(
259            "non-empty fields require at least one group",
260        ));
261    }
262
263    let width_count = match layout {
264        Grib1SecondOrderLayout::ConstantWidth => 1,
265        Grib1SecondOrderLayout::RowByRow | Grib1SecondOrderLayout::VariableWidth => group_count,
266    };
267    let descriptors_end = 21_usize
268        .checked_add(width_count)
269        .ok_or(Error::ArithmeticOverflow {
270            operation: "computing GRIB1 second-order width descriptor boundary",
271        })?;
272    let secondary_bitmap_bytes = if has_secondary_bitmap {
273        encoded_values.div_ceil(8)
274    } else {
275        0
276    };
277    let expected_first_order_index =
278        descriptors_end
279            .checked_add(secondary_bitmap_bytes)
280            .ok_or(Error::ArithmeticOverflow {
281                operation: "computing GRIB1 first-order value offset",
282            })?;
283    let first_order_index = n1
284        .checked_sub(1)
285        .ok_or_else(|| invalid_second_order("N1 must be a one-based octet position"))?;
286    if first_order_index != expected_first_order_index {
287        return Err(invalid_second_order(format!(
288            "N1 points to octet {n1}, expected {}",
289            expected_first_order_index + 1
290        )));
291    }
292    if expected_first_order_index > section.len() {
293        return Err(invalid_second_order(format!(
294            "width descriptors and secondary bitmap require {expected_first_order_index} bytes, got {}",
295            section.len()
296        )));
297    }
298
299    let width_bytes = &section[21..descriptors_end];
300    if width_bytes.iter().any(|width| *width > 64) {
301        let width = width_bytes
302            .iter()
303            .copied()
304            .find(|width| *width > 64)
305            .expect("out-of-range width exists");
306        return Err(Error::UnsupportedPackingWidth(width));
307    }
308    let mut group_widths = Vec::new();
309    group_widths
310        .try_reserve_exact(width_count)
311        .map_err(|error| Error::allocation("GRIB1 group widths", width_count, error))?;
312    group_widths.extend_from_slice(width_bytes);
313
314    let group_lengths = if has_secondary_bitmap {
315        parse_secondary_group_lengths(
316            &section[descriptors_end..expected_first_order_index],
317            encoded_values,
318            group_count,
319        )?
320    } else {
321        Vec::new()
322    };
323
324    let first_order_bits = group_count
325        .checked_mul(usize::from(packing.bits_per_value))
326        .ok_or(Error::BitOffsetOverflow)?;
327    let expected_second_order_index = expected_first_order_index
328        .checked_add(first_order_bits.div_ceil(8))
329        .ok_or(Error::ArithmeticOverflow {
330            operation: "computing GRIB1 second-order value offset",
331        })?;
332    let second_order_index = n2
333        .checked_sub(1)
334        .ok_or_else(|| invalid_second_order("N2 must be a one-based octet position"))?;
335    if second_order_index != expected_second_order_index {
336        return Err(invalid_second_order(format!(
337            "N2 points to octet {n2}, expected {}",
338            expected_second_order_index + 1
339        )));
340    }
341    if second_order_index > section.len() {
342        return Err(invalid_second_order(format!(
343            "first-order values require {second_order_index} bytes, got {}",
344            section.len()
345        )));
346    }
347
348    let first_order_values_offset = first_order_index - 11;
349    let second_order_values_offset = second_order_index - 11;
350    let payload_bits = section[11..]
351        .len()
352        .checked_mul(8)
353        .ok_or(Error::BitOffsetOverflow)?;
354    let usable_bits = payload_bits
355        .checked_sub(usize::from(unused_bits))
356        .ok_or_else(|| invalid_second_order("unused-bit count exceeds the BDS payload"))?;
357    let second_order_start = second_order_values_offset
358        .checked_mul(8)
359        .ok_or(Error::BitOffsetOverflow)?;
360
361    if layout != Grib1SecondOrderLayout::RowByRow {
362        let second_order_bits =
363            group_lengths
364                .iter()
365                .enumerate()
366                .try_fold(0usize, |total, (group_index, length)| {
367                    let width = match layout {
368                        Grib1SecondOrderLayout::ConstantWidth => group_widths[0],
369                        Grib1SecondOrderLayout::VariableWidth => group_widths[group_index],
370                        Grib1SecondOrderLayout::RowByRow => unreachable!(),
371                    };
372                    length
373                        .checked_mul(usize::from(width))
374                        .and_then(|bits| total.checked_add(bits))
375                        .ok_or(Error::BitOffsetOverflow)
376                })?;
377        let expected_end = second_order_start
378            .checked_add(second_order_bits)
379            .ok_or(Error::BitOffsetOverflow)?;
380        if expected_end != usable_bits {
381            return Err(invalid_second_order(format!(
382                "packed values end at bit {expected_end}, but the usable BDS payload ends at bit {usable_bits}"
383            )));
384        }
385    } else if second_order_start > usable_bits {
386        return Err(invalid_second_order(
387            "N2 points beyond the usable BDS payload",
388        ));
389    }
390
391    Ok(DataRepresentation::Grib1SecondOrderPacking(
392        Grib1SecondOrderPackingParams {
393            packing,
394            layout,
395            group_count,
396            group_widths,
397            group_lengths,
398            first_order_values_offset,
399            second_order_values_offset,
400            unused_bits,
401        },
402    ))
403}
404
405fn parse_secondary_group_lengths(
406    bitmap: &[u8],
407    encoded_values: usize,
408    group_count: usize,
409) -> Result<Vec<usize>> {
410    if encoded_values == 0 {
411        if group_count == 0 {
412            return Ok(Vec::new());
413        }
414        return Err(invalid_second_order(format!(
415            "empty secondary bitmap cannot describe {group_count} groups"
416        )));
417    }
418    if !read_bit(bitmap, 0)? {
419        return Err(invalid_second_order(
420            "secondary bitmap must start with a group boundary",
421        ));
422    }
423
424    let mut lengths = Vec::new();
425    lengths
426        .try_reserve_exact(group_count)
427        .map_err(|error| Error::allocation("GRIB1 group lengths", group_count, error))?;
428    let mut group_start = 0usize;
429    for bit_index in 1..encoded_values {
430        if read_bit(bitmap, bit_index)? {
431            lengths.push(bit_index - group_start);
432            group_start = bit_index;
433        }
434    }
435    lengths.push(encoded_values - group_start);
436    if lengths.len() != group_count {
437        return Err(invalid_second_order(format!(
438            "secondary bitmap defines {} groups, but P1 declares {group_count}",
439            lengths.len()
440        )));
441    }
442    Ok(lengths)
443}
444
445fn invalid_second_order(reason: impl Into<String>) -> Error {
446    Error::InvalidSection {
447        section: 4,
448        reason: format!("invalid GRIB1 second-order packing: {}", reason.into()),
449    }
450}
451
452fn parse_reference_time(section_bytes: &[u8]) -> Result<ReferenceTime> {
453    let century = section_bytes[24];
454    let year_of_century = u16::from(section_bytes[12]);
455    let year = match century {
456        0 => year_of_century,
457        c => (u16::from(c) - 1) * 100 + year_of_century,
458    };
459
460    let reference_time = ReferenceTime {
461        year,
462        month: section_bytes[13],
463        day: section_bytes[14],
464        hour: section_bytes[15],
465        minute: section_bytes[16],
466        second: 0,
467    };
468    reference_time.validate_in_section(1)?;
469    Ok(reference_time)
470}
471
472fn parse_latlon_grid(section_bytes: &[u8]) -> Result<GridDefinition> {
473    let ni = u32::from(u16::from_be_bytes(section_bytes[6..8].try_into().unwrap()));
474    let nj = u32::from(u16::from_be_bytes(section_bytes[8..10].try_into().unwrap()));
475    let lat_first = parse_coordinate(&section_bytes[10..13], 90_000_000, "first latitude")?;
476    let lon_first = parse_coordinate(&section_bytes[13..16], 360_000_000, "first longitude")?;
477    let lat_last = parse_coordinate(&section_bytes[17..20], 90_000_000, "last latitude")?;
478    let lon_last = parse_coordinate(&section_bytes[20..23], 360_000_000, "last longitude")?;
479    let increments_given = section_bytes[16] & 0x80 != 0;
480    let di = parse_increment(&section_bytes[23..25], increments_given);
481    let dj = parse_increment(&section_bytes[25..27], increments_given);
482    let scanning_mode = section_bytes[27];
483
484    Ok(GridDefinition::LatLon(LatLonGrid {
485        ni,
486        nj,
487        lat_first,
488        lon_first,
489        lat_last,
490        lon_last,
491        di,
492        dj,
493        scanning_mode,
494    }))
495}
496
497/// Converts a GRIB1 millidegree increment to microdegrees, mapping the
498/// all-ones pattern and increments flagged as not given to
499/// [`MISSING_INCREMENT`].
500fn parse_increment(bytes: &[u8], given: bool) -> u32 {
501    let raw = u16::from_be_bytes(bytes.try_into().unwrap());
502    if !given || raw == u16::MAX {
503        MISSING_INCREMENT
504    } else {
505        u32::from(raw) * 1_000
506    }
507}
508
509fn parse_coordinate(bytes: &[u8], absolute_limit: i64, name: &str) -> Result<i32> {
510    let millidegrees = decode_wmo_i24(bytes).ok_or_else(|| Error::InvalidSection {
511        section: 2,
512        reason: format!("truncated {name}"),
513    })?;
514    let microdegrees = i64::from(millidegrees) * 1_000;
515    if microdegrees.abs() > absolute_limit {
516        return Err(Error::InvalidSection {
517            section: 2,
518            reason: format!(
519                "{name} {microdegrees} microdegrees is outside the valid range -{absolute_limit}..={absolute_limit}"
520            ),
521        });
522    }
523
524    i32::try_from(microdegrees).map_err(|_| Error::InvalidSection {
525        section: 2,
526        reason: format!("{name} does not fit the internal microdegree representation"),
527    })
528}
529
530#[cfg(test)]
531mod tests {
532    use super::{BinaryDataSection, GridDescription, ProductDefinition};
533    use crate::data::{DataRepresentation, Grib1SecondOrderLayout};
534    use crate::error::Error;
535    use crate::grid::{GridDefinition, MISSING_INCREMENT};
536    use crate::metadata::ReferenceTime;
537
538    #[test]
539    fn decodes_indicator_ten_forecast_time_as_u16() {
540        let product = ProductDefinition {
541            table_version: 2,
542            center_id: 7,
543            generating_process_id: 255,
544            grid_id: 0,
545            has_grid_definition: true,
546            has_bitmap: false,
547            parameter_number: 11,
548            level_type: 100,
549            level_value: 850,
550            reference_time: ReferenceTime {
551                year: 2026,
552                month: 3,
553                day: 20,
554                hour: 12,
555                minute: 0,
556                second: 0,
557            },
558            forecast_time_unit: 1,
559            p1: 0x01,
560            p2: 0x2c,
561            time_range_indicator: 10,
562            average_count: 0,
563            missing_count: 0,
564            century: 21,
565            subcenter_id: 0,
566            decimal_scale: 0,
567        };
568
569        assert_eq!(product.forecast_time(), Some(300));
570    }
571
572    #[test]
573    fn rejects_invalid_product_definition_reference_time() {
574        let mut section = valid_product_definition_section();
575        section[13] = 2;
576        section[14] = 29;
577        let err = ProductDefinition::parse(&section).unwrap_err();
578        assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
579        assert!(err.to_string().contains("invalid reference timestamp"));
580
581        let mut section = valid_product_definition_section();
582        section[15] = 24;
583        let err = ProductDefinition::parse(&section).unwrap_err();
584        assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
585
586        let mut section = valid_product_definition_section();
587        section[16] = 60;
588        let err = ProductDefinition::parse(&section).unwrap_err();
589        assert!(matches!(err, Error::InvalidSection { section: 1, .. }));
590    }
591
592    #[test]
593    fn rejects_grib1_coordinates_outside_geographic_bounds_without_overflowing() {
594        let mut section = valid_grid_description_section();
595        section[10..13].copy_from_slice(&[0x7f, 0xff, 0xff]);
596
597        let err = GridDescription::parse(&section).unwrap_err();
598        assert!(matches!(err, Error::InvalidSection { section: 2, .. }));
599        assert!(err.to_string().contains("first latitude"));
600
601        let mut section = valid_grid_description_section();
602        section[13..16].copy_from_slice(&[0xff, 0xff, 0xff]);
603
604        let err = GridDescription::parse(&section).unwrap_err();
605        assert!(matches!(err, Error::InvalidSection { section: 2, .. }));
606        assert!(err.to_string().contains("first longitude"));
607    }
608
609    #[test]
610    fn maps_grib1_increments_not_given_to_missing_and_infers_axes() {
611        let grid_of = |section: &[u8]| match GridDescription::parse(section).unwrap().grid {
612            GridDefinition::LatLon(grid) => grid,
613            other => panic!("expected a latitude/longitude grid, got {other:?}"),
614        };
615
616        let mut section = valid_grid_description_section();
617        section[16] = 0x80;
618        let given = grid_of(&section);
619        assert_eq!((given.di, given.dj), (1_000_000, 1_000_000));
620
621        section[23..25].copy_from_slice(&u16::MAX.to_be_bytes());
622        assert_eq!(grid_of(&section).di, MISSING_INCREMENT);
623
624        let not_given = grid_of(&valid_grid_description_section());
625        assert_eq!(
626            (not_given.di, not_given.dj),
627            (MISSING_INCREMENT, MISSING_INCREMENT)
628        );
629        assert_eq!(not_given.longitudes().unwrap(), vec![0.0, 1.0]);
630        assert_eq!(not_given.latitudes().unwrap(), vec![0.0, -1.0]);
631    }
632
633    #[test]
634    fn parses_row_by_row_second_order_packing() {
635        let section = second_order_section(Grib1SecondOrderLayout::RowByRow);
636        let (_, representation) = BinaryDataSection::parse(&section, 0, 6).unwrap();
637        let DataRepresentation::Grib1SecondOrderPacking(params) = representation else {
638            panic!("expected GRIB1 second-order packing");
639        };
640
641        assert_eq!(params.layout(), Grib1SecondOrderLayout::RowByRow);
642        assert_eq!(params.group_count(), 2);
643        assert_eq!(params.group_widths(), &[2, 2]);
644        assert!(params.group_lengths().is_empty());
645        assert_eq!(params.first_order_values_offset(), 12);
646        assert_eq!(params.second_order_values_offset(), 14);
647    }
648
649    #[test]
650    fn parses_secondary_bitmap_second_order_layouts() {
651        for layout in [
652            Grib1SecondOrderLayout::ConstantWidth,
653            Grib1SecondOrderLayout::VariableWidth,
654        ] {
655            let section = second_order_section(layout);
656            let (_, representation) = BinaryDataSection::parse(&section, 0, 6).unwrap();
657            let DataRepresentation::Grib1SecondOrderPacking(params) = representation else {
658                panic!("expected GRIB1 second-order packing");
659            };
660
661            assert_eq!(params.layout(), layout);
662            assert_eq!(params.group_count(), 2);
663            assert_eq!(params.group_lengths(), &[3, 3]);
664            if layout == Grib1SecondOrderLayout::ConstantWidth {
665                assert_eq!(params.group_widths(), &[2]);
666            } else {
667                assert_eq!(params.group_widths(), &[2, 2]);
668            }
669        }
670    }
671
672    #[test]
673    fn rejects_second_order_secondary_bitmap_without_initial_boundary() {
674        let mut section = second_order_section(Grib1SecondOrderLayout::VariableWidth);
675        section[23] = 0b0001_0000;
676        let error = BinaryDataSection::parse(&section, 0, 6).unwrap_err();
677        assert!(matches!(error, Error::InvalidSection { section: 4, .. }));
678        assert!(error
679            .to_string()
680            .contains("must start with a group boundary"));
681    }
682
683    #[test]
684    fn rejects_nonstandard_second_order_extensions_and_inconsistent_offsets() {
685        let mut section = second_order_section(Grib1SecondOrderLayout::RowByRow);
686        section[20] = 1;
687        let error = BinaryDataSection::parse(&section, 0, 6).unwrap_err();
688        assert!(error.to_string().contains("reserved octet 21"));
689
690        let mut section = second_order_section(Grib1SecondOrderLayout::RowByRow);
691        section[13] |= 0x04;
692        let error = BinaryDataSection::parse(&section, 0, 6).unwrap_err();
693        assert!(error.to_string().contains("non-WMO extended ordering"));
694
695        let mut section = second_order_section(Grib1SecondOrderLayout::VariableWidth);
696        section[14..16].copy_from_slice(&28u16.to_be_bytes());
697        let error = BinaryDataSection::parse(&section, 0, 6).unwrap_err();
698        assert!(error.to_string().contains("N2 points to octet 28"));
699    }
700
701    #[test]
702    fn preserves_simple_integer_field_type() {
703        let mut section = vec![0u8; 12];
704        section[..3].copy_from_slice(&[0, 0, 12]);
705        section[3] = 0x20;
706        section[10] = 8;
707        let (_, representation) = BinaryDataSection::parse(&section, 0, 1).unwrap();
708        let DataRepresentation::SimplePacking(params) = representation else {
709            panic!("expected simple packing");
710        };
711        assert_eq!(params.original_field_type, 1);
712    }
713
714    fn valid_product_definition_section() -> Vec<u8> {
715        let mut section = vec![0u8; 28];
716        section[..3].copy_from_slice(&[0, 0, 28]);
717        section[3] = 2;
718        section[4] = 7;
719        section[5] = 255;
720        section[6] = 0;
721        section[7] = 0b1000_0000;
722        section[8] = 11;
723        section[9] = 100;
724        section[10..12].copy_from_slice(&850u16.to_be_bytes());
725        section[12] = 26;
726        section[13] = 3;
727        section[14] = 20;
728        section[15] = 12;
729        section[16] = 0;
730        section[17] = 1;
731        section[18] = 0;
732        section[19] = 0;
733        section[20] = 0;
734        section[24] = 21;
735        section[25] = 0;
736        section
737    }
738
739    fn valid_grid_description_section() -> Vec<u8> {
740        let mut section = vec![0u8; 32];
741        section[..3].copy_from_slice(&[0, 0, 32]);
742        section[5] = 0;
743        section[6..8].copy_from_slice(&2u16.to_be_bytes());
744        section[8..10].copy_from_slice(&2u16.to_be_bytes());
745        section[10..13].copy_from_slice(&[0, 0, 0]);
746        section[13..16].copy_from_slice(&[0, 0, 0]);
747        section[17..20].copy_from_slice(&[0, 3, 0xe8]);
748        section[20..23].copy_from_slice(&[0, 3, 0xe8]);
749        section[23..25].copy_from_slice(&1_000u16.to_be_bytes());
750        section[25..27].copy_from_slice(&1_000u16.to_be_bytes());
751        section
752    }
753
754    fn second_order_section(layout: Grib1SecondOrderLayout) -> Vec<u8> {
755        let (length, extended_flags, n1, n2, widths, bitmap_offset) = match layout {
756            Grib1SecondOrderLayout::RowByRow => (27usize, 0x10, 24u16, 26u16, &[2, 2][..], None),
757            Grib1SecondOrderLayout::ConstantWidth => (27, 0x20, 24, 26, &[2][..], Some(22usize)),
758            Grib1SecondOrderLayout::VariableWidth => (28, 0x30, 25, 27, &[2, 2][..], Some(23usize)),
759        };
760        let mut section = vec![0u8; length];
761        section[..3].copy_from_slice(&[
762            ((length >> 16) & 0xff) as u8,
763            ((length >> 8) & 0xff) as u8,
764            (length & 0xff) as u8,
765        ]);
766        section[3] = 0x44;
767        section[10] = 5;
768        section[11..13].copy_from_slice(&n1.to_be_bytes());
769        section[13] = extended_flags;
770        section[14..16].copy_from_slice(&n2.to_be_bytes());
771        section[16..18].copy_from_slice(&2u16.to_be_bytes());
772        section[18..20].copy_from_slice(&6u16.to_be_bytes());
773        section[21..21 + widths.len()].copy_from_slice(widths);
774        if let Some(offset) = bitmap_offset {
775            section[offset] = 0b1001_0000;
776        }
777        let first_order_index = usize::from(n1) - 1;
778        section[first_order_index..first_order_index + 2].copy_from_slice(&[0x55, 0x00]);
779        let second_order_index = usize::from(n2) - 1;
780        section[second_order_index..second_order_index + 2].copy_from_slice(&[0x18, 0xb0]);
781        section
782    }
783}