<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use std::{
    fmt,
    io::{self, Read, Seek, SeekFrom},
    str::from_utf8,
};

use byteorder::{ByteOrder, LittleEndian};
use chrono::{DateTime, Utc};
use crc::crc32;
use uuid::Uuid;

use devicemapper::{Bytes, Sectors, IEC, SECTOR_SIZE};

use crate::{
    engine::{strat_engine::device::SyncAll, DevUuid, PoolUuid},
    stratis::{ErrorEnum, StratisError, StratisResult},
};

pub use self::mda::{validate_mda_size, MIN_MDA_SECTORS};

const _BDA_STATIC_HDR_SIZE: usize = 16 * SECTOR_SIZE;
const BDA_STATIC_HDR_SIZE: Bytes = Bytes(_BDA_STATIC_HDR_SIZE as u64);

const MDA_RESERVED_SECTORS: Sectors = Sectors(3 * IEC::Mi / (SECTOR_SIZE as u64)); // = 3 MiB

const STRAT_MAGIC: &[u8] = b"!Stra0tis\x86\xff\x02^\x41rh";

const STRAT_SIGBLOCK_VERSION: u8 = 1;

#[derive(Debug)]
pub struct BDA {
    header: StaticHeader,
    regions: mda::MDARegions,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MetadataLocation {
    Both,
    First,
    Second,
}

impl BDA {
    /// Read the BDA from the device and return 2 SECTORS worth of data, one for each BDA returned
    /// in the order of layout on disk (location 1, location 2).
    /// Only the BDA sectors are read up from disk, zero areas are *not* read.
    fn read<F>(f: &mut F) -> (io::Result<[u8; SECTOR_SIZE]>, io::Result<[u8; SECTOR_SIZE]>)
    where
        F: Read + Seek,
    {
        // Theory of read procedure
        // We write the BDA in two operations with a sync in between.  The write operation
        // could fail (loss of power) for either write leaving sector(s) with potentially hard
        // read errors. It's best to read each of the specific BDA blocks individually, to limit
        // the probability of hitting a read error on a non-essential sector.

        let mut buf_loc_1 = [0u8; SECTOR_SIZE];
        let mut buf_loc_2 = [0u8; SECTOR_SIZE];

        /// Read a bda sector worth of data at the specified offset into buffer.
        fn read_sector_at_offset<F>(f: &mut F, offset: usize, mut buf: &mut [u8]) -> io::Result<()>
        where
            F: Read + Seek,
        {
            f.seek(SeekFrom::Start(offset as u64))?;
            f.read_exact(&mut buf)?;
            Ok(())
        }

        (
            read_sector_at_offset(f, SECTOR_SIZE, &mut buf_loc_1).map(|_| buf_loc_1),
            read_sector_at_offset(f, 9 * SECTOR_SIZE, &mut buf_loc_2).map(|_| buf_loc_2),
        )
    }

    // Writes bda_buf according to the value of which.
    // If first location is specified, write zeroes to empty regions in the
    // first 8 sectors. If the second location is specified, writes zeroes to empty
    // regions in the second 8 sectors.
    fn write<F>(f: &mut F, bda_buf: &[u8], which: MetadataLocation) -> io::Result<()>
    where
        F: Seek + SyncAll,
    {
        let zeroed = [0u8; _BDA_STATIC_HDR_SIZE];
        f.seek(SeekFrom::Start(0))?;

        // Write to a single region in the header. Zeroes the first sector,
        // writes bda_buf to the second sector, and then zeroes the remaining
        // six sectors.
        fn write_region<F>(f: &mut F, bda_buf: &[u8], zeroed: &[u8]) -> io::Result<()>
        where
            F: Seek + SyncAll,
        {
            f.write_all(&zeroed[..SECTOR_SIZE])?; // Zero 1 unused sector
            f.write_all(bda_buf)?;
            f.write_all(&zeroed[..SECTOR_SIZE * 6])?; // Zero 6 unused sectors
            f.sync_all()?;
            Ok(())
        };

        if which == MetadataLocation::Both || which == MetadataLocation::First {
            write_region(f, bda_buf, &zeroed)?;
        } else {
            f.seek(SeekFrom::Start(8 * SECTOR_SIZE as u64))?;
        }

        if which == MetadataLocation::Both || which == MetadataLocation::Second {
            write_region(f, bda_buf, &zeroed)?;
        }
        Ok(())
    }

    /// Initialize a blockdev with a Stratis BDA.
    pub fn initialize<F>(
        f: &mut F,
        pool_uuid: Uuid,
        dev_uuid: Uuid,
        mda_size: Sectors,
        blkdev_size: Sectors,
        initialization_time: u64,
    ) -> StratisResult<BDA>
    where
        F: Seek + SyncAll,
    {
        let header = StaticHeader::new(
            pool_uuid,
            dev_uuid,
            mda_size,
            blkdev_size,
            initialization_time,
        );

        BDA::write(f, &header.sigblock_to_buf(), MetadataLocation::Both)?;

        let regions = mda::MDARegions::initialize(BDA_STATIC_HDR_SIZE, header.mda_size, f)?;

        Ok(BDA { header, regions })
    }

    /// Load a BDA on initial setup of a device.
    /// Returns None if no BDA appears to exist.
    pub fn load<F>(f: &mut F) -> StratisResult<Option<BDA>>
    where
        F: Read + Seek + SyncAll,
    {
        let header = match StaticHeader::setup(f)? {
            Some(header) => header,
            None => return Ok(None),
        };

        let regions = mda::MDARegions::load(BDA_STATIC_HDR_SIZE, header.mda_size, f)?;

        Ok(Some(BDA { header, regions }))
    }

    /// Zero out Static Header on the blockdev. This causes it to no
    /// longer be seen as a Stratis blockdev.
    pub fn wipe<F>(f: &mut F) -> StratisResult<()>
    where
        F: Seek + SyncAll,
    {
        let zeroed = [0u8; _BDA_STATIC_HDR_SIZE];

        // Wiping Static Header should do it
        f.seek(SeekFrom::Start(0))?;
        f.write_all(&zeroed)?;
        f.sync_all()?;
        Ok(())
    }

