//
// pandora: Syd's Dump Inspector & Profile Writer
// log.rs: Access log parsing.
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

#![forbid(unsafe_code)]

use std::{collections::BTreeSet, env, fmt, io::Write as IoWrite};

use console::style;
use fixedbitset::FixedBitSet;
use nix::unistd::{Gid, Uid};
use patricia_tree::StringPatriciaSet;
use serde::{
    de::{MapAccess, SeqAccess, Visitor},
    Deserialize, Deserializer,
};

use crate::{
    glob::{
        limit_dir_path, lock_wide_collapse, parent_path, reduce_eager, reduce_magic,
        resolve_tmp_comp, subst_id_with,
    },
    PandoraMap, PandoraSet,
};

// Map a Syd capability name to corresponding landlock(7) categories.
pub(crate) fn cap2lock(cap: &str) -> (&'static [&'static str], &'static [&'static str]) {
    match cap {
        "read" => (&["read"], &[]),
        "readdir" => (&["readdir"], &["readdir"]),
        "write" => (&["write", "truncate"], &[]),
        "truncate" => (&["truncate"], &[]),
        "exec" => (&["read", "exec"], &[]),
        "create" => (&[], &["create"]),
        "delete" => (&[], &["delete"]),
        "rename" => (&[], &["rename"]),
        "symlink" => (&[], &["symlink"]),
        "mkdir" => (&[], &["mkdir"]),
        "rmdir" => (&[], &["rmdir"]),
        "mkbdev" => (&[], &["mkbdev"]),
        "mkcdev" => (&[], &["mkcdev"]),
        "mkfifo" => (&[], &["mkfifo"]),
        "stat" | "walk" | "list" | "chdir" | "notify" | "readlink" => (&[], &[]),
        "utime" | "chown" | "chgrp" | "chmod" | "chattr" => (&[], &[]),
        "net/bind" => (&[], &["bind"]),
        "net/connect" => (&["connect"], &[]),
        _ => (&[], &[]),
    }
}

#[derive(Clone, Debug)]
pub(crate) enum Capability {
    One(String),
    Some(PandoraSet<String>),
}

impl PartialEq for Capability {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Capability::One(s1), Capability::One(s2)) => s1 == s2,
            (Capability::One(s1), Capability::Some(set2)) => set2.len() == 1 && set2.contains(s1),
            (Capability::Some(set1), Capability::One(s2)) => set1.len() == 1 && set1.contains(s2),
            (Capability::Some(set1), Capability::Some(set2)) => {
                set1.len() == set2.len() && set1.is_subset(set2)
            }
        }
    }
}

impl Eq for Capability {}

// A custom visitor to handle "either a String or an array of strings."
struct CapabilityVisitor;

impl<'de> Visitor<'de> for CapabilityVisitor {
    type Value = Capability;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("either a string or an array for Capability")
    }

    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(Capability::One(value.to_owned()))
    }

    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
    where
        A: SeqAccess<'de>,
    {
        let mut set = PandoraSet::default();
        while let Some(elem) = seq.next_element::<String>()? {
            set.insert(elem);
        }
        Ok(Capability::Some(set))
    }
}

impl<'de> Deserialize<'de> for Capability {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_any(CapabilityVisitor)
    }
}

#[derive(Clone, Debug)]
pub(crate) enum IoctlEntry {
    Name(String),
    Val(u64),
}

impl<'de> Deserialize<'de> for IoctlEntry {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct IoctlEntryVisitor;

        impl<'de> Visitor<'de> for IoctlEntryVisitor {
            type Value = IoctlEntry;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("integer or string")
            }

            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(IoctlEntry::Val(value))
            }

            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                if value < 0 {
                    Err(E::custom("negative integer for ioctl"))
                } else {
                    Ok(IoctlEntry::Val(value as u64))
                }
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(IoctlEntry::Name(value.to_owned()))
            }

            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(IoctlEntry::Name(value))
            }
        }

        deserializer.deserialize_any(IoctlEntryVisitor)
    }
}

#[derive(Clone, Debug)]
pub(crate) enum Access {
    Path {
        ctx: String,
        cap: Option<Capability>,
        fs: Option<String>,
        path: String,
    },
    Ioctl {
        ctx: String,
        ctl: Vec<IoctlEntry>,
    },
    InetAddr {
        ctx: String,
        cap: Option<Capability>,
        addr: String,
    },
    UnixAddr {
        ctx: String,
        cap: Option<Capability>,
        unix: String,
    },
    Run {
        cmd: String,
        argv: Vec<String>,
        time: String,
    },
    Exit {
        code: u8,
    },
    Any {
        _ctx: String,
    },
}

