1#![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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum PackingStrategy {
23 SimpleAuto { decimal_scale: i16 },
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum ValueOrder {
30 #[default]
32 LogicalRowMajor,
33 GribScanOrder,
35}
36
37#[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#[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#[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#[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
312pub 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(§ion);
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(¶ms.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}