// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Copyright 2019 Intel Corporation. All Rights Reserved.
//
// Copyright 2018 The Chromium OS Authors. All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause

//! Traits for replacing a range with a hole and writing zeroes in a file.

use std::cmp::min;
use std::fs::File;
use std::io::{Error, ErrorKind, Result, Seek, SeekFrom};
use std::os::unix::fs::FileExt;

use crate::fallocate::{fallocate, FallocateMode};

/// A trait for deallocating space in a file.
pub trait PunchHole {
    /// Replace a range of bytes with a hole.
    ///
    /// # Arguments
    ///
    /// * `offset`: offset of the file where to replace with a hole.
    /// * `length`: the number of bytes of the hole to replace with.
    fn punch_hole(&mut self, offset: u64, length: u64) -> Result<()>;
}

impl PunchHole for File {
    fn punch_hole(&mut self, offset: u64, length: u64) -> Result<()> {
        fallocate(self, FallocateMode::PunchHole, true, offset, length as u64)
            .map_err(|e| Error::from_raw_os_error(e.errno()))
    }
}

/// A trait for writing zeroes to a stream.
pub trait WriteZeroes {
    /// Write up to `length` bytes of zeroes to the stream, returning how many bytes were written.
    ///
    /// # Arguments
    ///
    /// * `length`: the number of bytes of zeroes to write to the stream.
    fn write_zeroes(&mut self, length: usize) -> Result<usize>;

    /// Write zeroes to the stream until `length` bytes have been written.
    ///
    /// This method will continuously write zeroes until the requested `length` is satisfied or an
    /// unrecoverable error is encountered.
    ///
    /// # Arguments
    ///
    /// * `length`: the exact number of bytes of zeroes to write to the stream.
    fn write_all_zeroes(&mut self, mut length: usize) -> Result<()> {
        while length > 0 {
            match self.write_zeroes(length) {
                Ok(0) => return Err(Error::from(ErrorKind::WriteZero)),
                Ok(bytes_written) => {
                    length = length
                        .checked_sub(bytes_written)
                        .ok_or_else(|| Error::from(ErrorKind::Other))?
                }
                // If the operation was interrupted, we should retry it.
                Err(e) => {
                    if e.kind() != ErrorKind::Interrupted {
                        return Err(e);
                    }
                }
            }
        }
        Ok(())
    }
}

/// A trait for writing zeroes to an arbitrary position in a file.
pub trait WriteZeroesAt {
    /// Write up to `length` bytes of zeroes starting at `offset`, returning how many bytes were
    /// written.
    ///
    /// # Arguments
    ///
    /// * `offset`: offset of the file where to write zeroes.
    /// * `length`: the number of bytes of zeroes to write to the stream.
    fn write_zeroes_at(&mut self, offset: u64, length: usize) -> Result<usize>;

    /// Write zeroes starting at `offset` until `length` bytes have been written.
    ///
    /// This method will continuously write zeroes until the requested `length` is satisfied or an
    /// unrecoverable error is encountered.
    ///
    /// # Arguments
    ///
    /// * `offset`: offset of the file where to write zeroes.
    /// * `length`: the exact number of bytes of zeroes to write to the stream.
    fn write_all_zeroes_at(&mut self, mut offset: u64, mut length: usize) -> Result<()> {
        while length > 0 {
            match self.write_zeroes_at(offset, length) {
                Ok(0) => return Err(Error::from(ErrorKind::WriteZero)),
                Ok(bytes_written) => {
                    length = length
                        .checked_sub(bytes_written)
                        .ok_or_else(|| Error::from(ErrorKind::Other))?;
                    offset = offset
                        .checked_add(bytes_written as u64)
                        .ok_or_else(|| Error::from(ErrorKind::Other))?;
                }
                Err(e) => {
                    // If the operation was interrupted, we should retry it.
                    if e.kind() != ErrorKind::Interrupted {
                        return Err(e);
                    }
                }
            }
        }
        Ok(())
    }
}

