//! Extended attributes (xattrs) are typically stored in a separate data block
//! on the disk and referenced from inodes via `inode.file_acl*`.
//!
//! There are two places where extended attributes can be found. The first place
//! is between the end of each inode entry and the beginning of the next inode
//! entry. The second place where extended attributes can be found is in the block
//! pointed to by `inode.file_acl`.
//!
//! We only implement the seperate data block storage of extended attributes.

use super::{crc::crc32, AsBytes, Block, MetadataChecksumSeed};
use crate::constants::*;
use crate::prelude::*;
use core::cmp::Ordering;

/// The beginning of an extended attribute block.
#[repr(C)]
#[derive(Debug)]
pub struct XattrHeader {
    /// Magic number for identification, 0xEA020000.
    magic: u32,
    /// Reference count.
    refcount: u32,
    /// Number of disk blocks used.
    blocks: u32,
    /// Hash value of all attributes. (UNUSED by now)
    hash: u32,
    /// Checksum of the extended attribute block.
    checksum: u32,
    /// Reserved for future use.
    reserved: [u32; 3],
}

unsafe impl AsBytes for XattrHeader {}

impl XattrHeader {
    pub const XATTR_MAGIC: u32 = 0xEA020000;

    pub fn new() -> Self {
        XattrHeader {
            magic: Self::XATTR_MAGIC,
            refcount: 1,
            blocks: 1,
            hash: 0,
            checksum: 0,
            reserved: [0; 3],
        }
    }

    pub fn magic(&self) -> u32 {
        self.magic
    }

    pub fn refcount(&self) -> u32 {
        self.refcount
    }

    pub fn set_refcount(&mut self, refcount: u32) {
        self.refcount = refcount;
    }

    pub fn blocks(&self) -> u32 {
        self.blocks
    }

    pub fn checksum(&self) -> u32 {
        self.checksum
    }

    pub fn set_checksum(&mut self, checksum: u32) {
        self.checksum = checksum;
    }
}

/// Compute an ext4 external-xattr block checksum using the Linux 6.6 layout.
/// `seed` is the filesystem metadata checksum seed (normally CRC32C(uuid)).
pub fn xattr_block_checksum(
    seed: MetadataChecksumSeed,
    block_id: PBlockId,
    block: &[u8],
) -> Option<u32> {
    if block.len() != BLOCK_SIZE {
        return None;
    }
    let checksum_offset = core::mem::offset_of!(XattrHeader, checksum);
    let mut checksum = seed.crc32c(&block_id.to_le_bytes());
    checksum = crc32(checksum, &block[..checksum_offset]);
    checksum = crc32(checksum, &[0; core::mem::size_of::<u32>()]);
    Some(crc32(
        checksum,
        &block[checksum_offset + core::mem::size_of::<u32>()..],
    ))
}

#[inline]
fn xattr_value_storage_size(size: usize) -> Option<usize> {
    size.checked_add(3).map(|size| size & !3)
}