    /// Save metadata to the disk
    pub fn save_state<F>(
        &mut self,
        time: &DateTime<Utc>,
        metadata: &[u8],
        f: &mut F,
    ) -> StratisResult<()>
    where
        F: Seek + SyncAll,
    {
        self.regions
            .save_state(BDA_STATIC_HDR_SIZE, time, metadata, f)
    }

    /// Read latest metadata from the disk
    pub fn load_state<F>(&self, mut f: &mut F) -> StratisResult<Option<Vec<u8>>>
    where
        F: Read + Seek,
    {
        self.regions.load_state(BDA_STATIC_HDR_SIZE, &mut f)
    }

    /// The time when the most recent metadata was written to the BDA,
    /// if any.
    pub fn last_update_time(&self) -> Option<&DateTime<Utc>> {
        self.regions.last_update_time()
    }

    /// The UUID of the device.
    pub fn dev_uuid(&self) -> DevUuid {
        self.header.dev_uuid
    }

    /// The UUID of the device's pool.
    pub fn pool_uuid(&self) -> PoolUuid {
        self.header.pool_uuid
    }

    /// The size of the device.
    pub fn dev_size(&self) -> Sectors {
        self.header.blkdev_size
    }

    /// The number of sectors the BDA itself occupies.
    pub fn size(&self) -> Sectors {
        BDA_STATIC_HDR_SIZE.sectors() + self.header.mda_size + self.header.reserved_size
    }

    /// The maximum size of variable length metadata that can be accommodated.
    pub fn max_data_size(&self) -> Bytes {
        self.regions.max_data_size()
    }

    /// Timestamp when the device was initialized.
    pub fn initialization_time(&self) -> u64 {
        self.header.initialization_time
    }
}

#[derive(Eq, PartialEq)]
pub struct StaticHeader {
    blkdev_size: Sectors,
    pool_uuid: PoolUuid,
    dev_uuid: DevUuid,
    mda_size: Sectors,
    reserved_size: Sectors,
    flags: u64,
    /// Seconds portion of DateTime<Utc> value.
    initialization_time: u64,
}

impl StaticHeader {
    fn new(
        pool_uuid: PoolUuid,
        dev_uuid: DevUuid,
        mda_size: Sectors,
        blkdev_size: Sectors,
        initialization_time: u64,
    ) -> StaticHeader {
        StaticHeader {
            blkdev_size,
            pool_uuid,
            dev_uuid,
            mda_size,
            reserved_size: MDA_RESERVED_SECTORS,
            flags: 0,
            initialization_time,
        }
    }

    /// Try to find a valid StaticHeader on a device.
    /// Return the latest copy that validates as a Stratis BDA, however verify both
    /// copies and if one validates but one does not, re-write the one that is incorrect.  If both
    /// copies are valid, but one is newer than the other, rewrite the older one to match.
    /// Return None if it's not a Stratis device.
    /// Return an error if the metadata seems to indicate that the device is
    /// a Stratis device, but no well-formed signature block could be read.
    /// Return an error if neither sigblock location can be read.
    /// Return an error if the sigblocks differ in some unaccountable way.
    /// Returns an error if a write intended to repair an ill-formed,
    /// unreadable, or stale signature block failed.
    fn setup<F>(f: &mut F) -> StratisResult<Option<StaticHeader>>
    where
        F: Read + Seek + SyncAll,
    {
        match BDA::read(f) {
            (Ok(buf_loc_1), Ok(buf_loc_2)) => {
                // We read both copies without an IO error.
                match (
                    StaticHeader::sigblock_from_buf(&buf_loc_1),
                    StaticHeader::sigblock_from_buf(&buf_loc_2),
                ) {
                    (Ok(loc_1), Ok(loc_2)) => {
                        match (loc_1, loc_2) {
                            (Some(loc_1), Some(loc_2)) => {
                                if loc_1 == loc_2 {
                                    Ok(Some(loc_1))
                                } else if loc_1.initialization_time == loc_2.initialization_time {
                                    // Inexplicable disagreement among static headers
                                    let err_str = "Appeared to be a Stratis device, but signature blocks disagree.";
                                    Err(StratisError::Engine(ErrorEnum::Invalid, err_str.into()))
                                } else if loc_1.initialization_time > loc_2.initialization_time {
                                    // If the first header block is newer, overwrite second with
                                    // contents of first.
                                    BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                                    Ok(Some(loc_1))
                                } else {
                                    // The second header block must be newer, so overwrite first
                                    // with contents of second.
                                    BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                                    Ok(Some(loc_2))
                                }
                            }
                            (None, None) => Ok(None),
                            (Some(loc_1), None) => {
                                // Copy 1 has valid Stratis BDA, copy 2 has no magic, re-write copy 2
                                BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                                Ok(Some(loc_1))
                            }
                            (None, Some(loc_2)) => {
                                // Copy 2 has valid Stratis BDA, copy 1 has no magic, re-write copy 1
                                BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                                Ok(Some(loc_2))
                            }
                        }
                    }
                    (Ok(loc_1), Err(loc_2)) => {
                        // Re-write copy 2
                        if loc_1.is_some() {
                            BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                            Ok(loc_1)
                        } else {
                            // Location 1 doesn't have a signature, but location 2 did, but it got an error,
                            // lets return the error instead as this appears to be a stratis device that
                            // has gotten in a bad state.
                            Err(loc_2)
                        }
                    }
                    (Err(loc_1), Ok(loc_2)) => {
                        // Re-write copy 1
                        if loc_2.is_some() {
                            BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                            Ok(loc_2)
                        } else {
                            // Location 2 doesn't have a signature, but location 1 did, but it got an error,
                            // lets return the error instead as this appears to be a stratis device that
                            // has gotten in a bad state.
                            Err(loc_1)
                        }
                    }
                    (Err(_), Err(_)) => {
                        let err_str =
                            "Appeared to be a Stratis device, but no valid sigblock found";
                        Err(StratisError::Engine(ErrorEnum::Invalid, err_str.into()))
                    }
                }
            }
            (Ok(buf_loc_1), Err(_)) => {
                // Copy 1 read OK, 2 resulted in an IO error
                match StaticHeader::sigblock_from_buf(&buf_loc_1) {
                    Ok(loc_1) => {
                        if loc_1.is_some() {
                            BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                        }
                        Ok(loc_1)
                    }
                    Err(e) => {
                        // Unable to determine if location 2 has a signature, but location 1 did,
                        // but it got an error, lets return the error instead as this appears to
                        // be a stratis device that has gotten in a bad state.
                        Err(e)
                    }
                }
            }
            (Err(_), Ok(buf_loc_2)) => {
                // Copy 2 read OK, 1 resulted in IO Error
                match StaticHeader::sigblock_from_buf(&buf_loc_2) {
                    Ok(loc_2) => {
                        if loc_2.is_some() {
                            BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                        }
                        Ok(loc_2)
                    }
                    Err(e) => {
                        // Unable to determine if location 1 has a signature, but location 2 did,
                        // but it got an error, lets return the error instead as this appears to
                        // be a stratis device that has gotten in a bad state.
                        Err(e)
                    }
                }
            }
            (Err(_), Err(_)) => {
                // Unable to read the device at all.
                let err_str = "Unable to read data at sigblock locations.";
                Err(StratisError::Engine(ErrorEnum::Invalid, err_str.into()))
            }
        }
    }

