orphanage 0.5.6

Random collection of stuff that is still searching for a home.
Documentation
use std::{
  fs,
  io::{ErrorKind, Write},
  path::{Path, PathBuf}
};

#[cfg(windows)]
use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExA;

use crate::{err::Error, ffi::path_to_cstring};

/// Create a random file of a specified size.
///
/// # Errors
/// [`std::io::Error`]
pub fn rndfile(
  fname: impl AsRef<Path>,
  size: u64
) -> Result<u64, std::io::Error> {
  let mut i = super::iox::RngReader::with_lim(size);
  let mut o = fs::File::create(fname)?;
  std::io::copy(&mut i, &mut o)
}

/// Expand a string directory, make it absolute and create it (if it does not
/// exist).
#[allow(clippy::missing_errors_doc)]
pub fn gen_absdir(input: impl AsRef<str>) -> Result<PathBuf, Error> {
  let pth = super::path::expabs(input)?;

  if !pth.exists() {
    std::fs::create_dir_all(&pth)?;
  }

  if !pth.is_dir() {
    // std::io::ErrorKind::NotADirectory is unstable
    return Err(std::io::Error::other("Not a directory").into());
  }

  Ok(pth)
}

/// Given an input path, attempt to remove its containing (parent) directory.
///
/// If successful, return a `PathBuf` of the parent directory.
#[allow(clippy::missing_errors_doc)]
pub fn rm_containing(
  pth: impl AsRef<Path>
) -> Result<PathBuf, std::io::Error> {
  if let Some(parent) = pth.as_ref().parent() {
    std::fs::remove_dir(parent)?;
    Ok(parent.to_path_buf())
  } else {
    Err(std::io::Error::other("Parent not found"))
  }
}

/// Return the absolute path to an existing filesystem object.
#[allow(clippy::missing_errors_doc)]
pub fn abspath<P>(pth: P) -> Result<PathBuf, Error>
where
  P: AsRef<Path>
{
  Ok(pth.as_ref().canonicalize()?)
}

/// Call a closure if `outfile` is outdated (according to the rules of
/// [`outdated()`] with regards to `infile`.
///
/// Returns `Ok(true)` if the closure was called.
///
/// # Errors
/// Errors mirror the behavior of [`outdated()`]'s errors.
pub fn run_if_outdated<F>(
  srcfile: impl AsRef<Path>,
  dstfile: impl AsRef<Path>,
  f: F
) -> Result<bool, std::io::Error>
where
  F: FnOnce(&Path, &Path)
{
  if outdated(&srcfile, &dstfile)? {
    f(srcfile.as_ref(), dstfile.as_ref());
    Ok(true)
  } else {
    Ok(false)
  }
}

/// Returns `true` if `infile` is newer than `outfile` (or if `outfile` doesn't
/// exist).  Returns `false` otherwise.
///
/// # Errors
/// Returns [`std::io::Error`] if:
/// - `infile`'s metadata could not be read.
/// - `outfile`'s metadata could not be read and it is other than
///   `ErrorKind::NotFound`.
/// - Either `infile`' or `outfile`'s mtime could not be extracted.
pub fn outdated(
  infile: impl AsRef<Path>,
  outfile: impl AsRef<Path>
) -> Result<bool, std::io::Error> {
  let in_md = fs::metadata(infile.as_ref())?;
  let out_md = match fs::metadata(outfile.as_ref()) {
    Ok(md) => md,
    Err(err) if err.kind() == ErrorKind::NotFound => {
      // If the target file could not be found then treat target as outdated
      return Ok(true);
    }
    Err(e) => Err(e)?
  };

  let in_mtime = in_md.modified()?;
  let out_mtime = out_md.modified()?;

  Ok(in_mtime > out_mtime)
}


/// Given a directory, return how much disk space is available.
///
/// The directory must exist.
///
/// # Errors
/// [`Error::IO`] is returned if the input path is not a directory or if the
/// free space could not be probed.
pub fn get_free_space(path: &Path) -> Result<u64, Error> {
  #[cfg(unix)]
  let res = get_free_space_unix(path);

  #[cfg(windows)]
  let res = get_free_space_win(path);

  res
}


#[cfg(unix)]
fn get_free_space_unix(path: &Path) -> Result<u64, Error> {
  // Make sure directory exists
  if !path.is_dir() {
    return Err(Error::IO("Not a directory".into()));
  }

  // Must construct a null-terminated C string for libc::statfs()
  let cstr = path_to_cstring(path);
  let cstr_path = cstr.as_bytes_with_nul();

  let mut statfs = unsafe { std::mem::zeroed() };
  let result =
    unsafe { libc::statfs(cstr_path.as_ptr().cast::<i8>(), &raw mut statfs) };

  if result == 0 {
    #[cfg(target_os = "macos")]
    let free = statfs.f_bavail * u64::from(statfs.f_bsize);

    #[cfg(target_os = "linux")]
    #[allow(clippy::cast_sign_loss)]
    let free = statfs.f_bavail * statfs.f_bsize as u64;

    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
    let free = statfs.f_bavail as u64 * statfs.f_bsize;

    Ok(free)
  } else {
    Err(Error::IO("statfs() failed".into()))
  }
}

