1#!/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2018 Red Hat, Inc.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18#
19
20
21import argparse
22import os
23import re
24import sys
25import hidtools.hid
26import hidtools.hidraw
27import logging
28logging.basicConfig(format='%(levelname)s: %(name)s: %(message)s',
29                    level=logging.INFO)
30base_logger = logging.getLogger('hid')
31logger = logging.getLogger('hid.decode')
32
33
34class Oops(Exception):
35    pass
36
37
38def open_sysfs_rdesc(path):
39    logger.debug(f'Reading sysfs file {path}')
40    with open(path, 'rb') as fd:
41        data = fd.read()
42        return [hidtools.hid.ReportDescriptor.from_bytes(data)]
43
44
45def open_devnode_rdesc(path):
46    if not path.startswith('/dev/input/event'):
47        raise Oops(f'Unexpected event node: {path}')
48
49    node = path[len('/dev/input/'):]
50    # should use pyudev here, but let's keep that for later
51    sysfs = f'/sys/class/input/{node}/device/device/report_descriptor'
52
53    if not os.path.exists(sysfs):
54        raise Oops(f'Unable to find report descriptor for {path}, is this a HID device?')
55
56    return open_sysfs_rdesc(sysfs)
57
58
59def open_hidraw(path):
60    with open(path, 'rb+') as fd:
61        device = hidtools.hidraw.HidrawDevice(fd)
62        return [device.report_descriptor]
63
64
65def open_binary(path):
66    # This will misidentify a few files (e.g. UTF-16) as binary but for the
67    # inputs we need to accept it doesn't matter
68    with open(path, 'rb') as fd:
69        data = fd.read(4096)
70        if b'\0' in data:
71            logger.debug(f'{path} is a binary file')
72            return [hidtools.hid.ReportDescriptor.from_bytes(data)]
73    return None
74
75
76def interpret_file_hidrecorder(lines):
77    r_lines = [l for l in lines if l.startswith('R: ')]
78    if not r_lines:
79        return None
80
81    rdescs = []
82    for l in r_lines:
83        bytes = l[3:]  # drop R:
84        rdescs.append(hidtools.hid.ReportDescriptor.from_string(bytes))
85
86    return rdescs
87
88
89def open_report_descriptor(path):
90    abspath = os.path.abspath(path)
91    logger.debug(f'Processing {abspath}')
92
93    if os.path.isdir(abspath) or not os.path.exists(abspath):
94        raise Oops(f'Invalid path: {path}')
95
96    if re.match('/sys/.*/report_descriptor', abspath):
97        return open_sysfs_rdesc(path)
98    if re.match('/dev/input/event[0-9]+', abspath):
99        return open_devnode_rdesc(path)
100    if re.match('/dev/hidraw[0-9]+', abspath):
101        return open_hidraw(path)
102    rdesc = open_binary(path)
103    if rdesc is not None:
104        return rdesc
105
106    with open(path, 'r') as fd:
107        logger.debug(f'Opening {path} as text file')
108        lines = fd.readlines()
109        rdesc = interpret_file_hidrecorder(lines)
110        if rdesc is not None:
111            return rdesc
112
113    raise Oops(f'Unable to detect file type for {path}')
114
115
116def main(argv=sys.argv):
117    try:
118        parser = argparse.ArgumentParser(description='Decode a HID report descriptor to human-readable format ')
119        parser.add_argument('report_descriptor', help='Path to report descriptor(s)', nargs='+', type=str)
120        parser.add_argument('--output', metavar='output-file',
121                            nargs=1, default=[sys.stdout],
122                            type=argparse.FileType('w'),
123                            help='The file to record to (default: stdout)')
124        parser.add_argument('--verbose', action='store_true',
125                            default=False, help='Show debugging information')
126        args = parser.parse_args(argv[1:])
127        # argparse gives us a list size 1 for nargs 1
128        output = args.output[0]
129        if args.verbose:
130            base_logger.setLevel(logging.DEBUG)
131        for path in args.report_descriptor:
132            rdescs = open_report_descriptor(path)
133            for rdesc in rdescs:
134                rdesc.dump(output)
135                if rdesc.win8:
136                    output.write("**** win 8 certified ****\n")
137    except BrokenPipeError:
138        pass
139    except PermissionError as e:
140        print(f'{e}', file=sys.stderr)
141    except Oops as e:
142        print(f'{e}', file=sys.stderr)
143
144
145if __name__ == "__main__":
146    main()
147