    /// Retrieve the device and pool UUIDs from a stratis device.
    pub fn device_identifiers<F>(f: &mut F) -> StratisResult<Option<((PoolUuid, DevUuid))>>
    where
        F: Read + Seek + SyncAll,
    {
        // Using setup() as a test of ownership sets a high bar. It is
        // not sufficient to have STRAT_MAGIC to be considered "Ours",
        // it must also have correct CRC, no weird stuff in fields,
        // etc!
        match StaticHeader::setup(f) {
            Ok(Some(sh)) => Ok(Some((sh.pool_uuid, sh.dev_uuid))),
            Ok(None) => Ok(None),
            Err(err) => Err(err),
        }
    }

    /// Generate a buf suitable for writing to blockdev
    fn sigblock_to_buf(&self) -> [u8; SECTOR_SIZE] {
        let mut buf = [0u8; SECTOR_SIZE];
        buf[4..20].clone_from_slice(STRAT_MAGIC);
        LittleEndian::write_u64(&mut buf[20..28], *self.blkdev_size);
        buf[28] = STRAT_SIGBLOCK_VERSION;
        buf[32..64].clone_from_slice(self.pool_uuid.to_simple_ref().to_string().as_bytes());
        buf[64..96].clone_from_slice(self.dev_uuid.to_simple_ref().to_string().as_bytes());
        LittleEndian::write_u64(&mut buf[96..104], *self.mda_size);
        LittleEndian::write_u64(&mut buf[104..112], *self.reserved_size);
        LittleEndian::write_u64(&mut buf[120..128], self.initialization_time);

        let hdr_crc = crc32::checksum_castagnoli(&buf[4..SECTOR_SIZE]);
        LittleEndian::write_u32(&mut buf[..4], hdr_crc);
        buf
    }

    /// Build a StaticHeader from a SECTOR_SIZE buf that was read from
    /// a blockdev.
    fn sigblock_from_buf(buf: &[u8]) -> StratisResult<Option<StaticHeader>> {
        assert_eq!(buf.len(), SECTOR_SIZE);

        if &buf[4..20] != STRAT_MAGIC {
            return Ok(None);
        }

        let crc = crc32::checksum_castagnoli(&buf[4..SECTOR_SIZE]);
        if crc != LittleEndian::read_u32(&buf[..4]) {
            return Err(StratisError::Engine(
                ErrorEnum::Invalid,
                "header CRC invalid".into(),
            ));
        }

        let blkdev_size = Sectors(LittleEndian::read_u64(&buf[20..28]));

        let version = buf[28];
        if version != STRAT_SIGBLOCK_VERSION {
            return Err(StratisError::Engine(
                ErrorEnum::Invalid,
                format!("Unknown sigblock version: {}", version),
            ));
        }

        let pool_uuid = Uuid::parse_str(from_utf8(&buf[32..64])?)?;
        let dev_uuid = Uuid::parse_str(from_utf8(&buf[64..96])?)?;

        let mda_size = Sectors(LittleEndian::read_u64(&buf[96..104]));

        mda::validate_mda_size(mda_size)?;

        Ok(Some(StaticHeader {
            pool_uuid,
            dev_uuid,
            blkdev_size,
            mda_size,
            reserved_size: Sectors(LittleEndian::read_u64(&buf[104..112])),
            flags: 0,
            initialization_time: LittleEndian::read_u64(&buf[120..128]),
        }))
    }
}

impl fmt::Debug for StaticHeader {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("StaticHeader")
            .field("blkdev_size", &self.blkdev_size)
            .field("pool_uuid", &self.pool_uuid.to_simple_ref())
            .field("dev_uuid", &self.dev_uuid.to_simple_ref())
            .field("mda_size", &self.mda_size)
            .field("reserved_size", &self.reserved_size)
            .field("flags", &self.flags)
            .field("initialization_time", &self.initialization_time)
            .finish()
    }
}

mod mda {
    use std::{
        self,
        cmp::Ordering,
        io::{Read, Seek, SeekFrom},
    };

    use byteorder::{ByteOrder, LittleEndian};
    use chrono::{DateTime, TimeZone, Utc};
    use crc::crc32;

    use devicemapper::{Bytes, Sectors};

    use crate::{
        engine::strat_engine::device::SyncAll,
        stratis::{ErrorEnum, StratisError, StratisResult},
    };

    const _MDA_REGION_HDR_SIZE: usize = 32;
    const MDA_REGION_HDR_SIZE: Bytes = Bytes(_MDA_REGION_HDR_SIZE as u64);

    const NUM_PRIMARY_MDA_REGIONS: usize = 2;

    // There are two copies of every primary MDA region, so the total number
    // of MDA regions is twice the number of primary MDA regions.
    const NUM_MDA_REGIONS: usize = 2 * NUM_PRIMARY_MDA_REGIONS;

    pub const MIN_MDA_SECTORS: Sectors = Sectors(2032);

    const STRAT_REGION_HDR_VERSION: u8 = 1;
    const STRAT_METADATA_VERSION: u8 = 1;

    /// Manages the MDA regions which hold the variable length metadata.
    #[derive(Debug)]
    pub struct MDARegions {
        /// The size of a single MDA region. The MDAHeader occupies the
        /// first few bytes of its region, the rest is available for the
        /// variable length metadata.
        region_size: Sectors,
        /// The MDA headers which contain information about the variable
        /// length metadata. NUM_PRIMARY_MDA_REGIONS is 2: in the general
        /// case one is more recently written than the other.
        mdas: [Option<MDAHeader>; NUM_PRIMARY_MDA_REGIONS],
    }

    impl MDARegions {
        /// Calculate the offset from start of device for an MDARegion.
        fn mda_offset(header_size: Bytes, index: usize, per_region_size: Bytes) -> u64 {
            *(header_size + per_region_size * index)
        }

        /// The maximum size of variable length metadata that this region
        /// can accommodate.
        pub fn max_data_size(&self) -> Bytes {
            self.region_size.bytes() - MDA_REGION_HDR_SIZE
        }

        /// Initialize the space allotted to the MDA regions to 0.
        /// Return an MDARegions object with uninitialized MDAHeader objects.
        pub fn initialize<F>(
            header_size: Bytes,
            size: Sectors,
            f: &mut F,
        ) -> StratisResult<MDARegions>
        where
            F: Seek + SyncAll,
        {
            let hdr_buf = MDAHeader::default().to_buf();

            let region_size = size / NUM_MDA_REGIONS;
            let per_region_size = region_size.bytes();
            for region in 0..NUM_MDA_REGIONS {
                f.seek(SeekFrom::Start(MDARegions::mda_offset(
                    header_size,
                    region,
                    per_region_size,
                )))?;
                f.write_all(&hdr_buf)?;
            }

            f.sync_all()?;

            Ok(MDARegions {
                region_size,
                mdas: [None, None],
            })
        }

        /// Construct MDARegions from data on the disk.
        /// Note that this method is always called in a context where a
        /// StaticHeader has already been read. Therefore, it
        /// constitutes an error if it is not possible to discover two
        /// well-formed MDAHeaders for this device.
        pub fn load<F>(header_size: Bytes, size: Sectors, f: &mut F) -> StratisResult<MDARegions>
        where
            F: Read + Seek,
        {
            let region_size = size / NUM_MDA_REGIONS;
            let per_region_size = region_size.bytes();

            // Load a single region at the location specified by index.
            // If it appears that no metadata has been written at the location
            // return None. If it appears that there is metadata, but it has
            // been corrupted, return an error.
            let mut load_a_region = |index: usize| -> StratisResult<Option<MDAHeader>> {
                let mut hdr_buf = [0u8; _MDA_REGION_HDR_SIZE];
                f.seek(SeekFrom::Start(MDARegions::mda_offset(
                    header_size,
                    index,
                    per_region_size,
                )))?;
                f.read_exact(&mut hdr_buf)?;
                Ok(MDAHeader::from_buf(&hdr_buf)?)
            };

            // Get an MDAHeader for the given index.
            // If there is a failure reading the first, fall back on the
            // second. If there is a failure reading both, return an error.
            let mut get_mda = |index: usize| -> StratisResult<Option<MDAHeader>> {
                load_a_region(index).or_else(|_| load_a_region(index + NUM_PRIMARY_MDA_REGIONS))
            };

            Ok(MDARegions {
                region_size,
                mdas: [get_mda(0)?, get_mda(1)?],
            })
        }

        /// Write metadata to the older of the metadata regions.
        /// If operation is completed, update the value of the
        /// older MDAHeader with the new values.
        /// If time specified is earlier than the last update time, return an
        /// error. If the size of the data is greater than the available space,
        /// return an error. If there is an error when writing the data, return
        /// an error.
        pub fn save_state<F>(
            &mut self,
            header_size: Bytes,
            time: &DateTime<Utc>,
            data: &[u8],
            f: &mut F,
        ) -> StratisResult<()>
        where
            F: Seek + SyncAll,
        {
            if self.last_update_time() >= Some(time) {
                return Err(StratisError::Engine(
                    ErrorEnum::Invalid,
                    "Overwriting newer data".into(),
                ));
            }

            let region_size = self.region_size.bytes();
            let used = Bytes(data.len() as u64);

            if MDA_REGION_HDR_SIZE + used > region_size {
                let err_msg = format!(
                    "metadata length {} exceeds region available {}",
                    used,
                    // region_size > header size
                    region_size - MDA_REGION_HDR_SIZE
                );
                return Err(StratisError::Engine(ErrorEnum::Invalid, err_msg));
            };

            let header = MDAHeader {
                last_updated: *time,
                used,
                data_crc: crc32::checksum_castagnoli(data),
            };
            let hdr_buf = header.to_buf();

            // Write data to a region specified by index.
            let mut save_region = |index: usize| -> StratisResult<()> {
                f.seek(SeekFrom::Start(MDARegions::mda_offset(
                    header_size,
                    index,
                    region_size,
                )))?;
                f.write_all(&hdr_buf)?;
                f.write_all(data)?;
                f.sync_all()?;

                Ok(())
            };

            // TODO: Consider if there is an action that should be taken if
            // saving to one or the other region fails.
            let older_region = self.older();
            save_region(older_region)?;
            save_region(older_region + NUM_PRIMARY_MDA_REGIONS)?;

            self.mdas[older_region] = Some(header);

            Ok(())
        }