/// Validate the fixed header and entry table needed before releasing a block.
/// Returns whether any value is stored in an EA inode, which this crate cannot
/// reclaim yet.
pub fn validate_xattr_block_for_release(block: &[u8]) -> Option<(XattrHeader, bool)> {
    if block.len() != BLOCK_SIZE || block.len() < size_of::<XattrHeader>() {
        return None;
    }
    let header = XattrHeader::from_bytes(&block[..size_of::<XattrHeader>()]);
    if header.magic() != XattrHeader::XATTR_MAGIC || header.refcount() == 0 || header.blocks() != 1
    {
        return None;
    }

    let mut entry_start = size_of::<XattrHeader>();
    let mut has_ea_inode = false;
    let mut inline_values = Vec::new();
    loop {
        let terminator_end = entry_start.checked_add(size_of::<u32>())?;
        if terminator_end > BLOCK_SIZE {
            return None;
        }
        if block[entry_start..terminator_end] == [0; 4] {
            break;
        }
        let fixed_end = entry_start.checked_add(size_of::<FakeXattrEntry>())?;
        if fixed_end > BLOCK_SIZE {
            return None;
        }
        let entry = FakeXattrEntry::from_bytes(
            &block[entry_start..entry_start + size_of::<FakeXattrEntry>()],
        );
        let name_end = fixed_end.checked_add(entry.name_len as usize)?;
        if name_end > BLOCK_SIZE || block[fixed_end..name_end].contains(&0) {
            return None;
        }
        let entry_size = size_of::<FakeXattrEntry>()
            .checked_add(entry.name_len as usize)?
            .checked_add(3)?
            & !3;
        let next_entry = entry_start.checked_add(entry_size)?;
        // `next_entry == BLOCK_SIZE` cannot leave the mandatory four-byte
        // terminator. This mirrors Linux's `EXT4_XATTR_NEXT(e) >= end` check.
        if next_entry
            .checked_add(size_of::<u32>())
            .is_none_or(|end| end > BLOCK_SIZE)
        {
            return None;
        }
        has_ea_inode |= entry.value_inum != 0;
        if entry.value_inum != 0 {
            if entry.value_size == 0 {
                return None;
            }
        } else if entry.value_size != 0 {
            let value_start = entry.value_offset as usize;
            let value_size = entry.value_size as usize;
            let padded_size = value_size.checked_add(3)? & !3;
            let value_end = value_start.checked_add(padded_size)?;
            if value_start & 3 != 0 || value_end > BLOCK_SIZE {
                return None;
            }
            inline_values.push((value_start, value_end));
        }
        entry_start = next_entry;
    }

    let names_end = entry_start.checked_add(size_of::<u32>())?;
    for &(start, end) in &inline_values {
        if start < names_end || end > BLOCK_SIZE {
            return None;
        }
    }
    inline_values.sort_unstable_by_key(|range| range.0);
    if inline_values
        .windows(2)
        .any(|ranges| ranges[1].0 < ranges[0].1)
    {
        return None;
    }
    Some((header, has_ea_inode))
}

/// Following the struct `XattrHeader` is an array of `XattrEntry`.
#[repr(C)]
#[derive(Debug)]
pub struct XattrEntry {
    /// Length of name.
    name_len: u8,
    /// Attribute name index.
    ///
    /// To reduce the amount of on-disk space that the keys consume, the
    /// beginningof the key string is matched against the attribute name
    /// index. If a match is found, the attribute name index field is set,
    /// and matching string is removed from the key name.
    name_index: u8,
    /// Location of this attribute's value on the disk block where
    /// it is stored. For a block this value is relative to the start
    /// of the block (i.e. the header).
    /// value = `block[value_offset..value_offset + value_size]`
    value_offset: u16,
    /// The inode where the value is stored. Zero indicates the value
    /// is in the same block as this entry (FIXED 0 by now)
    value_inum: u32,
    /// Length of attribute value.
    value_size: u32,
    /// Hash value of attribute name and attribute value (UNUSED by now)
    hash: u32,
    /// Attribute name, max 255 bytes.
    name: [u8; 255],
}

/// Fake xattr entry. A normal entry without `name` field.
#[repr(C)]
pub struct FakeXattrEntry {
    name_len: u8,
    name_index: u8,
    value_offset: u16,
    value_inum: u32,
    value_size: u32,
    hash: u32,
}
unsafe impl AsBytes for FakeXattrEntry {}

/// The actual size of the extended attribute entry is determined by `name_len`.
/// So we need to implement `AsBytes` methods specifically for `XattrEntry`.
unsafe impl AsBytes for XattrEntry {
    fn from_bytes(bytes: &[u8]) -> Self {
        let fake_entry = FakeXattrEntry::from_bytes(bytes);
        let mut entry = XattrEntry {
            name_len: fake_entry.name_len,
            name_index: fake_entry.name_index,
            value_offset: fake_entry.value_offset,
            value_inum: fake_entry.value_inum,
            value_size: fake_entry.value_size,
            hash: fake_entry.hash,
            name: [0; 255],
        };
        let name_len = entry.name_len as usize;
        let name_offset = size_of::<FakeXattrEntry>();
        entry.name[..name_len].copy_from_slice(&bytes[name_offset..name_offset + name_len]);
        entry
    }
    fn to_bytes(&self) -> &[u8] {
        let name_len = self.name_len as usize;
        unsafe {
            core::slice::from_raw_parts(
                self as *const Self as *const u8,
                size_of::<FakeXattrEntry>() + name_len,
            )
        }
    }
}