impl<'de> Deserialize<'de> for Access {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct AccessVisitor;

        impl<'de> Visitor<'de> for AccessVisitor {
            type Value = Access;

            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "a map matching one of the Access enum variants")
            }

            fn visit_map<M>(self, mut map: M) -> Result<Access, M::Error>
            where
                M: MapAccess<'de>,
            {
                let mut ctx: Option<String> = None;
                let mut cap: Option<Option<Capability>> = None;

                let mut path: Option<String> = None;
                let mut args: Option<Vec<u64>> = None;
                let mut addr: Option<String> = None;
                let mut unix_: Option<String> = None;

                let mut ctl: Option<Vec<IoctlEntry>> = None;
                let mut fs: Option<String> = None;
                let mut cmd: Option<String> = None;
                let mut argv: Option<Vec<String>> = None;
                let mut time_: Option<String> = None;

                let mut op: Option<String> = None;
                let mut code: Option<u8> = None;

                // Read incoming map field by field.
                while let Some(key) = map.next_key::<String>()? {
                    match key.as_str() {
                        "ctx" => {
                            if ctx.is_some() {
                                return Err(serde::de::Error::duplicate_field("ctx"));
                            }
                            ctx = map.next_value()?;
                        }
                        "cap" => {
                            if cap.is_some() {
                                return Err(serde::de::Error::duplicate_field("cap"));
                            }
                            cap = map.next_value()?;
                        }
                        "args" => {
                            if args.is_some() {
                                return Err(serde::de::Error::duplicate_field("args"));
                            }
                            args = map.next_value()?;
                        }
                        "path" => {
                            if path.is_some() {
                                return Err(serde::de::Error::duplicate_field("path"));
                            }
                            path = map.next_value()?;
                        }
                        "addr" => {
                            if addr.is_some() {
                                return Err(serde::de::Error::duplicate_field("addr"));
                            }
                            addr = map.next_value()?;
                        }
                        "unix" => {
                            if unix_.is_some() {
                                return Err(serde::de::Error::duplicate_field("unix"));
                            }
                            unix_ = map.next_value()?;
                        }
                        "ctl" => {
                            if ctl.is_some() {
                                return Err(serde::de::Error::duplicate_field("ctl"));
                            }
                            ctl = map.next_value()?;
                        }
                        "fs" => {
                            if fs.is_some() {
                                return Err(serde::de::Error::duplicate_field("fs"));
                            }
                            fs = map.next_value()?;
                        }
                        "cmd" => {
                            if cmd.is_some() {
                                return Err(serde::de::Error::duplicate_field("cmd"));
                            }
                            cmd = map.next_value()?;
                        }
                        "argv" => {
                            if argv.is_some() {
                                return Err(serde::de::Error::duplicate_field("argv"));
                            }
                            argv = map.next_value()?;
                        }
                        "time" => {
                            if time_.is_some() {
                                return Err(serde::de::Error::duplicate_field("time"));
                            }
                            time_ = map.next_value()?;
                        }
                        "op" => {
                            if op.is_some() {
                                return Err(serde::de::Error::duplicate_field("op"));
                            }
                            op = map.next_value()?;
                        }
                        "code" => {
                            if code.is_some() {
                                return Err(serde::de::Error::duplicate_field("code"));
                            }
                            code = map.next_value()?;
                        }
                        _ => {
                            // Ignore unknown fields.
                            let _ignored: serde::de::IgnoredAny = map.next_value()?;
                        }
                    }
                }

                let ctx = ctx.ok_or_else(|| serde::de::Error::missing_field("ctx"))?;
                let cap = cap.unwrap_or(None);

                if let Some(path) = path {
                    Ok(Access::Path { ctx, cap, fs, path })
                } else if let Some(ctl) = ctl {
                    Ok(Access::Ioctl { ctx, ctl })
                } else if let Some(addr) = addr {
                    Ok(Access::InetAddr { ctx, cap, addr })
                } else if let Some(unix) = unix_ {
                    Ok(Access::UnixAddr { ctx, cap, unix })
                } else if let (Some(cmd), Some(argv), Some(time)) = (cmd, argv, time_) {
                    Ok(Access::Run { cmd, argv, time })
                } else if let (Some(_op), Some(code)) = (op, code) {
                    Ok(Access::Exit { code })
                } else {
                    Ok(Access::Any { _ctx: ctx })
                }
            }
        }

        deserializer.deserialize_map(AccessVisitor)
    }
}

