Skip to main content

grib_writer/
lib.rs

1//! GRIB writer crate.
2
3#![forbid(unsafe_code)]
4
5use std::io::Write;
6
7use grib_core::binary::{
8    encode_ibm_f32, encode_wmo_i16, encode_wmo_i24, encode_wmo_i32, encode_wmo_i8, write_u16_be,
9    write_u24_be, write_u32_be, write_u64_be, write_u8_be, U24_MAX,
10};
11use grib_core::bit::BitWriter;
12use grib_core::{
13    DataRepresentation, FixedSurface, GridDefinition, Identification, LatLonGrid,
14    ProductDefinition, ProductDefinitionTemplate, SimplePackingParams,
15};
16
17pub use grib_core::grib1::ProductDefinition as Grib1ProductDefinition;
18pub use grib_core::{Error, Result};
19
20/// Field packing strategy.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum PackingStrategy {
23    /// Simple packing with binary scale 0 and automatic bit-width selection.
24    SimpleAuto { decimal_scale: i16 },
25}
26
27/// Order of values supplied to field builders.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum ValueOrder {
30    /// Logical row-major order matching `grib-reader` ndarray output.
31    #[default]
32    LogicalRowMajor,
33    /// Native GRIB scan order; skips the logical-to-scan reordering pass.
34    GribScanOrder,
35}
36
37/// Builder for a single GRIB1 message field.
38#[derive(Debug, Clone, Default)]
39pub struct Grib1FieldBuilder {
40    product: Option<Grib1ProductDefinition>,
41    grid: Option<GridDefinition>,
42    packing: Option<PackingStrategy>,
43    values: Option<Vec<f64>>,
44    bitmap: Option<Vec<bool>>,
45    value_order: ValueOrder,
46}
47
48impl Grib1FieldBuilder {
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    pub fn product(mut self, product: Grib1ProductDefinition) -> Self {
54        self.product = Some(product);
55        self
56    }
57
58    pub fn grid(mut self, grid: GridDefinition) -> Self {
59        self.grid = Some(grid);
60        self
61    }
62
63    pub fn packing(mut self, packing: PackingStrategy) -> Self {
64        self.packing = Some(packing);
65        self
66    }
67
68    pub fn values<T>(mut self, values: &[T]) -> Self
69    where
70        T: Copy + Into<f64>,
71    {
72        self.values = Some(values.iter().copied().map(Into::into).collect());
73        self
74    }
75
76    pub fn bitmap(mut self, bitmap: &[bool]) -> Self {
77        self.bitmap = Some(bitmap.to_vec());
78        self
79    }
80
81    pub fn value_order(mut self, value_order: ValueOrder) -> Self {
82        self.value_order = value_order;
83        self
84    }
85
86    pub fn build(self) -> Result<Grib1Field> {
87        let mut product = self
88            .product
89            .ok_or_else(|| Error::Other("missing GRIB1 product definition".into()))?;
90        let grid = self
91            .grid
92            .ok_or_else(|| Error::Other("missing GRIB1 grid definition".into()))?;
93        let packing = self
94            .packing
95            .ok_or_else(|| Error::Other("missing GRIB1 packing strategy".into()))?;
96        let mut values = self
97            .values
98            .ok_or_else(|| Error::Other("missing GRIB1 field values".into()))?;
99        let mut bitmap = self.bitmap;
100
101        validate_supported_grib1_grid(&grid)?;
102
103        let expected = checked_grid_point_count(&grid)?;
104        if values.len() != expected {
105            return Err(Error::DataLengthMismatch {
106                expected,
107                actual: values.len(),
108            });
109        }
110        if let Some(bitmap) = &bitmap {
111            if bitmap.len() != expected {
112                return Err(Error::DataLengthMismatch {
113                    expected,
114                    actual: bitmap.len(),
115                });
116            }
117        }
118
119        if self.value_order == ValueOrder::LogicalRowMajor {
120            reorder_field_to_grib_scan_order(&grid, &mut values, bitmap.as_deref_mut())?;
121        }
122
123        let packed = match packing {
124            PackingStrategy::SimpleAuto { decimal_scale } => {
125                product.decimal_scale = decimal_scale;
126                pack_simple_auto(&values, bitmap.as_deref(), decimal_scale)?
127            }
128        };
129        product.has_grid_definition = true;
130        product.has_bitmap = packed.bitmap_payload.is_some();
131
132        Ok(Grib1Field {
133            product,
134            grid,
135            packed,
136        })
137    }
138}
139
140/// A validated, packed GRIB1 field ready for message serialization.
141#[derive(Debug, Clone)]
142pub struct Grib1Field {
143    product: Grib1ProductDefinition,
144    grid: GridDefinition,
145    packed: PackedField,
146}
147
148impl Grib1Field {
149    pub fn product(&self) -> &Grib1ProductDefinition {
150        &self.product
151    }
152
153    pub fn grid(&self) -> &GridDefinition {
154        &self.grid
155    }
156
157    pub fn data_representation(&self) -> &DataRepresentation {
158        &self.packed.representation
159    }
160}
161
162/// Builder for a single GRIB2 field.
163#[derive(Debug, Clone, Default)]
164pub struct Grib2FieldBuilder {
165    discipline: u8,
166    identification: Option<Identification>,
167    grid: Option<GridDefinition>,
168    product: Option<ProductDefinition>,
169    packing: Option<PackingStrategy>,
170    values: Option<Vec<f64>>,
171    bitmap: Option<Vec<bool>>,
172    value_order: ValueOrder,
173}
174
175impl Grib2FieldBuilder {
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    pub fn discipline(mut self, discipline: u8) -> Self {
181        self.discipline = discipline;
182        self
183    }
184
185    pub fn identification(mut self, identification: Identification) -> Self {
186        self.identification = Some(identification);
187        self
188    }
189
190    pub fn grid(mut self, grid: GridDefinition) -> Self {
191        self.grid = Some(grid);
192        self
193    }
194
195    pub fn product(mut self, product: ProductDefinition) -> Self {
196        self.product = Some(product);
197        self
198    }
199
200    pub fn packing(mut self, packing: PackingStrategy) -> Self {
201        self.packing = Some(packing);
202        self
203    }
204
205    pub fn values<T>(mut self, values: &[T]) -> Self
206    where
207        T: Copy + Into<f64>,
208    {
209        self.values = Some(values.iter().copied().map(Into::into).collect());
210        self
211    }
212
213    pub fn bitmap(mut self, bitmap: &[bool]) -> Self {
214        self.bitmap = Some(bitmap.to_vec());
215        self
216    }
217
218    pub fn value_order(mut self, value_order: ValueOrder) -> Self {
219        self.value_order = value_order;
220        self
221    }
222
223    pub fn build(self) -> Result<Grib2Field> {
224        let identification = self
225            .identification
226            .ok_or_else(|| Error::Other("missing GRIB2 identification".into()))?;
227        let grid = self
228            .grid
229            .ok_or_else(|| Error::Other("missing GRIB2 grid definition".into()))?;
230        let product = self
231            .product
232            .ok_or_else(|| Error::Other("missing GRIB2 product definition".into()))?;
233        let packing = self
234            .packing
235            .ok_or_else(|| Error::Other("missing GRIB2 packing strategy".into()))?;
236        let mut values = self
237            .values
238            .ok_or_else(|| Error::Other("missing GRIB2 field values".into()))?;
239        let mut bitmap = self.bitmap;
240
241        validate_supported_grid(&grid)?;
242        validate_supported_product(&product)?;
243
244        let expected = checked_grid_point_count(&grid)?;
245        if values.len() != expected {
246            return Err(Error::DataLengthMismatch {
247                expected,
248                actual: values.len(),
249            });
250        }
251        if let Some(bitmap) = &bitmap {
252            if bitmap.len() != expected {
253                return Err(Error::DataLengthMismatch {
254                    expected,
255                    actual: bitmap.len(),
256                });
257            }
258        }
259
260        if self.value_order == ValueOrder::LogicalRowMajor {
261            reorder_field_to_grib_scan_order(&grid, &mut values, bitmap.as_deref_mut())?;
262        }
263
264        let packed = match packing {
265            PackingStrategy::SimpleAuto { decimal_scale } => {
266                pack_simple_auto(&values, bitmap.as_deref(), decimal_scale)?
267            }
268        };
269
270        Ok(Grib2Field {
271            discipline: self.discipline,
272            identification,
273            grid,
274            product,
275            packed,
276        })
277    }
278}
279
280/// A validated, packed GRIB2 field ready for message serialization.
281#[derive(Debug, Clone)]
282pub struct Grib2Field {
283    discipline: u8,
284    identification: Identification,
285    grid: GridDefinition,
286    product: ProductDefinition,
287    packed: PackedField,
288}
289
290impl Grib2Field {
291    pub fn discipline(&self) -> u8 {
292        self.discipline
293    }
294
295    pub fn identification(&self) -> &Identification {
296        &self.identification
297    }
298
299    pub fn grid(&self) -> &GridDefinition {
300        &self.grid
301    }
302
303    pub fn product(&self) -> &ProductDefinition {
304        &self.product
305    }
306
307    pub fn data_representation(&self) -> &DataRepresentation {
308        &self.packed.representation
309    }
310}
311
312/// Streaming GRIB writer.
313pub struct GribWriter<'a, W> {
314    out: &'a mut W,
315}
316
317impl<'a, W: Write> GribWriter<'a, W> {
318    pub fn new(out: &'a mut W) -> Self {
319        Self { out }
320    }
321
322    pub fn write_grib1_message(&mut self, field: Grib1Field) -> Result<()> {
323        let mut body = Vec::new();
324        write_grib1_product_section(&mut body, &field.product)?;
325        write_grib1_grid_section(&mut body, &field.grid)?;
326        if let Some(bitmap) = &field.packed.bitmap_payload {
327            write_grib1_bitmap_section(&mut body, bitmap, field.grid.num_points())?;
328        }
329        write_grib1_data_section(&mut body, &field.packed, 0)?;
330
331        let total_len = checked_grib1_u24_length(8usize + body.len() + 4, 0)?;
332        let mut message = Vec::new();
333        message.extend_from_slice(b"GRIB");
334        write_u24_be(&mut message, total_len)?;
335        write_u8_be(&mut message, 1)?;
336        message.extend_from_slice(&body);
337        message.extend_from_slice(b"7777");
338
339        self.out
340            .write_all(&message)
341            .map_err(|err| Error::Io(err, "GRIB writer output".into()))
342    }
343
344    pub fn write_grib2_message<I>(&mut self, fields: I) -> Result<()>
345    where
346        I: IntoIterator<Item = Grib2Field>,
347    {
348        let fields = fields.into_iter().collect::<Vec<_>>();
349        if fields.is_empty() {
350            return Err(Error::InvalidMessage(
351                "cannot write a GRIB2 message without fields".into(),
352            ));
353        }
354
355        let first = &fields[0];
356        for field in &fields[1..] {
357            if field.discipline != first.discipline {
358                return Err(Error::InvalidMessage(
359                    "all fields in a GRIB2 message must share a discipline".into(),
360                ));
361            }
362            if field.identification != first.identification {
363                return Err(Error::InvalidMessage(
364                    "all fields in a GRIB2 message must share Section 1 identification".into(),
365                ));
366            }
367        }
368
369        let mut message = Vec::new();
370        write_indicator_placeholder(&mut message, first.discipline)?;
371        write_identification_section(&mut message, &first.identification)?;
372        let mut current_grid = None;
373        for field in &fields {
374            if current_grid != Some(&field.grid) {
375                write_grid_section(&mut message, &field.grid)?;
376                current_grid = Some(&field.grid);
377            }
378            write_product_section(&mut message, &field.product)?;
379            write_data_representation_section(&mut message, &field.packed)?;
380            if let Some(bitmap) = &field.packed.bitmap_payload {
381                write_bitmap_section(&mut message, bitmap)?;
382            }
383            write_data_section(&mut message, &field.packed.data_payload)?;
384        }
385        message.extend_from_slice(b"7777");
386
387        let total_len = u64::try_from(message.len())
388            .map_err(|_| Error::Other("GRIB2 message length exceeds u64".into()))?;
389        message[8..16].copy_from_slice(&total_len.to_be_bytes());
390
391        self.out
392            .write_all(&message)
393            .map_err(|err| Error::Io(err, "GRIB writer output".into()))
394    }
395}
396
397#[derive(Debug, Clone)]
398struct PackedField {
399    representation: DataRepresentation,
400    bitmap_payload: Option<Vec<u8>>,
401    data_payload: Vec<u8>,
402}
403
404fn pack_simple_auto(
405    values: &[f64],
406    explicit_bitmap: Option<&[bool]>,
407    decimal_scale: i16,
408) -> Result<PackedField> {
409    let present = present_mask(values, explicit_bitmap)?;
410    let present_count = present.iter().filter(|present| **present).count();
411    let bitmap_payload = if present.iter().any(|present| !*present) {
412        Some(pack_bitmap(&present)?)
413    } else {
414        None
415    };
416
417    let decimal_factor = 10.0_f64.powi(i32::from(decimal_scale));
418    if !decimal_factor.is_finite() || decimal_factor <= 0.0 {
419        return Err(Error::Other(format!(
420            "invalid decimal scale for simple packing: {decimal_scale}"
421        )));
422    }
423
424    let quantized = values
425        .iter()
426        .zip(&present)
427        .filter_map(|(value, present)| present.then_some(*value))
428        .map(|value| {
429            if !value.is_finite() {
430                return Err(Error::Other(
431                    "present values must be finite for simple packing".into(),
432                ));
433            }
434            let scaled = value * decimal_factor;
435            if !scaled.is_finite() {
436                return Err(Error::Other(
437                    "scaled value overflow during simple packing".into(),
438                ));
439            }
440            Ok(scaled.round())
441        })
442        .collect::<Result<Vec<_>>>()?;
443
444    let (reference_value, deltas) = simple_packing_deltas(&quantized)?;
445    let max_delta = deltas.iter().copied().max().unwrap_or(0);
446    let bits_per_value = if max_delta == 0 {
447        0
448    } else {
449        (u64::BITS - max_delta.leading_zeros()) as u8
450    };
451
452    let mut writer = BitWriter::with_capacity_bits(deltas.len() * usize::from(bits_per_value));
453    if bits_per_value > 0 {
454        for delta in &deltas {
455            writer.write(*delta, usize::from(bits_per_value))?;
456        }
457        writer.align_to_byte()?;
458    }
459
460    let representation = DataRepresentation::SimplePacking(SimplePackingParams {
461        encoded_values: present_count,
462        reference_value,
463        binary_scale: 0,
464        decimal_scale,
465        bits_per_value,
466        original_field_type: 0,
467    });
468
469    Ok(PackedField {
470        representation,
471        bitmap_payload,
472        data_payload: writer.into_bytes(),
473    })
474}
475
476fn reorder_field_to_grib_scan_order(
477    grid: &GridDefinition,
478    values: &mut [f64],
479    bitmap: Option<&mut [bool]>,
480) -> Result<()> {
481    match grid {
482        GridDefinition::LatLon(grid) => {
483            grid.reorder_for_ndarray_in_place(values)?;
484            if let Some(bitmap) = bitmap {
485                grid.reorder_for_ndarray_in_place(bitmap)?;
486            }
487            Ok(())
488        }
489        GridDefinition::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
490    }
491}
492
493fn present_mask(values: &[f64], explicit_bitmap: Option<&[bool]>) -> Result<Vec<bool>> {
494    match explicit_bitmap {
495        Some(bitmap) => values
496            .iter()
497            .zip(bitmap)
498            .map(|(value, present)| {
499                if *present && !value.is_finite() {
500                    return Err(Error::Other(
501                        "explicit bitmap marks a non-finite value as present".into(),
502                    ));
503                }
504                Ok(*present)
505            })
506            .collect(),
507        None => values
508            .iter()
509            .map(|value| {
510                if value.is_nan() {
511                    Ok(false)
512                } else if value.is_finite() {
513                    Ok(true)
514                } else {
515                    Err(Error::Other(
516                        "infinite values cannot be written as simple-packed data".into(),
517                    ))
518                }
519            })
520            .collect(),
521    }
522}
523
524fn simple_packing_deltas(quantized: &[f64]) -> Result<(f32, Vec<u64>)> {
525    if quantized.is_empty() {
526        return Ok((0.0, Vec::new()));
527    }
528
529    let min_value = quantized.iter().copied().fold(f64::INFINITY, f64::min);
530    let reference_value = f32_not_greater_than(min_value)
531        .ok_or_else(|| Error::Other("failed to choose simple-packing reference value".into()))?;
532    let reference = f64::from(reference_value);
533
534    let mut deltas = Vec::with_capacity(quantized.len());
535    for value in quantized {
536        let delta = (value - reference).round();
537        if !delta.is_finite() || delta < 0.0 || delta > u64::MAX as f64 {
538            return Err(Error::Other(
539                "packed simple-packing delta does not fit in u64".into(),
540            ));
541        }
542        deltas.push(delta as u64);
543    }
544
545    Ok((reference_value, deltas))
546}
547
548fn f32_not_greater_than(value: f64) -> Option<f32> {
549    if !value.is_finite() || value < f64::from(f32::MIN) || value > f64::from(f32::MAX) {
550        return None;
551    }
552
553    let mut candidate = value as f32;
554    while f64::from(candidate) > value {
555        candidate = next_down_f32(candidate)?;
556    }
557    Some(candidate)
558}
559
560fn next_down_f32(value: f32) -> Option<f32> {
561    if value.is_nan() || value == f32::NEG_INFINITY {
562        return None;
563    }
564    if value == 0.0 {
565        return Some(-f32::from_bits(1));
566    }
567    let bits = value.to_bits();
568    Some(if value.is_sign_positive() {
569        f32::from_bits(bits - 1)
570    } else {
571        f32::from_bits(bits + 1)
572    })
573}
574
575fn pack_bitmap(present: &[bool]) -> Result<Vec<u8>> {
576    let mut writer = BitWriter::with_capacity_bits(present.len());
577    for present in present {
578        writer.write(u64::from(*present), 1)?;
579    }
580    writer.align_to_byte()?;
581    Ok(writer.into_bytes())
582}
583
584fn write_grib1_product_section(out: &mut Vec<u8>, product: &Grib1ProductDefinition) -> Result<()> {
585    let (year_of_century, century) = grib1_reference_year_fields(product.reference_time.year)?;
586
587    write_u24_be(out, 28)?;
588    write_u8_be(out, product.table_version)?;
589    write_u8_be(out, product.center_id)?;
590    write_u8_be(out, product.generating_process_id)?;
591    write_u8_be(out, product.grid_id)?;
592    let mut flags = 0b1000_0000;
593    if product.has_bitmap {
594        flags |= 0b0100_0000;
595    }
596    write_u8_be(out, flags)?;
597    write_u8_be(out, product.parameter_number)?;
598    write_u8_be(out, product.level_type)?;
599    write_u16_be(out, product.level_value)?;
600    write_u8_be(out, year_of_century)?;
601    write_u8_be(out, product.reference_time.month)?;
602    write_u8_be(out, product.reference_time.day)?;
603    write_u8_be(out, product.reference_time.hour)?;
604    write_u8_be(out, product.reference_time.minute)?;
605    write_u8_be(out, product.forecast_time_unit)?;
606    write_u8_be(out, product.p1)?;
607    write_u8_be(out, product.p2)?;
608    write_u8_be(out, product.time_range_indicator)?;
609    write_u16_be(out, product.average_count)?;
610    write_u8_be(out, product.missing_count)?;
611    write_u8_be(out, century)?;
612    write_u8_be(out, product.subcenter_id)?;
613    out.extend_from_slice(
614        &encode_wmo_i16(product.decimal_scale)
615            .ok_or_else(|| Error::Other("decimal scale does not fit GRIB signed i16".into()))?,
616    );
617    Ok(())
618}
619
620fn write_grib1_grid_section(out: &mut Vec<u8>, grid: &GridDefinition) -> Result<()> {
621    let GridDefinition::LatLon(grid) = grid else {
622        return Err(Error::UnsupportedGridTemplate(match grid {
623            GridDefinition::Unsupported(template) => *template,
624            GridDefinition::LatLon(_) => unreachable!(),
625        }));
626    };
627
628    write_u24_be(out, 32)?;
629    write_u8_be(out, 0)?;
630    write_u8_be(out, 255)?;
631    write_u8_be(out, 0)?;
632    write_u16_be(out, checked_grib1_grid_dimension(grid.ni, "Ni")?)?;
633    write_u16_be(out, checked_grib1_grid_dimension(grid.nj, "Nj")?)?;
634    out.extend_from_slice(&encode_grib1_coordinate(
635        grid.lat_first,
636        "latitude of first grid point",
637    )?);
638    out.extend_from_slice(&encode_grib1_coordinate(
639        grid.lon_first,
640        "longitude of first grid point",
641    )?);
642    write_u8_be(out, 0x80)?;
643    out.extend_from_slice(&encode_grib1_coordinate(
644        grid.lat_last,
645        "latitude of last grid point",
646    )?);
647    out.extend_from_slice(&encode_grib1_coordinate(
648        grid.lon_last,
649        "longitude of last grid point",
650    )?);
651    write_u16_be(
652        out,
653        checked_grib1_increment(grid.di, "i direction increment")?,
654    )?;
655    write_u16_be(
656        out,
657        checked_grib1_increment(grid.dj, "j direction increment")?,
658    )?;
659    write_u8_be(out, grid.scanning_mode)?;
660    out.extend_from_slice(&[0; 4]);
661    Ok(())
662}
663
664fn write_grib1_bitmap_section(
665    out: &mut Vec<u8>,
666    bitmap_payload: &[u8],
667    num_points: usize,
668) -> Result<()> {
669    let length = checked_grib1_u24_length(6usize + bitmap_payload.len(), 3)?;
670    write_u24_be(out, length)?;
671    write_u8_be(out, unused_bits_for_width(num_points, 1)?)?;
672    write_u16_be(out, 0)?;
673    out.extend_from_slice(bitmap_payload);
674    Ok(())
675}
676
677fn write_grib1_data_section(out: &mut Vec<u8>, packed: &PackedField, flags: u8) -> Result<()> {
678    validate_grib1_binary_data_flags(flags)?;
679    let DataRepresentation::SimplePacking(params) = &packed.representation else {
680        return Err(Error::UnsupportedDataTemplate(1004));
681    };
682
683    let length = checked_grib1_u24_length(11usize + packed.data_payload.len(), 4)?;
684    write_u24_be(out, length)?;
685    let unused_bits = unused_bits_for_width(params.encoded_values, params.bits_per_value)?;
686    write_u8_be(out, (flags << 4) | unused_bits)?;
687    out.extend_from_slice(
688        &encode_wmo_i16(params.binary_scale)
689            .ok_or_else(|| Error::Other("binary scale does not fit GRIB signed i16".into()))?,
690    );
691    out.extend_from_slice(
692        &encode_ibm_f32(params.reference_value)
693            .ok_or_else(|| Error::Other("reference value does not fit GRIB1 IBM float".into()))?,
694    );
695    write_u8_be(out, params.bits_per_value)?;
696    out.extend_from_slice(&packed.data_payload);
697    Ok(())
698}
699
700fn validate_grib1_binary_data_flags(flags: u8) -> Result<()> {
701    if flags == 0 {
702        return Ok(());
703    }
704    if flags > 0x0f {
705        return Err(Error::Other(
706            "GRIB1 binary data flags must fit in four bits".into(),
707        ));
708    }
709    let template = if flags & 0b1000 != 0 {
710        1004
711    } else if flags & 0b0100 != 0 {
712        1005
713    } else if flags & 0b0010 != 0 {
714        1006
715    } else {
716        1007
717    };
718    Err(Error::UnsupportedDataTemplate(template))
719}
720
721fn unused_bits_for_width(values: usize, bits_per_value: u8) -> Result<u8> {
722    let bits = values
723        .checked_mul(usize::from(bits_per_value))
724        .ok_or_else(|| Error::Other("packed bit count overflow".into()))?;
725    Ok(((8 - (bits % 8)) % 8) as u8)
726}
727
728fn grib1_reference_year_fields(year: u16) -> Result<(u8, u8)> {
729    if year == 0 {
730        return Err(Error::Other(
731            "GRIB1 reference year 0 cannot be encoded".into(),
732        ));
733    }
734
735    let century = ((year - 1) / 100) + 1;
736    let year_of_century = year - ((century - 1) * 100);
737    Ok((
738        u8::try_from(year_of_century)
739            .map_err(|_| Error::Other("GRIB1 year of century exceeds u8".into()))?,
740        u8::try_from(century).map_err(|_| Error::Other("GRIB1 century exceeds u8".into()))?,
741    ))
742}
743
744fn encode_grib1_coordinate(value: i32, name: &str) -> Result<[u8; 3]> {
745    if value % 1_000 != 0 {
746        return Err(Error::Other(format!(
747            "{name} must be representable in GRIB1 millidegrees"
748        )));
749    }
750    encode_wmo_i24(value / 1_000)
751        .ok_or_else(|| Error::Other(format!("{name} does not fit GRIB signed i24")))
752}
753
754fn checked_grib1_grid_dimension(value: u32, name: &str) -> Result<u16> {
755    u16::try_from(value).map_err(|_| Error::Other(format!("{name} exceeds GRIB1 u16 limit")))
756}
757
758fn checked_grib1_increment(value: u32, name: &str) -> Result<u16> {
759    if value % 1_000 != 0 {
760        return Err(Error::Other(format!(
761            "{name} must be representable in GRIB1 millidegrees"
762        )));
763    }
764    u16::try_from(value / 1_000)
765        .map_err(|_| Error::Other(format!("{name} exceeds GRIB1 u16 millidegree limit")))
766}
767
768fn checked_grib1_u24_length(length: usize, section: u8) -> Result<u32> {
769    let length = u32::try_from(length).map_err(|_| Error::InvalidSection {
770        section,
771        reason: "GRIB1 length exceeds unsigned 24-bit limit".into(),
772    })?;
773    if length > U24_MAX {
774        return Err(Error::InvalidSection {
775            section,
776            reason: format!("GRIB1 length {length} exceeds unsigned 24-bit limit"),
777        });
778    }
779    Ok(length)
780}
781
782fn write_indicator_placeholder(out: &mut Vec<u8>, discipline: u8) -> Result<()> {
783    out.extend_from_slice(b"GRIB");
784    write_u16_be(out, 0)?;
785    write_u8_be(out, discipline)?;
786    write_u8_be(out, 2)?;
787    write_u64_be(out, 0)
788}
789
790fn write_identification_section(out: &mut Vec<u8>, identification: &Identification) -> Result<()> {
791    write_u32_be(out, 21)?;
792    write_u8_be(out, 1)?;
793    write_u16_be(out, identification.center_id)?;
794    write_u16_be(out, identification.subcenter_id)?;
795    write_u8_be(out, identification.master_table_version)?;
796    write_u8_be(out, identification.local_table_version)?;
797    write_u8_be(out, identification.significance_of_reference_time)?;
798    write_u16_be(out, identification.reference_year)?;
799    write_u8_be(out, identification.reference_month)?;
800    write_u8_be(out, identification.reference_day)?;
801    write_u8_be(out, identification.reference_hour)?;
802    write_u8_be(out, identification.reference_minute)?;
803    write_u8_be(out, identification.reference_second)?;
804    write_u8_be(out, identification.production_status)?;
805    write_u8_be(out, identification.processed_data_type)
806}
807
808fn write_grid_section(out: &mut Vec<u8>, grid: &GridDefinition) -> Result<()> {
809    let GridDefinition::LatLon(grid) = grid else {
810        return Err(Error::UnsupportedGridTemplate(match grid {
811            GridDefinition::Unsupported(template) => *template,
812            GridDefinition::LatLon(_) => unreachable!(),
813        }));
814    };
815
816    let mut section = vec![0u8; 72];
817    section[..4].copy_from_slice(&72u32.to_be_bytes());
818    section[4] = 3;
819    section[6..10].copy_from_slice(&checked_latlon_point_count(grid)?.to_be_bytes());
820    section[12..14].copy_from_slice(&0u16.to_be_bytes());
821    section[30..34].copy_from_slice(&grid.ni.to_be_bytes());
822    section[34..38].copy_from_slice(&grid.nj.to_be_bytes());
823    section[46..50].copy_from_slice(&encode_wmo_i32(grid.lat_first).ok_or_else(|| {
824        Error::Other("latitude of first grid point does not fit GRIB signed i32".into())
825    })?);
826    section[50..54].copy_from_slice(&encode_wmo_i32(grid.lon_first).ok_or_else(|| {
827        Error::Other("longitude of first grid point does not fit GRIB signed i32".into())
828    })?);
829    section[55..59].copy_from_slice(&encode_wmo_i32(grid.lat_last).ok_or_else(|| {
830        Error::Other("latitude of last grid point does not fit GRIB signed i32".into())
831    })?);
832    section[59..63].copy_from_slice(&encode_wmo_i32(grid.lon_last).ok_or_else(|| {
833        Error::Other("longitude of last grid point does not fit GRIB signed i32".into())
834    })?);
835    section[63..67].copy_from_slice(&grid.di.to_be_bytes());
836    section[67..71].copy_from_slice(&grid.dj.to_be_bytes());
837    section[71] = grid.scanning_mode;
838    out.extend_from_slice(&section);
839    Ok(())
840}
841
842fn write_product_section(out: &mut Vec<u8>, product: &ProductDefinition) -> Result<()> {
843    let ProductDefinitionTemplate::AnalysisOrForecast(template) = &product.template;
844
845    write_u32_be(out, 34)?;
846    write_u8_be(out, 4)?;
847    write_u16_be(out, 0)?;
848    write_u16_be(out, 0)?;
849    write_u8_be(out, product.parameter_category)?;
850    write_u8_be(out, product.parameter_number)?;
851    write_u8_be(out, template.generating_process)?;
852    write_u8_be(out, 0)?;
853    write_u8_be(out, 0)?;
854    write_u16_be(out, 0)?;
855    write_u8_be(out, 0)?;
856    write_u8_be(out, template.forecast_time_unit)?;
857    write_u32_be(out, template.forecast_time)?;
858    write_surface(out, template.first_surface.as_ref())?;
859    write_surface(out, template.second_surface.as_ref())
860}
861
862fn write_surface(out: &mut Vec<u8>, surface: Option<&FixedSurface>) -> Result<()> {
863    match surface {
864        Some(surface) => {
865            write_u8_be(out, surface.surface_type)?;
866            write_u8_be(
867                out,
868                encode_wmo_i8(surface.scale_factor).ok_or_else(|| {
869                    Error::Other("fixed-surface scale factor does not fit GRIB signed i8".into())
870                })?,
871            )?;
872            out.extend_from_slice(&encode_wmo_i32(surface.scaled_value).ok_or_else(|| {
873                Error::Other("fixed-surface scaled value does not fit GRIB signed i32".into())
874            })?);
875            Ok(())
876        }
877        None => {
878            write_u8_be(out, 255)?;
879            out.extend_from_slice(&[0xff; 5]);
880            Ok(())
881        }
882    }
883}
884
885fn write_data_representation_section(out: &mut Vec<u8>, packed: &PackedField) -> Result<()> {
886    let DataRepresentation::SimplePacking(params) = &packed.representation else {
887        return Err(Error::UnsupportedDataTemplate(0));
888    };
889
890    let encoded_values = u32::try_from(params.encoded_values)
891        .map_err(|_| Error::Other("encoded value count exceeds u32".into()))?;
892    write_u32_be(out, 21)?;
893    write_u8_be(out, 5)?;
894    write_u32_be(out, encoded_values)?;
895    write_u16_be(out, 0)?;
896    out.extend_from_slice(&params.reference_value.to_be_bytes());
897    out.extend_from_slice(
898        &encode_wmo_i16(params.binary_scale)
899            .ok_or_else(|| Error::Other("binary scale does not fit GRIB signed i16".into()))?,
900    );
901    out.extend_from_slice(
902        &encode_wmo_i16(params.decimal_scale)
903            .ok_or_else(|| Error::Other("decimal scale does not fit GRIB signed i16".into()))?,
904    );
905    write_u8_be(out, params.bits_per_value)?;
906    write_u8_be(out, params.original_field_type)
907}
908
909fn write_bitmap_section(out: &mut Vec<u8>, bitmap_payload: &[u8]) -> Result<()> {
910    let length = checked_section_length(6usize + bitmap_payload.len(), 6)?;
911    write_u32_be(out, length)?;
912    write_u8_be(out, 6)?;
913    write_u8_be(out, 0)?;
914    out.extend_from_slice(bitmap_payload);
915    Ok(())
916}
917
918fn write_data_section(out: &mut Vec<u8>, data_payload: &[u8]) -> Result<()> {
919    let length = checked_section_length(5usize + data_payload.len(), 7)?;
920    write_u32_be(out, length)?;
921    write_u8_be(out, 7)?;
922    out.extend_from_slice(data_payload);
923    Ok(())
924}
925
926fn checked_section_length(length: usize, section: u8) -> Result<u32> {
927    u32::try_from(length).map_err(|_| Error::InvalidSection {
928        section,
929        reason: format!("section length {length} exceeds u32"),
930    })
931}
932
933fn checked_grid_point_count(grid: &GridDefinition) -> Result<usize> {
934    match grid {
935        GridDefinition::LatLon(grid) => Ok(checked_latlon_point_count(grid)? as usize),
936        GridDefinition::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
937    }
938}
939
940fn checked_latlon_point_count(grid: &LatLonGrid) -> Result<u32> {
941    let count = u64::from(grid.ni)
942        .checked_mul(u64::from(grid.nj))
943        .ok_or_else(|| Error::Other("grid point count overflow".into()))?;
944    u32::try_from(count).map_err(|_| Error::Other("grid point count exceeds u32".into()))
945}
946
947fn validate_supported_grid(grid: &GridDefinition) -> Result<()> {
948    match grid {
949        GridDefinition::LatLon(grid) => validate_supported_scan_order(grid),
950        GridDefinition::Unsupported(template) => Err(Error::UnsupportedGridTemplate(*template)),
951    }
952}
953
954fn validate_supported_scan_order(grid: &LatLonGrid) -> Result<()> {
955    if grid.scanning_mode & 0b0010_0000 == 0 {
956        Ok(())
957    } else {
958        Err(Error::UnsupportedScanningMode(grid.scanning_mode))
959    }
960}
961
962fn validate_supported_grib1_grid(grid: &GridDefinition) -> Result<()> {
963    validate_supported_grid(grid)?;
964    let GridDefinition::LatLon(grid) = grid else {
965        return Ok(());
966    };
967    checked_grib1_grid_dimension(grid.ni, "Ni")?;
968    checked_grib1_grid_dimension(grid.nj, "Nj")?;
969    checked_grib1_increment(grid.di, "i direction increment")?;
970    checked_grib1_increment(grid.dj, "j direction increment")?;
971    encode_grib1_coordinate(grid.lat_first, "latitude of first grid point")?;
972    encode_grib1_coordinate(grid.lon_first, "longitude of first grid point")?;
973    encode_grib1_coordinate(grid.lat_last, "latitude of last grid point")?;
974    encode_grib1_coordinate(grid.lon_last, "longitude of last grid point")?;
975    Ok(())
976}
977
978fn validate_supported_product(product: &ProductDefinition) -> Result<()> {
979    match product.template {
980        ProductDefinitionTemplate::AnalysisOrForecast(_) => Ok(()),
981    }
982}
983
984#[cfg(test)]
985mod tests {
986    use super::{
987        Grib1FieldBuilder, Grib1ProductDefinition, Grib2FieldBuilder, GribWriter, PackingStrategy,
988        ValueOrder,
989    };
990    use std::process::Command;
991
992    use grib_core::binary::decode_ibm_f32;
993    use grib_core::metadata::ReferenceTime;
994    use grib_core::{
995        AnalysisOrForecastTemplate, DataRepresentation, FixedSurface, GridDefinition,
996        Identification, LatLonGrid, ProductDefinition, ProductDefinitionTemplate,
997    };
998    use grib_reader::sections::scan_sections;
999    use grib_reader::GribFile;
1000    use serde::Deserialize;
1001
1002    fn identification() -> Identification {
1003        Identification {
1004            center_id: 7,
1005            subcenter_id: 0,
1006            master_table_version: 35,
1007            local_table_version: 1,
1008            significance_of_reference_time: 1,
1009            reference_year: 2026,
1010            reference_month: 3,
1011            reference_day: 20,
1012            reference_hour: 12,
1013            reference_minute: 0,
1014            reference_second: 0,
1015            production_status: 0,
1016            processed_data_type: 1,
1017        }
1018    }
1019
1020    fn grib1_product() -> Grib1ProductDefinition {
1021        Grib1ProductDefinition {
1022            table_version: 2,
1023            center_id: 7,
1024            generating_process_id: 255,
1025            grid_id: 0,
1026            has_grid_definition: true,
1027            has_bitmap: false,
1028            parameter_number: 11,
1029            level_type: 100,
1030            level_value: 850,
1031            reference_time: ReferenceTime {
1032                year: 2026,
1033                month: 3,
1034                day: 20,
1035                hour: 12,
1036                minute: 0,
1037                second: 0,
1038            },
1039            forecast_time_unit: 1,
1040            p1: 6,
1041            p2: 0,
1042            time_range_indicator: 0,
1043            average_count: 0,
1044            missing_count: 0,
1045            century: 21,
1046            subcenter_id: 0,
1047            decimal_scale: 0,
1048        }
1049    }
1050
1051    fn grid() -> GridDefinition {
1052        grid_with_shape_and_scanning_mode(2, 2, 0)
1053    }
1054
1055    fn grid_with_scanning_mode(scanning_mode: u8) -> GridDefinition {
1056        grid_with_shape_and_scanning_mode(3, 2, scanning_mode)
1057    }
1058
1059    fn grid_with_shape_and_scanning_mode(ni: u32, nj: u32, scanning_mode: u8) -> GridDefinition {
1060        let lon_first = -120_000_000;
1061        let lat_first = 50_000_000;
1062        let di = 1_000_000;
1063        let dj = 1_000_000;
1064        let i_step = if scanning_mode & 0b1000_0000 == 0 {
1065            di as i32
1066        } else {
1067            -(di as i32)
1068        };
1069        let j_step = if scanning_mode & 0b0100_0000 != 0 {
1070            dj as i32
1071        } else {
1072            -(dj as i32)
1073        };
1074
1075        GridDefinition::LatLon(LatLonGrid {
1076            ni,
1077            nj,
1078            lat_first,
1079            lon_first,
1080            lat_last: lat_first + (nj.saturating_sub(1) as i32) * j_step,
1081            lon_last: lon_first + (ni.saturating_sub(1) as i32) * i_step,
1082            di,
1083            dj,
1084            scanning_mode,
1085        })
1086    }
1087
1088    fn product(parameter_category: u8, parameter_number: u8) -> ProductDefinition {
1089        ProductDefinition {
1090            parameter_category,
1091            parameter_number,
1092            template: ProductDefinitionTemplate::AnalysisOrForecast(AnalysisOrForecastTemplate {
1093                generating_process: 2,
1094                forecast_time_unit: 1,
1095                forecast_time: 6,
1096                first_surface: Some(FixedSurface {
1097                    surface_type: 103,
1098                    scale_factor: 0,
1099                    scaled_value: 850,
1100                }),
1101                second_surface: None,
1102            }),
1103        }
1104    }
1105
1106    fn write_message(fields: impl IntoIterator<Item = super::Grib2Field>) -> Vec<u8> {
1107        let mut bytes = Vec::new();
1108        GribWriter::new(&mut bytes)
1109            .write_grib2_message(fields)
1110            .unwrap();
1111        bytes
1112    }
1113
1114    fn write_grib1_message(field: super::Grib1Field) -> Vec<u8> {
1115        let mut bytes = Vec::new();
1116        GribWriter::new(&mut bytes)
1117            .write_grib1_message(field)
1118            .unwrap();
1119        bytes
1120    }
1121
1122    fn section_numbers(bytes: &[u8]) -> Vec<u8> {
1123        scan_sections(bytes)
1124            .unwrap()
1125            .iter()
1126            .map(|section| section.number)
1127            .collect()
1128    }
1129
1130    fn simple_field(
1131        values: &[f64],
1132        parameter_category: u8,
1133        parameter_number: u8,
1134    ) -> super::Grib2Field {
1135        Grib2FieldBuilder::new()
1136            .identification(identification())
1137            .grid(grid())
1138            .product(product(parameter_category, parameter_number))
1139            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1140            .values(values)
1141            .build()
1142            .unwrap()
1143    }
1144
1145    fn grib1_simple_field(values: &[f64]) -> super::Grib1Field {
1146        Grib1FieldBuilder::new()
1147            .product(grib1_product())
1148            .grid(grid())
1149            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1150            .values(values)
1151            .build()
1152            .unwrap()
1153    }
1154
1155    #[test]
1156    fn writes_simple_grib1_field_readable_by_reader() {
1157        let values = [1.0, 2.0, 3.0, 4.0];
1158        let bytes = write_grib1_message(grib1_simple_field(&values));
1159
1160        let file = GribFile::from_bytes(bytes).unwrap();
1161        let message = file.message(0).unwrap();
1162        assert_eq!(file.edition(), 1);
1163        assert_eq!(file.message_count(), 1);
1164        assert_eq!(message.parameter_name(), "TMP");
1165        assert_eq!(message.grid_shape(), (2, 2));
1166        assert_eq!(message.forecast_time(), Some(6));
1167        assert_eq!(message.read_flat_data_as_f64().unwrap(), values);
1168    }
1169
1170    #[test]
1171    fn writes_grib1_bitmap_from_nan_values() {
1172        let values = [5.0, f64::NAN, 7.0, 8.0];
1173        let bytes = write_grib1_message(grib1_simple_field(&values));
1174        let bitmap_offset = 8 + 28 + 32;
1175        assert_eq!(&bytes[bitmap_offset + 4..bitmap_offset + 6], &[0, 0]);
1176
1177        let file = GribFile::from_bytes(bytes).unwrap();
1178        let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
1179        assert_eq!(decoded[0], 5.0);
1180        assert!(decoded[1].is_nan());
1181        assert_eq!(decoded[2], 7.0);
1182        assert_eq!(decoded[3], 8.0);
1183    }
1184
1185    #[test]
1186    fn writes_grib1_bitmap_from_explicit_mask() {
1187        let field = Grib1FieldBuilder::new()
1188            .product(grib1_product())
1189            .grid(grid())
1190            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1191            .values(&[5.0, 999.0, 7.0, 8.0])
1192            .bitmap(&[true, false, true, true])
1193            .build()
1194            .unwrap();
1195
1196        let file = GribFile::from_bytes(write_grib1_message(field)).unwrap();
1197        let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
1198        assert_eq!(decoded[0], 5.0);
1199        assert!(decoded[1].is_nan());
1200        assert_eq!(decoded[2], 7.0);
1201        assert_eq!(decoded[3], 8.0);
1202    }
1203
1204    #[test]
1205    fn writes_grib1_ibm_float_reference_value() {
1206        let bytes = write_grib1_message(grib1_simple_field(&[10.0, 11.0, 12.0, 13.0]));
1207        let bds_offset = 8 + 28 + 32;
1208        let reference = decode_ibm_f32(bytes[bds_offset + 6..bds_offset + 10].try_into().unwrap());
1209        assert_eq!(reference, 10.0);
1210
1211        let file = GribFile::from_bytes(bytes).unwrap();
1212        assert_eq!(
1213            file.message(0).unwrap().read_flat_data_as_f64().unwrap(),
1214            vec![10.0, 11.0, 12.0, 13.0]
1215        );
1216    }
1217
1218    #[test]
1219    fn rejects_grib1_u24_length_overflow() {
1220        let err = super::checked_grib1_u24_length(grib_core::binary::U24_MAX as usize + 1, 0)
1221            .unwrap_err();
1222        assert!(matches!(
1223            err,
1224            grib_core::Error::InvalidSection { section: 0, .. }
1225        ));
1226    }
1227
1228    #[test]
1229    fn rejects_unsupported_grib1_binary_data_flags() {
1230        let err = super::validate_grib1_binary_data_flags(0b0001).unwrap_err();
1231        assert!(matches!(
1232            err,
1233            grib_core::Error::UnsupportedDataTemplate(1007)
1234        ));
1235    }
1236
1237    #[test]
1238    fn rejects_grib1_grid_dimensions_beyond_u16() {
1239        let err = Grib1FieldBuilder::new()
1240            .product(grib1_product())
1241            .grid(GridDefinition::LatLon(LatLonGrid {
1242                ni: 65_536,
1243                nj: 1,
1244                lat_first: 0,
1245                lon_first: 0,
1246                lat_last: 0,
1247                lon_last: 0,
1248                di: 1_000,
1249                dj: 1_000,
1250                scanning_mode: 0,
1251            }))
1252            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1253            .values(&[1.0])
1254            .build()
1255            .unwrap_err();
1256        assert!(matches!(err, grib_core::Error::Other(message) if message.contains("Ni exceeds")));
1257    }
1258
1259    #[test]
1260    fn writes_simple_grib2_field_readable_by_reader() {
1261        let values = [1.0, 2.0, 3.0, 4.0];
1262        let field = simple_field(&values, 0, 0);
1263
1264        let file = GribFile::from_bytes(write_message([field])).unwrap();
1265        let message = file.message(0).unwrap();
1266        assert_eq!(message.parameter_name(), "TMP");
1267        assert_eq!(message.grid_shape(), (2, 2));
1268        assert_eq!(message.forecast_time(), Some(6));
1269        assert_eq!(message.read_flat_data_as_f64().unwrap(), values);
1270    }
1271
1272    #[test]
1273    fn writes_constant_field_with_zero_width_simple_packing() {
1274        let field = Grib2FieldBuilder::new()
1275            .identification(identification())
1276            .grid(grid())
1277            .product(product(0, 0))
1278            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1279            .values(&[42.0, 42.0, 42.0, 42.0])
1280            .build()
1281            .unwrap();
1282
1283        let file = GribFile::from_bytes(write_message([field])).unwrap();
1284        let message = file.message(0).unwrap();
1285        match &message.metadata().data_representation {
1286            DataRepresentation::SimplePacking(params) => assert_eq!(params.bits_per_value, 0),
1287            other => panic!("expected simple packing, got {other:?}"),
1288        }
1289        assert_eq!(message.read_flat_data_as_f64().unwrap(), vec![42.0; 4]);
1290    }
1291
1292    #[test]
1293    fn writes_decimal_scaled_values_within_quantization_tolerance() {
1294        let values = [1.2, 2.3, 3.4, 4.5];
1295        let field = Grib2FieldBuilder::new()
1296            .identification(identification())
1297            .grid(grid())
1298            .product(product(0, 0))
1299            .packing(PackingStrategy::SimpleAuto { decimal_scale: 1 })
1300            .values(&values)
1301            .build()
1302            .unwrap();
1303
1304        let file = GribFile::from_bytes(write_message([field])).unwrap();
1305        let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
1306        for (actual, expected) in decoded.iter().zip(values) {
1307            assert!((actual - expected).abs() <= 0.05);
1308        }
1309    }
1310
1311    #[test]
1312    fn writes_bitmap_from_nan_values() {
1313        let values = [1.0, f64::NAN, 3.0, 4.0];
1314        let field = Grib2FieldBuilder::new()
1315            .identification(identification())
1316            .grid(grid())
1317            .product(product(0, 0))
1318            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1319            .values(&values)
1320            .build()
1321            .unwrap();
1322
1323        let file = GribFile::from_bytes(write_message([field])).unwrap();
1324        let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
1325        assert_eq!(decoded[0], 1.0);
1326        assert!(decoded[1].is_nan());
1327        assert_eq!(decoded[2], 3.0);
1328        assert_eq!(decoded[3], 4.0);
1329    }
1330
1331    #[test]
1332    fn writes_bitmap_from_explicit_mask() {
1333        let values = [1.0, 999.0, 3.0, 4.0];
1334        let bitmap = [true, false, true, true];
1335        let field = Grib2FieldBuilder::new()
1336            .identification(identification())
1337            .grid(grid())
1338            .product(product(0, 0))
1339            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1340            .values(&values)
1341            .bitmap(&bitmap)
1342            .build()
1343            .unwrap();
1344
1345        let file = GribFile::from_bytes(write_message([field])).unwrap();
1346        let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
1347        assert_eq!(decoded[0], 1.0);
1348        assert!(decoded[1].is_nan());
1349        assert_eq!(decoded[2], 3.0);
1350        assert_eq!(decoded[3], 4.0);
1351    }
1352
1353    #[test]
1354    fn writes_all_missing_bitmap_field() {
1355        let values = [f64::NAN; 4];
1356        let field = Grib2FieldBuilder::new()
1357            .identification(identification())
1358            .grid(grid())
1359            .product(product(0, 0))
1360            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1361            .values(&values)
1362            .build()
1363            .unwrap();
1364
1365        let file = GribFile::from_bytes(write_message([field])).unwrap();
1366        let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
1367        assert!(decoded.iter().all(|value| value.is_nan()));
1368    }
1369
1370    #[test]
1371    fn writes_single_grib2_message_with_multiple_fields() {
1372        let first = simple_field(&[1.0, 2.0, 3.0, 4.0], 0, 0);
1373        let second = simple_field(&[5.0, 6.0, 7.0, 8.0], 0, 2);
1374
1375        let bytes = write_message([first, second]);
1376        assert_eq!(section_numbers(&bytes), vec![1, 3, 4, 5, 7, 4, 5, 7, 8]);
1377
1378        let file = GribFile::from_bytes(bytes).unwrap();
1379        assert_eq!(file.message_count(), 2);
1380        assert_eq!(file.message(0).unwrap().parameter_name(), "TMP");
1381        assert_eq!(file.message(1).unwrap().parameter_name(), "POT");
1382        assert_eq!(file.message(0).unwrap().grid_shape(), (2, 2));
1383        assert_eq!(file.message(1).unwrap().grid_shape(), (2, 2));
1384        assert_eq!(
1385            file.message(0).unwrap().read_flat_data_as_f64().unwrap(),
1386            vec![1.0, 2.0, 3.0, 4.0]
1387        );
1388        assert_eq!(
1389            file.message(1).unwrap().read_flat_data_as_f64().unwrap(),
1390            vec![5.0, 6.0, 7.0, 8.0]
1391        );
1392    }
1393
1394    #[test]
1395    fn emits_new_grid_section_only_when_grid_changes() {
1396        let first = simple_field(&[1.0, 2.0, 3.0, 4.0], 0, 0);
1397        let second = simple_field(&[5.0, 6.0, 7.0, 8.0], 0, 2);
1398        let third = Grib2FieldBuilder::new()
1399            .identification(identification())
1400            .grid(grid_with_shape_and_scanning_mode(3, 2, 0))
1401            .product(product(0, 4))
1402            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1403            .values(&[9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
1404            .build()
1405            .unwrap();
1406
1407        let bytes = write_message([first, second, third]);
1408        assert_eq!(
1409            section_numbers(&bytes),
1410            vec![1, 3, 4, 5, 7, 4, 5, 7, 3, 4, 5, 7, 8]
1411        );
1412
1413        let file = GribFile::from_bytes(bytes).unwrap();
1414        assert_eq!(file.message_count(), 3);
1415        assert_eq!(file.message(0).unwrap().parameter_name(), "TMP");
1416        assert_eq!(file.message(1).unwrap().parameter_name(), "POT");
1417        assert_eq!(file.message(2).unwrap().parameter_name(), "TMAX");
1418        assert_eq!(file.message(0).unwrap().grid_shape(), (2, 2));
1419        assert_eq!(file.message(1).unwrap().grid_shape(), (2, 2));
1420        assert_eq!(file.message(2).unwrap().grid_shape(), (3, 2));
1421        assert_eq!(
1422            file.message(2).unwrap().read_flat_data_as_f64().unwrap(),
1423            vec![9.0, 10.0, 11.0, 12.0, 13.0, 14.0]
1424        );
1425    }
1426
1427    #[test]
1428    fn writes_reused_grid_multifield_message_with_bitmap() {
1429        let first = simple_field(&[1.0, 2.0, 3.0, 4.0], 0, 0);
1430        let second = Grib2FieldBuilder::new()
1431            .identification(identification())
1432            .grid(grid())
1433            .product(product(0, 2))
1434            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1435            .values(&[5.0, f64::NAN, 7.0, 8.0])
1436            .build()
1437            .unwrap();
1438
1439        let bytes = write_message([first, second]);
1440        assert_eq!(section_numbers(&bytes), vec![1, 3, 4, 5, 7, 4, 5, 6, 7, 8]);
1441
1442        let file = GribFile::from_bytes(bytes).unwrap();
1443        assert_eq!(file.message_count(), 2);
1444        let decoded = file.message(1).unwrap().read_flat_data_as_f64().unwrap();
1445        assert_eq!(decoded[0], 5.0);
1446        assert!(decoded[1].is_nan());
1447        assert_eq!(decoded[2], 7.0);
1448        assert_eq!(decoded[3], 8.0);
1449    }
1450
1451    #[test]
1452    fn roundtrips_logical_row_major_order_for_supported_scan_modes() {
1453        let logical = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1454        for scanning_mode in [
1455            0b0000_0000,
1456            0b1000_0000,
1457            0b0100_0000,
1458            0b1100_0000,
1459            0b0001_0000,
1460            0b1001_0000,
1461        ] {
1462            let field = Grib2FieldBuilder::new()
1463                .identification(identification())
1464                .grid(grid_with_scanning_mode(scanning_mode))
1465                .product(product(0, 0))
1466                .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1467                .values(&logical)
1468                .build()
1469                .unwrap();
1470
1471            let file = GribFile::from_bytes(write_message([field])).unwrap();
1472            assert_eq!(
1473                file.message(0).unwrap().read_flat_data_as_f64().unwrap(),
1474                logical,
1475                "scanning mode {scanning_mode:08b}"
1476            );
1477        }
1478    }
1479
1480    #[test]
1481    fn accepts_grib_scan_order_fast_path() {
1482        let scan_order = [1.0, 2.0, 3.0, 6.0, 5.0, 4.0];
1483        let field = Grib2FieldBuilder::new()
1484            .identification(identification())
1485            .grid(grid_with_scanning_mode(0b0001_0000))
1486            .product(product(0, 0))
1487            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1488            .values(&scan_order)
1489            .value_order(ValueOrder::GribScanOrder)
1490            .build()
1491            .unwrap();
1492
1493        let file = GribFile::from_bytes(write_message([field])).unwrap();
1494        assert_eq!(
1495            file.message(0).unwrap().read_flat_data_as_f64().unwrap(),
1496            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
1497        );
1498    }
1499
1500    #[test]
1501    fn reorders_explicit_bitmap_with_logical_values() {
1502        let values = [1.0, 2.0, 3.0, 4.0, 999.0, 6.0];
1503        let bitmap = [true, true, true, true, false, true];
1504        let field = Grib2FieldBuilder::new()
1505            .identification(identification())
1506            .grid(grid_with_scanning_mode(0b0001_0000))
1507            .product(product(0, 0))
1508            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1509            .values(&values)
1510            .bitmap(&bitmap)
1511            .build()
1512            .unwrap();
1513
1514        let file = GribFile::from_bytes(write_message([field])).unwrap();
1515        let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
1516        assert_eq!(decoded[..4], [1.0, 2.0, 3.0, 4.0]);
1517        assert!(decoded[4].is_nan());
1518        assert_eq!(decoded[5], 6.0);
1519    }
1520
1521    #[test]
1522    fn rejects_unsupported_scan_mode_before_writing() {
1523        let err = Grib2FieldBuilder::new()
1524            .identification(identification())
1525            .grid(grid_with_scanning_mode(0b0010_0000))
1526            .product(product(0, 0))
1527            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1528            .values(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
1529            .build()
1530            .unwrap_err();
1531
1532        assert!(matches!(
1533            err,
1534            grib_core::Error::UnsupportedScanningMode(0b0010_0000)
1535        ));
1536    }
1537
1538    #[test]
1539    fn rejects_value_count_mismatch_before_writing() {
1540        let err = Grib2FieldBuilder::new()
1541            .identification(identification())
1542            .grid(grid())
1543            .product(product(0, 0))
1544            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1545            .values(&[1.0, 2.0, 3.0])
1546            .build()
1547            .unwrap_err();
1548        assert!(matches!(
1549            err,
1550            grib_core::Error::DataLengthMismatch {
1551                expected: 4,
1552                actual: 3
1553            }
1554        ));
1555    }
1556
1557    #[derive(Debug, Deserialize)]
1558    struct ReferenceDump {
1559        messages: Vec<ReferenceMessage>,
1560    }
1561
1562    #[derive(Debug, Deserialize)]
1563    struct ReferenceMessage {
1564        edition: u8,
1565        name: String,
1566        values: Vec<Option<f64>>,
1567    }
1568
1569    #[test]
1570    #[ignore = "requires GRIB_READER_ECCODES_HELPER"]
1571    fn generated_grib1_fixture_matches_eccodes_when_configured() {
1572        let helper = std::env::var_os("GRIB_READER_ECCODES_HELPER")
1573            .expect("GRIB_READER_ECCODES_HELPER must be set");
1574        let bytes = write_grib1_message(grib1_simple_field(&[5.0, f64::NAN, 7.0, 8.0]));
1575
1576        let dir = tempfile::tempdir().unwrap();
1577        let path = dir.path().join("writer-generated.grib1");
1578        std::fs::write(&path, &bytes).unwrap();
1579
1580        let output = Command::new(helper)
1581            .arg("dump")
1582            .arg(&path)
1583            .output()
1584            .unwrap();
1585        assert!(
1586            output.status.success(),
1587            "ecCodes helper failed:\nstdout:\n{}\nstderr:\n{}",
1588            String::from_utf8_lossy(&output.stdout),
1589            String::from_utf8_lossy(&output.stderr)
1590        );
1591        let reference: ReferenceDump = serde_json::from_slice(&output.stdout).unwrap();
1592        let rust = GribFile::from_bytes(bytes).unwrap();
1593
1594        assert_eq!(reference.messages.len(), 1);
1595        assert_eq!(rust.message_count(), reference.messages.len());
1596        let message = rust.message(0).unwrap();
1597        let actual = message.read_flat_data_as_f64().unwrap();
1598        let expected = &reference.messages[0];
1599        assert_eq!(message.edition(), expected.edition);
1600        assert_eq!(message.parameter_description(), expected.name);
1601        assert_eq!(actual.len(), expected.values.len());
1602        for (actual, expected) in actual.iter().zip(&expected.values) {
1603            match expected {
1604                Some(expected) => assert!((actual - expected).abs() <= 1e-6),
1605                None => assert!(actual.is_nan()),
1606            }
1607        }
1608    }
1609
1610    #[test]
1611    #[ignore = "requires GRIB_READER_ECCODES_HELPER"]
1612    fn generated_grib2_fixture_matches_eccodes_when_configured() {
1613        let helper = std::env::var_os("GRIB_READER_ECCODES_HELPER")
1614            .expect("GRIB_READER_ECCODES_HELPER must be set");
1615        let first = simple_field(&[1.0, 2.0, 3.0, 4.0], 0, 0);
1616        let second = Grib2FieldBuilder::new()
1617            .identification(identification())
1618            .grid(grid())
1619            .product(product(0, 2))
1620            .packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
1621            .values(&[5.0, f64::NAN, 7.0, 8.0])
1622            .build()
1623            .unwrap();
1624        let bytes = write_message([first, second]);
1625
1626        let dir = tempfile::tempdir().unwrap();
1627        let path = dir.path().join("writer-generated.grib2");
1628        std::fs::write(&path, &bytes).unwrap();
1629
1630        let output = Command::new(helper)
1631            .arg("dump")
1632            .arg(&path)
1633            .output()
1634            .unwrap();
1635        assert!(
1636            output.status.success(),
1637            "ecCodes helper failed:\nstdout:\n{}\nstderr:\n{}",
1638            String::from_utf8_lossy(&output.stdout),
1639            String::from_utf8_lossy(&output.stderr)
1640        );
1641        let reference: ReferenceDump = serde_json::from_slice(&output.stdout).unwrap();
1642        let rust = GribFile::from_bytes(bytes).unwrap();
1643
1644        assert_eq!(reference.messages.len(), 2);
1645        assert_eq!(rust.message_count(), reference.messages.len());
1646        for (index, expected) in reference.messages.iter().enumerate() {
1647            let message = rust.message(index).unwrap();
1648            let actual = message.read_flat_data_as_f64().unwrap();
1649            assert_eq!(message.edition(), expected.edition);
1650            assert_eq!(message.parameter_description(), expected.name);
1651            assert_eq!(actual.len(), expected.values.len());
1652            for (actual, expected) in actual.iter().zip(&expected.values) {
1653                match expected {
1654                    Some(expected) => assert!((actual - expected).abs() <= 1e-6),
1655                    None => assert!(actual.is_nan()),
1656                }
1657            }
1658        }
1659    }
1660}