1use ndarray::ArrayD;
7
8use crate::error::{Error, Result};
9use crate::types::{
10 checked_mul_u64, checked_shape_elements, checked_usize_from_u64, NcType, NcVariable,
11};
12
13use super::data::{self, compute_record_stride, NcReadType};
14use super::ClassicFile;
15
16#[derive(Clone, Debug)]
17enum ResolvedClassicSelectionDim {
18 Index(u64),
19 Slice {
20 start: u64,
21 step: u64,
22 count: usize,
23 is_full_unit_stride: bool,
24 },
25}
26
27impl ResolvedClassicSelectionDim {
28 fn is_full_unit_stride(&self) -> bool {
29 matches!(
30 self,
31 Self::Slice {
32 is_full_unit_stride: true,
33 ..
34 }
35 )
36 }
37}
38
39#[derive(Clone, Debug)]
40struct ResolvedClassicSelection {
41 dims: Vec<ResolvedClassicSelectionDim>,
42 result_shape: Vec<usize>,
43 result_elements: usize,
44}
45
46struct BlockReadContext<'a> {
47 file_data: &'a [u8],
48 var_name: &'a str,
49 base_offset: usize,
50 plan: &'a ContiguousSelectionPlan,
51}
52
53struct ContiguousSelectionPlan {
54 dims: Vec<ResolvedClassicSelectionDim>,
55 strides: Vec<u64>,
56 tail_start: usize,
57 block_elements: usize,
58 block_bytes: usize,
59 elem_size: u64,
60}
61
62struct RecordSliceContext<'a> {
63 file_data: &'a [u8],
64 var_name: &'a str,
65 base_offset: usize,
66 record_stride: usize,
67 inner_resolved: &'a ResolvedClassicSelection,
68 inner_plan: &'a ContiguousSelectionPlan,
69}
70
71impl ClassicFile {
72 pub fn read_variable<T: NcReadType>(&self, name: &str) -> Result<ArrayD<T>> {
77 let var = self.find_variable(name)?;
78
79 let expected = T::nc_type();
81 if var.dtype != expected {
82 return Err(Error::TypeMismatch {
83 expected: format!("{:?}", expected),
84 actual: format!("{:?}", var.dtype),
85 });
86 }
87
88 let file_data = self.data.as_slice();
89
90 if var.is_record_var {
91 let record_stride = compute_record_stride(&self.root_group.variables);
92 data::read_record_variable(file_data, var, self.numrecs, record_stride)
93 } else {
94 data::read_non_record_variable(file_data, var)
95 }
96 }
97
98 pub fn read_variable_into<T: NcReadType>(&self, name: &str, dst: &mut [T]) -> Result<()> {
100 let var = self.find_variable(name)?;
101
102 let expected = T::nc_type();
103 if var.dtype != expected {
104 return Err(Error::TypeMismatch {
105 expected: format!("{:?}", expected),
106 actual: format!("{:?}", var.dtype),
107 });
108 }
109
110 let file_data = self.data.as_slice();
111
112 if var.is_record_var {
113 let record_stride = compute_record_stride(&self.root_group.variables);
114 data::read_record_variable_into(file_data, var, self.numrecs, record_stride, dst)
115 } else {
116 data::read_non_record_variable_into(file_data, var, dst)
117 }
118 }
119
120 pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
125 let var = self.find_variable(name)?;
126 let file_data = self.data.as_slice();
127
128 match var.dtype {
129 NcType::Byte => {
130 let arr = self.read_typed_variable::<i8>(var, file_data)?;
131 Ok(arr.mapv(|v| v as f64))
132 }
133 NcType::Short => {
134 let arr = self.read_typed_variable::<i16>(var, file_data)?;
135 Ok(arr.mapv(|v| v as f64))
136 }
137 NcType::Int => {
138 let arr = self.read_typed_variable::<i32>(var, file_data)?;
139 Ok(arr.mapv(|v| v as f64))
140 }
141 NcType::Float => {
142 let arr = self.read_typed_variable::<f32>(var, file_data)?;
143 Ok(arr.mapv(|v| v as f64))
144 }
145 NcType::Double => self.read_typed_variable::<f64>(var, file_data),
146 NcType::UByte => {
147 let arr = self.read_typed_variable::<u8>(var, file_data)?;
148 Ok(arr.mapv(|v| v as f64))
149 }
150 NcType::UShort => {
151 let arr = self.read_typed_variable::<u16>(var, file_data)?;
152 Ok(arr.mapv(|v| v as f64))
153 }
154 NcType::UInt => {
155 let arr = self.read_typed_variable::<u32>(var, file_data)?;
156 Ok(arr.mapv(|v| v as f64))
157 }
158 NcType::Int64 => {
159 let arr = self.read_typed_variable::<i64>(var, file_data)?;
160 Ok(arr.mapv(|v| v as f64))
161 }
162 NcType::UInt64 => {
163 let arr = self.read_typed_variable::<u64>(var, file_data)?;
164 Ok(arr.mapv(|v| v as f64))
165 }
166 NcType::Char => Err(Error::TypeMismatch {
167 expected: "numeric type".to_string(),
168 actual: "Char".to_string(),
169 }),
170 NcType::String => Err(Error::TypeMismatch {
171 expected: "numeric type".to_string(),
172 actual: "String".to_string(),
173 }),
174 _ => Err(Error::TypeMismatch {
175 expected: "numeric type".to_string(),
176 actual: format!("{:?}", var.dtype),
177 }),
178 }
179 }
180
181 pub fn read_variable_as_string(&self, name: &str) -> Result<String> {
183 let mut strings = self.read_variable_as_strings(name)?;
184 match strings.len() {
185 1 => Ok(strings.swap_remove(0)),
186 0 => Err(Error::InvalidData(format!(
187 "variable '{}' contains no string elements",
188 name
189 ))),
190 count => Err(Error::InvalidData(format!(
191 "variable '{}' contains {count} strings; use read_variable_as_strings()",
192 name
193 ))),
194 }
195 }
196
197 pub fn read_variable_as_strings(&self, name: &str) -> Result<Vec<String>> {
202 let var = self.find_variable(name)?;
203 if var.dtype != NcType::Char {
204 return Err(Error::TypeMismatch {
205 expected: "Char".to_string(),
206 actual: format!("{:?}", var.dtype),
207 });
208 }
209
210 let file_data = self.data.as_slice();
211 let arr = self.read_typed_variable::<u8>(var, file_data)?;
212 let bytes: Vec<u8> = arr.iter().copied().collect();
213 decode_char_variable_strings(var, &bytes)
214 }
215
216 pub fn read_variable_slice<T: NcReadType>(
221 &self,
222 name: &str,
223 selection: &crate::types::NcSliceInfo,
224 ) -> Result<ArrayD<T>> {
225 let var = self.find_variable(name)?;
226 let expected = T::nc_type();
227 if var.dtype != expected {
228 return Err(Error::TypeMismatch {
229 expected: format!("{:?}", expected),
230 actual: format!("{:?}", var.dtype),
231 });
232 }
233 let file_data = self.data.as_slice();
234 let resolved = resolve_classic_selection(
235 var,
236 selection,
237 if var.is_record_var { self.numrecs } else { 0 },
238 )?;
239
240 if !var.is_record_var {
241 return read_non_record_variable_slice_direct(file_data, var, &resolved);
242 }
243
244 let record_stride = compute_record_stride(&self.root_group.variables);
245 read_record_variable_slice_direct(file_data, var, self.numrecs, record_stride, &resolved)
246 }
247
248 pub fn read_variable_slice_as_f64(
250 &self,
251 name: &str,
252 selection: &crate::types::NcSliceInfo,
253 ) -> Result<ArrayD<f64>> {
254 let var = self.find_variable(name)?;
255
256 macro_rules! slice_promoted {
257 ($ty:ty) => {{
258 let sliced = self.read_variable_slice::<$ty>(name, selection)?;
259 Ok(sliced.mapv(|v| v as f64))
260 }};
261 }
262
263 match var.dtype {
264 NcType::Byte => slice_promoted!(i8),
265 NcType::Short => slice_promoted!(i16),
266 NcType::Int => slice_promoted!(i32),
267 NcType::Float => slice_promoted!(f32),
268 NcType::Double => slice_promoted!(f64),
269 NcType::UByte => slice_promoted!(u8),
270 NcType::UShort => slice_promoted!(u16),
271 NcType::UInt => slice_promoted!(u32),
272 NcType::Int64 => slice_promoted!(i64),
273 NcType::UInt64 => slice_promoted!(u64),
274 NcType::Char => Err(Error::TypeMismatch {
275 expected: "numeric type".to_string(),
276 actual: "Char".to_string(),
277 }),
278 _ => Err(Error::TypeMismatch {
279 expected: "numeric type".to_string(),
280 actual: format!("{:?}", var.dtype),
281 }),
282 }
283 }
284
285 fn find_variable(&self, name: &str) -> Result<&NcVariable> {
287 self.root_group
288 .variables
289 .iter()
290 .find(|v| v.name == name)
291 .ok_or_else(|| Error::VariableNotFound(name.to_string()))
292 }
293
294 fn read_typed_variable<T: NcReadType>(
296 &self,
297 var: &NcVariable,
298 file_data: &[u8],
299 ) -> Result<ArrayD<T>> {
300 if var.is_record_var {
301 let record_stride = compute_record_stride(&self.root_group.variables);
302 data::read_record_variable(file_data, var, self.numrecs, record_stride)
303 } else {
304 data::read_non_record_variable(file_data, var)
305 }
306 }
307}
308
309fn decode_char_variable_strings(var: &NcVariable, bytes: &[u8]) -> Result<Vec<String>> {
310 let shape = var.shape();
311 if shape.len() <= 1 {
312 return Ok(vec![decode_char_string(bytes)]);
313 }
314
315 let string_len = checked_usize_from_u64(
316 *shape
317 .last()
318 .ok_or_else(|| Error::InvalidData("char variable missing string axis".into()))?,
319 "char string length",
320 )?;
321 let string_count_u64 = checked_shape_elements(&shape[..shape.len() - 1], "char string count")?;
322 let string_count = checked_usize_from_u64(string_count_u64, "char string count")?;
323 let expected_bytes = string_count.checked_mul(string_len).ok_or_else(|| {
324 Error::InvalidData("char string byte count exceeds platform usize".to_string())
325 })?;
326
327 if bytes.len() < expected_bytes {
328 return Err(Error::InvalidData(format!(
329 "char variable '{}' data too short: need {} bytes, have {}",
330 var.name,
331 expected_bytes,
332 bytes.len()
333 )));
334 }
335
336 if string_len == 0 {
337 return Ok(vec![String::new(); string_count]);
338 }
339
340 Ok(bytes[..expected_bytes]
341 .chunks_exact(string_len)
342 .map(decode_char_string)
343 .collect())
344}
345
346fn decode_char_string(bytes: &[u8]) -> String {
347 String::from_utf8_lossy(bytes)
348 .trim_end_matches('\0')
349 .to_string()
350}
351
352fn variable_shape_for_selection(var: &NcVariable, numrecs: u64) -> Vec<u64> {
353 let mut shape = var.shape();
354 if var.is_record_var && !shape.is_empty() {
355 shape[0] = numrecs;
356 }
357 shape
358}
359
360fn row_major_strides(shape: &[u64], context: &str) -> Result<Vec<u64>> {
361 let ndim = shape.len();
362 if ndim == 0 {
363 return Ok(Vec::new());
364 }
365
366 let mut strides = vec![1u64; ndim];
367 for i in (0..ndim - 1).rev() {
368 strides[i] = checked_mul_u64(strides[i + 1], shape[i + 1], context)?;
369 }
370 Ok(strides)
371}
372
373fn resolve_classic_selection(
374 var: &NcVariable,
375 selection: &crate::types::NcSliceInfo,
376 numrecs: u64,
377) -> Result<ResolvedClassicSelection> {
378 use crate::types::NcSliceInfoElem;
379
380 let shape = variable_shape_for_selection(var, numrecs);
381 if selection.selections.len() != shape.len() {
382 return Err(Error::InvalidData(format!(
383 "selection has {} dimensions but variable '{}' has {}",
384 selection.selections.len(),
385 var.name,
386 shape.len()
387 )));
388 }
389
390 let mut dims = Vec::with_capacity(shape.len());
391 let mut result_shape = Vec::new();
392 let mut result_elements = 1usize;
393
394 for (dim, (sel, &dim_size)) in selection.selections.iter().zip(shape.iter()).enumerate() {
395 match sel {
396 NcSliceInfoElem::Index(idx) => {
397 if *idx >= dim_size {
398 return Err(Error::InvalidData(format!(
399 "index {} out of bounds for dimension {} (size {})",
400 idx, dim, dim_size
401 )));
402 }
403 dims.push(ResolvedClassicSelectionDim::Index(*idx));
404 }
405 NcSliceInfoElem::Slice { start, end, step } => {
406 if *step == 0 {
407 return Err(Error::InvalidData("slice step cannot be 0".to_string()));
408 }
409 if *start > dim_size {
410 return Err(Error::InvalidData(format!(
411 "slice start {} out of bounds for dimension {} (size {})",
412 start, dim, dim_size
413 )));
414 }
415
416 let actual_end = if *end == u64::MAX {
417 dim_size
418 } else {
419 (*end).min(dim_size)
420 };
421 let count_u64 = if *start >= actual_end {
422 0
423 } else {
424 (actual_end - *start).div_ceil(*step)
425 };
426 let count = checked_usize_from_u64(count_u64, "classic slice result dimension")?;
427
428 result_shape.push(count);
429 result_elements = result_elements.checked_mul(count).ok_or_else(|| {
430 Error::InvalidData(
431 "classic slice result element count exceeds platform usize".to_string(),
432 )
433 })?;
434 dims.push(ResolvedClassicSelectionDim::Slice {
435 start: *start,
436 step: *step,
437 count,
438 is_full_unit_stride: *start == 0 && actual_end == dim_size && *step == 1,
439 });
440 }
441 }
442 }
443
444 Ok(ResolvedClassicSelection {
445 dims,
446 result_shape,
447 result_elements,
448 })
449}
450
451fn read_non_record_variable_slice_direct<T: NcReadType>(
452 file_data: &[u8],
453 var: &NcVariable,
454 resolved: &ResolvedClassicSelection,
455) -> Result<ArrayD<T>> {
456 let shape = variable_shape_for_selection(var, 0);
457 let base_offset = checked_usize_from_u64(var.data_offset, "classic slice data offset")?;
458 build_array_from_contiguous_selection::<T>(file_data, &var.name, base_offset, &shape, resolved)
459}
460
461fn read_record_variable_slice_direct<T: NcReadType>(
462 file_data: &[u8],
463 var: &NcVariable,
464 numrecs: u64,
465 record_stride: u64,
466 resolved: &ResolvedClassicSelection,
467) -> Result<ArrayD<T>> {
468 use ndarray::IxDyn;
469
470 if resolved.result_elements == 0 {
471 return ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), Vec::new())
472 .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")));
473 }
474
475 let shape = variable_shape_for_selection(var, numrecs);
476 let inner_shape = &shape[1..];
477 let inner_dims = resolved.dims[1..].to_vec();
478 let inner_resolved = ResolvedClassicSelection {
479 result_shape: selection_result_shape(&inner_dims),
480 result_elements: selection_result_elements(&inner_dims)?,
481 dims: inner_dims,
482 };
483 let inner_plan = build_contiguous_selection_plan::<T>(inner_shape, &inner_resolved.dims)?;
484 let base_offset = checked_usize_from_u64(var.data_offset, "classic slice data offset")?;
485 let record_stride = checked_usize_from_u64(record_stride, "classic record stride")?;
486 let mut values = Vec::with_capacity(resolved.result_elements);
487 let context = RecordSliceContext {
488 file_data,
489 var_name: &var.name,
490 base_offset,
491 record_stride,
492 inner_resolved: &inner_resolved,
493 inner_plan: &inner_plan,
494 };
495
496 match &resolved.dims[0] {
497 ResolvedClassicSelectionDim::Index(record) => {
498 append_one_record_slice::<T>(&context, *record, &mut values)?
499 }
500 ResolvedClassicSelectionDim::Slice {
501 start, step, count, ..
502 } => {
503 for ordinal in 0..*count {
504 let record = start
505 .checked_add(checked_mul_u64(
506 ordinal as u64,
507 *step,
508 "classic record slice coordinate",
509 )?)
510 .ok_or_else(|| {
511 Error::InvalidData(
512 "classic record slice coordinate exceeds u64".to_string(),
513 )
514 })?;
515 append_one_record_slice::<T>(&context, record, &mut values)?;
516 }
517 }
518 }
519
520 debug_assert_eq!(values.len(), resolved.result_elements);
521 ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
522 .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
523}
524
525fn selection_result_shape(dims: &[ResolvedClassicSelectionDim]) -> Vec<usize> {
526 dims.iter()
527 .filter_map(|dim| match dim {
528 ResolvedClassicSelectionDim::Index(_) => None,
529 ResolvedClassicSelectionDim::Slice { count, .. } => Some(*count),
530 })
531 .collect()
532}
533
534fn selection_result_elements(dims: &[ResolvedClassicSelectionDim]) -> Result<usize> {
535 let mut elements = 1usize;
536 for dim in dims {
537 if let ResolvedClassicSelectionDim::Slice { count, .. } = dim {
538 elements = elements.checked_mul(*count).ok_or_else(|| {
539 Error::InvalidData(
540 "classic slice result element count exceeds platform usize".to_string(),
541 )
542 })?;
543 }
544 }
545 Ok(elements)
546}
547
548fn build_array_from_contiguous_selection<T: NcReadType>(
549 file_data: &[u8],
550 var_name: &str,
551 base_offset: usize,
552 shape: &[u64],
553 resolved: &ResolvedClassicSelection,
554) -> Result<ArrayD<T>> {
555 use ndarray::IxDyn;
556
557 let plan = build_contiguous_selection_plan::<T>(shape, &resolved.dims)?;
558 let values = read_contiguous_selection_values_with_plan::<T>(
559 file_data,
560 var_name,
561 base_offset,
562 &plan,
563 resolved.result_elements,
564 )?;
565 ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
566 .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
567}
568
569fn build_contiguous_selection_plan<T: NcReadType>(
570 shape: &[u64],
571 dims: &[ResolvedClassicSelectionDim],
572) -> Result<ContiguousSelectionPlan> {
573 let strides = row_major_strides(shape, "classic slice stride")?;
574 let tail_start = dims
575 .iter()
576 .rposition(|dim| !dim.is_full_unit_stride())
577 .map_or(0, |idx| idx + 1);
578 let block_elements_u64 = checked_shape_elements(
579 &shape[tail_start..],
580 "classic slice contiguous block element count",
581 )?;
582 let block_elements = checked_usize_from_u64(
583 block_elements_u64,
584 "classic slice contiguous block element count",
585 )?;
586 let block_bytes = checked_usize_from_u64(
587 checked_mul_u64(
588 block_elements_u64,
589 T::element_size() as u64,
590 "classic slice contiguous block size in bytes",
591 )?,
592 "classic slice contiguous block size in bytes",
593 )?;
594
595 Ok(ContiguousSelectionPlan {
596 dims: dims.to_vec(),
597 strides,
598 tail_start,
599 block_elements,
600 block_bytes,
601 elem_size: T::element_size() as u64,
602 })
603}
604
605fn read_contiguous_selection_values_with_plan<T: NcReadType>(
606 file_data: &[u8],
607 var_name: &str,
608 base_offset: usize,
609 plan: &ContiguousSelectionPlan,
610 result_elements: usize,
611) -> Result<Vec<T>> {
612 if result_elements == 0 {
613 return Ok(Vec::new());
614 }
615
616 let mut values = Vec::with_capacity(result_elements);
617 let context = BlockReadContext {
618 file_data,
619 var_name,
620 base_offset,
621 plan,
622 };
623 read_selected_blocks_recursive::<T>(0, 0, &context, &mut values)?;
624 Ok(values)
625}
626
627fn append_one_record_slice<T: NcReadType>(
628 context: &RecordSliceContext<'_>,
629 record: u64,
630 values: &mut Vec<T>,
631) -> Result<()> {
632 let record = checked_usize_from_u64(record, "classic record index")?;
633 let record_offset = context
634 .base_offset
635 .checked_add(record.checked_mul(context.record_stride).ok_or_else(|| {
636 Error::InvalidData("classic record byte offset exceeds platform usize".to_string())
637 })?)
638 .ok_or_else(|| {
639 Error::InvalidData("classic record byte offset exceeds platform usize".to_string())
640 })?;
641 let mut decoded = read_contiguous_selection_values_with_plan::<T>(
642 context.file_data,
643 context.var_name,
644 record_offset,
645 context.inner_plan,
646 context.inner_resolved.result_elements,
647 )?;
648 values.append(&mut decoded);
649 Ok(())
650}
651
652fn read_selected_blocks_recursive<T: NcReadType>(
653 level: usize,
654 current_offset: u64,
655 context: &BlockReadContext<'_>,
656 values: &mut Vec<T>,
657) -> Result<()> {
658 if level == context.plan.tail_start {
659 let byte_offset = checked_usize_from_u64(
660 checked_mul_u64(
661 current_offset,
662 context.plan.elem_size,
663 "classic slice element byte offset",
664 )?,
665 "classic slice element byte offset",
666 )?;
667 let start = context
668 .base_offset
669 .checked_add(byte_offset)
670 .ok_or_else(|| {
671 Error::InvalidData("classic slice byte offset exceeds platform usize".to_string())
672 })?;
673 let end = start.checked_add(context.plan.block_bytes).ok_or_else(|| {
674 Error::InvalidData("classic slice byte range exceeds platform usize".to_string())
675 })?;
676 if end > context.file_data.len() {
677 return Err(Error::InvalidData(format!(
678 "variable '{}' slice data extends beyond file",
679 context.var_name
680 )));
681 }
682
683 let mut decoded =
684 T::decode_bulk_be(&context.file_data[start..end], context.plan.block_elements)?;
685 values.append(&mut decoded);
686 return Ok(());
687 }
688
689 match &context.plan.dims[level] {
690 ResolvedClassicSelectionDim::Index(idx) => read_selected_blocks_recursive::<T>(
691 level + 1,
692 current_offset
693 .checked_add(checked_mul_u64(
694 *idx,
695 context.plan.strides[level],
696 "classic slice logical element offset",
697 )?)
698 .ok_or_else(|| {
699 Error::InvalidData(
700 "classic slice logical element offset exceeds u64".to_string(),
701 )
702 })?,
703 context,
704 values,
705 ),
706 ResolvedClassicSelectionDim::Slice {
707 start, step, count, ..
708 } => {
709 let start = *start;
710 let step = *step;
711 let count = *count;
712 for ordinal in 0..count {
713 let coord = start
714 .checked_add(checked_mul_u64(
715 ordinal as u64,
716 step,
717 "classic slice coordinate",
718 )?)
719 .ok_or_else(|| {
720 Error::InvalidData("classic slice coordinate exceeds u64".to_string())
721 })?;
722 read_selected_blocks_recursive::<T>(
723 level + 1,
724 current_offset
725 .checked_add(checked_mul_u64(
726 coord,
727 context.plan.strides[level],
728 "classic slice logical element offset",
729 )?)
730 .ok_or_else(|| {
731 Error::InvalidData(
732 "classic slice logical element offset exceeds u64".to_string(),
733 )
734 })?,
735 context,
736 values,
737 )?;
738 }
739 Ok(())
740 }
741 }
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use crate::types::NcDimension;
748
749 fn char_variable(shape: &[u64]) -> NcVariable {
750 NcVariable {
751 name: "chars".to_string(),
752 dimensions: shape
753 .iter()
754 .enumerate()
755 .map(|(i, &size)| NcDimension {
756 name: format!("d{i}"),
757 size,
758 is_unlimited: false,
759 })
760 .collect(),
761 dtype: NcType::Char,
762 attributes: vec![],
763 data_offset: 0,
764 _data_size: 0,
765 is_record_var: false,
766 record_size: 0,
767 }
768 }
769
770 #[test]
771 fn test_decode_char_variable_strings_1d() {
772 let var = char_variable(&[5]);
773 let strings = decode_char_variable_strings(&var, b"alpha").unwrap();
774 assert_eq!(strings, vec!["alpha"]);
775 }
776
777 #[test]
778 fn test_decode_char_variable_strings_2d() {
779 let var = char_variable(&[2, 5]);
780 let strings = decode_char_variable_strings(&var, b"alphabeta\0").unwrap();
781 assert_eq!(strings, vec!["alpha", "beta"]);
782 }
783}