#[cfg(windows)]
fn get_free_space_win(path: &Path) -> Result<u64, Error> {
  // Make sure directory exists
  if !path.is_dir() {
    return Err(Error::IO("Not a directory".into()));
  }

  // Need a null-terminated string for ffi
  let cstr = path_to_cstring(path);
  let cstr_path = cstr.as_bytes_with_nul();

  let mut free_bytes_available_to_caller: u64 = 0;
  let mut total_bytes: u64 = 0;
  let mut total_free_bytes: u64 = 0;

  let result = unsafe {
    GetDiskFreeSpaceExA(
      cstr_path.as_ptr(),
      &raw mut free_bytes_available_to_caller,
      &raw mut total_bytes,
      &raw mut total_free_bytes
    )
  };

  if result != 0 {
    Ok(free_bytes_available_to_caller)
  } else {
    Err(Error::IO(format!(
      "GetDiskFreeSpaceExW failed with code: {}",
      std::io::Error::last_os_error()
    )))
  }
}

/// Run a closure if a file system object does not exist.
pub fn if_not_exists<P, F>(pth: P, f: F)
where
  P: AsRef<Path>,
  F: FnOnce(&Path)
{
  let pth = pth.as_ref();
  if !pth.exists() {
    f(pth);
  }
}


pub enum TestFileContents {
  /// Random file contents
  Random,

  /// Increasing `u8` values.
  ///
  /// Starts at `0` and wraps around after `255`.
  U8Count,

  /// Increasing big-endian `u16` values.
  ///
  /// Starts at `0` and wraps around after maximum value.
  U16Count,

  /// Increasing big-endian `u32` values.
  ///
  /// Starts at `0` and wraps around after maximum value.
  U32Count,

  /// Increasing big-endian `u64` values.
  ///
  /// Starts at `0` and wraps around after maximum value.
  U64Count,

  /// Write fixed-size blocks that lead with increasing big-endian `u64`
  /// values.
  CountedBlock { block_size: usize }
}

/// Generate a variety of types of test files.
///
/// # Errors
/// # Panics
pub fn make_test_file<P>(
  fname: P,
  tfc: &TestFileContents,
  size: u64
) -> Result<(), std::io::Error>
where
  P: AsRef<Path>
{
  match tfc {
    TestFileContents::Random => {
      rndfile(fname, size)?;
    }
    TestFileContents::U8Count => {
      let mut o = fs::File::create(fname)?;

      // Prepare buffer of increasing bytes
      let mut buf: Vec<u8> = Vec::with_capacity(256);
      for i in 0..256 {
        buf.push(u8::try_from(i).unwrap());
      }

      let mut size = size;
      while size > 0 {
        let n = std::cmp::min(size, 256);
        let n = n.try_into().unwrap();
        o.write_all(&buf[..n])?;
        size -= n as u64;
      }
    }
    TestFileContents::U16Count => {
      let mut o = fs::File::create(fname)?;

      let mut c: u16 = 0;

      let mut size = size;
      while size > 0 {
        let buf = c.to_be_bytes();
        let n = std::cmp::min(size, buf.len() as u64);
        let n: usize = n.try_into().unwrap();
        o.write_all(&buf[..n])?;
        c = c.wrapping_add(1);
        size -= n as u64;
      }
    }
    TestFileContents::U32Count => {
      let mut o = fs::File::create(fname)?;

      let mut c: u32 = 0;

      let mut size = size;
      while size > 0 {
        let buf = c.to_be_bytes();
        let n = std::cmp::min(size, buf.len() as u64);
        let n: usize = n.try_into().unwrap();
        o.write_all(&buf[..n])?;
        c = c.wrapping_add(1);
        size -= n as u64;
      }
    }
    TestFileContents::U64Count => {
      let mut o = fs::File::create(fname)?;

      let mut c: u64 = 0;

      let mut size = size;
      while size > 0 {
        let buf = c.to_be_bytes();
        let n = std::cmp::min(size, buf.len() as u64);
        let n: usize = n.try_into().unwrap();
        o.write_all(&buf[..n])?;
        c = c.wrapping_add(1);
        size -= n as u64;
      }
    }
    TestFileContents::CountedBlock { block_size } => {
      assert!(*block_size >= std::mem::size_of::<u64>());

      let mut o = fs::File::create(fname)?;

      let mut c: u64 = 0;

      let mut buf = vec![0u8; *block_size];

      let mut size = size;
      while size > 0 {
        let cbuf = c.to_be_bytes();

        // Add counter to beginning of block
        buf[..8].copy_from_slice(&cbuf);

        let n = std::cmp::min(size, buf.len() as u64);
        let n: usize = n.try_into().unwrap();
        o.write_all(&buf[..n])?;
        c = c.wrapping_add(1);
        size -= n as u64;
      }
    }
  }

  Ok(())
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :