Skip to content

Instantly share code, notes, and snippets.

@developerfromjokela
Last active April 12, 2026 15:03
Show Gist options
  • Select an option

  • Save developerfromjokela/9c1ceb0a9e448460d0ad7a12cf716e1c to your computer and use it in GitHub Desktop.

Select an option

Save developerfromjokela/9c1ceb0a9e448460d0ad7a12cf716e1c to your computer and use it in GitHub Desktop.
Binary protocol diff tool, helps analyzing unknown binary protocols based on multiple capture datas
import tkinter as tk
from tkinter import ttk, filedialog
import os
import colorsys
FILE_HUES = [0.60, 0.05, 0.35, 0.15, 0.75, 0.50, 0.90, 0.28]
FONT_FAMILY = "Courier New"
FONT_SIZE = 11
LABEL_WIDTH = 180
MATCH_FG = "#888888"
def hue_fg(hue):
r, g, b = colorsys.hls_to_rgb(hue, 0.28, 0.85)
return "#{:02x}{:02x}{:02x}".format(int(r*255), int(g*255), int(b*255))
def hue_bg(hue):
r, g, b = colorsys.hls_to_rgb(hue, 0.85, 0.80)
return "#{:02x}{:02x}{:02x}".format(int(r*255), int(g*255), int(b*255))
class BinaryDiffViewer(tk.Tk):
def __init__(self):
super().__init__()
self.title("Binary Diff Viewer")
self.geometry("1100x520")
self.minsize(700, 300)
self.files: list[tuple[str, bytes]] = []
self._text_widgets: list[tk.Text] = []
self.view_mode = tk.StringVar(value="byte") # "byte" or "bit"
self._build_ui()
def _build_ui(self):
# Toolbar
top = ttk.Frame(self, padding=(8, 6))
top.pack(fill="x", side="top")
ttk.Button(top, text="Add Files…", command=self._add_files).pack(side="left", padx=2)
ttk.Button(top, text="Clear", command=self._clear).pack(side="left", padx=2)
ttk.Separator(top, orient="vertical").pack(side="left", fill="y", padx=6)
ttk.Button(top, text="Refresh", command=self._render).pack(side="left", padx=2)
ttk.Separator(top, orient="vertical").pack(side="left", fill="y", padx=6)
# View mode toggle
ttk.Label(top, text="View:").pack(side="left", padx=(4, 2))
ttk.Radiobutton(top, text="Byte", variable=self.view_mode,
value="byte", command=self._render).pack(side="left", padx=2)
ttk.Radiobutton(top, text="Bit", variable=self.view_mode,
value="bit", command=self._render).pack(side="left", padx=2)
ttk.Separator(self).pack(fill="x")
# Status bar
bot = ttk.Frame(self, padding=(8, 3))
bot.pack(fill="x", side="bottom")
ttk.Separator(self).pack(fill="x", side="bottom")
self.status_var = tk.StringVar(value="No files loaded.")
ttk.Label(bot, textvariable=self.status_var).pack(side="left")
# Shared horizontal scrollbar
self.hscroll = ttk.Scrollbar(self, orient="horizontal")
self.hscroll.pack(side="bottom", fill="x")
# Main area: canvas + vscroll
body = ttk.Frame(self)
body.pack(fill="both", expand=True)
body.columnconfigure(0, weight=1)
body.rowconfigure(0, weight=1)
vscroll = ttk.Scrollbar(body, orient="vertical")
vscroll.grid(row=0, column=1, sticky="ns")
self.canvas = tk.Canvas(body, highlightthickness=0,
yscrollcommand=vscroll.set, borderwidth=0)
self.canvas.grid(row=0, column=0, sticky="nsew")
vscroll.config(command=self.canvas.yview)
# Inner frame: each row is label + data side by side
self.inner = tk.Frame(self.canvas)
self._cwin = self.canvas.create_window(0, 0, anchor="nw", window=self.inner)
self.inner.columnconfigure(1, weight=1) # data column expands
self.canvas.bind("<Configure>",
lambda e: self.canvas.itemconfig(self._cwin, width=e.width))
self.inner.bind("<Configure>",
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.bind("<MouseWheel>", self._wheel_y)
self.canvas.bind("<Button-4>", self._wheel_y)
self.canvas.bind("<Button-5>", self._wheel_y)
self._show_placeholder()
def _add_files(self):
paths = filedialog.askopenfilenames(
title="Select binary files",
filetypes=[("All files", "*.*"),
("Binary", "*.bin *.exe *.dat *.rom *.img *.raw")])
added = False
for p in paths:
if not any(p == f[0] for f in self.files):
with open(p, "rb") as fh:
self.files.append((p, fh.read()))
added = True
if added:
self._render()
def _clear(self):
self.files.clear()
self._render()
def _remove(self, idx):
self.files.pop(idx)
self._render()
def _show_placeholder(self):
for w in self.inner.winfo_children():
w.destroy()
ttk.Label(self.inner,
text="\n Open files with 'Add Files…' to begin.\n").grid(
row=0, column=0, columnspan=2, sticky="w", padx=30, pady=40)
def _render(self):
if self.view_mode.get() == "bit":
self._render_bit()
else:
self._render_byte()
def _render_byte(self):
"""Original byte-level rendering"""
self._text_widgets = []
for w in self.inner.winfo_children():
w.destroy()
if not self.files:
self._show_placeholder()
self.status_var.set("No files loaded.")
self.hscroll.config(command=lambda *a: None)
return
sysbg = self.cget("bg")
n = len(self.files)
max_len = max(len(d) for _, d in self.files)
# Per-position agreement mask
agree = []
for i in range(max_len):
vals = set()
for _, d in self.files:
vals.add(d[i] if i < len(d) else None)
agree.append(len(vals) == 1)
row = 0 # grid row counter
ttk.Label(self.inner, text=" Offset →",
font=(FONT_FAMILY, FONT_SIZE - 1, "bold"),
anchor="w", width=22).grid(
row=row, column=0, sticky="nsew", padx=(4, 0), pady=(2, 0))
ruler = tk.Text(self.inner, font=(FONT_FAMILY, FONT_SIZE - 1),
wrap="none", relief="flat", bd=0,
height=1, padx=8, cursor="arrow",
spacing1=0, spacing3=0, bg=sysbg)
ruler.grid(row=row, column=1, sticky="ew", pady=(2, 0))
ruler.tag_config("addr", foreground="#555555")
ruler.tag_config("dim", foreground="#bbbbbb")
for i in range(0, max_len, 16):
lbl = f"0x{i:06X}"
ruler.insert("end", lbl, "addr")
ruler.insert("end", " " * (16 * 3 - len(lbl)), "dim")
ruler.config(state="disabled")
self._text_widgets.append(ruler)
row += 1
# Separator
ttk.Separator(self.inner, orient="horizontal").grid(
row=row, column=0, columnspan=2, sticky="ew")
row += 1
for fi, (path, data) in enumerate(self.files):
hue = FILE_HUES[fi % len(FILE_HUES)]
fg_c = hue_fg(hue)
bg_c = hue_bg(hue)
name = os.path.basename(path)
size = f"{len(data):,} B"
# Label cell — same grid row as the hex Text
lf = tk.Frame(self.inner, bg=bg_c)
lf.grid(row=row, column=0, sticky="nsew")
tk.Label(lf, text=f" {name}", bg=bg_c, fg=fg_c,
font=(FONT_FAMILY, FONT_SIZE, "bold"),
anchor="w").pack(side="left", padx=(6, 0))
tk.Label(lf, text=size, bg=bg_c, fg="#666666",
font=(FONT_FAMILY, 8), anchor="w").pack(side="left", padx=4)
rm = tk.Label(lf, text="✕", bg=bg_c, fg="#999999",
font=(FONT_FAMILY, 9), cursor="hand2")
rm.pack(side="right", padx=6)
rm.bind("<Button-1>", lambda e, i=fi: self._remove(i))
rm.bind("<Enter>", lambda e, w=rm: w.config(fg="#cc0000"))
rm.bind("<Leave>", lambda e, w=rm, c=bg_c: w.config(fg="#999999"))
# Hex Text — same grid row
txt = tk.Text(self.inner, font=(FONT_FAMILY, FONT_SIZE),
wrap="none", relief="flat", bd=0,
height=1, padx=8,
cursor="arrow", spacing1=0, spacing3=0, bg=sysbg)
txt.grid(row=row, column=1, sticky="ew")
txt.tag_config("match", foreground=MATCH_FG)
txt.tag_config("diff", foreground=fg_c, background=bg_c)
txt.tag_config("absent", foreground="#cccccc")
for i in range(max_len):
if i < len(data):
tag = "match" if agree[i] else "diff"
txt.insert("end", f"{data[i]:02X} ", tag)
else:
txt.insert("end", " ", "absent")
txt.config(state="disabled")
self._text_widgets.append(txt)
row += 1
ttk.Separator(self.inner, orient="horizontal").grid(
row=row, column=0, columnspan=2, sticky="ew")
row += 1
self._setup_scrolling()
diff_count = sum(1 for a in agree if not a)
pct = 100 * (max_len - diff_count) / max_len if max_len else 100
self.status_var.set(
f"{n} file(s) · {max_len:,} bytes max · "
f"{diff_count:,} differing · {pct:.1f}% identical"
)
self.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def _render_bit(self):
"""Bit-level rendering"""
self._text_widgets = []
for w in self.inner.winfo_children():
w.destroy()
if not self.files:
self._show_placeholder()
self.status_var.set("No files loaded.")
self.hscroll.config(command=lambda *a: None)
return
sysbg = self.cget("bg")
n = len(self.files)
max_len = max(len(d) for _, d in self.files)
max_bits = max_len * 8
# Per-position agreement mask (at bit level)
agree = []
for bit_idx in range(max_bits):
byte_idx = bit_idx // 8
bit_pos = 7 - (bit_idx % 8) # MSB first
vals = set()
for _, d in self.files:
if byte_idx < len(d):
bit_val = (d[byte_idx] >> bit_pos) & 1
vals.add(bit_val)
else:
vals.add(None)
agree.append(len(vals) == 1)
row = 0 # grid row counter
ttk.Label(self.inner, text=" Bit Offset →",
font=(FONT_FAMILY, FONT_SIZE - 1, "bold"),
anchor="w", width=22).grid(
row=row, column=0, sticky="nsew", padx=(4, 0), pady=(2, 0))
ruler = tk.Text(self.inner, font=(FONT_FAMILY, FONT_SIZE - 1),
wrap="none", relief="flat", bd=0,
height=1, padx=8, cursor="arrow",
spacing1=0, spacing3=0, bg=sysbg)
ruler.grid(row=row, column=1, sticky="ew", pady=(2, 0))
ruler.tag_config("addr", foreground="#555555")
ruler.tag_config("dim", foreground="#bbbbbb")
ruler.tag_config("byte_sep", foreground="#cccccc")
# Show byte boundaries in ruler
for byte_idx in range(max_len):
bit_start = byte_idx * 8
lbl = f"{bit_start}"
ruler.insert("end", lbl, "addr")
# Pad to align with 8 bits + space
padding = " " * (9 - len(lbl))
ruler.insert("end", padding, "dim")
ruler.config(state="disabled")
self._text_widgets.append(ruler)
row += 1
# Separator
ttk.Separator(self.inner, orient="horizontal").grid(
row=row, column=0, columnspan=2, sticky="ew")
row += 1
for fi, (path, data) in enumerate(self.files):
hue = FILE_HUES[fi % len(FILE_HUES)]
fg_c = hue_fg(hue)
bg_c = hue_bg(hue)
name = os.path.basename(path)
size = f"{len(data):,} B ({len(data) * 8:,} bits)"
# Label cell — same grid row as the bit Text
lf = tk.Frame(self.inner, bg=bg_c)
lf.grid(row=row, column=0, sticky="nsew")
tk.Label(lf, text=f" {name}", bg=bg_c, fg=fg_c,
font=(FONT_FAMILY, FONT_SIZE, "bold"),
anchor="w").pack(side="left", padx=(6, 0))
tk.Label(lf, text=size, bg=bg_c, fg="#666666",
font=(FONT_FAMILY, 8), anchor="w").pack(side="left", padx=4)
rm = tk.Label(lf, text="✕", bg=bg_c, fg="#999999",
font=(FONT_FAMILY, 9), cursor="hand2")
rm.pack(side="right", padx=6)
rm.bind("<Button-1>", lambda e, i=fi: self._remove(i))
rm.bind("<Enter>", lambda e, w=rm: w.config(fg="#cc0000"))
rm.bind("<Leave>", lambda e, w=rm, c=bg_c: w.config(fg="#999999"))
# Bit Text — same grid row
txt = tk.Text(self.inner, font=(FONT_FAMILY, FONT_SIZE),
wrap="none", relief="flat", bd=0,
height=1, padx=8,
cursor="arrow", spacing1=0, spacing3=0, bg=sysbg)
txt.grid(row=row, column=1, sticky="ew")
txt.tag_config("match", foreground=MATCH_FG)
txt.tag_config("diff", foreground=fg_c, background=bg_c)
txt.tag_config("absent", foreground="#cccccc")
txt.tag_config("byte_sep", foreground="#dddddd")
for bit_idx in range(max_bits):
byte_idx = bit_idx // 8
bit_pos = 7 - (bit_idx % 8) # MSB first
if byte_idx < len(data):
bit_val = (data[byte_idx] >> bit_pos) & 1
tag = "match" if agree[bit_idx] else "diff"
txt.insert("end", str(bit_val), tag)
else:
txt.insert("end", "·", "absent")
# Add space after each byte (every 8 bits)
if (bit_idx + 1) % 8 == 0:
txt.insert("end", " ", "byte_sep")
txt.config(state="disabled")
self._text_widgets.append(txt)
row += 1
ttk.Separator(self.inner, orient="horizontal").grid(
row=row, column=0, columnspan=2, sticky="ew")
row += 1
self._setup_scrolling()
diff_count = sum(1 for a in agree if not a)
pct = 100 * (max_bits - diff_count) / max_bits if max_bits else 100
self.status_var.set(
f"{n} file(s) · {max_bits:,} bits max · "
f"{diff_count:,} differing · {pct:.1f}% identical"
)
self.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def _setup_scrolling(self):
"""Setup shared scrolling for all text widgets"""
tw = self._text_widgets
def _on_xscroll(first, last):
self.hscroll.set(first, last)
for t in tw:
t.xview_moveto(first)
for t in tw:
t.config(xscrollcommand=_on_xscroll)
self.hscroll.config(command=lambda *a: [t.xview(*a) for t in tw])
def _wx(event):
d = (-5 if event.num == 6 else 5 if event.num == 7
else -int(event.delta / 40) if event.delta else 0)
for t in tw:
t.xview_scroll(d, "units")
return "break"
def _wy(event):
d = (-3 if event.num == 4 else 3 if event.num == 5
else -int(event.delta / 40) if event.delta else 0)
self.canvas.yview_scroll(d, "units")
return "break"
for t in tw:
t.bind("<MouseWheel>", _wy)
t.bind("<Button-4>", _wy)
t.bind("<Button-5>", _wy)
t.bind("<Shift-MouseWheel>", _wx)
t.bind("<Button-6>", _wx)
t.bind("<Button-7>", _wx)
def _wheel_y(self, event):
d = (-3 if event.num == 4 else 3 if event.num == 5
else -int(event.delta / 120) if event.delta else 0)
self.canvas.yview_scroll(d, "units")
if __name__ == "__main__":
BinaryDiffViewer().mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment