Add setuptools
This commit is contained in:
parent
74e0cfb40d
commit
3cb48b0345
3 changed files with 282 additions and 264 deletions
|
@ -1,266 +1,4 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import random
|
||||
import sys
|
||||
from typing import Sequence, Generator, Tuple, BinaryIO, Optional, Mapping
|
||||
|
||||
from rebindiff.utils import color
|
||||
from rebindiff.utils.strings import ellipsize
|
||||
from rebindiff.utils.values import str_base, unicode_ascii_repr
|
||||
|
||||
BLOCK_NUM_HEADER = "blk"
|
||||
BLOCK_NUM_PADDING_RIGHT = 2
|
||||
BLOCK_NUM_BASE = 16
|
||||
|
||||
BLOCK_BYTES = 4
|
||||
|
||||
LEFT_PADDING = 1
|
||||
BYTE_SPACING = 1
|
||||
HEX_ASCII_SPACING = 2
|
||||
FILE_SPACING = 4
|
||||
RIGHT_PADDING = 0
|
||||
|
||||
NOT_ASCII_CHAR = "‧"
|
||||
HORZ_ELLIPSIS = "…"
|
||||
VERT_ELLIPSIS = "⋮"
|
||||
|
||||
BLOCK_TEMPLATE = " " * LEFT_PADDING + "{hexr}" + " " * HEX_ASCII_SPACING + "|{asciir}|" + " " * RIGHT_PADDING
|
||||
|
||||
HEX_REPR_LENGTH = (2 + BYTE_SPACING) * BLOCK_BYTES - BYTE_SPACING
|
||||
ASCII_REPR_LENGTH = BLOCK_BYTES + 2
|
||||
|
||||
NAME_MAX_LENGTH = HEX_REPR_LENGTH + HEX_ASCII_SPACING + ASCII_REPR_LENGTH
|
||||
TOTAL_BLOCK_LENGTH = NAME_MAX_LENGTH + LEFT_PADDING + RIGHT_PADDING
|
||||
|
||||
if BLOCK_NUM_BASE > ord('Z') - ord('A') + 10 or BLOCK_NUM_BASE < 2:
|
||||
raise AssertionError("Invalid block number base")
|
||||
|
||||
global_colormap = {}
|
||||
|
||||
|
||||
def print_names(block_col_width: int, *names: str) -> None:
|
||||
print(end=color.BOLD + color.UNDERLINE)
|
||||
print(BLOCK_NUM_HEADER.center(block_col_width), end=" " * BLOCK_NUM_PADDING_RIGHT)
|
||||
|
||||
print(
|
||||
" " * LEFT_PADDING +
|
||||
(" " * (FILE_SPACING + LEFT_PADDING + RIGHT_PADDING)).join(
|
||||
[ellipsize(name, NAME_MAX_LENGTH).center(NAME_MAX_LENGTH) for name in names]
|
||||
) + " " * RIGHT_PADDING
|
||||
)
|
||||
|
||||
print(end=color.RESET)
|
||||
|
||||
|
||||
def colorize(string: str, byte: int, colormap: Mapping[int, str]) -> str:
|
||||
if byte in colormap:
|
||||
return color.colors[colormap[byte]] + string + color.RESET
|
||||
return string
|
||||
|
||||
|
||||
def get_other_halfbyte(half: int, byte: int) -> int:
|
||||
if byte == 0:
|
||||
return 0
|
||||
if half & 0x0F == 0:
|
||||
return byte & 0x0F
|
||||
return byte & 0x1F0
|
||||
|
||||
|
||||
def get_color_map(intgroup: Sequence[Optional[int]]) -> Mapping[int, str]:
|
||||
intset = set(intgroup)
|
||||
if len(intset) == 1:
|
||||
return {}
|
||||
|
||||
from .utils import color
|
||||
# ncolors = list(sorted(color.colors.keys(), key=lambda c: c.replace("BRIGHT_", "")))
|
||||
ncolors = list(color.colors.keys()).copy()
|
||||
random.shuffle(ncolors)
|
||||
ncolors *= 3
|
||||
colormap = {}
|
||||
halfbytes = {}
|
||||
|
||||
for byte in intgroup:
|
||||
lo = byte & 0x0F
|
||||
hi = byte & 0x1F0
|
||||
|
||||
if lo not in halfbytes:
|
||||
halfbytes[lo] = []
|
||||
if hi not in halfbytes:
|
||||
halfbytes[hi] = []
|
||||
|
||||
halfbytes[lo].append(byte)
|
||||
halfbytes[hi].append(byte)
|
||||
|
||||
for halfbyte in halfbytes: # sorted(halfbytes, key=lambda half: int_distance_sort_key(half, halfbytes.keys())):
|
||||
if len(halfbytes[halfbyte]) == len(intgroup):
|
||||
# Halfbyte does not change
|
||||
continue
|
||||
colormap[halfbyte] = ncolors.pop(0)
|
||||
|
||||
for byte in intset:
|
||||
lo = byte & 0x0F
|
||||
hi = byte & 0x1F0
|
||||
|
||||
# The byte gets the color of the half with the least occurrences
|
||||
try:
|
||||
least_common = sorted([hi, lo], key=lambda half: len(halfbytes[half]))[0]
|
||||
colormap[byte] = colormap[least_common]
|
||||
except:
|
||||
print(byte, colormap)
|
||||
|
||||
return colormap
|
||||
|
||||
|
||||
def print_line(block_col_width, block_num: int, line: Sequence[Sequence[Optional[bytes]]]) -> None:
|
||||
global global_colormap
|
||||
print(
|
||||
color.BOLD + str_base(block_num, BLOCK_NUM_BASE).rjust(block_col_width),
|
||||
end=" " * BLOCK_NUM_PADDING_RIGHT + color.RESET
|
||||
)
|
||||
|
||||
hex_reprs = ["" if set(block) != {None} else None for block in line]
|
||||
ascii_reprs = hex_reprs.copy()
|
||||
|
||||
byte_count = 0
|
||||
|
||||
for bgroup in zip(*line):
|
||||
byte_count += 1
|
||||
intgroup = [int.from_bytes(byte, 'little') | 0x100 for byte in bgroup]
|
||||
|
||||
colormap = get_color_map(intgroup)
|
||||
colormap = {byte: color if byte not in global_colormap
|
||||
else global_colormap[byte] for byte, color in colormap.items()}
|
||||
global_colormap.update(colormap)
|
||||
|
||||
for i, byte in zip(range(len(hex_reprs)), intgroup):
|
||||
if hex_reprs[i] is None:
|
||||
continue
|
||||
if byte is None:
|
||||
hex_reprs[i] += " "
|
||||
ascii_reprs[i] += " "
|
||||
else:
|
||||
asuni = unicode_ascii_repr(byte & 0xFF)
|
||||
|
||||
lo = byte & 0x0F
|
||||
hi = byte & 0x1F0
|
||||
|
||||
hi_str = colorize("%01X" % ((byte & 0xF0) >> 4), hi, colormap)
|
||||
lo_str = colorize("%01X" % lo, lo, colormap)
|
||||
|
||||
hex_reprs[i] += hi_str + lo_str
|
||||
ascii_reprs[i] += colorize(asuni, byte, colormap)
|
||||
|
||||
if byte_count < BLOCK_BYTES:
|
||||
hex_reprs[i] += " " * BYTE_SPACING
|
||||
|
||||
reprs = []
|
||||
for hexr, asciir in zip(hex_reprs, ascii_reprs):
|
||||
if not hexr:
|
||||
reprs.append(" " * TOTAL_BLOCK_LENGTH)
|
||||
continue
|
||||
reprs.append(BLOCK_TEMPLATE.format(hexr=hexr, asciir=asciir))
|
||||
|
||||
print((" " * FILE_SPACING).join(reprs))
|
||||
|
||||
|
||||
def print_identical_line_bogus(block_col_width: int, last_line: Sequence[Sequence[Optional[bytes]]]):
|
||||
print(end=color.BRIGHT_BLACK)
|
||||
print(str(VERT_ELLIPSIS).rjust(block_col_width), end=" " * BLOCK_NUM_PADDING_RIGHT)
|
||||
|
||||
bogus_blocks = []
|
||||
for block in last_line:
|
||||
byteset = set([tuple(i) for i in block if i])
|
||||
bogus_char = NOT_ASCII_CHAR if len(byteset) > 0 else " "
|
||||
|
||||
bogus_blocks.append(
|
||||
BLOCK_TEMPLATE.replace("|", bogus_char).format(
|
||||
hexr=(" " * BYTE_SPACING).join(bogus_char * 2 for _ in range(BLOCK_BYTES)),
|
||||
asciir="".join(bogus_char for _ in range(BLOCK_BYTES))
|
||||
)
|
||||
)
|
||||
|
||||
print((" " * FILE_SPACING).join(bogus_blocks))
|
||||
print(end=color.RESET)
|
||||
|
||||
|
||||
def get_block_col_width(*filenames):
|
||||
max_size = max(
|
||||
map(
|
||||
lambda filename: os.stat(filename).st_size,
|
||||
filenames
|
||||
)
|
||||
)
|
||||
max_block = (max_size + BLOCK_BYTES - (max_size % BLOCK_BYTES)) / BLOCK_BYTES
|
||||
|
||||
digits = 0
|
||||
while max_block > 0:
|
||||
max_block //= BLOCK_NUM_BASE
|
||||
digits += 1
|
||||
|
||||
return max(digits, len(BLOCK_NUM_HEADER))
|
||||
|
||||
|
||||
def print_hexdiff(*filenames: str):
|
||||
names, files = zip(*gen_files(*filenames))
|
||||
block_col_width = get_block_col_width(*filenames)
|
||||
|
||||
print_names(block_col_width, *names)
|
||||
|
||||
identical_lines = 0
|
||||
prev_line = None
|
||||
line = [[] for _ in range(len(files))]
|
||||
block_num = 0
|
||||
block_bytes = 0
|
||||
remaining_files = len(files)
|
||||
|
||||
while remaining_files > 0:
|
||||
if block_bytes >= BLOCK_BYTES:
|
||||
if prev_line == line:
|
||||
identical_lines += 1
|
||||
if identical_lines == 1:
|
||||
print_identical_line_bogus(block_col_width, line)
|
||||
else:
|
||||
identical_lines = 0
|
||||
print_line(block_col_width, block_num, line)
|
||||
prev_line = line
|
||||
|
||||
block_bytes = 0
|
||||
block_num += 1
|
||||
line = [[] for _ in range(len(files))]
|
||||
|
||||
block_bytes += 1
|
||||
|
||||
for i in range(len(files)):
|
||||
f = files[i]
|
||||
file_line = line[i]
|
||||
|
||||
if f.closed:
|
||||
file_line.append(None)
|
||||
continue
|
||||
|
||||
try:
|
||||
byte = f.read(1)
|
||||
if not byte:
|
||||
raise EOFError
|
||||
line[i].append(byte)
|
||||
except EOFError:
|
||||
file_line.append(None)
|
||||
f.close()
|
||||
remaining_files -= 1
|
||||
|
||||
|
||||
def gen_files(*filenames: str) -> Generator[Tuple[str, BinaryIO], None, None]:
|
||||
for filename in filenames:
|
||||
f = open(filename, "rb")
|
||||
basename = os.path.basename(filename)
|
||||
yield (basename, f)
|
||||
|
||||
from rebindiff.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit("At least one file needs to be specified")
|
||||
|
||||
print_hexdiff(*sys.argv[1:])
|
||||
main()
|
||||
|
|
267
rebindiff/main.py
Normal file
267
rebindiff/main.py
Normal file
|
@ -0,0 +1,267 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import random
|
||||
import sys
|
||||
from typing import Sequence, Generator, Tuple, BinaryIO, Optional, Mapping
|
||||
|
||||
from rebindiff.utils import color
|
||||
from rebindiff.utils.strings import ellipsize
|
||||
from rebindiff.utils.values import str_base, unicode_ascii_repr
|
||||
|
||||
BLOCK_NUM_HEADER = "blk"
|
||||
BLOCK_NUM_PADDING_RIGHT = 2
|
||||
BLOCK_NUM_BASE = 16
|
||||
|
||||
BLOCK_BYTES = 4
|
||||
|
||||
LEFT_PADDING = 1
|
||||
BYTE_SPACING = 1
|
||||
HEX_ASCII_SPACING = 2
|
||||
FILE_SPACING = 4
|
||||
RIGHT_PADDING = 0
|
||||
|
||||
NOT_ASCII_CHAR = "‧"
|
||||
HORZ_ELLIPSIS = "…"
|
||||
VERT_ELLIPSIS = "⋮"
|
||||
|
||||
BLOCK_TEMPLATE = " " * LEFT_PADDING + "{hexr}" + " " * HEX_ASCII_SPACING + "|{asciir}|" + " " * RIGHT_PADDING
|
||||
|
||||
HEX_REPR_LENGTH = (2 + BYTE_SPACING) * BLOCK_BYTES - BYTE_SPACING
|
||||
ASCII_REPR_LENGTH = BLOCK_BYTES + 2
|
||||
|
||||
NAME_MAX_LENGTH = HEX_REPR_LENGTH + HEX_ASCII_SPACING + ASCII_REPR_LENGTH
|
||||
TOTAL_BLOCK_LENGTH = NAME_MAX_LENGTH + LEFT_PADDING + RIGHT_PADDING
|
||||
|
||||
if BLOCK_NUM_BASE > ord('Z') - ord('A') + 10 or BLOCK_NUM_BASE < 2:
|
||||
raise AssertionError("Invalid block number base")
|
||||
|
||||
global_colormap = {}
|
||||
|
||||
|
||||
def print_names(block_col_width: int, *names: str) -> None:
|
||||
print(end=color.BOLD + color.UNDERLINE)
|
||||
print(BLOCK_NUM_HEADER.center(block_col_width), end=" " * BLOCK_NUM_PADDING_RIGHT)
|
||||
|
||||
print(
|
||||
" " * LEFT_PADDING +
|
||||
(" " * (FILE_SPACING + LEFT_PADDING + RIGHT_PADDING)).join(
|
||||
[ellipsize(name, NAME_MAX_LENGTH).center(NAME_MAX_LENGTH) for name in names]
|
||||
) + " " * RIGHT_PADDING
|
||||
)
|
||||
|
||||
print(end=color.RESET)
|
||||
|
||||
|
||||
def colorize(string: str, byte: int, colormap: Mapping[int, str]) -> str:
|
||||
if byte in colormap:
|
||||
return color.colors[colormap[byte]] + string + color.RESET
|
||||
return string
|
||||
|
||||
|
||||
def get_other_halfbyte(half: int, byte: int) -> int:
|
||||
if byte == 0:
|
||||
return 0
|
||||
if half & 0x0F == 0:
|
||||
return byte & 0x0F
|
||||
return byte & 0x1F0
|
||||
|
||||
|
||||
def get_color_map(intgroup: Sequence[Optional[int]]) -> Mapping[int, str]:
|
||||
intset = set(intgroup)
|
||||
if len(intset) == 1:
|
||||
return {}
|
||||
|
||||
from .utils import color
|
||||
# ncolors = list(sorted(color.colors.keys(), key=lambda c: c.replace("BRIGHT_", "")))
|
||||
ncolors = list(color.colors.keys()).copy()
|
||||
random.shuffle(ncolors)
|
||||
ncolors *= 3
|
||||
colormap = {}
|
||||
halfbytes = {}
|
||||
|
||||
for byte in intgroup:
|
||||
lo = byte & 0x0F
|
||||
hi = byte & 0x1F0
|
||||
|
||||
if lo not in halfbytes:
|
||||
halfbytes[lo] = []
|
||||
if hi not in halfbytes:
|
||||
halfbytes[hi] = []
|
||||
|
||||
halfbytes[lo].append(byte)
|
||||
halfbytes[hi].append(byte)
|
||||
|
||||
for halfbyte in halfbytes: # sorted(halfbytes, key=lambda half: int_distance_sort_key(half, halfbytes.keys())):
|
||||
if len(halfbytes[halfbyte]) == len(intgroup):
|
||||
# Halfbyte does not change
|
||||
continue
|
||||
colormap[halfbyte] = ncolors.pop(0)
|
||||
|
||||
for byte in intset:
|
||||
lo = byte & 0x0F
|
||||
hi = byte & 0x1F0
|
||||
|
||||
# The byte gets the color of the half with the least occurrences
|
||||
try:
|
||||
least_common = sorted([hi, lo], key=lambda half: len(halfbytes[half]))[0]
|
||||
colormap[byte] = colormap[least_common]
|
||||
except:
|
||||
print(byte, colormap)
|
||||
|
||||
return colormap
|
||||
|
||||
|
||||
def print_line(block_col_width, block_num: int, line: Sequence[Sequence[Optional[bytes]]]) -> None:
|
||||
global global_colormap
|
||||
print(
|
||||
color.BOLD + str_base(block_num, BLOCK_NUM_BASE).rjust(block_col_width),
|
||||
end=" " * BLOCK_NUM_PADDING_RIGHT + color.RESET
|
||||
)
|
||||
|
||||
hex_reprs = ["" if set(block) != {None} else None for block in line]
|
||||
ascii_reprs = hex_reprs.copy()
|
||||
|
||||
byte_count = 0
|
||||
|
||||
for bgroup in zip(*line):
|
||||
byte_count += 1
|
||||
intgroup = [int.from_bytes(byte, 'little') | 0x100 for byte in bgroup]
|
||||
|
||||
colormap = get_color_map(intgroup)
|
||||
colormap = {byte: color if byte not in global_colormap
|
||||
else global_colormap[byte] for byte, color in colormap.items()}
|
||||
global_colormap.update(colormap)
|
||||
|
||||
for i, byte in zip(range(len(hex_reprs)), intgroup):
|
||||
if hex_reprs[i] is None:
|
||||
continue
|
||||
if byte is None:
|
||||
hex_reprs[i] += " "
|
||||
ascii_reprs[i] += " "
|
||||
else:
|
||||
asuni = unicode_ascii_repr(byte & 0xFF)
|
||||
|
||||
lo = byte & 0x0F
|
||||
hi = byte & 0x1F0
|
||||
|
||||
hi_str = colorize("%01X" % ((byte & 0xF0) >> 4), hi, colormap)
|
||||
lo_str = colorize("%01X" % lo, lo, colormap)
|
||||
|
||||
hex_reprs[i] += hi_str + lo_str
|
||||
ascii_reprs[i] += colorize(asuni, byte, colormap)
|
||||
|
||||
if byte_count < BLOCK_BYTES:
|
||||
hex_reprs[i] += " " * BYTE_SPACING
|
||||
|
||||
reprs = []
|
||||
for hexr, asciir in zip(hex_reprs, ascii_reprs):
|
||||
if not hexr:
|
||||
reprs.append(" " * TOTAL_BLOCK_LENGTH)
|
||||
continue
|
||||
reprs.append(BLOCK_TEMPLATE.format(hexr=hexr, asciir=asciir))
|
||||
|
||||
print((" " * FILE_SPACING).join(reprs))
|
||||
|
||||
|
||||
def print_identical_line_bogus(block_col_width: int, last_line: Sequence[Sequence[Optional[bytes]]]):
|
||||
print(end=color.BRIGHT_BLACK)
|
||||
print(str(VERT_ELLIPSIS).rjust(block_col_width), end=" " * BLOCK_NUM_PADDING_RIGHT)
|
||||
|
||||
bogus_blocks = []
|
||||
for block in last_line:
|
||||
byteset = set([tuple(i) for i in block if i])
|
||||
bogus_char = NOT_ASCII_CHAR if len(byteset) > 0 else " "
|
||||
|
||||
bogus_blocks.append(
|
||||
BLOCK_TEMPLATE.replace("|", bogus_char).format(
|
||||
hexr=(" " * BYTE_SPACING).join(bogus_char * 2 for _ in range(BLOCK_BYTES)),
|
||||
asciir="".join(bogus_char for _ in range(BLOCK_BYTES))
|
||||
)
|
||||
)
|
||||
|
||||
print((" " * FILE_SPACING).join(bogus_blocks))
|
||||
print(end=color.RESET)
|
||||
|
||||
|
||||
def get_block_col_width(*filenames):
|
||||
max_size = max(
|
||||
map(
|
||||
lambda filename: os.stat(filename).st_size,
|
||||
filenames
|
||||
)
|
||||
)
|
||||
max_block = (max_size + BLOCK_BYTES - (max_size % BLOCK_BYTES)) / BLOCK_BYTES
|
||||
|
||||
digits = 0
|
||||
while max_block > 0:
|
||||
max_block //= BLOCK_NUM_BASE
|
||||
digits += 1
|
||||
|
||||
return max(digits, len(BLOCK_NUM_HEADER))
|
||||
|
||||
|
||||
def print_hexdiff(*filenames: str):
|
||||
names, files = zip(*gen_files(*filenames))
|
||||
block_col_width = get_block_col_width(*filenames)
|
||||
|
||||
print_names(block_col_width, *names)
|
||||
|
||||
identical_lines = 0
|
||||
prev_line = None
|
||||
line = [[] for _ in range(len(files))]
|
||||
block_num = 0
|
||||
block_bytes = 0
|
||||
remaining_files = len(files)
|
||||
|
||||
while remaining_files > 0:
|
||||
if block_bytes >= BLOCK_BYTES:
|
||||
if prev_line == line:
|
||||
identical_lines += 1
|
||||
if identical_lines == 1:
|
||||
print_identical_line_bogus(block_col_width, line)
|
||||
else:
|
||||
identical_lines = 0
|
||||
print_line(block_col_width, block_num, line)
|
||||
prev_line = line
|
||||
|
||||
block_bytes = 0
|
||||
block_num += 1
|
||||
line = [[] for _ in range(len(files))]
|
||||
|
||||
block_bytes += 1
|
||||
|
||||
for i in range(len(files)):
|
||||
f = files[i]
|
||||
file_line = line[i]
|
||||
|
||||
if f.closed:
|
||||
file_line.append(None)
|
||||
continue
|
||||
|
||||
try:
|
||||
byte = f.read(1)
|
||||
if not byte:
|
||||
raise EOFError
|
||||
line[i].append(byte)
|
||||
except EOFError:
|
||||
file_line.append(None)
|
||||
f.close()
|
||||
remaining_files -= 1
|
||||
|
||||
|
||||
def gen_files(*filenames: str) -> Generator[Tuple[str, BinaryIO], None, None]:
|
||||
for filename in filenames:
|
||||
f = open(filename, "rb")
|
||||
basename = os.path.basename(filename)
|
||||
yield (basename, f)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
raise SystemExit("At least one file needs to be specified")
|
||||
|
||||
print_hexdiff(*sys.argv[1:])
|
||||
|
13
setup.py
Normal file
13
setup.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='rebindiff',
|
||||
version='0.1',
|
||||
packages=find_packages(),
|
||||
license='GPL-3.0',
|
||||
author='Davide Depau',
|
||||
author_email='davide@depau.eu',
|
||||
entry_points = {
|
||||
'console_scripts': ['rebindiff=rebindiff.main:main'],
|
||||
}
|
||||
)
|
Loading…
Reference in a new issue