impl WriteZeroesAt for File {
    fn write_zeroes_at(&mut self, offset: u64, length: usize) -> Result<usize> {
        // Try to use fallocate() first, since it is more efficient than writing zeroes with
        // write().
        if fallocate(self, FallocateMode::ZeroRange, true, offset, length as u64).is_ok() {
            return Ok(length);
        }

        // Fall back to write().
        // fallocate() failed; fall back to writing a buffer of zeroes until we have written up
        // to `length`.
        let buf_size = min(length, 0x10000);
        let buf = vec![0u8; buf_size];
        let mut num_written: usize = 0;
        while num_written < length {
            let remaining = length - num_written;
            let write_size = min(remaining, buf_size);
            num_written += self.write_at(&buf[0..write_size], offset + num_written as u64)?;
        }
        Ok(length)
    }
}

impl<T: WriteZeroesAt + Seek> WriteZeroes for T {
    fn write_zeroes(&mut self, length: usize) -> Result<usize> {
        let offset = self.seek(SeekFrom::Current(0))?;
        let num_written = self.write_zeroes_at(offset, length)?;
        // Advance the seek cursor as if we had done a real write().
        self.seek(SeekFrom::Current(num_written as i64))?;
        Ok(length)
    }
}

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

    use std::io::{Read, Seek, SeekFrom, Write};

    use crate::tempfile::TempFile;

    #[test]
    fn test_small_write_zeroes() {
        const NON_ZERO_VALUE: u8 = 0x55;
        const BUF_SIZE: usize = 5678;

        let mut f = TempFile::new().unwrap().into_file();
        f.set_len(16384).unwrap();

        // Write buffer of non-zero bytes to offset 1234.
        let orig_data = [NON_ZERO_VALUE; BUF_SIZE];
        f.seek(SeekFrom::Start(1234)).unwrap();
        f.write_all(&orig_data).unwrap();

        // Read back the data plus some overlap on each side.
        let mut readback = [0u8; 16384];
        f.seek(SeekFrom::Start(0)).unwrap();
        f.read_exact(&mut readback).unwrap();
        // Bytes before the write should still be 0.
        for read in &readback[0..1234] {
            assert_eq!(*read, 0);
        }
        // Bytes that were just written should have `NON_ZERO_VALUE` value.
        for read in &readback[1234..(1234 + BUF_SIZE)] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }
        // Bytes after the written area should still be 0.
        for read in &readback[(1234 + BUF_SIZE)..] {
            assert_eq!(*read, 0);
        }

        // Overwrite some of the data with zeroes.
        f.seek(SeekFrom::Start(2345)).unwrap();
        f.write_all_zeroes(4321).unwrap();
        // Verify seek position after `write_all_zeroes()`.
        assert_eq!(f.seek(SeekFrom::Current(0)).unwrap(), 2345 + 4321);

        // Read back the data and verify that it is now zero.
        f.seek(SeekFrom::Start(0)).unwrap();
        f.read_exact(&mut readback).unwrap();
        // Bytes before the write should still be 0.
        for read in &readback[0..1234] {
            assert_eq!(*read, 0);
        }
        // Original data should still exist before the zeroed region.
        for read in &readback[1234..2345] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }
        // Verify that `write_all_zeroes()` zeroed the intended region.
        for read in &readback[2345..(2345 + 4321)] {
            assert_eq!(*read, 0);
        }
        // Original data should still exist after the zeroed region.
        for read in &readback[(2345 + 4321)..(1234 + BUF_SIZE)] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }
        // The rest of the file should still be 0.
        for read in &readback[(1234 + BUF_SIZE)..] {
            assert_eq!(*read, 0);
        }
    }

    #[test]
    fn test_large_write_zeroes() {
        const NON_ZERO_VALUE: u8 = 0x55;
        const SIZE: usize = 0x2_0000;

        let mut f = TempFile::new().unwrap().into_file();
        f.set_len(16384).unwrap();

        // Write buffer of non-zero bytes. The size of the buffer will be the new
        // size of the file.
        let orig_data = [NON_ZERO_VALUE; SIZE];
        f.seek(SeekFrom::Start(0)).unwrap();
        f.write_all(&orig_data).unwrap();
        assert_eq!(f.metadata().unwrap().len(), SIZE as u64);

        // Overwrite some of the data with zeroes.
        f.seek(SeekFrom::Start(0)).unwrap();
        f.write_all_zeroes(0x1_0001).unwrap();
        // Verify seek position after `write_all_zeroes()`.
        assert_eq!(f.seek(SeekFrom::Current(0)).unwrap(), 0x1_0001);

        // Read back the data and verify that it is now zero.
        let mut readback = [0u8; SIZE];
        f.seek(SeekFrom::Start(0)).unwrap();
        f.read_exact(&mut readback).unwrap();
        // Verify that `write_all_zeroes()` zeroed the intended region.
        for read in &readback[0..0x1_0001] {
            assert_eq!(*read, 0);
        }
        // Original data should still exist after the zeroed region.
        for read in &readback[0x1_0001..SIZE] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }

        // Now let's zero a certain region by using `write_all_zeroes_at()`.
        f.write_all_zeroes_at(0x1_8001, 0x200).unwrap();
        f.seek(SeekFrom::Start(0)).unwrap();
        f.read_exact(&mut readback).unwrap();

        // Original data should still exist before the zeroed region.
        for read in &readback[0x1_0001..0x1_8001] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }
        // Verify that `write_all_zeroes_at()` zeroed the intended region.
        for read in &readback[0x1_8001..(0x1_8001 + 0x200)] {
            assert_eq!(*read, 0);
        }
        // Original data should still exist after the zeroed region.
        for read in &readback[(0x1_8001 + 0x200)..SIZE] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }
    }

    #[test]
    fn test_punch_hole() {
        const NON_ZERO_VALUE: u8 = 0x55;
        const SIZE: usize = 0x2_0000;

        let mut f = TempFile::new().unwrap().into_file();
        f.set_len(16384).unwrap();

        // Write buffer of non-zero bytes. The size of the buffer will be the new
        // size of the file.
        let orig_data = [NON_ZERO_VALUE; SIZE];
        f.seek(SeekFrom::Start(0)).unwrap();
        f.write_all(&orig_data).unwrap();
        assert_eq!(f.metadata().unwrap().len(), SIZE as u64);

        // Punch a hole at offset 0x10001.
        // Subsequent reads from this range will return zeros.
        f.punch_hole(0x1_0001, 0x200).unwrap();

        // Read back the data.
        let mut readback = [0u8; SIZE];
        f.seek(SeekFrom::Start(0)).unwrap();
        f.read_exact(&mut readback).unwrap();
        // Original data should still exist before the hole.
        for read in &readback[0..0x1_0001] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }
        // Verify that `punch_hole()` zeroed the intended region.
        for read in &readback[0x1_0001..(0x1_0001 + 0x200)] {
            assert_eq!(*read, 0);
        }
        // Original data should still exist after the hole.
        for read in &readback[(0x1_0001 + 0x200)..] {
            assert_eq!(*read, NON_ZERO_VALUE);
        }

        // Punch a hole at the end of the file.
        // Subsequent reads from this range should return zeros.
        f.punch_hole(SIZE as u64 - 0x400, 0x400).unwrap();
        // Even though we punched a hole at the end of the file, the file size should remain the
        // same since FALLOC_FL_PUNCH_HOLE must be used with FALLOC_FL_KEEP_SIZE.
        assert_eq!(f.metadata().unwrap().len(), SIZE as u64);

        let mut readback = [0u8; 0x400];
        f.seek(SeekFrom::Start(SIZE as u64 - 0x400)).unwrap();
        f.read_exact(&mut readback).unwrap();
        // Verify that `punch_hole()` zeroed the intended region.
        for read in &readback[0..0x400] {
            assert_eq!(*read, 0);
        }

        // Punching a hole of len 0 should return an error.
        assert!(f.punch_hole(0x200, 0x0).is_err());
        // Zeroing a region of len 0 should not return an error since we have a fallback path
        // in `write_zeroes_at()` for `fallocate()` failure.
        assert!(f.write_zeroes_at(0x200, 0x0).is_ok());
    }
}
