├── libpebble ├── __init__.py ├── .gitignore ├── stm32_crc.py └── README.md ├── .gitignore ├── libpatcher ├── __init__.py ├── tests │ ├── test_mask.py │ ├── test_parser.py │ ├── test.pbp │ └── test_asm.py ├── ranges.py ├── block.py ├── patch.py ├── mask.py ├── parser.py └── asm.py ├── findlog.sh ├── generate_c_byte_array.py ├── calculateCrc.py ├── updatefw.sh ├── getver.sh ├── 29to30.sh ├── fonts.txt ├── img2c.py ├── pbpack.py ├── README.md ├── packResources.sh ├── lib2idc.py ├── findrefs.py ├── showimg.py ├── downloadFirmware.py ├── mkfonts.sh ├── patcher.py ├── prepareResourceProject.py ├── extract_codepoints.py ├── unpackFirmware.py ├── repackFirmware.py ├── fontgen.py ├── LICENSE └── translate.py /libpebble/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | *.pyc 3 | .*.swp 4 | -------------------------------------------------------------------------------- /libpatcher/__init__.py: -------------------------------------------------------------------------------- 1 | from .parser import * 2 | from .asm import * 3 | from .block import * 4 | from .mask import * 5 | from .patch import * 6 | from .ranges import * 7 | -------------------------------------------------------------------------------- /findlog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z $1 ]; then 3 | echo "Usage: $0 hex_hash" 4 | exit 1 5 | fi 6 | # get last 4 characters 7 | wcalc 0x${1: -4} | sed 's/^[^0-9]*/\\"/;s/$/\\" loghash_dict.json/' | xargs grep 8 | -------------------------------------------------------------------------------- /libpatcher/tests/test_mask.py: -------------------------------------------------------------------------------- 1 | from libpatcher.mask import Mask, MaskNotFoundError 2 | from nose.tools import eq_, raises 3 | 4 | ma = Mask([b'hello', 3, b'world']) 5 | da1 = b'hello!!!world' 6 | da2 = b'hello_world' 7 | 8 | def test_maskA_match(): 9 | eq_(ma.match(da1), 0) 10 | 11 | @raises(MaskNotFoundError) 12 | def test_maskA_not_match(): 13 | ma.match(da2) 14 | -------------------------------------------------------------------------------- /libpatcher/tests/test_parser.py: -------------------------------------------------------------------------------- 1 | from libpatcher.parser import parseFile 2 | from libpatcher.patch import Patch 3 | from pprint import pprint 4 | 5 | def test_file(): 6 | try: 7 | f = open('tests/test.pbp') 8 | except: 9 | f = open('libpatcher/tests/test.pbp') 10 | patch = parseFile(f, libpatch=Patch('library', binary=b'bin')) 11 | print(patch) 12 | pprint(patch.blocks) 13 | -------------------------------------------------------------------------------- /generate_c_byte_array.py: -------------------------------------------------------------------------------- 1 | 2 | def write(output_file, bytes, var_name): 3 | output_file.write("static const uint8_t {var_name}[] = {{\n ".format(var_name=var_name)) 4 | for byte, index in zip(bytes, xrange(0, len(bytes))): 5 | if index != 0 and index % 16 == 0: 6 | output_file.write("/* bytes {0} - {1} */\n ".format(index - 16, index)) 7 | output_file.write("0x%02x, " % ord(byte)) 8 | output_file.write("\n};\n") 9 | -------------------------------------------------------------------------------- /libpebble/.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /calculateCrc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from libpebble.stm32_crc import crc32 5 | 6 | if len(sys.argv) <= 1 or sys.argv[1] == '-h': 7 | print 'calculateCrc.py calculates STM32 CRC sum for a given file or stdin' 8 | print 'Usage: calculateCrc.py filename' 9 | print 'Use `-\' as filename to read from stdin.' 10 | exit() 11 | 12 | filename = sys.argv[1] 13 | f = sys.stdin if filename == '-' else open(filename, 'rb') 14 | c = crc32(f.read()) 15 | print 'Checksum for %s:' % (filename if filename!='-' else '') 16 | print 'Hex: 0x%08X\nDec: %d' % (c, c) 17 | -------------------------------------------------------------------------------- /libpatcher/tests/test.pbp: -------------------------------------------------------------------------------- 1 | ; this is a test patch 2 | 3 | #define hello world 4 | #ifndef hello 5 | "NoH" 6 | #else 7 | "H" 8 | #ifval world 9 | "W" 10 | #else 11 | "NoW" 12 | #endif 13 | #endif 14 | 15 | 00 bf 00 bf @ "String" ?3 ? ? 00 14 16 | 13 13 { 17 | ADD R0, 4 18 | } 19 | 20 | ;#include test.pbp 21 | 22 | 20 47 93 e8 23 | { 24 | DCB "Hello" 'world' 0 0x4 25 | DCW 1579 26 | DCD 0x10034 27 | NOP 28 | home: 29 | B.W nowhere 30 | nowhere: BL werewerewe 31 | BCC home 32 | CBZ R4, home 33 | ALIGN 4 34 | B none 35 | MOV R0, 3*4 36 | ; it should warn if block is unclosed 37 | } 38 | -------------------------------------------------------------------------------- /updatefw.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Usage: 3 | # updatefw.sh [filename.pbz] 4 | # filename defaults to patched.pbz 5 | 6 | origfile=${1:-patched.pbz} 7 | ext=${origfile##*.} 8 | 9 | # we randomize filename to make sure caching will not interfere 10 | filename=patched-$RANDOM.$ext 11 | 12 | # remove old files... 13 | adb shell rm '/sdcard/patched-*.'$ext 14 | 15 | # push new one 16 | if ! adb push "$origfile" /sdcard/$filename; then 17 | echo "Failed to push file to the phone. Is ADB connected?" 18 | exit 1 19 | fi 20 | 21 | # and show confirmation dialog 22 | adb shell am start \ 23 | -n com.getpebble.android.basalt/com.getpebble.android.main.activity.MainActivity \ 24 | -a android.intent.action.VIEW \ 25 | -d file:///sdcard/$filename 26 | 27 | echo 28 | echo "Now please confirm firmware installation on your phone" 29 | -------------------------------------------------------------------------------- /libpebble/stm32_crc.py: -------------------------------------------------------------------------------- 1 | import array 2 | import sys 3 | 4 | CRC_POLY = 0x04C11DB7 5 | 6 | def process_word(data, crc=0xffffffff): 7 | if (len(data) < 4): 8 | d_array = array.array('B', data) 9 | for x in range(0, 4 - len(data)): 10 | d_array.insert(0,0) 11 | d_array.reverse() 12 | data = d_array.tostring() 13 | 14 | d = array.array('I', data)[0] 15 | crc = crc ^ d 16 | 17 | for i in range(0, 32): 18 | if (crc & 0x80000000) != 0: 19 | crc = (crc << 1) ^ CRC_POLY 20 | else: 21 | crc = (crc << 1) 22 | 23 | result = crc & 0xffffffff 24 | return result 25 | 26 | def process_buffer(buf, c = 0xffffffff): 27 | word_count = len(buf) // 4 28 | if (len(buf) % 4 != 0): 29 | word_count += 1 30 | 31 | crc = c 32 | for i in range(0, word_count): 33 | crc = process_word(buf[i * 4 : (i + 1) * 4], crc) 34 | return crc 35 | 36 | def crc32(data): 37 | return process_buffer(data) 38 | -------------------------------------------------------------------------------- /getver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Latest channels: 4 | # http://pebblefw.s3.amazonaws.com/pebble/ev2_4/release-v2/latest.json 5 | # http://pebblefw.s3.amazonaws.com/pebble/ev2_4/beta/latest.json 6 | 7 | ver=$1 8 | short=$2 9 | if [[ "$ver" =~ .*/.* ]]; then 10 | ver_s=${ver%/*} 11 | ver=${ver#*/} 12 | elif [[ "$ver" =~ 3\.[7891].* ]]; then 13 | ver_s=${ver_s:-3.8} 14 | else 15 | ver_s=${ver_s:-$(echo ${ver:-0.0} | sed 's/\..*//')} 16 | fi 17 | channel=${4:-release-v${ver_s}} 18 | [[ $ver == *beta* ]] && channel=beta 19 | if [ -z "$short" ]; then 20 | echo "Usage: $0 [channel/]version shorthand ['hw_versions' [platform]" 21 | echo "Example: $0 2.2 v220" 22 | echo "Example: $0 2.9-beta5 v29b5" 23 | echo "Example: $0 3/3.6 v360" 24 | echo "Example: $0 3.8/3.10.1 v3A1" 25 | exit 1 26 | fi 27 | 28 | #hardwares=${3:-ev2_4 v1_5 v2_0} 29 | hardwares=${3:-snowy_dvt snowy_s3 spalding ev2_4 v1_5 v2_0} 30 | 31 | for hw in $hardwares; do 32 | echo "Downloading version $ver for HW $hw" 33 | mkdir $short-$hw 34 | cd $short-$hw 35 | outfile="Pebble-$ver-${hw}.pbz" 36 | [ -e "$outfile" ] && rm "$outfile" 37 | wget "https://pebblefw.s3.amazonaws.com/pebble/$hw/$channel/pbz/Pebble-$ver-$hw.pbz" || exit 1 38 | unzip "$outfile" 39 | cd .. 40 | done 41 | echo "Done." 42 | -------------------------------------------------------------------------------- /29to30.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # You need to have Pebble SDK 3.x installed for resource repacking. 3 | 4 | fw29="$1" 5 | fw30="$2" 6 | fwout="$3" 7 | 8 | if [ -z "fw29" ]; then 9 | echo "Usage: $0 fw29 fw30 fwOut" 10 | echo "fw29 is localized version of 2.9.1 firmware from PebbleBits," 11 | echo "fw30 is stock or any other 3.0 firmware," 12 | echo "and fwOut is desired name of output file." 13 | exit 1 14 | fi 15 | 16 | wd2=$(mktemp -d) 17 | wd3=$(mktemp -d) 18 | sp=$(dirname "$0") 19 | if [ ${sp:0:1} != "/" ]; then 20 | # if was relative path, convert to absolute 21 | sp=$(pwd)/"$sp" 22 | fi 23 | 24 | # unpack 2.9 fw 25 | "$sp"/unpackFirmware.py "$fw29" "$wd2/" 26 | # unpack 3.0 fw 27 | "$sp"/unpackFirmware.py "$fw30" "$wd3/" 28 | 29 | # replace resources in 3.0 30 | while read name id2 id3; do 31 | rm "$wd3"/res/0"$id3"* 32 | cp "$wd2"/res/0"$id2"* "$wd3"/res/0"$id3"_"$name" 33 | done < "$outfile" 79 | rm "$outfile".manifest "$outfile".table "$outfile".content > /dev/null 80 | echo "Bundle with $# resources saved to $outfile" 81 | -------------------------------------------------------------------------------- /lib2idc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from struct import unpack 5 | 6 | if len(sys.argv) < 3: 7 | print("Usage:") 8 | print("lib2idc.py libpebble.a (pbl_table-offset-in-fw)") 9 | sys.exit(1) 10 | 11 | szLibfile, pbl_table = sys.argv[1:] 12 | 13 | f = open(szLibfile, "rb") 14 | 15 | # validate file type 16 | header = f.read(8) 17 | if header != b'!\n': 18 | print("This doesn\'t look like a proper .a file") 19 | sys.exit(1) 20 | 21 | # find names start - in pbl libraries it starts with accel_, 22 | # unless they make a breaking change again (like 2 to 3) 23 | f.seek(0x48) # skip header stuff 24 | while True: 25 | val = f.read(4) 26 | if not val: 27 | print("Could not find names section, but reached EOF") 28 | sys.exit(1) 29 | if val != b'\x00\x00F\x98' and b'a' in val: 30 | right_apos = len(val) - val.index(b'a') 31 | f.seek(f.tell() - right_apos) 32 | break 33 | 34 | bNames = f.read() 35 | names = [] 36 | first = "" 37 | alphabet = b'_' + bytes(range(ord('a'), ord('z')+1)) 38 | for s in bNames.split(b'\0'): 39 | if s == first or len(s) == 0 or not (s[0] in alphabet): 40 | # if repeating or empty or doesn't look like function name 41 | break 42 | s = s.decode() 43 | names.append(s) 44 | if first == "": 45 | first = s 46 | 47 | # now let's find funcs 48 | f.seek(0) 49 | while True: 50 | val = f.read(4) 51 | if not val: 52 | print("Could not find funcs section, but reached EOF") 53 | sys.exit(1) 54 | if val == b'\xA8\xA8\xA8\xA8': 55 | # found 56 | break 57 | 58 | funcs = [] 59 | addrs = [] 60 | for i in range(len(names)): 61 | proc = f.read(12) 62 | if len(proc) < 12: # end of file reached 63 | break 64 | addr = unpack("") 69 | print() 70 | print("static main(void) {") 71 | print("\tauto pbl_table = %s;" % pbl_table) 72 | for f in funcs: 73 | print('\tMakeUnkn(Dword(pbl_table+0x%08X)-1, DOUNK_DELNAMES);' % f[0]) 74 | print('\tMakeName(Dword(pbl_table+0x%08X)-1, "%s");' % f) 75 | print('\tMakeFunction(Dword(pbl_table+0x%08X)-1, BADADDR);' % f[0]) 76 | print("\t// Now mark these offsets (including blanks)") 77 | for i in range(0, addrs[-1]+4, 4): 78 | print("\tOpOff(pbl_table+0x%08X, 0, 0);" % i) 79 | print("}") 80 | -------------------------------------------------------------------------------- /findrefs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # 3 | # Finds all references to given address 4 | # Either plain or procedure calls 5 | 6 | import sys 7 | from struct import pack 8 | 9 | def genCode(pos, to, is_bl): 10 | """ 11 | Returns assembler code for B.W or BL instruction 12 | placed at [pos], which refers to [to]. 13 | """ 14 | offset = to - (pos+4) 15 | offset = offset >> 1 16 | if abs(offset) >= 1<<22: 17 | #print ("Offset %X exceeds maximum of %X!" % 18 | # (offset, 1<<22)) 19 | return '' # we don't need exception here, 20 | # just return empty string which will not match anything 21 | raise ValueError("Offset %X exceeds maximum of %X!" % 22 | (offset, 1<<22)) 23 | hi_o = (offset >> 11) & 0b11111111111 24 | lo_o = (offset >> 0) & 0b11111111111 25 | hi_c = 0b11110 26 | lo_c = 0b11111 if is_bl else 0b10111 27 | hi = (hi_c << 11) + hi_o 28 | lo = (lo_c << 11) + lo_o 29 | code = pack(' 3: 48 | base = int(sys.argv[2], 0) # from hex 49 | val = sys.argv[3] 50 | else: 51 | val = sys.argv[2] 52 | 53 | val = int(val, 0) 54 | sval = pack('I', val) 55 | data = open(tintin, "rb").read() 56 | 57 | for i in range(0, len(data)-3, 2): 58 | for ix in (i, i+1) if i= width: 17 | break 18 | sys.stdout.write( PX[(p >> i) & 1] ) 19 | ptr+=1 20 | print "." 21 | print 22 | 23 | def calcHeightBySize(rowsize, datasize): 24 | return datasize/rowsize 25 | 26 | def parse_args(): 27 | import argparse 28 | parser = argparse.ArgumentParser( 29 | description = "This script displays images found in Pebble firmware or resource files") 30 | parser.add_argument("input", nargs='?', default="tintin_fw.bin", type=argparse.FileType("rb"), 31 | help="Input file, defaults to tintin_fw.bin") 32 | parser.add_argument("-o", "--offset", type=lambda(n): int(n, 0), 33 | help="Offset to beginning of image. If omitted, image will be treated as resource-type file") 34 | parser.add_argument("-i", "--invert", action="store_true", 35 | help="Inverse display (white/black)") 36 | parser.add_argument("-b", "--base", type=lambda n: int(n,0), default=0x8004000, 37 | help="Base address of firmware file " 38 | "(0x8010000 for v2, 0x8004000 for v3)") 39 | return parser.parse_args() 40 | 41 | if __name__ == "__main__": 42 | args = parse_args() 43 | f = args.input 44 | if args.offset: 45 | print args.offset 46 | if args.offset > args.base: 47 | args.offset -= args.base # convert from memory address to file offset 48 | f.read(args.offset) 49 | s = f.read(4) 50 | ofs = unpack('I', s)[0] 51 | if args.base < ofs and ofs < 0x80fffff: # memory offset 52 | rowsize = unpack('H', f.read(2))[0] 53 | f.read(2) # unknown field = 0x1000 54 | else: # resource file 55 | ofs = None 56 | rowsize = unpack('H', s[:2])[0] 57 | f.read(4) # unknown field 58 | width, height = unpack('HH', f.read(4)) 59 | if ofs: 60 | f.seek(ofs-args.base) 61 | data = f.read() 62 | showImage(data, rowsize, width, height, args.invert) # or: calcHeightBySize(rowsize, len(data)) 63 | -------------------------------------------------------------------------------- /libpatcher/ranges.py: -------------------------------------------------------------------------------- 1 | from .mask import MaskNotFoundError 2 | 3 | class RangeError(MaskNotFoundError): 4 | """ 5 | This is raised when no available range found 6 | """ 7 | 8 | class Ranges(object): 9 | """ 10 | This class represents collection of Ranges to use 11 | """ 12 | def __init__(self): 13 | self._ranges = [] 14 | self._remainder = None 15 | self._used = False 16 | def __repr__(self): 17 | return "Ranges: %s" % repr(self._ranges) 18 | 19 | def add(self, f, t): 20 | """ 21 | Adds a range to the collection 22 | """ 23 | if f > t: 24 | raise ValueError("Illegal range, from>to: %d,%d" % (f,t)) 25 | 26 | if f == t: 27 | return # empty range - ignore 28 | 29 | for r in list(self._ranges): # copy list to prevent remove() influence iterator 30 | if r[0] == r[1]: # collapsed range 31 | self._ranges.remove(r) 32 | continue 33 | if r[0] == f and r[1] == t: # duplicate range 34 | raise AssertionError("Duplicate range %d-%d" % (f,t)) 35 | if ((f <= r[0] and t > r[0]) or 36 | (f < r[1] and t >= r[1])): 37 | raise AssertionError("Range clash: %d-%d clashes with %d-%d" % 38 | (f,t, r[0],r[1])) 39 | for r in self._ranges: # now when we definitely have no clashes 40 | if r[1] == f: # append 41 | r[1] = t 42 | return 43 | if t == r[0]: # append 44 | r[0] = f 45 | return 46 | 47 | # this is completely new range 48 | self._ranges.append([f,t]) 49 | def add_eof(self, binary, maxbin, retain): 50 | """ Add end-of-file range, if applicable """ 51 | if len(binary) >= maxbin-retain: 52 | print("Will not append anything because binary is too large: " 53 | "%x > %x-%x" % (len(binary), maxbin, retain)) 54 | return 55 | self._remainder = binary[-retain:] 56 | self.add(len(binary), maxbin-retain) 57 | def restore_tail(self, binary): 58 | """ Restore file's ending bytes, if EOF range was used """ 59 | if self._remainder and self._used: 60 | return binary + self._remainder 61 | else: 62 | return binary 63 | 64 | def find(self, size, aligned=2): 65 | """ 66 | Returns the best matching range for block of given size, 67 | and excludes returned range from collection. 68 | Will align block to 2 by default; 69 | to get block for unaligned data, 70 | pass aligned=0 71 | :returns [from, to] 72 | """ 73 | self._used = True # for restore_tail 74 | for r in sorted(self._ranges, key=lambda r: r[1]-r[0]): # sort by size, ascending 75 | alshift = 0 76 | if aligned: 77 | alshift = (aligned-1) - ((r[0]+aligned-1) % aligned) 78 | if r[1]-r[0] >= size+alshift: 79 | ret = [r[0]+alshift,r[1]] # copy range 80 | r[0] += size+alshift # and reduce it 81 | return ret 82 | raise RangeError("No suitable range for %d bytes (align: %d)" % (size,aligned)) 83 | -------------------------------------------------------------------------------- /libpatcher/block.py: -------------------------------------------------------------------------------- 1 | # This module holds Block class 2 | from .asm import LabelInstruction 3 | from .patch import PatchingError 4 | 5 | class Block(object): 6 | def __init__(self, patch, mask, instructions): 7 | self.patch = patch 8 | self._mask = mask 9 | self.instructions = instructions 10 | self._context = {} 11 | self.position = None # to cache mask.match() result 12 | def __repr__(self): 13 | name="" 14 | if len(self.instructions) > 0: 15 | instr = self.instructions[0] 16 | if isinstance(instr, LabelInstruction) and instr.glob: 17 | name = " "+instr.name 18 | content = '\n'.join([repr(x) for x in self.instructions]) 19 | if self.mask: 20 | return "<<>>" % (name, repr(self.mask), content) 21 | else: 22 | return "<<>>" % (name, content) 23 | @property 24 | def context(self): 25 | " Block-local context dictionary " 26 | return self._context 27 | @property 28 | def mask(self): 29 | return self._mask 30 | def getSize(self): 31 | " Returns overall size of block's instructions " 32 | # FIXME: will this work before binding? 33 | # Replace with maxsize? 34 | return sum([i.getSize() for i in self.instructions]) 35 | def getPosition(self, binary=None, ranges=None): 36 | """ 37 | Returns position of this block's mask in given binary file. 38 | Will cache its result. 39 | """ 40 | if self.position == None: 41 | # if position was not calculated yet 42 | if self.mask.floating: 43 | if ranges == None: 44 | raise ValueError("No ranges provided for floating block") 45 | r = ranges.find(self.getSize()) 46 | self.position = r[0] 47 | self.mask.size = r[1]-r[0] 48 | else: 49 | if binary is None: 50 | raise ValueError("No saved position and binary not provided") 51 | self.position = self.mask.match(binary) 52 | return self.position 53 | def bind(self, addr, codebase): 54 | """ 55 | This method is called once after construction. 56 | It binds block to specific memory address 57 | (which is determined with mask.match or is obtained from ranges) 58 | """ 59 | self.addr = addr 60 | self.codebase = codebase # for instructions which may need it 61 | for i in self.instructions: 62 | i.setAddr(addr) 63 | addr += i.getSize() 64 | i.setBlock(self) # it may in return update our context, so call after setAddr 65 | def getCode(self): 66 | """ 67 | Calculstes and returns binary code of this whole block. 68 | """ 69 | code = b"" 70 | for i in self.instructions: 71 | try: 72 | icode = i.getCode() 73 | except Exception as e: 74 | raise PatchingError("Block %s, instruction %s" % (self.mask, i), e) 75 | if len(icode) != i.getSize(): 76 | raise AssertionError("Internal check failed: instruction length mismatch for %s" % repr(i)) 77 | code += icode 78 | return code 79 | -------------------------------------------------------------------------------- /downloadFirmware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # URI = 'http://pebble-static.s3.amazonaws.com/watchfaces/index.html' 3 | URIs = { 4 | '1': 'http://pebblefw.s3.amazonaws.com/pebble/{}/release/latest.json', 5 | '2': 'http://pebblefw.s3.amazonaws.com/pebble/{}/release-v2/latest.json', 6 | '3': 'http://pebblefw.s3.amazonaws.com/pebble/{}/release-v3/latest.json', 7 | '3.7': 'http://pebblefw.s3.amazonaws.com/pebble/{}/release-v3.7/latest.json', 8 | '3.8': 'http://pebblefw.s3.amazonaws.com/pebble/{}/release-v3.8/latest.json', 9 | } 10 | HWs = { 11 | '1': [ 12 | 'ev2_4', 13 | 'v1_5', 14 | ], 15 | '2': [ 16 | 'ev2_4', # V2R2 17 | 'v1_5', # V3Rx 18 | 'v2_0', # STEEL 19 | ], 20 | '3': [ 21 | 'ev2_4', # V2R2 22 | 'v1_5', # V3Rx 23 | 'v2_0', # STEEL 24 | 'snowy_dvt', # Time 25 | 'snowy_s3', # Time Steel 26 | 'spalding', # Round 27 | ], 28 | } 29 | 30 | import argparse 31 | from urllib2 import urlopen 32 | import hashlib 33 | import logging, os.path 34 | import json 35 | 36 | if __name__ == "__main__": 37 | log = logging.getLogger() 38 | logging.basicConfig(format='[%(levelname)-8s] %(message)s') 39 | log.setLevel(logging.DEBUG) 40 | 41 | 42 | parser = argparse.ArgumentParser( 43 | description='Download latest firmware bundle from Pebble') 44 | parser.add_argument('version', default='3.7', nargs='?', 45 | choices=sorted(URIs.keys()), 46 | help='Which version group to use.') 47 | parser.add_argument('hardware', nargs='?', 48 | help='Hardware version to use (see code or pebbledev.org)') 49 | 50 | args = parser.parse_args() 51 | 52 | 53 | curr_hws = HWs[args.version[0]] 54 | if args.hardware: 55 | if args.hardware not in curr_hws: 56 | parser.error('Available hardwares for version %s: %s' % ( 57 | args.version, 58 | ', '.join(curr_hws), 59 | )) 60 | else: 61 | args.hardware = curr_hws[-1] 62 | 63 | 64 | uri = URIs[args.version].format(args.hardware) 65 | log.info("Downloading firmware linked from %s" % uri) 66 | 67 | page = urlopen(uri).read() 68 | data = json.loads(page) 69 | 70 | firmware = data['normal']['url'] 71 | version = data['normal']['friendlyVersion'] 72 | sha = data['normal']['sha-256'] 73 | 74 | log.info("Latest firmware version: %s" % version) 75 | fwfile = firmware[firmware.rindex("/")+1:] 76 | if os.path.exists(fwfile): 77 | log.warn('Did not download "%s" because it would overwrite an existing file' % fwfile) 78 | exit() 79 | with open(fwfile, "wb") as f: 80 | download = urlopen(firmware); 81 | length = int(download.headers["Content-Length"]) 82 | log.info("Downloading %s -> %s" % (firmware, fwfile)) 83 | 84 | downloaded = 0 85 | 86 | while downloaded < length: 87 | data = download.read(1024*50) 88 | f.write(data) 89 | downloaded += len(data) 90 | log.info("Downloaded %.1f%%" % (downloaded * 100.0 / length)) 91 | 92 | f = open(fwfile, "rb") 93 | filesha = hashlib.sha256() 94 | filesha.update(f.read()) 95 | if(filesha.hexdigest() != sha): 96 | log.error('File download errer: SHA-256 hash mismatch. Please retry.') 97 | else: 98 | log.info('File download done.') 99 | -------------------------------------------------------------------------------- /libpatcher/patch.py: -------------------------------------------------------------------------------- 1 | # This module holds Patch class 2 | class PatchingError(Exception): 3 | def __init__(self, message = None, cause = None): 4 | self.cause = cause 5 | if cause: 6 | message += ": " + repr(cause) 7 | super(PatchingError, self).__init__(message) 8 | 9 | class Patch(object): 10 | """ 11 | This class represents one patch file, 12 | with all its blocks and its global context. 13 | It may also represent aggregation of #included ("library") patchfiles. 14 | """ 15 | def __init__(self, name, library=None, binary=None): 16 | """ 17 | library: if not provided, will link to self 18 | binary: reference to original binary data; 19 | must be provided if there is no library 20 | """ 21 | self.name = name 22 | if not binary and not library: 23 | raise ValueError("Neither binary nor library provided") 24 | self.binary = binary or library.binary 25 | self._blocks = [] 26 | self._library = library or self 27 | self._is_bound = False 28 | self._context = {} 29 | def __repr__(self): 30 | return "" % (self.name, len(self.blocks)) 31 | @property 32 | def blocks(self): 33 | """ 34 | Collection of blocks for this patch 35 | """ 36 | return self._blocks 37 | @property 38 | def library(self): 39 | """ 40 | This links to a library patch, which holds 41 | all "included" patches' data. 42 | It may link to itself. 43 | """ 44 | return self._library 45 | @property 46 | def context(self): 47 | """ 48 | This is a patch-level global context. 49 | """ 50 | return self._context 51 | def bindall(self, binary, ranges, codebase = 0x8004000): 52 | """ 53 | Tries to bind all blocks of this patch 54 | to addresses in given binary. 55 | May raise MaskNotFoundError. 56 | """ 57 | if self._is_bound: 58 | raise ValueError("Already bound") 59 | for block in self.blocks: 60 | oldSize = block.getSize() 61 | position = block.getPosition(binary, ranges) 62 | block.bind(position + codebase, codebase) 63 | # block size could shrink because of ALIGNs.. 64 | newSize = block.getSize() 65 | if newSize < oldSize: 66 | ranges.add(position+newSize, position+oldSize) 67 | self._is_bound = True 68 | def apply(self, binary, codebase = 0x8004000, ignore=False): 69 | """ 70 | Applies all blocks from this patch to given binary, 71 | and returns resulting patched binary. 72 | Will bind itself firstly if neccessary. 73 | """ 74 | if not self._is_bound: 75 | self.bindall(binary, None, codebase=codebase) #FIXME ranges 76 | for block in self.blocks: 77 | bpos = block.getPosition() 78 | code = block.getCode() 79 | if len(code) > block.mask.size and not ignore: 80 | raise PatchingError("Code length %d exceeds mask length %d! Mask at %s" % 81 | (len(code), block.mask.size, block.mask.pos)) 82 | align = b'' 83 | if len(binary) < bpos: 84 | align = b'\x00'*(bpos-len(binary)) # perform alignment 85 | binary = binary[0:bpos] + align + code + binary[bpos+len(code):] 86 | return binary 87 | -------------------------------------------------------------------------------- /mkfonts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$2" ]; then 3 | cat <lines), number lines, grep version and get line number 24 | vid=$(sed q fonts.txt | tr '\t' '\n' | nl | grep ${ver} | sed q | awk '{print $1}') 25 | if [ -z "$vid" ]; then # not found? 26 | if echo "$ver" | grep -Eq '\.[0-9]+\.'; then # can try shorter vid? - try it 27 | calc_vid "$(echo "$ver" | perl -pe 's/\.[0-9]+(?!\.)//')" # cut off last .x version component 28 | return 29 | else # 30 | echo "Couldn't find font resource info for fw $fver!" 31 | exit 1 32 | fi 33 | else 34 | echo "$vid" 35 | fi 36 | } 37 | calc_vid ${fver} # primary 38 | calc_vid ${fver}a # aplite 39 | 40 | HARDWARES=(snowy_dvt snowy_s3 spalding ev2_4 v1_5 v2_0) 41 | HARDPLATF=(basalt basalt chalk aplite aplite aplite) # different platforms use different resource sets 42 | LANGS=(LaCyr LaGrHb LaViTh LaRuHb) 43 | UTILS=../pebble-firmware-utils 44 | PATCHPATH=../patches 45 | PATCHES="StringFixer_290" 46 | PATCHINFO=StringFix 47 | 48 | if ! [ $sver == "uploaded" ]; then 49 | for hwid in ${!HARDWARES[*]}; do # enumerate indices 50 | hw=${HARDWARES[hwid]} 51 | platf=${HARDPLATF[hwid]} 52 | fver_vid=$fver 53 | [[ $platf == aplite ]] && fver_vid=${fver_vid}a 54 | vid=$(calc_vid ${fver_vid}) 55 | echo "Building for hw $hw, platform $platf, column $vid" 56 | for lang in ${LANGS[*]}; do 57 | echo " Building for lang $lang" 58 | echo 59 | 60 | OUT=Pebble-${fver}-${hw}-${lang}-${PATCHINFO}.pbz 61 | if [ -e $OUT ]; then 62 | echo "Already built, skipping" 63 | continue 64 | fi 65 | 66 | DIR=v${sver}-${hw} 67 | pushd $DIR 68 | 69 | RES=RES_${lang}_${sver}_${platf}.pbpack 70 | 71 | if ! [ -e ../$RES ]; then 72 | echo "Resource pack $RES not found, building" 73 | if ! [ -e res ]; then 74 | ../$UTILS/unpackFirmware.py system_resources.pbpack 75 | mv resres res 76 | fi 77 | cat ../fonts.txt | grep 'GOTHIC_' | grep -v 'GOTHIC_09' | grep -v '_E' | awk '{print $1" "$'$vid'}' | while read name id; do 78 | id=$(printf %03d $id) 79 | rm res/${id}_* 80 | cp ../$lang/$name res/${id}_${name} 81 | done 82 | ../$UTILS/packResources.sh -w ~/.pebble-sdk/SDKs/current/sdk-core/pebble -o ../$RES res/* 83 | fi 84 | 85 | 86 | # patch if necessary 87 | if ! [ -e patched.bin ]; then 88 | patchlist=$(echo $PATCHES | while read p; do echo "../$PATCHPATH/${p}.pbp"; done) 89 | ../$UTILS/patcher.py --always-append $patchlist --output patched.bin 90 | fi 91 | 92 | # now build fw 93 | ../$UTILS/repackFirmware.py --tintin-fw patched.bin --respack ../$RES --replace-all ../$OUT 94 | 95 | popd 96 | 97 | echo 98 | done 99 | echo 100 | done 101 | 102 | cp *${fver}* repo/ 103 | fi 104 | 105 | echo 106 | echo '-=-=-' 107 | echo 108 | for lang in ${LANGS[*]}; do 109 | echo -n "| ${fver} ${lang} " 110 | for hw in ${HARDWARES[*]}; do 111 | echo -n "| [GH](https://github.com/MarSoft/pebble-firmware-utils/raw/builds/Pebble-${fver}-${hw}-${lang}-${PATCHINFO}.pbz) " 112 | done 113 | echo "|" 114 | done 115 | -------------------------------------------------------------------------------- /patcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from libpatcher import Patch, Ranges, parseFile 4 | 5 | def parse_args(): 6 | import argparse 7 | parser = argparse.ArgumentParser( 8 | description="Pebble firmware patcher") 9 | parser.add_argument("patch", nargs='+', type=argparse.FileType("r"), 10 | help="File with a patch to apply") 11 | parser.add_argument("-o", "--output", required=True, 12 | type=argparse.FileType("wb"), 13 | help="Output file name") 14 | parser.add_argument("-t", "--tintin", nargs='?', default="tintin_fw.bin", 15 | type=argparse.FileType("rb"), 16 | help="Input tintin_fw file, defaults to tintin_fw.bin") 17 | parser.add_argument("-d", "--debug", action="store_true", 18 | help="Print debug information while patching") 19 | parser.add_argument("-D", "--define", action="append", default=[], 20 | help="Add some #define'd constant. " 21 | "Usage: either -D constant or -D name=val") 22 | parser.add_argument("-i", "--ignore-length", action="store_true", 23 | help="Don't check for mask length " 24 | "when overwriting block (dangerous!") 25 | parser.add_argument("-a", "--append", action="store_true", 26 | help="Use space in the end of firmware " 27 | "to store floating blocks") 28 | parser.add_argument("-A", "--always-append", action="store_true", 29 | help="Same as --append, but doesn't check " 30 | "for maximum file size. " 31 | "Useful for PebbleTime firmware " 32 | "which seems to have other size limits") 33 | parser.add_argument("-c", "--codebase", type=lambda x: int(x, base=0), 34 | default=0x8004000, 35 | help="Codebase of the binary. " 36 | "Defaults to 0x8004000 (which is for 3.x fw); " 37 | "for 1.x-2.x set it to 0x8010000") 38 | return parser.parse_args() 39 | 40 | def patch_fw(args): 41 | data = args.tintin.read() 42 | 43 | # this is a library patch, 44 | # which will hold all #included blocks 45 | library = Patch("#library", binary=data) 46 | 47 | # this holds list of ranges 48 | ranges = Ranges() 49 | 50 | if args.append or args.always_append: 51 | ranges.add_eof(data, 0x70000 if args.append else 0x1000000, 52 | 0x48) 53 | 54 | # this is for #defined and pre#defined values 55 | definitions = {} 56 | for d in args.define: 57 | if '=' in d: 58 | name, val = d.split('=', 1) 59 | definitions[name] = val 60 | else: 61 | definitions[d] = True 62 | 63 | # Read all requested patch files 64 | patches = [library] 65 | print("Loading files:") 66 | for f in args.patch: 67 | print(f.name) 68 | patches.append(parseFile(f, definitions, libpatch=library)) 69 | # Bind them all to real binary (i.e. scan masks)... 70 | print("Binding patches:") 71 | for p in patches: # including library 72 | print(p) 73 | p.bindall(data, ranges, args.codebase) 74 | # ...and apply 75 | print("Applying patches:") 76 | for p in patches: 77 | print(p) 78 | data = p.apply(data, args.codebase, ignore=args.ignore_length) 79 | print("Saving...") 80 | # restore eof bytes, if file-end range was used 81 | data = ranges.restore_tail(data) 82 | args.output.write(data) 83 | args.output.close() 84 | print("Done.") 85 | 86 | if __name__ == "__main__": 87 | args = parse_args() 88 | patch_fw(args) 89 | -------------------------------------------------------------------------------- /libpatcher/mask.py: -------------------------------------------------------------------------------- 1 | class MaskError(Exception): 2 | def __init__(self, mask): 3 | return super(MaskError, self).__init__(repr(mask)) 4 | 5 | class MaskNotFoundError(MaskError): 6 | "Thrown if mask was not found" 7 | 8 | class AmbiguousMaskError(MaskNotFoundError): 9 | "Thrown if mask was found more than 1 time" 10 | 11 | class Mask(object): 12 | "This class represents mask" 13 | def __init__(self, parts, offset=0, pos=None): 14 | """ 15 | :param parts: list of alternating strings and integers. 16 | strings mean parts which will be matched, 17 | integers means "skip N bytes". 18 | :param offset: how many bytes from matched beginning to skip 19 | :param pos: parser.FilePos object describing block's (starting) 20 | position in file 21 | """ 22 | self.parts = parts 23 | self.offset = offset 24 | self.pos = pos 25 | self._size = None 26 | # TODO: validate 27 | 28 | def __repr__(self): 29 | if not self.parts: # floating mask 30 | return "Floating mask" 31 | 32 | def bytes2hex(bs): 33 | if isinstance(bs, int): 34 | return "?%d" % bs 35 | else: 36 | return ' '.join(["%02X" % b for b in bytearray(bs)]) 37 | return "Mask at %s: %s @%d" % ( 38 | self.pos, ','.join([bytes2hex(x) for x in self.parts]), 39 | self.offset 40 | ) 41 | 42 | @property 43 | def floating(self): 44 | return not self.parts or len(self.parts) == 0 45 | 46 | def match(self, data): 47 | """ 48 | Tries to match this mask to given data. 49 | Returns matched position on success, 50 | False if not found 51 | or (exception?) if found more than one occurance. 52 | """ 53 | if self.floating: 54 | raise ValueError("Cannot match floating mask") 55 | # if mask starts with skip, append it to offset 56 | # as a negative offset! 57 | if isinstance(self.parts[0], int): 58 | self.offset -= self.parts[0] 59 | del self.parts[0] 60 | pos1 = data.find(self.parts[0]) 61 | found = False 62 | while pos1 != -1: 63 | pos = pos1+len(self.parts[0]) 64 | for p in self.parts[1:]: 65 | if isinstance(p, int): 66 | pos += p 67 | else: 68 | if p == data[pos:pos+len(p)]: 69 | pos += len(p) 70 | else: 71 | break 72 | else: # not breaked -> matched 73 | if found is not False: # was already found? -> duplicate match 74 | raise AmbiguousMaskError(self) 75 | found = pos1 76 | # and find next occurance: 77 | pos1 = data.find(self.parts[0], pos1+1) 78 | # all occurances checked 79 | if found is not False: 80 | return found + self.offset 81 | raise MaskNotFoundError(self) 82 | 83 | @property 84 | def size(self): 85 | """ 86 | Returns size (in bytes) of the 'active' part of mask 87 | (excluding its part before @, or covered by initial ?-s) 88 | """ 89 | if self.floating: 90 | return self._size 91 | else: 92 | return sum([ 93 | len(x) if isinstance(x, (str, bytes)) else x 94 | for x in self.parts 95 | ]) - self.offset 96 | 97 | @size.setter 98 | def size(self, size): 99 | """ For floating masks """ 100 | if not self.floating: 101 | raise ValueError(repr(self)) 102 | self._size = size 103 | 104 | def getPos(self): 105 | return self.pos 106 | -------------------------------------------------------------------------------- /prepareResourceProject.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os, os.path 5 | import json 6 | from subprocess import call 7 | 8 | def fail(msg = "Failed."): 9 | print msg 10 | exit(1) 11 | 12 | def analyzeResources(useManifest, pathToRes): 13 | r = [] 14 | if useManifest: 15 | with open("manifest.json", "r") as f: 16 | manifest = json.load(f) 17 | if "resourceMap" in manifest["debug"]: 18 | rm = manifest["debug"]["resourceMap"]["media"] 19 | else: 20 | print "##########################################################" 21 | print " Error!!! Manifest contains no resourceMap!" 22 | print "It must be a 2.0 firmware! Don't use this method with it!" 23 | print "##########################################################" 24 | rm = [] 25 | fail() # if you want to make this a warning rather an error, remove this line 26 | for i in rm: 27 | item = {"type": "raw", 28 | "defName": i["defName"], 29 | "file": os.path.dirname(i["file"]) + "/" + i["defName"]} 30 | if os.path.exists(item["file"]): 31 | print "Adding resource %s as %s..." % (i['file'], i['defName']) 32 | r.append(item) 33 | fullpath = pathToRes + "/" + item["file"] 34 | if not os.path.exists(os.path.dirname(fullpath)): 35 | os.mkdir(os.path.dirname(fullpath)) 36 | os.link(item["file"], fullpath) 37 | if os.path.isdir("res"): 38 | files = os.listdir("res") 39 | filter(lambda x: len(x)>4 and x[0].isdigit() and x[1].isdigit() and x[2]=="_", files) 40 | files.sort() 41 | for i in files: 42 | n = int(i[:2]) 43 | if n < len(r): 44 | continue 45 | item = {"type": "raw", 46 | "defName": i, 47 | "file": "res/" + i} 48 | print "Adding resourse %s..." % i 49 | r.append(item) 50 | fullpath = pathToRes + "/" + item["file"] 51 | if not os.path.exists(os.path.dirname(fullpath)): 52 | os.mkdir(os.path.dirname(fullpath)) 53 | os.link(item["file"], fullpath) 54 | return r 55 | 56 | def ver1(sdk): 57 | cpr = sdk+"../tools/create_pebble_project.py" 58 | if not os.path.isfile(cpr): 59 | print "This doesn't look like a usable Pebble 1.x SDK:" 60 | print "Couldn't read " + cpr 61 | exit(1) 62 | 63 | if os.path.exists("app"): 64 | fail("./app/: path already exists!") 65 | 66 | print " # Creating project..." 67 | call([cpr, sdk, "app"]) == 0 or fail() 68 | 69 | print " # Analyzing resources..." 70 | res = analyzeResources(True, "app/resources/src") 71 | print res 72 | 73 | print " # Populating resource data..." 74 | with open("app/resources/src/resource_map.json", "r+") as f: 75 | obj = json.load(f) 76 | obj['media'] = res 77 | obj['friendlyVersion'] = "v1.10-JW" 78 | #obj['versionDefName'] = "v1.10-JW" # buggy 79 | f.seek(0) 80 | json.dump(obj, f, indent=4) 81 | 82 | print " # Configuring project..." 83 | os.chdir("app") 84 | call("./waf configure", shell=True) == 0 or fail() 85 | print " # Building project..." 86 | call("./waf build", shell=True) == 0 or fail() 87 | 88 | def ver2(sdk): 89 | print "Not implemented yet." 90 | pass 91 | 92 | if __name__ == "__main__": 93 | if len(sys.argv) < 3 or sys.argv[1] == "-h": 94 | print "Usage: %s ( -1 | -2 ) /path/to/Pebble/SDK/" % __file__ 95 | print "Assuming that current directory is one with unpacked firmware." 96 | print "Will create a new project in directory `app' and configure it to pack resources." 97 | exit() 98 | sdk = sys.argv[2] 99 | if not os.path.isdir(sdk): 100 | print "%s is not a directory!" % sdk 101 | exit(1) 102 | if sys.argv[1] == "-1": 103 | ver1(sdk) 104 | elif sys.argv[1] == "-2": 105 | ver2(sdk) 106 | else: 107 | print "Illegal argument, try -h" 108 | exit(1) 109 | 110 | print " # Creating symlink..." 111 | if os.path.exists("../system_resources.new.pbpack"): 112 | print "Already exists." 113 | else: 114 | os.symlink("app/build/app_resources.pbpack", "../system_resources.new.pbpack") 115 | print " # Done! You may get pbpack at system_resources.new.pbpack" 116 | print " Also you may rebuild it with: cd app; ./waf build" 117 | -------------------------------------------------------------------------------- /extract_codepoints.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import codecs 4 | from struct import pack, unpack 5 | import json, sys, os 6 | 7 | import math 8 | from PIL import Image 9 | 10 | def extract_codepoints(font): 11 | version = unpack('B', font.read(1))[0] 12 | max_height = unpack('B', font.read(1))[0] 13 | numbers = unpack('H', font.read(2))[0] 14 | wildcard = unpack('H', font.read(2))[0] 15 | 16 | table_size = unpack('B', font.read(1))[0] 17 | codepoint_bytes = unpack('B', font.read(1))[0] 18 | 19 | print >>sys.stderr, 'Contains %d codepoints' % numbers 20 | print >>sys.stderr, 'Maximum height: %d' % max_height 21 | 22 | codepoints = [] 23 | 24 | hash_table = {} 25 | for i in range(table_size): 26 | index = unpack('B', font.read(1))[0] 27 | size = unpack('B', font.read(1))[0] 28 | offset = unpack('H', font.read(2))[0] 29 | hash_table[index] = (size, offset) 30 | 31 | table_offset = 0x08 + table_size*0x04 32 | offset_tables = {} 33 | for index in hash_table: 34 | size, offset = hash_table[index] 35 | font.seek(table_offset+offset) 36 | 37 | offset_table = [] 38 | for j in range(size): 39 | if codepoint_bytes == 4: 40 | codepoint, offset = unpack(' 0: 87 | charimg.putpixel((x, y), (0,0,0)) 88 | if count/side*gap+x+left > 0 and count%side*gap+y+top > 0: 89 | bitmap.putpixel((count/side*gap+x+left, count%side*gap+y+top), (0,0,0)) 90 | 91 | charfile = '%s/%05X.png' % (dir_name, codepoints[count]) 92 | if width and height: 93 | charimg.save(charfile) 94 | else: 95 | # just touch it 96 | open(charfile, 'wb').close() 97 | 98 | count += 1 99 | 100 | #bitmap.show() 101 | 102 | with codecs.open('%s/list.json' % dir_name, 'w', 'utf-8') as out: 103 | json.dump({ 104 | 'max_height': max_height, 105 | 'codepoints': codepoints, 106 | 'metadata': metadata, 107 | }, out, indent=4, ensure_ascii=False) 108 | 109 | if __name__ == '__main__': 110 | if len(sys.argv) <= 2: 111 | print 'Usage: extract_codepoints.py fontfile output_dir' 112 | exit(1) 113 | 114 | file_name, dir_name = sys.argv[1:3] 115 | if os.path.exists(dir_name): 116 | print('Directory %s already exists!' % dir_name) 117 | exit(1) 118 | os.mkdir(dir_name) 119 | font = open(file_name, 'rb') 120 | extract_codepoints(font) 121 | 122 | -------------------------------------------------------------------------------- /libpatcher/tests/test_asm.py: -------------------------------------------------------------------------------- 1 | from libpatcher.asm import * 2 | from libpatcher.parser import * 3 | from libpatcher.parser import parseBlock, parseInstruction 4 | from libpatcher.block import * 5 | from libpatcher.patch import Patch 6 | from nose.tools import * 7 | 8 | # test thumb expandable integers 9 | def test_thumbex(): 10 | tx = Num.ThumbExpandable() 11 | o = Num(0x00ff0000) 12 | assert tx.match(o) 13 | eq_(o.theval, 0b1111111 + (8<<8)) 14 | 15 | mock_patch = Patch('test_patch', binary=b"test_bin") 16 | def op_gen(instr, addr=0, context={}): 17 | pos = FilePos('test_asm.pbp',0) 18 | if isinstance(instr, str): 19 | i = parseInstruction(instr, pos) 20 | else: 21 | i = findInstruction(instr[0], instr[1], pos) 22 | assert i 23 | if addr < 0x8004000: 24 | addr += 0x8004000 25 | block = Block(mock_patch, None, [i]) 26 | block.bind(addr, 0x8004000) 27 | context['self'] = addr # add fake "self" label for our instruction 28 | context['next'] = addr+4 29 | block.context.update(context) # append our "fake" labels 30 | return i 31 | def op(instr, addr=0, context={}): 32 | return op_gen(instr, addr, context).getCode() 33 | 34 | def eq_(a,b): 35 | def unhex(s): 36 | return ' '.join(["%02X"%ord(c) for c in s]) 37 | if isinstance(a, str): 38 | a = unhex(a) 39 | if isinstance(b, str): 40 | b = unhex(b) 41 | assert a==b, "%s != %s" % (a,b) 42 | def test_BL_self(): 43 | eq_(op('BL self'), b'\xFF\xF7\xFE\xFF') 44 | def test_BW_self(): 45 | eq_(op('B.W self'), b'\xFF\xF7\xFE\xBF') 46 | def test_BW_next(): 47 | eq_(op('B.W next'), b'\x00\xF0\x00\xB8') 48 | def test_DCW_0x1234(): 49 | assert op('DCW 0x1234') == b'\x34\x12' 50 | @raises(ParseError) 51 | def test_DCW_too_large(): 52 | op('DCW 0x12345') 53 | def test_DCD_0xDEADBEEF(): 54 | assert op('DCD 0xDEADBEEF') == b'\xEF\xBE\xAD\xDE' 55 | def test_NOP(): 56 | assert op('NOP') == b'\x00\xBF' 57 | def test_BCC_self(): 58 | eq_(op('BCC self'), b'\xFE\xD3') 59 | def test_BEQ_self(): 60 | eq_(op('BEQ self'), b'\xFE\xD0') 61 | def test_BNE_W_self(): 62 | eq_(op('BNE.W self'), b'\x7F\xF4\xFE\xAF') 63 | def test_CBZ_R3_next(): 64 | eq_(op('CBZ R3, next'), b'\x03\xB1') 65 | def test_CBNZ_R7_next(): 66 | eq_(op('CBNZ R7, next'), b'\x07\xB9') 67 | def test_B_self(): 68 | eq_(op('B self'), b'\xFE\xE7') 69 | def test_global_label(): 70 | instr = op_gen('global globlabel') 71 | def test_val(): 72 | eq_(op('val name'), b'') 73 | eq_(mock_patch.context['name'], 0x74736574) 74 | # 0x74.. is an integer representation of 'test' 75 | def test_DCD_name(): 76 | eq_(op('DCD name'), b'test') 77 | def test_DCD_name_p_1(): 78 | eq_(op('DCD name+1'), b'uest') 79 | def test_ADD_R1_1(): 80 | assert op(('ADD', [Reg('R1'), Num(1)])) == b'\x01\x31' 81 | def test_ADD_R3_R0_R2(): 82 | eq_(op('ADD R3,R0,R2'), b'\x83\x18') 83 | #def test_ADD_R7_SP_12(): 84 | # eq_(op('ADD R7,SP,12'), b'\x03\xAF') 85 | def test_ADD_R0_R4_0x64(): 86 | eq_(op('ADD R0,R4,0x64'), b'\x04\xf1\x64\x00') 87 | def test_ADR_R2_next(): 88 | eq_(op('ADR R2,next'), b'\x00\xA2') 89 | def test_BLX_R8(): 90 | eq_(op('BLX R8'), b'\xC0\x47') 91 | def test_BX_LR(): 92 | eq_(op('BX LR'), b'\x70\x47') 93 | def test_CMP_R3_0xF(): 94 | eq_(op('CMP R3,0xF'), b'\x0F\x2B') 95 | def test_CMP_R2_R12(): 96 | eq_(op('CMP R2,R12'), b'\x62\x45') 97 | def test_CMP_R0_R1(): 98 | eq_(op('CMP R0,R1'), b'\x88\x42') 99 | def test_CMP_R5_0x240(): 100 | eq_(op('CMP R5, 0x240'), b'\xB5\xF5\x10\x7F') 101 | def test_MOV_R0_2C(): 102 | eq_(op('MOV R0,0x2C'), b'\x2c\x20') 103 | def test_MOV_R0_3x4(): 104 | eq_(op('MOV R0,3*4'), b'\x0c\x20') 105 | def test_MOV_R0_10m4(): 106 | eq_(op('MOV R0,10-4'), b'\x06\x20') 107 | def test_MOV_R0_10p4(): 108 | eq_(op('MOV R0,10+4'), b'\x0e\x20') 109 | def test_MOVS_R0_R5(): 110 | eq_(op('MOVS R0,R5'), b'\x28\x00') 111 | def test_MOV_R0_R5(): 112 | eq_(op('MOV R0,R5'), b'\x28\x46') 113 | def test_MOVW_R1_0xFF000(): 114 | eq_(op('MOV.W R1,0xFF000'),b'\x4F\xF4\x7F\x21') 115 | def test_MOV_R2_50000(): 116 | eq_(op('MOV R2,50000'),b'\x4C\xF2\x50\x32') 117 | @raises(ParseError) 118 | def test_MOV_R2_m50000_fails(): 119 | eq_(op('MOV R2,-50000'),b'\x4F\xF4\x7F\x21') 120 | @raises(ParseError) 121 | def test_MOVW_R1_m1_fails(): 122 | print(op('MOVW R1,-1')) 123 | def test_LDR_R3_next(): 124 | eq_(op('LDR R3, next'), b'\x00\x4B') 125 | def test_LDR_R5_R3(): 126 | eq_(op('LDR R5,[R3]'), b'\x1D\x68') 127 | def test_LDR_R12_SP_0x24(): 128 | eq_(op('LDR R12,[SP,0x24]'), b'\xDD\xF8\x24\xC0') 129 | def test_LDRB_R3_R3(): 130 | eq_(op('LDRB R3,[R3]'), b'\x1B\x78') 131 | def test_LDRB_R3_SP_3(): 132 | eq_(op('LDRB R3, [SP,3]'), b'\x9D\xF8\x03\x30') 133 | def test_LDRB_R2_R4_1(): 134 | eq_(op('LDRB R2,[R4],1'), b'\x14\xf8\x01\x2b') 135 | def test_MUL_R3_R7(): 136 | eq_(op('MUL R3,R7'), b'\x7b\x43') 137 | def test_PUSH_R3_LR(): 138 | eq_(op('PUSH {R3,LR}'), b'\x08\xb5') 139 | def test_PUSH_R4_R8_LR(): 140 | eq_(op('PUSH {R4-R8,LR}'), b'\x2d\xe9\xf0\x41') 141 | def test_POP_R4_R8_PC(): 142 | eq_(op('POP {R4-R8,PC}'), b'\xbd\xe8\xf0\x81') 143 | def test_POP_R3_R7_PC(): 144 | eq_(op('POP {R3-R7,PC}'), b'\xF8\xBD') 145 | def test_STR_R3_SP(): 146 | eq_(op('STR R3,[SP]'), b'\x00\x93') 147 | def test_STR_R3_SP_4(): 148 | eq_(op('STR R3,[SP,4]'), b'\x01\x93') 149 | def test_STR_R5_R2(): 150 | eq_(op('STR R5,[R2]'), b'\x15\x60') 151 | def test_STR_R8_SP_0x34(): 152 | eq_(op('STR R8,[SP,0x34]'), b'\xCD\xF8\x34\x80') 153 | def test_STRB_R6_R4_6(): 154 | eq_(op('STRB R6,[R4,6]'), b'\xA6\x71') 155 | def test_STRB_R3_SP_3(): 156 | eq_(op('STRB R3,[SP,3]'), b'\x8D\xF8\x03\x30') 157 | def test_SUB_R2_0x12(): 158 | eq_(op('SUB R2,0x12'), b'\x12\x3A') 159 | def test_SUB_R4_R6_R4(): 160 | eq_(op('SUB R4,R6,R4'), b'\x34\x1B') 161 | def test_SUB_R2_R0_8(): # with T4 encoding - f2; T3 -> f1 162 | eq_(op('SUB R2,R0,8'), b'\xa0\xf2\x08\x02') 163 | def test_SUB_R1_R4_1(): 164 | eq_(op('SUB R1,R4,1'), b'\x61\x1E') 165 | def test_TST_R5_R3(): 166 | eq_(op('TST R5,R3'), b'\x1D\x42') 167 | def test_TST_R1_100000(): 168 | eq_(op('TST R1,0x100000'), b'\x11\xF4\x80\x1F') 169 | def test_UXTB_R5_R4(): 170 | eq_(op('UXTB R5,R4'), b'\xE5\xB2') 171 | -------------------------------------------------------------------------------- /unpackFirmware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import zipfile 4 | import os 5 | import io 6 | import json 7 | from libpebble.stm32_crc import crc32 8 | from struct import unpack 9 | import argparse 10 | 11 | 12 | def mkdir(path): 13 | try: 14 | os.mkdir(path) 15 | except OSError: 16 | pass 17 | 18 | def extract_content(pbz, content, output_dir): 19 | print('Extracting %s...' % content['name']) 20 | pbz.extract(content['name'], output_dir) 21 | data = io.FileIO(output_dir + content['name']).readall() 22 | crc = crc32(data) 23 | if crc == content['crc']: 24 | print('\t[ OK] Checking CRC...') 25 | else: 26 | print('\t[Fail] Checking CRC...') 27 | print("\tIt's %d, but should be %d" % (content['crc'], crc)) 28 | 29 | def extract_resources(pbpack, resourceMap, output_dir): 30 | numbers = unpack('I', pbpack.read(4))[0] 31 | print('Resource pack has %d resources.' % numbers) 32 | 33 | pbpack.seek(4) 34 | crc_from_json = unpack('I', pbpack.read(4))[0] 35 | print('Resource pack claims to have crc 0x%X.' % crc_from_json) 36 | 37 | tbl_start = None # this will be set depending on firmware version 38 | res_start = None 39 | 40 | offsets = [ 41 | (0x200C, '3.x', 0x0C), 42 | (0x100C, '2.x', 0x0C), 43 | (0x101C, '1.x', 0x0C), 44 | ] 45 | for ofs, ver, tab in offsets: 46 | print('Checking CRC with offset {} ({})...'.format(hex(ofs), ver)) 47 | pbpack.seek(ofs) 48 | crc_resource = crc32(pbpack.read()) 49 | if crc_resource == crc_from_json: 50 | print('\t[ OK] This looks like {} firmware'.format(ver)) 51 | tbl_start = tab 52 | res_start = ofs 53 | break 54 | else: 55 | print('\t[????] CRC mismatch: found 0x%X' % crc_resource) 56 | else: 57 | print('\t[Fail] CRC check failed!') 58 | print('\tShould be 0x%X' % crc_from_json) 59 | print('Resource pack is either malformed or has unknown format.') 60 | return 61 | 62 | print('Reading resurce headers...') 63 | resources = {} 64 | for i in range(numbers): 65 | pbpack.seek(tbl_start + i * 16) 66 | index = unpack('i', pbpack.read(4))[0] - 1 67 | resources[index] = { 68 | 'offset': unpack('i', pbpack.read(4))[0], 69 | 'size': unpack('i', pbpack.read(4))[0], 70 | 'crc': unpack('I', pbpack.read(4))[0] 71 | } 72 | 73 | for i in range(len(resources)): 74 | entry = resources[i] 75 | hasRM = resourceMap and i < len(resourceMap) 76 | path = resourceMap[i]['file'] if hasRM else 'res/%03d_%08X' % ( 77 | i+1, entry['crc']) 78 | dirname = os.path.dirname(path) 79 | filepath = ("/".join((dirname, resourceMap[i]['defName'])) 80 | if hasRM else path) 81 | 82 | print('Extracting %s...' % filepath) 83 | mkdir(output_dir + dirname) 84 | 85 | pbpack.seek(res_start + entry['offset']) 86 | file = open(output_dir + filepath, 'wb') 87 | file.write(pbpack.read(entry['size'])) 88 | file.close() 89 | 90 | data = io.FileIO(output_dir + filepath).readall() 91 | crc = crc32(data) 92 | if crc == entry['crc']: 93 | print('\t[ OK] Checking CRC...') 94 | else: 95 | print('\t[Fail] Checking CRC...') 96 | print("\tIt's 0x%x, but should be 0x%x" % (crc, entry['crc'])) 97 | print('All resources unpacked.') 98 | 99 | 100 | def parse_args(): 101 | parser = argparse.ArgumentParser() 102 | parser.add_argument('-i', '--ignore-filenames', dest='useNaming', 103 | action='store_false', default=True, 104 | help='Ignore resource filenames from manifest. ' 105 | 'By default, if manifest contains resource names' 106 | '(as in 1.x firmwares), we will name resources' 107 | 'according to them.' 108 | 'With this option, resource names will be res/ID_CRC' 109 | 'where ID is sequence index of resource and CRC is' 110 | 'its CRC checksum (for comparing)' 111 | 'On 2.x firmwares, which don\'t contain resource names,' 112 | 'we always use that scheme.') 113 | parser.add_argument('infile', 114 | help='Input file: either pbz to be unzipped&extracted ' 115 | 'or pbpack/pbl/whatever to be just extracted') 116 | parser.add_argument('output_dir', nargs='?', default=None) 117 | return parser.parse_args() 118 | 119 | def main(): 120 | args = parse_args() 121 | 122 | if not args.output_dir: 123 | if args.infile.endswith('pbz'): 124 | args.output_dir = 'pebble-firmware/' 125 | else: 126 | args.output_dir = './' 127 | elif not args.output_dir.endswith('/'): 128 | args.output_dir += '/' 129 | 130 | if not args.infile.endswith('pbz'): # just unpack resources 131 | extract_resources(open(args.infile, 'rb'), None, args.output_dir) 132 | return 133 | 134 | print('Will unpack firmware from %s to directory %s, ' 135 | 'using %s for filenames' % ( 136 | args.infile, args.output_dir, 137 | 'resource names (if possible)' 138 | if args.useNaming else 'resource indices')) 139 | 140 | pbz = zipfile.ZipFile(args.infile) 141 | 142 | print('Extracting manifest.json...') 143 | pbz.extract('manifest.json', args.output_dir) 144 | manifest = json.load(open(args.output_dir + 'manifest.json', 'rb')) 145 | 146 | firmware = manifest['firmware'] 147 | extract_content(pbz, firmware, args.output_dir) 148 | 149 | if 'resources' in manifest: 150 | resources = manifest['resources'] 151 | extract_content(pbz, resources, args.output_dir) 152 | 153 | if 'resourceMap' in manifest['debug']: 154 | resourceMap = manifest['debug']['resourceMap']['media'] 155 | print('Found resource map in manifest. Looks like 1.x firmware.') 156 | if args.useNaming: 157 | print('Will use it to name resources correctly. ' 158 | 'To ignore, pass -i option.') 159 | else: 160 | print('Will not use it however because of -i option') 161 | else: 162 | resourceMap = None 163 | print("Couldn't find resource map in manifest. " 164 | "Looks like 2.x firmware.") 165 | print('Resources will be named by their indices.') 166 | args.useNaming = False 167 | pbpack = open(args.output_dir + resources['name'], 'rb') 168 | extract_resources(pbpack, 169 | resourceMap if args.useNaming else None, 170 | args.output_dir) 171 | 172 | if __name__ == '__main__': 173 | main() 174 | -------------------------------------------------------------------------------- /repackFirmware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import zipfile 4 | import os, os.path 5 | import json 6 | from libpebble.stm32_crc import crc32 7 | from struct import pack, unpack 8 | import tempfile 9 | import shutil 10 | 11 | def getCrc(pbpack): 12 | """read CRC from a pbpack file""" 13 | pbpack.seek(4) 14 | return unpack('I', pbpack.read(4))[0] 15 | 16 | def updateCrc(tintin, nNew, nOld = 0, byOffset = None, replace_all = False): 17 | """update CRC sum in tintin binary 18 | Passing byOffset means nOld will not be used. 19 | """ 20 | new = pack('I', nNew) 21 | offsets = [] 22 | if byOffset: 23 | offsets.append(byOffset) 24 | print "Checksum must be at 0x%08X." % offset 25 | else: 26 | old = pack('I', nOld) 27 | fw = tintin.read() 28 | i = fw.find(old) 29 | if i < 0: 30 | print "Oops... Couldn't find checksum 0x%08X in tintin_fw.bin! Maybe you specified incorrect data?.." 31 | exit(1) 32 | if replace_all: 33 | while i >= 0: 34 | offsets.append(i) 35 | i = fw.find(old, i+1) 36 | else: 37 | j = fw.find(old, i+1) 38 | if not j < 0: # if it was not the only occurance 39 | print "Oops... There are several occurances of possible checksum 0x%08X, at least at 0x%08X and 0x%08X." % (nOld, i, j) 40 | print "Bailing out!" 41 | exit(1) 42 | offsets.append(i) 43 | print "Found the only occurance of old checksum 0x%08X at 0x%08X" % (nOld, i) 44 | for offset in offsets: 45 | print "Replacing checksum at 0x%08X with the new value 0x%08X..." % (offset, nNew) 46 | tintin.seek(offset) 47 | tintin.write(new) 48 | tintin.flush() 49 | print "OK." 50 | 51 | def parse_args(): 52 | def readable(f): 53 | open(f, 'rb').close() 54 | return f 55 | import argparse 56 | parser = argparse.ArgumentParser(description="Update checksums and pack firmware package " 57 | "with modified resource pack or tintin_fw binary") 58 | parser.add_argument("outfile", 59 | help="Output file, e.g. Pebble-1.10-ev2_4.pbz") 60 | group = parser.add_mutually_exclusive_group(required=False) 61 | group.add_argument("-o", "--original", default="system_resources.pbpack", type=argparse.FileType('rb'), 62 | help="Original resource pack from the original firmware, defaults to system_resources.pbpack", 63 | metavar="ORIGINAL_RESPACK") 64 | group.add_argument("-c", "--orig-crc", type=lambda x: int(x,16), 65 | help="CRC sum from the original resource pack (hexadecimal), e.g. 0xDEADBE05") 66 | group.add_argument("-s", "--offset", type=lambda x: int(x,16), 67 | help="Offset from beginning of tintin_fw.bin to checksum value (hexadecimal), e.g. 0xFA57BA70; "+ 68 | "you may acquire it from previous run of this utility.") 69 | parser.add_argument("-m", "--manifest", default="manifest.json", type=readable, 70 | help="Manifest file from the original firmware, defaults to manifest.json") 71 | parser.add_argument("-t", "--tintin-fw", "--tintin", default="tintin_fw.bin", type=readable, 72 | help="Tintin_fw file from the original firmware, defaults to tintin_fw.bin") 73 | parser.add_argument("-r", "--respack", default="system_resources.pbpack", type=readable, 74 | help="Updated resource pack filename, defaults to system_resources.pbpack") 75 | parser.add_argument("-R", "--no-resources", action="store_true", 76 | help="Don't store resources in the bundle at all. " 77 | "This is experimental, and intended to eliminate unneeded delays " 78 | "during patch debugging. " 79 | "Requires patched SDK to avoid exceptions during flashing.") 80 | group = parser.add_argument_group("Optional parameters") 81 | group.add_argument("-k", "--keep-dir", action="store_true", 82 | help="Don't remove temporary directory after work") 83 | group.add_argument("-a", "--replace-all", action="store_true", 84 | help="If crc found more than 1 time, don't bail out but replace 'em all " 85 | "(may be required for v3.0 firmware)") 86 | return parser.parse_args() 87 | 88 | 89 | if __name__ == "__main__": 90 | args = parse_args() 91 | do_crc = not args.original or args.respack != args.original.name 92 | 93 | if do_crc and args.original: 94 | args.orig_crc = getCrc(args.original) 95 | args.original.close() 96 | 97 | print "Will create firmware at %s," % args.outfile 98 | print "using %s for manifest, %s for tintin binary" % (args.manifest, args.tintin_fw) 99 | print "and %s for resource pack." % args.respack 100 | 101 | if do_crc: 102 | if args.orig_crc: 103 | print "Will replace 0x%08X with new CRC." % args.orig_crc 104 | else: 105 | print "Will write new CRC at 0x%08X." % args.offset 106 | print 107 | 108 | try: 109 | workdir = tempfile.mkdtemp()+'/' 110 | 111 | print " # Copying new resource pack..." 112 | shutil.copy(args.respack, workdir+'system_resources.pbpack') 113 | with open(workdir+'system_resources.pbpack', 'rb') as newres: 114 | newCrc = getCrc(newres) 115 | 116 | print " # Copying tintin_fw.bin..." 117 | shutil.copy(args.tintin_fw, workdir+'tintin_fw.bin') 118 | if do_crc: 119 | print " # Updating CRC value in tintin_fw.bin from 0x%08X to 0x%08X:" % (args.orig_crc or 0, newCrc) 120 | with open(workdir+'tintin_fw.bin', 'r+b') as tintin: 121 | updateCrc(tintin, newCrc, args.orig_crc, args.offset, args.replace_all) 122 | 123 | print " # Reading manifest..." 124 | with open(args.manifest, 'r') as f: 125 | manifest = json.load(f) 126 | 127 | print " # Updating manifest..." 128 | if do_crc: 129 | rp_size = os.path.getsize(args.respack) 130 | print " res pack size = %d" % rp_size 131 | with open(args.respack) as f: 132 | rp_crc = crc32(f.read()) 133 | print " res pack crc = %d" % rp_crc 134 | tintin_size = os.path.getsize(workdir+"tintin_fw.bin") 135 | print " tintin size = %d" % tintin_size 136 | with open(workdir+"tintin_fw.bin") as f: 137 | tintin_crc = crc32(f.read()) 138 | print " tintin crc = %d" % tintin_crc 139 | 140 | print " # Changing values:" 141 | if do_crc: 142 | print " resources.size: %d => %d" % (manifest['resources']['size'], rp_size) 143 | manifest['resources']['size'] = rp_size 144 | print " resources.crc: %d => %d" % (manifest['resources']['crc'], rp_crc) 145 | manifest['resources']['crc'] = rp_crc 146 | print " firmware.size: %d => %d" % (manifest['firmware']['size'], tintin_size) 147 | manifest['firmware']['size'] = tintin_size 148 | print " firmware.crc: %d => %d" % (manifest['firmware']['crc'], tintin_crc) 149 | manifest['firmware']['crc'] = tintin_crc 150 | 151 | print " # Storing manifest..." 152 | with open(workdir+"manifest.json", "wb") as f: 153 | json.dump(manifest, f) 154 | 155 | print " # Creating output zip..." 156 | z = zipfile.ZipFile(args.outfile, "w", zipfile.ZIP_STORED) 157 | try: 158 | for f in ("tintin_fw.bin", 159 | "system_resources.pbpack", 160 | "manifest.json"): 161 | if f == "system_resources.pbpack" and args.no_resources: 162 | print " SKIPPING resource pack!" 163 | continue 164 | print " Storing %s..." % f 165 | z.write(workdir+f, f) 166 | for f in ("LICENSE.txt", 167 | "layouts.json.auto"): 168 | z.write(f, f) # form current dir - FIXME 169 | finally: 170 | z.close() 171 | 172 | print " # Done. Firmware is packed to %s" % args.outfile 173 | 174 | finally: 175 | if args.keep_dir: 176 | print "Kept temporary files in %s" % workdir 177 | else: 178 | print "Removing temporary files..." 179 | shutil.rmtree(workdir) 180 | 181 | -------------------------------------------------------------------------------- /fontgen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import argparse 4 | import freetype 5 | import os 6 | import re 7 | import struct 8 | import sys 9 | import itertools 10 | import json 11 | from math import ceil 12 | from PIL import Image 13 | 14 | sys.path.append(os.path.join(os.path.dirname(__file__), '../')) 15 | import generate_c_byte_array 16 | 17 | # Font 18 | # FontInfo 19 | # (uint8_t) version 20 | # (uint8_t) max_height 21 | # (uint16_t) number_of_glyphs 22 | # (uint16_t) wildcard_codepoint 23 | # (uint8_t) hash_table_size 24 | # (uint8_t) codepoint_bytes 25 | # 26 | # (uint32_t) hash_table[] 27 | # this hash table contains offsets to each glyph offset table. each offset is counted in 28 | # 32 bit blocks from the start of the offset tables block. Each entry in the hash table is 29 | # as follow: (uint8_t) hash value 30 | # (uint8_t) offset_table_size 31 | # (uint16_t) offset 32 | # 33 | # (uint32_t) offset_tables[][] 34 | # this list of tables contains offsets into the glyph_table for characters 0x20 to 0xff 35 | # each offset is counted in 32-bit blocks from the start of the glyph 36 | # each individual offset table contains ~10 sorted glyphs 37 | # table. 16-bit offsets are keyed by 16-bit codepoints. 38 | # packed: (codepoint_bytes [uint16_t | uint32_t]) codepoint 39 | # (uint_16) offset 40 | # 41 | # (uint32_t) glyph_table[] 42 | # [0]: the 32-bit block for offset 0 is used to indicate that a glyph is not supported 43 | # then for each glyph: 44 | # [offset + 0] packed: (int_8) offset_top 45 | # (int_8) offset_left, 46 | # (uint_8) bitmap_height, 47 | # (uint_8) bitmap_width (LSB) 48 | # 49 | # [offset + 1] (int_8) horizontal_advance 50 | # (24 bits) zero padding 51 | # [offset + 2] bitmap data (unaligned rows of bits), padded with 0's at 52 | # the end to make the bitmap data as a whole use multiples of 32-bit 53 | # blocks 54 | 55 | MIN_CODEPOINT = 0x20 56 | MAX_2_BYTES_CODEPOINT = 0xffff 57 | MAX_EXTENDED_CODEPOINT = 0x10ffff 58 | FONT_VERSION_1 = 1 59 | FONT_VERSION_2 = 2 60 | # Set a codepoint that the font doesn't know how to render 61 | # The watch will use this glyph as the wildcard character 62 | WILDCARD_CODEPOINT = 0x25AF # White vertical rectangle 63 | WILDCARD_CODEPOINT = 0x3456 # from def fonts 64 | ELLIPSIS_CODEPOINT = 0x2026 65 | 66 | HASH_TABLE_SIZE = 255 67 | OFFSET_TABLE_MAX_SIZE = 128 68 | MAX_GLYPHS_EXTENDED = HASH_TABLE_SIZE * OFFSET_TABLE_MAX_SIZE 69 | MAX_GLYPHS = 256 70 | OFFSET_SIZE_BYTES = 4 71 | 72 | def grouper(n, iterable, fillvalue=None): 73 | """grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx""" 74 | args = [iter(iterable)] * n 75 | return itertools.izip_longest(fillvalue=fillvalue, *args) 76 | 77 | def hasher(codepoint, num_glyphs): 78 | return (codepoint % num_glyphs) 79 | 80 | def bits(x): 81 | data = [] 82 | for i in range(8): 83 | data.insert(0, int((x & 1) == 1)) 84 | x = x >> 1 85 | return data 86 | 87 | class Font: 88 | def __init__(self, ttf_path, height, max_glyphs, legacy): 89 | self.version = FONT_VERSION_2 90 | self.ttf_path = ttf_path 91 | self.max_height = int(height) 92 | self.legacy = legacy 93 | if os.path.isdir(ttf_path): 94 | self.isdir = True 95 | else: 96 | self.isdir = False 97 | self.face = freetype.Face(self.ttf_path) 98 | self.face.set_pixel_sizes(0, self.max_height) 99 | self.name = self.face.family_name + "_" + self.face.style_name 100 | self.wildcard_codepoint = WILDCARD_CODEPOINT 101 | self.number_of_glyphs = 0 102 | self.table_size = HASH_TABLE_SIZE 103 | self.tracking_adjust = 0 104 | self.regex = None 105 | self.codepoints = range(MIN_CODEPOINT, MAX_EXTENDED_CODEPOINT) 106 | self.codepoint_bytes = 2 107 | self.max_glyphs = max_glyphs 108 | self.glyph_table = [] 109 | self.hash_table = [0] * self.table_size 110 | self.offset_tables = [[] for i in range(self.table_size)] 111 | return 112 | 113 | def set_tracking_adjust(self, adjust): 114 | self.tracking_adjust = adjust 115 | 116 | def set_regex_filter(self, regex_string): 117 | if regex_string != ".*": 118 | try: 119 | self.regex = re.compile(unicode(regex_string, 'utf8'), re.UNICODE) 120 | except Exception, e: 121 | raise Exception("Supplied filter argument was not a valid regular expression.") 122 | else: 123 | self.regex = None 124 | 125 | def set_codepoint_list(self, list_path): 126 | codepoints_file = open(list_path) 127 | codepoints_json = json.load(codepoints_file) 128 | if self.isdir: 129 | self.advances = {d['codepoint']: d for d in codepoints_json['metadata']} 130 | self.codepoints = self.advances.keys() 131 | else: 132 | self.codepoints = [int(cp) for cp in codepoints_json["codepoints"]] 133 | 134 | def char_filename(self, codepoint): 135 | return os.path.join(self.ttf_path, '%05X.png'%codepoint) 136 | 137 | def is_supported_glyph(self, codepoint): 138 | if self.isdir: 139 | return os.path.exists(self.char_filename(codepoint)) 140 | else: 141 | return (self.face.get_char_index(codepoint) > 0 or (codepoint == unichr(self.wildcard_codepoint))) 142 | 143 | def glyph_bits(self, gindex, codepoint): 144 | if self.isdir: 145 | if os.path.getsize(self.char_filename(codepoint)): 146 | image = Image.open(self.char_filename(codepoint)) 147 | width, height = image.size 148 | else: 149 | image = None 150 | width, height = 0, 0 151 | 152 | cpinfo = self.advances[codepoint] 153 | advance = cpinfo['advance'] 154 | left = cpinfo['left'] 155 | bottom = cpinfo['top'] # it is probably a misunderstanding of top and bottom? see below in struct 156 | 157 | pixel_mode = 99 # custom 158 | else: 159 | flags = (freetype.FT_LOAD_RENDER if self.legacy else 160 | freetype.FT_LOAD_RENDER | freetype.FT_LOAD_MONOCHROME | freetype.FT_LOAD_TARGET_MONO) 161 | self.face.load_glyph(gindex, flags) 162 | # Font metrics 163 | bitmap = self.face.glyph.bitmap 164 | advance = self.face.glyph.advance.x / 64 # Convert 26.6 fixed float format to px 165 | advance += self.tracking_adjust 166 | width = bitmap.width 167 | height = bitmap.rows 168 | left = self.face.glyph.bitmap_left 169 | bottom = self.max_height - self.face.glyph.bitmap_top 170 | pixel_mode = self.face.glyph.bitmap.pixel_mode 171 | 172 | glyph_structure = ''.join(( 173 | '<', #little_endian 174 | 'B', #bitmap_width 175 | 'B', #bitmap_height 176 | 'b', #offset_left 177 | 'b', #offset_top 178 | 'b' #horizontal_advance 179 | )) 180 | glyph_header = struct.pack(glyph_structure, width, height, left, bottom, advance) 181 | 182 | glyph_bitmap = [] 183 | if pixel_mode == 1: # monochrome font, 1 bit per pixel 184 | for i in range(bitmap.rows): 185 | row = [] 186 | for j in range(bitmap.pitch): 187 | row.extend(bits(bitmap.buffer[i*bitmap.pitch+j])) 188 | glyph_bitmap.extend(row[:bitmap.width]) 189 | elif pixel_mode == 2: # grey font, 255 bits per pixel 190 | for val in bitmap.buffer: 191 | glyph_bitmap.extend([1 if val > 127 else 0]) 192 | elif pixel_mode == 99: # PIL image 193 | for y in range(height): # TODO: reverse? 194 | for x in range(width): 195 | pixel = image.getpixel((x,y)) 196 | if isinstance(pixel, tuple): 197 | pixel = (sum(pixel)/3) # (r+g+b) / 3 - average 198 | # we code white as 0 while in pil it is (255,255,255) 199 | glyph_bitmap.append(0 if pixel > 127 else 1) 200 | else: 201 | # freetype-py should never give us a value not in (1,2) 202 | raise Exception("Unsupported pixel mode: {}".format(pixel_mode)) 203 | 204 | glyph_packed = [] 205 | for word in grouper(32, glyph_bitmap, 0): 206 | w = 0 207 | for index, bit in enumerate(word): 208 | w |= bit << index 209 | glyph_packed.append(struct.pack(' OFFSET_TABLE_MAX_SIZE: 239 | print "error: %d > 127" % bucket_sizes[glyph_hash] 240 | return bucket_sizes 241 | 242 | def add_glyph(codepoint, next_offset, gindex, glyph_indices_lookup): 243 | offset = next_offset 244 | if gindex not in glyph_indices_lookup: 245 | glyph_bits = self.glyph_bits(gindex, codepoint) 246 | glyph_indices_lookup[gindex] = offset 247 | self.glyph_table.append(glyph_bits) 248 | next_offset += len(glyph_bits) 249 | else: 250 | offset = glyph_indices_lookup[gindex] 251 | 252 | if (codepoint > MAX_2_BYTES_CODEPOINT): 253 | self.codepoint_bytes = 4 254 | 255 | self.number_of_glyphs += 1 256 | return offset, next_offset, glyph_indices_lookup 257 | 258 | def codepoint_is_in_subset(codepoint): 259 | if (codepoint not in (WILDCARD_CODEPOINT, ELLIPSIS_CODEPOINT)): 260 | if self.regex is not None: 261 | if self.regex.match(unichr(codepoint)) is None: 262 | return False 263 | if codepoint not in self.codepoints: 264 | return False 265 | return True 266 | 267 | glyph_entries = [] 268 | # MJZ: The 0th offset of the glyph table is 32-bits of 269 | # padding, no idea why. 270 | self.glyph_table.append(struct.pack(' self.max_glyphs): 286 | break 287 | 288 | if (codepoint is WILDCARD_CODEPOINT): 289 | raise Exception('Wildcard codepoint is used for something else in this font') 290 | 291 | if (gindex is 0): 292 | raise Exception('0 index is reused by a non wildcard glyph') 293 | 294 | if (codepoint_is_in_subset(codepoint)): 295 | offset, next_offset, glyph_indices_lookup = add_glyph(codepoint, next_offset, gindex, glyph_indices_lookup) 296 | glyph_entries.append((codepoint, offset)) 297 | 298 | if self.isdir: 299 | gindex += 1 300 | if gindex >= len(self.codepoints): 301 | gindex = None 302 | else: 303 | codepoint = self.codepoints[gindex-1] 304 | else: 305 | codepoint, gindex = self.face.get_next_char(codepoint, gindex) 306 | 307 | # Make sure the entries are sorted by codepoint 308 | sorted_entries = sorted(glyph_entries, key=lambda entry: entry[0]) 309 | hash_bucket_sizes = build_offset_tables(sorted_entries) 310 | build_hash_table(hash_bucket_sizes) 311 | 312 | def bitstring(self): 313 | btstr = self.fontinfo_bits() 314 | btstr += ''.join(self.hash_table) 315 | for table in self.offset_tables: 316 | btstr += ''.join(table) 317 | btstr += ''.join(self.glyph_table) 318 | 319 | return btstr 320 | 321 | def convert_to_h(self): 322 | to_file = os.path.splitext(self.ttf_path)[0] + '.h' 323 | f = open(to_file, 'wb') 324 | f.write("#pragma once\n\n") 325 | f.write("#include \n\n") 326 | f.write("// TODO: Load font from flash...\n\n") 327 | self.build_tables() 328 | bytes = self.bitstring() 329 | generate_c_byte_array.write(f, bytes, self.name) 330 | f.close() 331 | return to_file 332 | 333 | def convert_to_pfo(self, pfo_path=None): 334 | to_file = pfo_path if pfo_path else (os.path.splitext(self.ttf_path)[0] + '.pfo') 335 | with open(to_file, 'wb') as f: 336 | self.build_tables() 337 | f.write(self.bitstring()) 338 | return to_file 339 | 340 | def cmd_pfo(args): 341 | max_glyphs = MAX_GLYPHS_EXTENDED if args.extended else MAX_GLYPHS 342 | f = Font(args.input_ttf, args.height, max_glyphs, args.legacy) 343 | if (args.tracking): 344 | f.set_tracking_adjust(args.tracking) 345 | if (args.filter): 346 | f.set_regex_filter(args.filter) 347 | if (args.list): 348 | f.set_codepoint_list(args.list) 349 | f.convert_to_pfo(args.output_pfo) 350 | 351 | def cmd_header(args): 352 | f = Font(args.input_ttf, args.height, MAX_GLYPHS, args.legacy) 353 | if (args.filter): 354 | f.set_regex_filter(args.filter) 355 | f.convert_to_h() 356 | 357 | def process_all_fonts(): 358 | font_directory = "ttf" 359 | font_paths = [] 360 | for _, _, filenames in os.walk(font_directory): 361 | for filename in filenames: 362 | if os.path.splitext(filename)[1] == '.ttf': 363 | font_paths.append(os.path.join(font_directory, filename)) 364 | 365 | header_paths = [] 366 | for font_path in font_paths: 367 | f = Font(font_path, 14) 368 | print "Rendering {0}...".format(f.name) 369 | f.convert_to_pfo() 370 | to_file = f.convert_to_h() 371 | header_paths.append(os.path.basename(to_file)) 372 | 373 | f = open(os.path.join(font_directory, 'fonts.h'), 'w') 374 | print>>f, '#pragma once' 375 | for h in header_paths: 376 | print>>f, "#include \"{0}\"".format(h) 377 | f.close() 378 | 379 | def process_cmd_line_args(): 380 | parser = argparse.ArgumentParser(description="Generate pebble-usable fonts from ttf files") 381 | subparsers = parser.add_subparsers(help="commands", dest='which') 382 | 383 | pbi_parser = subparsers.add_parser('pfo', help="make a .pfo (pebble font) file") 384 | pbi_parser.add_argument('--extended', action='store_true', help="Whether or not to store > 256 glyphs") 385 | pbi_parser.add_argument('height', metavar='HEIGHT', help="Height at which to render the font") 386 | pbi_parser.add_argument('--tracking', type=int, help="Optional tracking adjustment of the font's horizontal advance") 387 | pbi_parser.add_argument('--filter', help="Regex to match the characters that should be included in the output") 388 | pbi_parser.add_argument('--list', help="json list of characters to include") 389 | pbi_parser.add_argument('--legacy', action='store_true', help="use legacy rasterizer (non-mono) to preserve font dimensions") 390 | pbi_parser.add_argument('input_ttf', metavar='INPUT_TTF', help="The ttf to process") 391 | pbi_parser.add_argument('output_pfo', metavar='OUTPUT_PFO', help="The pfo output file") 392 | pbi_parser.set_defaults(func=cmd_pfo) 393 | 394 | pbh_parser = subparsers.add_parser('header', help="make a .h (pebble fallback font) file") 395 | pbh_parser.add_argument('height', metavar='HEIGHT', help="Height at which to render the font") 396 | pbh_parser.add_argument('input_ttf', metavar='INPUT_TTF', help="The ttf to process - or directory with extracted font") 397 | pbh_parser.add_argument('output_header', metavar='OUTPUT_HEADER', help="The pfo output file") 398 | pbh_parser.add_argument('--filter', help="Regex to match the characters that should be included in the output") 399 | 400 | pbi_parser.set_defaults(func=cmd_pfo) 401 | pbh_parser.set_defaults(func=cmd_header) 402 | 403 | args = parser.parse_args() 404 | args.func(args) 405 | 406 | def main(): 407 | if len(sys.argv) < 2: 408 | # process all the fonts in the ttf folder 409 | process_all_fonts() 410 | else: 411 | # process an individual file 412 | process_cmd_line_args() 413 | 414 | 415 | if __name__ == "__main__": 416 | main() 417 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /libpatcher/parser.py: -------------------------------------------------------------------------------- 1 | # This is a parser for assembler listings (?) 2 | 3 | from . import asm 4 | from .mask import Mask 5 | from .block import Block 6 | from .patch import Patch 7 | 8 | __all__ = ['parseFile', 'ParseError', 'FilePos'] 9 | 10 | class FilePos: 11 | " This holds current line info (filename, line text, line number) " 12 | def __init__(self, filename, lnum=-1, line=''): 13 | self.filename = filename 14 | self.lnum = lnum 15 | self.line = line 16 | 17 | def setLine(self, lnum, line): 18 | self.lnum = lnum 19 | self.line = line 20 | 21 | def getLine(self): 22 | return self.line 23 | 24 | def getLnum(self): 25 | return self.lnum 26 | 27 | def clone(self): 28 | " Useful for instructions to hold exact position " 29 | return FilePos(self.filename, self.lnum, self.line) 30 | 31 | #def __repr__(self): 32 | # return "\"%s\"\n%s, line %s" % (self.line, self.filename, self.lnum+1) 33 | 34 | def __str__(self): 35 | return "%s, line %s" % (self.filename, self.lnum+1) 36 | 37 | class ParseError(Exception): 38 | def __init__(self, msg, pos): 39 | self.msg = msg 40 | self.pos = pos 41 | 42 | def __str__(self): 43 | return "%s: %s\n%s" % (str(self.pos), self.msg, self.pos.getLine()) 44 | 45 | def uncomment(line): 46 | """ Removes comment, if any, from line. Also strips line """ 47 | linewoc = '' # line without comment 48 | in_str = '' 49 | for c in line: 50 | if in_str: 51 | if c == in_str: 52 | in_str = '' 53 | else: 54 | if c in ';': # our comment character 55 | break 56 | elif c in '"\'': 57 | in_str = c 58 | linewoc += c 59 | return linewoc.strip() # remove leading and trailing spaces 60 | 61 | def parseInstruction(line, pos): 62 | """ 63 | This methods converts line from source file to Instruction 64 | (opcode and args?). 65 | """ 66 | try: 67 | opcode, arg = line.split(None, 1) 68 | except ValueError: # only one token 69 | opcode = line 70 | arg = '' 71 | 72 | # now parse args 73 | args = asm.List() 74 | reglist = None # here we'll collect register for current instruction 75 | s = '' # string repr of current arg 76 | t = None # type of current arg: 77 | # None (no current), n(numeric), "(quoted str), l(reg or label), '(charcode) 78 | br = False # whether we are in [] block 79 | rl = False # whether we are in {register list} block 80 | cp = None # previous char 81 | for c in arg+'\n': # \n will be processed as last character 82 | domore = False # if current character needs to be processed further 83 | if t is None: # state: no current arg 84 | domore = True 85 | elif t == '"': # quoted string 86 | if c == t: # end of string 87 | args.append(asm.Str(s)) 88 | s = '' 89 | t = None 90 | elif c == '\\': # backslash in string 91 | t += '\\' 92 | else: 93 | s += c 94 | elif t == '"\\': # state: backslash in quoted string 95 | c = c.replace('r', '\r') 96 | c = c.replace('n', '\n') 97 | s += c 98 | t = t[0] 99 | elif t == "'": # charcode 100 | if c == t and cp != c: # this is ' and something was already added 101 | # notice that ''' is valid construction for ord("'") char code 102 | t = None 103 | else: 104 | args.append(asm.Num(ord(c))) 105 | elif t in ['n', 'ns', 'nm']: 106 | # number, maybe 0xHEX or 0bBINARY or 0octal, or numshift 107 | if c.isdigit() or c in 'aAbBcCdDeEfFxX': 108 | s += c 109 | else: 110 | domore = True # need to process current character further 111 | # for consistency with old version's behaviour, 112 | # treat all numeric 'db' arguments as hexadecimal 113 | if opcode == "db": 114 | s = "0x"+s 115 | try: 116 | if t == 'ns' and isinstance(args[-1], asm.Label): 117 | # numshift for label 118 | # args[-1] must exist and be label, 119 | # or else t would not be 'ns' 120 | args[-1].shift = int(s, 0) 121 | elif t in ['ns', 'nm']: 122 | newnum = asm.Num(s) 123 | oldnum = args[-1] 124 | rval = oldnum*newnum if t == 'nm' else oldnum+newnum 125 | rin = ( 126 | str(oldnum) + 127 | ('*' if t == 'nm' else '+' if newnum > 0 else '-') + 128 | str(newnum) 129 | ) 130 | args[-1] = asm.Num(rval, rin) 131 | else: # regular number 132 | args.append(asm.Num(s)) 133 | except ValueError: 134 | raise ParseError("Invalid number: %s" % s, pos) 135 | s = '' 136 | t = None 137 | elif t == 'l': # label or reg 138 | if c.isalnum() or c == '_' or (rl and c == '-'): 139 | s += c 140 | else: 141 | domore = True 142 | if rl: # in list of registers 143 | reglist.append(s, pos) 144 | # it will handle all validation itself 145 | else: 146 | if asm.Reg.is_reg(s): 147 | a = asm.Reg(s) 148 | else: 149 | a = asm.Label(s) 150 | args.append(a) 151 | s = '' 152 | t = None 153 | else: 154 | raise ValueError("Internal error: illegal type state %s" % t) 155 | 156 | if domore: # current character was not processed yet 157 | if(c.isdigit() or c == '-' or 158 | (opcode == "db" and c in "AaBbCcDdEeFf")): 159 | s += c 160 | if c == '-' and args and isinstance(args[-1], asm.Num): 161 | # shift value for number 162 | # FIXME: this will break stuff like MOV R0,4,-2 163 | # But is such stuff possible? 164 | t = 'ns' 165 | else: 166 | t = 'n' 167 | elif c.isalpha() or c == '_': 168 | s += c 169 | t = 'l' # label 170 | elif c == '+': # shift-value for label or number 171 | if args and isinstance(args[-1], (asm.Label, asm.Num)): 172 | t = 'ns' # number,shift 173 | else: 174 | raise ParseError("Unexpected +", pos) 175 | elif c == '*': # multiplier for number 176 | if args and isinstance(args[-1], asm.Num): 177 | t = 'nm' # number,multiplier 178 | else: 179 | raise ParseError("Unexpected *", pos) 180 | elif c in "'\"": # quoted str or charcode 181 | t = c 182 | elif c.isspace(): # including last \n 183 | continue # skip 184 | elif c == ',': 185 | continue # skip - is it a good approach? 186 | # allows both "MOV R0,R1" and "MOV R1 R1" 187 | elif c == '[': 188 | if br: 189 | raise ParseError("Nested [] are not supported", pos) 190 | br = True 191 | gargs = args 192 | args = asm.List() 193 | elif c == ']': 194 | if not br: 195 | raise ParseError("Unmatched ]", pos) 196 | gargs.append(args) 197 | args = gargs 198 | br = False 199 | elif c == '{': 200 | if rl: 201 | raise ParseError("Already in register list", pos) 202 | rl = True 203 | reglist = asm.RegList() 204 | elif c == '}': 205 | if not rl: 206 | raise ParseError("Unmatched }", pos) 207 | args.append(reglist) 208 | reglist = None 209 | rl = False 210 | else: 211 | raise ParseError("Bad character: %c" % c, pos) 212 | 213 | cp = c 214 | # now let's check that everything went clean 215 | if t: 216 | raise ParseError("Unterminated string? %c" % t, pos) 217 | if br: 218 | raise ParseError("Unmatched '['", pos) 219 | 220 | try: 221 | return asm.findInstruction(opcode, args, pos) 222 | except IndexError: 223 | raise ParseError("Unknown instruction: %s %s" % 224 | (opcode, ','.join([repr(x) for x in args])), pos) 225 | 226 | def parseBlock(f, pos, definitions, if_state, patch): 227 | """ 228 | Parses one mask from patch file. 229 | Returns results (mask and block contents) as tuple 230 | """ 231 | 232 | # mask's starting position 233 | mpos = None 234 | # mask's tokens 235 | mask = [] 236 | # mask offset (for @) 237 | mofs = 0 238 | # current mask item (bytestring) 239 | bstr = b'' 240 | # current mask item (integer, number of bytes to skip) 241 | bskip = 0 242 | 243 | # and to be used when in block: 244 | instructions = None 245 | 246 | for lnum, line in enumerate(f, pos.getLnum()+1): 247 | pos.setLine(lnum, line.strip()) 248 | line = uncomment(line) 249 | if not line: # skip empty lines 250 | continue 251 | 252 | if line[0] == '#': 253 | tokens = line.split() 254 | cmd, args = tokens[0], tokens[1:] 255 | # these will not depend on if_state... 256 | if cmd in ["#ifdef", "#ifndef", "#ifval", "#ifnval"]: 257 | if not args: 258 | raise ParseError("%s requires at least one argument" % cmd, 259 | pos) 260 | newstate = 'n' in cmd # False for 'ifdef', etc. 261 | if "val" in cmd: 262 | vals = list(definitions.values()) 263 | # "OR" logic, as one can implement "AND" with nested #ifdef's 264 | # so any matched arg stops checking 265 | for a in args: 266 | if(("def" in cmd and a in definitions) or 267 | ("val" in cmd and a in vals)): 268 | newstate = not newstate 269 | break 270 | if_state.append(newstate) 271 | continue 272 | elif cmd == "#else": 273 | if len(if_state) <= 1: 274 | raise ParseError("Unexpected #else", pos) 275 | if_state[-1] = not if_state[-1] 276 | continue 277 | elif cmd == "#endif": 278 | if_state.pop() # remove latest state 279 | if not if_state: 280 | raise ParseError("Unmatched #endif", pos) 281 | continue 282 | elif cmd == "#ver": # desired FW version 283 | # FIXME: don't bail out on invalid values, just warn 284 | if not args: 285 | raise ParseError( 286 | "At least one argument required for #ver", pos) 287 | lo = int(args[0]) 288 | if args[1:]: 289 | hi = int(args[1]) 290 | else: 291 | hi = 65535 # max version 292 | # for now just store that version as a variable 293 | definitions['ver'] = str(lo) 294 | # TODO: perform some tests for this patch version 295 | continue 296 | # ...now check if_state... 297 | if False in if_state: 298 | continue # #define must only work if this is met 299 | # ...and following will depend on it 300 | if cmd in ["#define", "#default"]: 301 | # default is like define but will not override already set value 302 | if not args: 303 | raise ParseError( 304 | "At least one argument required for #define", pos) 305 | name = args[0] 306 | val = True 307 | if args[1:]: 308 | val = line.split(None, 2)[2] # remaining args as string 309 | # always set for #define, 310 | # and only if unset / just True if #default 311 | if cmd == "#define" \ 312 | or name not in definitions \ 313 | or definitions[name] == True: 314 | definitions[name] = val 315 | elif cmd == "#include": 316 | if not args: 317 | raise ParseError("#include requires an argument", pos) 318 | arg = line.split(None, 1)[1] # all args as a string 319 | import os.path 320 | if not os.path.isabs(arg): 321 | arg = os.path.join(os.path.dirname(f.name), arg) 322 | newf = open(arg, 'r') 323 | # parse this file into this patch's library patch. 324 | # If this is already library patch, 325 | # its library property will return itself. 326 | parseFile(newf, definitions, patch=patch.library) 327 | else: 328 | raise ParseError("Unknown command: %s" % cmd, pos) 329 | continue # to next line 330 | 331 | # and now for non-# lines 332 | if False in if_state: 333 | continue # skip any code if current condition is not met 334 | 335 | # process ${definitions} everywhere 336 | for d, v in definitions.items(): 337 | if isinstance(v, str) and '${'+d+'}' in line: 338 | line = line.replace('${'+d+'}', v) 339 | 340 | if instructions is None: # not in block, reading mask 341 | # read mask: it consists of 00 f7 items, ? ?4 items, and "strings" 342 | tokens = line.split('"') 343 | if len(tokens) % 2 == 0: 344 | raise ParseError("Unterminated string", pos) 345 | if not mpos: 346 | mpos = pos.clone() # save starting position of mask 347 | is_str = False 348 | for tokennum, token in enumerate(tokens): 349 | if is_str: 350 | if bskip: 351 | mask.append(bskip) 352 | bskip = 0 353 | bstr += token.encode() 354 | else: 355 | # process $definitions only outside of "strings" and 356 | # outside of {blocks} 357 | # FIXME: $definitions inside of {blocks} [and in "strings"?] 358 | for d, v in list(definitions.items()): 359 | if isinstance(v, str) and '$'+d in token: 360 | # ^^ FIXME: $var and $variable 361 | token = token.replace('$'+d, v) 362 | 363 | ts = token.split() 364 | for t in ts: 365 | if len(t) == 2 and t.isalnum(): 366 | if bskip: 367 | mask.append(bskip) 368 | bskip = 0 369 | try: 370 | # convert '65' to b'A' 371 | c = bytes(bytearray([int(t, 16)])) 372 | except ValueError: 373 | raise ParseError("Bad token: %s" % t, pos) 374 | bstr += c 375 | elif t[0] == '?': 376 | if len(t) == 1: 377 | count = 1 378 | else: 379 | try: 380 | count = int(t[1:]) 381 | except ValueError: 382 | raise ParseError("Bad token: %s" % t, pos) 383 | if bstr: 384 | mask.append(bstr) 385 | bstr = b'' 386 | bskip += count 387 | elif t == '@': 388 | if mofs: 389 | raise ParseError("Duplicate '@'", pos) 390 | mofs = sum([ 391 | len(x) if isinstance(x, bytes) else x 392 | for x in mask 393 | ]) + len(bstr) + bskip 394 | elif t == '{': 395 | if bstr: 396 | mask.append(bstr) 397 | bstr = b'' 398 | if bskip: 399 | print(mask, bstr, bskip) 400 | raise ParseError( 401 | "Internal error: " 402 | "both bstr and bskip used", pos) 403 | if bskip: 404 | mask.append(bskip) 405 | bskip = 0 406 | line = '"'.join(tokens[tokennum+1:]) 407 | # prepare remainder for next if 408 | instructions = [] # this will also break for's 409 | else: 410 | raise ParseError("Bad token: %s" % t, pos) 411 | if instructions is not None: # if entered block 412 | break 413 | is_str = not is_str 414 | if instructions is not None: # if entered block 415 | break 416 | # mask read finished. Now read block content, if in block 417 | if instructions is not None and line: 418 | # still have something in current line 419 | if line.startswith('}'): 420 | # FIXME: what to do with remainder? 421 | remainder = line[1:] 422 | if remainder: 423 | print("Warning: spare characters after '}', " 424 | "will ignore: %s" % remainder) 425 | return Block(patch, Mask(mask, mofs, mpos), instructions) 426 | 427 | # plain labels: 428 | label = line.split(None, 1)[0] 429 | if label.endswith(':'): # really label 430 | line = line.replace(label, '', 1).strip() # remove it 431 | instructions.append(asm.LabelInstruction(label[:-1], pos)) 432 | if not line: # had only the label 433 | continue 434 | 435 | instr = parseInstruction(line, pos) 436 | instructions.append(instr) 437 | if mask or bstr or bskip: 438 | raise ParseError("Unexpected end of file", pos) 439 | return None 440 | 441 | def parseFile(f, definitions=None, patch=None, libpatch=None): 442 | """ 443 | Parses patch file. 444 | Definitions dictionary is used for #define and its companions. 445 | If patch was not provided, it will be created, 446 | in which case libpatch (patch for includes) must be provided. 447 | """ 448 | if definitions is None: 449 | definitions = {} 450 | if not patch: 451 | if not libpatch: 452 | raise ValueError("Neither patch nor libpatch were provided") 453 | patch = Patch(f.name, libpatch) 454 | 455 | # for #commands: 456 | if_state = [True] # this True should always remain there 457 | 458 | pos = FilePos(f.name) 459 | while True: 460 | block = parseBlock(f, pos, definitions, if_state, patch) 461 | if not block: 462 | break 463 | patch.blocks.append(block) 464 | 465 | if len(if_state) != 1: 466 | raise ValueError('#ifdef count mismatch! '+str(if_state)) 467 | 468 | return patch 469 | -------------------------------------------------------------------------------- /translate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This script updates strings in tintin_fw.bin file 3 | 4 | import sys 5 | from struct import pack, unpack 6 | 7 | # data is a loaded tintin_fw file contents 8 | data = "" 9 | # datap is an original file converted to list of integers (pointers) 10 | datap = [] 11 | # datar is data to return 12 | datar = "" 13 | 14 | EOF = 0x70000 - 48 15 | 16 | # where to write logs 17 | log = sys.stdout 18 | 19 | def is_valid_pointer(n): 20 | """ Checks if a number looks like a valid pointer """ 21 | return n >= 0x08010000 and n < (0x08010000+len(data)) 22 | 23 | def is_string_pointer(ptr): 24 | """ 25 | Checks if a number points to somthing similar to string; 26 | returns string (maybe empty) if it is a valid string or False otherwise 27 | """ 28 | def is_string_char(c): 29 | return c in "\t\r\n" or (c >= ' ' and c <= '~') # tab, endline or printable latin 30 | 31 | if not is_valid_pointer(ptr): 32 | return False 33 | 34 | for i in range(ptr-0x08010000, len(data)): 35 | if data[i] == '\0': 36 | #return i - (ptr-0x08010000) # line ended without non-string chars, return strlen 37 | return data[ptr-0x08010000:i] # line ended without non-string chars, return it 38 | if not is_string_char(data[i]): 39 | return False # encountered non-string char, return False 40 | return False # reched end of file, return False 41 | 42 | def find_all_strings(): 43 | """ 44 | Scans input file for all referenced strings. 45 | Returns array of tuples: (offset, value, string) 46 | """ 47 | pointers = [] # tuples: offset to pointer, offset to its string, the string itself 48 | for i, n in enumerate(datap): 49 | s = is_string_pointer(n) 50 | if s: 51 | #print >>log, i,n,s 52 | pointers.append((i, n, s)) 53 | return pointers 54 | 55 | def find_pointers_to_offset(offset): 56 | """ 57 | Finds all pointers to given offset; returns offsets to them 58 | """ 59 | ptr = offset + 0x08010000 60 | return [i for i,v in enumerate(datap) if v == ptr] 61 | 62 | def find_string_offsets(s): 63 | """ Returns list of offsets to given string """ 64 | ret = [] 65 | s = s + '\0' # string in file must end with \0 ! 66 | i = data.find(s) 67 | while i != -1: 68 | ret.append(i) 69 | i = data.find(s, i+1) 70 | return ret 71 | 72 | def parse_args(): 73 | def hexarg(x): 74 | try: 75 | return x.decode("hex") 76 | except: 77 | return int(x,0) 78 | import argparse 79 | parser = argparse.ArgumentParser( 80 | description="Translation helper for Pebble firmware", 81 | epilog="Strings format:\nOriginal String:=Translated String\n"+ 82 | "Any newlines in strings must be replaced with '\\n', any backslashes with '\\\\'.\n"+ 83 | "Lines starting with # are comments, so if you need # at line start replace it with \\#.\n"+ 84 | "Lines starting with ! are those which may be translated 'in place' "+ 85 | "(for strings which have free space after them).") 86 | parser.add_argument("tintin", nargs='?', default="tintin_fw.bin", type=argparse.FileType("rb"), 87 | help="Input tintin_fw file, defaults to tintin_fw.bin") 88 | parser.add_argument("output", nargs='?', default=sys.stdout, type=argparse.FileType("wb"), 89 | help="Output file, defaults to stdout") 90 | parser.add_argument("-s", "--strings", default=sys.stdin, type=argparse.FileType("r"), 91 | help="File with strings to translate, by default will read from stdin") 92 | group = parser.add_mutually_exclusive_group() 93 | group.add_argument("-t", "--txt", dest="old_format", action="store_true", 94 | help="Use old (custom, text-based) format for strings") 95 | group.add_argument("-g", "--gettext", "--po", dest="old_format", action="store_false", 96 | help="Use gettext's PO format for strings (default)") 97 | parser.add_argument("-x", "--exclude", "--exclude-strings", action="append", metavar="REF", default=[], 98 | help="Don't translate strings with given reference ID (only for PO files). "+ 99 | "This option may be passed several times.") 100 | parser.add_argument("-p", "--print-only", action="store_true", 101 | help="Don't translate anything, just print out all referenced strings from input file") 102 | parser.add_argument("-f", "--force", action="store_true", 103 | help="Disable safety checks for inplace translations") 104 | parser.add_argument("-r", "--range", action="append", nargs=2, metavar=("start","end"), type=lambda x: int(x,0), 105 | dest="ranges", 106 | help="Offset range to use for translated messages (in addition to space at the end of file). "+ 107 | "Use this to specify unneeded firmware parts, e.g. debugging console or disabled watchfaces. "+ 108 | "Values may be either 0xHex, Decimal or 0octal. This option may be repeated.") 109 | parser.add_argument("-R", "--range-mask", action="append", nargs=3, metavar=("start","end","size"), 110 | type=hexarg, dest="ranges", 111 | help="Ranges defined by signatures: START and END are hex signatures of first and last bytes "+ 112 | "of range. For example, -R 48656C6C6F 3031323334 0x243 will select range of 0x243 bytes "+ 113 | "starting with 'Hello' and ending with '12345'. "+ 114 | "You must always specify range size for checking.") 115 | parser.add_argument("-e", "--end", action="append_const", const="append", dest="ranges", 116 | help="Use space between end of firmware and 0x08080000 (which seems to be the last address "+ 117 | "allowed) to store strings. Note that this will change size of firmware binary "+ 118 | "which may possible interfere with iOS Pebble app.") 119 | parser.add_argument("-u", "--reuse-ranges", action="store_true", 120 | help="Reuse freed (fully moved on translation) strings as ranges for next strings. "+ 121 | "This may slow process as every character needs to be checked for possible pointers.") 122 | return parser.parse_args() 123 | 124 | def read_strings_txt(f): 125 | strings = {} 126 | keys = [] 127 | inplace = [] 128 | for line in f: 129 | line = line[:-1] # remove trailing \n 130 | if len(line) == 0 or line.startswith('#'): # comment or empty 131 | continue 132 | line = line.replace('\\n', '\n').replace('\\#', '#').replace('\\\\', '\\') # unescape 133 | if not ':=' in line: 134 | print >>log, "Warning: bad line in strings:", line 135 | continue 136 | left, right = line.split(':=', 1) 137 | if not right: # empty 138 | print >>log, "Warning: translation is empty; ignoring:", line 139 | continue 140 | if ':=' in right: 141 | print >>log, "Warning: ambigous line in strings:", line 142 | continue 143 | if left.startswith('!'): # inplace translating 144 | left = left[1:] 145 | inplace.append(left) 146 | if left in strings: 147 | print >>log, "Warning: duplicate string, ignoring:", line 148 | print >>log, "Original: "+strings[left] 149 | continue 150 | strings[left] = right 151 | keys.append(left) 152 | return strings, keys, inplace 153 | 154 | def read_strings_po(f, exclude=[]): 155 | # TODO : multiline strings w/o \n 156 | def parsevalline(line, kwlen): # kwlen is keyword length 157 | line = line[kwlen :].strip() # remove 'msgid' and spaces 158 | if line[0] == '"': 159 | if line[-1] != '"': 160 | print >>log, "Warning! Expected '\"' not found in line %d" % line 161 | line = line[1 :-1] # remove quotes 162 | line = line.replace('\\n', '\n').replace('\\"', '"').replace('\\\\', '\\') # unescape - TODO: test 163 | return line 164 | 165 | strings = {} 166 | keys = [] 167 | inplaces = [] 168 | 169 | # our scratchpad 170 | left = None 171 | right = None 172 | inplace = False 173 | ref = None 174 | context = None 175 | 176 | skipnum = 0 # number of excluded lines 177 | for line in f: 178 | line = line[:-1] # remove tralining \n 179 | if len(line) == 0 : # end of record 180 | if ref in exclude: 181 | #print >>log, "Line %s has ref <%s> which is requested to be excluded; skipping" % (repr(left), ref) 182 | skipnum += 1 183 | elif left: # or else, if left is empty -> ignoring 184 | if right: # both left and right are provided 185 | # FIXME: support inplace for contexted lines? do we need this at all? 186 | if left == right: 187 | print >>log, "Translation = original, ignoring line %s" % left 188 | elif left in keys: 189 | if context or type(strings[left]) is list: # this or previous is contexted 190 | if type(strings[left]) is not list: 191 | strings[left] = [strings[left]] # convert to list 192 | # because POEditor omits lines with msgctxt=0 193 | if context == None: 194 | context = [0] # for the same reason as above 195 | for c in context: 196 | if len(strings[left]) <= c: 197 | strings[left] += [None] * (len(strings[left])-c) 198 | strings[left].append(right) 199 | else: # have such item already 200 | if strings[left][c]: 201 | print >>log, "Warning: duplicate contexted line %s @ %d" % (left, c) 202 | else: 203 | strings[left][c] = right 204 | else: 205 | print >>log, "Warning: ignoring duplicate line %s" % left 206 | else: 207 | keys.append(left) 208 | if context != None: 209 | r = [None] * (max(context)+1) 210 | for c in context: 211 | r[c] = right 212 | strings[left] = r 213 | else: 214 | strings[left] = right 215 | if inplace: 216 | inplaces.append(left) 217 | else: # only left provided -> line untranslated, ignoring 218 | print >>log, "Ignoring untranslated line %s" % left 219 | # now clear scratchpad 220 | left = None 221 | right = None 222 | inplace = False 223 | ref = None 224 | context = None 225 | elif line.startswith("#,"): # flags 226 | flags = [x.strip() for x in line[2 :].split(",")] # parse flags, removing leading "#," 227 | if "fuzzy" in flags: 228 | inplace = True 229 | # ignore all other flags, if any 230 | elif line.startswith("#:"): # reference 231 | ref = line[2 :].strip() 232 | elif line.startswith("#"): # comment, etc 233 | pass # ignore 234 | elif line.startswith("msgid"): 235 | left = parsevalline(line, 5) 236 | elif line.startswith("msgstr"): 237 | right = parsevalline(line, 6) 238 | elif line.startswith("msgctxt"): 239 | context = [] 240 | for num in parsevalline(line, 7).split(','): 241 | # inplace flag, to replace "fuzzy" usage 242 | if num.lower() == 'inplace': 243 | inplace = True 244 | continue 245 | # should be a number 246 | try: 247 | context.append(int(num)) 248 | except ValueError: 249 | print >>log, "*** ERROR: %s is not an integer " 250 | "or comma-separated list of integers " 251 | "and not a supported flag (line %s)" % (num, line) 252 | if not context: # only inplace flag 253 | context = None 254 | elif line.startswith('"'): # continuation? 255 | if right is not None: 256 | right += parsevalline(line, 0) 257 | elif left is not None: 258 | left += parsevalline(line, 0) 259 | else: 260 | print >>log, "Warning: unexpected continuation line: %s" % line 261 | else: 262 | print >>log, "Warning: unexpected line in input: %s" % line 263 | if skipnum: 264 | print >>log, "Excluded %d lines as requested" % skipnum 265 | return strings, keys, inplaces 266 | 267 | def translate_fw(args): 268 | global data, datap, datar, log 269 | if args.output == log == sys.stdout: 270 | log = sys.stderr # if writing new tintin to sdout, print >>log, all messages to stderr to avoid cluttering 271 | 272 | # load source fw: 273 | data = args.tintin.read() 274 | datar = data # start from just copy, later will change it 275 | # convert to pointers: 276 | for i in range(0, len(data)-3): # each 4-aligned int; -3 to avoid last (partial) value. 277 | # Also include not-aligned values 278 | n = unpack("I", data[i:i+4])[0] 279 | datap.append(n) 280 | 281 | ranges = [] 282 | def addrange(start, end): 283 | """ Check range for clashes and then add it to ranges list """ 284 | for r in list(ranges): 285 | if r[0] == r[1]: # singular range 286 | ranges.remove(r) # remove as it is unneeded 287 | continue # to next range 288 | if start == r[0] and end == r[1]: # duplicate 289 | print >>log, "### Duplicate range %x-%x, skipping." % (start, end) 290 | return 291 | if start >= r[0] and end <= r[1]: # fully inside; ignore 292 | print >>log, "### Range clash!! This must be an error! Range %x-%x fits within %x-%x; ignoring" % ( 293 | start, end, r[0], r[1]) 294 | return 295 | if start <= r[0] and end >= r[1]: 296 | # fully outside; replace. FIXME : this might introduce clashes with other ranges 297 | print >>log, "### Range clash!! This must be an error! Range %x-%x contained in %x-%x; replacing" % ( 298 | start, end, r[0], r[1]) 299 | r[0] = start 300 | r[1] = end 301 | return 302 | if start <= r[0] and end > r[0]: # clash with beginning; truncate 303 | print >>log, "### Range clash!! This must be an error! Range %x-%x clashes with %x-%x; truncating" % ( 304 | start, end, r[0], r[1]) 305 | end = r[0] 306 | if start < r[1] and end >= r[1]: # clash with end; truncate 307 | print >>log, "### Range clash!! This must be an error! Range %x-%x clashes with %x-%x; truncating" % ( 308 | start, end, r[0], r[1]) 309 | start = r[1] 310 | for r in ranges: # another loop for neighbours - now when we surely have no clashes 311 | if r[1] == start: 312 | print >>log, " # Range neighbourhood, merging %x-%x to %x-%x" % ( 313 | start, end, r[0], r[1]) 314 | r[1] = end 315 | return 316 | if end == r[0]: 317 | print >>log, " # Range neighbourhood, merging %x-%x to %x-%x" % ( 318 | start, end, r[0], r[1]) 319 | r[0] = start 320 | return 321 | ranges.append([start, end]) 322 | for r in args.ranges or []: 323 | if len(r) == 3: # signature-specified range - convert it to offsets 324 | if type(r[0]) != str or type(r[1]) != str or type(r[2]) != int: 325 | print >>log, "-Warning: invalid range mask specification %s; ignoring" % repr(r) 326 | continue 327 | start = data.find(r[0]) 328 | if start < 0: 329 | print >>log, "-Warning: starting mask %s not found, ignoring this range" % repr(r[0]) 330 | continue 331 | end = start+data[start:].find(r[1]) 332 | if end < start: 333 | print >>log, "-Warning: start at 0x%X, ending mask %s not found, ignoring this range" % (start, repr(r[1])) 334 | continue 335 | length = end + len(r[1]) - start 336 | if length != r[2]: 337 | print >>log, ("-Warning: length mismatch for range %s..%s (0x%X..0x%X), expected %d, found %d; "+ 338 | "ignoring this range") % (repr(r[0]), repr(r[1]), start, end, r[2], length) 339 | continue 340 | end += len(r[1]) # append ending mask size 341 | addrange(start, end) 342 | elif len(r) == 2: 343 | addrange(r[0], r[1]) 344 | elif r == "append": 345 | start = len(datar) 346 | end = EOF 347 | if start < end: 348 | addrange(start, end) 349 | else: 350 | print >>log, "Warning: cannot append to end of file because its size is >= 0x70000 (max fw size)" 351 | else: 352 | print >>log, "?!? confused: unexpected range", r 353 | if ranges: 354 | print >>log, "Using following ranges:" 355 | for r in ranges: 356 | print >>log, " * 0x%X..0x%X (%d bytes)" % (r[0], r[1], r[1]-r[0]) 357 | elif len(ranges) == 0: 358 | print >>log, "WARNING: no usable ranges!" 359 | 360 | if args.print_only: 361 | print >>log, "Scanning tintin_fw..." 362 | ptrs = find_all_strings() 363 | print >>log, "Found %d referenced strings" % len(ptrs) 364 | for p in ptrs: 365 | args.output.write(p[2]+'\n') 366 | args.output.close() 367 | sys.exit(0) 368 | 369 | if args.old_format: 370 | strings, keys, inplace = read_strings_txt(args.strings) 371 | else: 372 | strings, keys, inplace = read_strings_po(args.strings, args.exclude) 373 | print >>log, "Got %d valid strings to translate" % len(strings) 374 | if not strings: 375 | print >>log, "NOTICE: No strings, nothing to do! Will just duplicate fw" 376 | 377 | npass = 0 378 | while True: 379 | untranslated = 0 # number of strings we could not translate because of range lack 380 | translated = 0 # number of strings translated in this pass 381 | for key in list(keys): # use clone to avoid breaking on removal 382 | val = strings[key] # string or list 383 | vals = val if type(val) is list else [val] 384 | print >>log, "Processing", repr(key) 385 | os = find_string_offsets(key) 386 | if not os: # no such string 387 | print >>log, " -- not found, ignoring" 388 | continue 389 | if type(val) is list: # contexted 390 | if len(os) < len(val): 391 | print >>log, " ** Warning: too many contexts given for %s" % key 392 | elif len(os) > len(val): 393 | #print >>log, " ** Warning: too few contexts given for %s" % key 394 | # not all contexts may need to be translated 395 | val += [None] * (len(os) - len(val)) # pad it with Nones to avoid Index out of bounds 396 | mustrepoint=[] # list of "inplace" key occurances which cannot be replaced inplace 397 | if (type(val) is not list # val is not contexted 398 | and (len(val) <= len(key) or key in inplace)): # can just replace 399 | # but will not replace contexted vals 400 | print >>log, " -- found %d occurance(s), replacing" % len(os) 401 | for idx, o in enumerate(os): 402 | doreplace = True 403 | print >>log, " -- 0x%X:" % o, 404 | if key in inplace and len(val) > len(key) and not args.force: # check that "rest" has only \0's 405 | rest = datar[o+len(key):o+32] 406 | for i in range(len(rest)): 407 | if rest[i] != '\0': 408 | print >>log, " ** SKIPPING because overwriting is unsafe here; use -f to override. "+\ 409 | "Will try to rewrite pointers." 410 | mustrepoint.append(o) 411 | doreplace = False # don't replace this occurance 412 | break # break inner loop 413 | # Now check for optimized links: 414 | # Hello_World\0 415 | # ^p1 ^p2 416 | # - here we cannot just translate inplace "Hello_World" to 417 | # "Bonjour" as it world result in the following: 418 | # Bonjour\0ld\0 419 | # ^p1 ^p2 420 | for i in range(o+1, # there definitely is a pointer to o 421 | o+min(len(key)+1,len(val)+1)): # use min because we don't need to worry about the rest 422 | if find_pointers_to_offset(i): 423 | print >>log, " ** SKIPPING "+\ 424 | "because there are links to the rest of the string due to optimization; "+\ 425 | "will try to rewrite pointers." 426 | mustrepoint.append(o) 427 | doreplace = False 428 | break 429 | if not doreplace: 430 | continue # skip to next occurance, this will be handled later 431 | oldlen = len(datar) 432 | datar = datar[0:o] + val + '\0' + datar[o+len(val)+1:] 433 | if len(datar) != oldlen: 434 | raise AssertionError("Length mismatch") 435 | print >>log, "OK" # this occurance replaced successfully 436 | if not mustrepoint: 437 | keys.remove(key) # this string is translated 438 | translated += 1 439 | continue # everything replaced fine for that key 440 | # we are here means that new string is longer than old (and not an 441 | # inplace one - or at least has one non-inplace-possible occurance) 442 | # so will add it to end of tintin file or to ranges 443 | print >>log, " -- %s %d occurance(s), looking for pointers" % ("still have" if mustrepoint else "found", len(mustrepoint or os)) 444 | ps = [] 445 | for o in list(mustrepoint) or list(os): # use mustrepoint if it is not empty 446 | newps = find_pointers_to_offset(o) 447 | ps.extend(newps) 448 | if not newps: 449 | print >>log, " !? String at 0x%X is unreferenced, will ignore! (must be partial or something)" % o 450 | # and remove it from list (needed for reuse_ranges) 451 | if mustrepoint: 452 | mustrepoint.remove(o) 453 | else: 454 | os.remove(o) 455 | if not ps: 456 | print >>log, " !! No pointers to that string, cannot translate!" 457 | continue 458 | print >>log, " == found %d ptrs; appending or inserting string and updating them" % len(ps) 459 | 460 | stored = {} 461 | key_translated = True 462 | for idx, v in enumerate(vals): # for each contexted value (or for the only value) 463 | if v == None: 464 | continue # skip empty ones 465 | if idx >= len(ps): 466 | print >>log, " *! Warning: no pointers for given context %d" % idx 467 | continue 468 | 469 | if v in stored: # such string was already stored 470 | newps = stored[v] 471 | print >>log, " -- using stored ptr" 472 | else: 473 | r = None # range to use 474 | for rx in sorted(ranges, key=lambda r: r[1]-r[0]): 475 | if rx[1]-rx[0] >= len(v)+1: # this range have enough space 476 | r = rx 477 | break # break inner loop (on ranges) 478 | if not r: # suitable range not found 479 | print >>log, " ## Notice: no (more) ranges available large enough for this phrase. Will skip it." 480 | untranslated += 1 481 | key_translated = False 482 | continue # to next value variant 483 | print >>log, " -- using range 0x%X-0x%X%s" % (r[0],r[1]," (end of file)" if r[1] == EOF else "") 484 | newp = r[0] 485 | oldlen = len(datar) 486 | datar = datar[0:newp] + v + '\0' + datar[newp+len(v)+1:] 487 | if len(datar) != oldlen and r[1] != EOF: #70000 is "range" at the end of file 488 | raise AssertionError("Length mismatch") 489 | r[0] += len(v) + 1 # remove used space from that range 490 | newp += 0x08010000 # convert from offset to pointer 491 | newps = pack('I', newp) 492 | stored[v] = newps 493 | for pidx, p in enumerate(ps): # now update pointers 494 | if len(vals) > 1: # if contexted 495 | if pidx >= len(vals): 496 | print >>log, " *! Warning: exceeding pointer %d for context %d" % (pidx, idx) 497 | if idx != pidx: 498 | continue # skip irrelevant pointers 499 | oldlen = len(datar) 500 | datar = datar[0:p] + newps + datar[p+4:] 501 | if len(datar) != oldlen: 502 | raise AssertionError("Length mismatch") 503 | if key_translated and key in keys: 504 | keys.remove(key) # as it is translated now 505 | translated += 1 506 | # now that string is translated, we may reuse its place as ranges 507 | if key_translated and args.reuse_ranges: 508 | for o in mustrepoint or os: 509 | i = o+1 510 | while i < len(data): 511 | if find_pointers_to_offset(i): # string is overused starting from this point 512 | break 513 | if data[i] == '\0' : # last byte 514 | i += 1 # include it too 515 | break 516 | i += 1 517 | addrange(o, i) 518 | print >>log, " ++ Reclaimed %d bytes from this string" % (i-o) 519 | npass += 1 520 | print >>log, "Pass %d completed." % npass 521 | sizes = [r[1]-r[0] for r in ranges] 522 | print >>log, "Remaining space at this point: %d bytes scattered in %d ranges, max range size is %d bytes" % \ 523 | (sum(sizes), len(ranges), max(sizes or [0])) 524 | print >>log 525 | if not args.reuse_ranges: # new ranges definitely could not appear 526 | break 527 | if len(keys) == 0: 528 | print >>log, "All strings are translated. Enjoy!" 529 | break 530 | if untranslated == 0: 531 | print >>log, "No more exceeding strings. Nice." 532 | break 533 | if translated == 0: 534 | print >>log, "Nothing changed in this pass; giving up." 535 | break 536 | print >>log, "Translated %d strings in this pass; let's try to translate %d remaining" % (translated, untranslated) 537 | untranslated = 0 # restart counter as we will retry all these strings 538 | if keys: 539 | print >>log, "Strings still not translated:" 540 | print >>log, '\n'.join(["* "+k for k in keys]) 541 | else: 542 | print >>log, "Everything translated. Hooray!" 543 | print >>log, "Saving..." 544 | if len(datar) != len(data): # something appended 545 | datar += data[-48:] # add ending bytes - needed for iOS app 546 | args.output.write(datar) 547 | args.output.close() 548 | print >>log, "Done." 549 | if untranslated: 550 | print >>log, "WARNING: Couldn't translate %d strings because of ranges lack." % untranslated 551 | else: 552 | print >>log, "I think that all the strings were translated successfully :-)" 553 | 554 | if __name__ == "__main__": 555 | args = parse_args() 556 | translate_fw(args) 557 | -------------------------------------------------------------------------------- /libpatcher/asm.py: -------------------------------------------------------------------------------- 1 | # This is a library of ARM/THUMB assembler instruction definitions 2 | 3 | from struct import pack, unpack 4 | 5 | __all__ = ['Num', 'List', 'Reg', 'Label', 'Str', 6 | #'Argument', 'LabelError', 'Instruction', 7 | 'findInstruction'] 8 | 9 | ### 10 | # Instruction argument types: 11 | # integer, list of arguments, register, label 12 | 13 | 14 | class Argument(object): 15 | 16 | def match(self, other): 17 | """ Matches this instance with given obj """ 18 | raise NotImplementedError 19 | 20 | 21 | class Num(int, Argument): 22 | """ Just remember initially specified value format """ 23 | def __new__(cls, val=None, initial=None, bits='any', 24 | positive=False, lsl=None): 25 | if isinstance(val, str): 26 | ret = int.__new__(cls, val, 0) # auto determine base 27 | elif val is None: 28 | ret = int.__new__(cls, 0) 29 | ret.bits = bits 30 | if bits != 'any': 31 | ret.maximum = 1 << bits 32 | ret.positive = positive 33 | ret.lsl = lsl 34 | return ret 35 | else: 36 | ret = int.__new__(cls, val) 37 | ret.initial = str(val) if initial is None else initial 38 | # and for consistency with Reg: 39 | ret.val = ret 40 | ret.bits = None 41 | return ret 42 | 43 | def __repr__(self): 44 | if self.bits is not None: 45 | if self.bits != 'any': # numeric 46 | return "%d-bits integer%s" % ( 47 | self.bits, ", positive" if self.positive else "") 48 | return "Integer%s" % (", positive" if self.positive else "") 49 | return str(self.initial) 50 | 51 | def match(self, other): 52 | if not isinstance(other, Num): 53 | return False 54 | if self.bits is not None: 55 | if self.positive and other < 0: 56 | return False 57 | if self.bits != 'any' and abs(other) >= self.maximum: 58 | return False 59 | if self.lsl and (other & (1 << self.lsl - 1)): 60 | return False 61 | return True 62 | return other == self 63 | 64 | def part(self, bits, shift=0): 65 | return (self >> shift) & (2**bits - 1) 66 | 67 | class ThumbExpandable(Argument): 68 | """ Number compatible with ThumbExpandImm function """ 69 | 70 | def __init__(self, bits=12): 71 | self.bits = bits 72 | 73 | def __repr__(self): 74 | return "ThumbExpandable integer for %s bits" % self.bits 75 | 76 | def match(self, other): 77 | def encode(n): 78 | " Encodes n to thumb-form or raises ValueError if impossible " 79 | # 11110 i 0 0010 S 1111 0 imm3 rd4 imm8 80 | if n <= 0xFF: # 1 byte 81 | return n 82 | b1 = n >> 24 83 | b2 = (n >> 16) & 0xFF 84 | b3 = (n >> 8) & 0xFF 85 | b4 = n & 0xFF 86 | if b1 == b2 == b3 == b4: 87 | return (0b11 << 8) + b1 88 | if b1 == 0 and b3 == 0 and b2 == b4: 89 | return (0b01 << 8) + b2 90 | if b2 == 0 and b4 == 0 and b1 == b3: 91 | return (0b10 << 8) + b1 92 | # rotating scheme 93 | 94 | def rol(n, ofs): 95 | return ((n << ofs) & 0xFFFFFFFF) | (n >> (32 - ofs)) 96 | # maybe buggy for x >= 1<<32, 97 | # but we will not have such values - 98 | # see parseNumber above for explanation 99 | 100 | for i in range(0b1000, 32): 101 | # ^^ - lower values will cause autodetermining to fail 102 | val = rol(n, i) 103 | if((val & 0xFFFFFF00) == 0 and 104 | (val & 0xFF) == 0x80 + (val & 0x7F)): # correct 105 | return ((i << 7) & 0xFFF) + (val & 0x7F) 106 | raise ValueError 107 | 108 | def the(bits, shift): 109 | return (val >> shift) & (2**bits - 1) 110 | 111 | if not isinstance(other, Num): 112 | return False 113 | if abs(other) > 2**32 - 1: 114 | return False # too large 115 | if other < 0: 116 | other += 2**32 # convert to positive 117 | try: 118 | val = encode(other) 119 | except ValueError: 120 | return False 121 | other.theval = val 122 | other.the = the 123 | other.imm8 = val & (2**8 - 1) 124 | other.imm3 = (val >> 8) & (2**3 - 1) 125 | other.i = val >> 11 126 | return True 127 | 128 | 129 | class List(list, Argument): 130 | 131 | def match(self, other): 132 | if not isinstance(other, (List, list)): 133 | # it may be either our specific List obj or plain list 134 | return False 135 | if len(self) != len(other): 136 | return False 137 | for i, j in zip(self, other): 138 | if not isinstance(i, tuple): 139 | i = (i,) # to be iterable 140 | for ii in i: 141 | if ii.match(j): 142 | break 143 | else: # none matched 144 | return False 145 | return True 146 | 147 | 148 | class Reg(int, Argument): 149 | _regs = { 150 | 'R0': 0, 'R1': 1, 'R2': 2, 'R3': 3, 151 | 'R4': 4, 'R5': 5, 'R6': 6, 'R7': 7, 'WR': 7, 152 | 'R8': 8, 'R9': 9, 'SB': 9, 153 | 'R10': 10, 'SL': 10, 'R11': 11, 'FP': 11, 154 | 'R12': 12, 'IP': 12, 'R13': 13, 'SP': 13, 155 | 'R14': 14, 'LR': 14, 'R15': 15, 'PC': 15, 156 | 'A1': 0, 'A2': 1, 'A3': 2, 'A4': 3, 157 | 'V1': 4, 'V2': 5, 'V3': 6, 'V4': 7, 158 | 'V5': 8, 'V6': 9, 'V7': 10, 'V8': 11, 159 | } 160 | 161 | @staticmethod 162 | def lookup(name): 163 | """ 164 | Tries to convert given register name to its integer value. 165 | Will raise IndexError if name is invalid. 166 | """ 167 | return Reg._regs[name.upper()] 168 | 169 | @staticmethod 170 | def is_reg(name): 171 | """ Checks whether string is valid register name """ 172 | return name.upper() in Reg._regs 173 | 174 | def __new__(cls, name=None, hi='any'): 175 | """ 176 | Usage: either Reg('name') or Reg(hi=True/False) or Reg() 177 | First is a plain register, others are masks 178 | """ 179 | if not name or name in ['HI', 'LO']: # pure mask 180 | if name == 'HI': 181 | hi = True 182 | elif name == 'LO': 183 | hi = False 184 | mask = hi 185 | name = "%s register" % ( 186 | "High" if hi else 187 | "Low" if hi is False else 188 | "Any") 189 | val = -1 190 | else: 191 | val = Reg.lookup(name) 192 | mask = None 193 | ret = int.__new__(cls, val) 194 | ret.name = name 195 | ret.mask = mask 196 | return ret 197 | 198 | def __repr__(self): 199 | return self.name 200 | 201 | def match(self, other): 202 | if not isinstance(other, Reg): 203 | return False 204 | if self.mask is not None: 205 | if self.mask is True: # hireg 206 | return other >= 8 207 | elif self.mask is False: # loreg 208 | return other < 8 209 | else: # any 210 | return True 211 | return self == other 212 | 213 | 214 | class RegList(List): # list of registers 215 | 216 | def __init__(self, lo=None, lcount=8, pc=False, lr=False, sp=False): 217 | self.src = [] 218 | self.mask = False 219 | self.lcount = lcount # count of lo regs for matching 220 | if lo or pc or lr or sp: 221 | self.mask = True 222 | self.lo = lo 223 | self.pc = pc 224 | self.lr = lr 225 | self.sp = sp 226 | 227 | def __repr__(self): 228 | return '{%s}' % ','.join(self.src) 229 | 230 | def append(self, s, pos): 231 | if not isinstance(s, str): 232 | raise ValueError(s) 233 | if '-' in s: # registers range 234 | ss = s.split('-') 235 | if len(ss) != 2: 236 | raise ValueError("Invalid register range: %s" % s, pos) 237 | ra = Reg(ss[0]) 238 | rb = Reg(ss[1]) 239 | if ra >= rb: 240 | raise ValueError("Unordered register range: %s" % s, pos) 241 | for i in range(ra, rb + 1): 242 | super(RegList, self).append(Reg('R%d' % i)) 243 | else: # plain register 244 | super(RegList, self).append(Reg(s)) 245 | self.src.append(s) 246 | 247 | def match(self, other): 248 | if not isinstance(other, RegList): 249 | return False 250 | if self.mask or other.mask: 251 | m, o = (self, other) if self.mask else (other, self) 252 | oc = list(o) # other's clone, to clean it up 253 | if m.pc: 254 | if not Reg('PC') in o: 255 | return False 256 | oc.remove(Reg('PC')) # to avoid loreg test failure 257 | elif m.pc is False and Reg('PC') in o: 258 | return False 259 | elif Reg('PC') in oc: # None = nobody cares; avoid loreg failure 260 | oc.remove(Reg('PC')) 261 | if m.lr: 262 | if not Reg('LR') in o: 263 | return False 264 | oc.remove(Reg('LR')) # to avoid loreg test failure 265 | elif m.lr is False and Reg('LR') in o: 266 | return False 267 | elif Reg('LR') in oc: # None = nobody cares; avoid loreg failure 268 | oc.remove(Reg('LR')) 269 | if m.sp: 270 | if not Reg('SP') in o: 271 | return False 272 | oc.remove(Reg('SP')) 273 | elif m.sp is False and Reg('SP') in o: 274 | return False 275 | elif Reg('SP') in oc: 276 | oc.remove(Reg('SP')) 277 | if m.lo and oc and max(oc) >= self.lcount: 278 | return False 279 | elif m.lo is False and oc and min(oc) < self.lcount: 280 | return False 281 | return True 282 | if len(self) != len(other): 283 | return False 284 | for i, j in zip(self, other): 285 | if i != j: 286 | return False 287 | return True 288 | 289 | def has(self, reg): 290 | if isinstance(reg, str): 291 | reg = Reg(reg) 292 | if reg in self: 293 | return 1 294 | return 0 295 | 296 | def lomask(self, lcount=8): 297 | """ Returns low registers bitmask for this RegList """ 298 | if self.mask: 299 | raise ValueError("This is mask!") 300 | m = 0 301 | for r in self: 302 | if r < lcount: 303 | m += 2**r 304 | return m 305 | 306 | 307 | class LabelError(Exception): 308 | """ 309 | This exception is raised when label requested is not found in given context. 310 | """ 311 | pass 312 | 313 | 314 | class Label(Argument): 315 | 316 | def __init__(self, name=None): 317 | self.name = name 318 | # This is used by parser for constructions like DCD someProc+1 319 | self.shift = 0 320 | 321 | def __repr__(self): 322 | return (":%s" % self.name) if self.name else "Label" 323 | 324 | def match(self, other): 325 | return isinstance(other, Label) 326 | 327 | def getAddress(self, instr): 328 | if not self.name: 329 | raise LabelError("This is a mask, not label!") 330 | try: 331 | return instr.findLabel(self) + self.shift 332 | except IndexError: 333 | raise LabelError 334 | 335 | def _getOffset(self, instr, align=False): 336 | pc = instr.getAddr() + 4 337 | if align: 338 | pc = pc & 0xfffffffc 339 | return self.getAddress(instr) - pc 340 | 341 | def offset(self, instr, bits, shift=0, positive=False, align=False): 342 | """ 343 | Returns offset from given instruction to this label. 344 | bits - maximum bit-width for offset; 345 | if offset doesn't fit that width, 346 | LabelError will be raised. 347 | shift - how many bits to cut off from the end 348 | (they are added to Bits on checking); 349 | these bits must be 0. 350 | positive - if offset must be positive 351 | align - if we should use 4-aligned offset (as for LDR) instead of plain 352 | This method is intended to be used one time, in non-lambda procs. 353 | """ 354 | ofs = self._getOffset(instr, align) 355 | if abs(ofs) >= (1 << (bits + shift)): 356 | raise LabelError("Offset is too far: 0x%X" % ofs) 357 | if ofs < 0: 358 | if positive: 359 | raise LabelError( 360 | "Negative offset not allowed here: 0x%X" % ofs) 361 | ofs = (1 << (bits + shift)) + ofs 362 | if bits > 0: 363 | rem = ofs & (2**shift - 1) 364 | if rem: 365 | # FIXME 366 | raise LabelError("Spare bits in offset 0x%X: %X" % (ofs, rem)) 367 | ofs = ofs >> shift 368 | return ofs 369 | 370 | def off_s(self, instr, bits, shift): 371 | """ 372 | Returns `bits' bits of offset, starting from `shift' bit. 373 | To be used in lambdas. 374 | Doesn't test maximum width, so consider using off_max! 375 | Maximum supported offset width is 32 bits. 376 | """ 377 | if bits + shift > 32: 378 | raise ValueError("off_s doesn't support " 379 | "offset width more than 32 bits! " 380 | "bits=%s, shift=%s" % (bits, shift)) 381 | ofs = self.offset(instr, 32) # 32 for negative offsets to be 1-padded 382 | return (ofs >> shift) & (2**bits - 1) 383 | 384 | def off_max(self, instr, bits): 385 | """ 386 | Tests if offset from given instruction to this label 387 | fits in `bits' bits. 388 | Returns 0 on success, for usage in lambdas. 389 | Raises LabelError on failure. 390 | """ 391 | self.offset(instr, bits) 392 | return 0 393 | 394 | def off_pos(self, instr): 395 | """ 396 | Validates that offset from given instruction to this label 397 | is positive. 398 | Returns 0 on success, for usage in lambdas. 399 | """ 400 | if self._getOffset(instr) < 0: 401 | raise LabelError("Negative offset not allowed here") 402 | return 0 403 | 404 | def off_range(self, instr, min, max): 405 | """ 406 | Tests if offset from given instruction to this label 407 | fits given range. 408 | Returns 0 on success, for usage in lambdas. 409 | Raises LabelError on failure. 410 | """ 411 | ofs = self._getOffset(instr) 412 | if ofs < min or ofs > max: 413 | raise LabelError("Offset %X doesn't fit range %X..%X" % 414 | (ofs, min, max)) 415 | return 0 416 | 417 | 418 | class Str(bytes, Argument): 419 | """ This represents _quoted_ string """ 420 | def __new__(cls, val=None): 421 | # val is expected to be bytes 422 | if isinstance(val, str): 423 | val = val.encode() 424 | 425 | if val is None: 426 | val = "String" 427 | mask = True 428 | else: 429 | mask = False 430 | ret = bytes.__new__(cls, val) 431 | ret.mask = mask 432 | return ret 433 | 434 | def match(self, other): 435 | if not isinstance(other, Str): 436 | return False 437 | if self.mask: 438 | return True 439 | return self == other 440 | 441 | ### 442 | # Instructions description 443 | 444 | 445 | class Instruction(object): 446 | """ 447 | This class may represent either instruction definition 448 | (with masks instead of args) 449 | or real instruction (with concrete args and context). 450 | Instruction handler may access its current opcode via self.opcode field. 451 | """ 452 | 453 | def __init__(self, opcode, args, proc, mask=True, pos=None): 454 | self.opcode = opcode 455 | self.args = args 456 | self.proc = proc 457 | self.mask = mask 458 | self.pos = pos 459 | self.size = None 460 | self.addr = None 461 | self.original = None 462 | 463 | def __repr__(self): 464 | ret = "<%s %s>" % (self.opcode, ','.join([repr(x) for x in self.args])) 465 | if self.original: 466 | ret += "(mask:%s)" % self.original 467 | if self.pos: 468 | ret += " at " + str(self.pos) 469 | return ret 470 | 471 | def match(self, opcode, args): 472 | """ Match this definition to given instruction """ 473 | if not self.mask: 474 | raise ValueError("This is not mask, cannot match") 475 | # check mnemonic... 476 | if isinstance(self.opcode, str): 477 | if self.opcode != opcode: 478 | return False 479 | else: # multiple opcodes possible 480 | if opcode not in self.opcode: 481 | return False 482 | # ... and args 483 | if len(self.args) != len(args): 484 | return False 485 | # __func__ to avoid type checking, as match() will excellently work 486 | # on plain list. 487 | if not List.match.__func__(self.args, args): 488 | return False 489 | return True 490 | 491 | def instantiate(self, opcode, args, pos): 492 | if not self.mask: 493 | raise ValueError("This is not mask, cannot instantiate") 494 | # this magic is to correctly call constructor of subclass 495 | ret = self.__class__(opcode, args, self.proc, mask=False, pos=pos) 496 | if self.size is not None: 497 | ret.size = self.size 498 | else: 499 | # this magic is to correctly "replant" custom method to another 500 | # instance 501 | import types 502 | ret.getSize = types.MethodType(self.getSize.__func__, ret) 503 | ret.original = self 504 | return ret 505 | 506 | def setAddr(self, addr): 507 | """ 508 | Sets memory address at which 509 | this particular instruction instance resides. 510 | This is called somewhere after instantiate. 511 | """ 512 | self.addr = addr 513 | 514 | def getAddr(self): 515 | " Returns memory address for this instruction " 516 | return self.addr 517 | 518 | def setBlock(self, block): 519 | self.block = block 520 | 521 | def findLabel(self, label): 522 | if label.name in self.block.context: 523 | return self.block.context[label.name] 524 | if label.name in self.block.patch.context: 525 | return self.block.patch.context[label.name] 526 | if label.name in self.block.patch.library.context: 527 | return self.block.patch.library.context[label.name] 528 | raise LabelError("Label not found: %s" % repr(label)) 529 | 530 | def getCode(self): 531 | if not self.addr: 532 | raise ValueError("No address, cannot calculate code") 533 | if callable(self.proc): 534 | code = self.proc(self, *self.args) 535 | else: 536 | code = self.proc 537 | if isinstance(code, bytes): 538 | return code 539 | elif isinstance(code, int): 540 | return pack('" % ("global " if self.glob else "", self.name) 582 | 583 | def setBlock(self, block): 584 | self.block = block 585 | ctx = block.patch.context if self.glob else block.context 586 | if self.name in ctx: 587 | raise ValueError('Duplicate label ' + self.name) 588 | ctx[self.name] = self.getAddr() 589 | 590 | 591 | _instructions = [] 592 | 593 | 594 | def instruction(opcode, args, size=2, proc=None): 595 | """ 596 | This is a function decorator for instruction definitions. 597 | It may also be used as a plain function, 598 | then you should pass it a function as proc arg. 599 | Note that proc may also be in fact plain value, e.g. for "NOP" instruction. 600 | """ 601 | def gethandler(proc): 602 | # Replace all [lists] with List instances, recursing tuples 603 | def replace(args): 604 | for n, a in enumerate(args): 605 | if type(a) is list: 606 | # don't use isinstance, as we only need to replace 607 | # "exact" Lists 608 | args[n] = List() 609 | [args[n].append(k) for k in a] 610 | elif isinstance(a, tuple): 611 | args[n] = tuple(replace(list(a))) 612 | return args 613 | replace(args) 614 | 615 | instr = Instruction(opcode, args, proc) 616 | if callable(size): 617 | instr.getSize = size 618 | else: 619 | instr.size = size 620 | _instructions.append(instr) 621 | return proc 622 | if proc: # not used as decorator 623 | gethandler(proc) 624 | else: 625 | return gethandler 626 | 627 | 628 | def instruct_class(c): 629 | """ decorator for custom instruction classes """ 630 | _instructions.append(c()) 631 | return c 632 | 633 | 634 | def findInstruction(opcode, args, pos): 635 | """ 636 | This method tries to find matching instruction 637 | for given opcode and args. 638 | On success, it will instantiate that instruction with given pos 639 | (cloning that pos). 640 | On failure, it will throw IndexError. 641 | """ 642 | for i in _instructions: 643 | if i.match(opcode, args): 644 | return i.instantiate(opcode, args, pos.clone()) 645 | raise IndexError("Unsupported instruction: %s" % opcode) 646 | 647 | ### 648 | # All the instruction definitions 649 | 650 | 651 | def _longJump(self, dest, bl): 652 | offset = dest.offset(self, 23) 653 | offset = offset >> 1 654 | hi_o = (offset >> 11) & (2**11 - 1) 655 | lo_o = (offset >> 0) & (2**11 - 1) 656 | hi_c = 0b11110 657 | lo_c = 0b11111 if bl else 0b10111 658 | hi = (hi_c << 11) + hi_o 659 | lo = (lo_c << 11) + lo_o 660 | return (hi, lo) 661 | instruction('BL', [Label()], 4, lambda self, dest: 662 | _longJump(self, dest, True)) 663 | instruction('B.W', [Label()], 4, lambda self, dest: 664 | _longJump(self, dest, False)) 665 | 666 | 667 | @instruct_class 668 | class DCB(Instruction): 669 | 670 | def __init__(self, opcode=None, args=None, pos=None): 671 | Instruction.__init__(self, opcode, args, None, pos=pos) 672 | if args: 673 | code = b'' 674 | for a in args: 675 | if isinstance(a, Str): 676 | code += a 677 | elif isinstance(a, Num): 678 | code += pack('> 1)) 727 | instruction('B' + cond + '.W', [Label()], 4, lambda self, lbl: 728 | (lbl.off_max(self, 19) + # test for maximum 729 | 730 | (0b11110 << 11) + (lbl.off_s(self, 1, 18) << 10) + 731 | (val << 6) + (lbl.off_s(self, 6, 12) >> 0), 732 | 733 | (0b10101 << 11) + (lbl.off_s(self, 11, 1) >> 0))) 734 | for cond, val in { 735 | 'CC': 0x3, 'CS': 0x2, 'EQ': 0x0, 'GE': 0xA, 736 | 'GT': 0xC, 'HI': 0x8, 'LE': 0xD, 'LS': 0x9, 737 | 'LT': 0xB, 'MI': 0x4, 'NE': 0x1, 'PL': 0x5, 738 | 'VC': 0x7, 'VS': 0x6, 739 | }.items(): 740 | Bcond_instruction(cond, val) 741 | 742 | 743 | @instruction(['CBZ', 'CBNZ'], [Reg('LO'), Label()]) 744 | def CBx(self, reg, lbl): 745 | lbl.off_range(self, 0, 126) 746 | offset = lbl.offset(self, 7) >> 1 747 | op = 1 if 'N' in self.opcode else 0 748 | return ((0b1011 << 12) + 749 | (op << 11) + 750 | ((offset >> 5) << 9) + 751 | (1 << 8) + 752 | ((offset & (2**5 - 1)) << 3) + 753 | reg) 754 | instruction('B', [Label()], 2, lambda self, lbl: 755 | (0b11100 << 11) + (lbl.offset(self, 12) >> 1)) 756 | 757 | 758 | @instruct_class 759 | class GlobalLabel(LabelInstruction): 760 | 761 | def __init__(self): 762 | Instruction.__init__(self, ["global", "proc"], [Label()], None) 763 | 764 | def instantiate(self, opcode, args, pos): 765 | label = args[0].name 766 | return LabelInstruction(label, pos, glob=True) 767 | 768 | 769 | @instruct_class 770 | class ValInstruction(NullInstruction): 771 | """ 772 | This class represents "val" instruction. It has zero size. 773 | It stores 4bytes integer at its position on setBlock. 774 | """ 775 | 776 | def __init__(self, pos=None, name=None): 777 | Instruction.__init__(self, "val", [Label()], None, True, pos) 778 | self.name = name 779 | 780 | def __repr__(self): 781 | return "" % (self.name) 782 | 783 | def instantiate(self, opcode, args, pos): 784 | name = args[0].name 785 | return ValInstruction(pos, name) 786 | 787 | def setBlock(self, block): 788 | if block.mask and block.mask.floating: 789 | raise ValueError("Cannot use val instruction in floating block") 790 | self.block = block 791 | codebase = block.codebase 792 | # get value... 793 | addr = self.getAddr() - codebase 794 | value = unpack('> 2)) 834 | instruction('ADR', [Reg("LO"), Label()], 2, lambda self, rd, lbl: 835 | (0b10100 << 11) + (rd << 8) + lbl.offset(self, 8, 2, True, True)) 836 | instruction(['AND', 'ANDS'], [Reg(), Reg(), Num.ThumbExpandable()], 4, 837 | lambda self, rd, rn, imm: 838 | ( 839 | (0b11110 << 11) + 840 | (imm.the(1, 11) << 10) + 841 | ((1 if 'S' in self.opcode else 0) << 4) + 842 | (rn), 843 | (imm.the(3, 8) << 12) + 844 | (rd << 8) + 845 | (imm.the(8, 0) << 0) 846 | )) 847 | instruction('BLX', [Reg()], 2, lambda self, rx: 848 | (1 << 14) + (0b1111 << 7) + (rx << 3)) 849 | instruction('BX', [Reg()], 2, lambda self, rx: 850 | (0x11 << 10) + (0x3 << 8) + (rx << 3)) 851 | instruction('CMP', [Reg("LO"), Num(bits=8)], 2, lambda self, rn, imm: 852 | (0b101 << 11) + (rn << 8) + imm) 853 | instruction('CMP', [Reg("LO"), Reg("LO")], 2, lambda self, rn, rm: 854 | (1 << 14) + (0b101 << 7) + (rm << 3) + rn) 855 | instruction('CMP', [Reg(), Reg()], 2, lambda self, rn, rm: 856 | (1 << 14) + (0b101 << 8) + ((rn >> 3) << 7) + 857 | (rm << 3) + (rn & 0b111)) 858 | # T2 859 | instruction(['CMP.W', 'CMP'], [Reg(), Num.ThumbExpandable()], 4, 860 | lambda self, rn, imm: 861 | ( 862 | (0b11110 << 11) + 863 | (imm.the(1, 11) << 10) + 864 | (0b11011 << 4) + 865 | (rn), 866 | (imm.the(3, 8) << 12) + 867 | (0b1111 << 8) + 868 | (imm.the(8, 0)) 869 | )) 870 | instruction(['EOR', 'EORS'], [Reg(), Reg(), Num.ThumbExpandable()], 4, 871 | lambda self, rd, rn, imm: 872 | ( 873 | (0b11110 << 11) + 874 | (imm.the(1, 11) << 10) + 875 | (0b100 << 5) + 876 | ((1 if 'S' in self.opcode else 0) << 4) + 877 | (rn << 0), 878 | (imm.the(3, 8) << 12) + 879 | (rd << 8) + 880 | (imm.the(8, 0) << 0) 881 | )) 882 | instruction('MOVS', [Reg("LO"), Reg("LO")], 2, lambda self, rd, rm: 883 | (0 << 6) + (rm << 3) + rd) 884 | instruction(['MOV', 'MOVS'], [Reg(), Reg()], 2, lambda self, rd, rm: 885 | (0b1000110 << 8) + ((rd >> 3) << 7) + (rm << 3) + 886 | ((rd & 0b111) << 0)) 887 | instruction(['MOV', 'MOVS'], [Reg("LO"), Num(bits=8)], 2, lambda self, rd, imm: 888 | (1 << 13) + (rd << 8) + imm) 889 | instruction(['MOV', 'MOV.W', 'MOVS', 'MOVS.W'], 890 | [Reg(), Num.ThumbExpandable()], 4, lambda self, rd, imm: 891 | ( 892 | (0b11110 << 11) + 893 | (imm.the(1, 11) << 10) + 894 | (0b10 << 5) + 895 | ((1 if 'S' in self.opcode else 0) << 4) + 896 | (0b1111 << 0), 897 | (imm.the(3, 8) << 12) + 898 | (rd << 8) + 899 | (imm.the(8, 0) << 0) 900 | )) 901 | instruction(['MOV', 'MOV.W', 'MOVW'], [Reg(), Num(bits=16, positive=True)], 4, 902 | lambda self, rd, imm: 903 | ( 904 | (0b11110 << 11) + 905 | (imm.part(1, 11) << 10) + 906 | (0b1001 << 6) + 907 | (imm.part(4, 12)), 908 | (imm.part(3, 8) << 12) + 909 | (rd << 8) + 910 | (imm.part(8)) 911 | )) 912 | instruction('LDR', [Reg("LO"), ([Reg("LO")], [Reg("LO"), Num(bits=7, lsl=2)])], 913 | 2, lambda self, rt, lst: 914 | (0b01101 << 11) + 915 | ((lst[1].part(5, 2) if len(lst) > 1 else 0) << 6) + 916 | (lst[0] << 3) + 917 | rt) 918 | instruction('LDR', [Reg('LO'), [Reg('LO'), Reg('LO')]], 2, lambda self, rt, lst: 919 | (0b01011 << 11) + 920 | (lst[1] << 6) + 921 | (lst[0] << 3) + 922 | rt) 923 | instruction(['LDR.W', 'LDR'], 924 | [Reg(), ([Reg(), Reg()], [Reg(), Reg(), Num(bits=2)])], 4, 925 | lambda self, rt, lst: 926 | ((0b11111 << 11) + 927 | (0b101 << 4) + 928 | lst[0], 929 | (rt << 12) + 930 | ((lst[2] if len(lst) > 2 else 0) << 4) + 931 | lst[1])) 932 | instruction(['LDR.W', 'LDR'], [Reg(), ([Reg(), Num(bits=12)], [Reg()])], 4, 933 | lambda self, rt, lst: 934 | ((0b11111 << 11) + 935 | (0b1101 << 4) + 936 | lst[0], 937 | (rt << 12) + 938 | (lst[1] if len(lst) > 1 else 0))) 939 | instruction('LDR', [Reg("LO"), Label()], 2, lambda self, rt, lbl: 940 | (0b1001 << 11) + (rt << 8) + 941 | lbl.offset(self, 8, shift=2, positive=True, align=True)) 942 | # T1 943 | instruction('LDRB', [Reg("LO"), ([Reg("LO"), Num(bits=5)], [Reg("LO")])], 944 | 2, lambda self, rt, lst: 945 | (0b1111 << 11) + ((lst[1] if len(lst) > 1 else 0) << 6) + 946 | (lst[0] << 3) + rt) 947 | # in T2 948 | instruction(['LDRB', 'LDRB.W'], [Reg(), [Reg(), Num(bits=12)]], 4, 949 | lambda self, rt, rest: 950 | ( 951 | (0b11111 << 11) + 952 | (1 << 7) + 953 | (1 << 4) + 954 | (rest[0]), 955 | (rt << 12) + 956 | (rest[1]) 957 | )) 958 | # and in T3, with offset 959 | instruction(['LDRB', 'LDRB.W'], [Reg(), [Reg()], Num(bits=8)], 4, 960 | lambda self, rt, rn, imm: 961 | ( 962 | (0b11111 << 11) + 963 | (1 << 4) + 964 | (rn[0]), 965 | (rt << 12) + 966 | (1 << 11) + 967 | (0 << 10) + # P/index 968 | ((1 if imm >= 0 else 0) << 9) + # U/add 969 | (1 << 8) + # W/writeback 970 | (imm if imm >= 0 else -imm) 971 | )) 972 | instruction('LDRB', [Reg('LO'), [Reg('LO'), Reg('LO')]], 2, 973 | lambda self, rt, rest: 974 | ( 975 | (0b0101110 << 9) + 976 | (rest[1] << 6) + 977 | (rest[0] << 3) + 978 | (rt << 0) 979 | )) 980 | instruction('LDRH', [Reg('LO'), ([Reg('LO'), Num(bits=6, lsl=1)], [Reg('LO')])], 981 | 2, lambda self, rt, lst: 982 | ( 983 | (0b10001 << 11) + 984 | ((lst[1].part(5, 1) if len(lst) > 1 else 0) << 6) + 985 | (lst[0] << 3) + 986 | (rt) 987 | )) 988 | instruction(['LDRH.W', 'LDRH'], [Reg(), ([Reg(), Num(bits=12)], [Reg()])], 989 | 4, lambda self, rt, lst: 990 | ( 991 | (0b11111 << 11) + 992 | (0b1011 << 4) + 993 | lst[0], 994 | (rt << 12) + 995 | (lst[1] if len(lst) > 1 else 0) 996 | )) 997 | instruction(['LSL', 'LSLS'], [Reg("LO"), Reg("LO"), Num(bits=5)], 998 | 2, lambda self, rd, rm, imm: 999 | (0b00 << 11) + (imm << 6) + (rm << 3) + (rd)) 1000 | instruction(['LSR', 'LSRS'], [Reg("LO"), Reg("LO")], 2, lambda self, rdn, rm: 1001 | (0b010000 << 10) + (0b0010 << 6) + (rm << 3) + (rdn)) 1002 | instruction(['LSR', 'LSRS'], [Reg("LO"), Reg("LO"), Num(bits=5)], 1003 | 2, lambda self, rd, rm, imm: 1004 | (0b01 << 11) + (imm << 6) + (rm << 3) + (rd)) 1005 | instruction(['LSR', 'LSRS'], [Reg("LO"), Reg("LO")], 2, lambda self, rdn, rm: 1006 | (0b010000 << 10) + (0b0011 << 6) + (rm << 3) + (rdn)) 1007 | instruction(['MULS', 'MUL'], [Reg("LO"), Reg("LO")], 2, lambda self, rd, rm: 1008 | (1 << 14) + (0b1101 << 6) + (rm << 3) + rd) 1009 | instruction('PUSH', [RegList(lo=True, lr=None)], 2, lambda self, rl: 1010 | (0b1011010 << 9) + 1011 | (rl.has('LR') << 8) + 1012 | rl.lomask()) 1013 | instruction(['PUSH', 'PUSH.W'], [RegList(lo=True, lcount=13, lr=None)], 1014 | 4, lambda self, rl: 1015 | (0b1110100100101101, 1016 | (rl.has('LR') << 14) + rl.lomask(13))) 1017 | instruction('POP', [RegList(lo=True, pc=None)], 2, lambda self, rl: 1018 | (0xb << 12) + (1 << 11) + (0x2 << 9) + 1019 | (rl.has('PC') << 8) + rl.lomask()) 1020 | instruction(['POP', 'POP.W'], [RegList(lo=True, lcount=13, lr=None, pc=None)], 1021 | 4, lambda self, rl: 1022 | (0b1110100010111101, 1023 | (rl.has('PC') << 15) + (rl.has('LR') << 14) + 1024 | rl.lomask(13))) 1025 | instruction('RSB', [Reg("LO"), Reg("LO"), Num(0)], 2, lambda self, rd, rn, imm: 1026 | (1 << 14) + (0b1001 << 6) + (rn << 3) + rd) 1027 | instruction('STR', [Reg("LO"), ([Reg("SP"), Num(bits=10, lsl=2)], [Reg("SP")])], 1028 | 2, lambda self, rt, lst: 1029 | (0b10010 << 11) + (rt << 8) + 1030 | ((lst[1] >> 2) if len(lst) > 1 else 0)) 1031 | instruction('STR', [Reg("LO"), ([Reg("LO"), Num(bits=7, lsl=2)], [Reg("LO")])], 1032 | 2, lambda self, rt, lst: 1033 | (0b11 << 13) + (((lst[1] if len(lst) > 1 else 0) >> 2) << 6) + 1034 | (lst[0] << 3) + rt) 1035 | instruction(['STR.W', 'STR'], [Reg(), ([Reg(), Num(bits=12)], [Reg()])], 1036 | 4, lambda self, rt, lst: 1037 | ((0b11111 << 11) + 1038 | (0b1100 << 4) + 1039 | lst[0], 1040 | (rt << 12) + 1041 | (lst[1] if len(lst) > 1 else 0))) 1042 | instruction('STRB', [Reg("LO"), ([Reg("LO"), Num(bits=5)], [Reg("LO")])], 1043 | 2, lambda self, rt, lst: 1044 | (0b111 << 12) + ((lst[1] if len(lst) > 1 else 0) << 6) + 1045 | (lst[0] << 3) + rt) 1046 | instruction(['STRB.W', 'STRB'], [Reg(), ([Reg(), Num(bits=12)], [Reg()])], 1047 | 4, lambda self, rt, lst: 1048 | ((0b11111 << 11) + 1049 | (0b1000 << 4) + 1050 | lst[0], 1051 | (rt << 12) + 1052 | (lst[1] if len(lst) > 1 else 0))) 1053 | instruction('STRH', [Reg('LO'), ([Reg('LO'), Num(bits=6, lsl=1)], [Reg('LO')])], 1054 | 2, lambda self, rt, lst: 1055 | (1 << 15) + 1056 | ((lst[1].part(5, 1) if len(lst) > 1 else 0) << 6) + 1057 | (lst[0] << 3) + 1058 | rt) 1059 | instruction(['STRH.W', 'STRH'], [Reg(), ([Reg(), Num(bits=12)], [Reg()])], 1060 | 4, lambda self, rt, lst: 1061 | ((0b11111 << 11) + 1062 | (0b1010 << 4) + 1063 | (lst[0]), 1064 | (rt << 12) + 1065 | (lst[1] if len(lst) > 1 else 0))) 1066 | # SUB version for SP 1067 | instruction('SUB', [Reg('SP'), Reg('SP'), Num(bits=9, lsl=2)], 1068 | 2, lambda self, sp, sp1, imm: 1069 | (0b1011 << 12) + 1070 | (1 << 7) + 1071 | imm.part(7, 2)) 1072 | instruction(['SUBS', 'SUB'], [Reg("LO"), Num(bits=8)], 2, lambda self, rn, imm: 1073 | (0b111 << 11) + (rn << 8) + imm) 1074 | instruction(['SUBS', 'SUB'], [Reg("LO"), Reg("LO"), Reg("LO")], 1075 | 2, lambda self, rd, rn, rm: 1076 | simpleAddSub(self, rd, rn, rm, 1)) 1077 | instruction(['SUBS', 'SUB'], [Reg("LO"), Reg("LO")], 2, lambda self, rd, rm: 1078 | simpleAddSub(self, rd, rd, rm, 1)) 1079 | instruction(['SUBS', 'SUB'], [Reg("LO"), Reg("LO"), Num(bits=3)], 1080 | 2, lambda self, rd, rn, imm: 1081 | (0b1111 << 9) + (imm << 6) + (rn << 3) + (rd)) 1082 | instruction(['SUB.W', 'SUB'], [Reg(), Reg(), Num(bits=12)], 1083 | 4, lambda self, rd, rn, imm: 1084 | ((0b11110 << 11) + 1085 | (imm.part(1, 11) << 10) + 1086 | (0b101010 << 4) + 1087 | rn, 1088 | (imm.part(3, 8) << 12) + 1089 | (rd << 8) + 1090 | imm.part(8))) 1091 | instruction(['TST.W', 'TST'], [Reg(), Num.ThumbExpandable()], 1092 | 4, lambda self, rn, imm: 1093 | ( 1094 | (0b11110 << 11) + 1095 | (imm.the(1, 11) << 10) + 1096 | (1 << 4) + 1097 | rn, 1098 | (imm.the(3, 8) << 12) + 1099 | (0b1111 << 8) + 1100 | imm.the(8, 0) 1101 | )) 1102 | instruction('TST', [Reg('LO'), Reg('LO')], 2, lambda self, rn, rm: 1103 | ( 1104 | (1 << 14) + 1105 | (1 << 9) + 1106 | (rm << 3) + 1107 | rn 1108 | )) 1109 | instruction('UXTB', [Reg("LO"), Reg("LO")], 2, lambda self, rd, rm: 1110 | (0b1011001011 << 6) + (rm << 3) + rd) 1111 | --------------------------------------------------------------------------------