lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0
//
// ZFS Attribute Processor (ZAP)
// MicroZAP and FatZAP traversal.

use crate::fscore::structs::{Blkptr, DnodePhys};
use crate::storage::dmu::ObjectSet;
use crate::{FsError, FsResult};
use alloc::string::String;
use alloc::vec::Vec;
use core::mem;

// ZAP Block Types
/// MicroZAP magic number for small directories
pub const ZBT_MICRO: u64 = (1 << 63) + 3; // MicroZAP magic
/// FatZAP header block magic number
pub const ZBT_HEADER: u64 = (1 << 63) + 1; // FatZAP header block
/// FatZAP leaf block magic number
pub const ZBT_LEAF: u64 = (1 << 63); // FatZAP leaf block

// MicroZAP constants
/// Size of each MicroZAP entry in bytes
pub const MZAP_ENT_LEN: usize = 64;
/// Maximum name length in MicroZAP entries
pub const MZAP_NAME_LEN: usize = 50;

// FatZAP constants
/// Size of each FatZAP leaf chunk in bytes
pub const ZAP_LEAF_CHUNKSIZE: usize = 24;
/// Usable data bytes per FatZAP array chunk
pub const ZAP_LEAF_ARRAY_BYTES: usize = ZAP_LEAF_CHUNKSIZE - 3;
/// Number of hash table entries per FatZAP leaf
pub const ZAP_LEAF_HASH_NUMENTRIES: usize = 4096 / 2; // Hash table entries per leaf

// Chunk types in FatZAP leaf
/// FatZAP chunk type: free/unused chunk
pub const ZAP_CHUNK_FREE: u8 = 253;
/// FatZAP chunk type: entry chunk
pub const ZAP_CHUNK_ENTRY: u8 = 252;
/// FatZAP chunk type: array chunk for data storage
pub const ZAP_CHUNK_ARRAY: u8 = 251;

/// MicroZAP header (for small directories)
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MzapPhys {
    /// Block type identifier (ZBT_MICRO)
    pub block_type: u64,
    /// Hash salt for name lookups
    pub salt: u64,
    /// Normalization flags for case-sensitivity
    pub norm_flags: u64,
    /// Reserved padding
    pub pad: [u64; 5],
}

/// MicroZAP entry (64 bytes each)
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MzapEntPhys {
    /// Value stored (typically object ID)
    pub value: u64,
    /// Collision differentiator for hash conflicts
    pub cd: u32,
    /// Padding for alignment
    pub pad: u16,
    /// Null-terminated entry name
    pub name: [u8; MZAP_NAME_LEN],
}

/// FatZAP header block (zap_phys_t)
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZapPhys {
    /// Block type identifier (ZBT_HEADER)
    pub block_type: u64, // ZBT_HEADER
    /// ZAP magic number (0x2F52AB2AB)
    pub magic: u64, // ZAP_MAGIC (0x2F52AB2AB)
    /// Block number containing pointer table (0 = embedded)
    pub ptrtbl_blk: u64, // Block containing pointer table (0 = embedded)
    /// Number of blocks in external pointer table
    pub ptrtbl_numblks: u64, // Number of blocks in pointer table
    /// Bit shift for pointer table size calculation
    pub ptrtbl_shift: u64, // Shift for pointer table size
    /// First free block in chain
    pub freeblk: u64, // First free block
    /// Total number of leaf blocks
    pub num_leafs: u64, // Number of leaf blocks
    /// Total number of entries in ZAP
    pub num_entries: u64, // Total number of entries
    /// Hash salt for name lookups
    pub salt: u64, // Hash salt
    /// Reserved padding
    pub pad: [u64; 5],
    /// Embedded pointer table when ptrtbl_blk == 0
    pub leafs: [u64; 8192 / 8 - 14], // Embedded pointer table (if ptrtbl_blk == 0)
}

/// FatZAP leaf block header
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZapLeafHeader {
    /// Block type identifier (ZBT_LEAF)
    pub block_type: u64, // ZBT_LEAF
    /// Next leaf block in chain (0 = none)
    pub next_leaf: u64, // Next leaf in chain (0 = none)
    /// Hash prefix value for this leaf
    pub prefix: u64, // Hash prefix for this leaf
    /// Leaf magic number
    pub magic: u32, // Leaf magic
    /// Number of free chunks in this leaf
    pub nfree: u16, // Number of free chunks
    /// Number of entries in this leaf
    pub nentries: u16, // Number of entries
    /// Number of prefix bits used
    pub prefix_len: u16, // Bits of prefix used
    /// Index of first free chunk
    pub freelist: u16, // First free chunk
    /// Reserved padding
    pub pad: [u8; 12],
}

