/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "alloc-util.h"
#include "extract-word.h"
#include "hostname-util.h"
#include "in-addr-prefix-util.h"
#include "string-util.h"

/* 0.0.0.0/0 */
#define IN_ADDR_PREFIX_IPV4_ANY ((struct in_addr_prefix) { .family = AF_INET })
/* ::/0 */
#define IN_ADDR_PREFIX_IPV6_ANY ((struct in_addr_prefix) { .family = AF_INET6 })
/* 127.0.0.0/8 */
#define IN_ADDR_PREFIX_IPV4_LOCALHOST                                   \
        ((struct in_addr_prefix) {                                      \
                .family = AF_INET,                                      \
                .address.in.s_addr = htobe32(UINT32_C(127) << 24),      \
                .prefixlen = 8,                                         \
        })
/* ::1/128 */
#define IN_ADDR_PREFIX_IPV6_LOCALHOST                                   \
        ((struct in_addr_prefix) {                                      \
                .family = AF_INET6,                                     \
                .address.in6 = IN6ADDR_LOOPBACK_INIT,                   \
                .prefixlen = 128,                                       \
        })
/* 169.254.0.0/16 */
#define IN_ADDR_PREFIX_IPV4_LINKLOCAL                                   \
        ((struct in_addr_prefix) {                                      \
                .family = AF_INET,                                      \
                .address.in.s_addr = htobe32((UINT32_C(169) << 24) |    \
                                             (UINT32_C(254) << 16)),    \
                .prefixlen = 16,                                        \
        })
/* fe80::/64 */
#define IN_ADDR_PREFIX_IPV6_LINKLOCAL                                   \
        ((struct in_addr_prefix) {                                      \
                .family = AF_INET6,                                     \
                .address.in6.s6_addr[0] = 0xfe,                         \
                .address.in6.s6_addr[1] = 0x80,                         \
                .prefixlen = 64,                                        \
        })
/* 224.0.0.0/4 */
#define IN_ADDR_PREFIX_IPV4_MULTICAST                                   \
        ((struct in_addr_prefix) {                                      \
                .family = AF_INET,                                      \
                .address.in.s_addr = htobe32((UINT32_C(224) << 24)),    \
                .prefixlen = 4,                                         \
        })
/* ff00::/8 */
#define IN_ADDR_PREFIX_IPV6_MULTICAST                                   \
        ((struct in_addr_prefix) {                                      \
                .family = AF_INET6,                                     \
                .address.in6.s6_addr[0] = 0xff,                         \
                .prefixlen = 8,                                         \
        })

static void in_addr_prefix_hash_func(const struct in_addr_prefix *a, struct siphash *state) {
        assert(a);
        assert(state);

        siphash24_compress(&a->family, sizeof(a->family), state);
        siphash24_compress(&a->prefixlen, sizeof(a->prefixlen), state);
        siphash24_compress(&a->address, FAMILY_ADDRESS_SIZE(a->family), state);
}

static int in_addr_prefix_compare_func(const struct in_addr_prefix *x, const struct in_addr_prefix *y) {
        int r;

        assert(x);
        assert(y);

        r = CMP(x->family, y->family);
        if (r != 0)
                return r;

        r = CMP(x->prefixlen, y->prefixlen);
        if (r != 0)
                return r;

        return memcmp(&x->address, &y->address, FAMILY_ADDRESS_SIZE(x->family));
}

DEFINE_HASH_OPS(in_addr_prefix_hash_ops, struct in_addr_prefix, in_addr_prefix_hash_func, in_addr_prefix_compare_func);
DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(in_addr_prefix_hash_ops_free, struct in_addr_prefix, in_addr_prefix_hash_func, in_addr_prefix_compare_func, free);

int in_addr_prefix_add(Set **prefixes, const struct in_addr_prefix *prefix) {
        struct in_addr_prefix *copy;

        assert(prefixes);
        assert(prefix);
        assert(IN_SET(prefix->family, AF_INET, AF_INET6));

        copy = newdup(struct in_addr_prefix, prefix, 1);
        if (!copy)
                return -ENOMEM;

        (void) in_addr_mask(copy->family, &copy->address, copy->prefixlen);
        return set_ensure_consume(prefixes, &in_addr_prefix_hash_ops_free, copy);
}

int in_addr_prefixes_reduce(Set *prefixes) {
        uint32_t ipv4_prefixlen_bits = 0;
        uint64_t ipv6_prefixlen_bits[128 / sizeof(uint64_t)] = {};
        uint8_t ipv4_prefixlens[32] = {}, ipv6_prefixlens[128] = {};
        bool ipv4_has_any = false, ipv6_has_any = false;
        size_t ipv4_n_prefixlens = 0, ipv6_n_prefixlens = 0;
        struct in_addr_prefix *p;

        SET_FOREACH(p, prefixes)
                switch(p->family) {
                case AF_INET:
                        assert(p->prefixlen <= 32);
                        if (p->prefixlen == 0)
                                ipv4_has_any = true;
                        else
                                ipv4_prefixlen_bits |= UINT32_C(1) << (p->prefixlen - 1);
                        break;
                case AF_INET6:
                        assert(p->prefixlen <= 128);
                        if (p->prefixlen == 0)
                                ipv6_has_any = true;
                        else
                                ipv6_prefixlen_bits[(p->prefixlen - 1) / sizeof(uint64_t)] |=
                                        UINT64_C(1) << ((p->prefixlen - 1) % sizeof(uint64_t));
                        break;
                default:
                        assert_not_reached();
                }

        if (!ipv4_has_any)
                for (size_t i = 0; i < 32; i++)
                        if (ipv4_prefixlen_bits & (UINT32_C(1) << i))
                                ipv4_prefixlens[ipv4_n_prefixlens++] = i + 1;

        if (!ipv6_has_any)
                for (size_t i = 0; i < 128; i++)
                        if (ipv6_prefixlen_bits[i / sizeof(uint64_t)] &
                            (UINT64_C(1) << (i % sizeof(uint64_t))))
                                ipv6_prefixlens[ipv6_n_prefixlens++] = i + 1;

        SET_FOREACH(p, prefixes) {
                uint8_t *prefixlens;
                bool covered;
                size_t *n;

                if (p->prefixlen == 0)
                        continue;

                switch(p->family) {
                case AF_INET:
                        prefixlens = ipv4_prefixlens;
                        n = &ipv4_n_prefixlens;
                        covered = ipv4_has_any;
                        break;
                case AF_INET6:
                        prefixlens = ipv6_prefixlens;
                        n = &ipv6_n_prefixlens;
                        covered = ipv6_has_any;
                        break;
                default:
                        assert_not_reached();
                }

                for (size_t i = 0; i < *n; i++) {
                        struct in_addr_prefix tmp;

                        if (covered)
                                break;

                        if (prefixlens[i] >= p->prefixlen)
                                break;

                        tmp = *p;
                        tmp.prefixlen = prefixlens[i];
                        (void) in_addr_mask(tmp.family, &tmp.address, tmp.prefixlen);

                        covered = set_contains(prefixes, &tmp);
                }

                if (covered)
                        free(set_remove(prefixes, p));
        }

        return 0;
}

int in_addr_prefixes_merge(Set **dest, Set *src) {
        struct in_addr_prefix *p;
        int r;

        assert(dest);

        SET_FOREACH(p, src) {
                r = in_addr_prefix_add(dest, p);
                if (r < 0)
                        return r;
        }

        return 0;
}

bool in_addr_prefixes_is_any(Set *prefixes) {
        return
                set_contains(prefixes, &IN_ADDR_PREFIX_IPV4_ANY) &&
                set_contains(prefixes, &IN_ADDR_PREFIX_IPV6_ANY);
}

int config_parse_in_addr_prefixes(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        Set **prefixes = data;
        int r;

        assert(prefixes);
        assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6));

        if (isempty(rvalue)) {
                *prefixes = set_free(*prefixes);
                return 0;
        }

        for (const char *p = rvalue;;) {
                _cleanup_free_ char *word = NULL;

                r = extract_first_word(&p, &word, NULL, 0);
                if (r == -ENOMEM)
                        return log_oom();
                if (r < 0) {
                        log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
                        return 0;
                }
                if (r == 0)
                        return 0;

                if (streq(word, "any")) {
                        /* "any" is a shortcut for 0.0.0.0/0 and ::/0 */

                        if (ltype != AF_INET6) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV4_ANY);
                                if (r < 0)
                                        return log_oom();
                        }

                        if (ltype != AF_INET) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV6_ANY);
                                if (r < 0)
                                        return log_oom();
                        }

                } else if (is_localhost(word)) {
                        /* "localhost" is a shortcut for 127.0.0.0/8 and ::1/128 */

                        if (ltype != AF_INET6) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV4_LOCALHOST);
                                if (r < 0)
                                        return log_oom();
                        }

                        if (ltype != AF_INET) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV6_LOCALHOST);
                                if (r < 0)
                                        return log_oom();
                        }

                } else if (streq(word, "link-local")) {
                        /* "link-local" is a shortcut for 169.254.0.0/16 and fe80::/64 */

                        if (ltype != AF_INET6) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV4_LINKLOCAL);
                                if (r < 0)
                                        return log_oom();
                        }

                        if (ltype != AF_INET) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV6_LINKLOCAL);
                                if (r < 0)
                                        return log_oom();
                        }

                } else if (streq(word, "multicast")) {
                        /* "multicast" is a shortcut for 224.0.0.0/4 and ff00::/8 */

                        if (ltype != AF_INET6) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV4_MULTICAST);
                                if (r < 0)
                                        return log_oom();
                        }

                        if (ltype != AF_INET) {
                                r = in_addr_prefix_add(prefixes, &IN_ADDR_PREFIX_IPV6_MULTICAST);
                                if (r < 0)
                                        return log_oom();
                        }

                } else {
                        struct in_addr_prefix a;

                        if (ltype == AF_UNSPEC)
                                r = in_addr_prefix_from_string_auto(word, &a.family, &a.address, &a.prefixlen);
                        else {
                                a.family = ltype;
                                r = in_addr_prefix_from_string(word, a.family, &a.address, &a.prefixlen);
                        }
                        if (r < 0) {
                                log_syntax(unit, LOG_WARNING, filename, line, r,
                                           "Address prefix is invalid, ignoring assignment: %s", word);
                                continue;
                        }

                        r = in_addr_prefix_add(prefixes, &a);
                        if (r < 0)
                                return log_oom();
                }
        }
}