pub(crate) struct AccessMap {
    pub(crate) path_limit: u8,
    pub(crate) compact_at: usize,
    pub(crate) uid: String,
    pub(crate) gid: String,
    pub(crate) magic: PandoraMap<String, PandoraSet<String>>,
    pub(crate) force: PandoraSet<String>,
    pub(crate) fs_types: PandoraSet<String>,
    pub(crate) ioctl: PandoraSet<u64>,
    pub(crate) ioctl_names: StringPatriciaSet,
    pub(crate) lock_file_paths: PandoraMap<String, PandoraSet<String>>,
    pub(crate) lock_dir_paths: PandoraMap<String, PandoraSet<String>>,
    pub(crate) lock_bind_ports: FixedBitSet,
    pub(crate) lock_connect_ports: FixedBitSet,
    pub(crate) unsafe_opts: PandoraMap<String, UnsafeOptCtx>,
}

#[derive(Debug, Default)]
pub(crate) struct UnsafeOptCtx {
    pub(crate) ctx: Option<String>,
    pub(crate) msg: Option<String>,
    pub(crate) cmd: Option<String>,
    pub(crate) fns: BTreeSet<String>,
}

impl Default for AccessMap {
    fn default() -> Self {
        Self {
            path_limit: 3,
            compact_at: 65536,
            uid: Uid::current().to_string(),
            gid: Gid::current().to_string(),
            magic: PandoraMap::default(),
            force: PandoraSet::default(),
            fs_types: PandoraSet::default(),
            ioctl: PandoraSet::default(),
            ioctl_names: StringPatriciaSet::default(),
            lock_file_paths: PandoraMap::default(),
            lock_dir_paths: PandoraMap::default(),
            lock_bind_ports: FixedBitSet::with_capacity(65536),
            lock_connect_ports: FixedBitSet::with_capacity(65536),
            unsafe_opts: PandoraMap::default(),
        }
    }
}

impl AccessMap {
    fn lock_limit(&self) -> u8 {
        if self.path_limit > 0 {
            self.path_limit + 1
        } else {
            0
        }
    }

    fn compact(&mut self) {
        let magic = std::mem::take(&mut self.magic);
        let reduced = reduce_magic(magic, self.path_limit);
        self.magic = reduced;
        if self.lock_file_paths.len() >= self.compact_at {
            let paths = std::mem::take(&mut self.lock_file_paths);
            self.lock_file_paths = lock_wide_collapse(paths);
        }
        if self.lock_dir_paths.len() >= self.compact_at {
            let paths = std::mem::take(&mut self.lock_dir_paths);
            self.lock_dir_paths = lock_wide_collapse(paths);
        }
    }