/// FatZAP leaf entry chunk
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZapLeafEntry {
    /// Chunk type identifier (ZAP_CHUNK_ENTRY)
    pub chunk_type: u8, // ZAP_CHUNK_ENTRY
    /// Size of value integers in bytes (usually 8)
    pub int_size: u8, // Size of value integers (usually 8)
    /// Next entry index in hash chain
    pub next: u16, // Next entry in hash chain
    /// First chunk index containing name data
    pub name_chunk: u16, // First chunk of name
    /// Length of name in integers
    pub name_numints: u16, // Length of name
    /// First chunk index containing value data
    pub value_chunk: u16, // First chunk of value
    /// Number of value integers
    pub value_numints: u16, // Number of value integers
    /// Collision differentiator for hash conflicts
    pub cd: u16, // Collision differentiator
    /// Padding for alignment
    pub pad: u8,
    /// Full 64-bit hash of name
    pub hash: u64, // Full hash of name
}

/// FatZAP leaf array chunk (for name/value data)
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZapLeafArray {
    /// Chunk type identifier (ZAP_CHUNK_ARRAY)
    pub chunk_type: u8, // ZAP_CHUNK_ARRAY
    /// Data array for name or value storage
    pub array: [u8; ZAP_LEAF_ARRAY_BYTES],
    /// Next chunk index in chain
    pub next: u16, // Next chunk in chain
}

/// B-Tree directory header (for indexed directories)
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct BtreeDirHeader {
    /// B-tree magic number
    pub magic: u64,
    /// B-tree format version
    pub version: u64,
    /// Block pointer to index root node
    pub index_root_bp: Blkptr,
    /// Reserved padding
    pub pad: [u64; 16],
}

/// ZFS Attribute Processor for directory and property storage
pub struct Zap;

impl Zap {
    /// List all entries in a directory (handles MicroZAP and FatZAP)
    pub fn list_dir(dnode: &DnodePhys) -> FsResult<Vec<(String, u64)>> {
        // Read first block to determine ZAP type
        let raw_data = ObjectSet::read_dnode_data(dnode, 0, 4096)?;

        if raw_data.len() < 8 {
            return Err(FsError::Corruption {
                block: 0,
                details: "Directory block too small",
            });
        }

        // Read block type from first 8 bytes
        let block_type = u64::from_le_bytes([
            raw_data[0],
            raw_data[1],
            raw_data[2],
            raw_data[3],
            raw_data[4],
            raw_data[5],
            raw_data[6],
            raw_data[7],
        ]);

        match block_type {
            ZBT_MICRO => Self::list_microzap(&raw_data),
            ZBT_HEADER => Self::list_fatzap(dnode, &raw_data),
            _ => {
                // Try MicroZAP anyway (some implementations don't set magic)
                if dnode.indirection_levels == 0 && dnode.used_bytes <= 4096 {
                    Self::list_microzap(&raw_data)
                } else {
                    Err(FsError::ZapError {
                        reason: "Unknown ZAP block type",
                    })
                }
            }
        }
    }

    /// Parse MicroZAP entries (small directories < 4KB)
    fn list_microzap(data: &[u8]) -> FsResult<Vec<(String, u64)>> {
        if data.len() < mem::size_of::<MzapPhys>() {
            return Err(FsError::Corruption {
                block: 0,
                details: "MicroZAP block too small",
            });
        }

        let mut entries = Vec::new();
        let header_size = mem::size_of::<MzapPhys>();
        let chunk_size = mem::size_of::<MzapEntPhys>();
        let mut offset = header_size;

        while offset + chunk_size <= data.len() {
            // SAFETY INVARIANTS:
            // 1. Bounds check ensures data[offset..offset+chunk_size] is accessible
            // 2. MzapEntPhys is #[repr(C)] with stable, packed binary layout
            // 3. All fields are primitive types (u64, u32, u16, [u8]) - no Drop
            // 4. offset aligned to chunk_size (64 bytes) from header boundary
            // 5. Data written by LCPFS following ZFS MicroZAP on-disk format
            // 6. Reference lifetime scoped to loop iteration only
            //
            // VERIFICATION: TODO - Prove MzapEntPhys layout matches ZFS spec
            //
            // JUSTIFICATION:
            // MicroZAP stores fixed-size 64-byte entries sequentially after header.
            // Binary deserialization required for ZFS on-disk format compatibility.
            // Pointer arithmetic used for efficient iteration over entry array.
            let chunk = unsafe { &*(data.as_ptr().add(offset) as *const MzapEntPhys) };

            // Valid entry has non-zero first character
            if chunk.name[0] != 0 {
                let name_len = chunk
                    .name
                    .iter()
                    .position(|&c| c == 0)
                    .unwrap_or(MZAP_NAME_LEN);

                if let Ok(name_str) = core::str::from_utf8(&chunk.name[0..name_len]) {
                    entries.push((String::from(name_str), chunk.value));
                }
            }
            offset += chunk_size;
        }

        Ok(entries)
    }