        /// Load metadata from the newer MDA region.
        /// In case there is no record of metadata in regions, return None.
        /// If there is a record of metadata, and there is a failure to read<commit_msg>Use max_data_size in place of an expression<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use std::{
    fmt,
    io::{self, Read, Seek, SeekFrom},
    str::from_utf8,
};

use byteorder::{ByteOrder, LittleEndian};
use chrono::{DateTime, Utc};
use crc::crc32;
use uuid::Uuid;

use devicemapper::{Bytes, Sectors, IEC, SECTOR_SIZE};

use crate::{
    engine::{strat_engine::device::SyncAll, DevUuid, PoolUuid},
    stratis::{ErrorEnum, StratisError, StratisResult},
};

pub use self::mda::{validate_mda_size, MIN_MDA_SECTORS};

const _BDA_STATIC_HDR_SIZE: usize = 16 * SECTOR_SIZE;
const BDA_STATIC_HDR_SIZE: Bytes = Bytes(_BDA_STATIC_HDR_SIZE as u64);

const MDA_RESERVED_SECTORS: Sectors = Sectors(3 * IEC::Mi / (SECTOR_SIZE as u64)); // = 3 MiB

const STRAT_MAGIC: &[u8] = b"!Stra0tis\x86\xff\x02^\x41rh";

const STRAT_SIGBLOCK_VERSION: u8 = 1;

#[derive(Debug)]
pub struct BDA {
    header: StaticHeader,
    regions: mda::MDARegions,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MetadataLocation {
    Both,
    First,
    Second,
}

impl BDA {
    /// Read the BDA from the device and return 2 SECTORS worth of data, one for each BDA returned
    /// in the order of layout on disk (location 1, location 2).
    /// Only the BDA sectors are read up from disk, zero areas are *not* read.
    fn read<F>(f: &mut F) -> (io::Result<[u8; SECTOR_SIZE]>, io::Result<[u8; SECTOR_SIZE]>)
    where
        F: Read + Seek,
    {
        // Theory of read procedure
        // We write the BDA in two operations with a sync in between.  The write operation
        // could fail (loss of power) for either write leaving sector(s) with potentially hard
        // read errors. It's best to read each of the specific BDA blocks individually, to limit
        // the probability of hitting a read error on a non-essential sector.

        let mut buf_loc_1 = [0u8; SECTOR_SIZE];
        let mut buf_loc_2 = [0u8; SECTOR_SIZE];

        /// Read a bda sector worth of data at the specified offset into buffer.
        fn read_sector_at_offset<F>(f: &mut F, offset: usize, mut buf: &mut [u8]) -> io::Result<()>
        where
            F: Read + Seek,
        {
            f.seek(SeekFrom::Start(offset as u64))?;
            f.read_exact(&mut buf)?;
            Ok(())
        }

        (
            read_sector_at_offset(f, SECTOR_SIZE, &mut buf_loc_1).map(|_| buf_loc_1),
            read_sector_at_offset(f, 9 * SECTOR_SIZE, &mut buf_loc_2).map(|_| buf_loc_2),
        )
    }

    // Writes bda_buf according to the value of which.
    // If first location is specified, write zeroes to empty regions in the
    // first 8 sectors. If the second location is specified, writes zeroes to empty
    // regions in the second 8 sectors.
    fn write<F>(f: &mut F, bda_buf: &[u8], which: MetadataLocation) -> io::Result<()>
    where
        F: Seek + SyncAll,
    {
        let zeroed = [0u8; _BDA_STATIC_HDR_SIZE];
        f.seek(SeekFrom::Start(0))?;

        // Write to a single region in the header. Zeroes the first sector,
        // writes bda_buf to the second sector, and then zeroes the remaining
        // six sectors.
        fn write_region<F>(f: &mut F, bda_buf: &[u8], zeroed: &[u8]) -> io::Result<()>
        where
            F: Seek + SyncAll,
        {
            f.write_all(&zeroed[..SECTOR_SIZE])?; // Zero 1 unused sector
            f.write_all(bda_buf)?;
            f.write_all(&zeroed[..SECTOR_SIZE * 6])?; // Zero 6 unused sectors
            f.sync_all()?;
            Ok(())
        };

        if which == MetadataLocation::Both || which == MetadataLocation::First {
            write_region(f, bda_buf, &zeroed)?;
        } else {
            f.seek(SeekFrom::Start(8 * SECTOR_SIZE as u64))?;
        }

        if which == MetadataLocation::Both || which == MetadataLocation::Second {
            write_region(f, bda_buf, &zeroed)?;
        }
        Ok(())
    }

    /// Initialize a blockdev with a Stratis BDA.
    pub fn initialize<F>(
        f: &mut F,
        pool_uuid: Uuid,
        dev_uuid: Uuid,
        mda_size: Sectors,
        blkdev_size: Sectors,
        initialization_time: u64,
    ) -> StratisResult<BDA>
    where
        F: Seek + SyncAll,
    {
        let header = StaticHeader::new(
            pool_uuid,
            dev_uuid,
            mda_size,
            blkdev_size,
            initialization_time,
        );

        BDA::write(f, &header.sigblock_to_buf(), MetadataLocation::Both)?;

        let regions = mda::MDARegions::initialize(BDA_STATIC_HDR_SIZE, header.mda_size, f)?;

        Ok(BDA { header, regions })
    }

    /// Load a BDA on initial setup of a device.
    /// Returns None if no BDA appears to exist.
    pub fn load<F>(f: &mut F) -> StratisResult<Option<BDA>>
    where
        F: Read + Seek + SyncAll,
    {
        let header = match StaticHeader::setup(f)? {
            Some(header) => header,
            None => return Ok(None),
        };

        let regions = mda::MDARegions::load(BDA_STATIC_HDR_SIZE, header.mda_size, f)?;

        Ok(Some(BDA { header, regions }))
    }

    /// Zero out Static Header on the blockdev. This causes it to no
    /// longer be seen as a Stratis blockdev.
    pub fn wipe<F>(f: &mut F) -> StratisResult<()>
    where
        F: Seek + SyncAll,
    {
        let zeroed = [0u8; _BDA_STATIC_HDR_SIZE];

        // Wiping Static Header should do it
        f.seek(SeekFrom::Start(0))?;
        f.write_all(&zeroed)?;
        f.sync_all()?;
        Ok(())
    }

