//! A secret's file on Windows: an access list for this account alone, from the file's creation, and
//! the check that refuses a seed file whose list lets anyone else in. What mode 0600 and its check
//! are on Unix (`out.rs`, `dx/key.rs`).
//!
//! Everything is by SID, read through the system's calls in numeric form, never by a name, which is
//! localised, and never from `icacls`' output. All of this binary's unsafe code for access lists is
//! the block below marked `cfg(windows)`; what decides ([`exposure`], [`private_descriptor`],
//! [`Exposure::fix_command`]) is ordinary code over plain values, so its tests run on every system.
//!
//! **The list.** Owner: this account. One entry: this account, all rights. Inheritance off, so the
//! folder's entries never reach the file, whichever folder it is. SYSTEM and Administrators are not
//! listed. Nothing real needs them: a backup or a virus scanner reads through its privileges rather
//! than through the list, an administrator can still take ownership, as root can read a file of mode
//! 0600, and the account itself reads it whether or not it is elevated. So the rule is one rule.
//!
//! **The order.** A file is made by one call that carries its list (`CreateFileW`, with the security
//! descriptor), so it never exists under a wider list, not even empty, and no other account can hold
//! a handle to it from a moment before its list was set.

#![cfg_attr(not(all(windows, feature = "quickstart")), allow(dead_code, unused_imports))]

/// One entry of an access list, as its header states it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
    /// The ACE type.
    pub kind: u8,
    /// The ACE flags.
    pub flags: u8,
    /// The rights.
    pub mask: u32,
    /// Whom it is for, numeric; `None` for a kind whose layout this code does not read.
    pub sid: Option<String>,
}

/// A file's owner and access list.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Acl {
    pub owner: Option<String>,
    /// Whether inheritance is off.
    pub protected: bool,
    /// The entries; `None` when the file has no list at all, which lets everyone in.
    pub entries: Option<Vec<Entry>>,
}

const ACCESS_ALLOWED: u8 = 0;
const ACCESS_DENIED: u8 = 1;
const ACCESS_DENIED_OBJECT: u8 = 6;
const ACCESS_ALLOWED_CALLBACK: u8 = 9;
const ACCESS_DENIED_CALLBACK: u8 = 10;
const ACCESS_DENIED_CALLBACK_OBJECT: u8 = 12;
const INHERIT_ONLY_ACE: u8 = 8;

/// The security descriptor of a file (or a directory, whose children would inherit its one entry)
/// for the account `me` alone, in the system's own descriptor language: `P` is inheritance off, `FA`
/// all of a file's rights, and `OICI` that files and directories inside inherit the entry.
pub fn private_descriptor(me: &str, directory: bool) -> String {
    format!("O:{me}D:P(A;{};FA;;;{me})", if directory { "OICI" } else { "" })
}

/// Who, besides the account a file is for, its list lets in.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Exposure {
    /// The other accounts an entry grants anything to, numeric.
    pub granted: Vec<String>,
    /// The file has no list at all.
    pub no_list: bool,
    /// An entry that grants something, of a kind this code cannot read the account of.
    pub unread_entry: bool,
    /// Another account owns it, and an owner may change the list.
    pub other_owner: Option<String>,
    /// Whether the file inherits from its folder.
    pub inherits: bool,
}

/// What lets anyone but `me` in, or `None`. As Unix's check refuses any bit of a seed's mode for
/// group or other, this refuses an entry that grants another account anything at all. A deny entry
/// grants nothing, and an entry that only children inherit is not this file's.
pub fn exposure(acl: &Acl, me: &str) -> Option<Exposure> {
    let mut x = Exposure { inherits: !acl.protected, ..Exposure::default() };
    match &acl.entries {
        None => x.no_list = true,
        Some(entries) => {
            for e in entries {
                let deny = matches!(e.kind, ACCESS_DENIED | ACCESS_DENIED_OBJECT | ACCESS_DENIED_CALLBACK | ACCESS_DENIED_CALLBACK_OBJECT);
                if deny || e.flags & INHERIT_ONLY_ACE != 0 || e.mask == 0 {
                    continue;
                }
                match (&e.sid, e.kind) {
                    (Some(s), ACCESS_ALLOWED | ACCESS_ALLOWED_CALLBACK) => {
                        if s != me && !x.granted.contains(s) {
                            x.granted.push(s.clone());
                        }
                    }
                    _ => x.unread_entry = true,
                }
            }
        }
    }
    if acl.owner.as_deref() != Some(me) {
        x.other_owner = Some(acl.owner.clone().unwrap_or_else(|| "no account".into()));
    }
    (x.no_list || x.unread_entry || !x.granted.is_empty() || x.other_owner.is_some()).then_some(x)
}