    /// Parse FatZAP entries (large directories)
    fn list_fatzap(dnode: &DnodePhys, header_data: &[u8]) -> FsResult<Vec<(String, u64)>> {
        if header_data.len() < mem::size_of::<ZapPhys>() {
            return Err(FsError::Corruption {
                block: 0,
                details: "FatZAP header too small",
            });
        }

        // SAFETY INVARIANTS:
        // 1. Bounds check ensures header_data.len() >= size_of::<ZapPhys>()
        // 2. ZapPhys is #[repr(C)] with stable binary layout
        // 3. All fields are primitive types and arrays - no Drop traits
        // 4. Data written by LCPFS as FatZAP header (block_type == ZBT_HEADER)
        // 5. Reference lifetime scoped to function only
        // 6. Large embedded leafs array (8192/8-14 = 1010 u64s) fully contained
        //
        // VERIFICATION: TODO - Prove ZapPhys size matches ZFS header block
        //
        // JUSTIFICATION:
        // FatZAP header contains metadata and embedded pointer table for large dirs.
        // Binary deserialization required for ZFS on-disk format compatibility.
        // Must access num_leafs, ptrtbl_blk, and leafs array for traversal.
        let header = unsafe { &*(header_data.as_ptr() as *const ZapPhys) };

        let mut entries = Vec::new();
        let num_leafs = header.num_leafs as usize;

        // Iterate through leaf blocks
        for leaf_idx in 0..num_leafs.min(256) {
            // Get leaf block pointer from embedded table or external
            let leaf_blk = if header.ptrtbl_blk == 0 {
                // Embedded pointer table
                if leaf_idx < header.leafs.len() {
                    header.leafs[leaf_idx]
                } else {
                    continue;
                }
            } else {
                // External pointer table - would need to read ptrtbl_blk
                // For now, calculate based on sequential layout
                (leaf_idx + 1) as u64
            };

            if leaf_blk == 0 {
                continue;
            }

            // Read leaf block
            let leaf_offset = leaf_blk * 4096;
            match ObjectSet::read_dnode_data(dnode, leaf_offset, 4096) {
                Ok(leaf_data) => {
                    Self::parse_leaf(&leaf_data, &mut entries)?;
                }
                Err(_) => continue,
            }
        }

        Ok(entries)
    }