impl XattrEntry {
    /// Create a new xattr entry.
    pub fn new(name: &str, value_size: usize, value_offset: usize) -> Self {
        let mut name_bytes = [0u8; 255];
        let (name_index, name) = Self::match_name(name);
        let name_len = name.len();
        name_bytes[..name_len].copy_from_slice(name.as_bytes());
        Self {
            name_len: name.len() as u8,
            name_index,
            value_offset: value_offset as u16,
            value_inum: 0,
            value_size: value_size as u32,
            hash: 0,
            name: name_bytes,
        }
    }

    /// Get the name of the xattr entry
    pub fn name(&self) -> String {
        let prefix = match self.name_index {
            1 => "user.",
            2 => "system.posix_acl_access.",
            3 => "system.posix_acl_default.",
            4 => "trusted.",
            6 => "security.",
            7 => "system.",
            _ => "",
        };
        let name_bytes = &self.name[..self.name_len as usize];
        let name = unsafe { String::from_utf8_unchecked(name_bytes.to_vec()) };
        prefix.to_string() + &name
    }

    /// Get the required size to save a xattr entry, 4-byte aligned
    pub fn required_size(name: &str) -> usize {
        let (_, name) = Self::match_name(name);
        let name_len = name.len();
        // FakeXattrEntry + name -> align to 4
        (core::mem::size_of::<FakeXattrEntry>() + name_len).div_ceil(4) * 4
    }

    /// Get the used size of this xattr entry, 4-bytes alighed
    pub fn used_size(&self) -> usize {
        (core::mem::size_of::<FakeXattrEntry>() + self.name_len as usize).div_ceil(4) * 4
    }

    /// Compare the name of the xattr entry with a given name
    pub fn compare_name(&self, name: &str) -> Ordering {
        let (name_index, name) = Self::match_name(name);
        match self.name_index.cmp(&name_index) {
            Ordering::Equal => {}
            ordering => return ordering,
        };
        match self.name_len.cmp(&(name.len() as u8)) {
            Ordering::Equal => {}
            ordering => return ordering,
        }
        self.name[..self.name_len as usize].cmp(name.as_bytes())
    }

    /// Match the attribute name prefix to get name index. If one is found,
    /// return the name index and the string with the prefix removed.
    fn match_name(name: &str) -> (u8, &str) {
        let prefixes = [
            ("user.", 1),
            ("system.posix_acl_access.", 2),
            ("system.posix_acl_default.", 3),
            ("trusted.", 4),
            ("security.", 6),
            ("system.", 7),
        ];
        for (prefix, index) in prefixes {
            if let Some(stripped) = name.strip_prefix(prefix) {
                return (index, stripped);
            }
        }
        (0, name)
    }
}

/// The block that stores extended attributes for an inode. The block is
/// pointed to `by inode.file_acl`.
///
/// `XattrHeader` is the beginning of an extended attribute block. Following
/// the struct `XattrHeader` is an array of `XattrEntry`. Attribute values
/// follow the end of the entry table. The values are stored starting at the
/// end of the block and grow towards the xattr_header/xattr_entry table. When
/// the two collide, the disk block fills up, and the filesystem returns `ENOSPC`.
pub struct XattrBlock(Block);

impl XattrBlock {
    /// Wrap a data block as `XattrBlock`.
    pub fn new(block: Block) -> Self {
        XattrBlock(block)
    }

    /// Get the wrapped block.
    pub fn block(self) -> Block {
        self.0
    }

