1#!/usr/bin/env python 2""" 3This script extracts btsnooz content from bugreports and generates 4a valid btsnoop log file which can be viewed using standard tools 5like Wireshark. 6 7btsnooz is a custom format designed to be included in bugreports. 8It can be described as: 9 10base64 { 11 file_header 12 deflate { 13 repeated { 14 record_header 15 record_data 16 } 17 } 18} 19 20where the file_header and record_header are modified versions of 21the btsnoop headers. 22""" 23 24import base64 25import fileinput 26import struct 27import sys 28import zlib 29import subprocess 30 31# Enumeration of the values the 'type' field can take in a btsnooz 32# header. These values come from the Bluetooth stack's internal 33# representation of packet types. 34TYPE_IN_EVT = 0x10 35TYPE_IN_ACL = 0x11 36TYPE_IN_SCO = 0x12 37TYPE_IN_ISO = 0x17 38TYPE_OUT_CMD = 0x20 39TYPE_OUT_ACL = 0x21 40TYPE_OUT_SCO = 0x22 41TYPE_OUT_ISO = 0x2d 42 43 44def type_to_direction(type): 45 """ 46 Returns the inbound/outbound direction of a packet given its type. 47 0 = sent packet 48 1 = received packet 49 """ 50 if type in [TYPE_IN_EVT, TYPE_IN_ACL, TYPE_IN_SCO, TYPE_IN_ISO]: 51 return 1 52 return 0 53 54 55def type_to_hci(type): 56 """ 57 Returns the HCI type of a packet given its btsnooz type. 58 """ 59 if type == TYPE_OUT_CMD: 60 return b'\x01' 61 if type == TYPE_IN_ACL or type == TYPE_OUT_ACL: 62 return b'\x02' 63 if type == TYPE_IN_SCO or type == TYPE_OUT_SCO: 64 return b'\x03' 65 if type == TYPE_IN_EVT: 66 return b'\x04' 67 if type == TYPE_IN_ISO or type == TYPE_OUT_ISO: 68 return b'\x05' 69 raise RuntimeError("type_to_hci: unknown type (0x{:02x})".format(type)) 70 71 72def decode_snooz(snooz): 73 """ 74 Decodes all known versions of a btsnooz file into a btsnoop file. 75 """ 76 version, last_timestamp_ms = struct.unpack_from('=bQ', snooz) 77 78 if version != 1 and version != 2: 79 sys.stderr.write('Unsupported btsnooz version: %s\n' % version) 80 exit(1) 81 82 # Oddly, the file header (9 bytes) is not compressed, but the rest is. 83 decompressed = zlib.decompress(snooz[9:]) 84 85 sys.stdout.buffer.write(b'btsnoop\x00\x00\x00\x00\x01\x00\x00\x03\xea') 86 87 if version == 1: 88 decode_snooz_v1(decompressed, last_timestamp_ms) 89 elif version == 2: 90 decode_snooz_v2(decompressed, last_timestamp_ms) 91 92 93def decode_snooz_v1(decompressed, last_timestamp_ms): 94 """ 95 Decodes btsnooz v1 files into a btsnoop file. 96 """ 97 # An unfortunate consequence of the file format design: we have to do a 98 # pass of the entire file to determine the timestamp of the first packet. 99 first_timestamp_ms = last_timestamp_ms + 0x00dcddb30f2f8000 100 offset = 0 101 while offset < len(decompressed): 102 length, delta_time_ms, type = struct.unpack_from('=HIb', decompressed, offset) 103 offset += 7 + length - 1 104 first_timestamp_ms -= delta_time_ms 105 106 # Second pass does the actual writing out to stdout. 107 offset = 0 108 while offset < len(decompressed): 109 length, delta_time_ms, type = struct.unpack_from('=HIb', decompressed, offset) 110 first_timestamp_ms += delta_time_ms 111 offset += 7 112 sys.stdout.buffer.write(struct.pack('>II', length, length)) 113 sys.stdout.buffer.write(struct.pack('>II', type_to_direction(type), 0)) 114 sys.stdout.buffer.write(struct.pack('>II', (first_timestamp_ms >> 32), (first_timestamp_ms & 0xFFFFFFFF))) 115 sys.stdout.buffer.write(type_to_hci(type)) 116 sys.stdout.buffer.write(decompressed[offset:offset + length - 1]) 117 offset += length - 1 118 119 120def decode_snooz_v2(decompressed, last_timestamp_ms): 121 """ 122 Decodes btsnooz v2 files into a btsnoop file. 123 """ 124 # An unfortunate consequence of the file format design: we have to do a 125 # pass of the entire file to determine the timestamp of the first packet. 126 first_timestamp_ms = last_timestamp_ms + 0x00dcddb30f2f8000 127 offset = 0 128 while offset < len(decompressed): 129 length, packet_length, delta_time_ms, snooz_type = struct.unpack_from('=HHIb', decompressed, offset) 130 offset += 9 + length - 1 131 first_timestamp_ms -= delta_time_ms 132 133 # Second pass does the actual writing out to stdout. 134 offset = 0 135 while offset < len(decompressed): 136 length, packet_length, delta_time_ms, snooz_type = struct.unpack_from('=HHIb', decompressed, offset) 137 first_timestamp_ms += delta_time_ms 138 offset += 9 139 sys.stdout.buffer.write(struct.pack('>II', packet_length, length)) 140 sys.stdout.buffer.write(struct.pack('>II', type_to_direction(snooz_type), 0)) 141 sys.stdout.buffer.write(struct.pack('>II', (first_timestamp_ms >> 32), (first_timestamp_ms & 0xFFFFFFFF))) 142 sys.stdout.buffer.write(type_to_hci(snooz_type)) 143 sys.stdout.buffer.write(decompressed[offset:offset + length - 1]) 144 offset += length - 1 145 146 147def main(): 148 if len(sys.argv) > 2: 149 sys.stderr.write('Usage: %s [bugreport]\n' % sys.argv[0]) 150 sys.exit(1) 151 152 ## Assume the uudecoded data is being piped in 153 if not sys.stdin.isatty(): 154 base64_string = "" 155 try: 156 for line in sys.stdin.readlines(): 157 base64_string += line.strip() 158 decode_snooz(base64.standard_b64decode(base64_string)) 159 sys.exit(0) 160 except Exception as e: 161 sys.stderr.write('Failed uudecoding...ensure input is a valid uuencoded stream.\n') 162 sys.stderr.write(e) 163 sys.exit(1) 164 165 iterator = fileinput.input() 166 167 found = False 168 base64_string = "" 169 try: 170 for line in iterator: 171 if found: 172 if line.find('--- END:BTSNOOP_LOG_SUMMARY') != -1: 173 decode_snooz(base64.standard_b64decode(base64_string)) 174 sys.exit(0) 175 base64_string += line.strip() 176 177 if line.find('--- BEGIN:BTSNOOP_LOG_SUMMARY') != -1: 178 found = True 179 180 except UnicodeDecodeError: 181 ## Check if there is a BTSNOOP log uuencoded in the bugreport 182 p = subprocess.Popen(["egrep", "-a", "BTSNOOP_LOG_SUMMARY", sys.argv[1]], stdout=subprocess.PIPE) 183 p.wait() 184 185 if (p.returncode == 0): 186 sys.stderr.write('Failed to parse uuencoded btsnooz data from bugreport.\n') 187 sys.stderr.write(' Try:\n') 188 sys.stderr.write('LC_CTYPE=C sed -n "/BEGIN:BTSNOOP_LOG_SUMMARY/,/END:BTSNOOP_LOG_SUMMARY/p " ' + 189 sys.argv[1] + ' | egrep -av "BTSNOOP_LOG_SUMMARY" | ' + sys.argv[0] + ' > hci.log\n') 190 sys.exit(1) 191 192 if not found: 193 sys.stderr.write('No btsnooz section found in bugreport.\n') 194 sys.exit(1) 195 196 197if __name__ == '__main__': 198 main() 199