/// A well-known account's English name, for a person reading a message; nothing decides by it.
fn known(sid: &str) -> Option<&'static str> {
    Some(match sid {
        "S-1-1-0" => "Everyone",
        "S-1-5-3" => "BATCH",
        "S-1-5-4" => "INTERACTIVE",
        "S-1-5-6" => "SERVICE",
        "S-1-5-11" => "Authenticated Users",
        "S-1-5-18" => "SYSTEM",
        "S-1-5-32-544" => "Administrators",
        "S-1-5-32-545" => "Users",
        _ => return None,
    })
}

fn shown(sid: &str) -> String {
    match known(sid) {
        Some(n) => format!("{n} ({sid})"),
        None => sid.to_string(),
    }
}

impl Exposure {
    /// What is wrong, for the message that names the file.
    pub fn describe(&self) -> String {
        let mut what = Vec::new();
        if self.no_list {
            what.push("it has no access list, which lets everyone in".to_string());
        }
        if !self.granted.is_empty() {
            what.push(format!("its access list lets {} in", self.granted.iter().map(|s| shown(s)).collect::<Vec<_>>().join(", ")));
        }
        if self.unread_entry {
            what.push("its access list holds an entry of a kind wai does not read".to_string());
        }
        if let Some(o) = &self.other_owner {
            what.push(format!("it is owned by {}, who may change who reads it", shown(o)));
        }
        what.join("; ")
    }

    /// The command that makes the file the account's alone, naming every account by its SID.
    pub fn fix_command(&self, path: &str, me: &str) -> String {
        self.fix_with_grant(path, me, &format!("*{me}:F"))
    }

    /// [`Exposure::fix_command`] for a directory: its one entry reaches what is made inside, as
    /// [`private_descriptor`]'s does. In quotes, since a shell may read its parentheses.
    pub fn dir_fix_command(&self, path: &str, me: &str) -> String {
        self.fix_with_grant(path, me, &format!("\"*{me}:(OI)(CI)F\""))
    }

    fn fix_with_grant(&self, path: &str, me: &str, grant: &str) -> String {
        let mut c = String::new();
        if self.other_owner.is_some() {
            c.push_str(&format!("icacls \"{path}\" /setowner *{me} && "));
        }
        c.push_str(&format!("icacls \"{path}\""));
        if self.inherits {
            c.push_str(" /inheritance:r");
        }
        if !self.granted.is_empty() {
            c.push_str(" /remove");
            for s in &self.granted {
                c.push_str(&format!(" *{s}"));
            }
        }
        c.push_str(&format!(" /grant:r {grant}"));
        c
    }
}

// ─────────────────────────────── the system's calls ───────────────────────────────