    /// Save metadata to the disk
    pub fn save_state<F>(
        &mut self,
        time: &DateTime<Utc>,
        metadata: &[u8],
        f: &mut F,
    ) -> StratisResult<()>
    where
        F: Seek + SyncAll,
    {
        self.regions
            .save_state(BDA_STATIC_HDR_SIZE, time, metadata, f)
    }

    /// Read latest metadata from the disk
    pub fn load_state<F>(&self, mut f: &mut F) -> StratisResult<Option<Vec<u8>>>
    where
        F: Read + Seek,
    {
        self.regions.load_state(BDA_STATIC_HDR_SIZE, &mut f)
    }

    /// The time when the most recent metadata was written to the BDA,
    /// if any.
    pub fn last_update_time(&self) -> Option<&DateTime<Utc>> {
        self.regions.last_update_time()
    }

    /// The UUID of the device.
    pub fn dev_uuid(&self) -> DevUuid {
        self.header.dev_uuid
    }

    /// The UUID of the device's pool.
    pub fn pool_uuid(&self) -> PoolUuid {
        self.header.pool_uuid
    }

    /// The size of the device.
    pub fn dev_size(&self) -> Sectors {
        self.header.blkdev_size
    }

    /// The number of sectors the BDA itself occupies.
    pub fn size(&self) -> Sectors {
        BDA_STATIC_HDR_SIZE.sectors() + self.header.mda_size + self.header.reserved_size
    }

    /// The maximum size of variable length metadata that can be accommodated.
    pub fn max_data_size(&self) -> Bytes {
        self.regions.max_data_size()
    }

    /// Timestamp when the device was initialized.
    pub fn initialization_time(&self) -> u64 {
        self.header.initialization_time
    }
}

#[derive(Eq, PartialEq)]
pub struct StaticHeader {
    blkdev_size: Sectors,
    pool_uuid: PoolUuid,
    dev_uuid: DevUuid,
    mda_size: Sectors,
    reserved_size: Sectors,
    flags: u64,
    /// Seconds portion of DateTime<Utc> value.
    initialization_time: u64,
}

impl StaticHeader {
    fn new(
        pool_uuid: PoolUuid,
        dev_uuid: DevUuid,
        mda_size: Sectors,
        blkdev_size: Sectors,
        initialization_time: u64,
    ) -> StaticHeader {
        StaticHeader {
            blkdev_size,
            pool_uuid,
            dev_uuid,
            mda_size,
            reserved_size: MDA_RESERVED_SECTORS,
            flags: 0,
            initialization_time,
        }
    }

    /// Try to find a valid StaticHeader on a device.
    /// Return the latest copy that validates as a Stratis BDA, however verify both
    /// copies and if one validates but one does not, re-write the one that is incorrect.  If both
    /// copies are valid, but one is newer than the other, rewrite the older one to match.
    /// Return None if it's not a Stratis device.
    /// Return an error if the metadata seems to indicate that the device is
    /// a Stratis device, but no well-formed signature block could be read.
    /// Return an error if neither sigblock location can be read.
    /// Return an error if the sigblocks differ in some unaccountable way.
    /// Returns an error if a write intended to repair an ill-formed,
    /// unreadable, or stale signature block failed.
    fn setup<F>(f: &mut F) -> StratisResult<Option<StaticHeader>>
    where
        F: Read + Seek + SyncAll,
    {
        match BDA::read(f) {
            (Ok(buf_loc_1), Ok(buf_loc_2)) => {
                // We read both copies without an IO error.
                match (
                    StaticHeader::sigblock_from_buf(&buf_loc_1),
                    StaticHeader::sigblock_from_buf(&buf_loc_2),
                ) {
                    (Ok(loc_1), Ok(loc_2)) => {
                        match (loc_1, loc_2) {
                            (Some(loc_1), Some(loc_2)) => {
                                if loc_1 == loc_2 {
                                    Ok(Some(loc_1))
                                } else if loc_1.initialization_time == loc_2.initialization_time {
                                    // Inexplicable disagreement among static headers
                                    let err_str = "Appeared to be a Stratis device, but signature blocks disagree.";
                                    Err(StratisError::Engine(ErrorEnum::Invalid, err_str.into()))
                                } else if loc_1.initialization_time > loc_2.initialization_time {
                                    // If the first header block is newer, overwrite second with
                                    // contents of first.
                                    BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                                    Ok(Some(loc_1))
                                } else {
                                    // The second header block must be newer, so overwrite first
                                    // with contents of second.
                                    BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                                    Ok(Some(loc_2))
                                }
                            }
                            (None, None) => Ok(None),
                            (Some(loc_1), None) => {
                                // Copy 1 has valid Stratis BDA, copy 2 has no magic, re-write copy 2
                                BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                                Ok(Some(loc_1))
                            }
                            (None, Some(loc_2)) => {
                                // Copy 2 has valid Stratis BDA, copy 1 has no magic, re-write copy 1
                                BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                                Ok(Some(loc_2))
                            }
                        }
                    }
                    (Ok(loc_1), Err(loc_2)) => {
                        // Re-write copy 2
                        if loc_1.is_some() {
                            BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                            Ok(loc_1)
                        } else {
                            // Location 1 doesn't have a signature, but location 2 did, but it got an error,
                            // lets return the error instead as this appears to be a stratis device that
                            // has gotten in a bad state.
                            Err(loc_2)
                        }
                    }
                    (Err(loc_1), Ok(loc_2)) => {
                        // Re-write copy 1
                        if loc_2.is_some() {
                            BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                            Ok(loc_2)
                        } else {
                            // Location 2 doesn't have a signature, but location 1 did, but it got an error,
                            // lets return the error instead as this appears to be a stratis device that
                            // has gotten in a bad state.
                            Err(loc_1)
                        }
                    }
                    (Err(_), Err(_)) => {
                        let err_str =
                            "Appeared to be a Stratis device, but no valid sigblock found";
                        Err(StratisError::Engine(ErrorEnum::Invalid, err_str.into()))
                    }
                }
            }
            (Ok(buf_loc_1), Err(_)) => {
                // Copy 1 read OK, 2 resulted in an IO error
                match StaticHeader::sigblock_from_buf(&buf_loc_1) {
                    Ok(loc_1) => {
                        if loc_1.is_some() {
                            BDA::write(f, &buf_loc_1, MetadataLocation::Second)?;
                        }
                        Ok(loc_1)
                    }
                    Err(e) => {
                        // Unable to determine if location 2 has a signature, but location 1 did,
                        // but it got an error, lets return the error instead as this appears to
                        // be a stratis device that has gotten in a bad state.
                        Err(e)
                    }
                }
            }
            (Err(_), Ok(buf_loc_2)) => {
                // Copy 2 read OK, 1 resulted in IO Error
                match StaticHeader::sigblock_from_buf(&buf_loc_2) {
                    Ok(loc_2) => {
                        if loc_2.is_some() {
                            BDA::write(f, &buf_loc_2, MetadataLocation::First)?;
                        }
                        Ok(loc_2)
                    }
                    Err(e) => {
                        // Unable to determine if location 1 has a signature, but location 2 did,
                        // but it got an error, lets return the error instead as this appears to
                        // be a stratis device that has gotten in a bad state.
                        Err(e)
                    }
                }
            }
            (Err(_), Err(_)) => {
                // Unable to read the device at all.
                let err_str = "Unable to read data at sigblock locations.";
                Err(StratisError::Engine(ErrorEnum::Invalid, err_str.into()))
            }
        }
    }

