Skip to main content

netcdf_writer/
lib.rs

1//! Pure-Rust NetCDF writer.
2//!
3//! The current implementation writes CDF-1, CDF-2, and CDF-5 classic-family
4//! files, plus a conservative NetCDF-4/HDF5 subset backed by `hdf5-writer`.
5//! NetCDF-4 emission is intentionally strict about metadata it cannot yet
6//! represent losslessly.
7
8use std::io::Write;
9
10#[cfg(feature = "netcdf4")]
11use hdf5_writer::{
12    AttributeBuilder as H5AttributeBuilder, ByteOrder as H5ByteOrder,
13    CompoundField as H5CompoundField, DatasetBuilder as H5DatasetBuilder, Datatype as H5Datatype,
14    EnumMember as H5EnumMember, FilterDescription as H5FilterDescription, Hdf5Builder,
15    ReferenceType as H5ReferenceType, StringEncoding as H5StringEncoding,
16    StringPadding as H5StringPadding, StringSize as H5StringSize, VarLenKind as H5VarLenKind,
17    WriteOptions as H5WriteOptions, FILTER_DEFLATE as H5_FILTER_DEFLATE,
18    FILTER_FLETCHER32 as H5_FILTER_FLETCHER32, FILTER_SHUFFLE as H5_FILTER_SHUFFLE,
19    UNLIMITED as H5_UNLIMITED,
20};
21
22pub use netcdf_core::{
23    NcAttrValue, NcAttribute, NcCompoundField, NcDimension, NcEnumMember, NcFormat, NcGroup,
24    NcIntegerValue, NcSliceInfo, NcSliceInfoElem, NcType, NcVariable, NC_FILL_BYTE, NC_FILL_CHAR,
25    NC_FILL_DOUBLE, NC_FILL_FLOAT, NC_FILL_INT, NC_FILL_INT64, NC_FILL_SHORT, NC_FILL_UBYTE,
26    NC_FILL_UINT, NC_FILL_UINT64, NC_FILL_USHORT,
27};
28
29const ABSENT: u32 = 0x0000_0000;
30const NC_DIMENSION: u32 = 0x0000_000A;
31const NC_VARIABLE: u32 = 0x0000_000B;
32const NC_ATTRIBUTE: u32 = 0x0000_000C;
33
34/// NetCDF writer errors.
35#[derive(Debug, thiserror::Error)]
36pub enum Error {
37    #[error("I/O error: {0}")]
38    Io(#[from] std::io::Error),
39
40    #[error("NetCDF core error: {0}")]
41    Core(#[from] netcdf_core::Error),
42
43    #[error("invalid definition: {0}")]
44    InvalidDefinition(String),
45
46    #[error("type mismatch: expected {expected}, got {actual}")]
47    TypeMismatch { expected: String, actual: String },
48
49    #[error("data length mismatch: expected {expected}, got {actual}")]
50    DataLengthMismatch { expected: usize, actual: usize },
51
52    /// The schema needs the NetCDF-4 data model (e.g. a type, name, or
53    /// structure the requested classic format cannot represent). Callers can
54    /// match on this and retry with [`NcWriteFormat::Nc4`].
55    #[error("requires the NetCDF-4 data model: {reason}")]
56    RequiresNetcdf4 { reason: String },
57
58    /// A value exceeds the capacity of the requested classic format (for
59    /// example a 32-bit offset or size field). Callers can match on this and
60    /// retry with a larger classic format such as CDF-5.
61    #[error("exceeds the capacity of the requested classic format: {reason}")]
62    FormatCapacityExceeded { reason: String },
63
64    #[error("unsupported write feature: {0}")]
65    UnsupportedFeature(String),
66}
67
68pub type Result<T> = std::result::Result<T, Error>;
69
70/// Requested NetCDF output format.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum NcWriteFormat {
73    /// Pick CDF-1, CDF-2, or CDF-5 from the exact schema and layout.
74    AutoClassic,
75    Classic,
76    Offset64,
77    Cdf5,
78    Nc4,
79    Nc4Classic,
80}
81
82/// NetCDF write options.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct NcWriteOptions {
85    pub format: NcWriteFormat,
86}
87
88impl Default for NcWriteOptions {
89    fn default() -> Self {
90        Self {
91            format: NcWriteFormat::AutoClassic,
92        }
93    }
94}
95
96impl NcWriteOptions {
97    pub fn classic() -> Self {
98        Self {
99            format: NcWriteFormat::Classic,
100        }
101    }
102
103    pub fn offset64() -> Self {
104        Self {
105            format: NcWriteFormat::Offset64,
106        }
107    }
108
109    pub fn cdf5() -> Self {
110        Self {
111            format: NcWriteFormat::Cdf5,
112        }
113    }
114}
115
116/// Dimension handle returned by [`NcFileBuilder::add_dimension`].
117#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub struct DimensionId(usize);
119
120/// Variable handle returned by [`NcFileBuilder::add_variable`].
121#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
122pub struct VariableId(usize);
123
124/// Rust types that can be written as NetCDF classic-family variable data.
125pub trait NcWriteType: Copy {
126    fn nc_type() -> NcType;
127    fn write_one_be(self, dst: &mut Vec<u8>);
128
129    fn write_one_le(self, dst: &mut Vec<u8>) {
130        let start = dst.len();
131        self.write_one_be(dst);
132        dst[start..].reverse();
133    }
134}
135
136/// Rust primitive types that can be used as a NetCDF variable fill value.
137///
138/// Setting a fill value writes the canonical `_FillValue` variable attribute.
139/// NetCDF-4 output also records the value as an HDF5 dataset fill value, which
140/// allows fixed-size datasets to be represented without eagerly materializing
141/// every element.
142pub trait NcFillValueType: NcWriteType {
143    fn fill_attr_value(self) -> NcAttrValue;
144}
145
146macro_rules! impl_write_type {
147    ($ty:ty, $nc_type:expr, $write:expr) => {
148        impl NcWriteType for $ty {
149            fn nc_type() -> NcType {
150                $nc_type
151            }
152
153            fn write_one_be(self, dst: &mut Vec<u8>) {
154                $write(self, dst)
155            }
156        }
157    };
158}
159
160macro_rules! impl_fill_value_type {
161    ($ty:ty, $attr:ident) => {
162        impl NcFillValueType for $ty {
163            fn fill_attr_value(self) -> NcAttrValue {
164                NcAttrValue::$attr(vec![self])
165            }
166        }
167    };
168}
169
170impl_write_type!(i8, NcType::Byte, |value: i8, dst: &mut Vec<u8>| dst
171    .push(value as u8));
172impl_write_type!(u8, NcType::UByte, |value: u8, dst: &mut Vec<u8>| dst
173    .push(value));
174impl_write_type!(i16, NcType::Short, |value: i16, dst: &mut Vec<u8>| dst
175    .extend_from_slice(&value.to_be_bytes()));
176impl_write_type!(u16, NcType::UShort, |value: u16, dst: &mut Vec<u8>| dst
177    .extend_from_slice(&value.to_be_bytes()));
178impl_write_type!(i32, NcType::Int, |value: i32, dst: &mut Vec<u8>| dst
179    .extend_from_slice(&value.to_be_bytes()));
180impl_write_type!(u32, NcType::UInt, |value: u32, dst: &mut Vec<u8>| dst
181    .extend_from_slice(&value.to_be_bytes()));
182impl_write_type!(i64, NcType::Int64, |value: i64, dst: &mut Vec<u8>| dst
183    .extend_from_slice(&value.to_be_bytes()));
184impl_write_type!(u64, NcType::UInt64, |value: u64, dst: &mut Vec<u8>| dst
185    .extend_from_slice(&value.to_be_bytes()));
186impl_write_type!(f32, NcType::Float, |value: f32, dst: &mut Vec<u8>| dst
187    .extend_from_slice(&value.to_be_bytes()));
188impl_write_type!(f64, NcType::Double, |value: f64, dst: &mut Vec<u8>| dst
189    .extend_from_slice(&value.to_be_bytes()));
190
191impl_fill_value_type!(i8, Bytes);
192impl_fill_value_type!(u8, UBytes);
193impl_fill_value_type!(i16, Shorts);
194impl_fill_value_type!(u16, UShorts);
195impl_fill_value_type!(i32, Ints);
196impl_fill_value_type!(u32, UInts);
197impl_fill_value_type!(i64, Int64s);
198impl_fill_value_type!(u64, UInt64s);
199impl_fill_value_type!(f32, Floats);
200impl_fill_value_type!(f64, Doubles);
201
202const FILL_VALUE_ATTR_NAME: &str = "_FillValue";
203
204#[derive(Debug, Clone)]
205struct DimensionDef {
206    name: String,
207    size: u64,
208    is_unlimited: bool,
209    current_size: u64,
210}
211
212#[derive(Debug, Clone)]
213struct GroupAttributeDef {
214    group_path: String,
215    attribute: NcAttribute,
216}
217
218#[derive(Debug, Clone)]
219struct VariableDef {
220    name: String,
221    dim_ids: Vec<DimensionId>,
222    dtype: NcType,
223    attributes: Vec<NcAttribute>,
224    data: Vec<u8>,
225    data_encoding: VariableDataEncoding,
226    string_values: Option<Vec<String>>,
227    vlen_values: Option<Vec<Vec<u8>>>,
228    storage: VariableStorageDef,
229    fill_value: Option<VariableFillValue>,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233enum VariableDataEncoding {
234    ClassicBigEndian,
235    Hdf5Native,
236}
237
238#[derive(Debug, Clone)]
239struct VariableFillValue {
240    classic_bytes: Vec<u8>,
241    hdf5_bytes: Vec<u8>,
242}
243
244#[derive(Debug, Clone, Default)]
245struct VariableStorageDef {
246    chunk_shape: Option<Vec<u64>>,
247    shuffle: bool,
248    deflate_level: Option<u8>,
249    fletcher32: bool,
250}
251
252impl VariableStorageDef {
253    fn has_filters(&self) -> bool {
254        self.shuffle || self.deflate_level.is_some() || self.fletcher32
255    }
256
257    fn has_nc4_options(&self) -> bool {
258        self.chunk_shape.is_some() || self.has_filters()
259    }
260}
261
262/// Builder for a single NetCDF file.
263#[derive(Debug, Clone, Default)]
264pub struct NcFileBuilder {
265    dimensions: Vec<DimensionDef>,
266    attributes: Vec<NcAttribute>,
267    group_attributes: Vec<GroupAttributeDef>,
268    variables: Vec<VariableDef>,
269}
270
271impl NcFileBuilder {
272    pub fn new() -> Self {
273        Self::default()
274    }
275
276    pub fn add_dimension(&mut self, name: impl Into<String>, size: u64) -> Result<DimensionId> {
277        let name = name.into();
278        validate_name(&name, "dimension")?;
279        self.add_dimension_def(name, size, false)
280    }
281
282    pub fn add_dimension_path(
283        &mut self,
284        path: impl Into<String>,
285        size: u64,
286    ) -> Result<DimensionId> {
287        let path = validate_path_name(path.into(), "dimension")?;
288        self.add_dimension_def(path, size, false)
289    }
290
291    pub fn add_unlimited_dimension(&mut self, name: impl Into<String>) -> Result<DimensionId> {
292        let name = name.into();
293        validate_name(&name, "dimension")?;
294        self.add_dimension_def(name, 0, true)
295    }
296
297    pub fn add_unlimited_dimension_path(&mut self, path: impl Into<String>) -> Result<DimensionId> {
298        let path = validate_path_name(path.into(), "dimension")?;
299        self.add_dimension_def(path, 0, true)
300    }
301
302    pub fn add_attribute(&mut self, name: impl Into<String>, value: NcAttrValue) -> Result<()> {
303        let name = name.into();
304        validate_name(&name, "attribute")?;
305        ensure_unique_attr(&self.attributes, &name)?;
306        self.attributes.push(NcAttribute { name, value });
307        Ok(())
308    }
309
310    pub fn add_group_attribute(
311        &mut self,
312        group_path: impl Into<String>,
313        name: impl Into<String>,
314        value: NcAttrValue,
315    ) -> Result<()> {
316        let group_path = validate_path_name(group_path.into(), "group")?;
317        let name = name.into();
318        validate_name(&name, "attribute")?;
319        if self
320            .group_attributes
321            .iter()
322            .any(|attr| attr.group_path == group_path && attr.attribute.name == name)
323        {
324            return Err(Error::InvalidDefinition(format!(
325                "duplicate attribute '{name}' on group '{group_path}'"
326            )));
327        }
328        self.group_attributes.push(GroupAttributeDef {
329            group_path,
330            attribute: NcAttribute { name, value },
331        });
332        Ok(())
333    }
334
335    pub fn add_variable<T: NcWriteType>(
336        &mut self,
337        name: impl Into<String>,
338        dimensions: &[DimensionId],
339    ) -> Result<VariableId> {
340        self.add_variable_with_type(name, dimensions, T::nc_type())
341    }
342
343    pub fn add_variable_path<T: NcWriteType>(
344        &mut self,
345        path: impl Into<String>,
346        dimensions: &[DimensionId],
347    ) -> Result<VariableId> {
348        self.add_variable_path_with_type(path, dimensions, T::nc_type())
349    }
350
351    pub fn add_char_variable(
352        &mut self,
353        name: impl Into<String>,
354        dimensions: &[DimensionId],
355    ) -> Result<VariableId> {
356        self.add_variable_with_type(name, dimensions, NcType::Char)
357    }
358
359    pub fn add_char_variable_path(
360        &mut self,
361        path: impl Into<String>,
362        dimensions: &[DimensionId],
363    ) -> Result<VariableId> {
364        self.add_variable_path_with_type(path, dimensions, NcType::Char)
365    }
366
367    pub fn add_string_variable(
368        &mut self,
369        name: impl Into<String>,
370        dimensions: &[DimensionId],
371    ) -> Result<VariableId> {
372        self.add_variable_with_type(name, dimensions, NcType::String)
373    }
374
375    pub fn add_string_variable_path(
376        &mut self,
377        path: impl Into<String>,
378        dimensions: &[DimensionId],
379    ) -> Result<VariableId> {
380        self.add_variable_path_with_type(path, dimensions, NcType::String)
381    }
382
383    pub fn add_user_defined_variable(
384        &mut self,
385        name: impl Into<String>,
386        dimensions: &[DimensionId],
387        dtype: NcType,
388    ) -> Result<VariableId> {
389        validate_supported_user_defined_type(&dtype)?;
390        self.add_variable_with_type(name, dimensions, dtype)
391    }
392
393    pub fn add_user_defined_variable_path(
394        &mut self,
395        path: impl Into<String>,
396        dimensions: &[DimensionId],
397        dtype: NcType,
398    ) -> Result<VariableId> {
399        validate_supported_user_defined_type(&dtype)?;
400        self.add_variable_path_with_type(path, dimensions, dtype)
401    }
402
403    pub fn add_variable_attribute(
404        &mut self,
405        variable: VariableId,
406        name: impl Into<String>,
407        value: NcAttrValue,
408    ) -> Result<()> {
409        let name = name.into();
410        validate_name(&name, "attribute")?;
411        let variable = self.variable_mut(variable)?;
412        ensure_unique_attr(&variable.attributes, &name)?;
413        variable.attributes.push(NcAttribute { name, value });
414        Ok(())
415    }
416
417    pub fn set_variable_fill_value<T: NcFillValueType>(
418        &mut self,
419        variable: VariableId,
420        value: T,
421    ) -> Result<()> {
422        let variable = self.variable_mut(variable)?;
423        let expected = T::nc_type();
424        if variable.dtype != expected {
425            return Err(Error::TypeMismatch {
426                expected: format!("{:?}", variable.dtype),
427                actual: format!("{expected:?}"),
428            });
429        }
430
431        let mut classic_bytes = Vec::with_capacity(expected.size()?);
432        value.write_one_be(&mut classic_bytes);
433        let mut hdf5_bytes = Vec::with_capacity(expected.size()?);
434        value.write_one_le(&mut hdf5_bytes);
435        set_variable_fill_value_metadata(
436            variable,
437            value.fill_attr_value(),
438            classic_bytes,
439            hdf5_bytes,
440        )
441    }
442
443    pub fn set_variable_chunking(
444        &mut self,
445        variable: VariableId,
446        chunk_shape: impl Into<Vec<u64>>,
447    ) -> Result<()> {
448        let variable = self.variable_mut(variable)?;
449        let chunk_shape = chunk_shape.into();
450        validate_chunk_shape_for_variable(variable, &chunk_shape)?;
451        variable.storage.chunk_shape = Some(chunk_shape);
452        Ok(())
453    }
454
455    pub fn set_variable_shuffle(&mut self, variable: VariableId, enabled: bool) -> Result<()> {
456        let variable = self.variable_mut(variable)?;
457        reject_scalar_filtered_variable(variable)?;
458        variable.storage.shuffle = enabled;
459        Ok(())
460    }
461
462    pub fn set_variable_deflate(
463        &mut self,
464        variable: VariableId,
465        level: Option<u8>,
466        shuffle: bool,
467    ) -> Result<()> {
468        if let Some(level) = level {
469            if level > 9 {
470                return Err(Error::InvalidDefinition(
471                    "deflate level must be in 0..=9".into(),
472                ));
473            }
474        }
475        let variable = self.variable_mut(variable)?;
476        reject_scalar_filtered_variable(variable)?;
477        variable.storage.deflate_level = level;
478        variable.storage.shuffle = shuffle;
479        Ok(())
480    }
481
482    pub fn set_variable_fletcher32(&mut self, variable: VariableId, enabled: bool) -> Result<()> {
483        let variable = self.variable_mut(variable)?;
484        reject_scalar_filtered_variable(variable)?;
485        variable.storage.fletcher32 = enabled;
486        Ok(())
487    }
488
489    pub fn write_variable<T: NcWriteType>(
490        &mut self,
491        variable: VariableId,
492        values: &[T],
493    ) -> Result<()> {
494        {
495            let variable = self.variable_mut(variable)?;
496            let expected = T::nc_type();
497            if variable.dtype != expected {
498                return Err(Error::TypeMismatch {
499                    expected: format!("{:?}", variable.dtype),
500                    actual: format!("{:?}", expected),
501                });
502            }
503            let mut data = Vec::with_capacity(std::mem::size_of_val(values));
504            for &value in values {
505                value.write_one_be(&mut data);
506            }
507            variable.data = data;
508            variable.data_encoding = VariableDataEncoding::ClassicBigEndian;
509            variable.string_values = None;
510            variable.vlen_values = None;
511        }
512        self.update_unlimited_extents_from_element_count(variable, values.len() as u64)
513    }
514
515    pub fn write_variable_slice<T: NcWriteType>(
516        &mut self,
517        variable: VariableId,
518        selection: &NcSliceInfo,
519        values: &[T],
520    ) -> Result<()> {
521        let variable_def = self.variable(variable)?;
522        let expected = T::nc_type();
523        if variable_def.dtype != expected {
524            return Err(Error::TypeMismatch {
525                expected: format!("{:?}", variable_def.dtype),
526                actual: format!("{expected:?}"),
527            });
528        }
529        let resolved =
530            self.resolve_variable_write_selection(variable_def, selection, expected.size()?)?;
531        if values.len() != resolved.elements {
532            return Err(Error::DataLengthMismatch {
533                expected: resolved.elements,
534                actual: values.len(),
535            });
536        }
537
538        let elem_size = expected.size()?;
539        let mut encoded = Vec::with_capacity(checked_mul_usize(
540            values.len(),
541            elem_size,
542            "variable slice byte size",
543        )?);
544        for &value in values {
545            value.write_one_be(&mut encoded);
546        }
547
548        let strides = row_major_strides(&resolved.shape, "writer slice stride")?;
549        let total_elements =
550            checked_shape_elements(&resolved.shape, "writer variable element count")?;
551        {
552            let variable_def = self.variable_mut(variable)?;
553            ensure_variable_slice_buffer(
554                variable_def,
555                &resolved.old_shape,
556                &resolved.shape,
557                total_elements,
558                elem_size,
559                resolved.can_grow,
560            )?;
561            scatter_slice_bytes(
562                &mut variable_def.data,
563                elem_size,
564                &resolved.dims,
565                &strides,
566                &encoded,
567            )?;
568        }
569        self.update_unlimited_extents_from_shape(variable, &resolved.shape)
570    }
571
572    pub fn write_char_variable(&mut self, variable: VariableId, bytes: &[u8]) -> Result<()> {
573        {
574            let variable = self.variable_mut(variable)?;
575            if variable.dtype != NcType::Char {
576                return Err(Error::TypeMismatch {
577                    expected: "Char".into(),
578                    actual: format!("{:?}", variable.dtype),
579                });
580            }
581            variable.data = bytes.to_vec();
582            variable.data_encoding = VariableDataEncoding::ClassicBigEndian;
583            variable.string_values = None;
584            variable.vlen_values = None;
585        }
586        self.update_unlimited_extents_from_element_count(variable, bytes.len() as u64)
587    }
588
589    pub fn write_char_variable_slice(
590        &mut self,
591        variable: VariableId,
592        selection: &NcSliceInfo,
593        bytes: &[u8],
594    ) -> Result<()> {
595        let variable_def = self.variable(variable)?;
596        if variable_def.dtype != NcType::Char {
597            return Err(Error::TypeMismatch {
598                expected: "Char".into(),
599                actual: format!("{:?}", variable_def.dtype),
600            });
601        }
602        let resolved = self.resolve_variable_write_selection(variable_def, selection, 1)?;
603        if bytes.len() != resolved.elements {
604            return Err(Error::DataLengthMismatch {
605                expected: resolved.elements,
606                actual: bytes.len(),
607            });
608        }
609
610        let strides = row_major_strides(&resolved.shape, "writer char slice stride")?;
611        let total_elements =
612            checked_shape_elements(&resolved.shape, "writer char variable element count")?;
613        {
614            let variable_def = self.variable_mut(variable)?;
615            ensure_variable_slice_buffer(
616                variable_def,
617                &resolved.old_shape,
618                &resolved.shape,
619                total_elements,
620                1,
621                resolved.can_grow,
622            )?;
623            scatter_slice_bytes(&mut variable_def.data, 1, &resolved.dims, &strides, bytes)?;
624        }
625        self.update_unlimited_extents_from_shape(variable, &resolved.shape)
626    }
627
628    pub fn write_char_variable_strings<S: AsRef<str>>(
629        &mut self,
630        variable: VariableId,
631        values: &[S],
632    ) -> Result<()> {
633        let (name, width, inferred_unlimited) = {
634            let variable_def = self.variable(variable)?;
635            if variable_def.dtype != NcType::Char {
636                return Err(Error::TypeMismatch {
637                    expected: "Char".into(),
638                    actual: format!("{:?}", variable_def.dtype),
639                });
640            }
641            let inferred_unlimited =
642                self.validate_char_string_value_count(variable_def, values.len())?;
643            (
644                variable_def.name.clone(),
645                self.char_string_axis_width(variable_def)?,
646                inferred_unlimited,
647            )
648        };
649        let encoded = encode_char_string_values(&name, width, values)?;
650        self.write_char_variable(variable, &encoded)?;
651        if let Some((dimension, size)) = inferred_unlimited {
652            if size > self.dimensions[dimension.0].current_size {
653                self.dimensions[dimension.0].current_size = size;
654            }
655        }
656        Ok(())
657    }
658
659    pub fn write_char_variable_strings_slice<S: AsRef<str>>(
660        &mut self,
661        variable: VariableId,
662        selection: &NcSliceInfo,
663        values: &[S],
664    ) -> Result<()> {
665        let (name, width, resolved) = {
666            let variable_def = self.variable(variable)?;
667            if variable_def.dtype != NcType::Char {
668                return Err(Error::TypeMismatch {
669                    expected: "Char".into(),
670                    actual: format!("{:?}", variable_def.dtype),
671                });
672            }
673            (
674                variable_def.name.clone(),
675                self.char_string_axis_width(variable_def)?,
676                self.resolve_char_string_write_selection(variable_def, selection)?,
677            )
678        };
679        if values.len() != resolved.elements {
680            return Err(Error::DataLengthMismatch {
681                expected: resolved.elements,
682                actual: values.len(),
683            });
684        }
685
686        let encoded = encode_char_string_values(&name, width, values)?;
687        let mut selections = selection.selections.clone();
688        selections.push(NcSliceInfoElem::Slice {
689            start: 0,
690            end: u64::try_from(width).map_err(|_| {
691                Error::InvalidDefinition("char string width exceeds u64 capacity".into())
692            })?,
693            step: 1,
694        });
695        self.write_char_variable_slice(variable, &NcSliceInfo { selections }, &encoded)
696    }
697
698    pub fn write_user_defined_variable_bytes(
699        &mut self,
700        variable: VariableId,
701        bytes: &[u8],
702    ) -> Result<()> {
703        let elem_size = {
704            let variable = self.variable_mut(variable)?;
705            validate_fixed_width_user_defined_type(&variable.dtype)?;
706            let elem_size = variable.dtype.size()?;
707            variable.data = bytes.to_vec();
708            variable.data_encoding = VariableDataEncoding::Hdf5Native;
709            variable.string_values = None;
710            variable.vlen_values = None;
711            elem_size
712        };
713        if bytes.len() % elem_size != 0 {
714            return Err(Error::DataLengthMismatch {
715                expected: bytes.len() + (elem_size - bytes.len() % elem_size),
716                actual: bytes.len(),
717            });
718        }
719        self.update_unlimited_extents_from_element_count(variable, (bytes.len() / elem_size) as u64)
720    }
721
722    pub fn write_user_defined_variable_slice_bytes(
723        &mut self,
724        variable: VariableId,
725        selection: &NcSliceInfo,
726        bytes: &[u8],
727    ) -> Result<()> {
728        let elem_size = {
729            let variable = self.variable(variable)?;
730            validate_fixed_width_user_defined_type(&variable.dtype)?;
731            variable.dtype.size()?
732        };
733        self.write_native_variable_slice_bytes(variable, selection, bytes, elem_size)
734    }
735
736    pub fn write_enum_variable(
737        &mut self,
738        variable: VariableId,
739        values: &[NcIntegerValue],
740    ) -> Result<()> {
741        {
742            let variable = self.variable_mut(variable)?;
743            let NcType::Enum { base, .. } = &variable.dtype else {
744                return Err(Error::TypeMismatch {
745                    expected: "Enum".into(),
746                    actual: format!("{:?}", variable.dtype),
747                });
748            };
749            let mut data = Vec::with_capacity(values.len() * base.size()?);
750            for &value in values {
751                data.extend_from_slice(&nc_enum_value_to_le_bytes(base, value)?);
752            }
753            variable.data = data;
754            variable.data_encoding = VariableDataEncoding::Hdf5Native;
755            variable.string_values = None;
756            variable.vlen_values = None;
757        }
758        self.update_unlimited_extents_from_element_count(variable, values.len() as u64)
759    }
760
761    pub fn write_enum_variable_slice(
762        &mut self,
763        variable: VariableId,
764        selection: &NcSliceInfo,
765        values: &[NcIntegerValue],
766    ) -> Result<()> {
767        let base = {
768            let variable = self.variable(variable)?;
769            let NcType::Enum { base, .. } = &variable.dtype else {
770                return Err(Error::TypeMismatch {
771                    expected: "Enum".into(),
772                    actual: format!("{:?}", variable.dtype),
773                });
774            };
775            base.clone()
776        };
777        let elem_size = base.size()?;
778        let mut data = Vec::with_capacity(checked_mul_usize(
779            values.len(),
780            elem_size,
781            "enum slice byte size",
782        )?);
783        for &value in values {
784            data.extend_from_slice(&nc_enum_value_to_le_bytes(&base, value)?);
785        }
786        self.write_native_variable_slice_bytes(variable, selection, &data, elem_size)
787    }
788
789    pub fn write_opaque_variable<S: AsRef<[u8]>>(
790        &mut self,
791        variable: VariableId,
792        values: &[S],
793    ) -> Result<()> {
794        {
795            let variable = self.variable_mut(variable)?;
796            let NcType::Opaque { size, .. } = &variable.dtype else {
797                return Err(Error::TypeMismatch {
798                    expected: "Opaque".into(),
799                    actual: format!("{:?}", variable.dtype),
800                });
801            };
802            let size = usize::try_from(*size).map_err(|_| {
803                Error::InvalidDefinition("opaque element size exceeds platform usize".into())
804            })?;
805            let mut data = Vec::with_capacity(values.len() * size);
806            for value in values {
807                let value = value.as_ref();
808                if value.len() != size {
809                    return Err(Error::DataLengthMismatch {
810                        expected: size,
811                        actual: value.len(),
812                    });
813                }
814                data.extend_from_slice(value);
815            }
816            variable.data = data;
817            variable.data_encoding = VariableDataEncoding::Hdf5Native;
818            variable.string_values = None;
819            variable.vlen_values = None;
820        }
821        self.update_unlimited_extents_from_element_count(variable, values.len() as u64)
822    }
823
824    pub fn write_opaque_variable_slice<S: AsRef<[u8]>>(
825        &mut self,
826        variable: VariableId,
827        selection: &NcSliceInfo,
828        values: &[S],
829    ) -> Result<()> {
830        let size = {
831            let variable = self.variable(variable)?;
832            let NcType::Opaque { size, .. } = &variable.dtype else {
833                return Err(Error::TypeMismatch {
834                    expected: "Opaque".into(),
835                    actual: format!("{:?}", variable.dtype),
836                });
837            };
838            usize::try_from(*size).map_err(|_| {
839                Error::InvalidDefinition("opaque element size exceeds platform usize".into())
840            })?
841        };
842        let mut data = Vec::with_capacity(checked_mul_usize(
843            values.len(),
844            size,
845            "opaque slice byte size",
846        )?);
847        for value in values {
848            let value = value.as_ref();
849            if value.len() != size {
850                return Err(Error::DataLengthMismatch {
851                    expected: size,
852                    actual: value.len(),
853                });
854            }
855            data.extend_from_slice(value);
856        }
857        self.write_native_variable_slice_bytes(variable, selection, &data, size)
858    }
859
860    pub fn write_array_variable<T: NcWriteType>(
861        &mut self,
862        variable: VariableId,
863        values: &[T],
864    ) -> Result<()> {
865        let variable_elements = {
866            let variable = self.variable_mut(variable)?;
867            let NcType::Array { base, .. } = &variable.dtype else {
868                return Err(Error::TypeMismatch {
869                    expected: "Array".into(),
870                    actual: format!("{:?}", variable.dtype),
871                });
872            };
873            let expected = T::nc_type();
874            if base.as_ref() != &expected {
875                return Err(Error::TypeMismatch {
876                    expected: format!("{:?}", base),
877                    actual: format!("{expected:?}"),
878                });
879            }
880            let mut data = Vec::with_capacity(std::mem::size_of_val(values));
881            for &value in values {
882                value.write_one_le(&mut data);
883            }
884            let elem_size = variable.dtype.size()?;
885            if data.len() % elem_size != 0 {
886                return Err(Error::DataLengthMismatch {
887                    expected: data.len() + (elem_size - data.len() % elem_size),
888                    actual: data.len(),
889                });
890            }
891            let variable_elements = (data.len() / elem_size) as u64;
892            variable.data = data;
893            variable.data_encoding = VariableDataEncoding::Hdf5Native;
894            variable.string_values = None;
895            variable.vlen_values = None;
896            variable_elements
897        };
898        self.update_unlimited_extents_from_element_count(variable, variable_elements)
899    }
900
901    pub fn write_array_variable_slice<T: NcWriteType>(
902        &mut self,
903        variable: VariableId,
904        selection: &NcSliceInfo,
905        values: &[T],
906    ) -> Result<()> {
907        let elem_size = {
908            let variable = self.variable(variable)?;
909            let NcType::Array { base, .. } = &variable.dtype else {
910                return Err(Error::TypeMismatch {
911                    expected: "Array".into(),
912                    actual: format!("{:?}", variable.dtype),
913                });
914            };
915            let expected = T::nc_type();
916            if base.as_ref() != &expected {
917                return Err(Error::TypeMismatch {
918                    expected: format!("{:?}", base),
919                    actual: format!("{expected:?}"),
920                });
921            }
922            variable.dtype.size()?
923        };
924        let mut data = Vec::with_capacity(checked_mul_usize(
925            values.len(),
926            T::nc_type().size()?,
927            "array slice byte size",
928        )?);
929        for &value in values {
930            value.write_one_le(&mut data);
931        }
932        self.write_native_variable_slice_bytes(variable, selection, &data, elem_size)
933    }
934
935    pub fn write_vlen_variable_bytes<S: AsRef<[u8]>>(
936        &mut self,
937        variable: VariableId,
938        values: &[S],
939    ) -> Result<()> {
940        {
941            let variable = self.variable_mut(variable)?;
942            let NcType::VLen { base } = &variable.dtype else {
943                return Err(Error::TypeMismatch {
944                    expected: "VLen".into(),
945                    actual: format!("{:?}", variable.dtype),
946                });
947            };
948            validate_vlen_base_nc4_type(base)?;
949            let base_size = base.size()?;
950            let mut sequences = Vec::with_capacity(values.len());
951            for value in values {
952                let value = value.as_ref();
953                if value.len() % base_size != 0 {
954                    return Err(Error::InvalidDefinition(format!(
955                        "vlen sequence byte length {} is not a multiple of base element size {base_size}",
956                        value.len()
957                    )));
958                }
959                sequences.push(value.to_vec());
960            }
961            variable.data.clear();
962            variable.data_encoding = VariableDataEncoding::Hdf5Native;
963            variable.string_values = None;
964            variable.vlen_values = Some(sequences);
965        }
966        self.update_unlimited_extents_from_element_count(variable, values.len() as u64)
967    }
968
969    pub fn write_vlen_variable<T: NcWriteType>(
970        &mut self,
971        variable: VariableId,
972        values: &[Vec<T>],
973    ) -> Result<()> {
974        {
975            let variable_def = self.variable_mut(variable)?;
976            let NcType::VLen { base } = &variable_def.dtype else {
977                return Err(Error::TypeMismatch {
978                    expected: "VLen".into(),
979                    actual: format!("{:?}", variable_def.dtype),
980                });
981            };
982            let expected = T::nc_type();
983            if base.as_ref() != &expected {
984                return Err(Error::TypeMismatch {
985                    expected: format!("{:?}", base),
986                    actual: format!("{expected:?}"),
987                });
988            }
989
990            let mut sequences = Vec::with_capacity(values.len());
991            for sequence in values {
992                let mut bytes = Vec::with_capacity(std::mem::size_of_val(sequence.as_slice()));
993                for &value in sequence {
994                    value.write_one_le(&mut bytes);
995                }
996                sequences.push(bytes);
997            }
998            variable_def.data.clear();
999            variable_def.data_encoding = VariableDataEncoding::Hdf5Native;
1000            variable_def.string_values = None;
1001            variable_def.vlen_values = Some(sequences);
1002        }
1003        self.update_unlimited_extents_from_element_count(variable, values.len() as u64)
1004    }
1005
1006    pub fn write_vlen_variable_slice_bytes<S: AsRef<[u8]>>(
1007        &mut self,
1008        variable: VariableId,
1009        selection: &NcSliceInfo,
1010        values: &[S],
1011    ) -> Result<()> {
1012        let (resolved, base_size) = {
1013            let variable_def = self.variable(variable)?;
1014            let NcType::VLen { base } = &variable_def.dtype else {
1015                return Err(Error::TypeMismatch {
1016                    expected: "VLen".into(),
1017                    actual: format!("{:?}", variable_def.dtype),
1018                });
1019            };
1020            validate_vlen_base_nc4_type(base)?;
1021            (
1022                self.resolve_variable_write_selection(variable_def, selection, 1)?,
1023                base.size()?,
1024            )
1025        };
1026        if values.len() != resolved.elements {
1027            return Err(Error::DataLengthMismatch {
1028                expected: resolved.elements,
1029                actual: values.len(),
1030            });
1031        }
1032
1033        let mut sequences = Vec::with_capacity(values.len());
1034        for value in values {
1035            let value = value.as_ref();
1036            if value.len() % base_size != 0 {
1037                return Err(Error::InvalidDefinition(format!(
1038                    "vlen sequence byte length {} is not a multiple of base element size {base_size}",
1039                    value.len()
1040                )));
1041            }
1042            sequences.push(value.to_vec());
1043        }
1044
1045        let strides = row_major_strides(&resolved.shape, "writer vlen slice stride")?;
1046        let total_elements =
1047            checked_shape_elements(&resolved.shape, "writer vlen variable element count")?;
1048        {
1049            let variable_def = self.variable_mut(variable)?;
1050            ensure_variable_vlen_slice_buffer(
1051                variable_def,
1052                &resolved.old_shape,
1053                &resolved.shape,
1054                total_elements,
1055                resolved.can_grow,
1056            )?;
1057            let vlen_values = variable_def.vlen_values.as_mut().ok_or_else(|| {
1058                Error::InvalidDefinition(format!(
1059                    "vlen variable '{}' has no initialized sequence data",
1060                    variable_def.name
1061                ))
1062            })?;
1063            scatter_slice_values(vlen_values, &resolved.dims, &strides, &sequences)?;
1064        }
1065        self.update_unlimited_extents_from_shape(variable, &resolved.shape)
1066    }
1067
1068    pub fn write_vlen_variable_slice<T: NcWriteType>(
1069        &mut self,
1070        variable: VariableId,
1071        selection: &NcSliceInfo,
1072        values: &[Vec<T>],
1073    ) -> Result<()> {
1074        let resolved = {
1075            let variable_def = self.variable(variable)?;
1076            let NcType::VLen { base } = &variable_def.dtype else {
1077                return Err(Error::TypeMismatch {
1078                    expected: "VLen".into(),
1079                    actual: format!("{:?}", variable_def.dtype),
1080                });
1081            };
1082            let expected = T::nc_type();
1083            if base.as_ref() != &expected {
1084                return Err(Error::TypeMismatch {
1085                    expected: format!("{:?}", base),
1086                    actual: format!("{expected:?}"),
1087                });
1088            }
1089            self.resolve_variable_write_selection(variable_def, selection, 1)?
1090        };
1091        if values.len() != resolved.elements {
1092            return Err(Error::DataLengthMismatch {
1093                expected: resolved.elements,
1094                actual: values.len(),
1095            });
1096        }
1097
1098        let mut sequences = Vec::with_capacity(values.len());
1099        for sequence in values {
1100            let mut bytes = Vec::with_capacity(std::mem::size_of_val(sequence.as_slice()));
1101            for &value in sequence {
1102                value.write_one_le(&mut bytes);
1103            }
1104            sequences.push(bytes);
1105        }
1106
1107        let strides = row_major_strides(&resolved.shape, "writer vlen slice stride")?;
1108        let total_elements =
1109            checked_shape_elements(&resolved.shape, "writer vlen variable element count")?;
1110        {
1111            let variable_def = self.variable_mut(variable)?;
1112            ensure_variable_vlen_slice_buffer(
1113                variable_def,
1114                &resolved.old_shape,
1115                &resolved.shape,
1116                total_elements,
1117                resolved.can_grow,
1118            )?;
1119            let vlen_values = variable_def.vlen_values.as_mut().ok_or_else(|| {
1120                Error::InvalidDefinition(format!(
1121                    "vlen variable '{}' has no initialized sequence data",
1122                    variable_def.name
1123                ))
1124            })?;
1125            scatter_slice_values(vlen_values, &resolved.dims, &strides, &sequences)?;
1126        }
1127        self.update_unlimited_extents_from_shape(variable, &resolved.shape)
1128    }
1129
1130    pub fn write_string_variable<S: AsRef<str>>(
1131        &mut self,
1132        variable: VariableId,
1133        values: &[S],
1134    ) -> Result<()> {
1135        {
1136            let variable = self.variable_mut(variable)?;
1137            if variable.dtype != NcType::String {
1138                return Err(Error::TypeMismatch {
1139                    expected: "String".into(),
1140                    actual: format!("{:?}", variable.dtype),
1141                });
1142            }
1143
1144            let mut strings = Vec::with_capacity(values.len());
1145            for value in values {
1146                let value = value.as_ref();
1147                if value.as_bytes().contains(&0) {
1148                    return Err(Error::InvalidDefinition(
1149                        "NC_STRING variable values cannot contain NUL bytes".into(),
1150                    ));
1151                }
1152                strings.push(value.to_string());
1153            }
1154            variable.data.clear();
1155            variable.data_encoding = VariableDataEncoding::Hdf5Native;
1156            variable.string_values = Some(strings);
1157            variable.vlen_values = None;
1158        }
1159        self.update_unlimited_extents_from_element_count(variable, values.len() as u64)
1160    }
1161
1162    pub fn write_string_variable_slice<S: AsRef<str>>(
1163        &mut self,
1164        variable: VariableId,
1165        selection: &NcSliceInfo,
1166        values: &[S],
1167    ) -> Result<()> {
1168        let variable_def = self.variable(variable)?;
1169        if variable_def.dtype != NcType::String {
1170            return Err(Error::TypeMismatch {
1171                expected: "String".into(),
1172                actual: format!("{:?}", variable_def.dtype),
1173            });
1174        }
1175        let resolved = self.resolve_variable_write_selection(variable_def, selection, 1)?;
1176        if values.len() != resolved.elements {
1177            return Err(Error::DataLengthMismatch {
1178                expected: resolved.elements,
1179                actual: values.len(),
1180            });
1181        }
1182
1183        let mut strings = Vec::with_capacity(values.len());
1184        for value in values {
1185            let value = value.as_ref();
1186            if value.as_bytes().contains(&0) {
1187                return Err(Error::InvalidDefinition(
1188                    "NC_STRING variable values cannot contain NUL bytes".into(),
1189                ));
1190            }
1191            strings.push(value.to_string());
1192        }
1193
1194        let strides = row_major_strides(&resolved.shape, "writer string slice stride")?;
1195        let total_elements =
1196            checked_shape_elements(&resolved.shape, "writer string variable element count")?;
1197        {
1198            let variable_def = self.variable_mut(variable)?;
1199            ensure_variable_string_slice_buffer(
1200                variable_def,
1201                &resolved.old_shape,
1202                &resolved.shape,
1203                total_elements,
1204                resolved.can_grow,
1205            )?;
1206            let string_values = variable_def.string_values.as_mut().ok_or_else(|| {
1207                Error::InvalidDefinition(format!(
1208                    "NC_STRING variable '{}' has no initialized string data",
1209                    variable_def.name
1210                ))
1211            })?;
1212            scatter_slice_values(string_values, &resolved.dims, &strides, &strings)?;
1213        }
1214        self.update_unlimited_extents_from_shape(variable, &resolved.shape)
1215    }
1216
1217    pub fn write<W: Write>(&self, mut writer: W, options: NcWriteOptions) -> Result<NcFormat> {
1218        if options.format == NcWriteFormat::AutoClassic {
1219            return self.write_auto_classic(&mut writer);
1220        }
1221
1222        let format = self.select_format(options)?;
1223        match format {
1224            NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5 => {
1225                let plan = ClassicWritePlan::build(self, format)?;
1226                plan.write(&mut writer)?;
1227                Ok(format)
1228            }
1229            NcFormat::Nc4 | NcFormat::Nc4Classic => self.write_nc4(&mut writer, format),
1230        }
1231    }
1232
1233    pub fn to_vec(&self, options: NcWriteOptions) -> Result<(NcFormat, Vec<u8>)> {
1234        let mut data = Vec::new();
1235        let format = self.write(&mut data, options)?;
1236        Ok((format, data))
1237    }
1238
1239    fn write_auto_classic(&self, writer: &mut impl Write) -> Result<NcFormat> {
1240        if self.requires_cdf5() {
1241            self.validate_for_format(NcFormat::Cdf5)?;
1242            let plan = ClassicWritePlan::build(self, NcFormat::Cdf5)?;
1243            plan.write(writer)?;
1244            return Ok(NcFormat::Cdf5);
1245        }
1246
1247        match ClassicWritePlan::build(self, NcFormat::Classic) {
1248            Ok(plan) => {
1249                plan.write(writer)?;
1250                Ok(NcFormat::Classic)
1251            }
1252            Err(classic_err) => match ClassicWritePlan::build(self, NcFormat::Offset64) {
1253                Ok(plan) => {
1254                    plan.write(writer)?;
1255                    Ok(NcFormat::Offset64)
1256                }
1257                Err(_) => Err(classic_err),
1258            },
1259        }
1260    }
1261
1262    #[cfg(feature = "netcdf4")]
1263    fn write_nc4(&self, writer: &mut impl Write, format: NcFormat) -> Result<NcFormat> {
1264        self.validate_for_nc4_bridge(format)?;
1265        let hdf5_plan = self.build_hdf5_plan(format)?;
1266        // Encode straight to bytes and write them to the caller's sink, rather
1267        // than routing through an intermediate seekable in-memory writer.
1268        let bytes = hdf5_plan
1269            .encode(H5WriteOptions::default())
1270            .map_err(hdf5_error_to_unsupported)?;
1271        writer.write_all(&bytes)?;
1272        Ok(format)
1273    }
1274
1275    #[cfg(not(feature = "netcdf4"))]
1276    fn write_nc4(&self, _writer: &mut impl Write, _format: NcFormat) -> Result<NcFormat> {
1277        Err(Error::UnsupportedFeature(
1278            "NetCDF-4 writing requires the netcdf4 feature".into(),
1279        ))
1280    }
1281
1282    #[cfg(feature = "netcdf4")]
1283    fn validate_for_nc4_bridge(&self, format: NcFormat) -> Result<()> {
1284        if format == NcFormat::Nc4Classic {
1285            validate_nc4_classic_model(self)?;
1286            for variable in &self.variables {
1287                validate_classic_type(&variable.dtype)?;
1288                for attr in &variable.attributes {
1289                    validate_nc4_classic_attr_value(&attr.value)?;
1290                }
1291            }
1292            for attr in &self.attributes {
1293                validate_nc4_classic_attr_value(&attr.value)?;
1294            }
1295        }
1296        let dimension_sizes = self.nc4_dimension_sizes()?;
1297        for variable in &self.variables {
1298            validate_nc4_variable_data(variable, &dimension_sizes)?;
1299        }
1300        for (dim_id, dimension) in self.dimensions.iter().enumerate() {
1301            let dimension_id = DimensionId(dim_id);
1302            if self
1303                .coordinate_variable_for_dimension(dimension_id)
1304                .is_none()
1305                && self
1306                    .variables
1307                    .iter()
1308                    .any(|variable| variable.name == dimension.name)
1309            {
1310                return Err(Error::UnsupportedFeature(format!(
1311                    "NetCDF-4 dimension '{}' needs a hidden dimension-scale dataset, but a scalar variable already uses that name",
1312                    dimension.name
1313                )));
1314            }
1315        }
1316        Ok(())
1317    }
1318
1319    #[cfg(feature = "netcdf4")]
1320    fn build_hdf5_plan(&self, format: NcFormat) -> Result<hdf5_writer::Hdf5WritePlan> {
1321        let mut builder = Hdf5Builder::new();
1322        let dimension_sizes = self.nc4_dimension_sizes()?;
1323        if format == NcFormat::Nc4Classic {
1324            builder = builder.attribute(
1325                H5AttributeBuilder::scalar("_nc3_strict", 1_i32)
1326                    .map_err(hdf5_error_to_unsupported)?,
1327            );
1328        }
1329        for attr in &self.attributes {
1330            builder = builder.attribute(nc_attr_to_hdf5(attr)?);
1331        }
1332        for group_attr in &self.group_attributes {
1333            builder = builder.group_attribute(
1334                group_attr.group_path.as_str(),
1335                nc_attr_to_hdf5(&group_attr.attribute)?,
1336            );
1337        }
1338
1339        for (dim_id, dimension) in self.dimensions.iter().enumerate() {
1340            if self
1341                .coordinate_variable_for_dimension(DimensionId(dim_id))
1342                .is_some()
1343            {
1344                continue;
1345            }
1346            let size = dimension_sizes[dim_id];
1347            let zeros = vec![0_i32; checked_usize(size, "dimension scale size")?];
1348            let mut dataset = H5DatasetBuilder::typed_data(&dimension.name, vec![size], &zeros)
1349                .map_err(hdf5_error_to_unsupported)?
1350                .attribute(H5AttributeBuilder::fixed_string("CLASS", "DIMENSION_SCALE"))
1351                .attribute(H5AttributeBuilder::fixed_string(
1352                    "NAME",
1353                    format!(
1354                        "This is a netCDF dimension but not a netCDF variable. {}",
1355                        path_leaf_name(&dimension.name)
1356                    ),
1357                ))
1358                .attribute(
1359                    H5AttributeBuilder::scalar("_Netcdf4Dimid", dim_id as i32)
1360                        .map_err(hdf5_error_to_unsupported)?,
1361                );
1362            if let Some(reference_list) = self.reference_list_attribute(DimensionId(dim_id))? {
1363                dataset = dataset.attribute(reference_list);
1364            }
1365            if dimension.is_unlimited {
1366                dataset = dataset
1367                    .max_shape(vec![H5_UNLIMITED])
1368                    .chunked(nc4_default_chunk_shape(
1369                        &[size],
1370                        std::mem::size_of::<i32>(),
1371                    )?);
1372            }
1373            builder = builder.dataset(dataset);
1374        }
1375
1376        for variable in &self.variables {
1377            let shape = variable
1378                .dim_ids
1379                .iter()
1380                .map(|id| dimension_sizes[id.0])
1381                .collect::<Vec<_>>();
1382            let mut dataset = if variable.dtype == NcType::String {
1383                let values = variable.string_values.as_ref().ok_or_else(|| {
1384                    Error::InvalidDefinition(format!(
1385                        "NC_STRING variable '{}' has no string data",
1386                        variable.name
1387                    ))
1388                })?;
1389                H5DatasetBuilder::vlen_string_data(&variable.name, shape.clone(), values)
1390                    .map_err(hdf5_error_to_unsupported)?
1391            } else if let NcType::VLen { base } = &variable.dtype {
1392                let values = variable.vlen_values.as_ref().ok_or_else(|| {
1393                    Error::InvalidDefinition(format!(
1394                        "NC_VLEN variable '{}' has no sequence data",
1395                        variable.name
1396                    ))
1397                })?;
1398                H5DatasetBuilder::vlen_sequence_data(
1399                    &variable.name,
1400                    nc_type_to_hdf5(base)?,
1401                    shape.clone(),
1402                    values.clone(),
1403                )
1404                .map_err(hdf5_error_to_unsupported)?
1405            } else {
1406                let data = match variable.data_encoding {
1407                    VariableDataEncoding::ClassicBigEndian => {
1408                        convert_classic_be_data_to_hdf5_le(&variable.dtype, &variable.data)?
1409                    }
1410                    VariableDataEncoding::Hdf5Native => variable.data.clone(),
1411                };
1412                let dataset = H5DatasetBuilder::new(
1413                    &variable.name,
1414                    nc_type_to_hdf5(&variable.dtype)?,
1415                    shape.clone(),
1416                );
1417                if data.is_empty() && variable.fill_value.is_some() {
1418                    dataset
1419                } else {
1420                    dataset.raw_data(data)
1421                }
1422            };
1423            if let Some(fill_value) = &variable.fill_value {
1424                dataset = dataset.fill_value(fill_value.hdf5_bytes.clone());
1425            }
1426            let chunk_shape = nc4_variable_chunk_shape(
1427                variable,
1428                &shape,
1429                self.nc4_variable_max_shape(variable).is_some(),
1430            )?;
1431            if let Some(max_shape) = self.nc4_variable_max_shape(variable) {
1432                dataset = dataset.max_shape(max_shape);
1433            }
1434            if let Some(chunk_shape) = chunk_shape {
1435                dataset = dataset.chunked(chunk_shape);
1436            }
1437            for filter in nc4_variable_filters(variable) {
1438                dataset = dataset.filter(filter);
1439            }
1440
1441            if self.variable_is_coordinate_scale(variable)? {
1442                let dim_id = variable.dim_ids[0];
1443                dataset = dataset
1444                    .attribute(H5AttributeBuilder::fixed_string("CLASS", "DIMENSION_SCALE"))
1445                    .attribute(H5AttributeBuilder::fixed_string(
1446                        "NAME",
1447                        path_leaf_name(&self.dimensions[dim_id.0].name),
1448                    ))
1449                    .attribute(
1450                        H5AttributeBuilder::scalar("_Netcdf4Dimid", dim_id.0 as i32)
1451                            .map_err(hdf5_error_to_unsupported)?,
1452                    );
1453                if let Some(reference_list) = self.reference_list_attribute(dim_id)? {
1454                    dataset = dataset.attribute(reference_list);
1455                }
1456            } else if variable.dim_ids.is_empty() {
1457                dataset = dataset.attribute(empty_dimension_list_attribute());
1458            } else {
1459                dataset = dataset.attribute(self.dimension_list_attribute(variable)?);
1460            }
1461
1462            for attr in &variable.attributes {
1463                dataset = dataset.attribute(nc_attr_to_hdf5(attr)?);
1464            }
1465
1466            builder = builder.dataset(dataset);
1467        }
1468
1469        builder.into_plan().map_err(hdf5_error_to_unsupported)
1470    }
1471
1472    #[cfg(feature = "netcdf4")]
1473    fn nc4_dimension_sizes(&self) -> Result<Vec<u64>> {
1474        infer_nc4_dimension_sizes(self)
1475    }
1476
1477    #[cfg(feature = "netcdf4")]
1478    fn nc4_variable_max_shape(&self, variable: &VariableDef) -> Option<Vec<u64>> {
1479        variable
1480            .dim_ids
1481            .iter()
1482            .any(|id| self.dimensions[id.0].is_unlimited)
1483            .then(|| {
1484                variable
1485                    .dim_ids
1486                    .iter()
1487                    .map(|id| {
1488                        let dimension = &self.dimensions[id.0];
1489                        if dimension.is_unlimited {
1490                            H5_UNLIMITED
1491                        } else {
1492                            dimension.size
1493                        }
1494                    })
1495                    .collect()
1496            })
1497    }
1498
1499    #[cfg(feature = "netcdf4")]
1500    fn variable_is_coordinate_scale(&self, variable: &VariableDef) -> Result<bool> {
1501        Ok(match variable.dim_ids.as_slice() {
1502            [dim_id] => self.dimension(*dim_id)?.name == variable.name,
1503            _ => false,
1504        })
1505    }
1506
1507    #[cfg(feature = "netcdf4")]
1508    fn coordinate_variable_for_dimension(&self, dimension: DimensionId) -> Option<&VariableDef> {
1509        let dim = self.dimensions.get(dimension.0)?;
1510        self.variables.iter().find(|variable| {
1511            variable.name == dim.name && variable.dim_ids.as_slice() == [dimension]
1512        })
1513    }
1514
1515    /// Build the `REFERENCE_LIST` back-reference attribute for a dimension
1516    /// scale: one entry per (variable, dimension position) that references the
1517    /// dimension, excluding the scale's own coordinate variable. Returns `None`
1518    /// when no data variable references the dimension (netcdf-c omits the
1519    /// attribute in that case).
1520    #[cfg(feature = "netcdf4")]
1521    fn reference_list_attribute(&self, dim_id: DimensionId) -> Result<Option<H5AttributeBuilder>> {
1522        let dim_name = &self.dimensions[dim_id.0].name;
1523        let mut entries = Vec::new();
1524        for variable in &self.variables {
1525            let is_scale_itself =
1526                variable.name == *dim_name && variable.dim_ids.as_slice() == [dim_id];
1527            if is_scale_itself {
1528                continue;
1529            }
1530            for (position, vid) in variable.dim_ids.iter().enumerate() {
1531                if *vid == dim_id {
1532                    entries.push((variable.name.clone(), position as u32));
1533                }
1534            }
1535        }
1536        Ok((!entries.is_empty())
1537            .then(|| H5AttributeBuilder::object_reference_list("REFERENCE_LIST", entries)))
1538    }
1539
1540    #[cfg(feature = "netcdf4")]
1541    fn dimension_list_attribute(&self, variable: &VariableDef) -> Result<H5AttributeBuilder> {
1542        let target_sequences = variable
1543            .dim_ids
1544            .iter()
1545            .map(|dimension| Ok(vec![self.dimension(*dimension)?.name.clone()]))
1546            .collect::<Result<Vec<_>>>()?;
1547        Ok(H5AttributeBuilder::vlen_object_references(
1548            "DIMENSION_LIST",
1549            target_sequences,
1550        ))
1551    }
1552
1553    fn add_dimension_def(
1554        &mut self,
1555        name: String,
1556        size: u64,
1557        is_unlimited: bool,
1558    ) -> Result<DimensionId> {
1559        if !is_unlimited && size == 0 {
1560            return Err(Error::InvalidDefinition(
1561                "fixed dimensions must have non-zero size; use add_unlimited_dimension".into(),
1562            ));
1563        }
1564        self.ensure_unique_dimension(&name)?;
1565        let id = DimensionId(self.dimensions.len());
1566        self.dimensions.push(DimensionDef {
1567            name,
1568            size,
1569            is_unlimited,
1570            current_size: if is_unlimited { 0 } else { size },
1571        });
1572        Ok(id)
1573    }
1574
1575    fn add_variable_with_type(
1576        &mut self,
1577        name: impl Into<String>,
1578        dimensions: &[DimensionId],
1579        dtype: NcType,
1580    ) -> Result<VariableId> {
1581        let name = name.into();
1582        validate_name(&name, "variable")?;
1583        self.add_variable_def(name, dimensions, dtype)
1584    }
1585
1586    fn add_variable_path_with_type(
1587        &mut self,
1588        path: impl Into<String>,
1589        dimensions: &[DimensionId],
1590        dtype: NcType,
1591    ) -> Result<VariableId> {
1592        let path = validate_path_name(path.into(), "variable")?;
1593        self.add_variable_def(path, dimensions, dtype)
1594    }
1595
1596    fn add_variable_def(
1597        &mut self,
1598        name: String,
1599        dimensions: &[DimensionId],
1600        dtype: NcType,
1601    ) -> Result<VariableId> {
1602        if self.variables.iter().any(|v| v.name == name) {
1603            return Err(Error::InvalidDefinition(format!(
1604                "duplicate variable '{name}'"
1605            )));
1606        }
1607        for dim in dimensions {
1608            self.dimension(*dim)?;
1609        }
1610        let id = VariableId(self.variables.len());
1611        self.variables.push(VariableDef {
1612            name,
1613            dim_ids: dimensions.to_vec(),
1614            dtype,
1615            attributes: Vec::new(),
1616            data: Vec::new(),
1617            data_encoding: VariableDataEncoding::ClassicBigEndian,
1618            string_values: None,
1619            vlen_values: None,
1620            storage: VariableStorageDef::default(),
1621            fill_value: None,
1622        });
1623        Ok(id)
1624    }
1625
1626    fn dimension(&self, id: DimensionId) -> Result<&DimensionDef> {
1627        self.dimensions
1628            .get(id.0)
1629            .ok_or_else(|| Error::InvalidDefinition("invalid dimension handle".into()))
1630    }
1631
1632    fn variable(&self, id: VariableId) -> Result<&VariableDef> {
1633        self.variables
1634            .get(id.0)
1635            .ok_or_else(|| Error::InvalidDefinition("invalid variable handle".into()))
1636    }
1637
1638    fn variable_mut(&mut self, id: VariableId) -> Result<&mut VariableDef> {
1639        self.variables
1640            .get_mut(id.0)
1641            .ok_or_else(|| Error::InvalidDefinition("invalid variable handle".into()))
1642    }
1643
1644    fn resolve_variable_write_selection(
1645        &self,
1646        variable: &VariableDef,
1647        selection: &NcSliceInfo,
1648        elem_size: usize,
1649    ) -> Result<ResolvedWriteSelection> {
1650        let extents = self.variable_write_extents(variable, elem_size)?;
1651        resolve_write_selection(&variable.name, &extents, selection)
1652    }
1653
1654    fn resolve_char_string_write_selection(
1655        &self,
1656        variable: &VariableDef,
1657        selection: &NcSliceInfo,
1658    ) -> Result<ResolvedWriteSelection> {
1659        if variable.dim_ids.is_empty() {
1660            return Err(Error::InvalidDefinition(format!(
1661                "char variable '{}' must have a string-width dimension",
1662                variable.name
1663            )));
1664        }
1665        let extents = self.variable_write_extents(variable, 1)?;
1666        resolve_write_selection(&variable.name, &extents[..extents.len() - 1], selection)
1667    }
1668
1669    fn variable_write_extents(
1670        &self,
1671        variable: &VariableDef,
1672        _elem_size: usize,
1673    ) -> Result<Vec<WriteDimensionExtent>> {
1674        variable
1675            .dim_ids
1676            .iter()
1677            .enumerate()
1678            .map(|(position, id)| {
1679                let dimension = &self.dimensions[id.0];
1680                Ok(WriteDimensionExtent {
1681                    current: if dimension.is_unlimited {
1682                        dimension.current_size
1683                    } else {
1684                        dimension.size
1685                    },
1686                    is_unlimited: dimension.is_unlimited,
1687                    position,
1688                })
1689            })
1690            .collect()
1691    }
1692
1693    fn char_string_axis_width(&self, variable: &VariableDef) -> Result<usize> {
1694        let width_dim_id = variable.dim_ids.last().ok_or_else(|| {
1695            Error::InvalidDefinition(format!(
1696                "char variable '{}' must have a string-width dimension",
1697                variable.name
1698            ))
1699        })?;
1700        let dimension = &self.dimensions[width_dim_id.0];
1701        let width = if dimension.is_unlimited {
1702            dimension.current_size
1703        } else {
1704            dimension.size
1705        };
1706        if dimension.is_unlimited && width == 0 {
1707            return Err(Error::InvalidDefinition(format!(
1708                "char variable '{}' string-width dimension cannot be inferred from string values",
1709                variable.name
1710            )));
1711        }
1712        require_usize(width, "char string width")
1713    }
1714
1715    fn validate_char_string_value_count(
1716        &self,
1717        variable: &VariableDef,
1718        value_count: usize,
1719    ) -> Result<Option<(DimensionId, u64)>> {
1720        if variable.dim_ids.is_empty() {
1721            return Err(Error::InvalidDefinition(format!(
1722                "char variable '{}' must have a string-width dimension",
1723                variable.name
1724            )));
1725        }
1726
1727        let value_count = u64::try_from(value_count).map_err(|_| {
1728            Error::InvalidDefinition("char string value count exceeds u64 capacity".into())
1729        })?;
1730        let mut known_product = 1u64;
1731        let mut unknown = Vec::new();
1732        for dim_id in &variable.dim_ids[..variable.dim_ids.len() - 1] {
1733            let dimension = &self.dimensions[dim_id.0];
1734            let size = if dimension.is_unlimited {
1735                dimension.current_size
1736            } else {
1737                dimension.size
1738            };
1739            if dimension.is_unlimited && size == 0 {
1740                if !unknown.contains(dim_id) {
1741                    unknown.push(*dim_id);
1742                }
1743                continue;
1744            }
1745            known_product = checked_mul_u64(known_product, size, "char string value count shape")?;
1746        }
1747
1748        let mut inferred_unlimited = None;
1749        if unknown.is_empty() {
1750            if value_count != known_product {
1751                return Err(Error::DataLengthMismatch {
1752                    expected: require_usize(known_product, "char string value count")?,
1753                    actual: require_usize(value_count, "char string value count")?,
1754                });
1755            }
1756        } else if unknown.len() == 1 {
1757            if known_product == 0 {
1758                if value_count != 0 {
1759                    return Err(Error::DataLengthMismatch {
1760                        expected: 0,
1761                        actual: require_usize(value_count, "char string value count")?,
1762                    });
1763                }
1764            } else if value_count % known_product != 0 {
1765                return Err(Error::InvalidDefinition(format!(
1766                    "char string value count {value_count} is not divisible by known leading dimension product {known_product}"
1767                )));
1768            } else {
1769                inferred_unlimited = Some((unknown[0], value_count / known_product));
1770            }
1771        } else {
1772            let names = unknown
1773                .iter()
1774                .map(|id| self.dimensions[id.0].name.as_str())
1775                .collect::<Vec<_>>()
1776                .join(", ");
1777            return Err(Error::InvalidDefinition(format!(
1778                "cannot infer multiple unlimited char string dimensions {names} for variable '{}'",
1779                variable.name
1780            )));
1781        }
1782
1783        Ok(inferred_unlimited)
1784    }
1785
1786    fn update_unlimited_extents_from_element_count(
1787        &mut self,
1788        variable: VariableId,
1789        elements: u64,
1790    ) -> Result<()> {
1791        let variable = self.variable(variable)?;
1792        let mut known_product = 1u64;
1793        let mut unknown = Vec::new();
1794        for dim_id in &variable.dim_ids {
1795            let dimension = &self.dimensions[dim_id.0];
1796            let size = if dimension.is_unlimited {
1797                dimension.current_size
1798            } else {
1799                dimension.size
1800            };
1801            if dimension.is_unlimited && size == 0 {
1802                if !unknown.contains(dim_id) {
1803                    unknown.push(*dim_id);
1804                }
1805                continue;
1806            }
1807            known_product = match known_product.checked_mul(size) {
1808                Some(product) => product,
1809                None => return Ok(()),
1810            };
1811        }
1812
1813        if unknown.len() != 1 || known_product == 0 {
1814            return Ok(());
1815        }
1816        if elements % known_product != 0 {
1817            return Ok(());
1818        }
1819        self.dimensions[unknown[0].0].current_size = elements / known_product;
1820        Ok(())
1821    }
1822
1823    fn update_unlimited_extents_from_shape(
1824        &mut self,
1825        variable: VariableId,
1826        shape: &[u64],
1827    ) -> Result<()> {
1828        let dim_ids = self.variable(variable)?.dim_ids.clone();
1829        if dim_ids.len() != shape.len() {
1830            return Err(Error::InvalidDefinition(format!(
1831                "resolved shape rank {} does not match variable '{}' rank {}",
1832                shape.len(),
1833                self.variable(variable)?.name,
1834                dim_ids.len()
1835            )));
1836        }
1837        for (dim_id, &size) in dim_ids.iter().zip(shape) {
1838            let dimension = &mut self.dimensions[dim_id.0];
1839            if dimension.is_unlimited && size > dimension.current_size {
1840                dimension.current_size = size;
1841            }
1842        }
1843        Ok(())
1844    }
1845
1846    fn write_native_variable_slice_bytes(
1847        &mut self,
1848        variable: VariableId,
1849        selection: &NcSliceInfo,
1850        bytes: &[u8],
1851        elem_size: usize,
1852    ) -> Result<()> {
1853        let variable_def = self.variable(variable)?;
1854        let resolved = self.resolve_variable_write_selection(variable_def, selection, elem_size)?;
1855        let expected = checked_mul_usize(
1856            resolved.elements,
1857            elem_size,
1858            "native variable slice byte size",
1859        )?;
1860        if bytes.len() != expected {
1861            return Err(Error::DataLengthMismatch {
1862                expected,
1863                actual: bytes.len(),
1864            });
1865        }
1866
1867        let strides = row_major_strides(&resolved.shape, "writer native slice stride")?;
1868        let total_elements =
1869            checked_shape_elements(&resolved.shape, "writer native variable element count")?;
1870        {
1871            let variable_def = self.variable_mut(variable)?;
1872            ensure_variable_native_slice_buffer(
1873                variable_def,
1874                &resolved.old_shape,
1875                &resolved.shape,
1876                total_elements,
1877                elem_size,
1878                resolved.can_grow,
1879            )?;
1880            scatter_slice_bytes(
1881                &mut variable_def.data,
1882                elem_size,
1883                &resolved.dims,
1884                &strides,
1885                bytes,
1886            )?;
1887        }
1888        self.update_unlimited_extents_from_shape(variable, &resolved.shape)
1889    }
1890
1891    fn ensure_unique_dimension(&self, name: &str) -> Result<()> {
1892        if self.dimensions.iter().any(|d| d.name == name) {
1893            return Err(Error::InvalidDefinition(format!(
1894                "duplicate dimension '{name}'"
1895            )));
1896        }
1897        Ok(())
1898    }
1899
1900    fn select_format(&self, options: NcWriteOptions) -> Result<NcFormat> {
1901        match options.format {
1902            NcWriteFormat::Classic => {
1903                self.validate_for_format(NcFormat::Classic)?;
1904                Ok(NcFormat::Classic)
1905            }
1906            NcWriteFormat::Offset64 => {
1907                self.validate_for_format(NcFormat::Offset64)?;
1908                Ok(NcFormat::Offset64)
1909            }
1910            NcWriteFormat::Cdf5 => {
1911                self.validate_for_format(NcFormat::Cdf5)?;
1912                Ok(NcFormat::Cdf5)
1913            }
1914            NcWriteFormat::Nc4 => Ok(NcFormat::Nc4),
1915            NcWriteFormat::Nc4Classic => Ok(NcFormat::Nc4Classic),
1916            NcWriteFormat::AutoClassic => {
1917                let preferred = if self.requires_cdf5() {
1918                    NcFormat::Cdf5
1919                } else {
1920                    NcFormat::Classic
1921                };
1922                match self.validate_for_format(preferred) {
1923                    Ok(()) => Ok(preferred),
1924                    Err(_) if preferred == NcFormat::Classic => {
1925                        self.validate_for_format(NcFormat::Offset64)?;
1926                        Ok(NcFormat::Offset64)
1927                    }
1928                    Err(err) => Err(err),
1929                }
1930            }
1931        }
1932    }
1933
1934    fn requires_cdf5(&self) -> bool {
1935        self.dimensions.iter().any(|d| d.size > u32::MAX as u64)
1936            || self.variables.iter().any(|v| {
1937                matches!(
1938                    v.dtype,
1939                    NcType::UByte | NcType::UShort | NcType::UInt | NcType::Int64 | NcType::UInt64
1940                )
1941            })
1942            || self.attributes.iter().any(|a| attr_requires_cdf5(&a.value))
1943            || self
1944                .group_attributes
1945                .iter()
1946                .any(|a| attr_requires_cdf5(&a.attribute.value))
1947            || self
1948                .variables
1949                .iter()
1950                .flat_map(|v| &v.attributes)
1951                .any(|a| attr_requires_cdf5(&a.value))
1952    }
1953
1954    fn validate_for_format(&self, format: NcFormat) -> Result<()> {
1955        if !matches!(
1956            format,
1957            NcFormat::Classic | NcFormat::Offset64 | NcFormat::Cdf5
1958        ) {
1959            return Err(Error::UnsupportedFeature(
1960                "only classic-family NetCDF formats are implemented".into(),
1961            ));
1962        }
1963        let unlimited_count = self.dimensions.iter().filter(|d| d.is_unlimited).count();
1964        if unlimited_count > 1 {
1965            return Err(Error::RequiresNetcdf4 {
1966                reason: "classic NetCDF supports at most one unlimited dimension".into(),
1967            });
1968        }
1969        if format != NcFormat::Cdf5 && self.requires_cdf5() {
1970            return Err(Error::FormatCapacityExceeded {
1971                reason: "CDF-5 is required for unsigned integer, 64-bit integer, or 64-bit count \
1972                         data"
1973                    .into(),
1974            });
1975        }
1976        validate_root_only_names(self)?;
1977        for variable in &self.variables {
1978            if variable.storage.has_nc4_options() {
1979                return Err(Error::UnsupportedFeature(format!(
1980                    "variable '{}' uses NetCDF-4 storage options",
1981                    variable.name
1982                )));
1983            }
1984            validate_classic_type(&variable.dtype)?;
1985            let is_record = variable_is_record(self, variable)?;
1986            if variable
1987                .dim_ids
1988                .iter()
1989                .skip(1)
1990                .any(|id| self.dimensions[id.0].is_unlimited)
1991            {
1992                return Err(Error::InvalidDefinition(format!(
1993                    "record dimension must be first for variable '{}'",
1994                    variable.name
1995                )));
1996            }
1997            let expected = expected_variable_bytes(self, variable, is_record)?;
1998            if variable.data.len() != expected {
1999                return Err(Error::DataLengthMismatch {
2000                    expected,
2001                    actual: variable.data.len(),
2002                });
2003            }
2004        }
2005        Ok(())
2006    }
2007}
2008
2009#[derive(Debug, Clone)]
2010struct PlannedVar {
2011    def: VariableDef,
2012    is_record: bool,
2013    /// Header `vsize` field: the per-record (or fixed) byte size, always
2014    /// rounded up to a multiple of 4 as the classic spec requires.
2015    header_vsize: u64,
2016    /// Bytes this variable actually occupies per record (or in total for
2017    /// fixed variables) in the data layout. Equal to `header_vsize` except
2018    /// for the spec's lone-record-variable case, where records are packed
2019    /// without padding.
2020    layout_vsize: u64,
2021    begin: u64,
2022    fixed_data_len: u64,
2023    record_slab_len: u64,
2024}
2025
2026#[derive(Debug, Clone)]
2027struct ClassicWritePlan {
2028    num_records: u64,
2029    header: Vec<u8>,
2030    fixed_vars: Vec<PlannedVar>,
2031    record_vars: Vec<PlannedVar>,
2032}
2033
2034impl ClassicWritePlan {
2035    fn build(builder: &NcFileBuilder, format: NcFormat) -> Result<Self> {
2036        builder.validate_for_format(format)?;
2037        let num_records = infer_num_records(builder)?;
2038        let mut planned = plan_variables(builder, 0)?;
2039        let header_without_offsets = encode_header(builder, format, num_records, &planned)?;
2040        planned = plan_variables(builder, header_without_offsets.len() as u64)?;
2041        let header = encode_header(builder, format, num_records, &planned)?;
2042        let planned = plan_variables(builder, header.len() as u64)?;
2043        let header = encode_header(builder, format, num_records, &planned)?;
2044
2045        if format == NcFormat::Classic {
2046            for var in &planned {
2047                require_u32(var.begin, "CDF-1 variable offset")?;
2048            }
2049        }
2050        if matches!(format, NcFormat::Classic | NcFormat::Offset64) {
2051            require_u32(num_records, "record count")?;
2052        }
2053
2054        let fixed_vars = planned.iter().filter(|v| !v.is_record).cloned().collect();
2055        let record_vars = planned.iter().filter(|v| v.is_record).cloned().collect();
2056        Ok(Self {
2057            num_records,
2058            header,
2059            fixed_vars,
2060            record_vars,
2061        })
2062    }
2063
2064    fn write(&self, writer: &mut impl Write) -> Result<()> {
2065        writer.write_all(&self.header)?;
2066        for var in &self.fixed_vars {
2067            writer.write_all(&var.def.data)?;
2068            write_fill_padding(
2069                writer,
2070                var.layout_vsize - var.fixed_data_len,
2071                &variable_fill_pattern(&var.def)?,
2072            )?;
2073        }
2074        for record in 0..self.num_records {
2075            for var in &self.record_vars {
2076                let start = usize::try_from(record * var.record_slab_len).map_err(|_| {
2077                    Error::InvalidDefinition("record byte offset exceeds platform usize".into())
2078                })?;
2079                let slab_len = usize::try_from(var.record_slab_len).map_err(|_| {
2080                    Error::InvalidDefinition("record slab length exceeds platform usize".into())
2081                })?;
2082                writer.write_all(&var.def.data[start..start + slab_len])?;
2083                write_fill_padding(
2084                    writer,
2085                    var.layout_vsize - var.record_slab_len,
2086                    &variable_fill_pattern(&var.def)?,
2087                )?;
2088            }
2089        }
2090        Ok(())
2091    }
2092}
2093
2094/// The classic-encoded fill pattern used to pad a variable's data out to its
2095/// padded size, matching netcdf-c (which fills the slack with fill values).
2096/// The HDF5-native (little-endian) fill pattern used to initialize gaps when a
2097/// native-encoded variable buffer is created or grown, mirroring netcdf-c. An
2098/// explicit fill value wins; otherwise primitive types use their default fill
2099/// and user-defined types (which have no primitive default) use zeros.
2100fn native_fill_pattern(def: &VariableDef, elem_size: usize) -> Result<Vec<u8>> {
2101    if let Some(fill_value) = &def.fill_value {
2102        return Ok(fill_value.hdf5_bytes.clone());
2103    }
2104    match default_classic_fill_bytes(&def.dtype) {
2105        Ok(classic) => convert_classic_be_data_to_hdf5_le(&def.dtype, &classic),
2106        Err(_) => Ok(vec![0; elem_size]),
2107    }
2108}
2109
2110fn variable_fill_pattern(def: &VariableDef) -> Result<Vec<u8>> {
2111    match &def.fill_value {
2112        Some(fill_value) => Ok(fill_value.classic_bytes.clone()),
2113        None => default_classic_fill_bytes(&def.dtype),
2114    }
2115}
2116
2117fn plan_variables(builder: &NcFileBuilder, data_start: u64) -> Result<Vec<PlannedVar>> {
2118    let mut fixed_offset = data_start;
2119    let mut planned = Vec::with_capacity(builder.variables.len());
2120    for def in &builder.variables {
2121        let is_record = variable_is_record(builder, def)?;
2122        let elem_size = def.dtype.size()? as u64;
2123        let record_slab_len = if is_record {
2124            variable_shape_elements(builder, def, 1)? * elem_size
2125        } else {
2126            0
2127        };
2128        let fixed_data_len = if is_record {
2129            0
2130        } else {
2131            variable_shape_elements(builder, def, 0)? * elem_size
2132        };
2133        let vsize = if is_record {
2134            record_slab_len
2135        } else {
2136            fixed_data_len
2137        };
2138        let header_vsize = pad4_u64(vsize)?;
2139        let begin = if is_record { 0 } else { fixed_offset };
2140        if !is_record {
2141            fixed_offset = checked_add(fixed_offset, header_vsize, "fixed data offset")?;
2142        }
2143        planned.push(PlannedVar {
2144            def: def.clone(),
2145            is_record,
2146            header_vsize,
2147            layout_vsize: header_vsize,
2148            begin,
2149            fixed_data_len,
2150            record_slab_len,
2151        });
2152    }
2153
2154    // The classic spec packs records without padding when the file has
2155    // exactly one record variable; the header vsize stays padded.
2156    let record_var_count = planned.iter().filter(|var| var.is_record).count();
2157    if record_var_count == 1 {
2158        for var in &mut planned {
2159            if var.is_record {
2160                var.layout_vsize = var.record_slab_len;
2161            }
2162        }
2163    }
2164
2165    let record_data_start = fixed_offset;
2166    let mut record_offset = record_data_start;
2167    for var in &mut planned {
2168        if var.is_record {
2169            var.begin = record_offset;
2170            record_offset = checked_add(record_offset, var.layout_vsize, "record offset")?;
2171        }
2172    }
2173    Ok(planned)
2174}
2175
2176fn encode_header(
2177    builder: &NcFileBuilder,
2178    format: NcFormat,
2179    num_records: u64,
2180    planned: &[PlannedVar],
2181) -> Result<Vec<u8>> {
2182    let mut out = Vec::new();
2183    out.extend_from_slice(match format {
2184        NcFormat::Classic => b"CDF\x01",
2185        NcFormat::Offset64 => b"CDF\x02",
2186        NcFormat::Cdf5 => b"CDF\x05",
2187        _ => unreachable!("classic header only"),
2188    });
2189    write_count(&mut out, format, num_records)?;
2190    encode_dimensions(&mut out, builder, format)?;
2191    encode_attributes(&mut out, format, &builder.attributes)?;
2192    encode_variables(&mut out, builder, format, planned)?;
2193    Ok(out)
2194}
2195
2196fn encode_dimensions(out: &mut Vec<u8>, builder: &NcFileBuilder, format: NcFormat) -> Result<()> {
2197    if builder.dimensions.is_empty() {
2198        out.extend_from_slice(&ABSENT.to_be_bytes());
2199        write_count(out, format, 0)?;
2200        return Ok(());
2201    }
2202    out.extend_from_slice(&NC_DIMENSION.to_be_bytes());
2203    write_count(out, format, builder.dimensions.len() as u64)?;
2204    for dim in &builder.dimensions {
2205        write_name(out, format, &dim.name)?;
2206        write_count(out, format, if dim.is_unlimited { 0 } else { dim.size })?;
2207    }
2208    Ok(())
2209}
2210
2211fn encode_attributes(out: &mut Vec<u8>, format: NcFormat, attrs: &[NcAttribute]) -> Result<()> {
2212    if attrs.is_empty() {
2213        out.extend_from_slice(&ABSENT.to_be_bytes());
2214        write_count(out, format, 0)?;
2215        return Ok(());
2216    }
2217    out.extend_from_slice(&NC_ATTRIBUTE.to_be_bytes());
2218    write_count(out, format, attrs.len() as u64)?;
2219    for attr in attrs {
2220        write_name(out, format, &attr.name)?;
2221        let (dtype, count, bytes) = encode_attr_value(&attr.value)?;
2222        write_u32(
2223            out,
2224            dtype.classic_type_code().ok_or_else(|| {
2225                Error::InvalidDefinition(format!("{dtype:?} is not valid in classic attributes"))
2226            })?,
2227        );
2228        write_count(out, format, count)?;
2229        out.extend_from_slice(&bytes);
2230        pad_vec_to_4(out);
2231    }
2232    Ok(())
2233}
2234
2235fn encode_variables(
2236    out: &mut Vec<u8>,
2237    builder: &NcFileBuilder,
2238    format: NcFormat,
2239    planned: &[PlannedVar],
2240) -> Result<()> {
2241    if planned.is_empty() {
2242        out.extend_from_slice(&ABSENT.to_be_bytes());
2243        write_count(out, format, 0)?;
2244        return Ok(());
2245    }
2246    out.extend_from_slice(&NC_VARIABLE.to_be_bytes());
2247    write_count(out, format, planned.len() as u64)?;
2248    for var in planned {
2249        write_name(out, format, &var.def.name)?;
2250        write_count(out, format, var.def.dim_ids.len() as u64)?;
2251        for dim_id in &var.def.dim_ids {
2252            write_count(out, format, dim_id.0 as u64)?;
2253        }
2254        encode_attributes(out, format, &var.def.attributes)?;
2255        write_u32(
2256            out,
2257            var.def.dtype.classic_type_code().ok_or_else(|| {
2258                Error::InvalidDefinition(format!(
2259                    "{:?} is not valid in classic variables",
2260                    var.def.dtype
2261                ))
2262            })?,
2263        );
2264        // CDF-1/2 store vsize as a 32-bit field; the spec reserves 2^32 - 1
2265        // as the marker for sizes that exceed it (readers recompute from the
2266        // dimensions anyway).
2267        let header_vsize = match format {
2268            NcFormat::Classic | NcFormat::Offset64 if var.header_vsize > u64::from(u32::MAX) => {
2269                u64::from(u32::MAX)
2270            }
2271            _ => var.header_vsize,
2272        };
2273        write_count(out, format, header_vsize)?;
2274        match format {
2275            NcFormat::Classic => write_u32(out, require_u32(var.begin, "CDF-1 begin")?),
2276            NcFormat::Offset64 | NcFormat::Cdf5 => write_u64(out, var.begin),
2277            _ => unreachable!("classic header only"),
2278        }
2279        let _ = builder;
2280    }
2281    Ok(())
2282}
2283
2284fn encode_attr_value(value: &NcAttrValue) -> Result<(NcType, u64, Vec<u8>)> {
2285    let mut out = Vec::new();
2286    let (dtype, count) = match value {
2287        NcAttrValue::Bytes(values) => {
2288            out.extend(values.iter().map(|v| *v as u8));
2289            (NcType::Byte, values.len() as u64)
2290        }
2291        NcAttrValue::Chars(value) => {
2292            out.extend_from_slice(value.as_bytes());
2293            (NcType::Char, value.len() as u64)
2294        }
2295        NcAttrValue::Shorts(values) => {
2296            for value in values {
2297                out.extend_from_slice(&value.to_be_bytes());
2298            }
2299            (NcType::Short, values.len() as u64)
2300        }
2301        NcAttrValue::Ints(values) => {
2302            for value in values {
2303                out.extend_from_slice(&value.to_be_bytes());
2304            }
2305            (NcType::Int, values.len() as u64)
2306        }
2307        NcAttrValue::Floats(values) => {
2308            for value in values {
2309                out.extend_from_slice(&value.to_be_bytes());
2310            }
2311            (NcType::Float, values.len() as u64)
2312        }
2313        NcAttrValue::Doubles(values) => {
2314            for value in values {
2315                out.extend_from_slice(&value.to_be_bytes());
2316            }
2317            (NcType::Double, values.len() as u64)
2318        }
2319        NcAttrValue::UBytes(values) => {
2320            out.extend_from_slice(values);
2321            (NcType::UByte, values.len() as u64)
2322        }
2323        NcAttrValue::UShorts(values) => {
2324            for value in values {
2325                out.extend_from_slice(&value.to_be_bytes());
2326            }
2327            (NcType::UShort, values.len() as u64)
2328        }
2329        NcAttrValue::UInts(values) => {
2330            for value in values {
2331                out.extend_from_slice(&value.to_be_bytes());
2332            }
2333            (NcType::UInt, values.len() as u64)
2334        }
2335        NcAttrValue::Int64s(values) => {
2336            for value in values {
2337                out.extend_from_slice(&value.to_be_bytes());
2338            }
2339            (NcType::Int64, values.len() as u64)
2340        }
2341        NcAttrValue::UInt64s(values) => {
2342            for value in values {
2343                out.extend_from_slice(&value.to_be_bytes());
2344            }
2345            (NcType::UInt64, values.len() as u64)
2346        }
2347        NcAttrValue::Strings(_) => {
2348            return Err(Error::UnsupportedFeature(
2349                "NC_STRING attributes require NetCDF-4".into(),
2350            ));
2351        }
2352    };
2353    Ok((dtype, count, out))
2354}
2355
2356#[cfg(feature = "netcdf4")]
2357fn nc_type_to_hdf5(dtype: &NcType) -> Result<H5Datatype> {
2358    let byte_order = H5ByteOrder::LittleEndian;
2359    match dtype {
2360        NcType::Byte => Ok(H5Datatype::FixedPoint {
2361            size: 1,
2362            signed: true,
2363            byte_order,
2364        }),
2365        NcType::UByte => Ok(H5Datatype::FixedPoint {
2366            size: 1,
2367            signed: false,
2368            byte_order,
2369        }),
2370        NcType::Short => Ok(H5Datatype::FixedPoint {
2371            size: 2,
2372            signed: true,
2373            byte_order,
2374        }),
2375        NcType::UShort => Ok(H5Datatype::FixedPoint {
2376            size: 2,
2377            signed: false,
2378            byte_order,
2379        }),
2380        NcType::Int => Ok(H5Datatype::FixedPoint {
2381            size: 4,
2382            signed: true,
2383            byte_order,
2384        }),
2385        NcType::UInt => Ok(H5Datatype::FixedPoint {
2386            size: 4,
2387            signed: false,
2388            byte_order,
2389        }),
2390        NcType::Int64 => Ok(H5Datatype::FixedPoint {
2391            size: 8,
2392            signed: true,
2393            byte_order,
2394        }),
2395        NcType::UInt64 => Ok(H5Datatype::FixedPoint {
2396            size: 8,
2397            signed: false,
2398            byte_order,
2399        }),
2400        NcType::Float => Ok(H5Datatype::FloatingPoint {
2401            size: 4,
2402            byte_order,
2403        }),
2404        NcType::Double => Ok(H5Datatype::FloatingPoint {
2405            size: 8,
2406            byte_order,
2407        }),
2408        NcType::Char => Ok(H5Datatype::String {
2409            size: H5StringSize::Fixed(1),
2410            encoding: H5StringEncoding::Ascii,
2411            padding: H5StringPadding::NullPad,
2412        }),
2413        NcType::String => Ok(H5Datatype::String {
2414            size: H5StringSize::Variable,
2415            encoding: H5StringEncoding::Utf8,
2416            padding: H5StringPadding::NullTerminate,
2417        }),
2418        NcType::Enum { base, members } => {
2419            let h5_base = nc_type_to_hdf5(base)?;
2420            let members = members
2421                .iter()
2422                .map(|member| {
2423                    Ok(H5EnumMember {
2424                        name: member.name.clone(),
2425                        value: nc_enum_value_to_hdf5(base, member.value)?,
2426                    })
2427                })
2428                .collect::<Result<Vec<_>>>()?;
2429            Ok(H5Datatype::Enum {
2430                base: Box::new(h5_base),
2431                members,
2432            })
2433        }
2434        NcType::Compound { size, fields } => {
2435            let fields = fields
2436                .iter()
2437                .map(|field| {
2438                    let byte_offset = u32::try_from(field.offset).map_err(|_| {
2439                        Error::UnsupportedFeature(format!(
2440                            "compound field '{}' offset exceeds HDF5 u32 capacity",
2441                            field.name
2442                        ))
2443                    })?;
2444                    Ok(H5CompoundField {
2445                        name: field.name.clone(),
2446                        byte_offset,
2447                        datatype: nc_type_to_hdf5(&field.dtype)?,
2448                    })
2449                })
2450                .collect::<Result<Vec<_>>>()?;
2451            Ok(H5Datatype::Compound {
2452                size: *size,
2453                fields,
2454            })
2455        }
2456        NcType::Opaque { size, tag } => Ok(H5Datatype::Opaque {
2457            size: *size,
2458            tag: tag.clone(),
2459        }),
2460        NcType::Array { base, dims } => Ok(H5Datatype::Array {
2461            base: Box::new(nc_type_to_hdf5(base)?),
2462            dims: dims.clone(),
2463        }),
2464        NcType::VLen { base } => {
2465            validate_vlen_base_nc4_type(base)?;
2466            Ok(H5Datatype::VarLen {
2467                base: Box::new(nc_type_to_hdf5(base)?),
2468                kind: H5VarLenKind::Sequence,
2469                encoding: H5StringEncoding::Ascii,
2470                padding: H5StringPadding::NullTerminate,
2471            })
2472        }
2473    }
2474}
2475
2476#[cfg(feature = "netcdf4")]
2477fn nc_enum_value_to_hdf5(base: &NcType, value: NcIntegerValue) -> Result<Vec<u8>> {
2478    nc_enum_value_to_le_bytes(base, value)
2479}
2480
2481fn nc_enum_value_to_le_bytes(base: &NcType, value: NcIntegerValue) -> Result<Vec<u8>> {
2482    match (base, value) {
2483        (NcType::Byte, NcIntegerValue::I8(value)) => Ok(vec![value as u8]),
2484        (NcType::UByte, NcIntegerValue::U8(value)) => Ok(vec![value]),
2485        (NcType::Short, NcIntegerValue::I16(value)) => Ok(value.to_le_bytes().to_vec()),
2486        (NcType::UShort, NcIntegerValue::U16(value)) => Ok(value.to_le_bytes().to_vec()),
2487        (NcType::Int, NcIntegerValue::I32(value)) => Ok(value.to_le_bytes().to_vec()),
2488        (NcType::UInt, NcIntegerValue::U32(value)) => Ok(value.to_le_bytes().to_vec()),
2489        (NcType::Int64, NcIntegerValue::I64(value)) => Ok(value.to_le_bytes().to_vec()),
2490        (NcType::UInt64, NcIntegerValue::U64(value)) => Ok(value.to_le_bytes().to_vec()),
2491        (base, value) => Err(Error::InvalidDefinition(format!(
2492            "enum value {value:?} is incompatible with base type {base:?}"
2493        ))),
2494    }
2495}
2496
2497#[cfg(feature = "netcdf4")]
2498fn nc4_hdf5_element_size(dtype: &NcType) -> Result<usize> {
2499    match dtype {
2500        NcType::String | NcType::VLen { .. } => Ok(16),
2501        other => other.size().map_err(Error::Core),
2502    }
2503}
2504
2505fn convert_classic_be_data_to_hdf5_le(dtype: &NcType, data: &[u8]) -> Result<Vec<u8>> {
2506    let width = dtype.size()?;
2507    if width == 1 {
2508        return Ok(data.to_vec());
2509    }
2510    if data.len() % width != 0 {
2511        return Err(Error::InvalidDefinition(format!(
2512            "variable data length {} is not a multiple of element size {width}",
2513            data.len()
2514        )));
2515    }
2516
2517    match dtype {
2518        NcType::Short | NcType::UShort => Ok(data
2519            .chunks_exact(2)
2520            .flat_map(|chunk| [chunk[1], chunk[0]])
2521            .collect()),
2522        NcType::Int | NcType::UInt | NcType::Float => Ok(data
2523            .chunks_exact(4)
2524            .flat_map(|chunk| [chunk[3], chunk[2], chunk[1], chunk[0]])
2525            .collect()),
2526        NcType::Int64 | NcType::UInt64 | NcType::Double => Ok(data
2527            .chunks_exact(8)
2528            .flat_map(|chunk| {
2529                [
2530                    chunk[7], chunk[6], chunk[5], chunk[4], chunk[3], chunk[2], chunk[1], chunk[0],
2531                ]
2532            })
2533            .collect()),
2534        other => Err(Error::UnsupportedFeature(format!(
2535            "NetCDF-4 data conversion is not implemented for {other:?}"
2536        ))),
2537    }
2538}
2539
2540#[cfg(feature = "netcdf4")]
2541fn nc_attr_to_hdf5(attribute: &NcAttribute) -> Result<H5AttributeBuilder> {
2542    match &attribute.value {
2543        NcAttrValue::Chars(value) => Ok(H5AttributeBuilder::fixed_string(&attribute.name, value)),
2544        NcAttrValue::Bytes(values) => hdf5_numeric_attr(&attribute.name, NcType::Byte, values),
2545        NcAttrValue::UBytes(values) => hdf5_numeric_attr(&attribute.name, NcType::UByte, values),
2546        NcAttrValue::Shorts(values) => hdf5_numeric_attr(&attribute.name, NcType::Short, values),
2547        NcAttrValue::UShorts(values) => hdf5_numeric_attr(&attribute.name, NcType::UShort, values),
2548        NcAttrValue::Ints(values) => hdf5_numeric_attr(&attribute.name, NcType::Int, values),
2549        NcAttrValue::UInts(values) => hdf5_numeric_attr(&attribute.name, NcType::UInt, values),
2550        NcAttrValue::Int64s(values) => hdf5_numeric_attr(&attribute.name, NcType::Int64, values),
2551        NcAttrValue::UInt64s(values) => hdf5_numeric_attr(&attribute.name, NcType::UInt64, values),
2552        NcAttrValue::Floats(values) => hdf5_numeric_attr(&attribute.name, NcType::Float, values),
2553        NcAttrValue::Doubles(values) => hdf5_numeric_attr(&attribute.name, NcType::Double, values),
2554        NcAttrValue::Strings(values) => H5AttributeBuilder::vlen_strings(&attribute.name, values)
2555            .map_err(hdf5_error_to_unsupported),
2556    }
2557}
2558
2559#[cfg(feature = "netcdf4")]
2560trait NcAttrElement {
2561    fn write_le(&self, dst: &mut Vec<u8>);
2562}
2563
2564#[cfg(feature = "netcdf4")]
2565macro_rules! impl_attr_element_bytes {
2566    ($ty:ty) => {
2567        impl NcAttrElement for $ty {
2568            fn write_le(&self, dst: &mut Vec<u8>) {
2569                dst.push(*self as u8);
2570            }
2571        }
2572    };
2573}
2574
2575#[cfg(feature = "netcdf4")]
2576macro_rules! impl_attr_element_le {
2577    ($ty:ty) => {
2578        impl NcAttrElement for $ty {
2579            fn write_le(&self, dst: &mut Vec<u8>) {
2580                dst.extend_from_slice(&self.to_le_bytes());
2581            }
2582        }
2583    };
2584}
2585
2586#[cfg(feature = "netcdf4")]
2587impl_attr_element_bytes!(i8);
2588#[cfg(feature = "netcdf4")]
2589impl_attr_element_bytes!(u8);
2590#[cfg(feature = "netcdf4")]
2591impl_attr_element_le!(i16);
2592#[cfg(feature = "netcdf4")]
2593impl_attr_element_le!(u16);
2594#[cfg(feature = "netcdf4")]
2595impl_attr_element_le!(i32);
2596#[cfg(feature = "netcdf4")]
2597impl_attr_element_le!(u32);
2598#[cfg(feature = "netcdf4")]
2599impl_attr_element_le!(i64);
2600#[cfg(feature = "netcdf4")]
2601impl_attr_element_le!(u64);
2602#[cfg(feature = "netcdf4")]
2603impl_attr_element_le!(f32);
2604#[cfg(feature = "netcdf4")]
2605impl_attr_element_le!(f64);
2606
2607#[cfg(feature = "netcdf4")]
2608fn hdf5_numeric_attr<T: NcAttrElement>(
2609    name: &str,
2610    dtype: NcType,
2611    values: &[T],
2612) -> Result<H5AttributeBuilder> {
2613    let mut raw = Vec::with_capacity(values.len() * dtype.size()?);
2614    for value in values {
2615        value.write_le(&mut raw);
2616    }
2617    Ok(H5AttributeBuilder::new(
2618        name,
2619        nc_type_to_hdf5(&dtype)?,
2620        vec![values.len() as u64],
2621        raw,
2622    ))
2623}
2624
2625#[cfg(feature = "netcdf4")]
2626fn empty_dimension_list_attribute() -> H5AttributeBuilder {
2627    H5AttributeBuilder::new(
2628        "DIMENSION_LIST",
2629        H5Datatype::VarLen {
2630            base: Box::new(H5Datatype::Reference {
2631                ref_type: H5ReferenceType::Object,
2632                size: 8,
2633            }),
2634            kind: H5VarLenKind::Sequence,
2635            encoding: H5StringEncoding::Ascii,
2636            padding: H5StringPadding::NullTerminate,
2637        },
2638        vec![0],
2639        Vec::new(),
2640    )
2641}
2642
2643#[cfg(feature = "netcdf4")]
2644fn hdf5_error_to_unsupported(err: hdf5_writer::Error) -> Error {
2645    Error::UnsupportedFeature(format!("HDF5 writer error: {err}"))
2646}
2647
2648#[cfg(feature = "netcdf4")]
2649fn checked_usize(value: u64, context: &str) -> Result<usize> {
2650    usize::try_from(value).map_err(|_| {
2651        Error::InvalidDefinition(format!(
2652            "{context} value {value} exceeds platform usize capacity"
2653        ))
2654    })
2655}
2656
2657fn expected_variable_bytes(
2658    builder: &NcFileBuilder,
2659    variable: &VariableDef,
2660    is_record: bool,
2661) -> Result<usize> {
2662    let elements = if is_record {
2663        let records = infer_num_records_for_var(builder, variable)?;
2664        records
2665            .checked_mul(variable_shape_elements(builder, variable, 1)?)
2666            .ok_or_else(|| Error::InvalidDefinition("variable element count overflow".into()))?
2667    } else {
2668        variable_shape_elements(builder, variable, 0)?
2669    };
2670    let bytes = elements
2671        .checked_mul(variable.dtype.size()? as u64)
2672        .ok_or_else(|| Error::InvalidDefinition("variable byte size overflow".into()))?;
2673    usize::try_from(bytes)
2674        .map_err(|_| Error::InvalidDefinition("variable byte size exceeds platform usize".into()))
2675}
2676
2677#[derive(Debug, Clone)]
2678struct ResolvedWriteSelection {
2679    dims: Vec<ResolvedWriteSelectionDim>,
2680    elements: usize,
2681    old_shape: Vec<u64>,
2682    shape: Vec<u64>,
2683    can_grow: bool,
2684}
2685
2686#[derive(Debug, Clone)]
2687enum ResolvedWriteSelectionDim {
2688    Index(u64),
2689    Slice { start: u64, step: u64, count: usize },
2690}
2691
2692#[derive(Debug, Clone, Copy)]
2693struct WriteDimensionExtent {
2694    current: u64,
2695    is_unlimited: bool,
2696    position: usize,
2697}
2698
2699fn resolve_write_selection(
2700    variable_name: &str,
2701    extents: &[WriteDimensionExtent],
2702    selection: &NcSliceInfo,
2703) -> Result<ResolvedWriteSelection> {
2704    if selection.selections.len() != extents.len() {
2705        return Err(Error::InvalidDefinition(format!(
2706            "selection has {} dimensions but variable '{}' has {}",
2707            selection.selections.len(),
2708            variable_name,
2709            extents.len()
2710        )));
2711    }
2712
2713    let mut dims = Vec::with_capacity(extents.len());
2714    let mut old_shape = Vec::with_capacity(extents.len());
2715    let mut shape = Vec::with_capacity(extents.len());
2716    let mut elements = 1usize;
2717    let mut can_grow = false;
2718    for (selection, extent) in selection.selections.iter().zip(extents) {
2719        let dim_size = extent.current;
2720        old_shape.push(dim_size);
2721        let mut target_dim_size = dim_size;
2722        match selection {
2723            NcSliceInfoElem::Index(index) => {
2724                if extent.is_unlimited {
2725                    target_dim_size = checked_add(*index, 1, "unlimited slice extent")?;
2726                } else if *index >= dim_size {
2727                    return Err(Error::InvalidDefinition(format!(
2728                        "index {index} out of bounds for dimension {} (size {dim_size})",
2729                        extent.position
2730                    )));
2731                }
2732                dims.push(ResolvedWriteSelectionDim::Index(*index));
2733            }
2734            NcSliceInfoElem::Slice { start, end, step } => {
2735                if *step == 0 {
2736                    return Err(Error::InvalidDefinition("slice step cannot be 0".into()));
2737                }
2738                if !extent.is_unlimited && *start > dim_size {
2739                    return Err(Error::InvalidDefinition(format!(
2740                        "slice start {start} out of bounds for dimension {} (size {dim_size})",
2741                        extent.position
2742                    )));
2743                }
2744                let actual_end = if extent.is_unlimited {
2745                    if *end == u64::MAX {
2746                        dim_size
2747                    } else {
2748                        *end
2749                    }
2750                } else if *end == u64::MAX {
2751                    dim_size
2752                } else {
2753                    (*end).min(dim_size)
2754                };
2755                if extent.is_unlimited && *start > actual_end {
2756                    return Err(Error::InvalidDefinition(format!(
2757                        "slice start {start} exceeds end {actual_end} for unlimited dimension {}",
2758                        extent.position
2759                    )));
2760                }
2761                let count_u64 = if *start >= actual_end {
2762                    0
2763                } else {
2764                    (actual_end - *start).div_ceil(*step)
2765                };
2766                let count = require_usize(count_u64, "slice result dimension")?;
2767                if extent.is_unlimited && count > 0 {
2768                    let last_index = checked_add(
2769                        *start,
2770                        checked_mul_u64((count - 1) as u64, *step, "slice coordinate step")?,
2771                        "slice coordinate",
2772                    )?;
2773                    target_dim_size = checked_add(last_index, 1, "unlimited slice extent")?;
2774                }
2775                elements = elements.checked_mul(count).ok_or_else(|| {
2776                    Error::InvalidDefinition(
2777                        "slice result element count exceeds platform usize".into(),
2778                    )
2779                })?;
2780                dims.push(ResolvedWriteSelectionDim::Slice {
2781                    start: *start,
2782                    step: *step,
2783                    count,
2784                });
2785            }
2786        }
2787        if extent.is_unlimited && target_dim_size > dim_size {
2788            can_grow = true;
2789        }
2790        shape.push(target_dim_size.max(dim_size));
2791    }
2792
2793    Ok(ResolvedWriteSelection {
2794        dims,
2795        elements,
2796        old_shape,
2797        shape,
2798        can_grow,
2799    })
2800}
2801
2802fn ensure_variable_slice_buffer(
2803    variable: &mut VariableDef,
2804    old_shape: &[u64],
2805    new_shape: &[u64],
2806    total_elements: u64,
2807    elem_size: usize,
2808    can_grow: bool,
2809) -> Result<()> {
2810    ensure_variable_byte_slice_buffer(
2811        variable,
2812        old_shape,
2813        new_shape,
2814        total_elements,
2815        elem_size,
2816        can_grow,
2817        VariableDataEncoding::ClassicBigEndian,
2818    )
2819}
2820
2821fn ensure_variable_native_slice_buffer(
2822    variable: &mut VariableDef,
2823    old_shape: &[u64],
2824    new_shape: &[u64],
2825    total_elements: u64,
2826    elem_size: usize,
2827    can_grow: bool,
2828) -> Result<()> {
2829    ensure_variable_byte_slice_buffer(
2830        variable,
2831        old_shape,
2832        new_shape,
2833        total_elements,
2834        elem_size,
2835        can_grow,
2836        VariableDataEncoding::Hdf5Native,
2837    )
2838}
2839
2840/// Create or grow a variable's byte payload for a slice write, in either the
2841/// classic big-endian or HDF5 native little-endian encoding. Gaps are filled
2842/// with the encoding-appropriate fill pattern (the variable's fill value, or
2843/// the type default).
2844fn ensure_variable_byte_slice_buffer(
2845    variable: &mut VariableDef,
2846    old_shape: &[u64],
2847    new_shape: &[u64],
2848    total_elements: u64,
2849    elem_size: usize,
2850    can_grow: bool,
2851    encoding: VariableDataEncoding,
2852) -> Result<()> {
2853    if variable.data_encoding != encoding && !variable.data.is_empty() {
2854        return Err(Error::UnsupportedFeature(format!(
2855            "slice writes cannot change the payload encoding of variable '{}'",
2856            variable.name
2857        )));
2858    }
2859
2860    let expected = checked_mul_usize(
2861        require_usize(total_elements, "variable element count")?,
2862        elem_size,
2863        "variable byte size",
2864    )?;
2865    let fill_pattern = match encoding {
2866        VariableDataEncoding::Hdf5Native => native_fill_pattern(variable, elem_size)?,
2867        VariableDataEncoding::ClassicBigEndian => match &variable.fill_value {
2868            Some(fill_value) => fill_value.classic_bytes.clone(),
2869            None => default_classic_fill_bytes(&variable.dtype)?,
2870        },
2871    };
2872    if variable.data.is_empty() {
2873        variable.data = filled_data_buffer(expected, elem_size, &fill_pattern)?;
2874    } else if variable.data.len() < expected && can_grow {
2875        resize_variable_data(
2876            &mut variable.data,
2877            old_shape,
2878            new_shape,
2879            elem_size,
2880            &fill_pattern,
2881        )?;
2882    } else if variable.data.len() != expected {
2883        return Err(Error::DataLengthMismatch {
2884            expected,
2885            actual: variable.data.len(),
2886        });
2887    }
2888
2889    variable.data_encoding = encoding;
2890    variable.string_values = None;
2891    variable.vlen_values = None;
2892    Ok(())
2893}
2894
2895fn encode_char_string_values<S: AsRef<str>>(
2896    variable_name: &str,
2897    width: usize,
2898    values: &[S],
2899) -> Result<Vec<u8>> {
2900    let mut encoded = Vec::with_capacity(checked_mul_usize(
2901        values.len(),
2902        width,
2903        "char string byte count",
2904    )?);
2905    for value in values {
2906        let value = value.as_ref();
2907        let bytes = value.as_bytes();
2908        if bytes.contains(&0) {
2909            return Err(Error::InvalidDefinition(format!(
2910                "char variable '{variable_name}' string values cannot contain NUL bytes"
2911            )));
2912        }
2913        if bytes.len() > width {
2914            return Err(Error::InvalidDefinition(format!(
2915                "char variable '{variable_name}' string value is {} bytes, exceeding fixed width {width}",
2916                bytes.len()
2917            )));
2918        }
2919        encoded.extend_from_slice(bytes);
2920        encoded.resize(encoded.len() + (width - bytes.len()), 0);
2921    }
2922    Ok(encoded)
2923}
2924
2925fn ensure_variable_string_slice_buffer(
2926    variable: &mut VariableDef,
2927    old_shape: &[u64],
2928    new_shape: &[u64],
2929    total_elements: u64,
2930    can_grow: bool,
2931) -> Result<()> {
2932    if variable.dtype != NcType::String {
2933        return Err(Error::TypeMismatch {
2934            expected: "String".into(),
2935            actual: format!("{:?}", variable.dtype),
2936        });
2937    }
2938    let expected = require_usize(total_elements, "string variable element count")?;
2939    let values = variable.string_values.get_or_insert_with(Vec::new);
2940    if values.is_empty() {
2941        values.resize(expected, String::new());
2942    } else if values.len() < expected && can_grow {
2943        resize_variable_values(values, old_shape, new_shape, String::new())?;
2944    } else if values.len() != expected {
2945        return Err(Error::DataLengthMismatch {
2946            expected,
2947            actual: values.len(),
2948        });
2949    }
2950
2951    variable.data.clear();
2952    variable.data_encoding = VariableDataEncoding::Hdf5Native;
2953    variable.vlen_values = None;
2954    Ok(())
2955}
2956
2957fn ensure_variable_vlen_slice_buffer(
2958    variable: &mut VariableDef,
2959    old_shape: &[u64],
2960    new_shape: &[u64],
2961    total_elements: u64,
2962    can_grow: bool,
2963) -> Result<()> {
2964    if !matches!(&variable.dtype, NcType::VLen { .. }) {
2965        return Err(Error::TypeMismatch {
2966            expected: "VLen".into(),
2967            actual: format!("{:?}", variable.dtype),
2968        });
2969    }
2970    let expected = require_usize(total_elements, "vlen variable element count")?;
2971    let values = variable.vlen_values.get_or_insert_with(Vec::new);
2972    if values.is_empty() {
2973        values.resize(expected, Vec::new());
2974    } else if values.len() < expected && can_grow {
2975        resize_variable_values(values, old_shape, new_shape, Vec::new())?;
2976    } else if values.len() != expected {
2977        return Err(Error::DataLengthMismatch {
2978            expected,
2979            actual: values.len(),
2980        });
2981    }
2982
2983    variable.data.clear();
2984    variable.data_encoding = VariableDataEncoding::Hdf5Native;
2985    variable.string_values = None;
2986    Ok(())
2987}
2988
2989fn resize_variable_data(
2990    data: &mut Vec<u8>,
2991    old_shape: &[u64],
2992    new_shape: &[u64],
2993    elem_size: usize,
2994    fill_pattern: &[u8],
2995) -> Result<()> {
2996    if old_shape.len() != new_shape.len() {
2997        return Err(Error::InvalidDefinition(format!(
2998            "cannot resize variable data from rank {} to rank {}",
2999            old_shape.len(),
3000            new_shape.len()
3001        )));
3002    }
3003    let old_elements = checked_shape_elements(old_shape, "old variable shape element count")?;
3004    let old_expected = checked_mul_usize(
3005        require_usize(old_elements, "old variable element count")?,
3006        elem_size,
3007        "old variable byte size",
3008    )?;
3009    if data.len() != old_expected {
3010        return Err(Error::DataLengthMismatch {
3011            expected: old_expected,
3012            actual: data.len(),
3013        });
3014    }
3015
3016    let new_elements = checked_shape_elements(new_shape, "new variable shape element count")?;
3017    let new_len = checked_mul_usize(
3018        require_usize(new_elements, "new variable element count")?,
3019        elem_size,
3020        "new variable byte size",
3021    )?;
3022    if old_shape.get(1..) == new_shape.get(1..) {
3023        let additional = new_len.checked_sub(data.len()).ok_or_else(|| {
3024            Error::InvalidDefinition("new variable byte size is smaller than old size".into())
3025        })?;
3026        data.extend(filled_data_buffer(additional, elem_size, fill_pattern)?);
3027        return Ok(());
3028    }
3029
3030    let old = std::mem::take(data);
3031    let mut resized = filled_data_buffer(new_len, elem_size, fill_pattern)?;
3032    copy_reshaped_variable_data(&old, &mut resized, old_shape, new_shape, elem_size)?;
3033    *data = resized;
3034    Ok(())
3035}
3036
3037fn resize_variable_values<T: Clone>(
3038    values: &mut Vec<T>,
3039    old_shape: &[u64],
3040    new_shape: &[u64],
3041    fill_value: T,
3042) -> Result<()> {
3043    if old_shape.len() != new_shape.len() {
3044        return Err(Error::InvalidDefinition(format!(
3045            "cannot resize variable values from rank {} to rank {}",
3046            old_shape.len(),
3047            new_shape.len()
3048        )));
3049    }
3050    let old_elements = require_usize(
3051        checked_shape_elements(old_shape, "old variable value shape element count")?,
3052        "old variable value element count",
3053    )?;
3054    if values.len() != old_elements {
3055        return Err(Error::DataLengthMismatch {
3056            expected: old_elements,
3057            actual: values.len(),
3058        });
3059    }
3060
3061    let new_len = require_usize(
3062        checked_shape_elements(new_shape, "new variable value shape element count")?,
3063        "new variable value element count",
3064    )?;
3065    if old_shape.get(1..) == new_shape.get(1..) {
3066        values.resize(new_len, fill_value);
3067        return Ok(());
3068    }
3069
3070    let old = std::mem::take(values);
3071    let mut resized = vec![fill_value; new_len];
3072    copy_reshaped_values(&old, &mut resized, old_shape, new_shape)?;
3073    *values = resized;
3074    Ok(())
3075}
3076
3077fn copy_reshaped_variable_data(
3078    old: &[u8],
3079    new: &mut [u8],
3080    old_shape: &[u64],
3081    new_shape: &[u64],
3082    elem_size: usize,
3083) -> Result<()> {
3084    let old_elements = checked_shape_elements(old_shape, "old variable copy element count")?;
3085    if old_elements == 0 {
3086        return Ok(());
3087    }
3088    if old_shape.is_empty() {
3089        if old.len() != elem_size || new.len() != elem_size {
3090            return Err(Error::DataLengthMismatch {
3091                expected: elem_size,
3092                actual: old.len().max(new.len()),
3093            });
3094        }
3095        new.copy_from_slice(old);
3096        return Ok(());
3097    }
3098
3099    let old_strides = row_major_strides(old_shape, "old variable copy stride")?;
3100    let new_strides = row_major_strides(new_shape, "new variable copy stride")?;
3101    let rank = old_shape.len();
3102    let mut coords = vec![0u64; rank];
3103    for old_index in 0..old_elements {
3104        let mut remainder = old_index;
3105        for dim in 0..rank {
3106            let stride = old_strides[dim];
3107            coords[dim] = remainder / stride;
3108            remainder %= stride;
3109        }
3110        if coords
3111            .iter()
3112            .zip(new_shape)
3113            .any(|(&coord, &extent)| coord >= extent)
3114        {
3115            continue;
3116        }
3117        let new_index =
3118            coords
3119                .iter()
3120                .zip(&new_strides)
3121                .try_fold(0u64, |acc, (&coord, &stride)| {
3122                    checked_add(
3123                        acc,
3124                        checked_mul_u64(coord, stride, "new variable copy coordinate")?,
3125                        "new variable copy element index",
3126                    )
3127                })?;
3128        let old_start = checked_mul_usize(
3129            require_usize(old_index, "old variable copy element")?,
3130            elem_size,
3131            "old variable copy byte offset",
3132        )?;
3133        let new_start = checked_mul_usize(
3134            require_usize(new_index, "new variable copy element")?,
3135            elem_size,
3136            "new variable copy byte offset",
3137        )?;
3138        let old_end = old_start.checked_add(elem_size).ok_or_else(|| {
3139            Error::InvalidDefinition("old variable copy byte range exceeds usize".into())
3140        })?;
3141        let new_end = new_start.checked_add(elem_size).ok_or_else(|| {
3142            Error::InvalidDefinition("new variable copy byte range exceeds usize".into())
3143        })?;
3144        new[new_start..new_end].copy_from_slice(&old[old_start..old_end]);
3145    }
3146    Ok(())
3147}
3148
3149fn copy_reshaped_values<T: Clone>(
3150    old: &[T],
3151    new: &mut [T],
3152    old_shape: &[u64],
3153    new_shape: &[u64],
3154) -> Result<()> {
3155    let old_elements = checked_shape_elements(old_shape, "old variable value copy element count")?;
3156    if old_elements == 0 {
3157        return Ok(());
3158    }
3159    if old_shape.is_empty() {
3160        if old.len() != 1 || new.len() != 1 {
3161            return Err(Error::DataLengthMismatch {
3162                expected: 1,
3163                actual: old.len().max(new.len()),
3164            });
3165        }
3166        new[0] = old[0].clone();
3167        return Ok(());
3168    }
3169
3170    let old_strides = row_major_strides(old_shape, "old variable value copy stride")?;
3171    let new_strides = row_major_strides(new_shape, "new variable value copy stride")?;
3172    let rank = old_shape.len();
3173    let mut coords = vec![0u64; rank];
3174    for old_index in 0..old_elements {
3175        let mut remainder = old_index;
3176        for dim in 0..rank {
3177            let stride = old_strides[dim];
3178            coords[dim] = remainder / stride;
3179            remainder %= stride;
3180        }
3181        if coords
3182            .iter()
3183            .zip(new_shape)
3184            .any(|(&coord, &extent)| coord >= extent)
3185        {
3186            continue;
3187        }
3188        let new_index =
3189            coords
3190                .iter()
3191                .zip(&new_strides)
3192                .try_fold(0u64, |acc, (&coord, &stride)| {
3193                    checked_add(
3194                        acc,
3195                        checked_mul_u64(coord, stride, "new variable value copy coordinate")?,
3196                        "new variable value copy element index",
3197                    )
3198                })?;
3199        let old_index = require_usize(old_index, "old variable value copy element")?;
3200        let new_index = require_usize(new_index, "new variable value copy element")?;
3201        new[new_index] = old[old_index].clone();
3202    }
3203    Ok(())
3204}
3205
3206fn filled_data_buffer(len: usize, elem_size: usize, fill_pattern: &[u8]) -> Result<Vec<u8>> {
3207    if elem_size == 0 {
3208        return if len == 0 {
3209            Ok(Vec::new())
3210        } else {
3211            Err(Error::InvalidDefinition(
3212                "non-empty variable data requires a non-zero element size".into(),
3213            ))
3214        };
3215    }
3216    if len % elem_size != 0 {
3217        return Err(Error::InvalidDefinition(format!(
3218            "variable byte length {len} is not a multiple of element size {elem_size}"
3219        )));
3220    }
3221    if fill_pattern.len() != elem_size {
3222        return Err(Error::InvalidDefinition(format!(
3223            "fill value byte length must match datatype element size: expected {elem_size}, got {}",
3224            fill_pattern.len()
3225        )));
3226    }
3227    let mut data = Vec::with_capacity(len);
3228    for _ in 0..len / elem_size {
3229        data.extend_from_slice(fill_pattern);
3230    }
3231    Ok(data)
3232}
3233
3234fn default_classic_fill_bytes(dtype: &NcType) -> Result<Vec<u8>> {
3235    match dtype {
3236        NcType::Byte => Ok(vec![NC_FILL_BYTE as u8]),
3237        NcType::Char => Ok(vec![NC_FILL_CHAR]),
3238        NcType::Short => Ok(NC_FILL_SHORT.to_be_bytes().to_vec()),
3239        NcType::Int => Ok(NC_FILL_INT.to_be_bytes().to_vec()),
3240        NcType::Float => Ok(NC_FILL_FLOAT.to_be_bytes().to_vec()),
3241        NcType::Double => Ok(NC_FILL_DOUBLE.to_be_bytes().to_vec()),
3242        NcType::UByte => Ok(vec![NC_FILL_UBYTE]),
3243        NcType::UShort => Ok(NC_FILL_USHORT.to_be_bytes().to_vec()),
3244        NcType::UInt => Ok(NC_FILL_UINT.to_be_bytes().to_vec()),
3245        NcType::Int64 => Ok(NC_FILL_INT64.to_be_bytes().to_vec()),
3246        NcType::UInt64 => Ok(NC_FILL_UINT64.to_be_bytes().to_vec()),
3247        other => Err(Error::UnsupportedFeature(format!(
3248            "{other:?} does not have a primitive NetCDF default fill value"
3249        ))),
3250    }
3251}
3252
3253fn scatter_slice_bytes(
3254    data: &mut [u8],
3255    elem_size: usize,
3256    dims: &[ResolvedWriteSelectionDim],
3257    strides: &[u64],
3258    encoded: &[u8],
3259) -> Result<()> {
3260    let mut src_elem = 0usize;
3261    scatter_slice_bytes_recursive(data, elem_size, dims, strides, encoded, 0, 0, &mut src_elem)?;
3262    let expected = encoded.len() / elem_size.max(1);
3263    if src_elem != expected {
3264        return Err(Error::InvalidDefinition(format!(
3265            "slice scatter wrote {src_elem} elements but expected {expected}"
3266        )));
3267    }
3268    Ok(())
3269}
3270
3271fn scatter_slice_values<T: Clone>(
3272    data: &mut [T],
3273    dims: &[ResolvedWriteSelectionDim],
3274    strides: &[u64],
3275    values: &[T],
3276) -> Result<()> {
3277    let mut src_elem = 0usize;
3278    scatter_slice_values_recursive(data, dims, strides, values, 0, 0, &mut src_elem)?;
3279    if src_elem != values.len() {
3280        return Err(Error::InvalidDefinition(format!(
3281            "slice scatter wrote {src_elem} elements but expected {}",
3282            values.len()
3283        )));
3284    }
3285    Ok(())
3286}
3287
3288fn scatter_slice_values_recursive<T: Clone>(
3289    data: &mut [T],
3290    dims: &[ResolvedWriteSelectionDim],
3291    strides: &[u64],
3292    values: &[T],
3293    dim: usize,
3294    dst_elem: u64,
3295    src_elem: &mut usize,
3296) -> Result<()> {
3297    if dim == dims.len() {
3298        let dst = require_usize(dst_elem, "slice destination element")?;
3299        if dst >= data.len() || *src_elem >= values.len() {
3300            return Err(Error::InvalidDefinition(
3301                "slice write exceeded planned value bounds".into(),
3302            ));
3303        }
3304        data[dst] = values[*src_elem].clone();
3305        *src_elem += 1;
3306        return Ok(());
3307    }
3308
3309    match dims[dim] {
3310        ResolvedWriteSelectionDim::Index(index) => {
3311            let next = checked_add(
3312                dst_elem,
3313                checked_mul_u64(index, strides[dim], "slice index stride")?,
3314                "slice destination element",
3315            )?;
3316            scatter_slice_values_recursive(data, dims, strides, values, dim + 1, next, src_elem)
3317        }
3318        ResolvedWriteSelectionDim::Slice { start, step, count } => {
3319            for offset in 0..count {
3320                let coord = checked_add(
3321                    start,
3322                    checked_mul_u64(offset as u64, step, "slice coordinate step")?,
3323                    "slice coordinate",
3324                )?;
3325                let next = checked_add(
3326                    dst_elem,
3327                    checked_mul_u64(coord, strides[dim], "slice coordinate stride")?,
3328                    "slice destination element",
3329                )?;
3330                scatter_slice_values_recursive(
3331                    data,
3332                    dims,
3333                    strides,
3334                    values,
3335                    dim + 1,
3336                    next,
3337                    src_elem,
3338                )?;
3339            }
3340            Ok(())
3341        }
3342    }
3343}
3344
3345#[allow(clippy::too_many_arguments)]
3346fn scatter_slice_bytes_recursive(
3347    data: &mut [u8],
3348    elem_size: usize,
3349    dims: &[ResolvedWriteSelectionDim],
3350    strides: &[u64],
3351    encoded: &[u8],
3352    dim: usize,
3353    dst_elem: u64,
3354    src_elem: &mut usize,
3355) -> Result<()> {
3356    if dim == dims.len() {
3357        let dst_start = checked_mul_usize(
3358            require_usize(dst_elem, "slice destination element")?,
3359            elem_size,
3360            "slice destination byte offset",
3361        )?;
3362        let dst_end = dst_start.checked_add(elem_size).ok_or_else(|| {
3363            Error::InvalidDefinition("slice destination byte range exceeds usize".into())
3364        })?;
3365        let src_start = checked_mul_usize(*src_elem, elem_size, "slice source byte offset")?;
3366        let src_end = src_start.checked_add(elem_size).ok_or_else(|| {
3367            Error::InvalidDefinition("slice source byte range exceeds usize".into())
3368        })?;
3369        if dst_end > data.len() || src_end > encoded.len() {
3370            return Err(Error::InvalidDefinition(
3371                "slice write exceeded planned data bounds".into(),
3372            ));
3373        }
3374        data[dst_start..dst_end].copy_from_slice(&encoded[src_start..src_end]);
3375        *src_elem += 1;
3376        return Ok(());
3377    }
3378
3379    match dims[dim] {
3380        ResolvedWriteSelectionDim::Index(index) => {
3381            let offset = checked_add(
3382                dst_elem,
3383                checked_mul_u64(index, strides[dim], "slice index stride")?,
3384                "slice destination element",
3385            )?;
3386            scatter_slice_bytes_recursive(
3387                data,
3388                elem_size,
3389                dims,
3390                strides,
3391                encoded,
3392                dim + 1,
3393                offset,
3394                src_elem,
3395            )
3396        }
3397        ResolvedWriteSelectionDim::Slice { start, step, count } => {
3398            for i in 0..count {
3399                let coord = checked_add(
3400                    start,
3401                    checked_mul_u64(i as u64, step, "slice coordinate step")?,
3402                    "slice coordinate",
3403                )?;
3404                let offset = checked_add(
3405                    dst_elem,
3406                    checked_mul_u64(coord, strides[dim], "slice coordinate stride")?,
3407                    "slice destination element",
3408                )?;
3409                scatter_slice_bytes_recursive(
3410                    data,
3411                    elem_size,
3412                    dims,
3413                    strides,
3414                    encoded,
3415                    dim + 1,
3416                    offset,
3417                    src_elem,
3418                )?;
3419            }
3420            Ok(())
3421        }
3422    }
3423}
3424
3425fn row_major_strides(shape: &[u64], context: &str) -> Result<Vec<u64>> {
3426    if shape.is_empty() {
3427        return Ok(Vec::new());
3428    }
3429    let mut strides = vec![1u64; shape.len()];
3430    for dim in (0..shape.len() - 1).rev() {
3431        strides[dim] = checked_mul_u64(strides[dim + 1], shape[dim + 1], context)?;
3432    }
3433    Ok(strides)
3434}
3435
3436fn checked_shape_elements(shape: &[u64], context: &str) -> Result<u64> {
3437    shape
3438        .iter()
3439        .try_fold(1u64, |acc, &dim| checked_mul_u64(acc, dim, context))
3440}
3441
3442#[cfg(feature = "netcdf4")]
3443fn validate_nc4_variable_data(variable: &VariableDef, dimension_sizes: &[u64]) -> Result<()> {
3444    let elements = nc4_variable_elements(variable, dimension_sizes)?;
3445    validate_nc4_variable_storage(variable)?;
3446    validate_nc4_variable_fill_value(variable)?;
3447    if variable.dtype == NcType::String {
3448        let values = variable.string_values.as_ref().ok_or_else(|| {
3449            Error::InvalidDefinition(format!(
3450                "NC_STRING variable '{}' has no string data",
3451                variable.name
3452            ))
3453        })?;
3454        let expected = checked_usize(elements, "NetCDF-4 string variable element count")?;
3455        if values.len() != expected {
3456            return Err(Error::DataLengthMismatch {
3457                expected,
3458                actual: values.len(),
3459            });
3460        }
3461        return Ok(());
3462    }
3463    if let NcType::VLen { base } = &variable.dtype {
3464        let values = variable.vlen_values.as_ref().ok_or_else(|| {
3465            Error::InvalidDefinition(format!(
3466                "NC_VLEN variable '{}' has no sequence data",
3467                variable.name
3468            ))
3469        })?;
3470        validate_vlen_base_nc4_type(base)?;
3471        let expected = checked_usize(elements, "NetCDF-4 vlen variable element count")?;
3472        if values.len() != expected {
3473            return Err(Error::DataLengthMismatch {
3474                expected,
3475                actual: values.len(),
3476            });
3477        }
3478        let base_size = base.size()?;
3479        for value in values {
3480            if value.len() % base_size != 0 {
3481                return Err(Error::InvalidDefinition(format!(
3482                    "NC_VLEN variable '{}' sequence byte length {} is not a multiple of base element size {base_size}",
3483                    variable.name,
3484                    value.len()
3485                )));
3486            }
3487        }
3488        return Ok(());
3489    }
3490
3491    let bytes = checked_mul_u64(
3492        elements,
3493        variable.dtype.size()? as u64,
3494        "NetCDF-4 variable byte size",
3495    )?;
3496    let expected = usize::try_from(bytes).map_err(|_| {
3497        Error::InvalidDefinition("NetCDF-4 variable byte size exceeds platform usize".into())
3498    })?;
3499    if variable.data.is_empty() && variable.fill_value.is_some() {
3500        return Ok(());
3501    }
3502    if variable.data.len() != expected {
3503        return Err(Error::DataLengthMismatch {
3504            expected,
3505            actual: variable.data.len(),
3506        });
3507    }
3508    Ok(())
3509}
3510
3511#[cfg(feature = "netcdf4")]
3512fn validate_nc4_variable_fill_value(variable: &VariableDef) -> Result<()> {
3513    let Some(fill_value) = &variable.fill_value else {
3514        return Ok(());
3515    };
3516    if matches!(&variable.dtype, NcType::String | NcType::VLen { .. }) {
3517        return Err(Error::UnsupportedFeature(format!(
3518            "NetCDF-4 variable-length fill values are not supported yet: '{}'",
3519            variable.name
3520        )));
3521    }
3522    let expected = nc4_hdf5_element_size(&variable.dtype)?;
3523    if fill_value.hdf5_bytes.len() != expected {
3524        return Err(Error::InvalidDefinition(format!(
3525            "variable '{}' fill value byte length must match datatype element size: expected {expected}, got {}",
3526            variable.name,
3527            fill_value.hdf5_bytes.len()
3528        )));
3529    }
3530    Ok(())
3531}
3532
3533#[cfg(feature = "netcdf4")]
3534fn nc4_variable_elements(variable: &VariableDef, dimension_sizes: &[u64]) -> Result<u64> {
3535    let elements = variable.dim_ids.iter().try_fold(1u64, |acc, id| {
3536        checked_mul_u64(
3537            acc,
3538            dimension_sizes[id.0],
3539            "NetCDF-4 variable element count",
3540        )
3541    })?;
3542    Ok(elements)
3543}
3544
3545#[cfg(feature = "netcdf4")]
3546fn infer_nc4_dimension_sizes(builder: &NcFileBuilder) -> Result<Vec<u64>> {
3547    let mut sizes = builder
3548        .dimensions
3549        .iter()
3550        .map(|dimension| {
3551            if dimension.is_unlimited {
3552                (dimension.current_size > 0).then_some(dimension.current_size)
3553            } else {
3554                Some(dimension.size)
3555            }
3556        })
3557        .collect::<Vec<_>>();
3558
3559    loop {
3560        let mut changed = false;
3561        for variable in &builder.variables {
3562            changed |= infer_nc4_dimension_size_from_variable(builder, variable, &mut sizes)?;
3563        }
3564        if !changed {
3565            break;
3566        }
3567    }
3568
3569    for variable in &builder.variables {
3570        let unresolved = variable
3571            .dim_ids
3572            .iter()
3573            .copied()
3574            .filter(|id| sizes[id.0].is_none())
3575            .collect::<Vec<_>>();
3576        if !unresolved.is_empty() && variable_has_data(variable) {
3577            let names = unresolved
3578                .iter()
3579                .map(|id| builder.dimensions[id.0].name.as_str())
3580                .collect::<Vec<_>>()
3581                .join(", ");
3582            return Err(Error::InvalidDefinition(format!(
3583                "cannot infer current size for NetCDF-4 unlimited dimension(s) {names} from variable '{}'",
3584                variable.name
3585            )));
3586        }
3587    }
3588
3589    Ok(sizes.into_iter().map(|size| size.unwrap_or(0)).collect())
3590}
3591
3592#[cfg(feature = "netcdf4")]
3593fn infer_nc4_dimension_size_from_variable(
3594    builder: &NcFileBuilder,
3595    variable: &VariableDef,
3596    sizes: &mut [Option<u64>],
3597) -> Result<bool> {
3598    let (elements, elem_size, actual_bytes) = variable_payload_element_count(variable)?;
3599    let mut known_product = 1u64;
3600    let mut unknown = Vec::new();
3601    for dim_id in &variable.dim_ids {
3602        match sizes[dim_id.0] {
3603            Some(size) => {
3604                known_product =
3605                    checked_mul_u64(known_product, size, "NetCDF-4 known dimension product")?;
3606            }
3607            None if !unknown.contains(dim_id) => unknown.push(*dim_id),
3608            None => {
3609                return Err(Error::UnsupportedFeature(format!(
3610                    "NetCDF-4 variable '{}' repeats unlimited dimension '{}'",
3611                    variable.name, builder.dimensions[dim_id.0].name
3612                )));
3613            }
3614        }
3615    }
3616
3617    if unknown.len() != 1 || known_product == 0 {
3618        return Ok(false);
3619    }
3620    if elements % known_product != 0 {
3621        return Err(Error::DataLengthMismatch {
3622            expected: usize::try_from((elements / known_product + 1) * known_product * elem_size)
3623                .unwrap_or(usize::MAX),
3624            actual: actual_bytes,
3625        });
3626    }
3627    let inferred = elements / known_product;
3628    sizes[unknown[0].0] = Some(inferred);
3629    Ok(true)
3630}
3631
3632#[cfg(feature = "netcdf4")]
3633fn variable_has_data(variable: &VariableDef) -> bool {
3634    match &variable.dtype {
3635        NcType::String => variable
3636            .string_values
3637            .as_ref()
3638            .is_some_and(|values| !values.is_empty()),
3639        NcType::VLen { .. } => variable
3640            .vlen_values
3641            .as_ref()
3642            .is_some_and(|values| !values.is_empty()),
3643        _ => !variable.data.is_empty(),
3644    }
3645}
3646
3647#[cfg(feature = "netcdf4")]
3648fn variable_payload_element_count(variable: &VariableDef) -> Result<(u64, u64, usize)> {
3649    if variable.dtype == NcType::String {
3650        let values = variable.string_values.as_ref().map_or(0usize, Vec::len);
3651        return Ok((
3652            u64::try_from(values).map_err(|_| {
3653                Error::InvalidDefinition(
3654                    "NC_STRING variable element count exceeds u64 capacity".into(),
3655                )
3656            })?,
3657            1,
3658            values,
3659        ));
3660    }
3661    if matches!(&variable.dtype, NcType::VLen { .. }) {
3662        let values = variable.vlen_values.as_ref().map_or(0usize, Vec::len);
3663        return Ok((
3664            u64::try_from(values).map_err(|_| {
3665                Error::InvalidDefinition(
3666                    "NC_VLEN variable element count exceeds u64 capacity".into(),
3667                )
3668            })?,
3669            1,
3670            values,
3671        ));
3672    }
3673
3674    let elem_size = variable.dtype.size()? as u64;
3675    let data_len = variable.data.len() as u64;
3676    if data_len % elem_size != 0 {
3677        return Err(Error::DataLengthMismatch {
3678            expected: usize::try_from((data_len / elem_size + 1) * elem_size).unwrap_or(usize::MAX),
3679            actual: variable.data.len(),
3680        });
3681    }
3682    Ok((data_len / elem_size, elem_size, variable.data.len()))
3683}
3684
3685fn variable_shape_elements(
3686    builder: &NcFileBuilder,
3687    variable: &VariableDef,
3688    skip_dims: usize,
3689) -> Result<u64> {
3690    variable
3691        .dim_ids
3692        .iter()
3693        .skip(skip_dims)
3694        .try_fold(1u64, |acc, id| {
3695            let dim = &builder.dimensions[id.0];
3696            acc.checked_mul(dim.size).ok_or_else(|| {
3697                Error::InvalidDefinition(format!(
3698                    "variable '{}' element count overflows u64",
3699                    variable.name
3700                ))
3701            })
3702        })
3703}
3704
3705fn variable_is_record(builder: &NcFileBuilder, variable: &VariableDef) -> Result<bool> {
3706    Ok(variable
3707        .dim_ids
3708        .first()
3709        .is_some_and(|id| builder.dimensions[id.0].is_unlimited))
3710}
3711
3712fn infer_num_records(builder: &NcFileBuilder) -> Result<u64> {
3713    let mut records: Option<u64> = None;
3714    for variable in &builder.variables {
3715        if !variable_is_record(builder, variable)? {
3716            continue;
3717        }
3718        let current = infer_num_records_for_var(builder, variable)?;
3719        match records {
3720            Some(existing) if existing != current => {
3721                return Err(Error::InvalidDefinition(
3722                    "all record variables must contain the same record count".into(),
3723                ));
3724            }
3725            Some(_) => {}
3726            None => records = Some(current),
3727        }
3728    }
3729    Ok(records.unwrap_or(0))
3730}
3731
3732fn infer_num_records_for_var(builder: &NcFileBuilder, variable: &VariableDef) -> Result<u64> {
3733    let record_elems = variable_shape_elements(builder, variable, 1)?;
3734    let elem_size = variable.dtype.size()? as u64;
3735    let record_bytes = record_elems
3736        .checked_mul(elem_size)
3737        .ok_or_else(|| Error::InvalidDefinition("record byte size overflow".into()))?;
3738    if record_bytes == 0 {
3739        return Ok(0);
3740    }
3741    let data_len = variable.data.len() as u64;
3742    if data_len % record_bytes != 0 {
3743        return Err(Error::DataLengthMismatch {
3744            expected: usize::try_from((data_len / record_bytes + 1) * record_bytes)
3745                .unwrap_or(usize::MAX),
3746            actual: variable.data.len(),
3747        });
3748    }
3749    Ok(data_len / record_bytes)
3750}
3751
3752fn validate_classic_type(dtype: &NcType) -> Result<()> {
3753    if dtype.classic_type_code().is_none() {
3754        return Err(Error::RequiresNetcdf4 {
3755            reason: format!("the {dtype:?} datatype is not representable in classic NetCDF"),
3756        });
3757    }
3758    Ok(())
3759}
3760
3761fn validate_chunk_shape_for_variable(variable: &VariableDef, chunk_shape: &[u64]) -> Result<()> {
3762    if variable.dim_ids.is_empty() {
3763        return Err(Error::InvalidDefinition(format!(
3764            "chunked NetCDF-4 scalar variables are not supported: '{}'",
3765            variable.name
3766        )));
3767    }
3768    if chunk_shape.len() != variable.dim_ids.len() {
3769        return Err(Error::InvalidDefinition(format!(
3770            "variable '{}' chunk rank must match variable rank: expected {}, got {}",
3771            variable.name,
3772            variable.dim_ids.len(),
3773            chunk_shape.len()
3774        )));
3775    }
3776    if chunk_shape.contains(&0) {
3777        return Err(Error::InvalidDefinition(format!(
3778            "variable '{}' chunk dimensions must be non-zero",
3779            variable.name
3780        )));
3781    }
3782    Ok(())
3783}
3784
3785fn reject_scalar_filtered_variable(variable: &VariableDef) -> Result<()> {
3786    if variable.dim_ids.is_empty() {
3787        return Err(Error::InvalidDefinition(format!(
3788            "filtered NetCDF-4 scalar variables are not supported: '{}'",
3789            variable.name
3790        )));
3791    }
3792    Ok(())
3793}
3794
3795#[cfg(feature = "netcdf4")]
3796fn validate_nc4_variable_storage(variable: &VariableDef) -> Result<()> {
3797    if let Some(chunk_shape) = &variable.storage.chunk_shape {
3798        validate_chunk_shape_for_variable(variable, chunk_shape)?;
3799    }
3800    if variable.storage.has_filters() {
3801        reject_scalar_filtered_variable(variable)?;
3802    }
3803    Ok(())
3804}
3805
3806#[cfg(feature = "netcdf4")]
3807fn nc4_variable_chunk_shape(
3808    variable: &VariableDef,
3809    shape: &[u64],
3810    requires_chunking: bool,
3811) -> Result<Option<Vec<u64>>> {
3812    if let Some(chunk_shape) = &variable.storage.chunk_shape {
3813        validate_chunk_shape_for_variable(variable, chunk_shape)?;
3814        return Ok(Some(chunk_shape.clone()));
3815    }
3816    if requires_chunking || variable.storage.has_filters() {
3817        return Ok(Some(nc4_default_chunk_shape(
3818            shape,
3819            nc4_hdf5_element_size(&variable.dtype)?,
3820        )?));
3821    }
3822    Ok(None)
3823}
3824
3825#[cfg(feature = "netcdf4")]
3826fn nc4_variable_filters(variable: &VariableDef) -> Vec<H5FilterDescription> {
3827    let mut filters = Vec::new();
3828    if variable.storage.shuffle {
3829        filters.push(H5FilterDescription {
3830            id: H5_FILTER_SHUFFLE,
3831            name: None,
3832            client_data: Vec::new(),
3833        });
3834    }
3835    if let Some(level) = variable.storage.deflate_level {
3836        filters.push(H5FilterDescription {
3837            id: H5_FILTER_DEFLATE,
3838            name: None,
3839            client_data: vec![u32::from(level)],
3840        });
3841    }
3842    if variable.storage.fletcher32 {
3843        filters.push(H5FilterDescription {
3844            id: H5_FILTER_FLETCHER32,
3845            name: None,
3846            client_data: Vec::new(),
3847        });
3848    }
3849    filters
3850}
3851
3852fn validate_supported_user_defined_type(dtype: &NcType) -> Result<()> {
3853    if let NcType::VLen { base } = dtype {
3854        return validate_vlen_base_nc4_type(base);
3855    }
3856    if !matches!(
3857        dtype,
3858        NcType::Enum { .. }
3859            | NcType::Compound { .. }
3860            | NcType::Opaque { .. }
3861            | NcType::Array { .. }
3862    ) {
3863        return Err(Error::InvalidDefinition(format!(
3864            "user-defined variables require enum, compound, opaque, fixed-size array, or vlen type, got {dtype:?}"
3865        )));
3866    }
3867    validate_fixed_width_nc4_type(dtype)
3868}
3869
3870fn validate_fixed_width_user_defined_type(dtype: &NcType) -> Result<()> {
3871    if !matches!(
3872        dtype,
3873        NcType::Enum { .. }
3874            | NcType::Compound { .. }
3875            | NcType::Opaque { .. }
3876            | NcType::Array { .. }
3877    ) {
3878        return Err(Error::InvalidDefinition(format!(
3879            "raw user-defined variable bytes require enum, compound, opaque, or fixed-size array type, got {dtype:?}"
3880        )));
3881    }
3882    validate_fixed_width_nc4_type(dtype)
3883}
3884
3885fn validate_fixed_width_nc4_type(dtype: &NcType) -> Result<()> {
3886    match dtype {
3887        NcType::Byte
3888        | NcType::Char
3889        | NcType::Short
3890        | NcType::Int
3891        | NcType::Float
3892        | NcType::Double
3893        | NcType::UByte
3894        | NcType::UShort
3895        | NcType::UInt
3896        | NcType::Int64
3897        | NcType::UInt64
3898        | NcType::Opaque { .. } => Ok(()),
3899        NcType::Enum { base, .. } => match base.as_ref() {
3900            NcType::Byte
3901            | NcType::UByte
3902            | NcType::Short
3903            | NcType::UShort
3904            | NcType::Int
3905            | NcType::UInt
3906            | NcType::Int64
3907            | NcType::UInt64 => Ok(()),
3908            other => Err(Error::InvalidDefinition(format!(
3909                "enum base type must be fixed-width integer, got {other:?}"
3910            ))),
3911        },
3912        NcType::Compound { fields, .. } => {
3913            for field in fields {
3914                validate_fixed_width_nc4_type(&field.dtype)?;
3915            }
3916            Ok(())
3917        }
3918        NcType::Array { base, .. } => validate_fixed_width_nc4_type(base),
3919        NcType::String | NcType::VLen { .. } => Err(Error::UnsupportedFeature(format!(
3920            "{dtype:?} user-defined variable payloads require heap-backed sequence writing"
3921        ))),
3922    }
3923}
3924
3925fn validate_vlen_base_nc4_type(dtype: &NcType) -> Result<()> {
3926    if dtype.size()? == 0 {
3927        return Err(Error::InvalidDefinition(
3928            "vlen base type must have non-zero byte size".into(),
3929        ));
3930    }
3931    match dtype {
3932        NcType::Byte
3933        | NcType::Char
3934        | NcType::Short
3935        | NcType::Int
3936        | NcType::Float
3937        | NcType::Double
3938        | NcType::UByte
3939        | NcType::UShort
3940        | NcType::UInt
3941        | NcType::Int64
3942        | NcType::UInt64
3943        | NcType::Opaque { .. } => Ok(()),
3944        NcType::Enum { .. } => validate_fixed_width_nc4_type(dtype),
3945        NcType::Compound { fields, .. } => {
3946            for field in fields {
3947                validate_vlen_base_nc4_type(&field.dtype)?;
3948            }
3949            Ok(())
3950        }
3951        NcType::Array { base, .. } => validate_vlen_base_nc4_type(base),
3952        NcType::String | NcType::VLen { .. } => Err(Error::UnsupportedFeature(format!(
3953            "{dtype:?} cannot be used as a vlen base until recursive heap-backed payload writing is implemented"
3954        ))),
3955    }
3956}
3957
3958#[cfg(feature = "netcdf4")]
3959fn validate_nc4_classic_attr_value(value: &NcAttrValue) -> Result<()> {
3960    if matches!(value, NcAttrValue::Strings(_)) {
3961        return Err(Error::UnsupportedFeature(
3962            "NC_STRING attributes require full NetCDF-4".into(),
3963        ));
3964    }
3965    Ok(())
3966}
3967
3968#[cfg(feature = "netcdf4")]
3969fn validate_nc4_classic_model(builder: &NcFileBuilder) -> Result<()> {
3970    validate_root_only_names(builder)?;
3971    let unlimited_count = builder
3972        .dimensions
3973        .iter()
3974        .filter(|dimension| dimension.is_unlimited)
3975        .count();
3976    if unlimited_count > 1 {
3977        return Err(Error::InvalidDefinition(
3978            "classic NetCDF supports at most one unlimited dimension".into(),
3979        ));
3980    }
3981    for variable in &builder.variables {
3982        if variable
3983            .dim_ids
3984            .iter()
3985            .skip(1)
3986            .any(|id| builder.dimensions[id.0].is_unlimited)
3987        {
3988            return Err(Error::InvalidDefinition(format!(
3989                "record dimension must be first for variable '{}'",
3990                variable.name
3991            )));
3992        }
3993    }
3994    Ok(())
3995}
3996
3997#[cfg(feature = "netcdf4")]
3998fn nc4_default_chunk_shape(shape: &[u64], element_size: usize) -> Result<Vec<u64>> {
3999    if shape.is_empty() {
4000        return Err(Error::InvalidDefinition(
4001            "chunked NetCDF-4 scalar variables are not supported".into(),
4002        ));
4003    }
4004    let element_size = element_size.max(1);
4005    let target_elements = (1024usize * 1024usize / element_size).max(1) as u64;
4006    let mut remaining = target_elements;
4007    let mut chunk_shape = vec![1u64; shape.len()];
4008    for dim in (0..shape.len()).rev() {
4009        let extent = shape[dim].max(1);
4010        let chunk = extent.min(remaining.max(1));
4011        chunk_shape[dim] = chunk;
4012        remaining = (remaining / chunk).max(1);
4013    }
4014    Ok(chunk_shape)
4015}
4016
4017fn attr_requires_cdf5(value: &NcAttrValue) -> bool {
4018    matches!(
4019        value,
4020        NcAttrValue::UBytes(_)
4021            | NcAttrValue::UShorts(_)
4022            | NcAttrValue::UInts(_)
4023            | NcAttrValue::Int64s(_)
4024            | NcAttrValue::UInt64s(_)
4025    )
4026}
4027
4028fn validate_name(name: &str, kind: &str) -> Result<()> {
4029    if name.is_empty() {
4030        return Err(Error::InvalidDefinition(format!(
4031            "{kind} name must not be empty"
4032        )));
4033    }
4034    if name.contains('/') || name.bytes().any(|b| b == 0) {
4035        return Err(Error::InvalidDefinition(format!(
4036            "{kind} name '{name}' contains invalid characters"
4037        )));
4038    }
4039    Ok(())
4040}
4041
4042fn validate_path_name(path: String, kind: &str) -> Result<String> {
4043    let trimmed = path.trim_matches('/').to_string();
4044    if trimmed.is_empty() {
4045        return Err(Error::InvalidDefinition(format!(
4046            "{kind} path must not be empty"
4047        )));
4048    }
4049    for component in trimmed.split('/') {
4050        validate_name(component, kind)?;
4051    }
4052    Ok(trimmed)
4053}
4054
4055#[cfg(feature = "netcdf4")]
4056fn path_leaf_name(path: &str) -> &str {
4057    path.rsplit('/').next().unwrap_or(path)
4058}
4059
4060fn validate_root_only_names(builder: &NcFileBuilder) -> Result<()> {
4061    for dimension in &builder.dimensions {
4062        if dimension.name.contains('/') {
4063            return Err(Error::RequiresNetcdf4 {
4064                reason: format!("grouped dimension '{}' needs NetCDF-4", dimension.name),
4065            });
4066        }
4067    }
4068    for variable in &builder.variables {
4069        if variable.name.contains('/') {
4070            return Err(Error::RequiresNetcdf4 {
4071                reason: format!("grouped variable '{}' needs NetCDF-4", variable.name),
4072            });
4073        }
4074    }
4075    if let Some(group_attr) = builder.group_attributes.first() {
4076        return Err(Error::RequiresNetcdf4 {
4077            reason: format!(
4078                "group attribute on '{}' needs NetCDF-4",
4079                group_attr.group_path
4080            ),
4081        });
4082    }
4083    Ok(())
4084}
4085
4086fn ensure_unique_attr(attrs: &[NcAttribute], name: &str) -> Result<()> {
4087    if attrs.iter().any(|a| a.name == name) {
4088        return Err(Error::InvalidDefinition(format!(
4089            "duplicate attribute '{name}'"
4090        )));
4091    }
4092    Ok(())
4093}
4094
4095fn set_variable_fill_value_metadata(
4096    variable: &mut VariableDef,
4097    value: NcAttrValue,
4098    classic_bytes: Vec<u8>,
4099    hdf5_bytes: Vec<u8>,
4100) -> Result<()> {
4101    if let Some(attribute) = variable
4102        .attributes
4103        .iter_mut()
4104        .find(|attribute| attribute.name == FILL_VALUE_ATTR_NAME)
4105    {
4106        if variable.fill_value.is_none() {
4107            return Err(Error::InvalidDefinition(format!(
4108                "variable '{}' already has a manual _FillValue attribute",
4109                variable.name
4110            )));
4111        }
4112        attribute.value = value;
4113    } else {
4114        variable.attributes.push(NcAttribute {
4115            name: FILL_VALUE_ATTR_NAME.to_string(),
4116            value,
4117        });
4118    }
4119
4120    variable.fill_value = Some(VariableFillValue {
4121        classic_bytes,
4122        hdf5_bytes,
4123    });
4124    Ok(())
4125}
4126
4127fn write_name(out: &mut Vec<u8>, format: NcFormat, name: &str) -> Result<()> {
4128    write_count(out, format, name.len() as u64)?;
4129    out.extend_from_slice(name.as_bytes());
4130    pad_vec_to_4(out);
4131    Ok(())
4132}
4133
4134fn write_count(out: &mut Vec<u8>, format: NcFormat, value: u64) -> Result<()> {
4135    match format {
4136        NcFormat::Cdf5 => write_u64(out, value),
4137        NcFormat::Classic | NcFormat::Offset64 => write_u32(out, require_u32(value, "count")?),
4138        _ => unreachable!("classic count only"),
4139    }
4140    Ok(())
4141}
4142
4143fn write_u32(out: &mut Vec<u8>, value: u32) {
4144    out.extend_from_slice(&value.to_be_bytes());
4145}
4146
4147fn write_u64(out: &mut Vec<u8>, value: u64) {
4148    out.extend_from_slice(&value.to_be_bytes());
4149}
4150
4151fn pad_vec_to_4(out: &mut Vec<u8>) {
4152    let pad = netcdf_core::padding_to_4(out.len());
4153    out.resize(out.len() + pad, 0);
4154}
4155
4156/// Write up to 3 padding bytes, cycling the variable's fill pattern the way
4157/// netcdf-c fills the slack between a value slab and its 4-byte boundary.
4158fn write_fill_padding(writer: &mut impl Write, len: u64, fill_pattern: &[u8]) -> Result<()> {
4159    if len == 0 {
4160        return Ok(());
4161    }
4162    let len = usize::try_from(len)
4163        .map_err(|_| Error::InvalidDefinition("padding exceeds platform usize".into()))?;
4164    let mut pad = [0u8; 4];
4165    if !fill_pattern.is_empty() {
4166        for (i, byte) in pad.iter_mut().enumerate().take(len.min(4)) {
4167            *byte = fill_pattern[i % fill_pattern.len()];
4168        }
4169    }
4170    writer.write_all(&pad[..len.min(4)])?;
4171    Ok(())
4172}
4173
4174fn pad4_u64(value: u64) -> Result<u64> {
4175    let rem = value % 4;
4176    if rem == 0 {
4177        Ok(value)
4178    } else {
4179        checked_add(value, 4 - rem, "4-byte padding")
4180    }
4181}
4182
4183fn checked_add(lhs: u64, rhs: u64, context: &str) -> Result<u64> {
4184    lhs.checked_add(rhs)
4185        .ok_or_else(|| Error::InvalidDefinition(format!("{context} overflow")))
4186}
4187
4188fn checked_mul_u64(lhs: u64, rhs: u64, context: &str) -> Result<u64> {
4189    lhs.checked_mul(rhs)
4190        .ok_or_else(|| Error::InvalidDefinition(format!("{context} overflow")))
4191}
4192
4193fn checked_mul_usize(lhs: usize, rhs: usize, context: &str) -> Result<usize> {
4194    lhs.checked_mul(rhs)
4195        .ok_or_else(|| Error::InvalidDefinition(format!("{context} overflow")))
4196}
4197
4198fn require_u32(value: u64, context: &str) -> Result<u32> {
4199    u32::try_from(value).map_err(|_| Error::FormatCapacityExceeded {
4200        reason: format!("{context} exceeds the 32-bit classic field capacity"),
4201    })
4202}
4203
4204fn require_usize(value: u64, context: &str) -> Result<usize> {
4205    usize::try_from(value)
4206        .map_err(|_| Error::InvalidDefinition(format!("{context} exceeds platform usize capacity")))
4207}