//! On-disk XFS structure encoders.

use crate::crc32c;

pub const XFSLABEL_MAX: usize = 12;
pub const XFS_DSB_SIZE: usize = 304;
pub const XFS_SB_CRC_OFF: usize = 224;
pub const XFS_AGF_SIZE: usize = 224;
pub const XFS_AGI_SIZE: usize = 344;
pub const XFS_AGFL_HEADER_SIZE: usize = 36;
pub const XFS_AGF_CRC_OFF: usize = 216;
pub const XFS_AGI_CRC_OFF: usize = 312;
pub const XFS_AGFL_CRC_OFF: usize = 32;
pub const XFS_DINODE_SIZE: usize = 176;
pub const XFS_DINODE_CRC_OFF: usize = 100;
pub const XFS_BTREE_SBLOCK_CRC_LEN: usize = 56;
pub const XFS_BTREE_SBLOCK_CRC_OFF: usize = 52;
pub const XFS_MAX_SECTORSIZE: usize = 32_768;
pub const XFS_MAX_BLOCKSIZE: usize = 65_536;
pub const XFS_DINODE_MAX_SIZE: usize = XFS_MAX_BLOCKSIZE / 2;
pub const XLOG_HEADER_SIZE: usize = 512;
pub const XLOG_INIT_RECORD_SIZE: usize = 1024;
const XFS_CRC_SEED: u32 = !0_u32;

pub const XFS_SB_MAGIC: u32 = 0x5846_5342; // "XFSB"
pub const XFS_AGF_MAGIC: u32 = 0x5841_4746; // "XAGF"
pub const XFS_AGI_MAGIC: u32 = 0x5841_4749; // "XAGI"
pub const XFS_AGFL_MAGIC: u32 = 0x5841_464c; // "XAFL"
pub const XFS_DINODE_MAGIC: u16 = 0x494e; // "IN"
pub const XFS_AGF_VERSION: u32 = 1;
pub const XFS_AGI_VERSION: u32 = 1;
pub const XFS_DINODE_VERSION_3: u8 = 3;
pub const XFS_ABTB_CRC_MAGIC: u32 = 0x4142_3342; // "AB3B"
pub const XFS_ABTC_CRC_MAGIC: u32 = 0x4142_3343; // "AB3C"
pub const XFS_IBT_CRC_MAGIC: u32 = 0x4941_4233; // "IAB3"
pub const XFS_FIBT_CRC_MAGIC: u32 = 0x4649_4233; // "FIB3"
pub const XFS_RMAP_CRC_MAGIC: u32 = 0x524d_4233; // "RMB3"
pub const XFS_REFC_CRC_MAGIC: u32 = 0x5233_4643; // "R3FC"
pub const XLOG_HEADER_MAGIC_NUM: u32 = 0xfeed_babe;
pub const XLOG_VERSION_2: u32 = 2;
pub const XLOG_BIG_RECORD_BSIZE: u32 = 32 * 1024;
pub const XLOG_FMT_LINUX_LE: u32 = 1;
pub const XLOG_UNMOUNT_TYPE: u16 = 0x556e;
pub const XFS_LOG_CLIENT_ID: u8 = 0xaa;
pub const XLOG_UNMOUNT_TRANS: u8 = 0x20;
pub const XLOG_USERSPACE_TID: u32 = 0xb0c0_d0d0;

pub const XFS_SB_VERSION_4: u16 = 4;
pub const XFS_SB_VERSION_5: u16 = 5;
pub const XFS_SB_VERSION_ATTRBIT: u16 = 0x0010;
pub const XFS_SB_VERSION_NLINKBIT: u16 = 0x0020;
pub const XFS_SB_VERSION_ALIGNBIT: u16 = 0x0080;
pub const XFS_SB_VERSION_LOGV2BIT: u16 = 0x0400;
pub const XFS_SB_VERSION_SECTORBIT: u16 = 0x0800;
pub const XFS_SB_VERSION_EXTFLGBIT: u16 = 0x1000;
pub const XFS_SB_VERSION_DIRV2BIT: u16 = 0x2000;
pub const XFS_SB_VERSION_MOREBITSBIT: u16 = 0x8000;
pub const XFS_DFL_SB_VERSION_BITS: u16 =
    XFS_SB_VERSION_NLINKBIT | XFS_SB_VERSION_EXTFLGBIT | XFS_SB_VERSION_DIRV2BIT;

pub const XFS_SB_VERSION2_LAZYSBCOUNTBIT: u32 = 0x0000_0002;
pub const XFS_SB_VERSION2_ATTR2BIT: u32 = 0x0000_0008;
pub const XFS_SB_VERSION2_PROJID32BIT: u32 = 0x0000_0080;
pub const XFS_SB_VERSION2_CRCBIT: u32 = 0x0000_0100;
pub const XFS_SB_VERSION2_FTYPE: u32 = 0x0000_0200;

pub const XFS_SB_FEAT_RO_COMPAT_FINOBT: u32 = 1 << 0;
pub const XFS_SB_FEAT_RO_COMPAT_RMAPBT: u32 = 1 << 1;
pub const XFS_SB_FEAT_RO_COMPAT_REFLINK: u32 = 1 << 2;
pub const XFS_SB_FEAT_RO_COMPAT_INOBTCNT: u32 = 1 << 3;

pub const XFS_SB_FEAT_INCOMPAT_FTYPE: u32 = 1 << 0;
pub const XFS_SB_FEAT_INCOMPAT_SPINODES: u32 = 1 << 1;
pub const XFS_SB_FEAT_INCOMPAT_BIGTIME: u32 = 1 << 3;
pub const XFS_SB_FEAT_INCOMPAT_NREXT64: u32 = 1 << 5;
pub const XFS_SB_FEAT_INCOMPAT_EXCHRANGE: u32 = 1 << 6;
pub const XFS_SB_FEAT_INCOMPAT_PARENT: u32 = 1 << 7;

pub const XFS_INODE_BIG_CLUSTER_SIZE: u32 = 8192;
pub const XFS_INODES_PER_CHUNK: u32 = 64;
pub const XFS_INOBT_ALL_FREE: u64 = u64::MAX;
pub const NULLFSINO: u64 = u64::MAX;
pub const NULLAGBLOCK: u32 = u32::MAX;
pub const NULLAGINO: u32 = u32::MAX;
pub const NULLFSBLOCK: u64 = u64::MAX;

pub const XFS_DINODE_FMT_DEV: u8 = 0;
pub const XFS_DINODE_FMT_LOCAL: u8 = 1;
pub const XFS_DINODE_FMT_EXTENTS: u8 = 2;
pub const XFS_DINODE_FMT_BTREE: u8 = 3;
pub const XFS_DINODE_FMT_UUID: u8 = 4;
pub const XFS_DINODE_FMT_META_BTREE: u8 = 5;
pub const XFS_MODE_IFDIR: u16 = 0o040000;
pub const XFS_MODE_IFREG: u16 = 0o100000;
pub const XFS_MODE_755: u16 = 0o755;
pub const XFS_DIR2_SF_HDR_SIZE_SHORT: usize = 6;
pub const XFS_DIR2_SF_HDR_SIZE_LONG: usize = 10;
pub const XFS_DIR2_MAX_SHORT_INUM: u64 = u32::MAX as u64;
pub const XFS_DIFLAG_NEWRTBM: u16 = 1 << 2;
pub const XFS_DIFLAG2_BIGTIME: u64 = 1 << 3;
pub const XFS_DIFLAG2_NREXT64: u64 = 1 << 4;