#[cfg(windows)]
mod sys {
    use super::{Acl, Entry};
    use std::ffi::{OsStr, c_void};
    use std::io;
    use std::os::windows::ffi::OsStrExt;
    use std::path::Path;
    use std::ptr::null_mut;
    use windows_sys::Win32::Foundation::{CloseHandle, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE, LocalFree};
    use windows_sys::Win32::Security::Authorization::{ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW, GetNamedSecurityInfoW, SDDL_REVISION_1, SE_FILE_OBJECT};
    use windows_sys::Win32::Security::{
        ACE_HEADER, ACL, ACL_SIZE_INFORMATION, AclSizeInformation, DACL_SECURITY_INFORMATION, GetAce, GetAclInformation, GetSecurityDescriptorControl, GetTokenInformation, OWNER_SECURITY_INFORMATION, PSID,
        SE_DACL_PROTECTED, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER, TokenUser,
    };
    use windows_sys::Win32::Storage::FileSystem::{CREATE_NEW, CreateDirectoryW, CreateFileW, FILE_ATTRIBUTE_NORMAL};
    use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};

    /// Memory the system gave with `LocalAlloc`, freed when dropped.
    struct Local(*mut c_void);

    impl Drop for Local {
        fn drop(&mut self) {
            if !self.0.is_null() {
                // SAFETY: the pointer came from a call that says to free it with LocalFree, once.
                unsafe { LocalFree(self.0) };
            }
        }
    }

    fn wide(s: &OsStr) -> io::Result<Vec<u16>> {
        let mut w: Vec<u16> = s.encode_wide().collect();
        if w.contains(&0) {
            return Err(io::Error::new(io::ErrorKind::InvalidInput, "a NUL in a name"));
        }
        w.push(0);
        Ok(w)
    }

    /// A SID's numeric text.
    ///
    /// # Safety
    /// `sid` points at a valid SID.
    unsafe fn sid_text(sid: PSID) -> io::Result<String> {
        let mut text: *mut u16 = null_mut();
        // SAFETY: the caller's SID; `text` is written by the call.
        if unsafe { ConvertSidToStringSidW(sid, &mut text) } == 0 {
            return Err(io::Error::last_os_error());
        }
        let _free = Local(text.cast());
        let mut n = 0;
        // SAFETY: the system's text ends with a NUL, which this stops at.
        while unsafe { *text.add(n) } != 0 {
            n += 1;
        }
        // SAFETY: `n` units were just read from it.
        Ok(String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(text, n) }))
    }

    /// The SID of the account this process runs as.
    pub fn current_user() -> io::Result<String> {
        let mut token: HANDLE = null_mut();
        // SAFETY: the current process's pseudo-handle; `token` is written by the call.
        if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
            return Err(io::Error::last_os_error());
        }
        let mut len = 0u32;
        // SAFETY: a query for the length alone; it fails with the length written.
        unsafe { GetTokenInformation(token, TokenUser, null_mut(), 0, &mut len) };
        // u64s, for TOKEN_USER's alignment.
        let mut buf = vec![0u64; (len as usize).div_ceil(8).max(1)];
        // SAFETY: `buf` holds `len` bytes.
        let ok = unsafe { GetTokenInformation(token, TokenUser, buf.as_mut_ptr().cast(), len, &mut len) };
        let err = io::Error::last_os_error();
        // SAFETY: the handle opened above, closed once.
        unsafe { CloseHandle(token) };
        if ok == 0 {
            return Err(err);
        }
        // SAFETY: the call wrote a TOKEN_USER at the start of `buf`, whose SID lies inside `buf`.
        unsafe { sid_text((*buf.as_ptr().cast::<TOKEN_USER>()).User.Sid) }
    }

    /// `f`, given security attributes that carry the descriptor `text`.
    fn with_descriptor<T>(text: &str, f: impl FnOnce(*const SECURITY_ATTRIBUTES) -> T) -> io::Result<T> {
        let w = wide(OsStr::new(text))?;
        let mut sd: *mut c_void = null_mut();
        // SAFETY: `w` ends with a NUL; `sd` is written by the call and freed by `_free`.
        if unsafe { ConvertStringSecurityDescriptorToSecurityDescriptorW(w.as_ptr(), SDDL_REVISION_1, &mut sd, null_mut()) } == 0 {
            return Err(io::Error::last_os_error());
        }
        let _free = Local(sd);
        let sa = SECURITY_ATTRIBUTES { nLength: size_of::<SECURITY_ATTRIBUTES>() as u32, lpSecurityDescriptor: sd, bInheritHandle: 0 };
        Ok(f(&sa))
    }

    /// A new file, which must not exist yet, under the account's own list from the moment it exists.
    pub fn create_new_file(path: &Path) -> io::Result<std::fs::File> {
        use std::os::windows::io::{FromRawHandle, RawHandle};
        let name = wide(path.as_os_str())?;
        let descriptor = super::private_descriptor(&current_user()?, false);
        with_descriptor(&descriptor, |sa| {
            // SAFETY: `name` ends with a NUL, and `sa` and what it points at live for the call.
            let h = unsafe { CreateFileW(name.as_ptr(), GENERIC_WRITE, 0, sa, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, null_mut()) };
            if h == INVALID_HANDLE_VALUE {
                return Err(io::Error::last_os_error());
            }
            // SAFETY: a handle just opened, which nothing else owns.
            Ok(unsafe { std::fs::File::from_raw_handle(h as RawHandle) })
        })?
    }

    /// A directory under the account's own list, its parents made as usual; one that exists stays as
    /// it is, as a directory that exists does on Unix.
    pub fn create_dir(path: &Path) -> io::Result<()> {
        if path.is_dir() {
            return Ok(());
        }
        if let Some(p) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
            std::fs::create_dir_all(p)?;
        }
        let name = wide(path.as_os_str())?;
        let descriptor = super::private_descriptor(&current_user()?, true);
        with_descriptor(&descriptor, |sa| {
            // SAFETY: `name` ends with a NUL, and `sa` and what it points at live for the call.
            if unsafe { CreateDirectoryW(name.as_ptr(), sa) } != 0 {
                return Ok(());
            }
            let e = io::Error::last_os_error();
            if path.is_dir() { Ok(()) } else { Err(e) }
        })?
    }

    /// A file's owner and access list.
    pub fn read(path: &Path) -> io::Result<Acl> {
        let name = wide(path.as_os_str())?;
        let (mut owner, mut dacl, mut sd): (PSID, *mut ACL, *mut c_void) = (null_mut(), null_mut(), null_mut());
        // SAFETY: `name` ends with a NUL; the out pointers point into the descriptor `sd`, freed below.
        let e = unsafe { GetNamedSecurityInfoW(name.as_ptr(), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &mut owner, null_mut(), &mut dacl, null_mut(), &mut sd) };
        if e != 0 {
            return Err(io::Error::from_raw_os_error(e as i32));
        }
        let _free = Local(sd);
        let (mut control, mut revision) = (0u16, 0u32);
        // SAFETY: the descriptor the call above returned.
        if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) } == 0 {
            return Err(io::Error::last_os_error());
        }
        // SAFETY: the descriptor's owner, when it has one.
        let owner = if owner.is_null() { None } else { Some(unsafe { sid_text(owner) }?) };
        let mut entries = None;
        if !dacl.is_null() {
            let mut info = ACL_SIZE_INFORMATION { AceCount: 0, AclBytesInUse: 0, AclBytesFree: 0 };
            // SAFETY: the descriptor's list; `info` is the size the class asks for.
            if unsafe { GetAclInformation(dacl.cast_const(), (&raw mut info).cast(), size_of::<ACL_SIZE_INFORMATION>() as u32, AclSizeInformation) } == 0 {
                return Err(io::Error::last_os_error());
            }
            let mut list = Vec::with_capacity(info.AceCount as usize);
            for i in 0..info.AceCount {
                let mut ace: *mut c_void = null_mut();
                // SAFETY: an index the list says it holds.
                if unsafe { GetAce(dacl.cast_const(), i, &mut ace) } == 0 {
                    return Err(io::Error::last_os_error());
                }
                // SAFETY: an ACE begins with its header.
                let (kind, flags) = unsafe { ((*ace.cast::<ACE_HEADER>()).AceType, (*ace.cast::<ACE_HEADER>()).AceFlags) };
                // SAFETY: every ACE holds its mask after the header's four bytes.
                let mask = unsafe { ace.cast::<u8>().add(4).cast::<u32>().read_unaligned() };
                // These kinds hold the SID after the mask; the object kinds put more before it.
                let sid = if matches!(kind, super::ACCESS_ALLOWED | super::ACCESS_DENIED | super::ACCESS_ALLOWED_CALLBACK | super::ACCESS_DENIED_CALLBACK) {
                    // SAFETY: the SID of an ACE of such a kind, eight bytes in.
                    Some(unsafe { sid_text(ace.cast::<u8>().add(8).cast()) }?)
                } else {
                    None
                };
                list.push(Entry { kind, flags, mask, sid });
            }
            entries = Some(list);
        }
        Ok(Acl { owner, protected: control & SE_DACL_PROTECTED != 0, entries })
    }
}

