1use crate::error::{Error, Result};
2use crate::filters::FilterRegistry;
3use crate::fractal_heap::FractalHeap;
4use crate::global_heap::GlobalHeapCollection;
5use crate::io::Cursor;
6use crate::messages::attribute::AttributeMessage;
7use crate::messages::attribute_info::AttributeInfoMessage;
8use crate::messages::dataspace::DataspaceType;
9use crate::messages::datatype::{Datatype, StringEncoding, StringPadding, StringSize, VarLenKind};
10use crate::messages::HdfMessage;
11use crate::object_header::ObjectHeader;
12use crate::storage::Storage;
13use crate::{btree_v2, messages};
14
15fn checked_usize(value: u64, context: &str) -> Result<usize> {
16 usize::try_from(value).map_err(|_| {
17 Error::InvalidData(format!(
18 "{context} value {value} exceeds platform usize capacity"
19 ))
20 })
21}
22
23#[derive(Debug, Clone)]
25pub struct Attribute {
26 pub name: String,
27 pub datatype: Datatype,
28 pub shape: Vec<u64>,
29 pub raw_data: Vec<u8>,
30 pub decoded_strings: Option<Vec<String>>,
31}
32
33impl Attribute {
34 pub fn from_message(msg: AttributeMessage) -> Self {
36 Self::from_message_with_context(msg, None, 0)
37 }
38
39 pub fn from_message_with_context(
42 msg: AttributeMessage,
43 file_data: Option<&[u8]>,
44 offset_size: u8,
45 ) -> Self {
46 let shape = match msg.dataspace.dataspace_type {
47 DataspaceType::Scalar => vec![],
48 DataspaceType::Null => vec![0],
49 DataspaceType::Simple => msg.dataspace.dims.clone(),
50 };
51 let decoded_strings = file_data.and_then(|file_data| {
52 decoded_vlen_strings(
53 &msg.datatype,
54 &msg.raw_data,
55 file_data,
56 offset_size,
57 msg.dataspace.num_elements().ok()?,
58 )
59 });
60 let raw_data = if let (Some(file_data), Datatype::VarLen { base, kind, .. }) =
61 (file_data, &msg.datatype)
62 {
63 if *kind == VarLenKind::String && is_byte_vlen(base) && shape.is_empty() {
64 resolve_vlen_bytes(&msg.raw_data, file_data, offset_size)
65 .unwrap_or_else(|| msg.raw_data.clone())
66 } else {
67 msg.raw_data.clone()
68 }
69 } else {
70 msg.raw_data.clone()
71 };
72 Attribute {
73 name: msg.name,
74 datatype: msg.datatype,
75 shape,
76 raw_data,
77 decoded_strings,
78 }
79 }
80
81 pub fn num_elements(&self) -> Result<u64> {
83 if self.shape.is_empty() {
84 Ok(1) } else {
86 self.shape.iter().try_fold(1u64, |acc, &dim| {
87 acc.checked_mul(dim).ok_or_else(|| {
88 Error::InvalidData("attribute element count overflows u64".to_string())
89 })
90 })
91 }
92 }
93
94 pub fn read_scalar<T: crate::datatype_api::H5Type>(&self) -> Result<T> {
96 T::from_bytes(&self.raw_data, &self.datatype)
97 }
98
99 pub fn read_1d<T: crate::datatype_api::H5Type>(&self) -> Result<Vec<T>> {
101 let elem_size = T::element_size(&self.datatype);
102 let n = checked_usize(self.num_elements()?, "attribute element count")?;
103 let mut result = Vec::with_capacity(n);
104 for i in 0..n {
105 let start = i * elem_size;
106 let end = start + elem_size;
107 if end > self.raw_data.len() {
108 return Err(Error::InvalidData(format!(
109 "attribute data too short: need {} bytes, have {}",
110 end,
111 self.raw_data.len()
112 )));
113 }
114 result.push(T::from_bytes(&self.raw_data[start..end], &self.datatype)?);
115 }
116 Ok(result)
117 }
118
119 pub fn read_string(&self) -> Result<String> {
124 if let Some(strings) = &self.decoded_strings {
125 return match strings.len() {
126 1 => Ok(strings[0].clone()),
127 0 => Err(Error::InvalidData(format!(
128 "attribute '{}' contains no string elements",
129 self.name
130 ))),
131 count => Err(Error::InvalidData(format!(
132 "attribute '{}' contains {count} string elements; use read_strings()",
133 self.name
134 ))),
135 };
136 }
137 match &self.datatype {
138 Datatype::VarLen {
139 base,
140 kind: VarLenKind::String,
141 ..
142 } if is_byte_vlen(base) => decode_varlen_byte_string(&self.raw_data),
143 Datatype::String {
144 size,
145 encoding,
146 padding,
147 } => match size {
148 StringSize::Fixed(len) => {
149 let len = *len as usize;
150 let bytes = if self.raw_data.len() < len {
151 &self.raw_data
152 } else {
153 &self.raw_data[..len]
154 };
155 decode_string(bytes, *padding, *encoding)
156 }
157 StringSize::Variable => {
158 if self.raw_data.len() >= 12 {
162 let trimmed = match padding {
164 StringPadding::NullTerminate => {
165 let end = self
166 .raw_data
167 .iter()
168 .position(|&b| b == 0)
169 .unwrap_or(self.raw_data.len());
170 &self.raw_data[..end]
171 }
172 _ => &self.raw_data,
173 };
174 if let Ok(s) = String::from_utf8(trimmed.to_vec()) {
175 if s.chars()
176 .all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t')
177 {
178 return Ok(s);
179 }
180 }
181 }
182 decode_string(&self.raw_data, *padding, *encoding)
183 }
184 },
185 _ => Err(Error::TypeMismatch {
186 expected: "String".into(),
187 actual: format!("{:?}", self.datatype),
188 }),
189 }
190 }
191
192 pub fn read_vlen_string(&self, file_data: &[u8], offset_size: u8) -> Result<String> {
197 if let Some(strings) = &self.decoded_strings {
198 return match strings.len() {
199 1 => Ok(strings[0].clone()),
200 0 => Err(Error::InvalidData(format!(
201 "attribute '{}' contains no string elements",
202 self.name
203 ))),
204 count => Err(Error::InvalidData(format!(
205 "attribute '{}' contains {count} string elements; use read_vlen_strings()",
206 self.name
207 ))),
208 };
209 }
210 match &self.datatype {
211 Datatype::String {
212 size: StringSize::Variable,
213 encoding,
214 padding,
215 } => {
216 let ref_size = 4 + offset_size as usize + 4; if self.raw_data.len() < ref_size {
218 return decode_string(&self.raw_data, *padding, *encoding);
220 }
221 let bytes = read_one_vlen_string(
222 &self.raw_data,
223 0,
224 file_data,
225 offset_size,
226 *padding,
227 *encoding,
228 )?;
229 Ok(bytes)
230 }
231 Datatype::String {
232 size: StringSize::Fixed(_),
233 ..
234 } => self.read_string(),
235 _ => Err(Error::TypeMismatch {
236 expected: "String".into(),
237 actual: format!("{:?}", self.datatype),
238 }),
239 }
240 }
241
242 pub fn read_vlen_strings(&self, file_data: &[u8], offset_size: u8) -> Result<Vec<String>> {
244 if let Some(strings) = &self.decoded_strings {
245 return Ok(strings.clone());
246 }
247 match &self.datatype {
248 Datatype::String {
249 size: StringSize::Variable,
250 encoding,
251 padding,
252 } => {
253 let ref_size = 4 + offset_size as usize + 4;
254 let n = checked_usize(self.num_elements()?, "attribute string element count")?;
255 let mut result = Vec::with_capacity(n);
256 for i in 0..n {
257 let offset = i * ref_size;
258 if offset + ref_size > self.raw_data.len() {
259 break;
260 }
261 result.push(read_one_vlen_string(
262 &self.raw_data,
263 offset,
264 file_data,
265 offset_size,
266 *padding,
267 *encoding,
268 )?);
269 }
270 Ok(result)
271 }
272 Datatype::String {
273 size: StringSize::Fixed(_),
274 ..
275 } => self.read_strings(),
276 _ => Err(Error::TypeMismatch {
277 expected: "String array".into(),
278 actual: format!("{:?}", self.datatype),
279 }),
280 }
281 }
282
283 pub fn read_strings(&self) -> Result<Vec<String>> {
285 if let Some(strings) = &self.decoded_strings {
286 return Ok(strings.clone());
287 }
288 match &self.datatype {
289 Datatype::String {
290 size: StringSize::Fixed(len),
291 encoding,
292 padding,
293 } => {
294 let len = *len as usize;
295 let n = checked_usize(self.num_elements()?, "attribute string element count")?;
296 let mut result = Vec::with_capacity(n);
297 for i in 0..n {
298 let start = i * len;
299 let end = (start + len).min(self.raw_data.len());
300 if start >= self.raw_data.len() {
301 break;
302 }
303 result.push(decode_string(
304 &self.raw_data[start..end],
305 *padding,
306 *encoding,
307 )?);
308 }
309 Ok(result)
310 }
311 _ => Err(Error::TypeMismatch {
312 expected: "String array".into(),
313 actual: format!("{:?}", self.datatype),
314 }),
315 }
316 }
317
318 pub fn read_as_f64(&self) -> Result<f64> {
320 match &self.datatype {
321 Datatype::FloatingPoint { size, .. } => {
322 let val: f64 = match size {
323 4 => {
324 let v = self.read_scalar::<f32>()?;
325 v as f64
326 }
327 8 => self.read_scalar::<f64>()?,
328 _ => {
329 return Err(Error::TypeMismatch {
330 expected: "f32 or f64".into(),
331 actual: format!("FloatingPoint(size={})", size),
332 })
333 }
334 };
335 Ok(val)
336 }
337 Datatype::FixedPoint { size, signed, .. } => {
338 let val = match (size, signed) {
339 (1, true) => self.read_scalar::<i8>()? as f64,
340 (1, false) => self.read_scalar::<u8>()? as f64,
341 (2, true) => self.read_scalar::<i16>()? as f64,
342 (2, false) => self.read_scalar::<u16>()? as f64,
343 (4, true) => self.read_scalar::<i32>()? as f64,
344 (4, false) => self.read_scalar::<u32>()? as f64,
345 (8, true) => self.read_scalar::<i64>()? as f64,
346 (8, false) => self.read_scalar::<u64>()? as f64,
347 _ => {
348 return Err(Error::TypeMismatch {
349 expected: "numeric".into(),
350 actual: format!("FixedPoint(size={})", size),
351 })
352 }
353 };
354 Ok(val)
355 }
356 _ => Err(Error::TypeMismatch {
357 expected: "numeric".into(),
358 actual: format!("{:?}", self.datatype),
359 }),
360 }
361 }
362}
363
364pub(crate) fn collect_attribute_messages_storage(
365 header: &ObjectHeader,
366 storage: &dyn Storage,
367 offset_size: u8,
368 length_size: u8,
369 filter_registry: Option<&FilterRegistry>,
370) -> Result<Vec<AttributeMessage>> {
371 let mut attributes = Vec::new();
372 let mut attribute_info = None;
373
374 for msg in &header.messages {
375 match msg {
376 HdfMessage::Attribute(attr) => attributes.push(attr.clone()),
377 HdfMessage::AttributeInfo(info) => attribute_info = Some(info.clone()),
378 _ => {}
379 }
380 }
381
382 if let Some(info) = attribute_info {
383 attributes.extend(load_dense_attribute_messages_storage(
384 &info,
385 storage,
386 offset_size,
387 length_size,
388 filter_registry,
389 )?);
390 }
391
392 Ok(attributes)
393}
394
395fn load_dense_attribute_messages_storage(
396 info: &AttributeInfoMessage,
397 storage: &dyn Storage,
398 offset_size: u8,
399 length_size: u8,
400 filter_registry: Option<&FilterRegistry>,
401) -> Result<Vec<AttributeMessage>> {
402 if Cursor::is_undefined_offset(info.fractal_heap_address, offset_size) {
403 return Ok(Vec::new());
404 }
405
406 let heap = FractalHeap::parse_at_storage(
407 storage,
408 info.fractal_heap_address,
409 offset_size,
410 length_size,
411 )?;
412
413 let records = load_dense_attribute_records_storage(info, storage, offset_size, length_size)?;
414
415 let mut attributes = Vec::new();
416 for record in records {
417 let heap_id = match record {
418 btree_v2::BTreeV2Record::AttributeNameHash { heap_id, .. }
419 | btree_v2::BTreeV2Record::AttributeCreationOrder { heap_id, .. } => heap_id,
420 _ => continue,
421 };
422
423 let managed_bytes = heap.get_object_storage_with_registry(
424 &heap_id,
425 storage,
426 offset_size,
427 length_size,
428 filter_registry,
429 )?;
430
431 let mut attr_cursor = Cursor::new(&managed_bytes);
432 let attr = messages::attribute::parse(
433 &mut attr_cursor,
434 offset_size,
435 length_size,
436 managed_bytes.len(),
437 )?;
438 attributes.push(attr);
439 }
440
441 Ok(attributes)
442}
443
444fn load_dense_attribute_records_storage(
445 info: &AttributeInfoMessage,
446 storage: &dyn Storage,
447 offset_size: u8,
448 length_size: u8,
449) -> Result<Vec<btree_v2::BTreeV2Record>> {
450 let mut addrs = Vec::new();
451 if let Some(creation_order_addr) = info.btree_creation_order_address {
454 addrs.push(("creation-order", creation_order_addr));
455 }
456 addrs.push(("name", info.btree_name_index_address));
457
458 let mut last_error = None;
459 for (index_name, addr) in addrs {
460 if Cursor::is_undefined_offset(addr, offset_size) {
461 continue;
462 }
463
464 let header = match btree_v2::BTreeV2Header::parse_at_storage(
465 storage,
466 addr,
467 offset_size,
468 length_size,
469 ) {
470 Ok(header) => header,
471 Err(err) => {
472 last_error = Some(format!(
473 "failed to parse dense attribute {index_name} B-tree at {addr:#x}: {err}"
474 ));
475 continue;
476 }
477 };
478
479 match btree_v2::collect_btree_v2_records_storage(
480 storage,
481 &header,
482 offset_size,
483 length_size,
484 None,
485 &[],
486 None,
487 ) {
488 Ok(records) => return Ok(records),
489 Err(err) => {
490 last_error = Some(format!(
491 "failed to read dense attribute {index_name} B-tree at {addr:#x}: {err}"
492 ));
493 }
494 }
495 }
496
497 if let Some(err) = last_error {
498 Err(Error::InvalidData(format!(
499 "failed to load dense attribute records: {err}"
500 )))
501 } else {
502 Ok(Vec::new())
503 }
504}
505
506pub(crate) fn read_one_vlen_string(
508 raw_data: &[u8],
509 offset: usize,
510 file_data: &[u8],
511 offset_size: u8,
512 padding: StringPadding,
513 encoding: StringEncoding,
514) -> Result<String> {
515 let mut cursor = Cursor::new(&raw_data[offset..]);
516 let _seq_len = cursor.read_u32_le()?;
517 let heap_addr = cursor.read_offset(offset_size)?;
518 let obj_index = cursor.read_u32_le()?;
519
520 if Cursor::is_undefined_offset(heap_addr, offset_size) || obj_index == 0 {
521 return Ok(String::new());
522 }
523
524 let mut heap_cursor = Cursor::new(file_data);
525 heap_cursor.set_position(heap_addr);
526 let collection = GlobalHeapCollection::parse(&mut heap_cursor, offset_size, offset_size)?;
527
528 match collection.get_object(obj_index as u16) {
529 Some(obj) => decode_string(&obj.data, padding, encoding),
530 None => Ok(String::new()),
531 }
532}
533
534pub(crate) fn read_one_vlen_string_storage(
535 raw_data: &[u8],
536 offset: usize,
537 storage: &dyn Storage,
538 offset_size: u8,
539 length_size: u8,
540 padding: StringPadding,
541 encoding: StringEncoding,
542) -> Result<String> {
543 let mut cursor = Cursor::new(&raw_data[offset..]);
544 let _seq_len = cursor.read_u32_le()?;
545 let heap_addr = cursor.read_offset(offset_size)?;
546 let obj_index = cursor.read_u32_le()?;
547
548 if Cursor::is_undefined_offset(heap_addr, offset_size) || obj_index == 0 {
549 return Ok(String::new());
550 }
551
552 let collection =
553 GlobalHeapCollection::parse_at_storage(storage, heap_addr, offset_size, length_size)?;
554 match collection.get_object(obj_index as u16) {
555 Some(obj) => decode_string(&obj.data, padding, encoding),
556 None => Ok(String::new()),
557 }
558}
559
560pub(crate) fn decoded_vlen_strings_storage(
561 datatype: &Datatype,
562 raw_data: &[u8],
563 storage: &dyn Storage,
564 offset_size: u8,
565 length_size: u8,
566 element_count: u64,
567) -> Option<Vec<String>> {
568 let (padding, encoding) = vlen_string_format(datatype)?;
569 let ref_size = 4 + offset_size as usize + 4;
570 let n = checked_usize(element_count, "attribute string element count").ok()?;
571 if raw_data.len() < n.checked_mul(ref_size)? {
572 return None;
573 }
574 let mut result = Vec::with_capacity(n);
575 for i in 0..n {
576 result.push(
577 read_one_vlen_string_storage(
578 raw_data,
579 i * ref_size,
580 storage,
581 offset_size,
582 length_size,
583 padding,
584 encoding,
585 )
586 .ok()?,
587 );
588 }
589 Some(result)
590}
591
592fn decoded_vlen_strings(
593 datatype: &Datatype,
594 raw_data: &[u8],
595 file_data: &[u8],
596 offset_size: u8,
597 element_count: u64,
598) -> Option<Vec<String>> {
599 let (padding, encoding) = vlen_string_format(datatype)?;
600 let ref_size = 4 + offset_size as usize + 4;
601 let n = checked_usize(element_count, "attribute string element count").ok()?;
602 if raw_data.len() < n.checked_mul(ref_size)? {
603 return None;
604 }
605 let mut result = Vec::with_capacity(n);
606 for i in 0..n {
607 result.push(
608 read_one_vlen_string(
609 raw_data,
610 i * ref_size,
611 file_data,
612 offset_size,
613 padding,
614 encoding,
615 )
616 .ok()?,
617 );
618 }
619 Some(result)
620}
621
622fn vlen_string_format(datatype: &Datatype) -> Option<(StringPadding, StringEncoding)> {
623 match datatype {
624 Datatype::String {
625 size: StringSize::Variable,
626 padding,
627 encoding,
628 } => Some((*padding, *encoding)),
629 Datatype::VarLen {
630 base,
631 kind: VarLenKind::String,
632 padding,
633 encoding,
634 } if is_byte_vlen(base) => Some((*padding, *encoding)),
635 _ => None,
636 }
637}
638
639pub(crate) fn decode_string(
644 bytes: &[u8],
645 padding: StringPadding,
646 _encoding: StringEncoding,
647) -> Result<String> {
648 let trimmed = match padding {
649 StringPadding::NullTerminate => {
650 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
651 &bytes[..end]
652 }
653 StringPadding::NullPad => {
654 let end = bytes.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1);
655 &bytes[..end]
656 }
657 StringPadding::SpacePad => {
658 let end = bytes.iter().rposition(|&b| b != b' ').map_or(0, |i| i + 1);
659 &bytes[..end]
660 }
661 };
662
663 String::from_utf8(trimmed.to_vec())
664 .map_err(|e| Error::InvalidData(format!("invalid string data: {e}")))
665}
666
667fn is_byte_vlen(base: &Datatype) -> bool {
668 matches!(base, Datatype::FixedPoint { size: 1, .. })
669}
670
671pub(crate) fn decode_varlen_byte_string(bytes: &[u8]) -> Result<String> {
672 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
673 String::from_utf8(bytes[..end].to_vec())
674 .map_err(|e| Error::InvalidData(format!("invalid string data: {e}")))
675}
676
677pub(crate) fn resolve_vlen_bytes(
678 raw_data: &[u8],
679 file_data: &[u8],
680 offset_size: u8,
681) -> Option<Vec<u8>> {
682 if raw_data.len() < 4 + offset_size as usize + 4 {
683 return None;
684 }
685
686 let mut cursor = Cursor::new(raw_data);
687 let seq_len = cursor.read_u32_le().ok()? as usize;
688 let heap_addr = cursor.read_offset(offset_size).ok()?;
689 let obj_index = cursor.read_u32_le().ok()? as u16;
690
691 if Cursor::is_undefined_offset(heap_addr, offset_size) || obj_index == 0 {
692 return Some(Vec::new());
693 }
694
695 let mut heap_cursor = Cursor::new(file_data);
696 heap_cursor.set_position(heap_addr);
697 let collection =
698 GlobalHeapCollection::parse(&mut heap_cursor, offset_size, offset_size).ok()?;
699 let object = collection.get_object(obj_index)?;
700 Some(object.data[..object.data.len().min(seq_len)].to_vec())
701}
702
703pub(crate) fn resolve_vlen_bytes_storage(
704 raw_data: &[u8],
705 storage: &dyn Storage,
706 offset_size: u8,
707 length_size: u8,
708) -> Option<Vec<u8>> {
709 if raw_data.len() < 4 + offset_size as usize + 4 {
710 return None;
711 }
712
713 let mut cursor = Cursor::new(raw_data);
714 let seq_len = cursor.read_u32_le().ok()? as usize;
715 let heap_addr = cursor.read_offset(offset_size).ok()?;
716 let obj_index = cursor.read_u32_le().ok()? as u16;
717
718 if Cursor::is_undefined_offset(heap_addr, offset_size) || obj_index == 0 {
719 return Some(Vec::new());
720 }
721
722 let collection =
723 GlobalHeapCollection::parse_at_storage(storage, heap_addr, offset_size, length_size)
724 .ok()?;
725 let object = collection.get_object(obj_index)?;
726 Some(object.data[..object.data.len().min(seq_len)].to_vec())
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732 use crate::error::ByteOrder;
733 use crate::storage::BytesStorage;
734 use std::f64::consts::PI;
735
736 #[test]
737 fn scalar_f64_attribute() {
738 let value: f64 = PI;
739 let raw_data = value.to_le_bytes().to_vec();
740 let attr = Attribute {
741 name: "pi".to_string(),
742 datatype: Datatype::FloatingPoint {
743 size: 8,
744 byte_order: ByteOrder::LittleEndian,
745 },
746 shape: vec![],
747 raw_data,
748 decoded_strings: None,
749 };
750 let val = attr.read_scalar::<f64>().unwrap();
751 assert!((val - PI).abs() < 1e-10);
752 }
753
754 #[test]
755 fn one_dimensional_i32_attribute() {
756 let values = [1i32, 2, 3, 4];
757 let mut raw_data = Vec::new();
758 for v in &values {
759 raw_data.extend_from_slice(&v.to_le_bytes());
760 }
761 let attr = Attribute {
762 name: "data".to_string(),
763 datatype: Datatype::FixedPoint {
764 size: 4,
765 signed: true,
766 byte_order: ByteOrder::LittleEndian,
767 },
768 shape: vec![4],
769 raw_data,
770 decoded_strings: None,
771 };
772 let result = attr.read_1d::<i32>().unwrap();
773 assert_eq!(result, vec![1, 2, 3, 4]);
774 }
775
776 #[test]
777 fn string_attribute() {
778 let attr = Attribute {
779 name: "units".to_string(),
780 datatype: Datatype::String {
781 size: StringSize::Fixed(10),
782 encoding: StringEncoding::Ascii,
783 padding: StringPadding::NullPad,
784 },
785 shape: vec![],
786 raw_data: b"meters\0\0\0\0".to_vec(),
787 decoded_strings: None,
788 };
789 assert_eq!(attr.read_string().unwrap(), "meters");
790 }
791
792 #[test]
793 fn varlen_byte_string_attribute() {
794 let attr = Attribute {
795 name: "name".to_string(),
796 datatype: Datatype::VarLen {
797 base: Box::new(Datatype::FixedPoint {
798 size: 1,
799 signed: false,
800 byte_order: ByteOrder::LittleEndian,
801 }),
802 kind: VarLenKind::String,
803 encoding: StringEncoding::Utf8,
804 padding: StringPadding::NullTerminate,
805 },
806 shape: vec![],
807 raw_data: b"test_dataset".to_vec(),
808 decoded_strings: None,
809 };
810 assert_eq!(attr.read_string().unwrap(), "test_dataset");
811 }
812
813 #[test]
814 fn read_as_f64_from_int() {
815 let raw_data = 42i32.to_le_bytes().to_vec();
816 let attr = Attribute {
817 name: "count".to_string(),
818 datatype: Datatype::FixedPoint {
819 size: 4,
820 signed: true,
821 byte_order: ByteOrder::LittleEndian,
822 },
823 shape: vec![],
824 raw_data,
825 decoded_strings: None,
826 };
827 let val = attr.read_as_f64().unwrap();
828 assert!((val - 42.0).abs() < 1e-10);
829 }
830
831 #[test]
832 fn dense_attribute_btree_errors_surface() {
833 let info = AttributeInfoMessage {
834 creation_order_tracked: false,
835 creation_order_indexed: false,
836 max_creation_index: None,
837 fractal_heap_address: 0,
838 btree_name_index_address: 0,
839 btree_creation_order_address: None,
840 };
841 let storage = BytesStorage::new(Vec::new());
842
843 let err = load_dense_attribute_records_storage(&info, &storage, 8, 8).unwrap_err();
844 assert!(matches!(err, Error::InvalidData(_)));
845 assert!(err.to_string().contains("dense attribute"));
846 }
847}