use std::ffi::CString;
use std::fs;
use std::io;
use std::os::unix::prelude::*;
use std::path::Path;
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
};

use obnth::{open_beneath, LookupFlags};

fn same_meta(m1: &fs::Metadata, m2: &fs::Metadata) -> bool {
    m1.ino() == m2.ino() && m1.dev() == m2.dev()
}

fn check_beneath(base_file: &fs::File, dir_fd_meta: &fs::Metadata) -> io::Result<()> {
    // We need to rewind up the directory tree and make sure that we didn't escape because of
    // race conditions with "..".

    let mut prev_meta = None;

    let mut cur_file: Option<fs::File> = None;

    loop {
        let cur_file_ref = cur_file.as_ref().unwrap_or(base_file);

        let cur_meta = cur_file_ref.metadata()?;

        if same_meta(&cur_meta, dir_fd_meta) {
            // We found it! We *didn't* escape.
            return Ok(());
        } else if let Some(prev_meta) = prev_meta.as_ref() {
            if same_meta(&cur_meta, &prev_meta) {
                // Trying to open ".." brought us the same directory. That means we're at "/"
                // (the REAL "/").
                // So we escaped the "beneath" directory.
                return Err(io::Error::from_raw_os_error(libc::EXDEV));
            }
        }

        prev_meta = Some(cur_meta);

        let new_fd = unsafe {
            libc::openat(
                cur_file_ref.as_raw_fd(),
                b"..\0".as_ptr() as *const _,
                libc::O_RDONLY | libc::O_DIRECTORY,
            )
        };
        assert!(new_fd >= 0);
        cur_file = Some(unsafe { fs::File::from_raw_fd(new_fd) });
    }
}

#[test]
fn test_race_escape() {
    let tmpdir = tempfile::tempdir().unwrap();
    let tmpdir = tmpdir.as_ref();

    fs::create_dir(tmpdir.join("a")).unwrap();
    fs::create_dir(tmpdir.join("a/b")).unwrap();

    let a_file = fs::File::open(tmpdir.join("a")).unwrap();
    let a_fd = a_file.as_raw_fd();
    let a_meta = a_file.metadata().unwrap();

    let thread_running = Arc::new(AtomicBool::new(true));

    let path1 = CString::new(tmpdir.join("a/b").as_os_str().as_bytes()).unwrap();
    let path2 = CString::new(tmpdir.join("b").as_os_str().as_bytes()).unwrap();
    let thread_running_clone = thread_running.clone();
    let join_handle = std::thread::spawn(move || {
        while thread_running_clone.load(Ordering::SeqCst) {
            for _ in 0..10000 {
                unsafe {
                    libc::rename(path1.as_ptr(), path2.as_ptr());
                    libc::rename(path2.as_ptr(), path1.as_ptr());
                }
            }
        }
    });

    let mut successes = 0u64;
    let mut eagains = 0u64;
    let mut enoents = 0u64;
    let mut escapes = 0u64;

    for path in &[
        CString::new("b/..").unwrap(),
        CString::new(
            Path::new("b/../../")
                .join(tmpdir.file_name().unwrap())
                .as_os_str()
                .as_bytes(),
        )
        .unwrap(),
    ] {
        for _ in 0..20000 {
            let res = open_beneath(
                a_fd,
                path,
                libc::O_RDONLY | libc::O_DIRECTORY,
                0,
                LookupFlags::IN_ROOT,
            );

            match res {
                Ok(f) => {
                    if let Err(e) = check_beneath(&f, &a_meta) {
                        escapes += 1;
                        println!("WARNING: Possible escape detected! (Error: {})", e);
                    }

                    successes += 1;
                }
                Err(e) if e.raw_os_error() == Some(libc::EAGAIN) => eagains += 1,
                Err(e) if e.raw_os_error() == Some(libc::ENOENT) => enoents += 1,
                Err(e) => panic!("{}", e),
            }
        }
    }

    thread_running.store(false, Ordering::SeqCst);
    join_handle.join().unwrap();

    println!("Opened successfully: {} times", successes);
    println!("Failed with ENOENT: {} times", enoents);
    println!("Failed with EAGAIN: {} times", eagains);
    println!("Escaped: {} times", escapes);

    if escapes > 0 {
        panic!("Escape detected!");
    }
}
