//! Process memory-map capture primitives for the native kboxlike backend.
//!
//! Unlike the sentry backend, kboxlike snapshots real Linux processes with
//! ordinary ASLR. There is no low guest arena to filter on, so the first
//! snapshot primitive must preserve the full `/proc/<pid>/maps` shape and let
//! later capture policy decide whether a mapping is dumped, file-remapped, or
//! recreated by the kernel during restore.

#[cfg(target_os = "linux")]
use super::fd::FdError;
use std::path::PathBuf;

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct KboxlikeMapPerms {
    pub(crate) read: bool,
    pub(crate) write: bool,
    pub(crate) exec: bool,
    pub(crate) shared: bool,
}

impl KboxlikeMapPerms {
    pub(crate) fn prot(self) -> i32 {
        let mut prot = 0;
        if self.read {
            prot |= libc::PROT_READ;
        }
        if self.write {
            prot |= libc::PROT_WRITE;
        }
        if self.exec {
            prot |= libc::PROT_EXEC;
        }
        prot
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct KboxlikeMapEntry {
    pub(crate) start: u64,
    pub(crate) end: u64,
    pub(crate) perms: KboxlikeMapPerms,
    pub(crate) file_offset: u64,
    pub(crate) dev: String,
    pub(crate) inode: u64,
    pub(crate) path: Option<String>,
}

impl KboxlikeMapEntry {
    pub(crate) fn len(&self) -> u64 {
        self.end - self.start
    }

    pub(crate) fn is_kernel_synthetic(&self) -> bool {
        matches!(
            self.path.as_deref(),
            Some("[vdso]") | Some("[vvar]") | Some("[vvar_vclock]") | Some("[vsyscall]")
        )
    }

    pub(crate) fn is_dump_candidate(&self) -> bool {
        self.end > self.start
            && self.perms.read
            && (!self.is_kernel_synthetic() || self.path.as_deref() == Some("[vdso]"))
    }

    pub(crate) fn file_path(&self) -> Option<PathBuf> {
        let path = self.path.as_deref()?;
        if path.starts_with('[') {
            return None;
        }
        Some(PathBuf::from(path.trim_end_matches(" (deleted)")))
    }
}

pub(crate) fn parse_proc_maps(raw: &str) -> Vec<KboxlikeMapEntry> {
    raw.lines().filter_map(parse_proc_maps_line).collect()
}

#[cfg(target_os = "linux")]
pub(crate) fn read_proc_maps_for_pid(host_pid: i32) -> Result<Vec<KboxlikeMapEntry>, FdError> {
    if host_pid <= 0 {
        return Err(FdError::TraceeInstallFailed("invalid tracee pid"));
    }
    let raw = std::fs::read_to_string(format!("/proc/{host_pid}/maps"))
        .map_err(|_| FdError::TraceeInstallFailed("read tracee maps failed"))?;
    Ok(parse_proc_maps(&raw))
}

fn parse_proc_maps_line(line: &str) -> Option<KboxlikeMapEntry> {
    let mut fields = line.split_whitespace();
    let range = fields.next()?;
    let perms = parse_perms(fields.next()?)?;
    let file_offset = u64::from_str_radix(fields.next()?, 16).ok()?;
    let dev = fields.next()?.to_owned();
    let inode = fields.next()?.parse::<u64>().ok()?;

    let mut range_parts = range.splitn(2, '-');
    let start = u64::from_str_radix(range_parts.next()?, 16).ok()?;
    let end = u64::from_str_radix(range_parts.next()?, 16).ok()?;
    if end <= start {
        return None;
    }

    let path = fields.collect::<Vec<_>>();
    let path = if path.is_empty() {
        None
    } else {
        Some(path.join(" "))
    };

    Some(KboxlikeMapEntry {
        start,
        end,
        perms,
        file_offset,
        dev,
        inode,
        path,
    })
}

fn parse_perms(raw: &str) -> Option<KboxlikeMapPerms> {
    let bytes = raw.as_bytes();
    if bytes.len() < 4 {
        return None;
    }
    Some(KboxlikeMapPerms {
        read: bytes[0] == b'r',
        write: bytes[1] == b'w',
        exec: bytes[2] == b'x',
        shared: bytes[3] == b's',
    })
}

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

    #[test]
    fn parses_proc_maps_entries_with_full_metadata() {
        let raw = "\
00400000-00452000 r-xp 00000000 08:02 131073 /usr/bin/chrome\n\
00652000-00653000 r--p 00052000 08:02 131073 /usr/bin/chrome\n\
7f0000000000-7f0000021000 rw-p 00000000 00:00 0 [heap]\n\
7ffc00000000-7ffc00021000 rw-p 00000000 00:00 0 [stack]\n\
7ffc12345000-7ffc12347000 r--p 00000000 00:00 0 [vvar]\n\
7ffc12347000-7ffc12349000 r-xp 00000000 00:00 0 [vdso]\n";

        let maps = parse_proc_maps(raw);

        assert_eq!(maps.len(), 6);
        assert_eq!(maps[0].start, 0x0040_0000);
        assert_eq!(maps[0].end, 0x0045_2000);
        assert_eq!(
            maps[0].perms,
            KboxlikeMapPerms {
                read: true,
                write: false,
                exec: true,
                shared: false,
            }
        );
        assert_eq!(maps[0].file_offset, 0);
        assert_eq!(maps[0].dev, "08:02");
        assert_eq!(maps[0].inode, 131073);
        assert_eq!(maps[0].file_path(), Some(PathBuf::from("/usr/bin/chrome")));
        assert_eq!(maps[2].path.as_deref(), Some("[heap]"));
        assert!(maps[2].is_dump_candidate());
        assert!(maps[4].is_kernel_synthetic());
        assert!(!maps[4].is_dump_candidate());
        assert!(maps[5].is_kernel_synthetic());
        assert!(maps[5].is_dump_candidate());
    }

    #[test]
    fn parses_deleted_and_space_containing_paths_without_panicking() {
        let raw = "\
7f1000000000-7f1000001000 rw-s 00000000 00:01 999 /memfd:with spaces (deleted)\n\
bad line\n\
1000-1000 r--p 00000000 00:00 0 [zero]\n\
2000-3000 ---p 00000000 00:00 0\n";

        let maps = parse_proc_maps(raw);

        assert_eq!(maps.len(), 2);
        assert_eq!(
            maps[0].path.as_deref(),
            Some("/memfd:with spaces (deleted)")
        );
        assert_eq!(
            maps[0].file_path(),
            Some(PathBuf::from("/memfd:with spaces"))
        );
        assert!(maps[0].perms.shared);
        assert_eq!(maps[1].path, None);
        assert!(!maps[1].is_dump_candidate());
    }

    #[test]
    fn perms_convert_to_mmap_prot_bits() {
        let perms = KboxlikeMapPerms {
            read: true,
            write: true,
            exec: false,
            shared: false,
        };

        assert_eq!(perms.prot(), libc::PROT_READ | libc::PROT_WRITE);
    }
}
