// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore lseek

use rustix::fs::{OFlags, SeekFrom, fcntl_getfl};
use std::os::fd::AsFd;
use uucore::fs::FileInformation;

/// An unsafe overwrite occurs when the same nonempty file is used as both stdin and stdout,
/// and the file offset of stdin is positioned earlier than that of stdout.
/// In this scenario, bytes read from stdin are written to a later part of the file
/// via stdout, which can then be read again by stdin and written again by stdout,
/// causing an infinite loop and potential file corruption.
pub fn is_unsafe_overwrite<I: AsFd, O: AsFd>(input: &I, output: &O) -> bool {
    // `FileInformation::from_file` returns an error if the file descriptor is closed, invalid,
    // or refers to a non-regular file (e.g., socket, pipe, or special device).
    let Ok(input_info) = FileInformation::from_file(input) else {
        return false;
    };
    let Ok(output_info) = FileInformation::from_file(output) else {
        return false;
    };
    if input_info != output_info {
        return false;
    }
    let file_size = output_info.file_size();
    if file_size == 0 {
        return false;
    }
    // `seek` returns an error if the file descriptor is closed or it refers to
    // a non-seekable resource (e.g., pipe, socket, or some devices).
    let input_pos = rustix::fs::seek(input, SeekFrom::Current(0)).map(|v| v as i64);
    let output_pos = rustix::fs::seek(output, SeekFrom::Current(0)).map(|v| v as i64);
    if is_appending(output) {
        return !input_pos.is_ok_and(|pos| pos >= 0 && (pos as u64) >= file_size);
    }
    let Ok(input_pos) = input_pos else {
        return false;
    };
    let Ok(output_pos) = output_pos else {
        return false;
    };
    input_pos < output_pos
}

/// Whether the file is opened with the `O_APPEND` flag
fn is_appending<F: AsFd>(file: &F) -> bool {
    let flags = fcntl_getfl(file).unwrap_or(OFlags::empty());
    flags.contains(OFlags::APPEND)
}

#[cfg(test)]
mod tests {
    use crate::platform::unix::{is_appending, is_unsafe_overwrite};
    use std::fs::OpenOptions;
    use std::io::{Seek, SeekFrom, Write};
    use tempfile::NamedTempFile;

    #[test]
    fn test_is_appending() {
        let temp_file = NamedTempFile::new().unwrap();
        assert!(!is_appending(&temp_file));

        let read_file = OpenOptions::new().read(true).open(&temp_file).unwrap();
        assert!(!is_appending(&read_file));

        let write_file = OpenOptions::new().write(true).open(&temp_file).unwrap();
        assert!(!is_appending(&write_file));

        let append_file = OpenOptions::new().append(true).open(&temp_file).unwrap();
        assert!(is_appending(&append_file));
    }

    #[test]
    fn test_is_unsafe_overwrite() {
        // Create two temp files one of which is empty
        let empty = NamedTempFile::new().unwrap();
        let mut nonempty = NamedTempFile::new().unwrap();
        nonempty.write_all(b"anything").unwrap();
        nonempty.seek(SeekFrom::Start(0)).unwrap();

        // Using a different file as input and output does not result in an overwrite
        assert!(!is_unsafe_overwrite(&empty, &nonempty));

        // Overwriting an empty file is always safe
        assert!(!is_unsafe_overwrite(&empty, &empty));

        // Overwriting a nonempty file with itself is safe
        assert!(!is_unsafe_overwrite(&nonempty, &nonempty));

        // Overwriting an empty file opened in append mode is safe
        let empty_append = OpenOptions::new().append(true).open(&empty).unwrap();
        assert!(!is_unsafe_overwrite(&empty, &empty_append));

        // Overwriting a nonempty file opened in append mode is unsafe
        let nonempty_append = OpenOptions::new().append(true).open(&nonempty).unwrap();
        assert!(is_unsafe_overwrite(&nonempty, &nonempty_append));

        // Overwriting a file opened in write mode is safe
        let mut nonempty_write = OpenOptions::new().write(true).open(&nonempty).unwrap();
        assert!(!is_unsafe_overwrite(&nonempty, &nonempty_write));

        // Overwriting a file when the input and output file descriptors are pointing to
        // different offsets is safe if the input offset is further than the output offset
        nonempty_write.seek(SeekFrom::Start(1)).unwrap();
        assert!(!is_unsafe_overwrite(&nonempty_write, &nonempty));
        assert!(is_unsafe_overwrite(&nonempty, &nonempty_write));
    }
}