    // Parse each JSON line for relevant info.
    pub(crate) fn parse_json_line(&mut self, line: &str) -> Option<Access> {
        // Be permissive and skip all characters up until first '{'.
        // This makes it easy to pipe dmesg(1) output to pandora(1).
        let line = line.trim();
        let line = {
            let start = line.find('{')?;
            &line[start..]
        };

        // Skip lines that cannot be parsed.
        // Warn about errors if PANDORA_DEBUG is set.
        static DEBUG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
        let debug = *DEBUG.get_or_init(|| env::var_os("PANDORA_DEBUG").is_some());
        let json: Access = match serde_json::from_str(line) {
            Ok(a) => a,
            Err(err) => {
                if debug {
                    let _ = wln!(
                        stderr(),
                        "{} {}",
                        style("pandora:").bold().magenta(),
                        style("skip invalid JSON!").bold().yellow()
                    );
                    let _ = wln!(
                        stderr(),
                        "\t{} {}",
                        style("LINE:").bold().cyan(),
                        style(line).bold().red()
                    );
                    let _ = wln!(
                        stderr(),
                        "\t{} {}",
                        style("ERROR:").bold().cyan(),
                        style(err.to_string()).bold().red()
                    );
                }
                return None;
            }
        };

        // Extract trace/allow_unsafe_* options from tip messages.
        if line.contains("trace/allow_unsafe_") {
            if let Ok(value) = serde_json::from_str::<serde_json::Value>(line) {
                extract_unsafe_tips(&value, &mut self.unsafe_opts);
            }
        }

        match json {
            Access::Path {
                ctx, cap, fs, path, ..
            } if ctx == "access" => {
                let mut capabilities = match cap {
                    None => return None,
                    Some(Capability::One(cap)) => {
                        let mut caps = PandoraSet::default();
                        caps.insert(cap);
                        caps
                    }
                    Some(Capability::Some(caps)) => caps,
                };

                if let Some(ref fs_type) = fs {
                    self.fs_types.insert(fs_type.clone());
                    capabilities.remove("fs");
                    if capabilities.is_empty() {
                        return None;
                    }
                }

                if capabilities.contains("exec") {
                    self.force.insert(path.clone());
                }

                // Collect landlock(7) paths.
                if path.starts_with('/') {
                    // Promote nsfs entries to parent directory.
                    let is_nsfs = fs.as_deref() == Some("nsfs");
                    for cap in &capabilities {
                        let (file_cats, dir_cats) = cap2lock(cap);
                        if !file_cats.is_empty() {
                            if is_nsfs {
                                let parent = parent_path(&path);
                                let entry = self.lock_dir_paths.entry(parent).or_default();
                                for &lcat in file_cats {
                                    entry.insert(lcat.to_string());
                                }
                            } else {
                                let key = if capabilities.contains("exec") {
                                    path.clone()
                                } else {
                                    let key =
                                        resolve_tmp_comp(&path).unwrap_or_else(|| path.clone());
                                    limit_dir_path(&key, self.lock_limit()).into_owned()
                                };
                                let entry = self.lock_file_paths.entry(key).or_default();
                                for &lcat in file_cats {
                                    entry.insert(lcat.to_string());
                                }
                            }
                        }
                        if !dir_cats.is_empty() {
                            let parent = parent_path(&path);
                            let parent = resolve_tmp_comp(&parent).unwrap_or(parent);
                            let parent = limit_dir_path(&parent, self.lock_limit()).into_owned();
                            let entry = self.lock_dir_paths.entry(parent).or_default();
                            for &lcat in dir_cats {
                                entry.insert(lcat.to_string());
                            }
                        }
                    }

                    // Landlock ioctl right is required for TTY.
                    let cap_rw = capabilities.contains("read") || capabilities.contains("write");
                    if cap_rw && (path == "/dev/tty" || path == "/dev/ptmx") {
                        self.lock_file_paths
                            .entry(path.clone())
                            .or_default()
                            .insert("ioctl".to_string());
                    } else if cap_rw && path.starts_with("/dev/pts/") {
                        self.lock_dir_paths
                            .entry("/dev/pts".to_string())
                            .or_default()
                            .insert("ioctl".to_string());
                    }
                }

                // Substitute known SYD_* values.
                let path = subst_id_with(&path, &self.uid, &self.gid);
                let key = match reduce_eager(&path, self.path_limit) {
                    Some(glob) => glob,
                    None => path,
                };
                self.magic.entry(key).or_default().extend(capabilities);
                if self.magic.len() >= self.compact_at {
                    self.compact();
                }
            }
            Access::UnixAddr { ctx, cap, unix, .. } if ctx == "access" => {
                let capabilities = match cap {
                    None => return None,
                    Some(Capability::One(cap)) => {
                        let mut caps = PandoraSet::default();
                        caps.insert(cap);
                        caps
                    }
                    Some(Capability::Some(caps)) => caps,
                };

                // Collect landlock(7) directories for UNIX domain socket binds.
                // MAKE_SOCK is dir-only so use parent directory.
                if unix.starts_with('/') && capabilities.contains("net/bind") {
                    let parent = parent_path(&unix);
                    let lock_entry = self.lock_dir_paths.entry(parent).or_default();
                    lock_entry.insert("bind".to_string());
                }

                // Collect landlock(7) paths for UNIX domain socket connects.
                // RESOLVE_UNIX is a file right so use socket path.
                if unix.starts_with('/') && capabilities.contains("net/connect") {
                    let lock_entry = self.lock_file_paths.entry(unix.clone()).or_default();
                    lock_entry.insert("connect".to_string());
                }

                let key = reduce_eager(&unix, u8::MAX).unwrap_or(unix);
                self.magic.entry(key).or_default().extend(capabilities);
                if self.magic.len() >= self.compact_at {
                    self.compact();
                }
            }
            Access::Ioctl { ctx, ctl, .. } if ctx == "access" => {
                for req in ctl {
                    match req {
                        IoctlEntry::Val(val) => {
                            self.ioctl.insert(val);
                        }
                        IoctlEntry::Name(name) => {
                            self.ioctl_names.insert(name);
                        }
                    }
                }
            }
            Access::InetAddr { ctx, cap, addr, .. } if ctx == "access" => {
                let capabilities = match cap {
                    None => return None,
                    Some(Capability::One(cap)) => {
                        let mut caps = PandoraSet::default();
                        caps.insert(cap);
                        caps
                    }
                    Some(Capability::Some(caps)) => caps,
                };

                // Collect landlock(7) bind/connect ports from IP addresses.
                //
                // Address may be in following forms:
                // 1. ip!port
                // 2. ip!port-port
                // 3. dial(3) string "net!ip!port" whose service is last.
                // 4. ICMP isn't confined by landlock(7) yet.
                let icmp = addr
                    .split('!')
                    .next()
                    .is_some_and(|net| net.to_ascii_lowercase().starts_with("icmp"));
                if let Some(port_part) = addr.rsplit('!').next().filter(|_| !icmp) {
                    if let Ok(port) = port_part.split('-').next().unwrap_or("").parse::<u16>() {
                        if capabilities.contains("net/bind") {
                            self.lock_bind_ports.insert(port as usize);
                        }
                        if capabilities.contains("net/connect") {
                            self.lock_connect_ports.insert(port as usize);
                        }
                    }
                }

                self.magic.entry(addr).or_default().extend(capabilities);
            }
            Access::Run { .. } | Access::Exit { .. } => return Some(json),
            _ => {}
        };

        None
    }
}