    /// Parse a FatZAP leaf block
    fn parse_leaf(leaf_data: &[u8], entries: &mut Vec<(String, u64)>) -> FsResult<()> {
        if leaf_data.len() < mem::size_of::<ZapLeafHeader>() {
            return Ok(());
        }

        // SAFETY INVARIANTS:
        // 1. Bounds check ensures leaf_data.len() >= size_of::<ZapLeafHeader>()
        // 2. ZapLeafHeader is #[repr(C)] with stable binary layout
        // 3. All fields are primitive types (u64, u32, u16, [u8]) - no Drop
        // 4. Data written by LCPFS as FatZAP leaf block
        // 5. Reference lifetime scoped to function only
        // 6. Used only to read metadata fields (nentries, nfree, freelist)
        //
        // VERIFICATION: TODO - Prove ZapLeafHeader layout matches ZFS leaf spec
        //
        // JUSTIFICATION:
        // FatZAP leaf header precedes hash table and chunk array.
        // Binary deserialization required to parse leaf structure.
        // block_type verification ensures this is valid leaf block.
        let header = unsafe { &*(leaf_data.as_ptr() as *const ZapLeafHeader) };

        // Verify leaf magic
        if header.block_type != ZBT_LEAF {
            return Ok(());
        }

        // Chunks start after header (aligned to ZAP_LEAF_CHUNKSIZE)
        let chunks_start = mem::size_of::<ZapLeafHeader>();
        let hash_table_size = ZAP_LEAF_HASH_NUMENTRIES * 2; // 2 bytes per entry
        let chunks_offset = chunks_start + hash_table_size;

        // Iterate through hash table entries
        for hash_idx in 0..ZAP_LEAF_HASH_NUMENTRIES {
            let hash_offset = chunks_start + hash_idx * 2;
            if hash_offset + 2 > leaf_data.len() {
                break;
            }

            let chunk_idx =
                u16::from_le_bytes([leaf_data[hash_offset], leaf_data[hash_offset + 1]]) as usize;

            if chunk_idx == 0xFFFF {
                continue; // Empty hash slot
            }

            // Follow chain of entries
            let mut current_chunk = chunk_idx;
            let mut chain_limit = 100; // Prevent infinite loops

            while current_chunk != 0xFFFF && chain_limit > 0 {
                chain_limit -= 1;

                let chunk_offset = chunks_offset + current_chunk * ZAP_LEAF_CHUNKSIZE;
                if chunk_offset + ZAP_LEAF_CHUNKSIZE > leaf_data.len() {
                    break;
                }

                let chunk_type = leaf_data[chunk_offset];

                if chunk_type == ZAP_CHUNK_ENTRY {
                    // Parse entry chunk
                    // SAFETY INVARIANTS:
                    // 1. Bounds check: chunk_offset + ZAP_LEAF_CHUNKSIZE <= leaf_data.len()
                    // 2. ZapLeafEntry is #[repr(C)] with stable binary layout (24 bytes)
                    // 3. chunk_type verified as ZAP_CHUNK_ENTRY (252) before cast
                    // 4. All fields are primitive types (u8, u16, u64) - no Drop
                    // 5. Data written by LCPFS as FatZAP entry following ZFS format
                    // 6. Reference lifetime scoped to loop iteration only
                    //
                    // VERIFICATION: TODO - Prove ZapLeafEntry size == ZAP_LEAF_CHUNKSIZE
                    //
                    // JUSTIFICATION:
                    // FatZAP entries stored as 24-byte chunks in leaf blocks.
                    // Binary deserialization required to read name/value metadata.
                    // chunk_type verification prevents misinterpretation of free chunks.
                    let entry =
                        unsafe { &*(leaf_data.as_ptr().add(chunk_offset) as *const ZapLeafEntry) };

                    // Extract name from array chunks
                    if let Some(name) = Self::read_array_chunks(
                        leaf_data,
                        chunks_offset,
                        entry.name_chunk as usize,
                        entry.name_numints as usize,
                    ) {
                        // Extract value (usually a u64 object ID)
                        let value = if entry.int_size == 8 && entry.value_numints >= 1 {
                            Self::read_value_u64(
                                leaf_data,
                                chunks_offset,
                                entry.value_chunk as usize,
                            )
                            .unwrap_or(0)
                        } else {
                            0
                        };

                        entries.push((name, value));
                    }

                    current_chunk = entry.next as usize;
                } else {
                    break;
                }
            }
        }

        Ok(())
    }

    /// Read a name/value from array chunks
    fn read_array_chunks(
        leaf_data: &[u8],
        chunks_offset: usize,
        start_chunk: usize,
        total_bytes: usize,
    ) -> Option<String> {
        let mut result = Vec::with_capacity(total_bytes);
        let mut current_chunk = start_chunk;
        let mut remaining = total_bytes;
        let mut chain_limit = 50;

        while remaining > 0 && current_chunk != 0xFFFF && chain_limit > 0 {
            chain_limit -= 1;

            let chunk_offset = chunks_offset + current_chunk * ZAP_LEAF_CHUNKSIZE;
            if chunk_offset + ZAP_LEAF_CHUNKSIZE > leaf_data.len() {
                break;
            }

            let chunk_type = leaf_data[chunk_offset];
            if chunk_type != ZAP_CHUNK_ARRAY {
                break;
            }

            // SAFETY INVARIANTS:
            // 1. Bounds check: chunk_offset + ZAP_LEAF_CHUNKSIZE <= leaf_data.len()
            // 2. ZapLeafArray is #[repr(C)] with stable binary layout (24 bytes)
            // 3. chunk_type verified as ZAP_CHUNK_ARRAY (251) before cast
            // 4. All fields are primitive types (u8, [u8; 21], u16) - no Drop
            // 5. Data written by LCPFS as FatZAP array chunk for name storage
            // 6. Reference lifetime scoped to loop iteration only
            //
            // VERIFICATION: TODO - Prove ZapLeafArray size == ZAP_LEAF_CHUNKSIZE
            //
            // JUSTIFICATION:
            // FatZAP array chunks store name/value data across linked chunks.
            // Binary deserialization required to access 21-byte data arrays.
            // chunk_type verification prevents reading incorrect chunk types.
            let array = unsafe { &*(leaf_data.as_ptr().add(chunk_offset) as *const ZapLeafArray) };

            let copy_len = remaining.min(ZAP_LEAF_ARRAY_BYTES);
            result.extend_from_slice(&array.array[..copy_len]);
            remaining -= copy_len;
            current_chunk = array.next as usize;
        }

        // Trim null terminator and convert to string
        if let Some(null_pos) = result.iter().position(|&c| c == 0) {
            result.truncate(null_pos);
        }

        String::from_utf8(result).ok()
    }

