From 003bf5481ebbb820919fddbedabe0bbcad3f3cc9 Mon Sep 17 00:00:00 2001 From: godax84 Date: Thu, 5 Dec 2024 01:24:00 -0800 Subject: [PATCH] Carregar ficheiros para "/" --- binary_graph.py | 492 +++++++++++++++++++++ flash_window.py | 285 ++++++++++++ hexawork.log | Bin 0 -> 1024 bytes logger.py | 44 ++ main.py | 1093 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1914 insertions(+) create mode 100644 binary_graph.py create mode 100644 flash_window.py create mode 100644 hexawork.log create mode 100644 logger.py create mode 100644 main.py diff --git a/binary_graph.py b/binary_graph.py new file mode 100644 index 0000000..9a55940 --- /dev/null +++ b/binary_graph.py @@ -0,0 +1,492 @@ +import tkinter as tk +from tkinter import ttk, messagebox +import numpy as np +from logger import logger + +class BinaryGraphWindow: + def __init__(self, parent, binary_data): + try: + logger.info("Initializing Binary Graph Window") + + # Validate input + if not isinstance(binary_data, (list, np.ndarray)) or len(binary_data) == 0: + raise ValueError("Invalid binary data: must be a non-empty list or numpy array") + + # Store data as instance attribute + self.binary_data = list(binary_data) + self.parent = parent + + # Criar janela + self.window = tk.Toplevel(parent) + self.window.title("2D Binary Graph") + self.window.geometry("1000x600") + + # Frame principal + self.main_frame = ttk.Frame(self.window) + self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Frame para os botões de bits e configurações + self.top_frame = ttk.Frame(self.main_frame) + self.top_frame.pack(fill=tk.X, padx=5, pady=5) + + # Frame para os botões de bits + self.bits_frame = ttk.Frame(self.top_frame) + self.bits_frame.pack(side=tk.LEFT, padx=(0, 10)) + + # Frame para seleção de pontos + self.points_frame = ttk.Frame(self.top_frame) + self.points_frame.pack(side=tk.LEFT) + + # Variável para controlar o modo de bits + self.bits_mode = tk.StringVar(value="8b") + + # Variável para controlar o número de pontos + self.points_var = tk.IntVar(value=512) + + # Estilo para botões pequenos + style = ttk.Style() + style.configure("Small.TButton", padding=1) + + # Botões para seleção de bits + self.btn_8b = ttk.Button(self.bits_frame, text="8b", width=3, + style="Small.TButton", + command=lambda: self.change_bits_mode("8b")) + self.btn_8b.pack(side=tk.LEFT, padx=2) + + self.btn_16b = ttk.Button(self.bits_frame, text="16b", width=3, + style="Small.TButton", + command=lambda: self.change_bits_mode("16b")) + self.btn_16b.pack(side=tk.LEFT, padx=2) + + self.btn_32b = ttk.Button(self.bits_frame, text="32b", width=3, + style="Small.TButton", + command=lambda: self.change_bits_mode("32b")) + self.btn_32b.pack(side=tk.LEFT, padx=2) + + # Label e Spinbox para seleção de pontos + ttk.Label(self.points_frame, text="Points:").pack(side=tk.LEFT, padx=(0, 5)) + self.points_spinbox = ttk.Spinbox( + self.points_frame, + from_=5, + to=1500, + textvariable=self.points_var, + width=5, + command=self.update_points_display + ) + self.points_spinbox.pack(side=tk.LEFT, padx=(0, 5)) + + # Frame para canvas e scrollbar + self.graph_frame = ttk.Frame(self.main_frame) + self.graph_frame.pack(fill=tk.BOTH, expand=True) + + # Criar label para hover + self.hover_label = tk.Label(self.window, text="", bg='lightyellow', relief=tk.SOLID, borderwidth=1) + + # Canvas para o gráfico + self.canvas = tk.Canvas(self.graph_frame, bg='white') + self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + # Scrollbar horizontal + self.scrollbar = ttk.Scrollbar(self.graph_frame, + orient=tk.HORIZONTAL, + command=self.on_scroll) + self.scrollbar.pack(side=tk.BOTTOM, fill=tk.X) + + # Configurar canvas para usar scrollbar + self.canvas.configure(xscrollcommand=self.scrollbar.set) + + # Configurar eventos de mouse + self.canvas.bind('', self.on_hover) + self.canvas.bind('', self.hide_hover_label) + + # Configurações iniciais + self.max_display = 512 # Definir 512 pontos como padrão + self.current_start = 0 + self.points_var.set(512) # Definir valor inicial do spinbox + + # Processar dados iniciais + self.process_data() + + # Plotar dados iniciais + self.plot_data() + + # Destacar botão 8b inicialmente + self.update_bits_buttons() + + # Vincular evento de redimensionamento + self.canvas.bind('', self.on_resize) + + # Configurar scrolling com o mouse + self.canvas.bind('', self.on_mousewheel) + + logger.info(f"Binary Graph initialized with {len(binary_data)} data points") + + except Exception as e: + logger.error(f"Error initializing Binary Graph: {e}", exc_info=True) + messagebox.showerror("Initialization Error", str(e)) + if hasattr(self, 'window'): + self.window.destroy() + + def on_scroll(self, *args): + """Manipula a rolagem do scrollbar""" + try: + # Obter a posição atual do scrollbar + if len(args) > 1: # Scroll através da scrollbar + fraction = float(args[1]) + total_points = len(self.processed_data) + + # Calcular novo índice inicial baseado na fração da scrollbar + max_start = max(0, total_points - self.max_display) + self.current_start = int(fraction * max_start) + + # Garantir que current_start não ultrapasse o limite + self.current_start = max(0, min(self.current_start, max_start)) + + # Replottar com a nova posição + self.plot_data() + + logger.info(f"Scrolled to position {self.current_start}") + + except Exception as e: + logger.error(f"Error in scroll handling: {e}", exc_info=True) + + def update_scrollbar(self): + """Atualiza a posição e o tamanho do scrollbar""" + try: + total_points = len(self.processed_data) + + if total_points <= self.max_display: + # Se todos os pontos estão visíveis, desabilitar scrollbar + self.scrollbar.set(0, 1) + return + + # Calcular a fração visível + visible_fraction = self.max_display / total_points + + # Calcular a posição inicial como fração + max_start = total_points - self.max_display + start_fraction = self.current_start / max_start if max_start > 0 else 0 + + # Definir posição e tamanho do scrollbar + self.scrollbar.set(start_fraction, start_fraction + visible_fraction) + + logger.info(f"Updated scrollbar: position {start_fraction:.2f}, size {visible_fraction:.2f}") + + except Exception as e: + logger.error(f"Error updating scrollbar: {e}", exc_info=True) + + def change_bits_mode(self, mode): + # Definir o modo de bits + self.bits_mode.set(mode) + + # Atualizar estado dos botões + self.update_bits_buttons() + + # Processar dados com o novo modo de bits + self.process_data() + + # Resetar para 512 pontos ao mudar o modo de bits + self.points_var.set(512) + self.max_display = 512 + self.current_start = 0 + + # Replottar dados + self.plot_data() + logger.info(f"Changed graph mode to {mode}") + + def update_bits_buttons(self): + # Atualizar aparência dos botões + current_mode = self.bits_mode.get() + for btn, mode in [(self.btn_8b, "8b"), + (self.btn_16b, "16b"), + (self.btn_32b, "32b")]: + if mode == current_mode: + btn.state(['pressed']) + else: + btn.state(['!pressed']) + + def process_data(self): + # Processar dados de acordo com o modo selecionado + mode = self.bits_mode.get() + raw_data = self.binary_data + + if mode == "8b": + self.processed_data = raw_data + elif mode == "16b": + # Converter para 16 bits (combinar bytes adjacentes) + self.processed_data = [] + for i in range(0, len(raw_data)-1, 2): + value = (raw_data[i+1] << 8) | raw_data[i] + self.processed_data.append(value) + elif mode == "32b": + # Converter para 32 bits (combinar 4 bytes adjacentes) + self.processed_data = [] + for i in range(0, len(raw_data)-3, 4): + value = (raw_data[i+3] << 24) | (raw_data[i+2] << 16) | \ + (raw_data[i+1] << 8) | raw_data[i] + self.processed_data.append(value) + + def get_max_value_for_mode(self): + """Retorna o valor máximo para o modo de bits atual""" + mode = self.bits_mode.get() + if mode == "8b": + return 255 + elif mode == "16b": + return 65535 + else: # 32b + return 4294967295 + + def format_hex(self, value): + """Formata um número para hexadecimal sem o prefixo 0x""" + return format(value, 'X') + + def plot_data(self): + """Plota os dados no canvas""" + try: + # Limpar canvas + self.canvas.delete("all") + + # Obter dimensões do canvas + plot_width = self.canvas.winfo_width() - 100 # Margem para eixos + plot_height = self.canvas.winfo_height() - 100 # Margem para eixos + margin_left = 50 + margin_bottom = 50 + + if not self.processed_data or len(self.processed_data) == 0: + return + + # Calcular índices de início e fim para exibição + end_idx = min(self.current_start + self.max_display, len(self.processed_data)) + visible_data = self.processed_data[self.current_start:end_idx] + + if not visible_data: + return + + # Usar valor máximo fixo baseado no modo de bits + max_val = self.get_max_value_for_mode() + min_val = 0 # Sempre começar do zero + + # Calcular escalas + x_scale = plot_width / (len(visible_data) - 1) if len(visible_data) > 1 else plot_width + y_scale = plot_height / max_val if max_val != 0 else 1 + + # Preparar pontos para plotagem + points = [] + for i, value in enumerate(visible_data): + x = margin_left + (i * x_scale) + y = self.canvas.winfo_height() - margin_bottom - (value * y_scale) + points.extend([x, y]) + + # Plotar linha + if len(points) >= 4: + self.canvas.create_line(points, fill='blue', width=1) + + # Desenhar eixo X + self.canvas.create_line( + margin_left, self.canvas.winfo_height() - margin_bottom, + margin_left + plot_width, self.canvas.winfo_height() - margin_bottom, + fill='black' + ) + + # Desenhar eixo Y + self.canvas.create_line( + margin_left, self.canvas.winfo_height() - margin_bottom, + margin_left, margin_bottom, + fill='black' + ) + + # Desenhar marcações no eixo X + num_x_ticks = 10 + for i in range(num_x_ticks + 1): + x_pos = margin_left + (i * plot_width / num_x_ticks) + self.canvas.create_line( + x_pos, self.canvas.winfo_height() - margin_bottom, + x_pos, self.canvas.winfo_height() - margin_bottom + 5, + fill='black' + ) + x_val = self.current_start + int((i / num_x_ticks) * (end_idx - self.current_start)) + + # Multiplicar por 2 e converter para hex apenas no modo 16 bits + if self.bits_mode.get() == "16b": + display_val = x_val * 2 + hex_x_val = self.format_hex(display_val) + else: + hex_x_val = self.format_hex(x_val) + + self.canvas.create_text( + x_pos, self.canvas.winfo_height() - margin_bottom + 20, + text=hex_x_val + ) + + # Desenhar marcações no eixo Y + num_y_ticks = 5 + for i in range(num_y_ticks + 1): + y_val = int((i / num_y_ticks) * max_val) + y_pos = self.canvas.winfo_height() - margin_bottom - (y_val * y_scale) + self.canvas.create_line( + margin_left - 5, y_pos, + margin_left, y_pos, + fill='black' + ) + # Manter valor do eixo Y em decimal + self.canvas.create_text( + margin_left - 25, y_pos, + text=str(y_val) + ) + + # Atualizar scrollbar + self.update_scrollbar() + + # Atualizar título do gráfico + title = f"{self.bits_mode.get()}-bit Binary Data" + self.canvas.create_text( + self.canvas.winfo_width() // 2, + 20, + text=title, + font=('Arial', 12, 'bold') + ) + + logger.info(f"Plotted {len(visible_data)} points from index {self.current_start} to {end_idx}") + + except Exception as e: + logger.error(f"Error plotting data: {e}", exc_info=True) + messagebox.showerror("Plotting Error", str(e)) + + def on_resize(self, event): + # Replottar quando o canvas for redimensionado + self.plot_data() + + def update_points_display(self): + """Atualiza o número de pontos exibidos no gráfico""" + try: + # Obter o novo número de pontos do spinbox + new_points = self.points_var.get() + + # Validar entrada + if new_points < 5 or new_points > 1500: + messagebox.showerror("Invalid Input", "Points must be between 5 and 1500") + # Restaurar o valor anterior + self.points_var.set(self.max_display) + return + + # Limpar canvas antes de replottar + self.canvas.delete("all") + + # Atualizar número máximo de pontos + self.max_display = new_points + + # Garantir que o início atual não ultrapasse o total de pontos + total_points = len(self.processed_data) + self.current_start = min(self.current_start, max(0, total_points - new_points)) + + # Replottar dados + self.plot_data() + + # Atualizar scrollbar + self.update_scrollbar() + + logger.info(f"Updated graph display to {new_points} points") + + except Exception as e: + logger.error(f"Error updating points display: {e}", exc_info=True) + messagebox.showerror("Update Error", str(e)) + + def on_mousewheel(self, event): + # Manipula a rolagem com o mouse + try: + # Obter a posição atual do scrollbar + total_points = len(self.processed_data) + + # Calcular novo índice inicial baseado na fração da scrollbar + max_start = max(0, total_points - self.max_display) + if event.delta > 0: + self.current_start = max(0, self.current_start - 10) + else: + self.current_start = min(max_start, self.current_start + 10) + + # Replottar com a nova posição + self.plot_data() + + logger.info(f"Scrolled to position {self.current_start}") + + except Exception as e: + logger.error(f"Error in mousewheel handling: {e}", exc_info=True) + + def on_hover(self, event): + try: + # Parâmetros de plotagem + width = self.canvas.winfo_width() + height = self.canvas.winfo_height() + margin_left = 50 + margin_bottom = 50 + plot_width = width - 100 + plot_height = height - 100 + + # Remover linhas de hover anteriores + self.canvas.delete('hover_line', 'hover_marker') + + # Verificar se o mouse está dentro da área do gráfico + if (event.x < margin_left or event.x > width - 50 or + event.y < 50 or event.y > height - margin_bottom): + self.hide_hover_label(event) + return + + # Dados processados + data = self.processed_data + end_idx = min(self.current_start + self.max_display, len(data)) + visible_data = data[self.current_start:end_idx] + + if not visible_data: + return + + # Calcular índice X baseado na posição relativa do mouse + mouse_x_rel = event.x - margin_left + x_scale = plot_width / (len(visible_data) - 1) if len(visible_data) > 1 else plot_width + x_index = int(mouse_x_rel / x_scale) + + if x_index < 0 or x_index >= len(visible_data): + return + + # Calcular valor Y + y_value = visible_data[x_index] + x_pos = self.current_start + x_index + + # Calcular coordenadas para linha vertical + line_x = margin_left + (x_index * x_scale) + + # Desenhar linha vertical + self.canvas.create_line( + line_x, 50, + line_x, height - margin_bottom, + fill='red', + dash=(4, 4), + tags='hover_line' + ) + + # Formatar valores + hex_x = format(x_pos * 2, 'X') if self.bits_mode.get() == "16b" else format(x_pos, 'X') + hex_y = format(y_value, 'X') + + # Atualizar label de hover + hover_text = ( + f"X: 0x{hex_x}\n" + f"Y: {y_value} (0x{hex_y})" + ) + + self.hover_label.config(text=hover_text) + + # Posicionar label próximo ao cursor + self.hover_label.place( + x=event.x_root - self.window.winfo_rootx() + 10, + y=event.y_root - self.window.winfo_rooty() + 10 + ) + self.hover_label.lift() + + except Exception as e: + logger.error(f"Erro no hover: {e}") + + def hide_hover_label(self, event): + # Esconder label quando mouse sai do gráfico + self.hover_label.place_forget() + # Remover linhas de hover + self.canvas.delete('hover_line', 'hover_marker') diff --git a/flash_window.py b/flash_window.py new file mode 100644 index 0000000..04865ad --- /dev/null +++ b/flash_window.py @@ -0,0 +1,285 @@ +import tkinter as tk +from tkinter import filedialog, messagebox, ttk, scrolledtext +from view_parameters import ViewParametersDialog +import struct +import os +import logging +from logger import logger # Use the centralized logger + +# Create a logger +# logger = logging.getLogger(__name__) + +class FlashWindow: + def __init__(self, master, main_window): + try: + logger.info("Initializing FlashWindow") + + # Validate input + if master is None: + raise ValueError("Master window cannot be None") + + # Store references + self.parent = master + self.main_window = main_window # Referência à janela principal + + # Create window + self.window = tk.Toplevel(master) + self.window.title("Flash File - EDC15") + self.window.geometry("800x600") + + # Minimizar janela principal quando abrir Flash + self.main_window.iconify() + + # Configurar para restaurar janela principal quando fechar + self.window.protocol("WM_DELETE_WINDOW", self.on_close) + + # Add a frame to the new window + self.flash_frame = ttk.Frame(self.window) + self.flash_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) + + # Create buttons frame + self.buttons_frame = ttk.Frame(self.flash_frame) + self.buttons_frame.pack(fill=tk.X, pady=(0, 10)) + + # Add Open and Save buttons + ttk.Button(self.buttons_frame, text="Open File", command=self.open_file).pack(side=tk.LEFT, padx=5) + ttk.Button(self.buttons_frame, text="Save File", command=self.save_file).pack(side=tk.LEFT, padx=5) + ttk.Button(self.buttons_frame, text="Parâmetros", command=self.show_parameters).pack(side=tk.LEFT, padx=5) + + # Botão para alternar entre Hexa e Decimal + self.btn_view_mode = ttk.Button(self.buttons_frame, text="Hexa", command=self.toggle_view_mode) + self.btn_view_mode.pack(side=tk.LEFT, padx=5) + + # Botão 2D + self.btn_2d_view = ttk.Button(self.buttons_frame, text="2D", command=self.open_2d_graph) + self.btn_2d_view.pack(side=tk.LEFT, padx=5) + + # File path label + self.file_path = tk.StringVar() + self.file_path_label = ttk.Label(self.flash_frame, textvariable=self.file_path, wraplength=780) + self.file_path_label.pack(fill=tk.X, pady=5) + + # Create text area for binary content + self.content_frame = ttk.LabelFrame(self.flash_frame, text="Binary Content") + self.content_frame.pack(fill=tk.BOTH, expand=True, pady=5) + + self.text_area = scrolledtext.ScrolledText(self.content_frame, wrap=tk.WORD, width=80, height=20) + self.text_area.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.text_area.config(font=('Courier', 10)) + + # Store binary data and view parameters + self.binary_data = None + self.view_params = { + 'first_cell': '0000F0', + 'offset': 0, + 'columns': 16, + 'hex_view': False, + 'show_diff': True, + 'value_type': '8bit', + 'address_type': '8bit' + } + + logger.info("FlashWindow initialized successfully") + + except Exception as e: + logger.error(f"Error initializing FlashWindow: {e}", exc_info=True) + messagebox.showerror("Initialization Error", str(e)) + if hasattr(self, 'window'): + self.window.destroy() + + def format_value(self, data, start_idx, value_type, hex_view): + try: + # Primeiro, garantir que temos bytes suficientes + if value_type == '8bit' and len(data) < 1: + return "?" * (2 if hex_view else 3) + elif value_type == '16bit' and len(data) < 2: + return "?" * (4 if hex_view else 5) + elif (value_type == '32bit' or value_type == 'float') and len(data) < 4: + return "?" * (8 if hex_view else 10) + + # Agora processar o valor + if value_type == '8bit': + value = data[0] + if hex_view: + return f"{value:02X}" + return f"{value:03d}" + + elif value_type == '16bit': + value = struct.unpack('>H', data[:2])[0] # big-endian + if hex_view: + return f"{value:04X}" + return f"{value:05d}" + + elif value_type == '32bit': + value = struct.unpack('>I', data[:4])[0] # big-endian + if hex_view: + return f"{value:08X}" + return f"{value:010d}" + + elif value_type == 'float': + value = struct.unpack('>f', data[:4])[0] # big-endian + return f"{value:10.4f}" + + except (IndexError, struct.error) as e: + return "?" * (2 if hex_view else 3) + + def get_value_size(self, value_type): + if value_type == '8bit': + return 1 + elif value_type == '16bit': + return 2 + else: # 32bit ou float + return 4 + + def show_parameters(self): + dialog = ViewParametersDialog(self.window, self.view_params) + result = dialog.show() + if result: + self.view_params = result + self.display_binary_data() + + def open_file(self): + file_path = filedialog.askopenfilename( + title="Select Binary File", + filetypes=[ + ("Binary files", "*.bin"), + ("All files", "*.*") + ] + ) + if file_path: + self.file_path.set(f"Selected file: {file_path}") + try: + with open(file_path, 'rb') as f: + self.binary_data = f.read() + self.display_binary_data() + except Exception as e: + messagebox.showerror("Error", f"Error reading file: {str(e)}") + + def display_binary_data(self): + if self.binary_data: + self.text_area.delete('1.0', tk.END) + + # Get parameters + bytes_per_row = self.view_params['columns'] + start_offset = self.view_params['offset'] + hex_view = self.view_params['hex_view'] + value_type = self.view_params['value_type'] + + # Update frame title + mode_text = "Hexadecimal" if hex_view else "Decimal" + value_text = f"{value_type} values" + if value_type == 'float': + value_text = "Floating point values" + self.content_frame.configure(text=f"Binary Content - {mode_text} ({value_text})") + + # Calculate display range + display_data = self.binary_data[start_offset:] + + # Get the size of each value based on type + value_size = self.get_value_size(value_type) + values_per_row = bytes_per_row // value_size + + for i in range(0, len(display_data), bytes_per_row): + # Format offset (from the first_cell value) + base_offset = int(self.view_params['first_cell'], 16) + current_offset = base_offset + i + offset = f"{current_offset:08X}" + + # Get row data + row_data = display_data[i:i + bytes_per_row] + + # Format values + values = [] + ascii_values = [] + + # Process each value in the row + for j in range(0, len(row_data), value_size): + value_data = row_data[j:j + value_size] + if len(value_data) == value_size: + # Format the value according to type + value_str = self.format_value(value_data, j, value_type, hex_view) + values.append(value_str) + + # ASCII representation (only for 8-bit values) + if value_type == '8bit': + b = value_data[0] + ascii_values.append(chr(b) if 32 <= b <= 126 else '.') + else: + ascii_values.extend(['.' for _ in range(value_size)]) + + # Join values and ASCII + values_part = " ".join(values) + ascii_part = "".join(ascii_values) + + # Add the line to display + line = f"{offset}: {values_part} | {ascii_part}\n" + self.text_area.insert(tk.END, line) + + self.text_area.see('1.0') # Scroll to top + + def save_file(self): + if not self.binary_data: + messagebox.showwarning("Warning", "No data to save. Please open a file first.") + return + + file_path = filedialog.asksaveasfilename( + title="Save Binary File", + defaultextension=".bin", + filetypes=[ + ("Binary files", "*.bin"), + ("All files", "*.*") + ] + ) + if file_path: + try: + with open(file_path, 'wb') as f: + f.write(self.binary_data) + self.file_path.set(f"File saved to: {file_path}") + except Exception as e: + messagebox.showerror("Error", f"Error saving file: {str(e)}") + + def open_2d_graph(self): + try: + # Ensure binary data is available + if not hasattr(self, 'binary_data') or self.binary_data is None: + logger.warning("No binary data available to plot graph") + messagebox.showwarning("Graph Error", "No binary data available to plot.") + return + + # Import BinaryGraphWindow dynamically to avoid circular imports + from binary_graph import BinaryGraphWindow + + # Convert binary data to byte values if not already done + # Ensure we're passing a list of integers + byte_values = [int(byte) for byte in self.binary_data] + + # Open binary graph window + graph_window = BinaryGraphWindow(self.window, byte_values) + + logger.info("Binary graph window opened successfully") + + except ImportError as e: + logger.error(f"Failed to import BinaryGraphWindow: {e}") + messagebox.showerror("Import Error", "Could not load graph module.") + + except Exception as e: + logger.error(f"Error opening binary graph: {e}", exc_info=True) + messagebox.showerror("Graph Error", f"Could not open graph: {e}") + + def toggle_view_mode(self): + # Alternar entre hexadecimal e decimal + current_mode = self.view_params.get('hex_view', False) + self.view_params['hex_view'] = not current_mode + + # Atualizar texto do botão + self.btn_view_mode.config(text="Decimal" if current_mode else "Hexa") + + # Redesenhar dados + self.display_binary_data() + + def on_close(self): + # Restaurar janela principal + self.main_window.deiconify() + + # Fechar janela Flash + self.window.destroy() diff --git a/hexawork.log b/hexawork.log new file mode 100644 index 0000000000000000000000000000000000000000..06d7405020018ddf3cacee90fd4af10487da3d20 GIT binary patch literal 1024 ScmZQz7zLvtFd70QH3R?z00031 literal 0 HcmV?d00001 diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..fa82f7e --- /dev/null +++ b/logger.py @@ -0,0 +1,44 @@ +import logging +import os +from datetime import datetime + +def setup_logger(name='hexaw_logger', log_dir='logs'): + """ + Set up a logger with file and console output. + + :param name: Name of the logger + :param log_dir: Directory to store log files + :return: Configured logger + """ + # Create logs directory if it doesn't exist + os.makedirs(log_dir, exist_ok=True) + + # Generate a log filename with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_file = os.path.join(log_dir, f'{name}_{timestamp}.log') + + # Create logger + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + + # Create file handler + file_handler = logging.FileHandler(log_file, encoding='utf-8') + file_handler.setLevel(logging.DEBUG) + + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + + # Create formatter + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + # Add handlers to logger + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logger + +# Create a global logger instance +logger = setup_logger() diff --git a/main.py b/main.py new file mode 100644 index 0000000..8d5effa --- /dev/null +++ b/main.py @@ -0,0 +1,1093 @@ +import tkinter as tk +from tkinter import filedialog, messagebox, ttk, scrolledtext +import logging +import binascii +import os +import functools +import re +import struct +import traceback +import unicodedata +from flash_window import FlashWindow + +class HexaWorkApp: + def __init__(self, master): + self.master = master + master.title("HexaWork - Byte Inspector") + master.geometry("1000x800") + + # Configure logging + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('hexawork.log', mode='w'), + logging.StreamHandler() + ] + ) + self.logger = logging.getLogger(__name__) + + # Create main frame + self.main_frame = ttk.Frame(master) + self.main_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) + + # Create menu + self.create_menu() + + # File selection + self.create_file_selection_section() + + # Hex view + self.create_hex_view_section() + + # Order Analysis section + self.create_order_analysis_section() + + # Inspector section + self.create_inspector_section() + + # Search section + self.create_search_section() + + # Selected bytes for analysis + self.selected_bytes = None + + def create_menu(self): + """Create application menu""" + menubar = tk.Menu(self.master) + self.master.config(menu=menubar) + + # File menu + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Open", command=self.select_file) + file_menu.add_command(label="Save", command=self.save_edited_file, state=tk.NORMAL) + file_menu.add_command(label="Analyze ASCII", command=self.analyze_ascii_patterns, state=tk.NORMAL) + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self.master.quit) + + # Flash menu + flash_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Flash", menu=flash_menu) + flash_menu.add_command(label="EDC15", command=self.open_flash_window) + + def show_compare_dialog(self): + """Show dialog to select files for comparison""" + try: + # Get original file + original_file = filedialog.askopenfilename(title="Select Original File") + if not original_file: + return + + # Get modified file + modified_file = filedialog.askopenfilename(title="Select Modified File") + if not modified_file: + return + + self.compare_files(original_file, modified_file) + except Exception as e: + self.logger.error(f"Error in compare dialog: {str(e)}") + messagebox.showerror("Error", str(e)) + + def compare_files(self, original_path, modified_path): + """Compare two binary files and show differences side by side""" + try: + # Read files + with open(original_path, 'rb') as f1, open(modified_path, 'rb') as f2: + original_data = f1.read() + modified_data = f2.read() + + # Check file sizes + if len(original_data) != len(modified_data): + raise ValueError(f"File sizes do not match!\nOriginal: {len(original_data)} bytes\nModified: {len(modified_data)} bytes") + + # Create comparison window + compare_window = tk.Toplevel(self.master) + compare_window.title("File Comparison") + compare_window.geometry("1400x850") + + # Create main frame with paned window + main_pane = ttk.PanedWindow(compare_window, orient=tk.VERTICAL) + main_pane.pack(fill=tk.BOTH, expand=True) + + # Top pane for file comparison + top_pane = ttk.Frame(main_pane) + main_pane.add(top_pane) + + # Navigation frame + nav_frame = ttk.Frame(top_pane) + nav_frame.pack(fill=tk.X, pady=5) + + # Create horizontal paned window for files and diff + file_pane = ttk.PanedWindow(top_pane, orient=tk.HORIZONTAL) + file_pane.pack(fill=tk.BOTH, expand=True) + + # Create frames for original and modified files with their order analysis + original_container = ttk.Frame(file_pane) + modified_container = ttk.Frame(file_pane) + diff_frame = ttk.Frame(file_pane) + + file_pane.add(original_container, weight=1) + file_pane.add(modified_container, weight=1) + file_pane.add(diff_frame, weight=1) + + # Original file section + original_file_frame = ttk.Frame(original_container) + original_file_frame.pack(fill=tk.BOTH, expand=True) + + # Create labels for file names + ttk.Label(original_file_frame, text=f"Original: {os.path.basename(original_path)}").pack() + + # Create scrolled text widgets for hex views + original_text = scrolledtext.ScrolledText(original_file_frame, wrap=tk.NONE, font=('Courier', 10)) + original_text.pack(fill=tk.BOTH, expand=True) + + # Modified file section + modified_file_frame = ttk.Frame(modified_container) + modified_file_frame.pack(fill=tk.BOTH, expand=True) + + # Create labels for file names + ttk.Label(modified_file_frame, text=f"Modified: {os.path.basename(modified_path)}").pack() + + modified_text = scrolledtext.ScrolledText(modified_file_frame, wrap=tk.NONE, font=('Courier', 10)) + modified_text.pack(fill=tk.BOTH, expand=True) + + # Order analysis frames for original and modified files + original_order_container = ttk.Frame(original_container) + original_order_container.pack(fill=tk.X) + + modified_order_container = ttk.Frame(modified_container) + modified_order_container.pack(fill=tk.X) + + # Create differences table + columns = ('Address', 'Original', 'Modified') + diff_table = ttk.Treeview(diff_frame, columns=columns, show='headings') + for col in columns: + diff_table.heading(col, text=col) + diff_table.column(col, width=100) + + # Add scrollbar to table + table_scroll = ttk.Scrollbar(diff_frame, orient=tk.VERTICAL, command=diff_table.yview) + diff_table.configure(yscrollcommand=table_scroll.set) + + # Pack table and scrollbar + table_scroll.pack(side=tk.RIGHT, fill=tk.Y) + diff_table.pack(fill=tk.BOTH, expand=True) + + # Find and store differences + differences = list(self.find_differences(original_data, modified_data)) + + # Populate differences table + for offset, orig, mod in differences: + diff_table.insert('', tk.END, values=( + f"{offset:08X}", + orig, + mod + )) + + # Populate hex views + for i in range(0, len(original_data), 16): + # Format offset + offset = f"{i:08X}" + + # Original file line + orig_line = [offset, ": "] + mod_line = [offset, ": "] + + # Process 16 bytes per line + for j in range(16): + if i + j < len(original_data): + orig_byte = original_data[i + j] + mod_byte = modified_data[i + j] + + # Check for differences + if orig_byte != mod_byte: + orig_line.append(("normal", f"{orig_byte:02X} ")) + mod_line.append(("red", f"{mod_byte:02X} ")) + else: + orig_line.append(("normal", f"{orig_byte:02X} ")) + mod_line.append(("normal", f"{mod_byte:02X} ")) + + # Add ASCII representation + orig_line.append(" ") + mod_line.append(" ") + for j in range(16): + if i + j < len(original_data): + orig_byte = original_data[i + j] + mod_byte = modified_data[i + j] + + # ASCII for original + if 32 <= orig_byte <= 126: + orig_line.append(("normal", chr(orig_byte))) + else: + orig_line.append(("normal", ".")) + + # ASCII for modified + if 32 <= mod_byte <= 126: + mod_line.append(("normal" if orig_byte == mod_byte else "red", chr(mod_byte))) + else: + mod_line.append(("normal" if orig_byte == mod_byte else "red", ".")) + + orig_line.append("\n") + mod_line.append("\n") + + # Display lines + original_text.tag_config("red", foreground="black", background="lightcoral") + modified_text.tag_config("red", foreground="black", background="lightcoral") + + # Display original line + for part in orig_line: + if isinstance(part, tuple): + color, text = part + original_text.insert(tk.END, text) + else: + original_text.insert(tk.END, part) + + # Display modified line + for part in mod_line: + if isinstance(part, tuple): + color, text = part + if color == "red": + modified_text.insert(tk.END, text, "red") + else: + modified_text.insert(tk.END, text) + else: + modified_text.insert(tk.END, part) + + # Initial order analysis for first 16 bytes + original_analysis = self.generate_order_analysis(original_data[:16]) + modified_analysis = self.generate_order_analysis(modified_data[:16]) + + # Create initial order analysis frames + self.original_order_frame = self.create_order_analysis_frame(original_order_container, original_analysis) + self.modified_order_frame = self.create_order_analysis_frame(modified_order_container, modified_analysis) + + # Make text views read-only + original_text.configure(state='disabled') + modified_text.configure(state='disabled') + + # Navigation variables + current_diff_index = tk.IntVar(value=-1) + + # Navigation buttons + def navigate_differences(direction): + nonlocal current_diff_index + + if not differences: + messagebox.showinfo("Info", "No differences found.") + return + + # Update index based on direction + if direction == 'next': + current_diff_index.set(min(current_diff_index.get() + 1, len(differences) - 1)) + else: # previous + current_diff_index.set(max(current_diff_index.get() - 1, 0)) + + # Get current difference + offset, orig_byte, mod_byte = differences[current_diff_index.get()] + + # Calculate line and scroll position + line_number = offset // 16 + byte_in_line = offset % 16 + + # Enable text widgets temporarily to scroll + original_text.configure(state='normal') + modified_text.configure(state='normal') + + # Scroll to the specific line + original_text.see(f"{line_number + 1}.0") + modified_text.see(f"{line_number + 1}.0") + + # Remove previous highlights + original_text.tag_remove("current_diff", "1.0", tk.END) + modified_text.tag_remove("current_diff", "1.0", tk.END) + original_text.tag_remove("exact_diff", "1.0", tk.END) + modified_text.tag_remove("exact_diff", "1.0", tk.END) + + # Highlight the entire line in yellow + original_text.tag_add("current_diff", f"{line_number + 1}.0", f"{line_number + 2}.0") + modified_text.tag_add("current_diff", f"{line_number + 1}.0", f"{line_number + 2}.0") + + # Configure tags + original_text.tag_config("current_diff", background="lightyellow") + modified_text.tag_config("current_diff", background="lightyellow") + original_text.tag_config("exact_diff", background="lightcoral") + modified_text.tag_config("exact_diff", background="lightcoral") + + # Calculate exact byte position in the text + # Each hex value is 3 characters long (2 hex + 1 space) + # Offset starts after the line number and ": " + hex_start_offset = 10 + (byte_in_line * 3) + + # Add exact difference highlight + original_text.tag_add("exact_diff", + f"{line_number + 1}.{hex_start_offset}", + f"{line_number + 1}.{hex_start_offset + 2}") + modified_text.tag_add("exact_diff", + f"{line_number + 1}.{hex_start_offset}", + f"{line_number + 1}.{hex_start_offset + 2}") + + # Disable text widgets again + original_text.configure(state='disabled') + modified_text.configure(state='disabled') + + # Update diff table selection + diff_table.selection_remove(diff_table.selection()) + diff_table.selection_add(diff_table.get_children()[current_diff_index.get()]) + diff_table.focus(diff_table.get_children()[current_diff_index.get()]) + diff_table.see(diff_table.get_children()[current_diff_index.get()]) + + # Create navigation buttons + prev_button = ttk.Button(nav_frame, text="Previous Difference", + command=lambda: navigate_differences('previous')) + prev_button.pack(side=tk.LEFT, padx=5) + + next_button = ttk.Button(nav_frame, text="Next Difference", + command=lambda: navigate_differences('next')) + next_button.pack(side=tk.LEFT, padx=5) + + # Difference count label + diff_count_label = ttk.Label(nav_frame, + text=f"Total Differences: {len(differences)}") + diff_count_label.pack(side=tk.LEFT, padx=10) + + # Enable text selection and analysis + def create_selection_handler(text_widget, data): + def on_select(event=None): + try: + # Get selected text + if not text_widget.tag_ranges(tk.SEL): + return + + start = text_widget.index(tk.SEL_FIRST) + end = text_widget.index(tk.SEL_LAST) + selected_text = text_widget.get(start, end) + + # Extract hex values + hex_values = [] + for part in selected_text.split(): + if len(part) == 2 and all(c in '0123456789ABCDEFabcdef' for c in part): + hex_values.append(part.upper()) + + if hex_values: + # Convert to bytes + hex_str = ''.join(hex_values) + selected_bytes = bytes.fromhex(hex_str) + + # Generate and update order analysis + analysis = self.generate_order_analysis(selected_bytes) + + # Update the corresponding order analysis frame + if text_widget == original_text: + # Destroy existing frame and create new one + for widget in original_order_container.winfo_children(): + widget.destroy() + self.create_order_analysis_frame(original_order_container, analysis) + else: + # Destroy existing frame and create new one + for widget in modified_order_container.winfo_children(): + widget.destroy() + self.create_order_analysis_frame(modified_order_container, analysis) + + except Exception as e: + self.logger.error(f"Error in selection handler: {e}") + + return on_select + + # Bind selection handlers + original_text.bind('<>', create_selection_handler(original_text, original_data)) + modified_text.bind('<>', create_selection_handler(modified_text, modified_data)) + + except Exception as e: + self.logger.error(f"Error comparing files: {str(e)}") + messagebox.showerror("Error", str(e)) + + def find_differences(self, original_data, modified_data): + """Find unique byte differences between two files""" + differences = [] + i = 0 + while i < min(len(original_data), len(modified_data)): + # If bytes are different + if original_data[i] != modified_data[i]: + # Look ahead to find the extent of the difference + diff_length = 1 + while (i + diff_length < len(original_data) and + i + diff_length < len(modified_data) and + original_data[i + diff_length] != modified_data[i + diff_length]): + diff_length += 1 + + # Only add if this is the start of a unique difference sequence + # Check if this difference is not a continuation of a previous one + if not differences or i > differences[-1][0] + len(differences[-1][2]): + # Convert to hex for easier comparison + orig_sequence = ' '.join(f'{original_data[i + j]:02X}' for j in range(min(6, diff_length))) + mod_sequence = ' '.join(f'{modified_data[i + j]:02X}' for j in range(min(6, diff_length))) + + differences.append((i, orig_sequence, mod_sequence)) + + # Skip to the end of this difference + i += diff_length + else: + i += 1 + + return differences + + def generate_order_analysis(self, data): + """ + Generate order analysis for a given byte sequence + + Args: + data (bytes): Byte sequence to analyze + + Returns: + dict: Dictionary with order analysis results + """ + try: + # Normal Order Analysis + normal_hex_str = ''.join(f'{b:02X}' for b in data) + normal_int = int(normal_hex_str, 16) + + # Reverse Order Analysis + reverse_hex_str = ''.join(f'{b:02X}' for b in reversed(data)) + reverse_int = int(reverse_hex_str, 16) + + # Calculate number of bits + num_bits = len(data) * 8 + + return { + 'normal': { + 'hex': normal_hex_str, + 'dec': str(normal_int), + 'ihex': f'{~normal_int & ((1 << num_bits) - 1):X}', + 'idec': str(~normal_int & ((1 << num_bits) - 1)) + }, + 'reverse': { + 'hex': reverse_hex_str, + 'dec': str(reverse_int), + 'ihex': f'{~reverse_int & ((1 << num_bits) - 1):X}', + 'idec': str(~reverse_int & ((1 << num_bits) - 1)) + } + } + except Exception as e: + self.logger.error(f"Error in order analysis: {e}") + return None + + def create_order_analysis_frame(self, parent, analysis): + """ + Create a frame to display order analysis + + Args: + parent (tk.Widget): Parent widget + analysis (dict): Order analysis results + + Returns: + ttk.LabelFrame: Frame with order analysis + """ + order_frame = ttk.LabelFrame(parent, text="Order Analysis") + order_frame.pack(fill=tk.X, pady=5) + + # Normal Order + normal_frame = ttk.Frame(order_frame) + normal_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + ttk.Label(normal_frame, text="Normal Order", font=('Helvetica', 10, 'bold')).pack() + ttk.Label(normal_frame, text=f"HEX: {analysis['normal']['hex']}").pack(anchor='w') + ttk.Label(normal_frame, text=f"DEC: {analysis['normal']['dec']}").pack(anchor='w') + ttk.Label(normal_frame, text=f"!HEX: {analysis['normal']['ihex']}").pack(anchor='w') + ttk.Label(normal_frame, text=f"!DEC: {analysis['normal']['idec']}").pack(anchor='w') + + # Reverse Order + reverse_frame = ttk.Frame(order_frame) + reverse_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) + + ttk.Label(reverse_frame, text="Reverse Order", font=('Helvetica', 10, 'bold')).pack() + ttk.Label(reverse_frame, text=f"HEX: {analysis['reverse']['hex']}").pack(anchor='w') + ttk.Label(reverse_frame, text=f"DEC: {analysis['reverse']['dec']}").pack(anchor='w') + ttk.Label(reverse_frame, text=f"!HEX: {analysis['reverse']['ihex']}").pack(anchor='w') + ttk.Label(reverse_frame, text=f"!DEC: {analysis['reverse']['idec']}").pack(anchor='w') + + return order_frame + + def create_file_selection_section(self): + file_frame = ttk.LabelFrame(self.main_frame, text="File Selection") + file_frame.pack(fill=tk.X, pady=5) + + self.file_path = tk.StringVar() + ttk.Label(file_frame, textvariable=self.file_path).pack(side=tk.LEFT, padx=5) + + ttk.Button(file_frame, text="Select File", command=self.select_file).pack(side=tk.RIGHT, padx=5) + + def open_flash_window(self): + FlashWindow(self.master, self.master) + + def create_hex_view_section(self): + """Create hex view section with scrollable text widget""" + # Hex view frame + hex_frame = ttk.LabelFrame(self.main_frame, text="Hex View") + hex_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) + + # Scrolled text widget for hex view + self.hex_text = tk.Text(hex_frame, wrap=tk.NONE, font=('Courier', 10)) + self.hex_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Add vertical scrollbar + v_scrollbar = ttk.Scrollbar(hex_frame, orient=tk.VERTICAL, command=self.hex_text.yview) + v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.hex_text.configure(yscrollcommand=v_scrollbar.set) + + # Add horizontal scrollbar + h_scrollbar = ttk.Scrollbar(hex_frame, orient=tk.HORIZONTAL, command=self.hex_text.xview) + h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) + self.hex_text.configure(xscrollcommand=h_scrollbar.set) + + # Bind key events for custom processing + self.hex_text.bind('', self.on_hex_text_key) + + def on_hex_text_key(self, event): + """Custom key event handler for hex text editing""" + # Allow navigation and editing keys + if event.keysym in ['Left', 'Right', 'Up', 'Down', 'Home', 'End', 'Prior', 'Next']: + return + + # Allow backspace and delete + if event.keysym in ['BackSpace', 'Delete']: + self.master.after(10, self.process_hex_text_edit) + return + + # Only process for printable hex characters + if event.char and event.char not in '0123456789ABCDEFabcdef': + return 'break' + + # Delay processing to allow default key handling + self.master.after(10, self.process_hex_text_edit) + + def process_hex_text_edit(self): + """Process hex text edits to maintain formatting""" + try: + # Get entire text content + full_text = self.hex_text.get('1.0', tk.END) + + # Get current cursor position + cursor_index = self.hex_text.index(tk.INSERT) + + # Log current state for debugging + self.logger.debug(f"Current cursor index: {cursor_index}") + self.logger.debug(f"Full text length: {len(full_text)}") + + # Split text into lines + lines = full_text.split('\n') + + # Process each line to extract hex values + clean_hex_values = [] + for line in lines: + # Extract hex values from each line (between ':' and ASCII representation) + if ':' in line: + hex_part = line.split(':', 1)[1].split(' ', 1)[0] + # Remove spaces and collect hex values + line_hex = hex_part.replace(' ', '') + if line_hex: + clean_hex_values.append(line_hex) + + # Join all hex values + clean_text = ''.join(clean_hex_values) + + # Reformat the text + formatted_text = self.reformat_hex_text(clean_text) + + # Update text widget + self.hex_text.delete('1.0', tk.END) + self.hex_text.insert('1.0', formatted_text) + + # Restore cursor position + total_lines = len(formatted_text.split('\n')) + cursor_line, cursor_column = map(int, cursor_index.split('.')) + + # Ensure cursor line is within new text + if cursor_line > total_lines: + cursor_line = total_lines + + # Get the last line's length + last_line = formatted_text.split('\n')[-1] + last_line_hex = last_line.split(':', 1)[1].split(' ', 1)[0].replace(' ', '') + + # Adjust column if it exceeds line length + if cursor_column > len(last_line_hex) + 1: + cursor_column = len(last_line_hex) + 1 + + # Set cursor position + new_cursor_index = f'{cursor_line}.{cursor_column}' + self.logger.debug(f"Restored cursor index: {new_cursor_index}") + + self.hex_text.mark_set(tk.INSERT, new_cursor_index) + self.hex_text.see(new_cursor_index) + + except Exception as e: + self.logger.error(f"Error processing hex text edit: {str(e)}") + + def reformat_hex_text(self, hex_string): + """Reformat hex string to maintain original view""" + # Ensure hex string length is even + if len(hex_string) % 2 != 0: + hex_string = hex_string + '0' + + # Convert to bytes for consistent formatting + try: + data = bytes.fromhex(hex_string) + return self.format_hex_view(data) + except ValueError: + # Fallback if conversion fails + return hex_string + + def create_order_analysis_section(self): + order_frame = ttk.LabelFrame(self.main_frame, text="Order Analysis") + order_frame.pack(fill=tk.X, pady=5) + + # Normal Order + normal_frame = ttk.LabelFrame(order_frame, text="Normal Order") + normal_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5) + + self.normal_hex = tk.StringVar() + self.normal_dec = tk.StringVar() + self.normal_ihex = tk.StringVar() + self.normal_idec = tk.StringVar() + + ttk.Label(normal_frame, text="HEX:").grid(row=0, column=0, sticky=tk.W) + ttk.Label(normal_frame, textvariable=self.normal_hex).grid(row=0, column=1, sticky=tk.W) + + ttk.Label(normal_frame, text="DEC:").grid(row=1, column=0, sticky=tk.W) + ttk.Label(normal_frame, textvariable=self.normal_dec).grid(row=1, column=1, sticky=tk.W) + + ttk.Label(normal_frame, text="!HEX:").grid(row=0, column=2, sticky=tk.W) + ttk.Label(normal_frame, textvariable=self.normal_ihex).grid(row=0, column=3, sticky=tk.W) + + ttk.Label(normal_frame, text="!DEC:").grid(row=1, column=2, sticky=tk.W) + ttk.Label(normal_frame, textvariable=self.normal_idec).grid(row=1, column=3, sticky=tk.W) + + # Reverse Order + reverse_frame = ttk.LabelFrame(order_frame, text="Reverse Order") + reverse_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5) + + self.reverse_hex = tk.StringVar() + self.reverse_dec = tk.StringVar() + self.reverse_ihex = tk.StringVar() + self.reverse_idec = tk.StringVar() + + ttk.Label(reverse_frame, text="HEX:").grid(row=0, column=0, sticky=tk.W) + ttk.Label(reverse_frame, textvariable=self.reverse_hex).grid(row=0, column=1, sticky=tk.W) + + ttk.Label(reverse_frame, text="DEC:").grid(row=1, column=0, sticky=tk.W) + ttk.Label(reverse_frame, textvariable=self.reverse_dec).grid(row=1, column=1, sticky=tk.W) + + ttk.Label(reverse_frame, text="!HEX:").grid(row=0, column=2, sticky=tk.W) + ttk.Label(reverse_frame, textvariable=self.reverse_ihex).grid(row=0, column=3, sticky=tk.W) + + ttk.Label(reverse_frame, text="!DEC:").grid(row=1, column=2, sticky=tk.W) + ttk.Label(reverse_frame, textvariable=self.reverse_idec).grid(row=1, column=3, sticky=tk.W) + + def create_inspector_section(self): + inspector_frame = ttk.LabelFrame(self.main_frame, text="Inspector") + inspector_frame.pack(fill=tk.X, pady=5) + + # Create labels for various representations + self.checksum_sum = tk.StringVar() + self.checksum_xor = tk.StringVar() + self.checksum_16 = tk.StringVar() + + ttk.Label(inspector_frame, text="Checksum Sum:").grid(row=0, column=0, sticky=tk.W) + ttk.Label(inspector_frame, textvariable=self.checksum_sum).grid(row=0, column=1, sticky=tk.W) + + ttk.Label(inspector_frame, text="Checksum XOR:").grid(row=1, column=0, sticky=tk.W) + ttk.Label(inspector_frame, textvariable=self.checksum_xor).grid(row=1, column=1, sticky=tk.W) + + ttk.Label(inspector_frame, text="Checksum 16-bit:").grid(row=2, column=0, sticky=tk.W) + ttk.Label(inspector_frame, textvariable=self.checksum_16).grid(row=2, column=1, sticky=tk.W) + + def create_search_section(self): + search_frame = ttk.LabelFrame(self.main_frame, text="Search") + search_frame.pack(fill=tk.X, pady=5) + + ttk.Label(search_frame, text="Search Value:").grid(row=0, column=0) + self.search_entry = ttk.Entry(search_frame, width=30) + self.search_entry.grid(row=0, column=1, padx=5) + + ttk.Button(search_frame, text="Search", command=self.perform_search).grid(row=0, column=2) + ttk.Button(search_frame, text="Next Result", command=self.next_result).grid(row=0, column=3) + ttk.Button(search_frame, text="Clear", command=self.clear_search).grid(row=0, column=4) + + def select_file(self): + file_path = filedialog.askopenfilename() + if file_path: + self.file_path.set(file_path) + self.inspect_file(file_path) + + def inspect_file(self, file_path): + """Inspect and display contents of a binary file""" + try: + # Read file in binary mode + with open(file_path, 'rb') as f: + data = f.read() + + # Clear previous content + self.hex_text.config(state=tk.NORMAL) + self.hex_text.delete('1.0', tk.END) + + # Format and display hex view + hex_view = self.format_hex_view(data) + self.hex_text.insert(tk.END, hex_view) + + # Make text editable and highlight in light red + self.hex_text.tag_config('editable', background='#FFE6E6') + self.hex_text.tag_add('editable', '1.0', tk.END) + + # Configure text widget for editing + self.hex_text.config(state=tk.NORMAL) + + # Calculate and update checksums + checksums = self.calculate_checksums(data) + self.checksum_sum.set(checksums['sum']) + self.checksum_xor.set(checksums['xor']) + self.checksum_16.set(checksums['sum16']) + + # Store original file path for potential saving + self.current_file_path = file_path + + # Update window title + self.master.title(f"HexaWork - {os.path.basename(file_path)}") + + except Exception as e: + self.logger.error(f"Error inspecting file: {e}") + messagebox.showerror("Error", f"Could not inspect file: {e}") + + def save_edited_file(self): + """Save the edited binary file""" + if not hasattr(self, 'current_file_path') or not self.current_file_path: + messagebox.showerror("Error", "No file is currently open") + return + + try: + # Get the original file size + original_size = os.path.getsize(self.current_file_path) + + # Get the edited text and convert to bytes + edited_text = self.hex_text.get('1.0', tk.END).strip() + + # Remove spaces and convert to bytes + hex_values = edited_text.replace('\n', '').replace(' ', '') + + # Validate hex string + if not all(c in '0123456789ABCDEFabcdef' for c in hex_values): + messagebox.showerror("Error", "Invalid hex characters") + return + + # Convert to bytes + edited_bytes = bytes.fromhex(hex_values) + + # Check file size change + if len(edited_bytes) != original_size: + # Ask user about file size change + response = messagebox.askyesno( + "File Size Change", + f"Warning: File size will change!\n\n" + f"Original Size: {original_size} bytes\n" + f"New Size: {len(edited_bytes)} bytes\n\n" + "Do you want to continue saving?" + ) + + if not response: + return + + # Save the file + with open(self.current_file_path, 'wb') as f: + f.write(edited_bytes) + + messagebox.showinfo("Success", "File saved successfully") + + # Update the current file view after saving + self.inspect_file(self.current_file_path) + + except Exception as e: + self.logger.error(f"Error saving file: {e}") + messagebox.showerror("Error", f"Could not save file: {e}") + + def format_hex_view(self, data, bytes_per_row=16): + """Format binary data into hex view with offset, hex values, and ASCII representation""" + hex_view = [] + + # Determine the maximum length of the offset for consistent formatting + max_offset = len(f'{len(data):X}') + + for i in range(0, len(data), bytes_per_row): + # Get the current chunk of bytes + chunk = data[i:i+bytes_per_row] + + # Format offset (right-aligned with leading zeros) + offset = f'{i:0{max_offset}X}' + + # Format hex values (2 digits per byte, space-separated) + hex_values = ' '.join(f'{byte:02X}' for byte in chunk) + + # Pad hex values to ensure consistent width + hex_values = hex_values.ljust(bytes_per_row * 3 - 1) + + # Format ASCII representation + ascii_repr = ''.join(chr(byte) if 32 <= byte <= 126 else '.' for byte in chunk) + + # Combine all parts + line = f'{offset}: {hex_values} {ascii_repr}' + hex_view.append(line) + + return '\n'.join(hex_view) + + def calculate_checksums(self, data): + """Calculate various checksums for the given data""" + # Ensure data is converted to bytes if it's not already + if not isinstance(data, bytes): + data = bytes(data) + + # Sum checksum (1 byte) + sum_checksum = sum(data) & 0xFF + + # XOR checksum (1 byte) + xor_checksum = 0 + for byte in data: + xor_checksum ^= byte + xor_checksum &= 0xFF + + # 16-bit checksum (2 bytes) + bit16_checksum = sum(data) & 0xFFFF + + return { + 'sum': f'{sum_checksum:02X}', + 'xor': f'{xor_checksum:02X}', + 'sum16': f'{bit16_checksum:04X}' + } + + def perform_search(self): + search_value = self.search_entry.get() + if not search_value: + return + + # Placeholder search implementation + hex_content = self.hex_text.get('1.0', tk.END) + results = [m.start() for m in re.finditer(re.escape(search_value), hex_content)] + + if results: + self.current_search_results = results + self.current_search_index = 0 + self.highlight_current_result() + else: + messagebox.showinfo("Search", "No results found.") + + def next_result(self): + if not hasattr(self, 'current_search_results') or not self.current_search_results: + return + + self.current_search_index = (self.current_search_index + 1) % len(self.current_search_results) + self.highlight_current_result() + + def highlight_current_result(self): + if not hasattr(self, 'current_search_results'): + return + + result_pos = self.current_search_results[self.current_search_index] + self.hex_text.tag_remove('search_highlight', '1.0', tk.END) + start_index = self.hex_text.index(f'1.0 + {result_pos}c') + end_index = self.hex_text.index(f'{start_index} + {len(self.search_entry.get())}c') + self.hex_text.tag_add('search_highlight', start_index, end_index) + self.hex_text.tag_config('search_highlight', background='yellow') + self.hex_text.see(start_index) + + def clear_search(self): + self.search_entry.delete(0, tk.END) + self.hex_text.tag_remove('search_highlight', '1.0', tk.END) + if hasattr(self, 'current_search_results'): + del self.current_search_results + self.current_search_index = -1 + + def on_text_select(self, event=None): + """Handle text selection in hex view""" + try: + # Get selected text + if not self.hex_text.tag_ranges(tk.SEL): + return + + start = self.hex_text.index(tk.SEL_FIRST) + end = self.hex_text.index(tk.SEL_LAST) + selected_text = self.hex_text.get(start, end) + + # Extract hex values + hex_values = [] + for part in selected_text.split(): + if len(part) == 2 and all(c in '0123456789ABCDEFabcdef' for c in part): + hex_values.append(part.upper()) + + if hex_values: + # Convert to bytes + hex_str = ''.join(hex_values) + self.selected_bytes = bytes.fromhex(hex_str) + self.logger.info(f"Converted to bytes: {self.selected_bytes.hex().upper()}") + + # Update order analysis + self.update_order_analysis() + + # Update checksums + checksums = self.calculate_checksums(self.selected_bytes) + self.checksum_sum.set(checksums['sum']) + self.checksum_xor.set(checksums['xor']) + self.checksum_16.set(checksums['sum16']) + else: + self.logger.warning("No valid hex values found in selection") + self.clear_order_analysis() + + # Clear checksum values + self.checksum_sum.set("") + self.checksum_xor.set("") + self.checksum_16.set("") + + except Exception as e: + self.logger.error(f"Error processing selection: {str(e)}\nTraceback: {traceback.format_exc()}") + self.clear_order_analysis() + + # Clear checksum values + self.checksum_sum.set("") + self.checksum_xor.set("") + self.checksum_16.set("") + + def clear_order_analysis(self): + """Clear all order analysis values""" + for var in [self.normal_hex, self.normal_dec, self.normal_ihex, self.normal_idec, + self.reverse_hex, self.reverse_dec, self.reverse_ihex, self.reverse_idec]: + var.set("") + + def update_order_analysis(self): + if not self.selected_bytes: + return + + try: + # Normal Order Analysis + normal_hex_str = ''.join(f'{b:02X}' for b in self.selected_bytes) + self.normal_hex.set(normal_hex_str) + + # Convert to integer (normal order) + normal_int = int(normal_hex_str, 16) + self.normal_dec.set(str(normal_int)) + + # Calculate !HEX (bitwise NOT) + num_bits = len(self.selected_bytes) * 8 + normal_complement = ~normal_int & ((1 << num_bits) - 1) + self.normal_ihex.set(f'{normal_complement:X}') + + # Calculate !DEC (decimal complement) + self.normal_idec.set(str(normal_complement)) + + # Reverse Order Analysis + reverse_hex_str = ''.join(f'{b:02X}' for b in reversed(self.selected_bytes)) + self.reverse_hex.set(reverse_hex_str) + + # Convert to integer (reverse order) + reverse_int = int(reverse_hex_str, 16) + self.reverse_dec.set(str(reverse_int)) + + # Calculate !HEX for reverse + reverse_complement = ~reverse_int & ((1 << num_bits) - 1) + self.reverse_ihex.set(f'{reverse_complement:X}') + + # Calculate !DEC for reverse + self.reverse_idec.set(str(reverse_complement)) + + except Exception as e: + self.logger.error(f"Error updating order analysis: {e}") + self.clear_order_analysis() + + def analyze_ascii_patterns(self): + """Analyze ASCII patterns in the current file""" + if not hasattr(self, 'current_file_path') or not self.current_file_path: + messagebox.showerror("Error", "No file is currently open") + return + + try: + # Read file in binary mode + with open(self.current_file_path, 'rb') as f: + data = f.read() + + # Convert to ASCII string, replacing non-printable characters + ascii_text = ''.join(chr(byte) if 32 <= byte <= 126 else ' ' for byte in data) + + # Patterns to search + patterns = { + 'Possible Software Number': r'\b\d{10,11}\b', + 'VIN (Vehicle Identification Number)': r'\b[A-Z0-9]{17}\b', + } + + # Collect results + results = {} + for pattern_name, pattern in patterns.items(): + matches = re.findall(pattern, ascii_text) + if matches: + # Count unique occurrences + unique_matches = {} + for match in matches: + unique_matches[match] = unique_matches.get(match, 0) + 1 + results[pattern_name] = unique_matches + + # Additional custom pattern searches with minimum 3 character requirement + custom_patterns = [ + ('Potential Codes', r'\b[A-Z]{3,}\b'), # 3 or more letter codes + ('Long Numbers', r'\b\d{10,}\b'), + ('Possible Pin Code', r'\b[A-Z0-9]{4}\b') # 4-character alphanumeric codes + ] + + for pattern_name, pattern in custom_patterns: + matches = re.findall(pattern, ascii_text) + if matches: + # Count unique occurrences + unique_matches = {} + for match in matches: + unique_matches[match] = unique_matches.get(match, 0) + 1 + results[pattern_name] = unique_matches + + # Create results window + if results: + self.show_ascii_analysis_results(results) + else: + messagebox.showinfo("ASCII Analysis", "No interesting patterns found.") + + except Exception as e: + self.logger.error(f"Error in ASCII analysis: {e}") + messagebox.showerror("Error", f"Could not perform ASCII analysis: {e}") + + def show_ascii_analysis_results(self, results): + """Display ASCII analysis results in a popup window""" + # Create top-level window + result_window = tk.Toplevel(self.master) + result_window.title("ASCII Pattern Analysis") + result_window.geometry("600x400") + + # Create text widget to display results + results_text = tk.Text(result_window, wrap=tk.WORD, font=('Courier', 10)) + results_text.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) + + # Add scrollbar + scrollbar = ttk.Scrollbar(result_window, command=results_text.yview) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + results_text.configure(yscrollcommand=scrollbar.set) + + # Format and insert results + results_text.insert(tk.END, "ASCII Pattern Analysis Results:\n\n") + for category, matches in results.items(): + results_text.insert(tk.END, f"{category}:\n") + for match, count in matches.items(): + results_text.insert(tk.END, f" - {match} (Occurrences: {count})\n") + results_text.insert(tk.END, "\n") + + # Add a close button + close_button = ttk.Button(result_window, text="Close", command=result_window.destroy) + close_button.pack(pady=10) + +def main(): + root = tk.Tk() + app = HexaWorkApp(root) + root.mainloop() + +if __name__ == "__main__": + main()