    /// Retrieve the device and pool UUIDs from a stratis device.
    pub fn device_identifiers<F>(f: &mut F) -> StratisResult<Option<((PoolUuid, DevUuid))>>
    where
        F: Read + Seek + SyncAll,
    {
        // Using setup() as a test of ownership sets a high bar. It is
        // not sufficient to have STRAT_MAGIC to be considered "Ours",
        // it must also have correct CRC, no weird stuff in fields,
        // etc!
        match StaticHeader::setup(f) {
            Ok(Some(sh)) => Ok(Some((sh.pool_uuid, sh.dev_uuid))),
            Ok(None) => Ok(None),
            Err(err) => Err(err),
        }
    }

    /// Generate a buf suitable for writing to blockdev
    fn sigblock_to_buf(&self) -> [u8; SECTOR_SIZE] {
        let mut buf = [0u8; SECTOR_SIZE];
        buf[4..20].clone_from_slice(STRAT_MAGIC);
        LittleEndian::write_u64(&mut buf[20..28], *self.blkdev_size);
        buf[28] = STRAT_SIGBLOCK_VERSION;
        buf[32..64].clone_from_slice(self.pool_uuid.to_simple_ref().to_string().as_bytes());
        buf[64..96].clone_from_slice(self.dev_uuid.to_simple_ref().to_string().as_bytes());
        LittleEndian::write_u64(&mut buf[96..104], *self.mda_size);
        LittleEndian::write_u64(&mut buf[104..112], *self.reserved_size);
        LittleEndian::write_u64(&mut buf[120..128], self.initialization_time);

        let hdr_crc = crc32::checksum_castagnoli(&buf[4..SECTOR_SIZE]);
        LittleEndian::write_u32(&mut buf[..4], hdr_crc);
        buf
    }

    /// Build a StaticHeader from a SECTOR_SIZE buf that was read from
    /// a blockdev.
    fn sigblock_from_buf(buf: &[u8]) -> StratisResult<Option<StaticHeader>> {
        assert_eq!(buf.len(), SECTOR_SIZE);

        if &buf[4..20] != STRAT_MAGIC {
            return Ok(None);
        }

        let crc = crc32::checksum_castagnoli(&buf[4..SECTOR_SIZE]);
        if crc != LittleEndian::read_u32(&buf[..4]) {
            return Err(StratisError::Engine(
                ErrorEnum::Invalid,
                "header CRC invalid".into(),
            ));
        }

        let blkdev_size = Sectors(LittleEndian::read_u64(&buf[20..28]));

        let version = buf[28];
        if version != STRAT_SIGBLOCK_VERSION {
            return Err(StratisError::Engine(
                ErrorEnum::Invalid,
                format!("Unknown sigblock version: {}", version),
            ));
        }

        let pool_uuid = Uuid::parse_str(from_utf8(&buf[32..64])?)?;
        let dev_uuid = Uuid::parse_str(from_utf8(&buf[64..96])?)?;

        let mda_size = Sectors(LittleEndian::read_u64(&buf[96..104]));

        mda::validate_mda_size(mda_size)?;

        Ok(Some(StaticHeader {
            pool_uuid,
            dev_uuid,
            blkdev_size,
            mda_size,
            reserved_size: Sectors(LittleEndian::read_u64(&buf[104..112])),
            flags: 0,
            initialization_time: LittleEndian::read_u64(&buf[120..128]),
        }))
    }
}

impl fmt::Debug for StaticHeader {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("StaticHeader")
            .field("blkdev_size", &self.blkdev_size)
            .field("pool_uuid", &self.pool_uuid.to_simple_ref())
            .field("dev_uuid", &self.dev_uuid.to_simple_ref())
            .field("mda_size", &self.mda_size)
            .field("reserved_size", &self.reserved_size)
            .field("flags", &self.flags)
            .field("initialization_time", &self.initialization_time)
            .finish()
    }
}

mod mda {
    use std::{
        self,
        cmp::Ordering,
        io::{Read, Seek, SeekFrom},
    };

    use byteorder::{ByteOrder, LittleEndian};
    use chrono::{DateTime, TimeZone, Utc};
    use crc::crc32;

    use devicemapper::{Bytes, Sectors};

    use crate::{
        engine::strat_engine::device::SyncAll,
        stratis::{ErrorEnum, StratisError, StratisResult},
    };

    const _MDA_REGION_HDR_SIZE: usize = 32;
    const MDA_REGION_HDR_SIZE: Bytes = Bytes(_MDA_REGION_HDR_SIZE as u64);

    const NUM_PRIMARY_MDA_REGIONS: usize = 2;

    // There are two copies of every primary MDA region, so the total number
    // of MDA regions is twice the number of primary MDA regions.
    const NUM_MDA_REGIONS: usize = 2 * NUM_PRIMARY_MDA_REGIONS;