#[cfg(windows)]
#[allow(unused_imports)] // A binary that includes `out.rs` for its other writes calls none of these.
pub use sys::{create_dir, create_new_file, current_user, read};

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(windows)]
    use std::io;

    const ME: &str = "S-1-5-21-1-2-3-1001";
    const FILE_ALL: u32 = 0x1f01ff;

    fn allow(sid: &str, mask: u32) -> Entry {
        Entry { kind: ACCESS_ALLOWED, flags: 0, mask, sid: Some(sid.into()) }
    }

    fn acl(entries: Vec<Entry>) -> Acl {
        Acl { owner: Some(ME.into()), protected: true, entries: Some(entries) }
    }

    #[test]
    fn the_descriptor_is_this_account_alone_with_inheritance_off() {
        assert_eq!(private_descriptor(ME, false), format!("O:{ME}D:P(A;;FA;;;{ME})"));
        assert_eq!(private_descriptor(ME, true), format!("O:{ME}D:P(A;OICI;FA;;;{ME})"));
    }

    #[test]
    fn only_this_account_is_let_in_or_the_file_is_exposed() {
        assert_eq!(exposure(&acl(vec![allow(ME, FILE_ALL)]), ME), None);
        // Each account named, SYSTEM and Administrators as any other, whatever the right.
        for sid in ["S-1-1-0", "S-1-5-18", "S-1-5-32-544", "S-1-5-32-545", "S-1-5-21-1-2-3-1002"] {
            let x = exposure(&acl(vec![allow(ME, FILE_ALL), allow(sid, 0x120089)]), ME).unwrap_or_else(|| panic!("{sid}"));
            assert_eq!(x.granted, [sid]);
            let x = exposure(&acl(vec![allow(sid, 0x100000)]), ME).unwrap_or_else(|| panic!("{sid}"));
            assert_eq!(x.granted, [sid], "any right, as any bit of a mode");
        }
        // No list at all lets everyone in.
        let x = exposure(&Acl { entries: None, ..acl(vec![]) }, ME).unwrap();
        assert!(x.no_list && x.describe().contains("no access list"));
        // Another owner may change the list.
        let x = exposure(&Acl { owner: Some("S-1-5-32-544".into()), ..acl(vec![allow(ME, FILE_ALL)]) }, ME).unwrap();
        assert_eq!(x.other_owner.as_deref(), Some("S-1-5-32-544"));
        assert!(x.describe().contains("Administrators (S-1-5-32-544)"), "{}", x.describe());
        assert!(exposure(&Acl { owner: None, ..acl(vec![allow(ME, FILE_ALL)]) }, ME).is_some());
        // An entry that grants, of a kind whose account is not read, is refused, not passed.
        for kind in [5u8, 11, 4, 200] {
            let x = exposure(&acl(vec![Entry { kind, flags: 0, mask: 1, sid: None }]), ME).unwrap_or_else(|| panic!("{kind}"));
            assert!(x.unread_entry && x.describe().contains("does not read"), "{kind}");
        }
        // A callback entry is read like an allow entry.
        let x = exposure(&acl(vec![Entry { kind: ACCESS_ALLOWED_CALLBACK, flags: 0, mask: 1, sid: Some("S-1-1-0".into()) }]), ME).unwrap();
        assert_eq!(x.granted, ["S-1-1-0"]);
    }

    #[test]
    fn what_grants_nothing_to_this_file_is_not_an_exposure() {
        let denied = Entry { kind: ACCESS_DENIED, flags: 0, mask: FILE_ALL, sid: Some("S-1-1-0".into()) };
        assert_eq!(exposure(&acl(vec![denied, allow(ME, FILE_ALL)]), ME), None);
        for kind in [ACCESS_DENIED_OBJECT, ACCESS_DENIED_CALLBACK, ACCESS_DENIED_CALLBACK_OBJECT] {
            assert_eq!(exposure(&acl(vec![Entry { kind, flags: 0, mask: 1, sid: None }]), ME), None, "{kind}");
        }
        let for_children = Entry { kind: ACCESS_ALLOWED, flags: INHERIT_ONLY_ACE, mask: FILE_ALL, sid: Some("S-1-1-0".into()) };
        assert_eq!(exposure(&acl(vec![for_children]), ME), None);
        assert_eq!(exposure(&acl(vec![allow("S-1-1-0", 0)]), ME), None, "no right at all");
        // Inheriting from a folder whose entries are this account's alone lets no one else in.
        assert_eq!(exposure(&Acl { protected: false, ..acl(vec![allow(ME, FILE_ALL)]) }, ME), None);
    }

    #[test]
    fn the_fix_names_every_account_by_its_sid() {
        let f = "C:\\ks\\k.seed";
        let wide = exposure(&Acl { protected: false, ..acl(vec![allow("S-1-1-0", 1), allow("S-1-5-18", 1), allow(ME, FILE_ALL)]) }, ME).unwrap();
        assert_eq!(wide.fix_command(f, ME), format!("icacls \"{f}\" /inheritance:r /remove *S-1-1-0 *S-1-5-18 /grant:r *{ME}:F"));
        let granted = exposure(&acl(vec![allow("S-1-1-0", 1)]), ME).unwrap();
        assert_eq!(granted.fix_command(f, ME), format!("icacls \"{f}\" /remove *S-1-1-0 /grant:r *{ME}:F"));
        let owned = exposure(&Acl { owner: Some("S-1-5-32-544".into()), ..acl(vec![allow(ME, FILE_ALL)]) }, ME).unwrap();
        assert_eq!(owned.fix_command(f, ME), format!("icacls \"{f}\" /setowner *{ME} && icacls \"{f}\" /grant:r *{ME}:F"));
        // A directory's own entry reaches what is made inside it.
        let d = "C:\\ks";
        assert_eq!(wide.dir_fix_command(d, ME), format!("icacls \"{d}\" /inheritance:r /remove *S-1-1-0 *S-1-5-18 /grant:r \"*{ME}:(OI)(CI)F\""));
    }

    /// The system's calls: a file made under its list before a byte is in it, read back by SID.
    #[cfg(windows)]
    #[test]
    fn a_file_and_a_directory_are_this_accounts_from_their_creation() {
        let d = std::env::temp_dir().join(format!("wai-win-acl-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&d);
        let inner = d.join("parent").join("ks");
        create_dir(&inner).unwrap();
        create_dir(&inner).unwrap();
        let me = current_user().unwrap();
        let a = read(&inner).unwrap();
        assert!(a.protected, "{a:?}");
        assert_eq!(exposure(&a, &me), None, "{a:?}");
        // Its entry reaches what is made inside by any means.
        let p = inner.join("k.seed");
        let f = create_new_file(&p).unwrap();
        // Before a byte is written: empty, and already under its list alone.
        assert_eq!(std::fs::metadata(&p).unwrap().len(), 0);
        let a = read(&p).unwrap();
        assert!(a.protected, "{a:?}");
        assert_eq!(a.owner.as_deref(), Some(me.as_str()), "{a:?}");
        let entries = a.entries.clone().unwrap();
        assert_eq!(entries.len(), 1, "{a:?}");
        assert_eq!((entries[0].kind, entries[0].sid.as_deref()), (ACCESS_ALLOWED, Some(me.as_str())), "{a:?}");
        assert_eq!(exposure(&a, &me), None, "{a:?}");
        drop(f);
        assert_eq!(create_new_file(&p).unwrap_err().kind(), io::ErrorKind::AlreadyExists);
        std::fs::remove_dir_all(&d).unwrap();
    }
}
