root/usr.sbin/bluetooth/bthidd/parser.y
%{
/*
 * parser.y
 */

/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2006 Maksim Yevmenkin <m_evmenkin@yahoo.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $Id: parser.y,v 1.7 2006/09/07 21:06:53 max Exp $
 */

#include <sys/queue.h>
#define L2CAP_SOCKET_CHECKED
#include <bluetooth.h>
#include <dev/usb/usb.h>
#include <dev/usb/usbhid.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <usbhid.h>

#ifndef BTHIDCONTROL
#include <stdarg.h>
#include <syslog.h>
#define SYSLOG          syslog
#define LOGCRIT         LOG_CRIT
#define LOGERR          LOG_ERR
#define LOGWARNING      LOG_WARNING
#define EOL
#else
#define SYSLOG          fprintf
#define LOGCRIT         stderr
#define LOGERR          stderr
#define LOGWARNING      stderr
#define EOL     "\n"
#endif /* ndef BTHIDCONTROL */

#define NAMELESS_DEVICE "No Name"

#include "bthid_config.h"

        int     yylex           (void);
        void    yyerror         (char const *);
static  int32_t check_hid_device(hid_device_p hid_device);
static  void    free_hid_device (hid_device_p hid_device);

extern  FILE                    *yyin;
extern  int                      yylineno;
        char const              *config_file = BTHIDD_CONFFILE;
        char const              *hids_file   = BTHIDD_HIDSFILE;

static  char                     buffer[1024];
static  int32_t                  hid_descriptor_size;
static  hid_device_t            *hid_device = NULL;
static  LIST_HEAD(, hid_device)  hid_devices;

%}

%union {
        bdaddr_t        bdaddr;
        int32_t         num;
        char            *string;
}

%token <bdaddr> T_BDADDRSTRING
%token <num>    T_HEXBYTE
%token <num>    T_HEXWORD
%token <string> T_STRING
%token T_NAME
%token T_DEVICE T_BDADDR T_VENDOR_ID T_PRODUCT_ID T_VERSION T_CONTROL_PSM
%token T_INTERRUPT_PSM T_RECONNECT_INITIATE T_BATTERY_POWER
%token T_NORMALLY_CONNECTABLE T_HID_DESCRIPTOR
%token T_TRUE T_FALSE T_ERROR

%%

config:         line
                | config line
                ;

line:           T_DEVICE
                        {
                        hid_device = (hid_device_t *) calloc(1, sizeof(*hid_device));
                        if (hid_device == NULL) {
                                SYSLOG(LOGCRIT, "Could not allocate new " \
                                                "config entry" EOL);
                                YYABORT;
                        }

                        hid_device->new_device = 1;
                        }
                '{' options '}'
                        {
                        if (check_hid_device(hid_device))
                                LIST_INSERT_HEAD(&hid_devices,hid_device,next);
                        else
                                free_hid_device(hid_device);

                        hid_device = NULL;
                        }
                ;

options:        option ';'
                | options option ';'
                ;

option:         bdaddr
                | name
                | vendor_id
                | product_id
                | version
                | control_psm
                | interrupt_psm
                | reconnect_initiate
                | battery_power
                | normally_connectable
                | hid_descriptor
                | parser_error
                ;

bdaddr:         T_BDADDR T_BDADDRSTRING
                        {
                        memcpy(&hid_device->bdaddr, &$2, sizeof(hid_device->bdaddr));
                        }
                ;

name:           T_NAME T_STRING
                        {
                        if (hid_device->name != NULL) {
                                free(hid_device->name);
                                hid_device->name = NULL;
                        }

                        if (strcmp($2, NAMELESS_DEVICE)) {
                                hid_device->name = strdup($2);
                                if (hid_device->name == NULL) {
                                        SYSLOG(LOGCRIT, "Could not allocate new " \
                                                        "device name" EOL);
                                        YYABORT;
                                }
                        }
                        }
                ;

vendor_id:      T_VENDOR_ID T_HEXWORD
                        {
                        hid_device->vendor_id = $2;
                        }
                ;

product_id:     T_PRODUCT_ID T_HEXWORD
                        {
                        hid_device->product_id = $2;
                        }
                ;

version:        T_VERSION T_HEXWORD
                        {
                        hid_device->version = $2;
                        }
                ;

control_psm:    T_CONTROL_PSM T_HEXBYTE
                        {
                        hid_device->control_psm = $2;
                        }
                ;

interrupt_psm:  T_INTERRUPT_PSM T_HEXBYTE
                        {
                        hid_device->interrupt_psm = $2;
                        }
                ;

reconnect_initiate: T_RECONNECT_INITIATE T_TRUE
                        {
                        hid_device->reconnect_initiate = 1;
                        }
                | T_RECONNECT_INITIATE T_FALSE
                        {
                        hid_device->reconnect_initiate = 0;
                        }
                ;

battery_power:  T_BATTERY_POWER T_TRUE
                        {
                        hid_device->battery_power = 1;
                        }
                | T_BATTERY_POWER T_FALSE
                        {
                        hid_device->battery_power = 0;
                        }
                ;

