1use std::path::Path;
9
10use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
11use crate::document::html::{HtmlBlock, render_blocks_to_pages};
12use crate::error::{Error, Result};
13use crate::table::{TableAlign, TableData};
14
15const MAX_NETCDF_BYTES: u64 = 512 * 1024 * 1024;
16const MAX_NETCDF_DIMS: usize = 1024;
17const MAX_NETCDF_VARS: usize = 1024;
18const MAX_NETCDF_ATTRIBUTES: usize = 4096;
19const MAX_NETCDF_NAME_BYTES: usize = 4096;
20const MAX_NETCDF_VALUES_PER_VAR: usize = 200_000;
21const MAX_NETCDF_TOTAL_VALUES: usize = 1_000_000;
22const MAX_NETCDF_TEXT_BYTES: usize = 64 * 1024 * 1024;
23const MAX_NETCDF_VALUE_CHARS: usize = 512;
24
25pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
26 bytes.len() >= 4 && bytes[..3] == *b"CDF" && matches!(bytes[3], 1 | 2 | 5)
27}
28
29pub(crate) fn convert(
30 path: &Path,
31 options: &ConvertOptions,
32 sink: &mut dyn PageConsumer,
33) -> Result<Vec<String>> {
34 convert_with_profile(path, options, sink, "netcdf", "NetCDF dataset", None)
35}
36
37pub(crate) fn convert_exodus(
38 path: &Path,
39 options: &ConvertOptions,
40 sink: &mut dyn PageConsumer,
41) -> Result<Vec<String>> {
42 convert_with_profile(
43 path,
44 options,
45 sink,
46 "exodus",
47 "Exodus II dataset",
48 Some(
49 "Exodus-specific mesh topology, element blocks, sets, result semantics, and solver operations are not reconstructed",
50 ),
51 )
52}
53
54fn convert_with_profile(
55 path: &Path,
56 options: &ConvertOptions,
57 sink: &mut dyn PageConsumer,
58 source_format: &'static str,
59 title: &'static str,
60 profile_warning: Option<&'static str>,
61) -> Result<Vec<String>> {
62 let bytes = read_limited_file(
63 path,
64 options.max_input_bytes.min(MAX_NETCDF_BYTES),
65 if source_format == "exodus" {
66 "Exodus II input"
67 } else {
68 "NetCDF input"
69 },
70 )?;
71 let (dataset, mut warnings) = parse_netcdf(bytes)?;
72 if let Some(profile_warning) = profile_warning {
73 warnings.push(profile_warning.into());
74 }
75 let mut blocks = Vec::new();
76 blocks.push(HtmlBlock::Heading {
77 level: 1,
78 text: if source_format == "netcdf" {
79 format!("NetCDF {} dataset", dataset.variant)
80 } else {
81 format!("{title} ({})", dataset.variant)
82 },
83 });
84 if !dataset.dimensions.is_empty() {
85 blocks.push(HtmlBlock::Heading {
86 level: 2,
87 text: "Dimensions".into(),
88 });
89 blocks.push(HtmlBlock::Table(TableData {
90 headers: vec!["Name".into(), "Length".into(), "Role".into()],
91 rows: dataset
92 .dimensions
93 .iter()
94 .map(|dimension| {
95 vec![
96 dimension.name.clone(),
97 if dimension.unlimited {
98 dataset.num_records.to_string()
99 } else {
100 dimension.length.to_string()
101 },
102 if dimension.unlimited {
103 "unlimited record dimension".into()
104 } else {
105 "fixed".into()
106 },
107 ]
108 })
109 .collect(),
110 alignments: vec![TableAlign::Left, TableAlign::Right, TableAlign::Left],
111 raw_source: String::new(),
112 }));
113 }
114 if !dataset.global_attributes.is_empty() {
115 blocks.push(HtmlBlock::Heading {
116 level: 2,
117 text: "Global attributes".into(),
118 });
119 blocks.push(HtmlBlock::Table(TableData {
120 headers: vec!["Name".into(), "Type".into(), "Value".into()],
121 rows: dataset
122 .global_attributes
123 .iter()
124 .map(|attribute| {
125 vec![
126 attribute.name.clone(),
127 attribute.kind.name().into(),
128 attribute.display_value(),
129 ]
130 })
131 .collect(),
132 alignments: vec![TableAlign::Left; 3],
133 raw_source: String::new(),
134 }));
135 }
136 for variable in &dataset.variables {
137 blocks.push(HtmlBlock::Heading {
138 level: 2,
139 text: format!(
140 "{} ({}, {})",
141 variable.name,
142 variable.kind.name(),
143 variable.dimension_label(&dataset.dimensions)
144 ),
145 });
146 if !variable.attributes.is_empty() {
147 blocks.push(HtmlBlock::Table(TableData {
148 headers: vec!["Attribute".into(), "Type".into(), "Value".into()],
149 rows: variable
150 .attributes
151 .iter()
152 .map(|attribute| {
153 vec![
154 attribute.name.clone(),
155 attribute.kind.name().into(),
156 attribute.display_value(),
157 ]
158 })
159 .collect(),
160 alignments: vec![TableAlign::Left; 3],
161 raw_source: String::new(),
162 }));
163 }
164 let rows = dataset.read_variable_rows(variable)?;
165 if rows.is_empty() {
166 warnings.push(format!(
167 "{source_format} variable '{}' has no values within the preview budget",
168 variable.name
169 ));
170 } else {
171 blocks.push(HtmlBlock::Table(TableData {
172 headers: vec!["Index".into(), "Value".into()],
173 rows,
174 alignments: vec![TableAlign::Right, TableAlign::Left],
175 raw_source: String::new(),
176 }));
177 }
178 }
179 let mut page_sink = NetcdfPageSink {
180 inner: sink,
181 source_format,
182 title,
183 warnings: &warnings,
184 };
185 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
186 warnings.sort();
187 warnings.dedup();
188 Ok(warnings)
189}
190
191struct NetcdfPageSink<'a> {
192 inner: &'a mut dyn PageConsumer,
193 source_format: &'static str,
194 title: &'static str,
195 warnings: &'a [String],
196}
197
198impl PageConsumer for NetcdfPageSink<'_> {
199 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
200 page.source_format = self.source_format.into();
201 page.title = self.title.into();
202 for warning in self.warnings {
203 page.warn(warning.clone());
204 }
205 self.inner.consume(page)
206 }
207}
208
209#[derive(Clone, Debug)]
210struct Dimension {
211 name: String,
212 length: usize,
213 unlimited: bool,
214}
215
216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
217enum NcType {
218 Byte,
219 Char,
220 Short,
221 Int,
222 Float,
223 Double,
224}
225
226impl NcType {
227 fn from_tag(tag: u32) -> Result<Self> {
228 match tag {
229 1 => Ok(Self::Byte),
230 2 => Ok(Self::Char),
231 3 => Ok(Self::Short),
232 4 => Ok(Self::Int),
233 5 => Ok(Self::Float),
234 6 => Ok(Self::Double),
235 _ => Err(Error::Unsupported(format!(
236 "NetCDF type tag {tag} is unsupported"
237 ))),
238 }
239 }
240
241 fn name(self) -> &'static str {
242 match self {
243 Self::Byte => "byte",
244 Self::Char => "char",
245 Self::Short => "short",
246 Self::Int => "int",
247 Self::Float => "float",
248 Self::Double => "double",
249 }
250 }
251
252 fn bytes(self) -> usize {
253 match self {
254 Self::Byte | Self::Char => 1,
255 Self::Short => 2,
256 Self::Int | Self::Float => 4,
257 Self::Double => 8,
258 }
259 }
260}
261
262#[derive(Clone, Debug)]
263struct Attribute {
264 name: String,
265 kind: NcType,
266 values: Vec<u8>,
267 count: usize,
268}
269
270impl Attribute {
271 fn display_value(&self) -> String {
272 if self.kind == NcType::Char {
273 let text = String::from_utf8_lossy(&self.values)
274 .trim_end_matches('\0')
275 .to_string();
276 return truncate(&text);
277 }
278 let mut values = Vec::new();
279 for index in 0..self.count.min(16) {
280 if let Some(value) = decode_value(self.kind, &self.values, index) {
281 values.push(value);
282 }
283 }
284 if self.count > 16 {
285 values.push("…".into());
286 }
287 truncate(&values.join(", "))
288 }
289}
290
291#[derive(Clone, Debug)]
292struct Variable {
293 name: String,
294 dimensions: Vec<usize>,
295 attributes: Vec<Attribute>,
296 kind: NcType,
297 vsize: u64,
298 begin: u64,
299}
300
301impl Variable {
302 fn record(&self, unlimited_dim: Option<usize>) -> bool {
303 self.dimensions.first().copied() == unlimited_dim
304 }
305
306 fn element_count(&self, dimensions: &[Dimension], records: usize) -> Result<usize> {
307 let mut count = 1usize;
308 for (index, dimension_id) in self.dimensions.iter().enumerate() {
309 let dimension = dimensions.get(*dimension_id).ok_or_else(|| {
310 Error::InvalidInput(format!(
311 "NetCDF variable '{}' references unknown dimension {}",
312 self.name, dimension_id
313 ))
314 })?;
315 let length = if index == 0 && dimension.unlimited {
316 records
317 } else {
318 dimension.length
319 };
320 count = count
321 .checked_mul(length)
322 .ok_or_else(|| Error::LimitExceeded("NetCDF variable size overflowed".into()))?;
323 }
324 Ok(count)
325 }
326
327 fn dimension_label(&self, dimensions: &[Dimension]) -> String {
328 if self.dimensions.is_empty() {
329 return "scalar".into();
330 }
331 self.dimensions
332 .iter()
333 .filter_map(|id| dimensions.get(*id).map(|dimension| dimension.name.as_str()))
334 .collect::<Vec<_>>()
335 .join(" × ")
336 }
337
338 fn fill_value(&self) -> Option<String> {
339 self.attributes
340 .iter()
341 .find(|attribute| attribute.name == "_FillValue" && attribute.count == 1)
342 .and_then(|attribute| decode_value(attribute.kind, &attribute.values, 0))
343 }
344}
345
346#[derive(Debug)]
347struct Dataset {
348 variant: &'static str,
349 bytes: Vec<u8>,
350 dimensions: Vec<Dimension>,
351 global_attributes: Vec<Attribute>,
352 variables: Vec<Variable>,
353 num_records: usize,
354 record_size: u64,
355}
356
357impl Dataset {
358 fn read_variable_rows(&self, variable: &Variable) -> Result<Vec<Vec<String>>> {
359 let unlimited = self
360 .dimensions
361 .iter()
362 .position(|dimension| dimension.unlimited);
363 let count = variable.element_count(&self.dimensions, self.num_records)?;
364 let wanted = count.min(MAX_NETCDF_VALUES_PER_VAR);
365 let mut rows = Vec::with_capacity(wanted);
366 let fill = variable.fill_value();
367 for index in 0..wanted {
368 let offset = self.value_offset(variable, index, unlimited)?;
369 let value = read_value(variable.kind, &self.bytes, offset)?;
370 let display = if fill.as_deref() == Some(value.as_str()) {
371 "_FillValue".into()
372 } else {
373 truncate(&value)
374 };
375 rows.push(vec![index.to_string(), display]);
376 }
377 Ok(rows)
378 }
379
380 fn value_offset(
381 &self,
382 variable: &Variable,
383 index: usize,
384 unlimited: Option<usize>,
385 ) -> Result<usize> {
386 if !variable.record(unlimited) {
387 let byte_offset = variable
388 .begin
389 .checked_add((index as u64).saturating_mul(variable.kind.bytes() as u64))
390 .ok_or_else(|| Error::LimitExceeded("NetCDF data offset overflowed".into()))?;
391 return usize::try_from(byte_offset).map_err(|_| {
392 Error::LimitExceeded("NetCDF data offset exceeds platform size".into())
393 });
394 }
395 if unlimited.is_none() {
396 return Err(Error::InvalidInput(
397 "NetCDF record variable has no unlimited dimension".into(),
398 ));
399 }
400 let inner_count = variable.element_count(&self.dimensions, 1)?.max(1);
401 let record_index = index / inner_count;
402 let inner_index = index % inner_count;
403 if record_index >= self.num_records {
404 return Err(Error::InvalidInput(
405 "NetCDF record index is outside numrecs".into(),
406 ));
407 }
408 let byte_offset = variable
409 .begin
410 .checked_add((record_index as u64).saturating_mul(self.record_size))
411 .and_then(|value| {
412 value.checked_add((inner_index as u64).saturating_mul(variable.kind.bytes() as u64))
413 })
414 .ok_or_else(|| Error::LimitExceeded("NetCDF record data offset overflowed".into()))?;
415 usize::try_from(byte_offset)
416 .map_err(|_| Error::LimitExceeded("NetCDF record offset exceeds platform size".into()))
417 }
418}
419
420fn parse_netcdf(bytes: Vec<u8>) -> Result<(Dataset, Vec<String>)> {
421 if bytes.len() < 4 || &bytes[..3] != b"CDF" {
422 return Err(Error::InvalidInput("NetCDF CDF magic is missing".into()));
423 }
424 let version = bytes[3];
425 if version == 5 {
426 return Err(Error::Unsupported(
427 "NetCDF CDF-5 uses a different 64-bit header model and is unsupported".into(),
428 ));
429 }
430 if !matches!(version, 1 | 2) {
431 return Err(Error::Unsupported(format!(
432 "NetCDF CDF version {version} is unsupported; expected CDF-1 or CDF-2"
433 )));
434 }
435 let mut reader = Reader::new(&bytes[4..], version == 2);
436 let numrecs_raw = reader.u32("numrecs")?;
437 let dimensions = reader.dimensions()?;
438 let global_attributes = reader.attributes("global attributes")?;
439 let variables = reader.variables()?;
440 let attribute_bytes = global_attributes
441 .iter()
442 .chain(
443 variables
444 .iter()
445 .flat_map(|variable| variable.attributes.iter()),
446 )
447 .map(|attribute| attribute.values.len())
448 .try_fold(0usize, |sum, value| sum.checked_add(value))
449 .ok_or_else(|| Error::LimitExceeded("NetCDF attribute byte size overflowed".into()))?;
450 if attribute_bytes > MAX_NETCDF_TEXT_BYTES {
451 return Err(Error::LimitExceeded(format!(
452 "NetCDF attribute data exceeds {MAX_NETCDF_TEXT_BYTES} bytes"
453 )));
454 }
455 if reader.position > bytes.len() {
456 return Err(Error::InvalidInput(
457 "NetCDF header extends beyond the file".into(),
458 ));
459 }
460 let unlimited = dimensions.iter().position(|dimension| dimension.unlimited);
461 if unlimited.is_none() && numrecs_raw != 0 && numrecs_raw != u32::MAX {
462 return Err(Error::InvalidInput(
463 "NetCDF has a nonzero record count but no unlimited dimension".into(),
464 ));
465 }
466 let num_records = if numrecs_raw == u32::MAX {
467 0
468 } else {
469 usize::try_from(numrecs_raw)
470 .map_err(|_| Error::LimitExceeded("NetCDF numrecs exceeds platform size".into()))?
471 };
472 let record_variables = variables
473 .iter()
474 .filter(|variable| variable.record(unlimited))
475 .collect::<Vec<_>>();
476 let mut record_size = record_variables.iter().try_fold(0u64, |sum, variable| {
477 sum.checked_add(variable.vsize)
478 .ok_or_else(|| Error::LimitExceeded("NetCDF record size overflowed".into()))
479 })?;
480 if record_variables.len() == 1
481 && matches!(
482 record_variables[0].kind,
483 NcType::Byte | NcType::Char | NcType::Short
484 )
485 {
486 let raw_record_values = record_variables[0].element_count(&dimensions, 1)?;
487 record_size =
488 (raw_record_values as u64).saturating_mul(record_variables[0].kind.bytes() as u64);
489 }
490 let mut effective_records = num_records;
491 let mut warnings = Vec::new();
492 if numrecs_raw == u32::MAX {
493 if record_size == 0 {
494 effective_records = 0;
495 } else {
496 let starts = record_variables
497 .iter()
498 .map(|variable| variable.begin)
499 .min()
500 .unwrap_or(bytes.len() as u64);
501 effective_records = ((bytes.len() as u64).saturating_sub(starts) / record_size)
502 .try_into()
503 .map_err(|_| {
504 Error::LimitExceeded("NetCDF streaming record count is too large".into())
505 })?;
506 }
507 warnings.push(
508 "NetCDF numrecs uses the streaming sentinel; record count was derived from file length"
509 .into(),
510 );
511 }
512 if effective_records > MAX_NETCDF_VALUES_PER_VAR {
513 return Err(Error::LimitExceeded(format!(
514 "NetCDF record count exceeds {MAX_NETCDF_VALUES_PER_VAR}"
515 )));
516 }
517 let mut total_values = 0usize;
518 for variable in &variables {
519 for (index, dimension_id) in variable.dimensions.iter().enumerate() {
520 if dimensions
521 .get(*dimension_id)
522 .is_some_and(|dimension| dimension.unlimited && index != 0)
523 {
524 return Err(Error::Unsupported(format!(
525 "NetCDF variable '{}' uses an unlimited dimension after its first dimension",
526 variable.name
527 )));
528 }
529 }
530 let count = variable.element_count(&dimensions, effective_records)?;
531 total_values = total_values
532 .checked_add(count.min(MAX_NETCDF_VALUES_PER_VAR))
533 .ok_or_else(|| Error::LimitExceeded("NetCDF total value count overflowed".into()))?;
534 if total_values > MAX_NETCDF_TOTAL_VALUES {
535 return Err(Error::LimitExceeded(format!(
536 "NetCDF values exceed {MAX_NETCDF_TOTAL_VALUES}"
537 )));
538 }
539 let data_bytes = if variable.record(unlimited) {
540 let per_record = if record_variables.len() == 1
541 && matches!(variable.kind, NcType::Byte | NcType::Char | NcType::Short)
542 {
543 record_size
544 } else {
545 variable.vsize
546 };
547 per_record.saturating_mul(effective_records as u64)
548 } else {
549 variable.vsize
550 };
551 if variable
552 .begin
553 .checked_add(data_bytes)
554 .is_none_or(|end| end > bytes.len() as u64)
555 {
556 return Err(Error::InvalidInput(format!(
557 "NetCDF variable '{}' data extends beyond the file",
558 variable.name
559 )));
560 }
561 }
562 let dataset = Dataset {
563 variant: if version == 1 { "CDF-1" } else { "CDF-2" },
564 bytes,
565 dimensions,
566 global_attributes,
567 variables,
568 num_records: effective_records,
569 record_size,
570 };
571 Ok((dataset, warnings))
572}
573
574struct Reader<'a> {
575 bytes: &'a [u8],
576 position: usize,
577 wide_offsets: bool,
578 unlimited: Option<usize>,
579}
580
581impl<'a> Reader<'a> {
582 fn new(bytes: &'a [u8], wide_offsets: bool) -> Self {
583 Self {
584 bytes,
585 position: 0,
586 wide_offsets,
587 unlimited: None,
588 }
589 }
590
591 fn bytes(&mut self, count: usize, context: &str) -> Result<&'a [u8]> {
592 let end = self
593 .position
594 .checked_add(count)
595 .ok_or_else(|| Error::LimitExceeded(format!("NetCDF {context} offset overflowed")))?;
596 let slice = self.bytes.get(self.position..end).ok_or_else(|| {
597 Error::InvalidInput(format!(
598 "NetCDF {context} ends before the header is complete"
599 ))
600 })?;
601 self.position = end;
602 Ok(slice)
603 }
604
605 fn u32(&mut self, context: &str) -> Result<u32> {
606 let bytes = self.bytes(4, context)?;
607 Ok(u32::from_be_bytes(bytes.try_into().unwrap()))
608 }
609
610 fn u64(&mut self, context: &str) -> Result<u64> {
611 let bytes = self.bytes(8, context)?;
612 Ok(u64::from_be_bytes(bytes.try_into().unwrap()))
613 }
614
615 fn offset(&mut self, context: &str) -> Result<u64> {
616 if self.wide_offsets {
617 self.u64(context)
618 } else {
619 Ok(u64::from(self.u32(context)?))
620 }
621 }
622
623 fn padded_bytes(&mut self, count: usize, context: &str) -> Result<Vec<u8>> {
624 let data = self.bytes(count, context)?.to_vec();
625 let padded = (count + 3) & !3;
626 if padded > count {
627 let _ = self.bytes(padded - count, "padding")?;
628 }
629 Ok(data)
630 }
631
632 fn name(&mut self, context: &str) -> Result<String> {
633 let length = usize::try_from(self.u32(context)?)
634 .map_err(|_| Error::LimitExceeded("NetCDF name length exceeds platform size".into()))?;
635 if length == 0 || length > MAX_NETCDF_NAME_BYTES {
636 return Err(Error::LimitExceeded(format!(
637 "NetCDF {context} name length {length} is outside the supported range"
638 )));
639 }
640 let bytes = self.padded_bytes(length, context)?;
641 if bytes.contains(&0) {
642 return Err(Error::InvalidInput(format!(
643 "NetCDF {context} name contains NUL"
644 )));
645 }
646 String::from_utf8(bytes).map_err(|error| {
647 Error::InvalidInput(format!("NetCDF {context} name is not UTF-8: {error}"))
648 })
649 }
650
651 fn dimensions(&mut self) -> Result<Vec<Dimension>> {
652 let tag = self.u32("dimension list tag")?;
653 if tag == 0 {
654 self.unlimited = None;
655 return Ok(Vec::new());
656 }
657 if tag != 10 {
658 return Err(Error::InvalidInput(format!(
659 "NetCDF dimension list has unexpected tag {tag}"
660 )));
661 }
662 let count = checked_count(self.u32("dimension count")?, MAX_NETCDF_DIMS, "dimensions")?;
663 let mut dimensions = Vec::with_capacity(count);
664 for index in 0..count {
665 let name = self.name("dimension")?;
666 let length = usize::try_from(self.u32("dimension length")?).map_err(|_| {
667 Error::LimitExceeded("NetCDF dimension length exceeds platform size".into())
668 })?;
669 let unlimited = length == 0;
670 if unlimited {
671 if self.unlimited.is_some() {
672 return Err(Error::InvalidInput(
673 "NetCDF declares more than one unlimited dimension".into(),
674 ));
675 }
676 self.unlimited = Some(index);
677 }
678 dimensions.push(Dimension {
679 name,
680 length,
681 unlimited,
682 });
683 }
684 Ok(dimensions)
685 }
686
687 fn attributes(&mut self, context: &str) -> Result<Vec<Attribute>> {
688 let tag = self.u32(&format!("{context} tag"))?;
689 if tag == 0 {
690 return Ok(Vec::new());
691 }
692 if tag != 12 {
693 return Err(Error::InvalidInput(format!(
694 "NetCDF {context} has unexpected tag {tag}"
695 )));
696 }
697 let count = checked_count(
698 self.u32(&format!("{context} count"))?,
699 MAX_NETCDF_ATTRIBUTES,
700 context,
701 )?;
702 let mut attributes = Vec::with_capacity(count);
703 for _ in 0..count {
704 let name = self.name("attribute")?;
705 let kind = NcType::from_tag(self.u32("attribute type")?)?;
706 let count = checked_count(
707 self.u32("attribute value count")?,
708 MAX_NETCDF_VALUES_PER_VAR,
709 "attribute values",
710 )?;
711 let bytes = count.checked_mul(kind.bytes()).ok_or_else(|| {
712 Error::LimitExceeded("NetCDF attribute byte size overflowed".into())
713 })?;
714 if bytes > MAX_NETCDF_TEXT_BYTES {
715 return Err(Error::LimitExceeded(
716 "NetCDF attribute values exceed the text budget".into(),
717 ));
718 }
719 let values = self.padded_bytes(bytes, "attribute values")?;
720 attributes.push(Attribute {
721 name,
722 kind,
723 values,
724 count,
725 });
726 }
727 Ok(attributes)
728 }
729
730 fn variables(&mut self) -> Result<Vec<Variable>> {
731 let tag = self.u32("variable list tag")?;
732 if tag == 0 {
733 return Ok(Vec::new());
734 }
735 if tag != 11 {
736 return Err(Error::InvalidInput(format!(
737 "NetCDF variable list has unexpected tag {tag}"
738 )));
739 }
740 let count = checked_count(self.u32("variable count")?, MAX_NETCDF_VARS, "variables")?;
741 let mut variables = Vec::with_capacity(count);
742 for _ in 0..count {
743 let name = self.name("variable")?;
744 let rank = checked_count(
745 self.u32("variable rank")?,
746 MAX_NETCDF_DIMS,
747 "variable dimensions",
748 )?;
749 let mut dimensions = Vec::with_capacity(rank);
750 for _ in 0..rank {
751 let dimension_id = checked_index(
752 self.u32("variable dimension id")?,
753 MAX_NETCDF_DIMS,
754 "variable dimension",
755 )?;
756 dimensions.push(dimension_id);
757 }
758 let attributes = self.attributes("variable attributes")?;
759 let kind = NcType::from_tag(self.u32("variable type")?)?;
760 let vsize = u64::from(self.u32("variable vsize")?);
761 let begin = self.offset("variable begin offset")?;
762 variables.push(Variable {
763 name,
764 dimensions,
765 attributes,
766 kind,
767 vsize,
768 begin,
769 });
770 }
771 Ok(variables)
772 }
773}
774
775fn checked_count(raw: u32, maximum: usize, context: &str) -> Result<usize> {
776 let count = usize::try_from(raw).map_err(|_| {
777 Error::LimitExceeded(format!("NetCDF {context} count exceeds platform size"))
778 })?;
779 if count > maximum {
780 return Err(Error::LimitExceeded(format!(
781 "NetCDF {context} count {count} exceeds {maximum}"
782 )));
783 }
784 Ok(count)
785}
786
787fn checked_index(raw: u32, maximum: usize, context: &str) -> Result<usize> {
788 let index = usize::try_from(raw).map_err(|_| {
789 Error::LimitExceeded(format!("NetCDF {context} index exceeds platform size"))
790 })?;
791 if index >= maximum {
792 return Err(Error::InvalidInput(format!(
793 "NetCDF {context} index {index} is out of range"
794 )));
795 }
796 Ok(index)
797}
798
799fn decode_value(kind: NcType, bytes: &[u8], index: usize) -> Option<String> {
800 let width = kind.bytes();
801 let start = index.checked_mul(width)?;
802 let value = bytes.get(start..start + width)?;
803 Some(match kind {
804 NcType::Byte => (value[0] as i8).to_string(),
805 NcType::Char => String::from_utf8_lossy(value).to_string(),
806 NcType::Short => i16::from_be_bytes(value.try_into().ok()?).to_string(),
807 NcType::Int => i32::from_be_bytes(value.try_into().ok()?).to_string(),
808 NcType::Float => f32::from_bits(u32::from_be_bytes(value.try_into().ok()?)).to_string(),
809 NcType::Double => f64::from_bits(u64::from_be_bytes(value.try_into().ok()?)).to_string(),
810 })
811}
812
813fn read_value(kind: NcType, bytes: &[u8], offset: usize) -> Result<String> {
814 let width = kind.bytes();
815 let value = bytes
816 .get(offset..offset.saturating_add(width))
817 .ok_or_else(|| {
818 Error::InvalidInput("NetCDF variable data ends before a complete value".into())
819 })?;
820 decode_value(kind, value, 0)
821 .ok_or_else(|| Error::InvalidInput("NetCDF variable value could not be decoded".into()))
822}
823
824fn truncate(value: &str) -> String {
825 value.chars().take(MAX_NETCDF_VALUE_CHARS).collect()
826}
827
828#[cfg(test)]
829mod tests {
830 use super::{looks_like_prefix, parse_netcdf};
831
832 fn tiny_cdf1() -> Vec<u8> {
833 let mut bytes = Vec::new();
834 bytes.extend_from_slice(b"CDF\x01");
835 bytes.extend_from_slice(&0u32.to_be_bytes()); bytes.extend_from_slice(&10u32.to_be_bytes()); bytes.extend_from_slice(&1u32.to_be_bytes());
838 bytes.extend_from_slice(&1u32.to_be_bytes());
839 bytes.extend_from_slice(b"x\0\0\0");
840 bytes.extend_from_slice(&3u32.to_be_bytes());
841 bytes.extend_from_slice(&0u32.to_be_bytes()); bytes.extend_from_slice(&11u32.to_be_bytes()); bytes.extend_from_slice(&1u32.to_be_bytes());
844 bytes.extend_from_slice(&1u32.to_be_bytes());
845 bytes.extend_from_slice(b"v\0\0\0");
846 bytes.extend_from_slice(&1u32.to_be_bytes()); bytes.extend_from_slice(&0u32.to_be_bytes()); bytes.extend_from_slice(&0u32.to_be_bytes()); bytes.extend_from_slice(&4u32.to_be_bytes()); bytes.extend_from_slice(&12u32.to_be_bytes()); bytes.extend_from_slice(&72u32.to_be_bytes()); for value in [1i32, 2, 3] {
853 bytes.extend_from_slice(&value.to_be_bytes());
854 }
855 bytes
856 }
857
858 fn single_char_record_cdf1() -> Vec<u8> {
859 let mut bytes = Vec::new();
860 bytes.extend_from_slice(b"CDF\x01");
861 bytes.extend_from_slice(&2u32.to_be_bytes());
862 bytes.extend_from_slice(&10u32.to_be_bytes());
863 bytes.extend_from_slice(&1u32.to_be_bytes());
864 bytes.extend_from_slice(&4u32.to_be_bytes());
865 bytes.extend_from_slice(b"time");
866 bytes.extend_from_slice(&0u32.to_be_bytes());
867 bytes.extend_from_slice(&0u32.to_be_bytes());
868 bytes.extend_from_slice(&11u32.to_be_bytes());
869 bytes.extend_from_slice(&1u32.to_be_bytes());
870 bytes.extend_from_slice(&1u32.to_be_bytes());
871 bytes.extend_from_slice(b"c\0\0\0");
872 bytes.extend_from_slice(&1u32.to_be_bytes());
873 bytes.extend_from_slice(&0u32.to_be_bytes());
874 bytes.extend_from_slice(&0u32.to_be_bytes());
875 bytes.extend_from_slice(&2u32.to_be_bytes());
876 bytes.extend_from_slice(&4u32.to_be_bytes());
877 let begin = bytes.len() + 4;
878 bytes.extend_from_slice(&(begin as u32).to_be_bytes());
879 bytes.extend_from_slice(b"ab");
880 bytes
881 }
882
883 #[test]
884 fn parses_big_endian_classic_dimensions_and_values() {
885 let bytes = tiny_cdf1();
886 let (dataset, warnings) = parse_netcdf(bytes).unwrap();
887 assert!(warnings.is_empty());
888 assert_eq!(dataset.variant, "CDF-1");
889 assert_eq!(dataset.variables[0].name, "v");
890 assert_eq!(
891 dataset.read_variable_rows(&dataset.variables[0]).unwrap()[1][1],
892 "2"
893 );
894 }
895
896 #[test]
897 fn recognizes_only_supported_cdf_magic_versions() {
898 assert!(looks_like_prefix(b"CDF\x01"));
899 assert!(looks_like_prefix(b"CDF\x02"));
900 assert!(looks_like_prefix(b"CDF\x05"));
901 assert!(!looks_like_prefix(b"HDF\x89"));
902 }
903
904 #[test]
905 fn handles_the_unpadded_single_byte_record_special_case() {
906 let bytes = single_char_record_cdf1();
907 let (dataset, _) = parse_netcdf(bytes).unwrap();
908 let rows = dataset.read_variable_rows(&dataset.variables[0]).unwrap();
909 assert_eq!(rows[0][1], "a");
910 assert_eq!(rows[1][1], "b");
911 }
912
913 #[test]
914 fn rejects_unexpected_header_list_tags() {
915 let mut bytes = tiny_cdf1();
916 bytes[8..12].copy_from_slice(&99u32.to_be_bytes());
917 let error = parse_netcdf(bytes).unwrap_err();
918 assert!(error.to_string().contains("unexpected tag"));
919 }
920}