// Extract "trace/allow_unsafe_*" options from a parsed JSON value.
fn extract_unsafe_tips(value: &serde_json::Value, opts: &mut PandoraMap<String, UnsafeOptCtx>) {
    let map = match value.as_object() {
        Some(m) => m,
        None => return,
    };

    let needle = "trace/allow_unsafe_";
    let tip = match map.get("tip").and_then(|v| v.as_str()) {
        Some(t) if t.contains(needle) => t,
        _ => return,
    };

    // Extract option string from tip value.
    let mut pos = 0;
    while let Some(start) = tip[pos..].find(needle) {
        let abs_start = pos + start;
        let rest = &tip[abs_start..];
        let end = rest
            .find(|c: char| !c.is_alphanumeric() && c != '_' && c != ':' && c != '/')
            .unwrap_or(rest.len());
        let option = &rest[..end];
        if !option.is_empty() {
            let entry = opts.entry(option.to_string()).or_default();
            if entry.ctx.is_none() {
                entry.ctx = map.get("ctx").and_then(|v| v.as_str()).map(String::from);
            }
            if entry.msg.is_none() {
                entry.msg = map.get("msg").and_then(|v| v.as_str()).map(String::from);
            }
            if entry.cmd.is_none() {
                entry.cmd = map.get("cmd").and_then(|v| v.as_str()).map(String::from);
            }
            if let Some(path) = map.get("path").and_then(|v| v.as_str()) {
                entry.fns.insert(path.to_string());
            }
        }
        pos = abs_start + end;
    }
}

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

    fn churn(access: &mut AccessMap, n: usize) {
        for i in 0..n {
            let line = format!(
                "{{\"ctx\":\"access\",\"cap\":\"read\",\"path\":\"/tmp/work_{i:06}/data-Kf8mQ2pR\"}}"
            );
            access.parse_json_line(&line);
            let line = format!(
                "{{\"ctx\":\"access\",\"cap\":\"read\",\"path\":\"/proc/{}/status\"}}",
                i + 2
            );
            access.parse_json_line(&line);
        }
    }

    #[test]
    fn test_parse_json_line_1() {
        let mut access = AccessMap::default();
        churn(&mut access, 5000);
        assert!(access.magic.len() <= 4);
    }

    #[test]
    fn test_parse_json_line_2() {
        let mut access = AccessMap::default();
        churn(&mut access, 5000);
        assert!(access.lock_file_paths.len() <= 4);
        assert!(access.lock_dir_paths.len() <= 4);
    }

    #[test]
    fn test_parse_json_line_3() {
        let mut access = AccessMap {
            compact_at: 64,
            ..AccessMap::default()
        };
        for i in 0..5000usize {
            let line = format!(
                "{{\"ctx\":\"access\",\"cap\":[\"create\",\"delete\"],\"path\":\"/home/user/build/obj_{i:06}.o\"}}"
            );
            access.parse_json_line(&line);
        }
        assert!(access.magic.len() <= 96);
    }

    #[test]
    fn test_parse_json_line_4() {
        let mut access = AccessMap::default();
        assert!(access
            .parse_json_line("\u{e9}{\"ctx\":\"access\"}")
            .is_none());
        assert!(access
            .parse_json_line("foo={\"ctx\":\"access\",\"cap\":\"read\",\"path\":\"/e\"}")
            .is_none());
        assert!(access.magic.contains_key("/e"));
    }

    #[test]
    fn test_parse_json_line_5() {
        let mut access = AccessMap {
            compact_at: 64,
            ..AccessMap::default()
        };
        for i in 0..5000usize {
            let line = format!(
                "{{\"ctx\":\"access\",\"cap\":\"read\",\"path\":\"/usr/share/doc/pkg{i}/README\"}}"
            );
            access.parse_json_line(&line);
        }
        assert!(access.lock_file_paths.len() <= 96);
        assert!(access.magic.len() <= 96);
    }

    #[test]
    fn test_parse_json_line_6() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/connect","addr":"tcp!127.0.0.1!80"}"#;
        assert!(access.parse_json_line(line).is_none());
        let caps = access
            .magic
            .get("tcp!127.0.0.1!80")
            .expect("dial(3) address learned as a rule pattern");
        assert!(caps.contains("net/connect"));
        assert!(access.lock_connect_ports.contains(80));
    }

    #[test]
    fn test_parse_json_line_7() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/connect","addr":"127.0.0.1!80"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("127.0.0.1!80"));
        assert!(access.lock_connect_ports.contains(80));
    }

    #[test]
    fn test_parse_json_line_8() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/connect","addr":"udp!9.9.9.9!53"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("udp!9.9.9.9!53"));
        assert!(access.lock_connect_ports.contains(53));
    }

    #[test]
    fn test_parse_json_line_9() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/bind","addr":"tcp!127.0.0.1!8080"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("tcp!127.0.0.1!8080"));
        assert!(access.lock_bind_ports.contains(8080));
        assert!(!access.lock_connect_ports.contains(8080));
    }

    #[test]
    fn test_parse_json_line_10() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/connect","addr":"tcp6!::1!443"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("tcp6!::1!443"));
        assert!(access.lock_connect_ports.contains(443));
    }

    #[test]
    fn test_parse_json_line_11() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/bind","addr":"0.0.0.0!0"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("0.0.0.0!0"));
        assert!(access.lock_bind_ports.contains(0));
    }

    #[test]
    fn test_parse_json_line_12() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/connect","addr":"::1!8080"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("::1!8080"));
        assert!(access.lock_connect_ports.contains(8080));
    }

    #[test]
    fn test_parse_json_line_13() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/connect","addr":"tcp!127.0.0.1!4711"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.lock_connect_ports.contains(4711));
        assert!(!access.lock_bind_ports.contains(4711));
    }

    #[test]
    fn test_parse_json_line_14() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"block","cap":"net/connect","addr":"tcp!1.2.3.4!80"}"#;
        let _ = access.parse_json_line(line);
        assert!(!access.magic.contains_key("tcp!1.2.3.4!80"));
        assert!(!access.lock_connect_ports.contains(80));
    }

    #[test]
    fn test_parse_json_line_15() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/bind","addr":"icmp4!127.0.0.1!0"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("icmp4!127.0.0.1!0"));
        assert!(!access.lock_bind_ports.contains(0));
    }

    #[test]
    fn test_parse_json_line_16() {
        let mut access = AccessMap::default();
        let line = r#"{"ctx":"access","cap":"net/connect","addr":"icmp6!::1!0"}"#;
        assert!(access.parse_json_line(line).is_none());
        assert!(access.magic.contains_key("icmp6!::1!0"));
        assert!(!access.lock_connect_ports.contains(0));
        assert!(!access.lock_bind_ports.contains(0));
    }
}