pub const XFS_RMAP_OWN_FS: u64 = (-3_i64) as u64;
pub const XFS_RMAP_OWN_LOG: u64 = (-4_i64) as u64;
pub const XFS_RMAP_OWN_AG: u64 = (-5_i64) as u64;
pub const XFS_RMAP_OWN_INOBT: u64 = (-6_i64) as u64;
pub const XFS_RMAP_OWN_INODES: u64 = (-7_i64) as u64;
pub const XFS_RMAP_OWN_REFC: u64 = (-8_i64) as u64;

/// Host-order representation of `struct xfs_dsb`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Superblock {
    pub magic: u32,
    pub block_size: u32,
    pub data_blocks: u64,
    pub realtime_blocks: u64,
    pub realtime_extents: u64,
    pub uuid: [u8; 16],
    pub log_start: u64,
    pub root_inode: u64,
    pub realtime_bitmap_inode: u64,
    pub realtime_summary_inode: u64,
    pub realtime_extent_size: u32,
    pub ag_blocks: u32,
    pub ag_count: u32,
    pub realtime_bitmap_blocks: u32,
    pub log_blocks: u32,
    pub version: u16,
    pub sector_size: u16,
    pub inode_size: u16,
    pub inodes_per_block: u16,
    pub label: [u8; XFSLABEL_MAX],
    pub block_log: u8,
    pub sector_log: u8,
    pub inode_log: u8,
    pub inodes_per_block_log: u8,
    pub ag_blocks_log: u8,
    pub realtime_extents_log: u8,
    pub inprogress: u8,
    pub imax_pct: u8,
    pub inode_count: u64,
    pub free_inode_count: u64,
    pub free_data_blocks: u64,
    pub free_realtime_extents: u64,
    pub user_quota_inode: u64,
    pub group_quota_inode: u64,
    pub quota_flags: u16,
    pub flags: u8,
    pub shared_version: u8,
    pub inode_align: u32,
    pub stripe_unit: u32,
    pub stripe_width: u32,
    pub dir_block_log: u8,
    pub log_sector_log: u8,
    pub log_sector_size: u16,
    pub log_sunit: u32,
    pub features2: u32,
    pub bad_features2: u32,
    pub features_compat: u32,
    pub features_ro_compat: u32,
    pub features_incompat: u32,
    pub features_log_incompat: u32,
    pub crc: u32,
    pub spino_align: u32,
    pub project_quota_inode: u64,
    pub lsn: u64,
    pub meta_uuid: [u8; 16],
    pub metadir_inode: u64,
    pub rg_count: u32,
    pub rg_extents: u32,
    pub rg_block_log: u8,
    pub rt_start: u64,
    pub rt_reserved: u64,
}

impl Default for Superblock {
    fn default() -> Self {
        Self {
            magic: XFS_SB_MAGIC,
            block_size: 0,
            data_blocks: 0,
            realtime_blocks: 0,
            realtime_extents: 0,
            uuid: [0; 16],
            log_start: 0,
            root_inode: NULLFSINO,
            realtime_bitmap_inode: NULLFSINO,
            realtime_summary_inode: NULLFSINO,
            realtime_extent_size: 0,
            ag_blocks: 0,
            ag_count: 0,
            realtime_bitmap_blocks: 0,
            log_blocks: 0,
            version: 0,
            sector_size: 0,
            inode_size: 0,
            inodes_per_block: 0,
            label: [0; XFSLABEL_MAX],
            block_log: 0,
            sector_log: 0,
            inode_log: 0,
            inodes_per_block_log: 0,
            ag_blocks_log: 0,
            realtime_extents_log: 0,
            inprogress: 1,
            imax_pct: 0,
            inode_count: 0,
            free_inode_count: 0,
            free_data_blocks: 0,
            free_realtime_extents: 0,
            user_quota_inode: 0,
            group_quota_inode: 0,
            quota_flags: 0,
            flags: 0,
            shared_version: 0,
            inode_align: 0,
            stripe_unit: 0,
            stripe_width: 0,
            dir_block_log: 0,
            log_sector_log: 0,
            log_sector_size: 0,
            log_sunit: 0,
            features2: 0,
            bad_features2: 0,
            features_compat: 0,
            features_ro_compat: 0,
            features_incompat: 0,
            features_log_incompat: 0,
            crc: 0,
            spino_align: 0,
            project_quota_inode: 0,
            lsn: 0,
            meta_uuid: [0; 16],
            metadir_inode: NULLFSINO,
            rg_count: 1,
            rg_extents: 0,
            rg_block_log: 0,
            rt_start: 0,
            rt_reserved: 0,
        }
    }
}

impl Superblock {
    pub fn set_label(&mut self, label: &[u8]) {
        self.label = [0; XFSLABEL_MAX];
        let len = label.len().min(XFSLABEL_MAX);
        self.label[..len].copy_from_slice(&label[..len]);
    }

    /// Encode `struct xfs_dsb` without calculating the v5 metadata CRC.
    pub fn encode_without_crc(&self) -> [u8; XFS_DSB_SIZE] {
        let mut out = [0_u8; XFS_DSB_SIZE];
        let mut w = Writer::new(&mut out);

        w.be32(self.magic);
        w.be32(self.block_size);
        w.be64(self.data_blocks);
        w.be64(self.realtime_blocks);
        w.be64(self.realtime_extents);
        w.bytes(&self.uuid);
        w.be64(self.log_start);
        w.be64(self.root_inode);
        w.be64(self.realtime_bitmap_inode);
        w.be64(self.realtime_summary_inode);
        w.be32(self.realtime_extent_size);
        w.be32(self.ag_blocks);
        w.be32(self.ag_count);
        w.be32(self.realtime_bitmap_blocks);
        w.be32(self.log_blocks);
        w.be16(self.version);
        w.be16(self.sector_size);
        w.be16(self.inode_size);
        w.be16(self.inodes_per_block);
        w.bytes(&self.label);
        w.u8(self.block_log);
        w.u8(self.sector_log);
        w.u8(self.inode_log);
        w.u8(self.inodes_per_block_log);
        w.u8(self.ag_blocks_log);
        w.u8(self.realtime_extents_log);
        w.u8(self.inprogress);
        w.u8(self.imax_pct);
        w.be64(self.inode_count);
        w.be64(self.free_inode_count);
        w.be64(self.free_data_blocks);
        w.be64(self.free_realtime_extents);
        w.be64(self.user_quota_inode);
        w.be64(self.group_quota_inode);
        w.be16(self.quota_flags);
        w.u8(self.flags);
        w.u8(self.shared_version);
        w.be32(self.inode_align);
        w.be32(self.stripe_unit);
        w.be32(self.stripe_width);
        w.u8(self.dir_block_log);
        w.u8(self.log_sector_log);
        w.be16(self.log_sector_size);
        w.be32(self.log_sunit);
        w.be32(self.features2);
        w.be32(self.bad_features2);
        w.be32(self.features_compat);
        w.be32(self.features_ro_compat);
        w.be32(self.features_incompat);
        w.be32(self.features_log_incompat);
        w.le32(self.crc);
        w.be32(self.spino_align);
        w.be64(self.project_quota_inode);
        w.be64(self.lsn);
        w.bytes(&self.meta_uuid);
        w.be64(self.metadir_inode);
        w.be32(self.rg_count);
        w.be32(self.rg_extents);
        w.u8(self.rg_block_log);
        w.zeros(7);
        w.be64(self.rt_start);
        w.be64(self.rt_reserved);

        debug_assert!(w.finished(XFS_DSB_SIZE));
        out
    }

