rtimelog 1.1.1

System for tracking time in a text-log-based format.
Documentation
//! Interface for manipulating the end of a file.

use std::fs::{self, File};
use std::io::{BufRead, Seek};
use std::path::PathBuf;

#[doc(inline)]
use crate::error::PathError;

/// Create a new file for appending from the supplied filename.
///
/// # Errors
///
/// - Return [`PathError::FileAccess`] if unable to append to the file
pub fn append_open(filename: &str) -> Result<File, PathError> {
    fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(filename)
        .map_err(|e| PathError::FileAccess(filename.to_string(), e.to_string()))
}

/// Open the supplied file for reading and writing, return a [`File`].
///
/// # Errors
///
/// - Return [`PathError::FileAccess`] if unable to open the file.
pub fn rw_open(filename: &str) -> Result<File, PathError> {
    fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open(filename)
        .map_err(|e| PathError::FileAccess(filename.to_string(), e.to_string()))
}

// Which kind of file
pub(crate) enum FileKind {
    LogFile,
    StackFile
}

impl From<FileKind> for PathError {
    // Generate a invalid path error from the kind of file.
    fn from(kind: FileKind) -> PathError {
        match kind {
            FileKind::LogFile => PathError::InvalidTimelogPath,
            FileKind::StackFile => PathError::InvalidStackPath
        }
    }
}

// Test and canonicalize the supplied filename.
//
// # Errors
//
// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
// - Return [`PathError::InvalidStackPath`] if stack path is invalid.
pub(crate) fn canonical_filename(file: &str, kind: FileKind) -> Result<String, PathError> {
    if file.is_empty() {
        return Err(PathError::FilenameMissing);
    }
    let mut dir = PathBuf::from(file);
    let filename = dir
        .file_name()
        .ok_or(PathError::FilenameMissing)?
        .to_os_string();
    dir.pop();

    let mut candir = fs::canonicalize(dir)
        .map_err(|e| PathError::InvalidPath(file.to_string(), e.to_string()))?;
    candir.push(filename);

    candir.into_os_string().into_string().map_err(|_| kind.into())
}

/// Remove the most recent task from the stack file and return the task string.
pub fn pop_last_line(file: &mut File) -> Option<String> {
    let (line, pos) = find_last_line(file)?;

    file.set_len(pos).ok()?;
    Some(line)
}

/// Find the last line in the supplied file.
/// Return an optional tuple of the line and the offset to that line.
pub fn find_last_line(file: &mut File) -> Option<(String, u64)> {
    let mut last_pos = 0u64;
    let mut last_line = String::new();

    if file.metadata().ok()?.len() == 0u64 {
        return None;
    }

    let mut reader = std::io::BufReader::new(file);
    while let Some((line, pos)) = last_line_and_pos(&mut reader) {
        last_pos = pos;
        last_line = line.trim_end().to_string();
    }

    Some((last_line, last_pos))
}

// Find the last line and the position of the beginning of that line in the stack file.
fn last_line_and_pos<R>(reader: &mut R) -> Option<(String, u64)>
where
    R: BufRead + Seek
{
    let pos = reader.stream_position().ok()?;
    let mut line = String::new();
    (0 != reader.read_line(&mut line).ok()?).then_some((line, pos))
}