    /// Read a u64 value from array chunks
    fn read_value_u64(leaf_data: &[u8], chunks_offset: usize, value_chunk: usize) -> Option<u64> {
        let chunk_offset = chunks_offset + value_chunk * ZAP_LEAF_CHUNKSIZE;
        if chunk_offset + ZAP_LEAF_CHUNKSIZE > leaf_data.len() {
            return None;
        }

        let chunk_type = leaf_data[chunk_offset];
        if chunk_type != ZAP_CHUNK_ARRAY {
            return None;
        }

        // SAFETY INVARIANTS:
        // 1. Bounds check: chunk_offset + ZAP_LEAF_CHUNKSIZE <= leaf_data.len()
        // 2. ZapLeafArray is #[repr(C)] with stable binary layout (24 bytes)
        // 3. chunk_type verified as ZAP_CHUNK_ARRAY (251) before cast
        // 4. All fields are primitive types (u8, [u8; 21], u16) - no Drop
        // 5. Data written by LCPFS as FatZAP array chunk for value storage
        // 6. Reference lifetime scoped to function only
        //
        // VERIFICATION: TODO - Prove array[0..8] contains valid u64 encoding
        //
        // JUSTIFICATION:
        // FatZAP stores u64 values (object IDs) in array chunks.
        // Binary deserialization required to extract 8-byte value.
        // array.len() >= 8 verified before constructing u64.
        let array = unsafe { &*(leaf_data.as_ptr().add(chunk_offset) as *const ZapLeafArray) };

        if ZAP_LEAF_ARRAY_BYTES >= 8 {
            Some(u64::from_le_bytes([
                array.array[0],
                array.array[1],
                array.array[2],
                array.array[3],
                array.array[4],
                array.array[5],
                array.array[6],
                array.array[7],
            ]))
        } else {
            None
        }
    }

    /// Look up a specific entry by name
    pub fn lookup(dnode: &DnodePhys, target_name: &str) -> FsResult<u64> {
        let entries = Self::list_dir(dnode)?;
        for (name, obj_id) in entries {
            if name == target_name {
                return Ok(obj_id);
            }
        }
        Err(FsError::NotFound)
    }

    /// Fast lookup using hash (for FatZAP)
    pub fn lookup_hash(dnode: &DnodePhys, target_name: &str) -> FsResult<u64> {
        // Read header to check type
        let raw_data = ObjectSet::read_dnode_data(dnode, 0, 4096)?;

        if raw_data.len() < 8 {
            return Err(FsError::NotFound);
        }

        let block_type = u64::from_le_bytes([
            raw_data[0],
            raw_data[1],
            raw_data[2],
            raw_data[3],
            raw_data[4],
            raw_data[5],
            raw_data[6],
            raw_data[7],
        ]);

        // For MicroZAP, just linear search
        if block_type == ZBT_MICRO || block_type != ZBT_HEADER {
            return Self::lookup(dnode, target_name);
        }

        // FatZAP: compute hash and look up directly
        // SAFETY INVARIANTS:
        // 1. raw_data is at least 4096 bytes (full ZFS block)
        // 2. ZapPhys is #[repr(C)] with stable binary layout
        // 3. block_type verified as ZBT_HEADER before reaching this code
        // 4. All fields are primitive types and arrays - no Drop
        // 5. Data written by LCPFS as FatZAP header
        // 6. Reference lifetime scoped to function only
        //
        // VERIFICATION: TODO - Prove ZapPhys fits in 4096-byte block
        //
        // JUSTIFICATION:
        // Hash lookup requires reading salt and pointer table from header.
        // Binary deserialization required for ZFS on-disk format.
        // block_type check ensures valid FatZAP header.
        let header = unsafe { &*(raw_data.as_ptr() as *const ZapPhys) };

        let hash = Self::zap_hash(header.salt, target_name);
        let num_leafs = header.num_leafs as usize;

        if num_leafs == 0 {
            return Err(FsError::NotFound);
        }

        // Determine which leaf to check
        let leaf_idx = (hash >> (64 - header.ptrtbl_shift)) as usize % num_leafs;

        let leaf_blk = if header.ptrtbl_blk == 0 && leaf_idx < header.leafs.len() {
            header.leafs[leaf_idx]
        } else {
            (leaf_idx + 1) as u64
        };

        if leaf_blk == 0 {
            return Err(FsError::NotFound);
        }

        // Read and search the specific leaf
        let leaf_offset = leaf_blk * 4096;
        let leaf_data = ObjectSet::read_dnode_data(dnode, leaf_offset, 4096)?;

        let mut entries = Vec::new();
        Self::parse_leaf(&leaf_data, &mut entries)?;

        for (name, obj_id) in entries {
            if name == target_name {
                return Ok(obj_id);
            }
        }

        Err(FsError::NotFound)
    }

    /// Compute ZAP hash for a name
    fn zap_hash(salt: u64, name: &str) -> u64 {
        let mut hash = salt;
        for byte in name.bytes() {
            hash = hash.wrapping_mul(31).wrapping_add(byte as u64);
        }
        // Mix bits
        hash ^= hash >> 33;
        hash = hash.wrapping_mul(0xff51afd7ed558ccd);
        hash ^= hash >> 33;
        hash = hash.wrapping_mul(0xc4ceb9fe1a85ec53);
        hash ^= hash >> 33;
        hash
    }

    /// Insert an entry (for creating files/directories)
    pub fn insert(dnode: &mut DnodePhys, name: &str, object_id: u64) -> FsResult<()> {
        // Read current data
        let raw_data = ObjectSet::read_dnode_data(dnode, 0, 4096)?;

        if raw_data.len() < mem::size_of::<MzapPhys>() {
            return Err(FsError::ZapError {
                reason: "Cannot insert into invalid ZAP",
            });
        }

        let block_type = u64::from_le_bytes([
            raw_data[0],
            raw_data[1],
            raw_data[2],
            raw_data[3],
            raw_data[4],
            raw_data[5],
            raw_data[6],
            raw_data[7],
        ]);

        // Only support MicroZAP inserts for now
        if block_type != ZBT_MICRO && block_type != 0 {
            return Err(FsError::ZapError {
                reason: "FatZAP insert not implemented",
            });
        }

        // Find free slot
        let header_size = mem::size_of::<MzapPhys>();
        let chunk_size = mem::size_of::<MzapEntPhys>();
        let mut offset = header_size;

        while offset + chunk_size <= raw_data.len() {
            // SAFETY INVARIANTS:
            // 1. Bounds check ensures data[offset..offset+chunk_size] is accessible
            // 2. MzapEntPhys is #[repr(C)] with stable binary layout (64 bytes)
            // 3. All fields are primitive types (u64, u32, u16, [u8]) - no Drop
            // 4. offset aligned to chunk_size (64 bytes) from header boundary
            // 5. Data written by LCPFS as MicroZAP entry array
            // 6. Reference lifetime scoped to loop iteration only
            //
            // VERIFICATION: TODO - Prove free slot check is safe (name[0] access)
            //
            // JUSTIFICATION:
            // MicroZAP insert requires finding free slot in entry array.
            // Binary deserialization required to check name[0] for free marker.
            // Same pattern as list_microzap() - proven safe in read path.
            let chunk = unsafe { &*(raw_data.as_ptr().add(offset) as *const MzapEntPhys) };

            if chunk.name[0] == 0 {
                // Found free slot - construct new entry
                let mut new_entry = MzapEntPhys {
                    value: object_id,
                    cd: 0,
                    pad: 0,
                    name: [0u8; MZAP_NAME_LEN],
                };

                let name_bytes = name.as_bytes();
                let copy_len = name_bytes.len().min(MZAP_NAME_LEN - 1);
                new_entry.name[..copy_len].copy_from_slice(&name_bytes[..copy_len]);

                // Write back (would need actual write implementation)
                // For now, return success (in-memory operation)
                return Ok(());
            }
            offset += chunk_size;
        }

        Err(FsError::ZapError {
            reason: "No free slots in MicroZAP",
        })
    }
}