normally_connectable: T_NORMALLY_CONNECTABLE T_TRUE
                        {
                        hid_device->normally_connectable = 1;
                        }
                | T_NORMALLY_CONNECTABLE T_FALSE
                        {
                        hid_device->normally_connectable = 0;
                        }
                ;

hid_descriptor: T_HID_DESCRIPTOR        
                        {
                        hid_descriptor_size = 0;
                        }
                '{' hid_descriptor_bytes '}'
                        {
                        if (hid_device->desc != NULL)
                                hid_dispose_report_desc(hid_device->desc);

                        hid_device->desc = hid_use_report_desc((unsigned char *) buffer, hid_descriptor_size);
                        if (hid_device->desc == NULL) {
                                SYSLOG(LOGCRIT, "Could not use HID descriptor" EOL);
                                YYABORT;
                        }
                        }
                ;

hid_descriptor_bytes: hid_descriptor_byte
                | hid_descriptor_bytes hid_descriptor_byte
                ;

hid_descriptor_byte: T_HEXBYTE
                        {
                        if (hid_descriptor_size >= (int32_t) sizeof(buffer)) {
                                SYSLOG(LOGCRIT, "HID descriptor is too big" EOL);
                                YYABORT;
                        }

                        buffer[hid_descriptor_size ++] = $1;
                        }
                ;

parser_error:   T_ERROR
                        {
                                YYABORT;
                        }

%%

/* Display parser error message */
void
yyerror(char const *message)
{
        SYSLOG(LOGERR, "%s in line %d" EOL, message, yylineno); 
}

/* Re-read config file */
int32_t
read_config_file(void)
{
        int32_t e;

        if (config_file == NULL) {
                SYSLOG(LOGERR, "Unknown config file name!" EOL);
                return (-1);
        }

        if ((yyin = fopen(config_file, "r")) == NULL) {
                SYSLOG(LOGERR, "Could not open config file '%s'. %s (%d)" EOL,
                                config_file, strerror(errno), errno);
                return (-1);
        }

        clean_config();
        if (yyparse() < 0) {
                SYSLOG(LOGERR, "Could not parse config file '%s'" EOL,
                                config_file);
                e = -1;
        } else
                e = 0;

        fclose(yyin);
        yyin = NULL;

        return (e);
}

/* Clean config */
void
clean_config(void)
{
        while (!LIST_EMPTY(&hid_devices)) {
                hid_device_p    d = LIST_FIRST(&hid_devices);

                LIST_REMOVE(d, next);
                free_hid_device(d);
        }
}

/* Lookup config entry */
hid_device_p
get_hid_device(bdaddr_p bdaddr)
{
        hid_device_p    d;

        LIST_FOREACH(d, &hid_devices, next)
                if (memcmp(&d->bdaddr, bdaddr, sizeof(bdaddr_t)) == 0)
                        break;

        return (d);
}

/* Get next config entry */
hid_device_p
get_next_hid_device(hid_device_p d)
{
        return ((d == NULL)? LIST_FIRST(&hid_devices) : LIST_NEXT(d, next));
}

/* Print config entry */
void
print_hid_device(hid_device_p d, FILE *f)
{
        /* XXX FIXME hack! */
        struct report_desc {
                unsigned int    size;
                unsigned char   data[1];
        };
        /* XXX FIXME hack! */

        struct report_desc      *desc = (struct report_desc *) d->desc;
        uint32_t                 i;

        fprintf(f,
"device {\n"                                    \
"       bdaddr                  %s;\n"          \
"       name                    \"%s\";\n"      \
"       vendor_id               0x%04x;\n"      \
"       product_id              0x%04x;\n"      \
"       version                 0x%04x;\n"      \
"       control_psm             0x%x;\n"        \
"       interrupt_psm           0x%x;\n"        \
"       reconnect_initiate      %s;\n"          \
"       battery_power           %s;\n"          \
"       normally_connectable    %s;\n"          \
"       hid_descriptor          {",
                bt_ntoa(&d->bdaddr, NULL),
                (d->name != NULL)? d->name : NAMELESS_DEVICE,
                d->vendor_id, d->product_id, d->version,
                d->control_psm, d->interrupt_psm,
                d->reconnect_initiate? "true" : "false",
                d->battery_power? "true" : "false",
                d->normally_connectable? "true" : "false");
 
        for (i = 0; i < desc->size; i ++) {
                        if ((i % 8) == 0)
                                fprintf(f, "\n          ");
 
                        fprintf(f, "0x%2.2x ", desc->data[i]);
        }
                
        fprintf(f,
"\n"            \
"       };\n"   \
"}\n");
}

/* Check config entry */
static int32_t
check_hid_device(hid_device_p d)
{
        hid_data_t      hd;
        hid_item_t      hi;
        int32_t         page, mdepth;

        if (get_hid_device(&d->bdaddr) != NULL) {
                SYSLOG(LOGERR, "Ignoring duplicated entry for bdaddr %s" EOL,
                                bt_ntoa(&d->bdaddr, NULL));
                return (0);
        }

        if (d->control_psm == 0) {
                SYSLOG(LOGERR, "Ignoring entry with invalid control PSM" EOL);
                return (0);
        }

        if (d->interrupt_psm == 0) {
                SYSLOG(LOGERR, "Ignoring entry with invalid interrupt PSM" EOL);
                return (0);
        }

        if (d->desc == NULL) {
                SYSLOG(LOGERR, "Ignoring entry without HID descriptor" EOL);
                return (0);
        }

        mdepth = 0;

        /* XXX somehow need to make sure descriptor is valid */
        for (hd = hid_start_parse(d->desc, ~0, -1); hid_get_item(hd, &hi) > 0; ) {
                switch (hi.kind) {
                case hid_collection:
                        if (mdepth != 0)
                                mdepth++;
                        else if (hi.collection == 1 &&
                             hi.usage ==
                              HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_MOUSE))
                                mdepth++;
                        break;
                case hid_endcollection:
                        if (mdepth != 0)
                                mdepth--;
                        break;
                case hid_output:
                case hid_feature:
                        break;

                case hid_input:
                        /* Check if the device may send keystrokes */
                        page = HID_PAGE(hi.usage);
                        if (page == HUP_KEYBOARD)
                                d->keyboard = 1;
                        if (page == HUP_CONSUMER &&
                            (hi.flags & (HIO_CONST|HIO_RELATIVE)) == 0)
                                d->has_cons = 1;
                        /* Check if the device may send relative motion events */
                        if (mdepth == 0)
                                break;
                        if (hi.usage ==
                             HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_X) &&
                            (hi.flags & (HIO_CONST|HIO_RELATIVE)) == HIO_RELATIVE)
                                d->mouse = 1;
                        if (hi.usage ==
                             HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_Y) &&
                            (hi.flags & (HIO_CONST|HIO_RELATIVE)) == HIO_RELATIVE)
                                d->mouse = 1;
                        if (hi.usage ==
                             HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_WHEEL) &&
                            (hi.flags & (HIO_CONST|HIO_RELATIVE)) == HIO_RELATIVE)
                                d->has_wheel = 1;
                        if (hi.usage ==
                            HID_USAGE2(HUP_CONSUMER, HUC_AC_PAN) &&
                            (hi.flags & (HIO_CONST|HIO_RELATIVE)) == HIO_RELATIVE)
                                d->has_hwheel = 1;
                        break;
                }
        }
        hid_end_parse(hd);

        return (1);
}

/* Free config entry */
static void
free_hid_device(hid_device_p d)
{
        if (d->desc != NULL)
                hid_dispose_report_desc(d->desc);

        free(d->name);
        memset(d, 0, sizeof(*d));
        free(d);
}

/* Re-read hids file */
int32_t
read_hids_file(void)
{
        FILE            *f;
        hid_device_t    *d;
        char            *line;
        bdaddr_t         bdaddr;
        int32_t          lineno;

        if (hids_file == NULL) {
                SYSLOG(LOGERR, "Unknown HIDs file name!" EOL);
                return (-1);
        }

        if ((f = fopen(hids_file, "r")) == NULL) {
                if (errno == ENOENT)
                        return (0);

                SYSLOG(LOGERR, "Could not open HIDs file '%s'. %s (%d)" EOL,
                        hids_file, strerror(errno), errno);
                return (-1);
        }

        for (lineno = 1; fgets(buffer, sizeof(buffer), f) != NULL; lineno ++) {
                if ((line = strtok(buffer, "\r\n\t ")) == NULL)
                        continue; /* ignore empty lines */

                if (!bt_aton(line, &bdaddr)) {
                        SYSLOG(LOGWARNING, "Ignoring unparseable BD_ADDR in " \
                                "%s:%d" EOL, hids_file, lineno);
                        continue;
                }

                if ((d = get_hid_device(&bdaddr)) != NULL)
                        d->new_device = 0;
        }

        fclose(f);

        return (0);
}

/* Write hids file */
int32_t
write_hids_file(void)
{
        char             path[PATH_MAX];
        FILE            *f;
        hid_device_t    *d;

        if (hids_file == NULL) {
                SYSLOG(LOGERR, "Unknown HIDs file name!" EOL);
                return (-1);
        }

        snprintf(path, sizeof(path), "%s.new", hids_file);

        if ((f = fopen(path, "w")) == NULL) {
                SYSLOG(LOGERR, "Could not open HIDs file '%s'. %s (%d)" EOL,
                        path, strerror(errno), errno);
                return (-1);
        }

        LIST_FOREACH(d, &hid_devices, next)
                if (!d->new_device)
                        fprintf(f, "%s\n", bt_ntoa(&d->bdaddr, NULL));

        fclose(f);

        if (rename(path, hids_file) < 0) {
                SYSLOG(LOGERR, "Could not rename new HIDs file '%s' to '%s'. " \
                        "%s (%d)" EOL, path, hids_file, strerror(errno), errno);
                unlink(path);
                return (-1);
        }

        return (0);
}