Last active
April 10, 2025 08:44
-
-
Save dmknght/c8a1fa1fad1255eeb01042d93793e684 to your computer and use it in GitHub Desktop.
Likely signature management of Kaspersky 2008
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tkinter as tk | |
from tkinter import ttk, messagebox, filedialog | |
import struct | |
import os | |
import hashlib | |
from datetime import datetime | |
from dataclasses import dataclass | |
from typing import List, Optional, Tuple | |
import re | |
# Record Types | |
RT_KERNEL = 0 | |
RT_JUMP = 1 | |
RT_MEMORY = 2 | |
RT_SECTOR = 3 | |
RT_FILE = 4 | |
RT_CA = 5 | |
RT_UNPACK = 6 | |
RT_EXTRACT = 7 | |
RT_SEPARATOR = 8 | |
# Record Type Strings | |
TYPE_STRINGS = [ | |
"Kernel", | |
"Jump", | |
"Mem", | |
"Sector", | |
"File", | |
"CA", | |
"Unpack", | |
"Extract", | |
"" | |
] | |
# SubType Strings for File/CA/Unpack/Extract | |
SUBTYPE_STRINGS = [ | |
"Com ", | |
"Exe ", | |
"Sys ", | |
"NE ", | |
"OLE2 ", | |
"", | |
"", | |
"" | |
] | |
# SubType Strings for Sector | |
SUBTYPE_SECTOR = [ | |
"A", # ABOOT | |
"F", # FDBOOT | |
"H", # HDBOOT | |
"M", # MBR | |
"" | |
] | |
# Method Strings | |
METHOD_STRINGS = { | |
RT_FILE: { | |
0: "Signature", | |
1: "Heuristic", | |
2: "Behavior", | |
3: "Cloud", | |
4: "Other" | |
}, | |
RT_SECTOR: { | |
0: "Boot", | |
1: "MBR", | |
2: "Partition", | |
3: "Other" | |
}, | |
RT_JUMP: { | |
0: "Jump", | |
1: "Call", | |
2: "Other" | |
}, | |
RT_MEMORY: { | |
0: "Memory", | |
1: "Stack", | |
2: "Other" | |
} | |
} | |
class RecordEdit: | |
def __init__(self, type=0): | |
self.type = type | |
self.subtype = 0 | |
self.method = 0 | |
self.name = "" | |
self.comment = "" | |
self.link16_buffer = None | |
self.link32_buffer = None | |
self.extra_info = None | |
self.record = b"" | |
self.modify_flag = 0 | |
def get_type_string(self): | |
"""Get record type as string""" | |
if 0 <= self.type < len(TYPE_STRINGS): | |
return TYPE_STRINGS[self.type] | |
return f"Unknown ({self.type})" | |
def get_sub_type_string(self): | |
"""Get record subtype as string""" | |
if self.type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]: | |
sub_types = [] | |
for i in range(8): | |
if self.subtype & (1 << i): | |
sub_types.append(SUBTYPE_STRINGS[i]) | |
return "".join(sub_types) | |
elif self.type == RT_SECTOR: | |
sub_types = [] | |
if self.record: | |
sector = struct.unpack("<I", self.record[:4])[0] | |
if sector & 0x01: # ABOOT | |
sub_types.append(SUBTYPE_SECTOR[0]) | |
if sector & 0x02: # FDBOOT | |
sub_types.append(SUBTYPE_SECTOR[1]) | |
if sector & 0x04: # HDBOOT | |
sub_types.append(SUBTYPE_SECTOR[2]) | |
if sector & 0x08: # MBR | |
sub_types.append(SUBTYPE_SECTOR[3]) | |
return "".join(sub_types) | |
return "" | |
def get_method_string(self): | |
"""Get record method as string""" | |
if self.type not in METHOD_STRINGS: | |
return "" | |
if not self.record: | |
return "" | |
try: | |
if self.type == RT_FILE: | |
method = struct.unpack("<B", self.record[:1])[0] & 0x7F | |
elif self.type == RT_SECTOR: | |
method1 = struct.unpack("<B", self.record[:1])[0] & 0x0F | |
method2 = struct.unpack("<B", self.record[1:2])[0] & 0x0F | |
return f"{METHOD_STRINGS[RT_SECTOR].get(method1, 'Unknown')} / {METHOD_STRINGS[RT_SECTOR].get(method2, 'Unknown')}" | |
elif self.type == RT_JUMP: | |
method = struct.unpack("<B", self.record[:1])[0] | |
elif self.type == RT_MEMORY: | |
method = struct.unpack("<B", self.record[:1])[0] | |
else: | |
return "" | |
return METHOD_STRINGS[self.type].get(method, "Unknown") | |
except: | |
return "" | |
def get_link16_string(self): | |
"""Get Link16 buffer as string""" | |
if not self.link16_buffer: | |
return "" | |
return " ".join(f"{x:02X}" for x in self.link16_buffer) | |
def get_link32_string(self): | |
"""Get Link32 buffer as string""" | |
if not self.link32_buffer: | |
return "" | |
return " ".join(f"{x:02X}" for x in self.link32_buffer) | |
def get_name(self): | |
"""Get record name""" | |
return self.name | |
def get_comment(self): | |
"""Get record comment""" | |
return self.comment | |
def set_name(self, name): | |
"""Set record name""" | |
self.name = name | |
return True | |
def set_comment(self, comment): | |
"""Set record comment""" | |
self.comment = comment | |
return True | |
def set_type(self, type): | |
"""Set record type""" | |
if type in TYPE_STRINGS: | |
self.type = type | |
return True | |
return False | |
def set_subtype(self, subtype): | |
"""Set record subtype""" | |
if self.type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]: | |
self.subtype = subtype | |
return True | |
return False | |
def set_method(self, method): | |
"""Set record method""" | |
if self.type not in METHOD_STRINGS: | |
return False | |
if not self.record: | |
self.record = b"\x00" | |
try: | |
if self.type == RT_FILE: | |
self.record = struct.pack("<B", method & 0x7F) + self.record[1:] | |
elif self.type == RT_SECTOR: | |
self.record = struct.pack("<B", method & 0x0F) + self.record[1:] | |
elif self.type in [RT_JUMP, RT_MEMORY]: | |
self.record = struct.pack("<B", method) + self.record[1:] | |
return True | |
except: | |
return False | |
def set_link16(self, buffer): | |
"""Set Link16 buffer""" | |
if buffer is None or isinstance(buffer, bytes): | |
self.link16_buffer = buffer | |
return True | |
return False | |
def set_link32(self, buffer): | |
"""Set Link32 buffer""" | |
if buffer is None or isinstance(buffer, bytes): | |
self.link32_buffer = buffer | |
return True | |
return False | |
def pack_record(self): | |
"""Pack record data into bytes""" | |
data = bytearray() | |
# Pack type and subtype | |
data.extend(struct.pack("<BB", self.type, self.subtype)) | |
# Pack name | |
name_bytes = self.name.encode('utf-8') | |
data.extend(struct.pack("<H", len(name_bytes))) | |
data.extend(name_bytes) | |
# Pack comment | |
comment_bytes = self.comment.encode('utf-8') | |
data.extend(struct.pack("<H", len(comment_bytes))) | |
data.extend(comment_bytes) | |
# Pack record data | |
if self.record: | |
data.extend(struct.pack("<H", len(self.record))) | |
data.extend(self.record) | |
else: | |
data.extend(struct.pack("<H", 0)) | |
# Pack link buffers | |
if self.link16_buffer: | |
data.extend(struct.pack("<H", len(self.link16_buffer))) | |
data.extend(self.link16_buffer) | |
else: | |
data.extend(struct.pack("<H", 0)) | |
if self.link32_buffer: | |
data.extend(struct.pack("<H", len(self.link32_buffer))) | |
data.extend(self.link32_buffer) | |
else: | |
data.extend(struct.pack("<H", 0)) | |
return bytes(data) | |
def unpack_record(self, data): | |
"""Unpack record data from bytes""" | |
offset = 0 | |
# Unpack type and subtype | |
self.type, self.subtype = struct.unpack("<BB", data[offset:offset+2]) | |
offset += 2 | |
# Unpack name | |
name_len = struct.unpack("<H", data[offset:offset+2])[0] | |
offset += 2 | |
self.name = data[offset:offset+name_len].decode('utf-8') | |
offset += name_len | |
# Unpack comment | |
comment_len = struct.unpack("<H", data[offset:offset+2])[0] | |
offset += 2 | |
self.comment = data[offset:offset+comment_len].decode('utf-8') | |
offset += comment_len | |
# Unpack record data | |
record_len = struct.unpack("<H", data[offset:offset+2])[0] | |
offset += 2 | |
if record_len > 0: | |
self.record = data[offset:offset+record_len] | |
offset += record_len | |
else: | |
self.record = b"" | |
# Unpack link buffers | |
link16_len = struct.unpack("<H", data[offset:offset+2])[0] | |
offset += 2 | |
if link16_len > 0: | |
self.link16_buffer = data[offset:offset+link16_len] | |
offset += link16_len | |
else: | |
self.link16_buffer = None | |
link32_len = struct.unpack("<H", data[offset:offset+2])[0] | |
offset += 2 | |
if link32_len > 0: | |
self.link32_buffer = data[offset:offset+link32_len] | |
offset += link32_len | |
else: | |
self.link32_buffer = None | |
return True | |
def add_link(self, filename: str) -> bool: | |
"""Add link from OBJ/COFF file""" | |
try: | |
with open(filename, 'rb') as f: | |
data = f.read() | |
# Check for special functions | |
has_cure = b"_cure" in data or b"_Cure" in data | |
has_decode = b"_decode" in data | |
has_jmp = b"_jmp" in data | |
has_link = b"_Link" in data | |
# Set bits based on found functions | |
b = 0 | |
if has_cure: | |
b |= 2 | |
if has_decode or has_jmp or has_link or self.type == RT_KERNEL: | |
b |= 1 | |
if not b: | |
return False | |
# Create new buffer for links | |
buffer = bytearray() | |
# Set bits based on found functions | |
if has_cure: | |
buffer.append(0x02) | |
if has_decode or has_jmp or has_link or self.type == RT_KERNEL: | |
buffer.append(0x01) | |
# Set Link32 buffer | |
if self.link32_buffer: | |
# In original code, it would ask for confirmation here | |
# For simplicity, we'll just replace it | |
self.link32_buffer = None | |
self.link32_buffer = bytes(buffer) | |
self.modify_flag = 1 | |
return True | |
except: | |
return False | |
def unlink(self) -> bool: | |
"""Remove link buffers""" | |
try: | |
# Check if buffers exist | |
if not self.link16_buffer and not self.link32_buffer: | |
return False | |
# In original code, it would ask for confirmation here | |
# For simplicity, we'll just remove them | |
# Delete buffers | |
self.link16_buffer = None | |
self.link32_buffer = None | |
# Set comment if ExtraInfo exists | |
if self.extra_info: | |
self.extra_info.set_comment(self.comment) | |
self.modify_flag = 1 | |
return True | |
except: | |
return False | |
class AvpEditDoc: | |
def __init__(self): | |
self.filename = None | |
self.modified = False | |
self.compression = False | |
self.records = [] | |
self.clipboard = [] | |
# Column settings - match original code | |
self.column_names = ["Name", "*", "Type", "SubType", "Method", | |
"Link16", "Link32", "Comment"] | |
self.column_widths = [100, 20, 50, 80, 50, 100, 100, 200] | |
self.header = AvpBaseHeader() | |
self.compression_level = 9 | |
def new_document(self): | |
self.records.clear() | |
self.header = AvpBaseHeader() | |
self.filename = "" | |
self.modified = False | |
def open_document(self, filename: str) -> bool: | |
try: | |
with open(filename, 'rb') as f: | |
data = f.read() | |
if not self.header.unpack(data): | |
return False | |
offset = len(self.header.pack()) | |
# Read compression flag | |
self.compression = bool(data[offset]) | |
offset += 1 | |
self.records.clear() | |
for _ in range(self.header.records_count): | |
record = RecordEdit(0) | |
record_size = struct.unpack("<I", data[offset:offset+4])[0] | |
offset += 4 | |
record_data = data[offset:offset+record_size] | |
offset += record_size | |
if self.compression: | |
# Decompress record data | |
import zlib | |
record_data = zlib.decompress(record_data) | |
if not record.unpack_record(record_data): | |
return False | |
self.records.append(record) | |
self.filename = filename | |
self.modified = False | |
return True | |
except: | |
return False | |
def save_document(self, filename: str = None) -> bool: | |
if filename: | |
self.filename = filename | |
if not self.filename: | |
return False | |
try: | |
self.header.records_count = len(self.records) | |
self.header.modification_date = datetime.now() | |
data = bytearray(self.header.pack()) | |
# Add compression flag | |
data.extend(struct.pack("<B", 1 if self.compression else 0)) | |
for record in self.records: | |
record_data = record.pack_record() | |
if self.compression: | |
# Compress record data | |
import zlib | |
compressed = zlib.compress(record_data, self.compression_level) | |
data.extend(struct.pack("<I", len(compressed))) | |
data.extend(compressed) | |
else: | |
data.extend(struct.pack("<I", len(record_data))) | |
data.extend(record_data) | |
with open(self.filename, 'wb') as f: | |
f.write(data) | |
self.modified = False | |
return True | |
except: | |
return False | |
def add_record(self, record: RecordEdit) -> bool: | |
self.records.append(record) | |
self.modified = True | |
return True | |
def delete_record(self, index: int) -> bool: | |
if 0 <= index < len(self.records): | |
del self.records[index] | |
self.modified = True | |
return True | |
return False | |
def edit_record(self, index: int, record: RecordEdit) -> bool: | |
if 0 <= index < len(self.records): | |
self.records[index] = record | |
self.modified = True | |
return True | |
return False | |
def get_record(self, index: int) -> Optional[RecordEdit]: | |
if 0 <= index < len(self.records): | |
return self.records[index] | |
return None | |
def clipboard_copy(self, index: int) -> bool: | |
"""Copy record to clipboard""" | |
if 0 <= index < len(self.records): | |
self.clipboard.append(self.records[index]) | |
return True | |
return False | |
def clipboard_cut(self, index: int) -> bool: | |
"""Cut record to clipboard""" | |
if self.clipboard_copy(index): | |
return self.delete_record(index) | |
return False | |
def clipboard_paste(self, index: int) -> bool: | |
"""Paste record from clipboard""" | |
if not self.clipboard: | |
return False | |
for record in self.clipboard: | |
self.insert_record(index, record) | |
index += 1 | |
return True | |
def insert_record(self, index: int, record: RecordEdit) -> bool: | |
"""Insert record at specified index""" | |
if 0 <= index <= len(self.records): | |
self.records.insert(index, record) | |
self.modified = True | |
return True | |
return False | |
class AvpBaseHeader: | |
def __init__(self): | |
self.signature = b"AVP" | |
self.version = 1 | |
self.records_count = 0 | |
self.creation_date = datetime.now() | |
self.modification_date = datetime.now() | |
self.description = "" | |
self.checksum = 0 | |
def pack(self) -> bytes: | |
data = bytearray() | |
data.extend(self.signature) | |
data.extend(struct.pack("<I", self.version)) | |
data.extend(struct.pack("<I", self.records_count)) | |
data.extend(struct.pack("<Q", int(self.creation_date.timestamp()))) | |
data.extend(struct.pack("<Q", int(self.modification_date.timestamp()))) | |
data.extend(struct.pack("<I", len(self.description))) | |
data.extend(self.description.encode()) | |
data.extend(struct.pack("<I", self.checksum)) | |
return bytes(data) | |
def unpack(self, data: bytes) -> bool: | |
try: | |
offset = 0 | |
self.signature = data[offset:offset+3] | |
offset += 3 | |
self.version = struct.unpack("<I", data[offset:offset+4])[0] | |
offset += 4 | |
self.records_count = struct.unpack("<I", data[offset:offset+4])[0] | |
offset += 4 | |
creation_timestamp = struct.unpack("<Q", data[offset:offset+8])[0] | |
offset += 8 | |
self.creation_date = datetime.fromtimestamp(creation_timestamp) | |
modification_timestamp = struct.unpack("<Q", data[offset:offset+8])[0] | |
offset += 8 | |
self.modification_date = datetime.fromtimestamp(modification_timestamp) | |
desc_len = struct.unpack("<I", data[offset:offset+4])[0] | |
offset += 4 | |
self.description = data[offset:offset+desc_len].decode() | |
offset += desc_len | |
self.checksum = struct.unpack("<I", data[offset:offset+4])[0] | |
return True | |
except: | |
return False | |
class RecordEditDialog(tk.Toplevel): | |
def __init__(self, parent, record: RecordEdit): | |
super().__init__(parent) | |
self.record = record | |
self.result = None | |
self.title("Edit Record") | |
self.resizable(False, False) | |
# Create widgets | |
main_frame = ttk.Frame(self, padding="10") | |
main_frame.grid(row=0, column=0, sticky="nsew") | |
# Name | |
ttk.Label(main_frame, text="Name:").grid(row=0, column=0, sticky="w", pady=5) | |
self.name_entry = ttk.Entry(main_frame, width=40) | |
self.name_entry.grid(row=0, column=1, columnspan=2, sticky="ew", pady=5) | |
self.name_entry.insert(0, record.get_name()) | |
# Type | |
ttk.Label(main_frame, text="Type:").grid(row=1, column=0, sticky="w", pady=5) | |
self.type_combo = ttk.Combobox(main_frame, values=TYPE_STRINGS[:-1], state="readonly", width=37) | |
self.type_combo.grid(row=1, column=1, columnspan=2, sticky="ew", pady=5) | |
self.type_combo.set(TYPE_STRINGS[record.type]) | |
self.type_combo.bind("<<ComboboxSelected>>", self.on_type_changed) | |
# SubType | |
ttk.Label(main_frame, text="SubType:").grid(row=2, column=0, sticky="w", pady=5) | |
self.subtype_frame = ttk.Frame(main_frame) | |
self.subtype_frame.grid(row=2, column=1, columnspan=2, sticky="ew", pady=5) | |
self.subtype_vars = [] | |
self.update_subtype_widgets() | |
# Method | |
ttk.Label(main_frame, text="Method:").grid(row=3, column=0, sticky="w", pady=5) | |
self.method_combo = ttk.Combobox(main_frame, state="readonly", width=37) | |
self.method_combo.grid(row=3, column=1, columnspan=2, sticky="ew", pady=5) | |
self.update_method_widgets() | |
# Comment | |
ttk.Label(main_frame, text="Comment:").grid(row=4, column=0, sticky="w", pady=5) | |
self.comment_text = tk.Text(main_frame, width=40, height=5) | |
self.comment_text.grid(row=4, column=1, columnspan=2, sticky="ew", pady=5) | |
self.comment_text.insert("1.0", record.get_comment()) | |
# Add Link buttons | |
link_frame = ttk.Frame(main_frame) | |
link_frame.grid(row=5, column=0, columnspan=3, pady=5) | |
ttk.Button(link_frame, text="Link 16-bit", command=self.on_link16).pack(side="left", padx=5) | |
ttk.Button(link_frame, text="Link 32-bit", command=self.on_link32).pack(side="left", padx=5) | |
ttk.Button(link_frame, text="Unlink", command=self.on_unlink).pack(side="left", padx=5) | |
# Move buttons to row 6 | |
button_frame = ttk.Frame(main_frame) | |
button_frame.grid(row=6, column=0, columnspan=3, pady=10) | |
ttk.Button(button_frame, text="OK", command=self.on_ok).pack(side="left", padx=5) | |
ttk.Button(button_frame, text="Cancel", command=self.on_cancel).pack(side="left", padx=5) | |
# Configure grid weights | |
main_frame.columnconfigure(1, weight=1) | |
# Make dialog modal | |
self.transient(parent) | |
self.grab_set() | |
# Set minimum size and update geometry | |
self.update_idletasks() | |
min_width = main_frame.winfo_reqwidth() + 40 # Add padding | |
min_height = main_frame.winfo_reqheight() + 40 # Add padding | |
self.minsize(min_width, min_height) | |
# Center dialog | |
width = self.winfo_width() | |
height = self.winfo_height() | |
x = (self.winfo_screenwidth() // 2) - (width // 2) | |
y = (self.winfo_screenheight() // 2) - (height // 2) | |
self.geometry(f"{width}x{height}+{x}+{y}") | |
parent.wait_window(self) | |
def update_subtype_widgets(self): | |
# Clear existing widgets | |
for widget in self.subtype_frame.winfo_children(): | |
widget.destroy() | |
self.subtype_vars.clear() | |
record_type = TYPE_STRINGS.index(self.type_combo.get()) | |
if record_type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]: | |
for i, st in enumerate(SUBTYPE_STRINGS): | |
if st: | |
var = tk.BooleanVar(value=bool(self.record.subtype & (1 << i))) | |
self.subtype_vars.append(var) | |
ttk.Checkbutton(self.subtype_frame, text=st.strip(), variable=var).pack(side="left") | |
elif record_type == RT_SECTOR: | |
for i, st in enumerate(SUBTYPE_SECTOR): | |
if st: | |
var = tk.BooleanVar(value=bool(self.record.record and struct.unpack("<I", self.record.record[:4])[0] & (1 << i))) | |
self.subtype_vars.append(var) | |
ttk.Checkbutton(self.subtype_frame, text=st, variable=var).pack(side="left") | |
def update_method_widgets(self): | |
record_type = TYPE_STRINGS.index(self.type_combo.get()) | |
if record_type in METHOD_STRINGS: | |
self.method_combo["values"] = list(METHOD_STRINGS[record_type].values()) | |
if self.record.record: | |
try: | |
if record_type == RT_FILE: | |
method = struct.unpack("<B", self.record.record[:1])[0] & 0x7F | |
elif record_type == RT_SECTOR: | |
method = struct.unpack("<B", self.record.record[:1])[0] & 0x0F | |
elif record_type == RT_JUMP: | |
method = struct.unpack("<B", self.record.record[:1])[0] | |
elif record_type == RT_MEMORY: | |
method = struct.unpack("<B", self.record.record[:1])[0] | |
else: | |
method = 0 | |
self.method_combo.set(METHOD_STRINGS[record_type].get(method, "Unknown")) | |
except: | |
self.method_combo.set("Unknown") | |
else: | |
self.method_combo["values"] = [] | |
self.method_combo.set("") | |
def on_type_changed(self, event): | |
self.update_subtype_widgets() | |
self.update_method_widgets() | |
def on_ok(self): | |
# Validate and save changes | |
name = self.name_entry.get() | |
if not self.record.set_name(name): | |
messagebox.showerror("Error", "Invalid name format") | |
return | |
comment = self.comment_text.get("1.0", "end-1c") | |
if not self.record.set_comment(comment): | |
messagebox.showerror("Error", "Invalid comment format") | |
return | |
# Update type and subtype | |
self.record.type = TYPE_STRINGS.index(self.type_combo.get()) | |
if self.record.type in [RT_FILE, RT_CA, RT_UNPACK, RT_EXTRACT]: | |
self.record.subtype = 0 | |
for i, var in enumerate(self.subtype_vars): | |
if var.get(): | |
self.record.subtype |= (1 << i) | |
elif self.record.type == RT_SECTOR: | |
if not self.record.record: | |
self.record.record = struct.pack("<I", 0) | |
sector = list(struct.unpack("<I", self.record.record[:4])) | |
for i, var in enumerate(self.subtype_vars): | |
if var.get(): | |
sector[0] |= (1 << i) | |
else: | |
sector[0] &= ~(1 << i) | |
self.record.record = struct.pack("<I", *sector) | |
# Update method | |
if self.record.type in METHOD_STRINGS: | |
method = list(METHOD_STRINGS[self.record.type].keys())[list(METHOD_STRINGS[self.record.type].values()).index(self.method_combo.get())] | |
if not self.record.record: | |
self.record.record = struct.pack("<B", 0) | |
if self.record.type == RT_FILE: | |
self.record.record = struct.pack("<B", method & 0x7F) + self.record.record[1:] | |
elif self.record.type == RT_SECTOR: | |
self.record.record = struct.pack("<B", method & 0x0F) + self.record.record[1:] | |
elif self.record.type in [RT_JUMP, RT_MEMORY]: | |
self.record.record = struct.pack("<B", method) + self.record.record[1:] | |
# Set result and close dialog | |
self.result = self.record | |
self.destroy() | |
def on_cancel(self): | |
self.destroy() | |
def on_link16(self): | |
"""Handle 16-bit link""" | |
filename = filedialog.askopenfilename( | |
title="Select 16-bit OBJ file", | |
filetypes=[ | |
("16-bit OBJ files", "*.o16"), | |
("All files", "*.*") | |
] | |
) | |
if filename: | |
if self.record.add_link(filename): | |
messagebox.showinfo("Success", "16-bit link added successfully") | |
else: | |
messagebox.showerror("Error", "Failed to add 16-bit link") | |
def on_link32(self): | |
"""Handle 32-bit link""" | |
filename = filedialog.askopenfilename( | |
title="Select 32-bit OBJ file", | |
filetypes=[ | |
("32-bit OBJ files", "*.o32"), | |
("All files", "*.*") | |
] | |
) | |
if filename: | |
if self.record.add_link(filename): | |
messagebox.showinfo("Success", "32-bit link added successfully") | |
else: | |
messagebox.showerror("Error", "Failed to add 32-bit link") | |
def on_unlink(self): | |
"""Handle unlink""" | |
if self.record.unlink(): | |
messagebox.showinfo("Success", "Links removed successfully") | |
else: | |
messagebox.showerror("Error", "Failed to remove links") | |
class AboutDialog(tk.Toplevel): | |
def __init__(self, parent): | |
super().__init__(parent) | |
self.title("About AVP Edit") | |
# Make dialog modal | |
self.transient(parent) | |
self.grab_set() | |
self.create_widgets() | |
self.center_window() | |
def create_widgets(self): | |
main_frame = ttk.Frame(self, padding="20") | |
main_frame.grid(row=0, column=0, sticky="nsew") | |
# Title | |
title_label = ttk.Label(main_frame, text="AVP Edit", font=("Helvetica", 16, "bold")) | |
title_label.grid(row=0, column=0, pady=(0, 10)) | |
# Version | |
version_label = ttk.Label(main_frame, text="Version 1.0") | |
version_label.grid(row=1, column=0, pady=(0, 10)) | |
# Description | |
desc_text = "AVP Edit is a tool for managing virus signature databases.\n\n" | |
desc_text += "Features:\n" | |
desc_text += "- View and edit virus signatures\n" | |
desc_text += "- Add/remove records\n" | |
desc_text += "- Manage links\n" | |
desc_text += "- Import/export databases" | |
desc_label = ttk.Label(main_frame, text=desc_text, justify="left") | |
desc_label.grid(row=2, column=0, pady=(0, 20)) | |
# OK button | |
ttk.Button(main_frame, text="OK", command=self.destroy).grid(row=3, column=0) | |
def center_window(self): | |
self.update_idletasks() | |
width = self.winfo_width() | |
height = self.winfo_height() | |
x = (self.winfo_screenwidth() // 2) - (width // 2) | |
y = (self.winfo_screenheight() // 2) - (height // 2) | |
self.geometry(f"{width}x{height}+{x}+{y}") | |
class SettingsDialog(tk.Toplevel): | |
def __init__(self, parent): | |
super().__init__(parent) | |
self.title("Settings") | |
# Make dialog modal | |
self.transient(parent) | |
self.grab_set() | |
self.result = None | |
self.create_widgets() | |
self.center_window() | |
def create_widgets(self): | |
main_frame = ttk.Frame(self, padding="10") | |
main_frame.grid(row=0, column=0, sticky="nsew") | |
# Compression | |
self.compression_var = tk.BooleanVar(value=False) | |
ttk.Checkbutton(main_frame, text="Enable Compression", | |
variable=self.compression_var).grid(row=0, column=0, sticky="w", pady=5) | |
# Auto-save | |
self.autosave_var = tk.BooleanVar(value=True) | |
ttk.Checkbutton(main_frame, text="Auto-save on Exit", | |
variable=self.autosave_var).grid(row=1, column=0, sticky="w", pady=5) | |
# Default directory | |
ttk.Label(main_frame, text="Default Directory:").grid(row=2, column=0, sticky="w", pady=5) | |
self.dir_entry = ttk.Entry(main_frame, width=40) | |
self.dir_entry.grid(row=2, column=1, sticky="ew", pady=5) | |
ttk.Button(main_frame, text="Browse", | |
command=self.browse_directory).grid(row=2, column=2, padx=5, pady=5) | |
# Buttons | |
button_frame = ttk.Frame(main_frame) | |
button_frame.grid(row=3, column=0, columnspan=3, pady=10) | |
ttk.Button(button_frame, text="OK", command=self.on_ok).pack(side="left", padx=5) | |
ttk.Button(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) | |
# Configure grid weights | |
main_frame.columnconfigure(1, weight=1) | |
def center_window(self): | |
self.update_idletasks() | |
width = self.winfo_width() | |
height = self.winfo_height() | |
x = (self.winfo_screenwidth() // 2) - (width // 2) | |
y = (self.winfo_screenheight() // 2) - (height // 2) | |
self.geometry(f"{width}x{height}+{x}+{y}") | |
def browse_directory(self): | |
directory = filedialog.askdirectory( | |
title="Select Default Directory" | |
) | |
if directory: | |
self.dir_entry.delete(0, tk.END) | |
self.dir_entry.insert(0, directory) | |
def on_ok(self): | |
self.result = { | |
"compression": self.compression_var.get(), | |
"autosave": self.autosave_var.get(), | |
"default_dir": self.dir_entry.get() | |
} | |
self.destroy() | |
class SearchDialog(tk.Toplevel): | |
def __init__(self, parent): | |
super().__init__(parent) | |
self.title("Find") | |
self.resizable(False, False) | |
self.result = None | |
# Create widgets | |
main_frame = ttk.Frame(self, padding="10") | |
main_frame.grid(row=0, column=0, sticky="nsew") | |
# Search text | |
ttk.Label(main_frame, text="Find what:").grid(row=0, column=0, sticky="w", pady=5) | |
self.search_text = ttk.Entry(main_frame, width=40) | |
self.search_text.grid(row=0, column=1, columnspan=2, sticky="ew", pady=5) | |
# Search type | |
ttk.Label(main_frame, text="Look in:").grid(row=1, column=0, sticky="w", pady=5) | |
self.search_type = ttk.Combobox(main_frame, | |
values=["Name", "Comment", "Type", "SubType", "Method"], | |
state="readonly") | |
self.search_type.grid(row=1, column=1, columnspan=2, sticky="ew", pady=5) | |
self.search_type.set("Name") | |
# Options | |
options_frame = ttk.LabelFrame(main_frame, text="Options", padding="5") | |
options_frame.grid(row=2, column=0, columnspan=3, sticky="ew", pady=5) | |
self.case_sensitive = tk.BooleanVar() | |
ttk.Checkbutton(options_frame, text="Match case", | |
variable=self.case_sensitive).pack(side="left", padx=5) | |
self.direction = tk.StringVar(value="forward") | |
ttk.Radiobutton(options_frame, text="Forward", variable=self.direction, | |
value="forward").pack(side="left", padx=5) | |
ttk.Radiobutton(options_frame, text="Backward", variable=self.direction, | |
value="backward").pack(side="left", padx=5) | |
# Buttons | |
button_frame = ttk.Frame(main_frame) | |
button_frame.grid(row=3, column=0, columnspan=3, pady=10) | |
ttk.Button(button_frame, text="Find Next", command=self.on_ok).pack(side="left", padx=5) | |
ttk.Button(button_frame, text="Cancel", command=self.on_cancel).pack(side="left", padx=5) | |
# Center dialog | |
self.update_idletasks() | |
width = self.winfo_width() | |
height = self.winfo_height() | |
x = (self.winfo_screenwidth() // 2) - (width // 2) | |
y = (self.winfo_screenheight() // 2) - (height // 2) | |
self.geometry(f"{width}x{height}+{x}+{y}") | |
# Make dialog modal | |
self.transient(parent) | |
self.grab_set() | |
parent.wait_window(self) | |
def on_ok(self): | |
"""Handle OK button click""" | |
self.result = { | |
"text": self.search_text.get(), | |
"search_type": self.search_type.get().lower(), | |
"case_sensitive": self.case_sensitive.get(), | |
"direction": self.direction.get() | |
} | |
self.destroy() | |
def on_cancel(self): | |
"""Handle Cancel button click""" | |
self.destroy() | |
class AvpEditView(ttk.Frame): | |
def __init__(self, parent, doc: AvpEditDoc): | |
super().__init__(parent) | |
self.doc = doc | |
self.create_widgets() | |
def create_widgets(self): | |
# Create treeview | |
self.tree = ttk.Treeview(self, columns=self.doc.column_names, show='headings') | |
for i, name in enumerate(self.doc.column_names): | |
self.tree.heading(name, text=name) | |
self.tree.column(name, width=self.doc.column_widths[i]) | |
# Add scrollbars | |
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) | |
hsb = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview) | |
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) | |
# Grid layout | |
self.tree.grid(row=0, column=0, sticky='nsew') | |
vsb.grid(row=0, column=1, sticky='ns') | |
hsb.grid(row=1, column=0, sticky='ew') | |
# Configure grid weights | |
self.grid_rowconfigure(0, weight=1) | |
self.grid_columnconfigure(0, weight=1) | |
# Bind events | |
self.tree.bind('<Double-1>', self.on_double_click) | |
self.tree.bind('<Delete>', self.on_delete) | |
def refresh_view(self): | |
# Clear existing items | |
for item in self.tree.get_children(): | |
self.tree.delete(item) | |
# Add records | |
for record in self.doc.records: | |
values = [ | |
record.get_name(), # Name | |
"", # Mark (empty for now) | |
record.get_type_string(), # Type | |
record.get_sub_type_string(), # SubType | |
record.get_method_string(), # Method | |
record.get_link16_string(), # Link16 | |
record.get_link32_string(), # Link32 | |
record.get_comment() # Comment | |
] | |
self.tree.insert('', 'end', values=values) | |
def on_double_click(self, event): | |
item = self.tree.selection()[0] | |
index = self.tree.index(item) | |
record = self.doc.get_record(index) | |
if record: | |
dialog = RecordEditDialog(self, record) | |
if dialog.result: | |
self.doc.edit_record(index, dialog.result) | |
self.refresh_view() | |
def on_delete(self, event): | |
items = self.tree.selection() | |
for item in items: | |
index = self.tree.index(item) | |
self.doc.delete_record(index) | |
self.refresh_view() | |
class AvpEditApp(tk.Tk): | |
def __init__(self): | |
super().__init__() | |
self.title("AVP Edit") | |
self.geometry("800x600") | |
self.doc = AvpEditDoc() | |
self.create_widgets() | |
self.create_menu() | |
def create_widgets(self): | |
self.view = AvpEditView(self, self.doc) | |
self.view.pack(fill=tk.BOTH, expand=True) | |
def create_menu(self): | |
menubar = tk.Menu(self) | |
self.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="New", command=self.on_new) | |
file_menu.add_command(label="Open...", command=self.on_open) | |
file_menu.add_command(label="Save", command=self.on_save) | |
file_menu.add_command(label="Save As...", command=self.on_save_as) | |
file_menu.add_separator() | |
file_menu.add_command(label="Pack/Compress", command=self.on_pack_file) | |
file_menu.add_command(label="Save and Reload", command=self.on_save_reload) | |
file_menu.add_separator() | |
file_menu.add_command(label="Exit", command=self.quit) | |
# Edit menu | |
edit_menu = tk.Menu(menubar, tearoff=0) | |
menubar.add_cascade(label="Edit", menu=edit_menu) | |
edit_menu.add_command(label="Insert Record", command=self.on_insert_record) | |
edit_menu.add_command(label="Delete Record", command=self.on_delete_record) | |
edit_menu.add_command(label="Edit Record", command=self.on_edit_record) | |
edit_menu.add_separator() | |
edit_menu.add_command(label="Cut", command=self.on_cut) | |
edit_menu.add_command(label="Copy", command=self.on_copy) | |
edit_menu.add_command(label="Paste", command=self.on_paste) | |
edit_menu.add_separator() | |
edit_menu.add_command(label="Find...", command=self.on_find) | |
edit_menu.add_command(label="Find Again", command=self.on_find_again) | |
# View menu | |
view_menu = tk.Menu(menubar, tearoff=0) | |
menubar.add_cascade(label="View", menu=view_menu) | |
view_menu.add_command(label="Column Settings...", command=self.on_column_settings) | |
# Status bar | |
self.status = ttk.Label(self, text="Ready", relief=tk.SUNKEN) | |
self.status.pack(side=tk.BOTTOM, fill=tk.X) | |
def on_new(self): | |
if self.doc.modified: | |
if not messagebox.askyesno("Save Changes", "Do you want to save changes?"): | |
return | |
if not self.on_save(): | |
return | |
self.doc.new_document() | |
self.view.refresh_view() | |
def on_open(self): | |
if self.doc.modified: | |
if not messagebox.askyesno("Save Changes", "Do you want to save changes?"): | |
return | |
if not self.on_save(): | |
return | |
filename = filedialog.askopenfilename( | |
title="Open AVP Database", | |
filetypes=[("AVP Database", "*.avp"), ("All files", "*.*")] | |
) | |
if filename: | |
if self.doc.open_document(filename): | |
self.view.refresh_view() | |
else: | |
messagebox.showerror("Error", "Failed to open file") | |
def on_save(self): | |
if not self.doc.filename: | |
return self.on_save_as() | |
return self.doc.save_document() | |
def on_save_as(self): | |
filename = filedialog.asksaveasfilename( | |
title="Save AVP Database", | |
filetypes=[("AVP Database", "*.avp"), ("All files", "*.*")], | |
defaultextension=".avp" | |
) | |
if filename: | |
return self.doc.save_document(filename) | |
return False | |
def on_pack_file(self): | |
"""Pack/compress the current file""" | |
if not self.doc.filename: | |
messagebox.showerror("Error", "No file is open") | |
return | |
if not self.doc.modified and not messagebox.askyesno("Pack File", | |
"File is not modified. Pack anyway?"): | |
return | |
self.doc.compression = True | |
if self.on_save(): | |
messagebox.showinfo("Success", "File packed successfully") | |
else: | |
messagebox.showerror("Error", "Failed to pack file") | |
def on_save_reload(self): | |
"""Save the current file and reload it""" | |
if not self.doc.filename: | |
messagebox.showerror("Error", "No file is open") | |
return | |
if self.on_save(): | |
self.on_open(self.doc.filename) | |
messagebox.showinfo("Success", "File saved and reloaded") | |
else: | |
messagebox.showerror("Error", "Failed to save file") | |
def on_insert_record(self): | |
"""Insert a new record at the selected position""" | |
items = self.view.tree.selection() | |
if not items: | |
index = len(self.doc.records) | |
else: | |
index = self.view.tree.index(items[0]) | |
record = RecordEdit(0) # Initialize with type 0 | |
dialog = RecordEditDialog(self, record) | |
if dialog.result: | |
self.doc.insert_record(index, dialog.result) | |
self.view.refresh_view() | |
def on_delete_record(self): | |
"""Delete selected records""" | |
items = self.view.tree.selection() | |
if not items: | |
return | |
if messagebox.askyesno("Confirm Delete", | |
"Are you sure you want to delete selected records?"): | |
for item in reversed(items): | |
index = self.view.tree.index(item) | |
self.doc.delete_record(index) | |
self.view.refresh_view() | |
def on_edit_record(self): | |
"""Edit selected record""" | |
items = self.view.tree.selection() | |
if not items: | |
return | |
index = self.view.tree.index(items[0]) | |
record = self.doc.get_record(index) | |
if record: | |
dialog = RecordEditDialog(self, record) | |
if dialog.result: | |
self.doc.edit_record(index, dialog.result) | |
self.view.refresh_view() | |
def on_cut(self): | |
"""Cut selected records to clipboard""" | |
items = self.view.tree.selection() | |
if not items: | |
return | |
# Copy to clipboard | |
self.on_copy() | |
# Delete selected | |
self.on_delete_record() | |
def on_copy(self): | |
"""Copy selected records to clipboard""" | |
items = self.view.tree.selection() | |
if not items: | |
return | |
self.doc.clipboard.clear() | |
for item in items: | |
index = self.view.tree.index(item) | |
record = self.doc.get_record(index) | |
if record: | |
self.doc.clipboard.append(record) | |
def on_paste(self): | |
"""Paste records from clipboard""" | |
if not self.doc.clipboard: | |
return | |
items = self.view.tree.selection() | |
if not items: | |
index = len(self.doc.records) | |
else: | |
index = self.view.tree.index(items[0]) | |
for record in self.doc.clipboard: | |
self.doc.insert_record(index, record) | |
index += 1 | |
self.view.refresh_view() | |
def on_find(self): | |
"""Find records matching search criteria""" | |
dialog = SearchDialog(self) | |
if dialog.result: | |
self.last_search = dialog.result | |
self.find_next_match() | |
def on_find_again(self): | |
"""Find next match using last search criteria""" | |
if not hasattr(self, "last_search"): | |
self.on_find() | |
return | |
self.find_next_match() | |
def find_next_match(self): | |
"""Find next record matching search criteria""" | |
if not self.last_search: | |
return | |
items = self.view.tree.get_children() | |
if not items: | |
return | |
# Get start index | |
current = self.view.tree.selection() | |
if not current: | |
start_index = 0 | |
else: | |
start_index = self.view.tree.index(current[0]) | |
if self.last_search["direction"] == "forward": | |
start_index += 1 | |
else: | |
start_index -= 1 | |
if start_index < 0: | |
start_index = len(items) - 1 | |
elif start_index >= len(items): | |
start_index = 0 | |
# Search for match | |
for i in range(len(items)): | |
index = (start_index + i) % len(items) | |
if self.match_record(index): | |
self.view.tree.selection_set(items[index]) | |
self.view.tree.see(items[index]) | |
return | |
messagebox.showinfo("Search", "No more matches found") | |
def match_record(self, index): | |
"""Check if record matches search criteria""" | |
record = self.doc.get_record(index) | |
if not record: | |
return False | |
search_text = self.last_search["text"] | |
if not self.last_search["case_sensitive"]: | |
search_text = search_text.lower() | |
if self.last_search["search_type"] == "name": | |
text = record.name | |
elif self.last_search["search_type"] == "comment": | |
text = record.comment | |
elif self.last_search["search_type"] == "type": | |
text = record.get_type_string() | |
elif self.last_search["search_type"] == "subtype": | |
text = record.get_sub_type_string() | |
elif self.last_search["search_type"] == "method": | |
text = record.get_method_string() | |
else: | |
return False | |
if not self.last_search["case_sensitive"]: | |
text = text.lower() | |
return search_text in text | |
def on_column_settings(self): | |
"""Change column settings""" | |
dialog = ColumnDialog(self) | |
if dialog.result: | |
# Update column widths | |
self.view.tree.column("#0", width=dialog.result["name"]) | |
self.view.tree.column("Mark", width=dialog.result["mark"]) | |
self.view.tree.column("Type", width=dialog.result["type"]) | |
self.view.tree.column("SubType", width=dialog.result["subtype"]) | |
self.view.tree.column("Method", width=dialog.result["method"]) | |
self.view.tree.column("Link16", width=dialog.result["link16"]) | |
self.view.tree.column("Link32", width=dialog.result["link32"]) | |
self.view.tree.column("Comment", width=dialog.result["comment"]) | |
# Save settings | |
self.settings["column_widths"] = dialog.result | |
self.save_settings() | |
if __name__ == "__main__": | |
app = AvpEditApp() | |
app.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment