Skip to main content

hdf5_pure_rust/hl/mutable_file/
attr_mutation.rs

1use std::io::{Seek, SeekFrom, Write};
2
3use crate::error::{Error, Result};
4use crate::format::btree_v2::BTreeV2Header;
5use crate::format::checksum::checksum_metadata;
6use crate::format::fractal_heap::FractalHeapHeader;
7use crate::format::messages::attribute::AttributeMessage;
8use crate::format::messages::attribute_info::AttributeInfoMessage;
9use crate::format::object_header::{
10    self, HDR_ATTR_STORE_PHASE_CHANGE, HDR_CHUNK0_SIZE_MASK, HDR_STORE_TIMES, HDR_V2_KNOWN_FLAGS,
11};
12
13use super::MutableFile;
14
15#[derive(Debug)]
16struct CompactAttributeMessageLocation {
17    msg_type_offset: u64,
18    msg_data_offset: u64,
19    oh_start: u64,
20    oh_check_len: usize,
21    raw_data: Vec<u8>,
22}
23
24#[derive(Debug)]
25struct DenseAttributeLocation {
26    attr_info_addr: u64,
27    heap: FractalHeapHeader,
28    btree: BTreeV2Header,
29    leaf_records: Vec<u8>,
30    record_index: usize,
31    direct_block_addr: u64,
32    direct_block_size: u64,
33    object_offset: u64,
34    raw_data: Vec<u8>,
35}
36
37impl MutableFile {
38    /// Delete a compact root attribute by name.
39    ///
40    /// This is an in-place object-header edit for compact attributes. Dense
41    /// attribute storage requires fractal-heap/B-tree mutation and is rejected
42    /// explicitly for now.
43    pub fn delete_root_attr(&mut self, name: &str) -> Result<()> {
44        self.delete_attr_at(self.superblock.root_addr, name)
45    }
46
47    /// Delete a compact group attribute by group path and attribute name.
48    pub fn delete_group_attr(&mut self, group_path: &str, name: &str) -> Result<()> {
49        let group = self.group(group_path)?;
50        self.delete_attr_at(group.addr(), name)
51    }
52
53    /// Delete a compact dataset attribute by dataset path and attribute name.
54    pub fn delete_dataset_attr(&mut self, dataset_path: &str, name: &str) -> Result<()> {
55        let dataset = self.dataset(dataset_path)?;
56        self.delete_attr_at(dataset.addr(), name)
57    }
58
59    /// Rename a compact root attribute in-place.
60    ///
61    /// The new name must fit in the existing compact attribute name field so
62    /// the compact object-header message size remains unchanged.
63    pub fn rename_root_attr(&mut self, old_name: &str, new_name: &str) -> Result<()> {
64        self.rename_attr_at(self.superblock.root_addr, old_name, new_name)
65    }
66
67    /// Rename a compact group attribute in-place.
68    pub fn rename_group_attr(
69        &mut self,
70        group_path: &str,
71        old_name: &str,
72        new_name: &str,
73    ) -> Result<()> {
74        let group = self.group(group_path)?;
75        self.rename_attr_at(group.addr(), old_name, new_name)
76    }
77
78    /// Rename a compact dataset attribute in-place.
79    pub fn rename_dataset_attr(
80        &mut self,
81        dataset_path: &str,
82        old_name: &str,
83        new_name: &str,
84    ) -> Result<()> {
85        let dataset = self.dataset(dataset_path)?;
86        self.rename_attr_at(dataset.addr(), old_name, new_name)
87    }
88
89    fn delete_attr_at(&mut self, object_addr: u64, name: &str) -> Result<()> {
90        match self.find_compact_attribute_message_in_oh(object_addr, name) {
91            Ok(location) => {
92                self.write_handle
93                    .seek(SeekFrom::Start(location.msg_type_offset))?;
94                self.write_handle
95                    .write_all(&[object_header::MSG_NIL as u8])?;
96                self.rewrite_oh_checksum(location.oh_start, location.oh_check_len)?;
97            }
98            Err(Error::Unsupported(message)) if message.contains("dense attributes") => {
99                let location = self.find_dense_attribute_location(object_addr, name, None)?;
100                self.delete_dense_attribute(location)?;
101            }
102            Err(err) => return Err(err),
103        }
104        self.write_handle.flush()?;
105        self.reopen_reader()?;
106        Ok(())
107    }
108
109    fn rename_attr_at(&mut self, object_addr: u64, old_name: &str, new_name: &str) -> Result<()> {
110        if new_name.is_empty() {
111            return Err(Error::InvalidFormat(
112                "attribute name cannot be empty".into(),
113            ));
114        }
115        if old_name == new_name {
116            return Ok(());
117        }
118        match self.find_compact_attribute_rename_location(object_addr, old_name, new_name) {
119            Ok(mut location) => {
120                let (name_offset, name_size) =
121                    Self::compact_attribute_name_field(&location.raw_data)?;
122                if new_name.len() + 1 != name_size {
123                    return Err(Error::Unsupported(
124                        "in-place compact attribute rename cannot grow or shrink the encoded name field"
125                            .into(),
126                    ));
127                }
128                let name_offset_u64 = Self::usize_to_u64(name_offset, "attribute name offset")?;
129                let file_name_offset = location
130                    .msg_data_offset
131                    .checked_add(name_offset_u64)
132                    .ok_or_else(|| Error::InvalidFormat("attribute name offset overflow".into()))?;
133
134                let encoded_name = encode_attribute_name_in_place(
135                    &mut location.raw_data,
136                    name_offset,
137                    name_size,
138                    new_name,
139                )?;
140                self.write_handle.seek(SeekFrom::Start(file_name_offset))?;
141                self.write_handle.write_all(encoded_name)?;
142                self.rewrite_oh_checksum(location.oh_start, location.oh_check_len)?;
143            }
144            Err(Error::Unsupported(message)) if message.contains("dense attributes") => {
145                let location =
146                    self.find_dense_attribute_location(object_addr, old_name, Some(new_name))?;
147                self.rename_dense_attribute(location, new_name)?;
148            }
149            Err(err) => return Err(err),
150        }
151        self.write_handle.flush()?;
152        self.reopen_reader()?;
153        Ok(())
154    }
155
156    fn compact_attribute_name_field(raw: &[u8]) -> Result<(usize, usize)> {
157        if raw.len() < 4 {
158            return Err(Error::InvalidFormat("attribute message too short".into()));
159        }
160        let version = raw
161            .first()
162            .copied()
163            .ok_or_else(|| Error::InvalidFormat("attribute message too short".into()))?;
164        let name_size = usize::from(read_u16_le_at(raw, 2, "attribute name size")?);
165        if name_size == 0 {
166            return Err(Error::InvalidFormat(
167                "attribute message name length is zero".into(),
168            ));
169        }
170        let offset: usize = match version {
171            1 | 2 => 8,
172            3 => 9,
173            other => Err(Error::InvalidFormat(format!(
174                "attribute message version {other}"
175            )))?,
176        };
177        let end = offset
178            .checked_add(name_size)
179            .ok_or_else(|| Error::InvalidFormat("attribute name field overflow".into()))?;
180        if end > raw.len() {
181            return Err(Error::InvalidFormat(
182                "attribute name field exceeds message".into(),
183            ));
184        }
185        Ok((offset, name_size))
186    }
187
188    fn find_compact_attribute_message_in_oh(
189        &self,
190        oh_addr: u64,
191        name: &str,
192    ) -> Result<CompactAttributeMessageLocation> {
193        self.find_compact_attribute_in_oh(oh_addr, name, None)
194    }
195
196    fn find_compact_attribute_rename_location(
197        &self,
198        oh_addr: u64,
199        old_name: &str,
200        new_name: &str,
201    ) -> Result<CompactAttributeMessageLocation> {
202        self.find_compact_attribute_in_oh(oh_addr, old_name, Some(new_name))
203    }
204
205    fn find_compact_attribute_in_oh(
206        &self,
207        oh_addr: u64,
208        target_name: &str,
209        reject_duplicate_name: Option<&str>,
210    ) -> Result<CompactAttributeMessageLocation> {
211        let mut guard = self.inner.lock();
212        let reader = &mut guard.reader;
213        reader.seek(oh_addr)?;
214
215        let mut first_bytes = [0u8; 4];
216        reader.read_bytes_into(&mut first_bytes)?;
217        if first_bytes != [b'O', b'H', b'D', b'R'] {
218            return Err(Error::Unsupported(
219                "attribute mutation currently supports only v2 object headers".into(),
220            ));
221        }
222
223        let version = reader.read_u8()?;
224        if version != 2 {
225            return Err(Error::Unsupported(
226                "attribute mutation currently supports only v2 object headers".into(),
227            ));
228        }
229
230        let flags = reader.read_u8()?;
231        if flags & !HDR_V2_KNOWN_FLAGS != 0 {
232            return Err(Error::InvalidFormat(format!(
233                "object header v2 flags contain reserved bits: {flags:#04x}"
234            )));
235        }
236        if flags & HDR_STORE_TIMES != 0 {
237            reader.skip(16)?;
238        }
239        if flags & HDR_ATTR_STORE_PHASE_CHANGE != 0 {
240            reader.skip(4)?;
241        }
242
243        let chunk0_size_bytes = 1u8 << (flags & HDR_CHUNK0_SIZE_MASK);
244        let chunk0_data_size = reader.read_uint(chunk0_size_bytes)?;
245        let chunk0_data_start = reader.position()?;
246        let chunk0_data_end = chunk0_data_start
247            .checked_add(chunk0_data_size)
248            .ok_or_else(|| Error::InvalidFormat("object-header chunk range overflow".into()))?;
249        let oh_check_len = usize::try_from(chunk0_data_end - oh_addr)
250            .map_err(|_| Error::InvalidFormat("object-header checksum range overflow".into()))?;
251
252        let mut has_dense_attrs = false;
253        let mut found = None;
254        while reader.position()? < chunk0_data_end {
255            let msg_header_pos = reader.position()?;
256            if msg_header_pos
257                .checked_add(4)
258                .is_none_or(|end| end > chunk0_data_end)
259            {
260                break;
261            }
262
263            let msg_type = u16::from(reader.read_u8()?);
264            let msg_size = usize::from(reader.read_u16()?);
265            let _msg_flags = reader.read_u8()?;
266            if flags & object_header::HDR_ATTR_CRT_ORDER_TRACKED != 0 {
267                reader.skip(2)?;
268            }
269
270            let msg_data_offset = reader.position()?;
271            let msg_size_u64 = Self::usize_to_u64(msg_size, "object-header message size")?;
272            if msg_data_offset
273                .checked_add(msg_size_u64)
274                .is_none_or(|end| end > chunk0_data_end)
275            {
276                return Err(Error::InvalidFormat(
277                    "object-header message payload exceeds chunk".into(),
278                ));
279            }
280
281            if msg_type == object_header::MSG_ATTRIBUTE {
282                let mut data = vec![0u8; msg_size];
283                reader.read_bytes_into(&mut data)?;
284                let attr = AttributeMessage::decode(&data)?;
285                if reject_duplicate_name.is_some_and(|name| attr.name == name) {
286                    return Err(Error::InvalidFormat(format!(
287                        "attribute '{}' already exists",
288                        attr.name
289                    )));
290                }
291                if attr.name == target_name {
292                    let location = CompactAttributeMessageLocation {
293                        msg_type_offset: msg_header_pos,
294                        msg_data_offset,
295                        oh_start: oh_addr,
296                        oh_check_len,
297                        raw_data: data,
298                    };
299                    if reject_duplicate_name.is_none() {
300                        return Ok(location);
301                    }
302                    found = Some(location);
303                }
304            } else {
305                if msg_type == object_header::MSG_ATTR_INFO {
306                    has_dense_attrs = true;
307                }
308                reader.skip(msg_size_u64)?;
309            }
310        }
311
312        if let Some(location) = found {
313            return Ok(location);
314        }
315
316        if has_dense_attrs {
317            Err(Error::Unsupported(
318                "mutating dense attributes is not implemented".into(),
319            ))
320        } else {
321            Err(Error::InvalidFormat(format!(
322                "attribute '{target_name}' not found"
323            )))
324        }
325    }
326
327    fn find_dense_attribute_location(
328        &self,
329        object_addr: u64,
330        target_name: &str,
331        reject_duplicate_name: Option<&str>,
332    ) -> Result<DenseAttributeLocation> {
333        let mut guard = self.inner.lock();
334        let attr_info =
335            Self::read_dense_attribute_info_message(&mut guard.reader, object_addr, target_name)?;
336        if !attr_info.has_dense_storage() {
337            return Err(Error::InvalidFormat(format!(
338                "attribute '{target_name}' not found"
339            )));
340        }
341        if attr_info.corder_btree_addr.is_some() {
342            return Err(Error::Unsupported(
343                "mutating creation-order indexed dense attributes is not implemented".into(),
344            ));
345        }
346
347        let heap = FractalHeapHeader::read_at(&mut guard.reader, attr_info.fractal_heap_addr)?;
348        if heap.io_filter_len != 0 {
349            return Err(Error::Unsupported(
350                "mutating filtered dense attribute heaps is not implemented".into(),
351            ));
352        }
353
354        let btree = BTreeV2Header::read_at(&mut guard.reader, attr_info.name_btree_addr)?;
355        if btree.tree_type != 8 || btree.depth != 0 {
356            return Err(Error::Unsupported(
357                "mutating non-leaf dense attribute name indexes is not implemented".into(),
358            ));
359        }
360        let mut leaf_records = Vec::new();
361        Self::read_dense_attribute_leaf_records_into(&mut guard.reader, &btree, &mut leaf_records)?;
362        let heap_id_len = usize::from(heap.heap_id_len);
363        let mut found = None;
364        let record_size = usize::from(btree.record_size);
365        for (idx, record) in leaf_records.chunks_exact(record_size).enumerate() {
366            let heap_id = checked_window(record, 0, heap_id_len, "dense attribute heap ID")?;
367            let raw_data = heap.read_managed_object(&mut guard.reader, heap_id)?;
368            let attr = AttributeMessage::decode(&raw_data)?;
369            if reject_duplicate_name.is_some_and(|name| attr.name == name) {
370                return Err(Error::InvalidFormat(format!(
371                    "attribute '{}' already exists",
372                    attr.name
373                )));
374            }
375            if attr.name == target_name {
376                let object_location = heap.managed_object_location(&mut guard.reader, heap_id)?;
377                found = Some((idx, object_location, raw_data));
378            }
379        }
380
381        let (record_index, object_location, raw_data) = found
382            .ok_or_else(|| Error::InvalidFormat(format!("attribute '{target_name}' not found")))?;
383        Ok(DenseAttributeLocation {
384            attr_info_addr: attr_info.name_btree_addr,
385            heap,
386            btree,
387            leaf_records,
388            record_index,
389            direct_block_addr: object_location.block_addr,
390            direct_block_size: object_location.block_size,
391            object_offset: object_location.object_offset,
392            raw_data,
393        })
394    }
395
396    fn delete_dense_attribute(&mut self, mut location: DenseAttributeLocation) -> Result<()> {
397        let record_size = usize::from(location.btree.record_size);
398        let start = location
399            .record_index
400            .checked_mul(record_size)
401            .ok_or_else(|| Error::InvalidFormat("dense attribute record offset overflow".into()))?;
402        let end = start
403            .checked_add(record_size)
404            .ok_or_else(|| Error::InvalidFormat("dense attribute record offset overflow".into()))?;
405        if end > location.leaf_records.len() {
406            return Err(Error::InvalidFormat(
407                "dense attribute record index is invalid".into(),
408            ));
409        }
410        location.leaf_records.copy_within(end.., start);
411        location
412            .leaf_records
413            .truncate(location.leaf_records.len() - record_size);
414        self.rewrite_dense_attribute_name_index(&location)
415    }
416
417    fn rename_dense_attribute(
418        &mut self,
419        mut location: DenseAttributeLocation,
420        new_name: &str,
421    ) -> Result<()> {
422        let (name_offset, name_size) = Self::compact_attribute_name_field(&location.raw_data)?;
423        if new_name.len() + 1 != name_size {
424            return Err(Error::Unsupported(
425                "in-place dense attribute rename cannot grow or shrink the encoded name field"
426                    .into(),
427            ));
428        }
429
430        let name_offset_u64 = Self::usize_to_u64(name_offset, "dense attribute name offset")?;
431        let file_name_offset = location
432            .direct_block_addr
433            .checked_add(location.object_offset)
434            .and_then(|offset| offset.checked_add(name_offset_u64))
435            .ok_or_else(|| Error::InvalidFormat("dense attribute name offset overflow".into()))?;
436        self.write_handle.seek(SeekFrom::Start(file_name_offset))?;
437        {
438            let encoded_name = encode_attribute_name_in_place(
439                &mut location.raw_data,
440                name_offset,
441                name_size,
442                new_name,
443            )?;
444            self.write_handle.write_all(encoded_name)?;
445        }
446
447        let record_size = usize::from(location.btree.record_size);
448        let record_start = location
449            .record_index
450            .checked_mul(record_size)
451            .ok_or_else(|| Error::InvalidFormat("dense attribute record offset overflow".into()))?;
452        let record_end = record_start
453            .checked_add(record_size)
454            .ok_or_else(|| Error::InvalidFormat("dense attribute record offset overflow".into()))?;
455        let record = location
456            .leaf_records
457            .get_mut(record_start..record_end)
458            .ok_or_else(|| {
459                Error::InvalidFormat("dense attribute record index is invalid".into())
460            })?;
461        let hash_pos = dense_attribute_record_hash_pos(&location.heap, record)?;
462        let hash_end = hash_pos
463            .checked_add(4)
464            .ok_or_else(|| Error::InvalidFormat("dense attribute hash offset overflow".into()))?;
465        record[hash_pos..hash_end].copy_from_slice(&dense_name_hash(new_name).to_le_bytes());
466        Self::reposition_dense_attribute_record_by_hash(
467            &location.heap,
468            &mut location.leaf_records,
469            location.record_index,
470            record_size,
471        )?;
472
473        let encoded_name = checked_window(
474            &location.raw_data,
475            name_offset,
476            name_size,
477            "dense attribute encoded name",
478        )?;
479        self.rewrite_dense_attribute_direct_block_checksum(
480            &location.heap,
481            location.direct_block_addr,
482            location.direct_block_size,
483            file_name_offset,
484            encoded_name,
485        )?;
486        self.rewrite_dense_attribute_name_index(&location)
487    }
488
489    fn rewrite_dense_attribute_name_index(
490        &mut self,
491        location: &DenseAttributeLocation,
492    ) -> Result<()> {
493        let record_size = usize::from(location.btree.record_size);
494        if record_size == 0 || !location.leaf_records.len().is_multiple_of(record_size) {
495            return Err(Error::InvalidFormat(
496                "dense attribute records have inconsistent sizes".into(),
497            ));
498        }
499        let record_count = location.leaf_records.len() / record_size;
500        let record_count_u16 = Self::usize_to_u16(record_count, "dense attribute record count")?;
501        let record_count_u64 = Self::usize_to_u64(record_count, "dense attribute record count")?;
502
503        let mut leaf = Vec::with_capacity(
504            6usize
505                .checked_add(location.leaf_records.len())
506                .and_then(|len| len.checked_add(4))
507                .ok_or_else(|| Error::InvalidFormat("dense attribute leaf size overflow".into()))?,
508        );
509        leaf.extend_from_slice(b"BTLF");
510        leaf.push(0);
511        leaf.push(location.btree.tree_type);
512        leaf.extend_from_slice(&location.leaf_records);
513        let checksum = checksum_metadata(&leaf);
514        leaf.extend_from_slice(&checksum.to_le_bytes());
515        self.write_handle
516            .seek(SeekFrom::Start(location.btree.root_addr))?;
517        self.write_handle.write_all(&leaf)?;
518
519        let sa = usize::from(self.superblock.sizeof_addr);
520        let ss = usize::from(self.superblock.sizeof_size);
521        let header_capacity = 22usize
522            .checked_add(sa)
523            .and_then(|len| len.checked_add(ss))
524            .ok_or_else(|| {
525                Error::InvalidFormat("dense attribute B-tree header size overflow".into())
526            })?;
527        let mut header = Vec::with_capacity(header_capacity);
528        header.extend_from_slice(b"BTHD");
529        header.push(0);
530        header.push(location.btree.tree_type);
531        header.extend_from_slice(&location.btree.node_size.to_le_bytes());
532        header.extend_from_slice(&location.btree.record_size.to_le_bytes());
533        header.extend_from_slice(&0u16.to_le_bytes());
534        header.push(location.btree.split_pct);
535        header.push(location.btree.merge_pct);
536        let mut scratch = [0u8; 8];
537        header.extend_from_slice(Self::encode_uint_le_into(
538            location.btree.root_addr,
539            &mut scratch,
540            sa,
541            "dense attribute B-tree root address",
542        )?);
543        header.extend_from_slice(&record_count_u16.to_le_bytes());
544        header.extend_from_slice(Self::encode_uint_le_into(
545            record_count_u64,
546            &mut scratch,
547            ss,
548            "dense attribute B-tree total record count",
549        )?);
550        let checksum = checksum_metadata(&header);
551        header.extend_from_slice(&checksum.to_le_bytes());
552        self.write_handle
553            .seek(SeekFrom::Start(location.attr_info_addr))?;
554        self.write_handle.write_all(&header)?;
555        Ok(())
556    }
557
558    fn read_dense_attribute_info_message<R: std::io::Read + Seek>(
559        reader: &mut crate::io::reader::HdfReader<R>,
560        oh_addr: u64,
561        target_name: &str,
562    ) -> Result<AttributeInfoMessage> {
563        reader.seek(oh_addr)?;
564
565        let mut first_bytes = [0u8; 4];
566        reader.read_bytes_into(&mut first_bytes)?;
567        if first_bytes != [b'O', b'H', b'D', b'R'] {
568            return Err(Error::Unsupported(
569                "attribute mutation currently supports only v2 object headers".into(),
570            ));
571        }
572
573        let version = reader.read_u8()?;
574        if version != 2 {
575            return Err(Error::Unsupported(
576                "attribute mutation currently supports only v2 object headers".into(),
577            ));
578        }
579
580        let flags = reader.read_u8()?;
581        if flags & !HDR_V2_KNOWN_FLAGS != 0 {
582            return Err(Error::InvalidFormat(format!(
583                "object header v2 flags contain reserved bits: {flags:#04x}"
584            )));
585        }
586        if flags & HDR_STORE_TIMES != 0 {
587            reader.skip(16)?;
588        }
589        if flags & HDR_ATTR_STORE_PHASE_CHANGE != 0 {
590            reader.skip(4)?;
591        }
592
593        let chunk0_size_bytes = 1u8 << (flags & HDR_CHUNK0_SIZE_MASK);
594        let chunk0_data_size = reader.read_uint(chunk0_size_bytes)?;
595        let chunk0_data_start = reader.position()?;
596        let chunk0_data_end = chunk0_data_start
597            .checked_add(chunk0_data_size)
598            .ok_or_else(|| Error::InvalidFormat("object-header chunk range overflow".into()))?;
599
600        while reader.position()? < chunk0_data_end {
601            let msg_header_pos = reader.position()?;
602            if msg_header_pos
603                .checked_add(4)
604                .is_none_or(|end| end > chunk0_data_end)
605            {
606                break;
607            }
608
609            let msg_type = u16::from(reader.read_u8()?);
610            let msg_size = usize::from(reader.read_u16()?);
611            let _msg_flags = reader.read_u8()?;
612            if flags & object_header::HDR_ATTR_CRT_ORDER_TRACKED != 0 {
613                reader.skip(2)?;
614            }
615
616            let msg_data_offset = reader.position()?;
617            let msg_size_u64 = Self::usize_to_u64(msg_size, "object-header message size")?;
618            if msg_data_offset
619                .checked_add(msg_size_u64)
620                .is_none_or(|end| end > chunk0_data_end)
621            {
622                return Err(Error::InvalidFormat(
623                    "object-header message payload exceeds chunk".into(),
624                ));
625            }
626
627            if msg_type == object_header::MSG_ATTR_INFO {
628                let mut data = vec![0u8; msg_size];
629                reader.read_bytes_into(&mut data)?;
630                return AttributeInfoMessage::decode(&data, reader.sizeof_addr());
631            }
632            reader.skip(msg_size_u64)?;
633        }
634
635        Err(Error::InvalidFormat(format!(
636            "attribute '{target_name}' not found"
637        )))
638    }
639
640    fn read_dense_attribute_leaf_records_into<R: std::io::Read + Seek>(
641        reader: &mut crate::io::reader::HdfReader<R>,
642        btree: &BTreeV2Header,
643        records: &mut Vec<u8>,
644    ) -> Result<()> {
645        let record_size = usize::from(btree.record_size);
646        let record_count = usize::from(btree.root_nrecords);
647        let records_len = record_size
648            .checked_mul(record_count)
649            .ok_or_else(|| Error::InvalidFormat("dense attribute leaf records overflow".into()))?;
650        let check_len = 6usize
651            .checked_add(records_len)
652            .ok_or_else(|| Error::InvalidFormat("dense attribute leaf size overflow".into()))?;
653        let total_len = check_len
654            .checked_add(4)
655            .ok_or_else(|| Error::InvalidFormat("dense attribute leaf size overflow".into()))?;
656
657        records.clear();
658        records.resize(total_len, 0);
659        reader.seek(btree.root_addr)?;
660        reader.read_bytes_into(records)?;
661        if records.get(..4) != Some(&b"BTLF"[..]) {
662            return Err(Error::InvalidFormat(
663                "invalid dense attribute B-tree leaf magic".into(),
664            ));
665        }
666        if records.get(4).copied() != Some(0) || records.get(5).copied() != Some(btree.tree_type) {
667            return Err(Error::InvalidFormat(
668                "dense attribute B-tree leaf header does not match index".into(),
669            ));
670        }
671        let stored = read_u32_le_at(records, check_len, "dense attribute leaf checksum")?;
672        let computed = checksum_metadata(&records[..check_len]);
673        if stored != computed {
674            return Err(Error::InvalidFormat(format!(
675                "dense attribute leaf checksum mismatch: stored={stored:#010x}, computed={computed:#010x}"
676            )));
677        }
678        records.drain(..6);
679        records.truncate(records_len);
680        Ok(())
681    }
682
683    fn reposition_dense_attribute_record_by_hash(
684        heap: &FractalHeapHeader,
685        records: &mut [u8],
686        record_index: usize,
687        record_size: usize,
688    ) -> Result<()> {
689        if record_size == 0 || !records.len().is_multiple_of(record_size) {
690            return Err(Error::InvalidFormat(
691                "dense attribute records have inconsistent sizes".into(),
692            ));
693        }
694        let record_count = records.len() / record_size;
695        if record_index >= record_count {
696            return Err(Error::InvalidFormat(
697                "dense attribute record index is invalid".into(),
698            ));
699        }
700        let start = record_index
701            .checked_mul(record_size)
702            .ok_or_else(|| Error::InvalidFormat("dense attribute record offset overflow".into()))?;
703        let end = start
704            .checked_add(record_size)
705            .ok_or_else(|| Error::InvalidFormat("dense attribute record offset overflow".into()))?;
706        let record = records.get(start..end).ok_or_else(|| {
707            Error::InvalidFormat("dense attribute record index is invalid".into())
708        })?;
709        let hash_pos = dense_attribute_record_hash_pos(heap, record)?;
710        let changed_hash = read_u32_le_at(record, hash_pos, "dense attribute record hash")?;
711
712        let mut insert_index = record_count - 1;
713        let mut logical_idx = 0usize;
714        for (idx, record) in records.chunks_exact(record_size).enumerate() {
715            if idx == record_index {
716                continue;
717            }
718            let hash_pos = dense_attribute_record_hash_pos(heap, record)?;
719            let hash = read_u32_le_at(record, hash_pos, "dense attribute record hash")?;
720            if changed_hash < hash {
721                insert_index = logical_idx;
722                break;
723            }
724            logical_idx += 1;
725        }
726
727        if insert_index == record_index {
728            return Ok(());
729        }
730        let insert_pos = insert_index
731            .checked_mul(record_size)
732            .ok_or_else(|| Error::InvalidFormat("dense attribute record offset overflow".into()))?;
733        if insert_index < record_index {
734            records[insert_pos..end].rotate_right(record_size);
735        } else {
736            let rotate_end = insert_pos.checked_add(record_size).ok_or_else(|| {
737                Error::InvalidFormat("dense attribute record offset overflow".into())
738            })?;
739            records[start..rotate_end].rotate_left(record_size);
740        }
741        Ok(())
742    }
743
744    fn rewrite_dense_attribute_direct_block_checksum(
745        &mut self,
746        heap: &FractalHeapHeader,
747        block_addr: u64,
748        block_size: u64,
749        patched_addr: u64,
750        patched_data: &[u8],
751    ) -> Result<()> {
752        if !heap.has_checksum {
753            return Ok(());
754        }
755        let block_size = usize::try_from(block_size)
756            .map_err(|_| Error::InvalidFormat("dense attribute direct block too large".into()))?;
757        let checksum_pos = direct_block_checksum_pos(heap, self.superblock.sizeof_addr)?;
758        let checksum_end = checksum_pos
759            .checked_add(4)
760            .ok_or_else(|| Error::InvalidFormat("direct block checksum offset overflow".into()))?;
761        let mut guard = self.inner.lock();
762        guard.reader.seek(block_addr)?;
763        let mut block = vec![0u8; block_size];
764        guard.reader.read_bytes_into(&mut block)?;
765        drop(guard);
766        let patch_start = patched_addr
767            .checked_sub(block_addr)
768            .ok_or_else(|| Error::InvalidFormat("direct block patch address underflow".into()))?;
769        let patch_start = usize::try_from(patch_start)
770            .map_err(|_| Error::InvalidFormat("direct block patch offset too large".into()))?;
771        let patch_end = patch_start
772            .checked_add(patched_data.len())
773            .ok_or_else(|| Error::InvalidFormat("direct block patch range overflow".into()))?;
774        block
775            .get_mut(patch_start..patch_end)
776            .ok_or_else(|| Error::InvalidFormat("direct block patch exceeds block".into()))?
777            .copy_from_slice(patched_data);
778        let checksum_window = block.get_mut(checksum_pos..checksum_end).ok_or_else(|| {
779            Error::InvalidFormat("direct block checksum field is truncated".into())
780        })?;
781        checksum_window.fill(0);
782        let checksum = checksum_metadata(&block);
783        let checksum_pos_u64 = Self::usize_to_u64(checksum_pos, "direct block checksum position")?;
784        self.write_handle.seek(SeekFrom::Start(
785            block_addr.checked_add(checksum_pos_u64).ok_or_else(|| {
786                Error::InvalidFormat("direct block checksum address overflow".into())
787            })?,
788        ))?;
789        self.write_handle.write_all(&checksum.to_le_bytes())?;
790        Ok(())
791    }
792}
793
794fn checked_window<'a>(raw: &'a [u8], pos: usize, len: usize, context: &str) -> Result<&'a [u8]> {
795    let end = pos
796        .checked_add(len)
797        .ok_or_else(|| Error::InvalidFormat(format!("{context} offset overflow")))?;
798    raw.get(pos..end)
799        .ok_or_else(|| Error::InvalidFormat(format!("{context} is truncated")))
800}
801
802fn read_u16_le_at(raw: &[u8], pos: usize, context: &str) -> Result<u16> {
803    let bytes = checked_window(raw, pos, 2, context)?;
804    let bytes: [u8; 2] = bytes
805        .try_into()
806        .map_err(|_| Error::InvalidFormat(format!("{context} is truncated")))?;
807    Ok(u16::from_le_bytes(bytes))
808}
809
810fn read_u32_le_at(raw: &[u8], pos: usize, context: &str) -> Result<u32> {
811    let bytes = checked_window(raw, pos, 4, context)?;
812    let bytes: [u8; 4] = bytes
813        .try_into()
814        .map_err(|_| Error::InvalidFormat(format!("{context} is truncated")))?;
815    Ok(u32::from_le_bytes(bytes))
816}
817
818fn encode_attribute_name_in_place<'a>(
819    raw: &'a mut [u8],
820    name_offset: usize,
821    name_size: usize,
822    name: &str,
823) -> Result<&'a [u8]> {
824    let name_field = raw
825        .get_mut(
826            name_offset..name_offset.checked_add(name_size).ok_or_else(|| {
827                Error::InvalidFormat("attribute name field offset overflow".into())
828            })?,
829        )
830        .ok_or_else(|| Error::InvalidFormat("attribute name field exceeds message".into()))?;
831    name_field.fill(0);
832    name_field[..name.len()].copy_from_slice(name.as_bytes());
833    Ok(name_field)
834}
835
836fn direct_block_checksum_pos(heap: &FractalHeapHeader, sizeof_addr: u8) -> Result<usize> {
837    let offset_bytes = fractal_heap_offset_width(heap)?;
838    5usize
839        .checked_add(usize::from(sizeof_addr))
840        .and_then(|pos| pos.checked_add(offset_bytes))
841        .ok_or_else(|| Error::InvalidFormat("direct block checksum position overflow".into()))
842}
843
844fn fractal_heap_offset_width(heap: &FractalHeapHeader) -> Result<usize> {
845    let max_heap_size = usize::from(heap.max_heap_size);
846    let offset_bytes = max_heap_size
847        .checked_add(7)
848        .ok_or_else(|| Error::InvalidFormat("dense attribute heap offset width overflow".into()))?
849        / 8;
850    if offset_bytes == 0 || offset_bytes > 8 {
851        return Err(Error::Unsupported(format!(
852            "dense attribute heap offset width {offset_bytes} is unsupported"
853        )));
854    }
855    Ok(offset_bytes)
856}
857
858fn dense_attribute_record_hash_pos(heap: &FractalHeapHeader, record: &[u8]) -> Result<usize> {
859    let heap_id_len = usize::from(heap.heap_id_len);
860    let hash_pos = heap_id_len
861        .checked_add(1)
862        .and_then(|pos| pos.checked_add(4))
863        .ok_or_else(|| Error::InvalidFormat("dense attribute hash position overflow".into()))?;
864    checked_window(record, hash_pos, 4, "dense attribute record hash")?;
865    Ok(hash_pos)
866}
867
868fn dense_name_hash(name: &str) -> u32 {
869    crate::format::checksum::checksum_lookup3(name.as_bytes(), 0)
870}
871
872#[cfg(test)]
873mod tests {
874    use super::*;
875
876    #[test]
877    fn checked_window_rejects_offset_overflow() {
878        let err = checked_window(&[], usize::MAX, 1, "attribute mutation test").unwrap_err();
879        assert!(
880            err.to_string()
881                .contains("attribute mutation test offset overflow"),
882            "unexpected error: {err}"
883        );
884    }
885
886    #[test]
887    fn fractal_heap_offset_width_rejects_invalid_widths() {
888        let heap = FractalHeapHeader {
889            heap_addr: 0,
890            heap_id_len: 0,
891            io_filter_len: 0,
892            flags: 0,
893            max_managed_obj_size: 0,
894            table_width: 0,
895            start_block_size: 0,
896            max_direct_block_size: 0,
897            max_heap_size: 0,
898            start_root_rows: 0,
899            root_block_addr: 0,
900            current_root_rows: 0,
901            num_managed_objects: 0,
902            has_checksum: false,
903            sizeof_addr: 8,
904            sizeof_size: 8,
905            huge_btree_addr: 0,
906            root_direct_filtered_size: None,
907            root_direct_filter_mask: 0,
908            filter_pipeline: None,
909        };
910        assert!(fractal_heap_offset_width(&heap).is_err());
911
912        let mut heap = heap;
913        heap.max_heap_size = 65;
914        assert!(fractal_heap_offset_width(&heap).is_err());
915
916        heap.max_heap_size = 64;
917        assert_eq!(fractal_heap_offset_width(&heap).unwrap(), 8);
918    }
919}