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