    /// Initialize a xattr block, write a `XattrHeader` to the
    /// beginning of the block.
    pub fn init(&mut self) {
        self.0.data.fill(0);
        let header = XattrHeader::new();
        self.0.write_offset_as(0, &header);
    }

    /// Verify the Linux ext4 external-xattr block checksum.
    pub fn verify_checksum(&self, seed: MetadataChecksumSeed, block_id: PBlockId) -> bool {
        let header: XattrHeader = self.0.read_offset_as(0);
        xattr_block_checksum(seed, block_id, &*self.0.data)
            .is_some_and(|checksum| checksum == header.checksum())
    }

    /// Recompute the Linux ext4 external-xattr block checksum after mutation.
    pub fn update_checksum(&mut self, seed: MetadataChecksumSeed, block_id: PBlockId) -> bool {
        let mut header: XattrHeader = self.0.read_offset_as(0);
        header.set_checksum(0);
        self.0.write_offset_as(0, &header);
        let Some(checksum) = xattr_block_checksum(seed, block_id, &*self.0.data) else {
            return false;
        };
        header.set_checksum(checksum);
        self.0.write_offset_as(0, &header);
        true
    }

    /// Get a xattr by name, return the value.
    pub fn get(&self, name: &str) -> Option<&[u8]> {
        let mut entry_start = size_of::<XattrHeader>();
        // Iterate over entry table
        while entry_start < BLOCK_SIZE {
            // Check `name_len`, 0 indicates the end of the entry table.
            if self.0.data[entry_start] == 0 {
                // Target xattr not found
                break;
            }
            let entry: XattrEntry = self.0.read_offset_as(entry_start);
            if entry.compare_name(name).is_eq() {
                let offset = entry.value_offset as usize;
                let size = entry.value_size as usize;
                return Some(&self.0.data[offset..offset + size]);
            }
            entry_start += entry.used_size();
        }
        None
    }

    /// List all xattr names
    pub fn list(&self) -> Vec<String> {
        let mut entry_start = size_of::<XattrHeader>();
        let mut names = Vec::new();
        // Iterate over entry table
        while entry_start < BLOCK_SIZE {
            // Check `name_len`, 0 indicates the end of the entry table.
            if self.0.data[entry_start] == 0 {
                break;
            }
            let entry: XattrEntry = self.0.read_offset_as(entry_start);
            names.push(entry.name());
            entry_start += entry.used_size();
        }
        names
    }