    pub const MIN_MDA_SECTORS: Sectors = Sectors(2032);

    const STRAT_REGION_HDR_VERSION: u8 = 1;
    const STRAT_METADATA_VERSION: u8 = 1;

    /// Manages the MDA regions which hold the variable length metadata.
    #[derive(Debug)]
    pub struct MDARegions {
        /// The size of a single MDA region. The MDAHeader occupies the
        /// first few bytes of its region, the rest is available for the
        /// variable length metadata.
        region_size: Sectors,
        /// The MDA headers which contain information about the variable
        /// length metadata. NUM_PRIMARY_MDA_REGIONS is 2: in the general
        /// case one is more recently written than the other.
        mdas: [Option<MDAHeader>; NUM_PRIMARY_MDA_REGIONS],
    }

    impl MDARegions {
        /// Calculate the offset from start of device for an MDARegion.
        fn mda_offset(header_size: Bytes, index: usize, per_region_size: Bytes) -> u64 {
            *(header_size + per_region_size * index)
        }

        /// The maximum size of variable length metadata that this region
        /// can accommodate.
        pub fn max_data_size(&self) -> Bytes {
            self.region_size.bytes() - MDA_REGION_HDR_SIZE
        }

        /// Initialize the space allotted to the MDA regions to 0.
        /// Return an MDARegions object with uninitialized MDAHeader objects.
        pub fn initialize<F>(
            header_size: Bytes,
            size: Sectors,
            f: &mut F,
        ) -> StratisResult<MDARegions>
        where
            F: Seek + SyncAll,
        {
            let hdr_buf = MDAHeader::default().to_buf();

            let region_size = size / NUM_MDA_REGIONS;
            let per_region_size = region_size.bytes();
            for region in 0..NUM_MDA_REGIONS {
                f.seek(SeekFrom::Start(MDARegions::mda_offset(
                    header_size,
                    region,
                    per_region_size,
                )))?;
                f.write_all(&hdr_buf)?;
            }

            f.sync_all()?;

            Ok(MDARegions {
                region_size,
                mdas: [None, None],
            })
        }

        /// Construct MDARegions from data on the disk.
        /// Note that this method is always called in a context where a
        /// StaticHeader has already been read. Therefore, it
        /// constitutes an error if it is not possible to discover two
        /// well-formed MDAHeaders for this device.
        pub fn load<F>(header_size: Bytes, size: Sectors, f: &mut F) -> StratisResult<MDARegions>
        where
            F: Read + Seek,
        {
            let region_size = size / NUM_MDA_REGIONS;
            let per_region_size = region_size.bytes();

            // Load a single region at the location specified by index.
            // If it appears that no metadata has been written at the location
            // return None. If it appears that there is metadata, but it has
            // been corrupted, return an error.
            let mut load_a_region = |index: usize| -> StratisResult<Option<MDAHeader>> {
                let mut hdr_buf = [0u8; _MDA_REGION_HDR_SIZE];
                f.seek(SeekFrom::Start(MDARegions::mda_offset(
                    header_size,
                    index,
                    per_region_size,
                )))?;
                f.read_exact(&mut hdr_buf)?;
                Ok(MDAHeader::from_buf(&hdr_buf)?)
            };

            // Get an MDAHeader for the given index.
            // If there is a failure reading the first, fall back on the
            // second. If there is a failure reading both, return an error.
            let mut get_mda = |index: usize| -> StratisResult<Option<MDAHeader>> {
                load_a_region(index).or_else(|_| load_a_region(index + NUM_PRIMARY_MDA_REGIONS))
            };

            Ok(MDARegions {
                region_size,
                mdas: [get_mda(0)?, get_mda(1)?],
            })
        }

        /// Write metadata to the older of the metadata regions.
        /// If operation is completed, update the value of the
        /// older MDAHeader with the new values.
        /// If time specified is earlier than the last update time, return an
        /// error. If the size of the data is greater than the available space,
        /// return an error. If there is an error when writing the data, return
        /// an error.
        pub fn save_state<F>(
            &mut self,
            header_size: Bytes,
            time: &DateTime<Utc>,
            data: &[u8],
            f: &mut F,
        ) -> StratisResult<()>
        where
            F: Seek + SyncAll,
        {
            if self.last_update_time() >= Some(time) {
                return Err(StratisError::Engine(
                    ErrorEnum::Invalid,
                    "Overwriting newer data".into(),
                ));
            }

            let used = Bytes(data.len() as u64);
            let max_available = self.max_data_size();
            if used > max_available {
                let err_msg = format!(
                    "metadata length {} exceeds region available {}",
                    used, max_available
                );
                return Err(StratisError::Engine(ErrorEnum::Invalid, err_msg));
            };

            let header = MDAHeader {
                last_updated: *time,
                used,
                data_crc: crc32::checksum_castagnoli(data),
            };
            let hdr_buf = header.to_buf();

            // Write data to a region specified by index.
            let region_size = self.region_size.bytes();
            let mut save_region = |index: usize| -> StratisResult<()> {
                f.seek(SeekFrom::Start(MDARegions::mda_offset(
                    header_size,
                    index,
                    region_size,
                )))?;
                f.write_all(&hdr_buf)?;
                f.write_all(data)?;
                f.sync_all()?;

                Ok(())
            };

            // TODO: Consider if there is an action that should be taken if
            // saving to one or the other region fails.
            let older_region = self.older();
            save_region(older_region)?;
            save_region(older_region + NUM_PRIMARY_MDA_REGIONS)?;

            self.mdas[older_region] = Some(header);

            Ok(())
        }

        /// Load metadata from the newer MDA region.
        /// In case there is no record of metadata in regions, return None.
        /// If there is a record of metadata, and there is a failure to read
{'fraction_non_alphanumeric': 0.10375858190140026, 'fraction_numerical': 0.010783134866762344, 'mean_word_length': 1.6051129187086344, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 206, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 42, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6531767', 'n_tokens_mistral': 15522, 'n_tokens_neox': 14822, 'n_words': 4205}