    /// Encode `struct xfs_dsb` and fill `sb_crc`.
    ///
    /// The checksum is calculated over a full filesystem-sector buffer when
    /// `sector_size` is available, then truncated back to the structure bytes.
    pub fn encode_with_crc(&self) -> [u8; XFS_DSB_SIZE] {
        let requested_sector_size = (self.sector_size as usize).max(XFS_DSB_SIZE);
        let sector_size = if valid_sector_size(requested_sector_size, XFS_DSB_SIZE) {
            requested_sector_size
        } else {
            XFS_DSB_SIZE
        };

        let mut sector = [0_u8; XFS_MAX_SECTORSIZE];
        sector[..XFS_DSB_SIZE].copy_from_slice(&self.encode_without_crc());
        let _ = update_xfs_metadata_crc(&mut sector[..sector_size], XFS_SB_CRC_OFF);

        let mut out = [0_u8; XFS_DSB_SIZE];
        out.copy_from_slice(&sector[..XFS_DSB_SIZE]);
        out
    }

    /// Encode a full filesystem-sector superblock buffer and fill `sb_crc`.
    pub fn encode_sector_with_crc(&self, sector_size: usize) -> Option<Vec<u8>> {
        if !valid_sector_size(sector_size, XFS_DSB_SIZE) {
            return None;
        }

        let mut out = vec![0_u8; sector_size];
        out[..XFS_DSB_SIZE].copy_from_slice(&self.encode_without_crc());
        update_xfs_metadata_crc(&mut out, XFS_SB_CRC_OFF).then_some(out)
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Timestamp {
    pub seconds: i32,
    pub nanoseconds: u32,
}

impl Timestamp {
    pub fn xfsprogs_create_time() -> Self {
        Self {
            seconds: 315_532_800,
            nanoseconds: 0,
        }
    }

    fn encode(self, w: &mut Writer<'_>) {
        w.be32(self.seconds as u32);
        w.be32(self.nanoseconds);
    }

    fn encode_bigtime(self, w: &mut Writer<'_>) {
        const XFS_BIGTIME_EPOCH_OFFSET: i64 = 2_147_483_648;
        const NSEC_PER_SEC: u64 = 1_000_000_000;

        let seconds = (i64::from(self.seconds) + XFS_BIGTIME_EPOCH_OFFSET) as u64;
        w.be64(seconds * NSEC_PER_SEC + u64::from(self.nanoseconds));
    }
}

/// Host-order representation of the v3 `struct xfs_dinode` core.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dinode {
    pub mode: u16,
    pub version: u8,
    pub format: u8,
    pub metatype: u16,
    pub uid: u32,
    pub gid: u32,
    pub nlink: u32,
    pub project_id: u32,
    pub atime: Timestamp,
    pub mtime: Timestamp,
    pub ctime: Timestamp,
    pub size: u64,
    pub nblocks: u64,
    pub extsize: u32,
    pub nextents: u32,
    pub anextents: u16,
    pub forkoff: u8,
    pub aformat: u8,
    pub dmevmask: u32,
    pub dmstate: u16,
    pub flags: u16,
    pub generation: u32,
    pub next_unlinked: u32,
    pub changecount: u64,
    pub lsn: u64,
    pub flags2: u64,
    pub cowextsize: u32,
    pub crtime: Timestamp,
    pub ino: u64,
    pub uuid: [u8; 16],
}

impl Dinode {
    pub fn root_directory(ino: u64, uuid: [u8; 16]) -> Self {
        let time = Timestamp::xfsprogs_create_time();
        Self {
            mode: XFS_MODE_IFDIR | XFS_MODE_755,
            version: XFS_DINODE_VERSION_3,
            format: XFS_DINODE_FMT_LOCAL,
            nlink: 2,
            atime: time,
            mtime: time,
            ctime: time,
            size: shortform_directory_header_size(ino) as u64,
            next_unlinked: NULLAGINO,
            changecount: 2,
            flags2: XFS_DIFLAG2_BIGTIME | XFS_DIFLAG2_NREXT64,
            crtime: time,
            aformat: XFS_DINODE_FMT_EXTENTS,
            ino,
            uuid,
            ..Self::default()
        }
    }

    pub fn realtime_bitmap(ino: u64, uuid: [u8; 16]) -> Self {
        let time = Timestamp::xfsprogs_create_time();
        Self {
            mode: XFS_MODE_IFREG,
            version: XFS_DINODE_VERSION_3,
            format: XFS_DINODE_FMT_EXTENTS,
            nlink: 1,
            mtime: time,
            ctime: time,
            next_unlinked: NULLAGINO,
            flags: XFS_DIFLAG_NEWRTBM,
            changecount: 2,
            flags2: XFS_DIFLAG2_BIGTIME | XFS_DIFLAG2_NREXT64,
            crtime: time,
            ino,
            uuid,
            ..Self::default()
        }
    }

    pub fn realtime_summary(ino: u64, uuid: [u8; 16]) -> Self {
        let time = Timestamp::xfsprogs_create_time();
        Self {
            mode: XFS_MODE_IFREG,
            version: XFS_DINODE_VERSION_3,
            format: XFS_DINODE_FMT_EXTENTS,
            nlink: 1,
            atime: time,
            mtime: time,
            ctime: time,
            next_unlinked: NULLAGINO,
            changecount: 2,
            flags2: XFS_DIFLAG2_BIGTIME | XFS_DIFLAG2_NREXT64,
            crtime: time,
            ino,
            uuid,
            ..Self::default()
        }
    }

    pub fn free(ino: u64, uuid: [u8; 16]) -> Self {
        Self {
            version: XFS_DINODE_VERSION_3,
            format: XFS_DINODE_FMT_DEV,
            next_unlinked: NULLAGINO,
            aformat: XFS_DINODE_FMT_DEV,
            ino,
            uuid,
            ..Self::default()
        }
    }

    pub fn encode_without_crc(&self) -> [u8; XFS_DINODE_SIZE] {
        let mut out = [0_u8; XFS_DINODE_SIZE];
        let mut w = Writer::new(&mut out);

        w.be16(XFS_DINODE_MAGIC);
        w.be16(self.mode);
        w.u8(self.version);
        w.u8(self.format);
        w.be16(self.metatype);
        w.be32(self.uid);
        w.be32(self.gid);
        w.be32(self.nlink);
        w.be16((self.project_id & 0xffff) as u16);
        w.be16((self.project_id >> 16) as u16);
        w.be64(0);
        if self.flags2 & XFS_DIFLAG2_BIGTIME != 0 {
            self.atime.encode_bigtime(&mut w);
            self.mtime.encode_bigtime(&mut w);
            self.ctime.encode_bigtime(&mut w);
        } else {
            self.atime.encode(&mut w);
            self.mtime.encode(&mut w);
            self.ctime.encode(&mut w);
        }
        w.be64(self.size);
        w.be64(self.nblocks);
        w.be32(self.extsize);
        w.be32(self.nextents);
        w.be16(self.anextents);
        w.u8(self.forkoff);
        w.u8(self.aformat);
        w.be32(self.dmevmask);
        w.be16(self.dmstate);
        w.be16(self.flags);
        w.be32(self.generation);
        w.be32(self.next_unlinked);
        w.le32(0);
        w.be64(self.changecount);
        w.be64(self.lsn);
        w.be64(self.flags2);
        w.be32(self.cowextsize);
        w.zeros(12);
        if self.flags2 & XFS_DIFLAG2_BIGTIME != 0 {
            self.crtime.encode_bigtime(&mut w);
        } else {
            self.crtime.encode(&mut w);
        }
        w.be64(self.ino);
        w.bytes(&self.uuid);

        debug_assert!(w.finished(XFS_DINODE_SIZE));
        out
    }