    /// Insert a xattr entry into the block. Return true if success.
    pub fn insert(&mut self, name: &str, value: &[u8]) -> bool {
        let mut p_entry = size_of::<XattrHeader>();
        let mut p_value = BLOCK_SIZE;

        let mut is_ins_pos_found = false;
        let mut ins_entry_pos = p_entry;
        let mut ins_value_pos = p_value;
        let ins_entry_size = XattrEntry::required_size(name);
        let Some(ins_value_size) = xattr_value_storage_size(value.len()) else {
            return false;
        };

        // Iterate over entry table, find the position to insert entry
        // and the end of entry table
        while p_entry < BLOCK_SIZE {
            // Check `name_len`, 0 indicates the end of the entry table.
            if self.0.data[p_entry] == 0 {
                // Reach the end of table
                break;
            }
            let entry: XattrEntry = self.0.read_offset_as(p_entry);
            if !is_ins_pos_found && entry.compare_name(name).is_gt() {
                // Insert before this entry
                ins_entry_pos = p_entry;
                ins_value_pos = p_value;
                is_ins_pos_found = true;
            }
            p_value = entry.value_offset as usize;
            p_entry += entry.used_size();
        }
        if !is_ins_pos_found {
            // Insert at the end of table
            ins_entry_pos = p_entry;
            ins_value_pos = p_value;
        }

        // `ins_entry_pos` points to the position to insert entry,
        // `ins_value_pos - ins_value_size` points to the position to insert value.
        // `p_entry` points to the start of blank area,
        // `p_value` points to the last value,
        // `[p_entry, p_value)` is the blank area.

        // Check space, '+1' is reserved for blank area
        let Some(required) = ins_entry_size
            .checked_add(ins_value_size)
            .and_then(|size| size.checked_add(size_of::<u32>()))
        else {
            return false;
        };
        if p_value
            .checked_sub(p_entry)
            .is_none_or(|free| free < required)
        {
            // Not enough space
            return false;
        }

        // Move the entries from `ins_entry_pos` to `p_entry`
        // Copy `[ins_entry_pos, p_entry)` to `[ins_entry_pos+ins_entry_size, p_entry+ins_entry_size)`
        self.0
            .data
            .copy_within(ins_entry_pos..p_entry, ins_entry_pos + ins_entry_size);
        // Set `[ins_entry_pos..ins_entry_pos+entry_req_size)` to 0
        self.0.data[ins_entry_pos..ins_entry_pos + ins_entry_size].fill(0);

        // Move the corresponding values
        // Copy `[p_value, ins_value_pos)` to `[p_value-ins_value_size, ins_value_pos-ins_value_size)`
        self.0
            .data
            .copy_within(p_value..ins_value_pos, p_value - ins_value_size);
        // Set `[ins_value_pos-ins_value_size, ins_value_pos)` to 0
        self.0.data[ins_value_pos - ins_value_size..ins_value_pos].fill(0);

        // Update the value offset of the moved entries
        let mut p_entry2 = ins_entry_pos + ins_entry_size;
        while p_entry2 < p_entry + ins_entry_size {
            let mut entry: XattrEntry = self.0.read_offset_as(p_entry2);
            entry.value_offset -= ins_value_size as u16;
            self.0.write_offset_as(p_entry2, &entry);
            p_entry2 += entry.used_size();
        }

        // Insert entry to `[ins_entry_pos, ins_entry_pos+ins_entry_size)`
        let entry = XattrEntry::new(name, value.len(), ins_value_pos - ins_value_size);
        self.0.write_offset_as(ins_entry_pos, &entry);
        // Insert value to `[ins_value_pos-ins_value_size, ins_value_pos)`
        self.0.write_offset(ins_value_pos - ins_value_size, value);

        true
    }

    /// Remove a xattr entry from the block. Return true if success.
    pub fn remove(&mut self, name: &str) -> bool {
        let mut p_entry = size_of::<XattrHeader>();
        let mut p_value = BLOCK_SIZE;

        let mut is_rem_pos_found = false;
        let mut rem_entry_pos = p_entry;
        let mut rem_value_pos = p_value;
        let mut rem_entry_size = 0;
        let mut rem_value_size = 0;

        // Iterate over entry table, find the entry to remove
        while p_entry < BLOCK_SIZE {
            // Check `name_len`, 0 indicates the end of the entry table.
            if self.0.data[p_entry] == 0 {
                break;
            }
            let entry: XattrEntry = self.0.read_offset_as(p_entry);
            p_value = entry.value_offset as usize;
            // Compare name
            if !is_rem_pos_found && entry.compare_name(name).is_eq() {
                rem_entry_pos = p_entry;
                rem_value_pos = p_value;
                rem_entry_size = entry.used_size();
                let Some(storage_size) = xattr_value_storage_size(entry.value_size as usize) else {
                    return false;
                };
                rem_value_size = storage_size;
                is_rem_pos_found = true;
            }
            p_entry += entry.used_size();
        }
        if !is_rem_pos_found {
            return false;
        }

        // `rem_entry_pos` points to the entry to remove,
        // `rem_value_pos` points to the value to remove.
        // `p_entry` points to the start of blank area,
        // `p_value` points to the last value,
        // `[p_entry, p_value)` is the blank area.

        // Move the following entries
        // Copy `[rem_entry_pos + rem_entry_size, p_entry)` to `[rem_entry_pos, p_entry - rem_entry_size)`
        self.0
            .data
            .copy_within(rem_entry_pos + rem_entry_size..p_entry, rem_entry_pos);
        // Set `[p_entry - rem_entry_size, p_entry)` to 0
        self.0.data[p_entry - rem_entry_size..p_entry].fill(0);

        // Move the corresponding values
        // Copy `[p_value, rem_value_pos)` to `[p_value + rem_value_size, rem_value_pos + rem_value_size)`
        self.0
            .data
            .copy_within(p_value..rem_value_pos, p_value + rem_value_size);
        // Set `[p_value, p_value + rem_value_size)` to 0
        self.0.data[p_value..p_value + rem_value_size].fill(0);

        // Update the value offset of the moved entries
        let mut p_entry2 = rem_entry_pos;
        while p_entry2 < p_entry - rem_entry_size {
            let mut entry: XattrEntry = self.0.read_offset_as(p_entry2);
            entry.value_offset += rem_value_size as u16;
            self.0.write_offset_as(p_entry2, &entry);
            p_entry2 += entry.used_size();
        }

        true
    }
}

