//! The Windows implementation of std::fs::remove_dir_all.
//!
//! This needs to address two issues:
//!
//! - It must not be possible to trick this into deleting files outside of
//!   the parent directory (see CVE-2022-21658).
//! - It should not fail if many threads or processes call `remove_dir_all`
//!   on the same path.
//!
//! The first is handled by using the low-level `NtOpenFile` API to open a file
//! relative to a parent directory.
//!
//! The second is trickier. Deleting a file works by setting its "disposition"
//! to delete. However, it isn't actually deleted until the file is closed.
//! During the gap between these two events, the file is in a kind of limbo
//! state where it still exists in the filesystem but anything trying to open
//! it fails with an error.
//!
//! The mitigations we use here are:
//!
//! - When attempting to open the file, we treat ERROR_DELETE_PENDING as a
//!   successful delete.
//! - If the file still hasn't been removed from the filesystem by the time we
//!   attempt to delete the parent directory, we try to wait for it to finish.
//!   We can't wait indefinitely though so after some number of spins, we give
//!   up and return an error.
//!
//! In short, we can't guarantee this will always succeed in the event of a
//! race but we do make a best effort such that it *should* do so.

use core::ptr;
use core::sync::atomic::{Atomic, AtomicU32, Ordering};

use super::{AsRawHandle, DirBuff, File, FromRawHandle};
use crate::sys::c;
use crate::sys::pal::api::{UnicodeStrRef, WinError, unicode_str};
use crate::thread;

// The maximum number of times to spin when waiting for deletes to complete.
const MAX_RETRIES: usize = 50;

/// A wrapper around a raw NtOpenFile call.
///
/// This isn't completely safe because `OBJECT_ATTRIBUTES` contains raw pointers.
unsafe fn nt_open_file(
    access: u32,
    object_attribute: &c::OBJECT_ATTRIBUTES,
    share: u32,
    options: u32,
) -> Result<File, WinError> {
    unsafe {
        let mut handle = ptr::null_mut();
        let mut io_status = c::IO_STATUS_BLOCK::PENDING;
        let status =
            c::NtOpenFile(&mut handle, access, object_attribute, &mut io_status, share, options);
        if c::nt_success(status) {
            Ok(File::from_raw_handle(handle))
        } else {
            // Convert an NTSTATUS to the more familiar Win32 error code (aka "DosError")
            let win_error = if status == c::STATUS_DELETE_PENDING {
                // We make a special exception for `STATUS_DELETE_PENDING` because
                // otherwise this will be mapped to `ERROR_ACCESS_DENIED` which is
                // very unhelpful because that can also mean a permission error.
                WinError::DELETE_PENDING
            } else {
                WinError::new(c::RtlNtStatusToDosError(status))
            };
            Err(win_error)
        }
    }
}

/// Open the file `path` in the directory `parent`, requesting the given `access` rights.
/// `options` will be OR'd with `FILE_OPEN_REPARSE_POINT`.
fn open_link_no_reparse(
    parent: &File,
    path: UnicodeStrRef<'_>,
    access: u32,
    options: u32,
) -> Result<Option<File>, WinError> {
    // This is implemented using the lower level `NtOpenFile` function as
    // unfortunately opening a file relative to a parent is not supported by
    // win32 functions.
    //
    // See https://learn.microsoft.com/windows/win32/api/winternl/nf-winternl-ntopenfile

    // The `OBJ_DONT_REPARSE` attribute ensures that we haven't been
    // tricked into following a symlink. However, it may not be available in
    // earlier versions of Windows.
    static ATTRIBUTES: Atomic<u32> = AtomicU32::new(c::OBJ_DONT_REPARSE);

    let result = unsafe {
        let mut object = c::OBJECT_ATTRIBUTES {
            ObjectName: path.as_ptr(),
            RootDirectory: parent.as_raw_handle(),
            Attributes: ATTRIBUTES.load(Ordering::Relaxed),
            ..c::OBJECT_ATTRIBUTES::with_length()
        };
        let share = c::FILE_SHARE_DELETE | c::FILE_SHARE_READ | c::FILE_SHARE_WRITE;
        let options = c::FILE_OPEN_REPARSE_POINT | options;
        let result = nt_open_file(access, &object, share, options);

        // Retry without OBJ_DONT_REPARSE if it's not supported.
        if matches!(result, Err(WinError::INVALID_PARAMETER))
            && ATTRIBUTES.load(Ordering::Relaxed) == c::OBJ_DONT_REPARSE
        {
            ATTRIBUTES.store(0, Ordering::Relaxed);
            object.Attributes = 0;
            nt_open_file(access, &object, share, options)
        } else {
            result
        }
    };

    // Ignore not found errors
    match result {
        Ok(f) => Ok(Some(f)),
        Err(
            WinError::FILE_NOT_FOUND
            | WinError::PATH_NOT_FOUND
            | WinError::BAD_NETPATH
            | WinError::BAD_NET_NAME
            // `DELETE_PENDING` means something else is already trying to delete it
            // so we assume that will eventually succeed.
            | WinError::DELETE_PENDING,
        ) => Ok(None),
        Err(e) => Err(e),
    }
}

fn open_dir(parent: &File, name: UnicodeStrRef<'_>) -> Result<Option<File>, WinError> {
    // Open the directory for synchronous directory listing.
    open_link_no_reparse(
        parent,
        name,
        c::SYNCHRONIZE | c::FILE_LIST_DIRECTORY,
        // "_IO_NONALERT" means that a synchronous call won't be interrupted.
        c::FILE_SYNCHRONOUS_IO_NONALERT,
    )
}

fn delete(parent: &File, name: UnicodeStrRef<'_>) -> Result<(), WinError> {
    // Note that the `delete` function consumes the opened file to ensure it's
    // dropped immediately. See module comments for why this is important.
    match open_link_no_reparse(parent, name, c::DELETE, 0) {
        Ok(Some(f)) => f.delete(),
        Ok(None) => Ok(()),
        Err(e) => Err(e),
    }
}

/// A simple retry loop that keeps running `f` while it fails with the given
/// error code or until `MAX_RETRIES` is reached.
fn retry<T: PartialEq>(
    mut f: impl FnMut() -> Result<T, WinError>,
    ignore: WinError,
) -> Result<T, WinError> {
    let mut i = MAX_RETRIES;
    loop {
        i -= 1;
        if i == 0 {
            return f();
        } else {
            let result = f();
            if result != Err(ignore) {
                return result;
            }
        }
        thread::yield_now();
    }
}

pub fn remove_dir_all_iterative(dir: File) -> Result<(), WinError> {
    let mut buffer = DirBuff::new();
    let mut dirlist = vec![dir];

    let mut restart = true;
    'outer: while let Some(dir) = dirlist.pop() {
        let more_data = dir.fill_dir_buff(&mut buffer, restart)?;
        for (name, is_directory) in buffer.iter() {
            let name = unicode_str!(&name);
            if is_directory {
                let Some(subdir) = open_dir(&dir, name)? else { continue };
                dirlist.push(dir);
                dirlist.push(subdir);
                continue 'outer;
            } else {
                // Attempt to delete, retrying on sharing violation errors as these
                // can often be very temporary. E.g. if something takes just a
                // bit longer than expected to release a file handle.
                retry(|| delete(&dir, name), WinError::SHARING_VIOLATION)?;
            }
        }
        if more_data {
            dirlist.push(dir);
            restart = false;
        } else {
            // Attempt to delete, retrying on not empty errors because we may
            // need to wait some time for files to be removed from the filesystem.
            let name = unicode_str!("");
            retry(|| delete(&dir, name), WinError::DIR_NOT_EMPTY)?;
            restart = true;
        }
    }
    Ok(())
}