    pub fn encode_inode_with_crc(&self, inode_size: usize) -> Option<Vec<u8>> {
        if !valid_inode_size(inode_size) {
            return None;
        }

        let mut out = vec![0_u8; inode_size];
        out[..XFS_DINODE_SIZE].copy_from_slice(&self.encode_without_crc());
        update_xfs_metadata_crc(&mut out, XFS_DINODE_CRC_OFF).then_some(out)
    }

    pub fn encode_inode_with_data_fork_and_crc(
        &self,
        inode_size: usize,
        data_fork: &[u8],
    ) -> Option<Vec<u8>> {
        let fork_end = XFS_DINODE_SIZE.checked_add(data_fork.len())?;
        if !valid_inode_size(inode_size) || inode_size < fork_end {
            return None;
        }

        let mut out = vec![0_u8; inode_size];
        out[..XFS_DINODE_SIZE].copy_from_slice(&self.encode_without_crc());
        out[XFS_DINODE_SIZE..fork_end].copy_from_slice(data_fork);
        update_xfs_metadata_crc(&mut out, XFS_DINODE_CRC_OFF).then_some(out)
    }
}

impl Default for Dinode {
    fn default() -> Self {
        Self {
            mode: 0,
            version: XFS_DINODE_VERSION_3,
            format: XFS_DINODE_FMT_EXTENTS,
            metatype: 0,
            uid: 0,
            gid: 0,
            nlink: 0,
            project_id: 0,
            atime: Timestamp::default(),
            mtime: Timestamp::default(),
            ctime: Timestamp::default(),
            size: 0,
            nblocks: 0,
            extsize: 0,
            nextents: 0,
            anextents: 0,
            forkoff: 0,
            aformat: XFS_DINODE_FMT_EXTENTS,
            dmevmask: 0,
            dmstate: 0,
            flags: 0,
            generation: 0,
            next_unlinked: NULLAGINO,
            changecount: 0,
            lsn: 0,
            flags2: 0,
            cowextsize: 0,
            crtime: Timestamp::default(),
            ino: 0,
            uuid: [0; 16],
        }
    }
}

pub fn root_shortform_directory(ino: u64) -> Option<Vec<u8>> {
    let mut out = Vec::with_capacity(shortform_directory_header_size(ino));
    out.push(0);
    if ino <= XFS_DIR2_MAX_SHORT_INUM {
        out.push(0);
        out.extend_from_slice(&(ino as u32).to_be_bytes());
    } else {
        out.push(1);
        out.extend_from_slice(&ino.to_be_bytes());
    }
    Some(out)
}

fn shortform_directory_header_size(parent_ino: u64) -> usize {
    if parent_ino <= XFS_DIR2_MAX_SHORT_INUM {
        XFS_DIR2_SF_HDR_SIZE_SHORT
    } else {
        XFS_DIR2_SF_HDR_SIZE_LONG
    }
}

pub fn initial_log_record_size(log_sector_size: u32) -> Option<usize> {
    let record_size = usize::try_from(log_sector_size)
        .ok()?
        .max(XLOG_INIT_RECORD_SIZE)
        .checked_next_power_of_two()?;
    (record_size <= XFS_MAX_SECTORSIZE).then_some(record_size)
}

pub fn initial_log_record(uuid: [u8; 16], log_sector_size: u32) -> Option<Vec<u8>> {
    let record_size = initial_log_record_size(log_sector_size)?;
    let data_len = record_size.checked_sub(XLOG_HEADER_SIZE)?;
    let mut out = vec![0_u8; record_size];
    {
        let mut w = Writer::new(&mut out[..XLOG_HEADER_SIZE]);
        w.be32(XLOG_HEADER_MAGIC_NUM);
        w.be32(1);
        w.be32(XLOG_VERSION_2);
        w.be32(data_len.try_into().ok()?);
        w.be64(1_u64 << 32);
        w.be64(1_u64 << 32);
        w.le32(0);
        w.be32(u32::MAX);
        w.be32(1);
        w.be32(XLOG_USERSPACE_TID);
        w.zeros(63 * std::mem::size_of::<u32>());
        w.be32(XLOG_FMT_LINUX_LE);
        w.bytes(&uuid);
        w.be32(XLOG_BIG_RECORD_BSIZE);
        w.pad_to(XLOG_HEADER_SIZE);
    }
    {
        let mut w = Writer::new(&mut out[XLOG_HEADER_SIZE..]);
        w.be32(1);
        w.be32(std::mem::size_of::<XfsUnmountLogFormat>() as u32);
        w.u8(XFS_LOG_CLIENT_ID);
        w.u8(XLOG_UNMOUNT_TRANS);
        w.be16(0);
        w.le16(XLOG_UNMOUNT_TYPE);
        w.le16(0);
        w.le32(0);
        w.pad_to(data_len);
    }

    for offset in (XLOG_HEADER_SIZE..record_size).step_by(XLOG_HEADER_SIZE) {
        out[offset..offset + std::mem::size_of::<u32>()].copy_from_slice(&1_u32.to_be_bytes());
    }

    Some(out)
}

#[repr(C)]
struct XfsUnmountLogFormat {
    magic: u16,
    pad1: u16,
    pad2: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Agf {
    pub seqno: u32,
    pub length: u32,
    pub bno_root: u32,
    pub cnt_root: u32,
    pub rmap_root: u32,
    pub bno_level: u32,
    pub cnt_level: u32,
    pub rmap_level: u32,
    pub flfirst: u32,
    pub fllast: u32,
    pub flcount: u32,
    pub free_blocks: u32,
    pub longest: u32,
    pub btree_blocks: u32,
    pub uuid: [u8; 16],
    pub rmap_blocks: u32,
    pub refcount_blocks: u32,
    pub refcount_root: u32,
    pub refcount_level: u32,
    pub lsn: u64,
}

impl Agf {
    pub fn encode_without_crc(&self) -> [u8; XFS_AGF_SIZE] {
        let mut out = [0_u8; XFS_AGF_SIZE];
        let mut w = Writer::new(&mut out);

        w.be32(XFS_AGF_MAGIC);
        w.be32(XFS_AGF_VERSION);
        w.be32(self.seqno);
        w.be32(self.length);
        w.be32(self.bno_root);
        w.be32(self.cnt_root);
        w.be32(self.rmap_root);
        w.be32(self.bno_level);
        w.be32(self.cnt_level);
        w.be32(self.rmap_level);
        w.be32(self.flfirst);
        w.be32(self.fllast);
        w.be32(self.flcount);
        w.be32(self.free_blocks);
        w.be32(self.longest);
        w.be32(self.btree_blocks);
        w.bytes(&self.uuid);
        w.be32(self.rmap_blocks);
        w.be32(self.refcount_blocks);
        w.be32(self.refcount_root);
        w.be32(self.refcount_level);
        for _ in 0..14 {
            w.be64(0);
        }
        w.be64(self.lsn);
        w.le32(0);
        w.be32(0);

        debug_assert!(w.finished(XFS_AGF_SIZE));
        out
    }

    pub fn encode_sector_with_crc(&self, sector_size: usize) -> Option<Vec<u8>> {
        encode_struct_sector_with_crc(&self.encode_without_crc(), sector_size, XFS_AGF_CRC_OFF)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Agi {
    pub seqno: u32,
    pub length: u32,
    pub count: u32,
    pub root: u32,
    pub level: u32,
    pub free_count: u32,
    pub newino: u32,
    pub dirino: u32,
    pub unlinked: [u32; 64],
    pub uuid: [u8; 16],
    pub lsn: u64,
    pub free_root: u32,
    pub free_level: u32,
    pub iblocks: u32,
    pub fblocks: u32,
}

impl Agi {
    pub fn encode_without_crc(&self) -> [u8; XFS_AGI_SIZE] {
        let mut out = [0_u8; XFS_AGI_SIZE];
        let mut w = Writer::new(&mut out);

        w.be32(XFS_AGI_MAGIC);
        w.be32(XFS_AGI_VERSION);
        w.be32(self.seqno);
        w.be32(self.length);
        w.be32(self.count);
        w.be32(self.root);
        w.be32(self.level);
        w.be32(self.free_count);
        w.be32(self.newino);
        w.be32(self.dirino);
        for agino in self.unlinked {
            w.be32(agino);
        }
        w.bytes(&self.uuid);
        w.le32(0);
        w.be32(0);
        w.be64(self.lsn);
        w.be32(self.free_root);
        w.be32(self.free_level);
        w.be32(self.iblocks);
        w.be32(self.fblocks);

        debug_assert!(w.finished(XFS_AGI_SIZE));
        out
    }

    pub fn encode_sector_with_crc(&self, sector_size: usize) -> Option<Vec<u8>> {
        encode_struct_sector_with_crc(&self.encode_without_crc(), sector_size, XFS_AGI_CRC_OFF)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Agfl {
    pub seqno: u32,
    pub uuid: [u8; 16],
    pub lsn: u64,
    pub block_numbers: Vec<u32>,
}

impl Agfl {
    pub fn new(seqno: u32, uuid: [u8; 16], sector_size: usize) -> Option<Self> {
        if !valid_sector_size(sector_size, XFS_AGFL_HEADER_SIZE) {
            return None;
        }
        let slots = (sector_size - XFS_AGFL_HEADER_SIZE) / std::mem::size_of::<u32>();
        Some(Self {
            seqno,
            uuid,
            lsn: 0,
            block_numbers: vec![NULLAGBLOCK; slots],
        })
    }

    pub fn encode_sector_with_crc(&self, sector_size: usize) -> Option<Vec<u8>> {
        if !valid_sector_size(sector_size, XFS_AGFL_HEADER_SIZE) {
            return None;
        }
        let expected_slots =
            (sector_size.checked_sub(XFS_AGFL_HEADER_SIZE)?) / std::mem::size_of::<u32>();
        if self.block_numbers.len() != expected_slots {
            return None;
        }

        let mut out = vec![0_u8; sector_size];
        let mut w = Writer::new(&mut out);
        w.be32(XFS_AGFL_MAGIC);
        w.be32(self.seqno);
        w.bytes(&self.uuid);
        w.be64(self.lsn);
        w.le32(0);
        for &agbno in &self.block_numbers {
            w.be32(agbno);
        }

        update_xfs_metadata_crc(&mut out, XFS_AGFL_CRC_OFF).then_some(out)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShortBtreeKind {
    Bno,
    Cnt,
    Inobt,
    Finobt,
    Rmap,
    Refcount,
}

impl ShortBtreeKind {
    fn magic(self) -> u32 {
        match self {
            Self::Bno => XFS_ABTB_CRC_MAGIC,
            Self::Cnt => XFS_ABTC_CRC_MAGIC,
            Self::Inobt => XFS_IBT_CRC_MAGIC,
            Self::Finobt => XFS_FIBT_CRC_MAGIC,
            Self::Rmap => XFS_RMAP_CRC_MAGIC,
            Self::Refcount => XFS_REFC_CRC_MAGIC,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocRecord {
    pub start_block: u32,
    pub block_count: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RmapRecord {
    pub start_block: u32,
    pub block_count: u32,
    pub owner: u64,
    pub offset: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InodeRecord {
    pub start_inode: u32,
    pub holemask: u16,
    pub count: u8,
    pub free_count: u8,
    pub free: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShortBtreeRecords {
    None,
    Alloc(Vec<AllocRecord>),
    Inode(Vec<InodeRecord>),
    Rmap(Vec<RmapRecord>),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShortBtreeBlock {
    pub kind: ShortBtreeKind,
    pub level: u16,
    pub daddr: u64,
    pub owner: u32,
    pub uuid: [u8; 16],
    pub records: ShortBtreeRecords,
}

impl ShortBtreeBlock {
    pub fn new_leaf(
        kind: ShortBtreeKind,
        daddr: u64,
        owner: u32,
        uuid: [u8; 16],
        records: ShortBtreeRecords,
    ) -> Self {
        Self {
            kind,
            level: 0,
            daddr,
            owner,
            uuid,
            records,
        }
    }

    pub fn encode_block_with_crc(&self, block_size: usize) -> Option<Vec<u8>> {
        if !valid_block_size(block_size, XFS_BTREE_SBLOCK_CRC_LEN) {
            return None;
        }
        let num_records = self.num_records()?;
        let record_bytes = self.record_bytes()?;
        if XFS_BTREE_SBLOCK_CRC_LEN.checked_add(record_bytes)? > block_size {
            return None;
        }

        let mut out = vec![0_u8; block_size];
        {
            let mut w = Writer::new(&mut out);
            w.be32(self.kind.magic());
            w.be16(self.level);
            w.be16(num_records);
            w.be32(NULLAGBLOCK);
            w.be32(NULLAGBLOCK);
            w.be64(self.daddr);
            w.be64(0);
            w.bytes(&self.uuid);
            w.be32(self.owner);
            w.le32(0);

            match &self.records {
                ShortBtreeRecords::None => {}
                ShortBtreeRecords::Alloc(records) => {
                    for record in records {
                        w.be32(record.start_block);
                        w.be32(record.block_count);
                    }
                }
                ShortBtreeRecords::Inode(records) => {
                    for record in records {
                        w.be32(record.start_inode);
                        w.be16(record.holemask);
                        w.u8(record.count);
                        w.u8(record.free_count);
                        w.be64(record.free);
                    }
                }
                ShortBtreeRecords::Rmap(records) => {
                    for record in records {
                        w.be32(record.start_block);
                        w.be32(record.block_count);
                        w.be64(record.owner);
                        w.be64(record.offset);
                    }
                }
            }
        }

        update_xfs_metadata_crc(&mut out, XFS_BTREE_SBLOCK_CRC_OFF).then_some(out)
    }

    fn num_records(&self) -> Option<u16> {
        match &self.records {
            ShortBtreeRecords::None => Some(0),
            ShortBtreeRecords::Alloc(records) => records.len().try_into().ok(),
            ShortBtreeRecords::Inode(records) => records.len().try_into().ok(),
            ShortBtreeRecords::Rmap(records) => records.len().try_into().ok(),
        }
    }

    fn record_bytes(&self) -> Option<usize> {
        match &self.records {
            ShortBtreeRecords::None => Some(0),
            ShortBtreeRecords::Alloc(records) => records.len().checked_mul(8),
            ShortBtreeRecords::Inode(records) => records.len().checked_mul(16),
            ShortBtreeRecords::Rmap(records) => records.len().checked_mul(24),
        }
    }
}

/// Update an XFS metadata CRC field in-place.
///
/// The checksum field is written as little-endian `!crc32c(0xffffffff, bytes)`,
/// after first zeroing the field itself, matching `xfs_update_cksum`.
pub fn update_xfs_metadata_crc(buffer: &mut [u8], offset: usize) -> bool {
    let Some(end) = offset.checked_add(std::mem::size_of::<u32>()) else {
        return false;
    };
    if end > buffer.len() {
        return false;
    }

    buffer[offset..end].fill(0);
    let crc = crc32c::update_raw(XFS_CRC_SEED, buffer);
    buffer[offset..end].copy_from_slice(&(!crc).to_le_bytes());
    true
}

fn encode_struct_sector_with_crc(
    encoded_struct: &[u8],
    sector_size: usize,
    crc_offset: usize,
) -> Option<Vec<u8>> {
    if !valid_sector_size(sector_size, encoded_struct.len()) {
        return None;
    }

    let mut out = vec![0_u8; sector_size];
    out[..encoded_struct.len()].copy_from_slice(encoded_struct);
    update_xfs_metadata_crc(&mut out, crc_offset).then_some(out)
}

fn valid_sector_size(size: usize, min: usize) -> bool {
    size >= min && size <= XFS_MAX_SECTORSIZE
}

fn valid_block_size(size: usize, min: usize) -> bool {
    size >= min && size <= XFS_MAX_BLOCKSIZE
}

fn valid_inode_size(size: usize) -> bool {
    (XFS_DINODE_SIZE..=XFS_DINODE_MAX_SIZE).contains(&size)
}

struct Writer<'a> {
    out: &'a mut [u8],
    pos: usize,
    failed: bool,
}

impl<'a> Writer<'a> {
    fn new(out: &'a mut [u8]) -> Self {
        Self {
            out,
            pos: 0,
            failed: false,
        }
    }

    fn finished(&self, expected: usize) -> bool {
        !self.failed && self.pos == expected
    }

    fn u8(&mut self, value: u8) {
        self.bytes(&[value]);
    }

    fn zeros(&mut self, len: usize) {
        let Some(end) = self.pos.checked_add(len) else {
            self.failed = true;
            self.pos = self.out.len();
            return;
        };
        let Some(dst) = self.out.get_mut(self.pos..end) else {
            self.failed = true;
            self.pos = self.out.len();
            return;
        };
        dst.fill(0);
        self.pos = end;
    }

    fn pad_to(&mut self, target: usize) {
        if target < self.pos {
            self.failed = true;
            return;
        }
        self.zeros(target - self.pos);
    }

    fn bytes(&mut self, value: &[u8]) {
        let Some(end) = self.pos.checked_add(value.len()) else {
            self.failed = true;
            self.pos = self.out.len();
            return;
        };
        let Some(dst) = self.out.get_mut(self.pos..end) else {
            self.failed = true;
            self.pos = self.out.len();
            return;
        };
        dst.copy_from_slice(value);
        self.pos = end;
    }

    fn be16(&mut self, value: u16) {
        self.bytes(&value.to_be_bytes());
    }

    fn be32(&mut self, value: u32) {
        self.bytes(&value.to_be_bytes());
    }

    fn be64(&mut self, value: u64) {
        self.bytes(&value.to_be_bytes());
    }

    fn le32(&mut self, value: u32) {
        self.bytes(&value.to_le_bytes());
    }

    fn le16(&mut self, value: u16) {
        self.bytes(&value.to_le_bytes());
    }
}

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

    #[test]
    fn encoded_superblock_has_xfsprogs_struct_size() {
        assert_eq!(Superblock::default().encode_without_crc().len(), 304);
    }

    #[test]
    fn encodes_fixed_fields_at_xfs_dsb_offsets() {
        let mut sb = Superblock {
            block_size: 4096,
            data_blocks: 262_144,
            uuid: [
                0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
                0xee, 0xff,
            ],
            ag_blocks: 65_536,
            ag_count: 4,
            version: 0xb4b5,
            sector_size: 512,
            inode_size: 512,
            inodes_per_block: 8,
            block_log: 12,
            sector_log: 9,
            inode_log: 9,
            inodes_per_block_log: 3,
            ag_blocks_log: 16,
            imax_pct: 25,
            ..Superblock::default()
        };
        sb.set_label(b"testfs");

        let bytes = sb.encode_without_crc();

        assert_eq!(&bytes[0..4], b"XFSB");
        assert_eq!(&bytes[4..8], &4096_u32.to_be_bytes());
        assert_eq!(&bytes[8..16], &262_144_u64.to_be_bytes());
        assert_eq!(&bytes[32..48], &sb.uuid);
        assert_eq!(&bytes[84..88], &65_536_u32.to_be_bytes());
        assert_eq!(&bytes[88..92], &4_u32.to_be_bytes());
        assert_eq!(&bytes[100..102], &0xb4b5_u16.to_be_bytes());
        assert_eq!(&bytes[108..114], b"testfs");
        assert_eq!(bytes[120], 12);
        assert_eq!(bytes[121], 9);
        assert_eq!(bytes[122], 9);
        assert_eq!(bytes[123], 3);
        assert_eq!(bytes[124], 16);
        assert_eq!(bytes[126], 1);
        assert_eq!(bytes[127], 25);
    }

    #[test]
    fn encodes_superblock_crc_at_xfs_offset() {
        let mut sb = Superblock {
            block_size: 4096,
            data_blocks: 262_144,
            ag_blocks: 65_536,
            ag_count: 4,
            version: 0xb4b5,
            sector_size: 512,
            inode_size: 512,
            inodes_per_block: 8,
            block_log: 12,
            sector_log: 9,
            inode_log: 9,
            inodes_per_block_log: 3,
            ag_blocks_log: 16,
            imax_pct: 25,
            ..Superblock::default()
        };
        sb.crc = 0xdead_beef;

        let without_crc = sb.encode_without_crc();
        let with_crc = sb.encode_with_crc();
        let mut expected = vec![0_u8; 512];
        expected[..XFS_DSB_SIZE].copy_from_slice(&without_crc);
        assert!(update_xfs_metadata_crc(&mut expected, XFS_SB_CRC_OFF));

        assert_eq!(with_crc, expected[..XFS_DSB_SIZE]);
        assert_ne!(&with_crc[XFS_SB_CRC_OFF..XFS_SB_CRC_OFF + 4], &[0; 4]);
        assert_eq!(
            &with_crc[XFS_SB_CRC_OFF..XFS_SB_CRC_OFF + 4],
            &expected[XFS_SB_CRC_OFF..XFS_SB_CRC_OFF + 4]
        );
    }

    #[test]
    fn metadata_crc_rejects_invalid_offset() {
        let mut bytes = [0_u8; 8];

        assert!(!update_xfs_metadata_crc(&mut bytes, 6));
    }

    #[test]
    fn public_sector_encoders_reject_oversized_allocations() {
        let sb = Superblock::default();
        assert!(sb.encode_sector_with_crc(usize::MAX).is_none());

        let agf = Agf {
            seqno: 0,
            length: 0,
            bno_root: 0,
            cnt_root: 0,
            rmap_root: 0,
            bno_level: 0,
            cnt_level: 0,
            rmap_level: 0,
            flfirst: 0,
            fllast: 0,
            flcount: 0,
            free_blocks: 0,
            longest: 0,
            btree_blocks: 0,
            uuid: [0; 16],
            rmap_blocks: 0,
            refcount_blocks: 0,
            refcount_root: 0,
            refcount_level: 0,
            lsn: 0,
        };
        assert!(agf.encode_sector_with_crc(usize::MAX).is_none());

        let agi = Agi {
            seqno: 0,
            length: 0,
            count: 0,
            root: 0,
            level: 0,
            free_count: 0,
            newino: 0,
            dirino: 0,
            unlinked: [0; 64],
            uuid: [0; 16],
            lsn: 0,
            free_root: 0,
            free_level: 0,
            iblocks: 0,
            fblocks: 0,
        };
        assert!(agi.encode_sector_with_crc(usize::MAX).is_none());

        assert!(Agfl::new(0, [0; 16], usize::MAX).is_none());
        let agfl = Agfl::new(0, [0; 16], 512).unwrap();
        assert!(agfl.encode_sector_with_crc(usize::MAX).is_none());
    }

    #[test]
    fn public_inode_and_btree_encoders_reject_oversized_allocations() {
        let dinode = Dinode::default();
        assert!(dinode.encode_inode_with_crc(usize::MAX).is_none());
        assert!(dinode
            .encode_inode_with_data_fork_and_crc(usize::MAX, &[])
            .is_none());

        let block = ShortBtreeBlock::new_leaf(
            ShortBtreeKind::Bno,
            0,
            0,
            [0; 16],
            ShortBtreeRecords::Alloc(Vec::new()),
        );
        assert!(block.encode_block_with_crc(usize::MAX).is_none());
    }

    #[test]
    fn public_log_record_encoder_rejects_oversized_allocations() {
        assert!(initial_log_record_size(u32::MAX).is_none());
        assert!(initial_log_record([0; 16], u32::MAX).is_none());
    }

    #[test]
    fn superblock_fixed_crc_encoder_is_infallible_for_invalid_public_sector_size() {
        let sb = Superblock {
            sector_size: u16::MAX,
            ..Superblock::default()
        };

        let bytes = sb.encode_with_crc();

        assert_eq!(bytes.len(), XFS_DSB_SIZE);
        assert_ne!(&bytes[XFS_SB_CRC_OFF..XFS_SB_CRC_OFF + 4], &[0; 4]);
    }

    #[test]
    fn encodes_initial_internal_log_record() {
        let uuid = [
            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
            0xee, 0xff,
        ];
        let bytes = initial_log_record(uuid, 512).unwrap();

        assert_eq!(bytes.len(), XLOG_INIT_RECORD_SIZE);
        assert_eq!(&bytes[0..4], &XLOG_HEADER_MAGIC_NUM.to_be_bytes());
        assert_eq!(&bytes[4..8], &1_u32.to_be_bytes());
        assert_eq!(&bytes[8..12], &XLOG_VERSION_2.to_be_bytes());
        assert_eq!(&bytes[12..16], &(XLOG_HEADER_SIZE as u32).to_be_bytes());
        assert_eq!(&bytes[16..24], &(1_u64 << 32).to_be_bytes());
        assert_eq!(&bytes[24..32], &(1_u64 << 32).to_be_bytes());
        assert_eq!(&bytes[32..36], &[0; 4]);
        assert_eq!(&bytes[36..40], &u32::MAX.to_be_bytes());
        assert_eq!(&bytes[40..44], &1_u32.to_be_bytes());
        assert_eq!(&bytes[44..48], &XLOG_USERSPACE_TID.to_be_bytes());
        assert_eq!(&bytes[300..304], &XLOG_FMT_LINUX_LE.to_be_bytes());
        assert_eq!(&bytes[304..320], &uuid);
        assert_eq!(&bytes[320..324], &XLOG_BIG_RECORD_BSIZE.to_be_bytes());

        assert_eq!(&bytes[512..516], &1_u32.to_be_bytes());
        assert_eq!(&bytes[516..520], &8_u32.to_be_bytes());
        assert_eq!(bytes[520], XFS_LOG_CLIENT_ID);
        assert_eq!(bytes[521], XLOG_UNMOUNT_TRANS);
        assert_eq!(&bytes[522..524], &[0; 2]);
        assert_eq!(&bytes[524..526], &XLOG_UNMOUNT_TYPE.to_le_bytes());
        assert_eq!(&bytes[526..528], &[0; 2]);
        assert_eq!(&bytes[528..532], &[0; 4]);
        assert!(bytes[532..].iter().all(|&byte| byte == 0));
    }

    #[test]
    fn encodes_initial_internal_log_record_for_large_log_sector() {
        let uuid = [
            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
            0xee, 0xff,
        ];
        let bytes = initial_log_record(uuid, 4096).unwrap();

        assert_eq!(bytes.len(), 4096);
        assert_eq!(&bytes[0..4], &XLOG_HEADER_MAGIC_NUM.to_be_bytes());
        assert_eq!(&bytes[12..16], &3584_u32.to_be_bytes());
        assert_eq!(&bytes[512..516], &1_u32.to_be_bytes());
        assert_eq!(&bytes[516..520], &8_u32.to_be_bytes());
        assert_eq!(&bytes[520..522], &[XFS_LOG_CLIENT_ID, XLOG_UNMOUNT_TRANS]);
        for offset in (1024..4096).step_by(XLOG_HEADER_SIZE) {
            assert_eq!(&bytes[offset..offset + 4], &1_u32.to_be_bytes());
            assert!(bytes[offset + 4..offset + XLOG_HEADER_SIZE]
                .iter()
                .all(|&byte| byte == 0));
        }
    }

    #[test]
    fn encoded_dinode_has_xfsprogs_struct_size() {
        assert_eq!(Dinode::default().encode_without_crc().len(), 176);
    }

    #[test]
    fn encodes_v3_root_dinode_core_offsets() {
        let dinode = Dinode::root_directory(
            128,
            [
                0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
                0xee, 0xff,
            ],
        );

        let bytes = dinode.encode_without_crc();

        assert_eq!(&bytes[0..2], &XFS_DINODE_MAGIC.to_be_bytes());
        assert_eq!(&bytes[2..4], &(XFS_MODE_IFDIR | XFS_MODE_755).to_be_bytes());
        assert_eq!(bytes[4], XFS_DINODE_VERSION_3);
        assert_eq!(bytes[5], XFS_DINODE_FMT_LOCAL);
        assert_eq!(&bytes[16..20], &2_u32.to_be_bytes());
        assert_eq!(
            &bytes[56..64],
            &(XFS_DIR2_SF_HDR_SIZE_SHORT as u64).to_be_bytes()
        );
        assert_eq!(&bytes[80..82], &0_u16.to_be_bytes());
        assert_eq!(bytes[82], 0);
        assert_eq!(bytes[83], XFS_DINODE_FMT_EXTENTS);
        assert_eq!(&bytes[96..100], &NULLAGINO.to_be_bytes());
        assert_eq!(&bytes[XFS_DINODE_CRC_OFF..XFS_DINODE_CRC_OFF + 4], &[0; 4]);
        assert_eq!(&bytes[152..160], &128_u64.to_be_bytes());
        assert_eq!(&bytes[160..176], &dinode.uuid);
    }

    #[test]
    fn encodes_dinode_crc_at_xfs_offset() {
        let mut dinode = Dinode::root_directory(128, [0xaa; 16]);
        dinode.changecount = 1;
        let without_crc = dinode.encode_without_crc();
        let with_crc = dinode.encode_inode_with_crc(512).unwrap();
        let mut expected = vec![0_u8; 512];
        expected[..XFS_DINODE_SIZE].copy_from_slice(&without_crc);
        assert!(update_xfs_metadata_crc(&mut expected, XFS_DINODE_CRC_OFF));

        assert_eq!(with_crc.len(), 512);
        assert_eq!(&with_crc[0..2], b"IN");
        assert_ne!(
            &with_crc[XFS_DINODE_CRC_OFF..XFS_DINODE_CRC_OFF + 4],
            &[0; 4]
        );
        assert_eq!(
            &with_crc[XFS_DINODE_CRC_OFF..XFS_DINODE_CRC_OFF + 4],
            &expected[XFS_DINODE_CRC_OFF..XFS_DINODE_CRC_OFF + 4]
        );
    }

    #[test]
    fn encodes_shortform_root_directory_header() {
        assert_eq!(
            root_shortform_directory(128).unwrap(),
            vec![0, 0, 0, 0, 0, 128]
        );
        assert_eq!(
            root_shortform_directory(u64::from(u32::MAX) + 1).unwrap(),
            vec![0, 1, 0, 0, 0, 1, 0, 0, 0, 0]
        );
    }

    #[test]
    fn encodes_dinode_data_fork_before_crc() {
        let dinode = Dinode::root_directory(128, [0xaa; 16]);
        let dir = root_shortform_directory(128).unwrap();
        let bytes = dinode
            .encode_inode_with_data_fork_and_crc(512, &dir)
            .unwrap();

        assert_eq!(&bytes[XFS_DINODE_SIZE..XFS_DINODE_SIZE + dir.len()], &dir);
        assert_ne!(&bytes[XFS_DINODE_CRC_OFF..XFS_DINODE_CRC_OFF + 4], &[0; 4]);
    }

    #[test]
    fn encodes_agf_at_xfs_offsets() {
        let agf = Agf {
            seqno: 2,
            length: 65_536,
            bno_root: 1,
            cnt_root: 2,
            rmap_root: 5,
            bno_level: 1,
            cnt_level: 1,
            rmap_level: 1,
            flfirst: 1,
            fllast: 0,
            flcount: 0,
            free_blocks: 65_529,
            longest: 65_529,
            btree_blocks: 0,
            uuid: [0xaa; 16],
            rmap_blocks: 1,
            refcount_blocks: 1,
            refcount_root: 6,
            refcount_level: 1,
            lsn: 0,
        };

        let bytes = agf.encode_without_crc();
        assert_eq!(bytes.len(), 224);
        assert_eq!(&bytes[0..4], b"XAGF");
        assert_eq!(&bytes[8..12], &2_u32.to_be_bytes());
        assert_eq!(&bytes[16..20], &1_u32.to_be_bytes());
        assert_eq!(&bytes[24..28], &5_u32.to_be_bytes());
        assert_eq!(&bytes[56..60], &65_529_u32.to_be_bytes());
        assert_eq!(&bytes[64..80], &[0xaa; 16]);
        assert_eq!(&bytes[88..92], &6_u32.to_be_bytes());
        assert_eq!(&bytes[XFS_AGF_CRC_OFF..XFS_AGF_CRC_OFF + 4], &[0; 4]);
    }

    #[test]
    fn encodes_agi_at_xfs_offsets() {
        let mut unlinked = [0_u32; 64];
        unlinked.fill(NULLAGINO);
        let agi = Agi {
            seqno: 3,
            length: 65_536,
            count: 0,
            root: 3,
            level: 1,
            free_count: 0,
            newino: NULLAGINO,
            dirino: NULLAGINO,
            unlinked,
            uuid: [0xbb; 16],
            lsn: 0,
            free_root: 4,
            free_level: 1,
            iblocks: 1,
            fblocks: 1,
        };

        let bytes = agi.encode_without_crc();
        assert_eq!(bytes.len(), 344);
        assert_eq!(&bytes[0..4], b"XAGI");
        assert_eq!(&bytes[8..12], &3_u32.to_be_bytes());
        assert_eq!(&bytes[20..24], &3_u32.to_be_bytes());
        assert_eq!(&bytes[32..36], &NULLAGINO.to_be_bytes());
        assert_eq!(&bytes[40..44], &NULLAGINO.to_be_bytes());
        assert_eq!(&bytes[296..312], &[0xbb; 16]);
        assert_eq!(&bytes[XFS_AGI_CRC_OFF..XFS_AGI_CRC_OFF + 4], &[0; 4]);
        assert_eq!(&bytes[328..332], &4_u32.to_be_bytes());
    }

    #[test]
    fn encodes_agfl_sector_with_crc() {
        let agfl = Agfl::new(1, [0xcc; 16], 512).unwrap();
        let bytes = agfl.encode_sector_with_crc(512).unwrap();

        assert_eq!(bytes.len(), 512);
        assert_eq!(&bytes[0..4], b"XAFL");
        assert_eq!(&bytes[4..8], &1_u32.to_be_bytes());
        assert_eq!(&bytes[8..24], &[0xcc; 16]);
        assert_ne!(&bytes[XFS_AGFL_CRC_OFF..XFS_AGFL_CRC_OFF + 4], &[0; 4]);
        assert_eq!(&bytes[36..40], &NULLAGBLOCK.to_be_bytes());
        assert_eq!(&bytes[508..512], &NULLAGBLOCK.to_be_bytes());
    }

    #[test]
    fn encodes_short_btree_alloc_root() {
        let block = ShortBtreeBlock::new_leaf(
            ShortBtreeKind::Bno,
            8,
            0,
            [0xdd; 16],
            ShortBtreeRecords::Alloc(vec![AllocRecord {
                start_block: 7,
                block_count: 65_529,
            }]),
        );

        let bytes = block.encode_block_with_crc(4096).unwrap();
        assert_eq!(bytes.len(), 4096);
        assert_eq!(&bytes[0..4], b"AB3B");
        assert_eq!(&bytes[4..6], &0_u16.to_be_bytes());
        assert_eq!(&bytes[6..8], &1_u16.to_be_bytes());
        assert_eq!(&bytes[8..12], &NULLAGBLOCK.to_be_bytes());
        assert_eq!(&bytes[12..16], &NULLAGBLOCK.to_be_bytes());
        assert_eq!(&bytes[16..24], &8_u64.to_be_bytes());
        assert_eq!(&bytes[32..48], &[0xdd; 16]);
        assert_eq!(&bytes[48..52], &0_u32.to_be_bytes());
        assert_ne!(
            &bytes[XFS_BTREE_SBLOCK_CRC_OFF..XFS_BTREE_SBLOCK_CRC_OFF + 4],
            &[0; 4]
        );
        assert_eq!(&bytes[56..60], &7_u32.to_be_bytes());
        assert_eq!(&bytes[60..64], &65_529_u32.to_be_bytes());
    }

    #[test]
    fn encodes_short_btree_inode_root() {
        let block = ShortBtreeBlock::new_leaf(
            ShortBtreeKind::Inobt,
            24,
            0,
            [0xaa; 16],
            ShortBtreeRecords::Inode(vec![InodeRecord {
                start_inode: 128,
                holemask: 0,
                count: XFS_INODES_PER_CHUNK as u8,
                free_count: 63,
                free: XFS_INOBT_ALL_FREE & !1,
            }]),
        );

        let bytes = block.encode_block_with_crc(4096).unwrap();
        assert_eq!(&bytes[0..4], b"IAB3");
        assert_eq!(&bytes[6..8], &1_u16.to_be_bytes());
        assert_eq!(&bytes[16..24], &24_u64.to_be_bytes());
        assert_eq!(&bytes[56..60], &128_u32.to_be_bytes());
        assert_eq!(&bytes[60..62], &0_u16.to_be_bytes());
        assert_eq!(bytes[62], 64);
        assert_eq!(bytes[63], 63);
        assert_eq!(&bytes[64..72], &(XFS_INOBT_ALL_FREE & !1).to_be_bytes());
    }

    #[test]
    fn encodes_short_btree_rmap_root() {
        let block = ShortBtreeBlock::new_leaf(
            ShortBtreeKind::Rmap,
            40,
            0,
            [0xee; 16],
            ShortBtreeRecords::Rmap(vec![RmapRecord {
                start_block: 0,
                block_count: 1,
                owner: XFS_RMAP_OWN_FS,
                offset: 0,
            }]),
        );

        let bytes = block.encode_block_with_crc(4096).unwrap();
        assert_eq!(&bytes[0..4], b"RMB3");
        assert_eq!(&bytes[6..8], &1_u16.to_be_bytes());
        assert_eq!(&bytes[16..24], &40_u64.to_be_bytes());
        assert_eq!(&bytes[56..60], &0_u32.to_be_bytes());
        assert_eq!(&bytes[60..64], &1_u32.to_be_bytes());
        assert_eq!(&bytes[64..72], &XFS_RMAP_OWN_FS.to_be_bytes());
        assert_eq!(&bytes[72..80], &0_u64.to_be_bytes());
    }
}
