databankr.py (7267B)
1 #!/usr/bin/env python3 2 # Databankr: a CLI tool to encode arbitrary data to Casio Databank/Telememo 3 # watches and restore it from there 4 # Created by Luxferre in 2024, released into public domain 5 6 import sys, math, json, re 7 8 # universal base conversion methods 9 10 def to_base(number, base, charset): 11 if not number: 12 return charset[0] 13 res = '' 14 while number > 0: 15 res = charset[number % base] + res 16 number //= base 17 return res 18 19 def from_base(number, base, charset): 20 if number == charset[0]: 21 return 0 22 res = 0 23 for c in number: 24 ind = charset.find(c) 25 if ind > -1: 26 res = res * base + ind 27 else: 28 return 0 29 return res 30 31 # record preparation methods 32 33 # create a padded field from a numeric value 34 def val_to_field(n, charset, padlen): 35 return to_base(n, len(charset), charset).rjust(padlen, charset[0]) 36 37 # main encoding method 38 def encode(data: bytes, config): 39 effective_namelen = config['namelen'] - 1 40 alphabase = len(config['alpha']) 41 numbase = len(config['digit']) 42 indexsize = len(config['index']) 43 # calculate record estimation 44 name_part_bits = int(effective_namelen * math.log2(alphabase)) 45 number_part_bits = int(config['numberlen'] * math.log2(numbase)) 46 record_bits = name_part_bits + number_part_bits # single record capacity 47 max_bits = record_bits * indexsize # overall databank capacity 48 # start processing 49 bitstr = ''.join(f'{c:08b}' for c in data) # create an aligned bitstring 50 bitlen = len(bitstr) # overall bitstring length 51 if bitlen > max_bits: # truncate the excess 52 bitlen = max_bits 53 bitstr = bitstr[:max_bits] 54 rec_len = int(math.ceil(bitlen / record_bits)) # message length in records 55 records = [] # list of lists 56 pos = 0 # current position tracker 57 for i in range(0, rec_len): # slice over the bitstring 58 namebin = bitstr[pos:pos+name_part_bits].ljust(name_part_bits, '0') 59 pos += name_part_bits 60 numbin = bitstr[pos:pos+number_part_bits].ljust(number_part_bits, '0') 61 pos += number_part_bits 62 # now we only got binary representation of both parts 63 # let's convert them to bigints and then to the actual records 64 namefield = config['index'][i] + val_to_field(int(namebin, 2), 65 config['alpha'], effective_namelen) 66 numfield = val_to_field(int(numbin, 2), 67 config['digit'], config['numberlen']) 68 records.append([namefield, numfield]) 69 return records 70 71 # main decoding method 72 def decode(records, config, expected=0): 73 alphabase = len(config['alpha']) 74 numbase = len(config['digit']) 75 effective_namelen = config['namelen'] - 1 76 name_part_bits = int(effective_namelen * math.log2(alphabase)) 77 number_part_bits = int(config['numberlen'] * math.log2(numbase)) 78 bitstr = '' # bit string storage 79 for rec in records: # iterate over records 80 nameval = from_base(rec[0][1:], alphabase, config['alpha']) 81 numval = from_base(rec[1], numbase, config['digit']) 82 bitstr += format(nameval, '0b').zfill(name_part_bits) 83 bitstr += format(numval, '0b').zfill(number_part_bits) 84 # reconstruct the raw data from the bitstring and return it 85 datalen = int(math.ceil(len(bitstr)/8)) # estimate the data length in bytes 86 if expected > 0: # truncate if expected length is specified 87 datalen = expected 88 data = b'' # raw data placeholder 89 for i in range(0, datalen): # iterate over byte slices 90 ind = i << 3 91 data += int(bitstr[ind:ind+8].ljust(8, '0'), 2).to_bytes(1, 'big') 92 return data 93 94 def auto_int(x): # helps to convert from any base natively supported in Python 95 return int(x,0) 96 97 if __name__ == '__main__': # main app start 98 from argparse import ArgumentParser 99 parser = ArgumentParser(description='Databankr: Casio Databank/Telememo record format encoder/decoder', epilog='(c) Luxferre 2024 --- No rights reserved <https://unlicense.org>') 100 parser.add_argument('mode', help='Operation mode (enc/dec)') 101 parser.add_argument('-t', '--type', type=str, default='bin', help='Data type (bin/hex, default bin)') 102 parser.add_argument('-i', '--input-file', type=str, default='-', help='Source input file (default "-", stdin)') 103 parser.add_argument('-o', '--output-file', type=str, default='-', help='Result output file (default "-", stdout)') 104 parser.add_argument('-c', '--config', type=str, default='config.json', help='Configuration JSON file path (default config.json in current working directory)') 105 parser.add_argument('-m', '--module', type=str, default='2515-lat', help='Module configuration code according to your watch (default 2515-lat)') 106 parser.add_argument('-l', '--expected-length', type=auto_int, default=0, help='Expected decoded data length in bytes (default 0 - no limits)') 107 args = parser.parse_args() 108 109 # detect the mode 110 if args.mode == 'enc': 111 flow = 'enc' 112 elif args.mode == 'dec': 113 flow = 'dec' 114 else: 115 print('Invalid mode! Please specify enc or dec!') 116 exit(1) 117 118 # load the configuration file 119 try: 120 f = open(args.config) 121 confdata = json.load(f) 122 f.close() 123 except: 124 print('Config file missing or invalid!') 125 exit(1) 126 127 # load the module config 128 if args.module in confdata: 129 moduleconfig = confdata[args.module] 130 print('Loaded the configuration for %s' % moduleconfig['description']) 131 else: 132 print('Module configuration %s not found in the config file!' % args.module) 133 exit(1) 134 135 # load the input data 136 try: 137 if args.input_file == '-': 138 infd = sys.stdin 139 else: 140 infd = open(args.input_file, mode='rb') 141 indata = infd.read() 142 if infd == sys.stdin: # perform additional conversion 143 indata = indata.encode('utf-8') 144 else: 145 infd.close() 146 except: 147 print('Error reading the input data!') 148 print(sys.exc_info()) 149 exit(1) 150 151 # run the selected flow 152 if flow == 'enc': # encoding flow 153 if args.type == 'hex': # convert the input data if the type is hex 154 indata = bytes.fromhex(re.sub(r"[^0-9a-fA-F]", "" ,indata.decode('utf-8'))) 155 records = encode(indata, moduleconfig) 156 outdata = '' 157 for rec in records: # separate each record with double newline 158 outdata += rec[0] + '\n' + rec[1] + '\n\n' 159 else: # decoding flow 160 # parse the records 161 rawrecs = indata.decode('utf-8').split('\n\n') 162 records = [] # records will be stored here 163 for pairstr in rawrecs: 164 if len(pairstr) > 0: # exclude empty records 165 pair = pairstr.split('\n') # get raw pair and then left-adjust 166 records.append([pair[0].ljust(moduleconfig['namelen'], ' '), 167 pair[1].ljust(moduleconfig['numberlen'], ' ')]) 168 # decode the records 169 outdata = decode(records, moduleconfig, args.expected_length) 170 if args.type == 'hex': # convert the output data if the type is hex 171 outdata = outdata.hex() 172 # now, write the output file 173 try: 174 if args.output_file == '-': 175 outfd = sys.stdout 176 if type(outdata) == type(b''): 177 outdata = outdata.decode('utf-8') 178 outfd.write(outdata) 179 else: 180 outfd = open(args.output_file, mode='wb') 181 if type(outdata) == type(''): 182 outdata = outdata.encode('utf-8') 183 outfd.write(outdata) 184 if outfd != sys.stdout: 185 outfd.close() 186 except: 187 print('Error writing the output file!') 188 print(sys.exc_info()) 189 exit(1) 190 191 print('\nOperation complete')