#[cfg(test)]
mod release_tests {
    use super::*;

    fn add_inline_entry(
        bytes: &mut [u8; BLOCK_SIZE],
        start: usize,
        name: &[u8],
        value_offset: u16,
        value_size: u32,
    ) -> usize {
        let entry = FakeXattrEntry {
            name_len: name.len() as u8,
            name_index: 1,
            value_offset,
            value_inum: 0,
            value_size,
            hash: 0,
        };
        bytes[start..start + size_of::<FakeXattrEntry>()].copy_from_slice(entry.to_bytes());
        let name_start = start + size_of::<FakeXattrEntry>();
        bytes[name_start..name_start + name.len()].copy_from_slice(name);
        start + (size_of::<FakeXattrEntry>() + name.len()).div_ceil(4) * 4
    }

    fn checksummed_block(
        block_id: PBlockId,
        refcount: u32,
    ) -> ([u8; BLOCK_SIZE], MetadataChecksumSeed) {
        let mut bytes = [0; BLOCK_SIZE];
        let mut header = XattrHeader::new();
        header.set_refcount(refcount);
        bytes[..size_of::<XattrHeader>()].copy_from_slice(header.to_bytes());
        let seed = MetadataChecksumSeed::from_raw(0x1234_5678);
        let checksum = xattr_block_checksum(seed, block_id, &bytes).unwrap();
        header.set_checksum(checksum);
        bytes[..size_of::<XattrHeader>()].copy_from_slice(header.to_bytes());
        (bytes, seed)
    }

    #[test]
    fn release_validation_checks_fixed_header_fields() {
        let (mut bytes, _) = checksummed_block(17, 2);
        let (header, has_ea_inode) = validate_xattr_block_for_release(&bytes).unwrap();
        assert_eq!(header.refcount(), 2);
        assert!(!has_ea_inode);

        bytes[..4].copy_from_slice(&0u32.to_le_bytes());
        assert!(validate_xattr_block_for_release(&bytes).is_none());
        let (mut bytes, _) = checksummed_block(17, 1);
        bytes[8..12].copy_from_slice(&2u32.to_le_bytes());
        assert!(validate_xattr_block_for_release(&bytes).is_none());
    }

    #[test]
    fn checksum_binds_block_number_and_detects_damage() {
        let (mut bytes, seed) = checksummed_block(17, 2);
        let header = XattrHeader::from_bytes(&bytes[..size_of::<XattrHeader>()]);
        assert_eq!(
            xattr_block_checksum(seed, 17, &bytes).unwrap(),
            header.checksum()
        );
        assert_ne!(
            xattr_block_checksum(seed, 18, &bytes).unwrap(),
            header.checksum()
        );
        bytes[BLOCK_SIZE - 1] ^= 1;
        assert_ne!(
            xattr_block_checksum(seed, 17, &bytes).unwrap(),
            header.checksum()
        );
    }

