/* usbredirfilter.h usb redirection filter header

   Copyright 2012 Red Hat, Inc.

   Red Hat Authors:
   Hans de Goede <hdegoede@redhat.com>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/

#include <stdlib.h>
#include <string.h>
#include <errno.h>

#ifdef WIN32
#include "strtok_r.h"
#define strtok_r  glibc_strtok_r
#endif

#include "usbredirfilter.h"

int usbredirfilter_string_to_rules(
    const char *filter_str, const char *token_sep, const char *rule_sep,
    struct usbredirfilter_rule **rules_ret, int *rules_count_ret)
{
    char *rule, *rule_saveptr, *token, *token_saveptr, *ep;
    struct usbredirfilter_rule *rules = NULL;
    int i, rules_count, *values, ret = 0;
    char *buf = NULL;
    const char *r;

    *rules_ret = NULL;
    *rules_count_ret = 0;

    /* Figure out how much rules there are in the file, so we know how
       much memory we must allocate for the rules array.
       Note this will come up with a slightly too large number if there are
       empty rule strings in the set. */
    r = filter_str;
    rules_count = 0;
    while (r) {
        r = strchr(r, rule_sep[0]);
        if (r)
            r++;
        rules_count++;
    }

    rules = calloc(rules_count, sizeof(struct usbredirfilter_rule));
    if (!rules)
        return -ENOMEM;

    /* Make a copy since strtok mangles the string */
    buf = strdup(filter_str);
    if (!buf) {
        ret = -ENOMEM;
        goto leave;
    }

    /* And actually parse the string */
    rules_count = 0;
    rule = strtok_r(buf, rule_sep, &rule_saveptr);
    while (rule) {
        /* We treat the filter rule as an array of ints for easier parsing */
        values = (int *)&rules[rules_count];
        token = strtok_r(rule, token_sep, &token_saveptr);
        for (i = 0; i < 5 && token; i++) {
            values[i] = strtol(token, &ep, 0);
            if (*ep)
                break;
            token = strtok_r(NULL, token_sep, &token_saveptr);
        }
        if (i != 5 || token != NULL ||
                usbredirfilter_verify(&rules[rules_count], 1)) {
            ret = -EINVAL;
            goto leave;
        }
        rules_count++;
        rule = strtok_r(NULL, rule_sep, &rule_saveptr);
    }

    *rules_ret = rules;
    *rules_count_ret = rules_count;

leave:
    if (ret)
        free(rules);
    free(buf);
    return ret;
}

char *usbredirfilter_rules_to_string(const struct usbredirfilter_rule *rules,
    int rules_count, const char *token_sep, const char *rule_sep)
{
    int i;
    char *str, *p;

    if (usbredirfilter_verify(rules, rules_count))
        return NULL;

    /* We need 28 bytes per rule in the worst case */
    str = malloc(28 * rules_count + 1);
    if (!str)
        return NULL;

    p = str;
    for (i = 0; i < rules_count; i++) {
        if (rules[i].device_class != -1)
            p += sprintf(p, "0x%02x%c", rules[i].device_class, *token_sep);
        else
            p += sprintf(p, "-1%c", *token_sep);

        if (rules[i].vendor_id != -1)
            p += sprintf(p, "0x%04x%c", rules[i].vendor_id, *token_sep);
        else
            p += sprintf(p, "-1%c", *token_sep);

        if (rules[i].product_id != -1)
            p += sprintf(p, "0x%04x%c", rules[i].product_id, *token_sep);
        else
            p += sprintf(p, "-1%c", *token_sep);

        if (rules[i].device_version_bcd != -1)
            p += sprintf(p, "0x%04x%c", rules[i].device_version_bcd, *token_sep);
        else
            p += sprintf(p, "-1%c", *token_sep);

        p += sprintf(p, "%d%c", rules[i].allow ? 1:0, *rule_sep);
    }

    return str;
}

static int usbredirfilter_check1(const struct usbredirfilter_rule *rules,
    int rules_count, uint8_t device_class, uint16_t vendor_id,
    uint16_t product_id, uint16_t device_version_bcd, int default_allow)
{
    int i;

    for (i = 0; i < rules_count; i++) {
        if ((rules[i].device_class == -1 ||
                rules[i].device_class == device_class) &&
            (rules[i].vendor_id == -1 ||
                rules[i].vendor_id == vendor_id) &&
            (rules[i].product_id == -1 ||
                rules[i].product_id == product_id) &&
            (rules[i].device_version_bcd == -1 ||
                rules[i].device_version_bcd == device_version_bcd)) {
            /* Found a match ! */
            return rules[i].allow ? 0 : -EPERM;
        }
    }

    return default_allow ? 0 : -ENOENT;
}

int usbredirfilter_check(
    const struct usbredirfilter_rule *rules, int rules_count,
    uint8_t device_class, uint8_t device_subclass, uint8_t device_protocol,
    uint8_t *interface_class, uint8_t *interface_subclass,
    uint8_t *interface_protocol, int interface_count,
    uint16_t vendor_id, uint16_t product_id, uint16_t device_version_bcd,
    int flags)
{
    int i, rc;

    if (usbredirfilter_verify(rules, rules_count))
        return -EINVAL;

    /* Check the device_class */
    if (device_class != 0x00 && device_class != 0xef) {
        rc = usbredirfilter_check1(rules, rules_count, device_class,
                                   vendor_id, product_id, device_version_bcd,
                                   flags & usbredirfilter_fl_default_allow);
        if (rc)
            return rc;
    }

    /* Check the interface classes */
    for (i = 0; i < interface_count; i++) {
        if (!(flags & usbredirfilter_fl_dont_skip_non_boot_hid) &&
                interface_count > 1 && interface_class[i] == 0x03 &&
                interface_subclass[i] == 0x00 && interface_protocol[i] == 0x00)
            continue;

        rc = usbredirfilter_check1(rules, rules_count, interface_class[i],
                                   vendor_id, product_id, device_version_bcd,
                                   flags & usbredirfilter_fl_default_allow);
        if (rc)
            return rc;
    }

    return 0;
}

int usbredirfilter_verify(
    const struct usbredirfilter_rule *rules, int rules_count)
{
    int i;

    for (i = 0; i < rules_count; i++) {
        if (rules[i].device_class < -1 || rules[i].device_class > 255)
            return -EINVAL;
        if (rules[i].vendor_id < -1 || rules[i].vendor_id > 65535)
            return -EINVAL;
        if (rules[i].product_id < -1 || rules[i].product_id > 65535)
            return -EINVAL;
        if (rules[i].device_version_bcd < -1 ||
                rules[i].device_version_bcd > 65535)
            return -EINVAL;
    }
    return 0;
}

void usbredirfilter_print(
    const struct usbredirfilter_rule *rules, int rules_count, FILE *out)
{
    int i;
    char device_class[16], vendor[16], product[16], version[16];

    for (i = 0; i < rules_count; i++) {
        if (rules[i].device_class != -1)
            sprintf(device_class, " %02x", rules[i].device_class);
        else
            strcpy(device_class, "ANY");

        if (rules[i].vendor_id != -1)
            sprintf(vendor, "%04x", rules[i].vendor_id);
        else
            strcpy(vendor, " ANY");

        if (rules[i].product_id != -1)
            sprintf(product, "%04x", rules[i].product_id);
        else
            strcpy(product, " ANY");

        if (rules[i].device_version_bcd != -1)
            sprintf(version, "%2d.%02d",
                    ((rules[i].device_version_bcd & 0xf000) >> 12) * 10 +
                    ((rules[i].device_version_bcd & 0x0f00) >>  8),
                    ((rules[i].device_version_bcd & 0x00f0) >>  4) * 10 +
                    ((rules[i].device_version_bcd & 0x000f)));
        else
            strcpy(version, "  ANY");

        fprintf(out, "Class %s ID %s:%s Version %s %s\n", device_class, vendor,
                product, version, rules[i].allow ? "Allow":"Block");
    }
}