    #[test]
    fn xattr_block_mutation_recomputes_linux_checksum() {
        let seed = MetadataChecksumSeed::from_raw(0x1234_5678);
        let block_id = 17;
        let mut block = XattrBlock::new(Block::new(block_id, Box::new([0; BLOCK_SIZE])));
        block.init();
        assert!(block.insert("user.test", b"value"));
        assert!(validate_xattr_block_for_release(&block.0.data[..]).is_some());
        assert!(block.update_checksum(seed, block_id));
        assert!(block.verify_checksum(seed, block_id));

        assert!(block.remove("user.test"));
        assert!(validate_xattr_block_for_release(&block.0.data[..]).is_some());
        assert!(!block.verify_checksum(seed, block_id));
        assert!(block.update_checksum(seed, block_id));
        assert!(block.verify_checksum(seed, block_id));
        assert!(!block.verify_checksum(seed, block_id + 1));
    }

    #[test]
    fn unaligned_values_keep_linux_padded_layout_across_insert_and_remove() {
        let mut block = XattrBlock::new(Block::new(17, Box::new([0; BLOCK_SIZE])));
        block.init();

        assert!(block.insert("user.first", b"1234567"));
        assert!(block.insert("user.second", b"abc"));
        assert!(block.insert("user.third", b"12345"));
        assert_eq!(block.get("user.first"), Some(&b"1234567"[..]));
        assert_eq!(block.get("user.second"), Some(&b"abc"[..]));
        assert_eq!(block.get("user.third"), Some(&b"12345"[..]));
        assert!(validate_xattr_block_for_release(&block.0.data[..]).is_some());

        assert!(block.remove("user.first"));
        assert!(block.remove("user.third"));
        assert_eq!(block.get("user.first"), None);
        assert_eq!(block.get("user.second"), Some(&b"abc"[..]));
        assert_eq!(block.get("user.third"), None);
        assert!(validate_xattr_block_for_release(&block.0.data[..]).is_some());
    }

    #[test]
    fn release_validation_reports_ea_inode_entries() {
        let (mut bytes, seed) = checksummed_block(17, 1);
        bytes[size_of::<XattrHeader>()] = 1; // name_len
        bytes[size_of::<XattrHeader>() + 4..size_of::<XattrHeader>() + 8]
            .copy_from_slice(&42u32.to_le_bytes()); // value_inum
        bytes[size_of::<XattrHeader>() + 8..size_of::<XattrHeader>() + 12]
            .copy_from_slice(&1u32.to_le_bytes()); // value_size
        bytes[size_of::<XattrHeader>() + size_of::<FakeXattrEntry>()] = b'x';
        let checksum = xattr_block_checksum(seed, 17, &bytes).unwrap();
        bytes[core::mem::offset_of!(XattrHeader, checksum)
            ..core::mem::offset_of!(XattrHeader, checksum) + 4]
            .copy_from_slice(&checksum.to_le_bytes());
        assert_eq!(validate_xattr_block_for_release(&bytes).unwrap().1, true);
    }

    #[test]
    fn release_validation_rejects_malformed_layout_without_checksum_help() {
        let (template, _) = checksummed_block(17, 1);
        let first = size_of::<XattrHeader>();

        let mut bytes = template;
        let next = add_inline_entry(&mut bytes, first, b"a\0b", 4088, 4);
        bytes[next..next + 4].fill(0);
        assert!(validate_xattr_block_for_release(&bytes).is_none());

        let mut bytes = template;
        let next = add_inline_entry(&mut bytes, first, b"a", 33, 4);
        bytes[next..next + 4].fill(0);
        assert!(validate_xattr_block_for_release(&bytes).is_none());

        let mut bytes = template;
        let next = add_inline_entry(&mut bytes, first, b"a", first as u16, 4);
        bytes[next..next + 4].fill(0);
        assert!(validate_xattr_block_for_release(&bytes).is_none());

        let mut bytes = template;
        let second = add_inline_entry(&mut bytes, first, b"a", 4088, 8);
        let end = add_inline_entry(&mut bytes, second, b"b", 4092, 4);
        bytes[end..end + 4].fill(0);
        assert!(validate_xattr_block_for_release(&bytes).is_none());

        let mut bytes = template;
        bytes[first..].fill(0xff);
        assert!(validate_xattr_block_for_release(&bytes).is_none());
    }
}
