import struct
import os
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog, filedialog, scrolledtext
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import platform
import sys
import datetime
import math
import hashlib
import re
import binascii
class FAT32Parser:
def __init__(self, device_path, readonly=True):
self.device_path = device_path
self.readonly = readonly
self.sector_size = 512
self.cluster_size = 0
self.fat_start = 0
self.data_start = 0
self.root_cluster = 0
self.fat = []
self.free_clusters = []
self.total_clusters = 0
try:
# 打开设备
mode = 'rb' if readonly else 'r+b'
self.fd = open(device_path, mode)
except Exception as e:
raise ValueError(f"无法打开设备: {e}")
try:
# 读取引导扇区
self.fd.seek(0)
boot_sector = self.fd.read(512)
# 检查FAT32签名
if len(boot_sector) < 512 or boot_sector[510] != 0x55 or boot_sector[511] != 0xAA:
raise ValueError("无效的引导扇区签名 - 可能不是FAT32格式")
# 解析关键参数
self.sectors_per_cluster = boot_sector[13]
if self.sectors_per_cluster not in [1, 2, 4, 8, 16, 32, 64, 128]:
raise ValueError("无效的每簇扇区数")
self.reserved_sectors = struct.unpack('<H', boot_sector[14:16])[0]
self.num_fats = boot_sector[16]
self.sectors_per_fat = struct.unpack('<I', boot_sector[36:40])[0]
self.root_cluster = struct.unpack('<I', boot_sector[44:48])[0]
self.total_sectors = struct.unpack('<I', boot_sector[32:36])[0] or struct.unpack('<I', boot_sector[40:44])[0]
# 计算关键位置
self.cluster_size = self.sectors_per_cluster * self.sector_size
self.fat_start = self.reserved_sectors * self.sector_size
self.data_start = self.fat_start + (self.num_fats * self.sectors_per_fat * self.sector_size)
# 计算总簇数
self.total_clusters = (self.total_sectors - self.reserved_sectors - self.num_fats * self.sectors_per_fat) // self.sectors_per_cluster
# 加载FAT表
self._load_fat_table()
# 检查根簇号是否有效
if self.root_cluster < 2 or self.root_cluster >= len(self.fat):
raise ValueError(f"无效的根簇号: {self.root_cluster}")
# 扫描空闲簇
self._find_free_clusters()
# 保存引导扇区信息
self.boot_sector_info = self._parse_boot_sector(boot_sector)
except Exception as e:
self.fd.close()
raise e
def _parse_boot_sector(self, boot_sector):
"""解析引导扇区信息"""
info = {}
info['OEM Name'] = boot_sector[3:11].decode('ascii', errors='replace').strip()
info['Bytes Per Sector'] = struct.unpack('<H', boot_sector[11:13])[0]
info['Sectors Per Cluster'] = boot_sector[13]
info['Reserved Sectors'] = struct.unpack('<H', boot_sector[14:16])[0]
info['Number of FATs'] = boot_sector[16]
info['Root Entries'] = struct.unpack('<H', boot_sector[17:19])[0] # FAT16 only
info['Total Sectors 16'] = struct.unpack('<H', boot_sector[19:21])[0] # FAT16 only
info['Media Descriptor'] = hex(boot_sector[21])
info['Sectors Per FAT 16'] = struct.unpack('<H', boot_sector[22:24])[0] # FAT16 only
info['Sectors Per Track'] = struct.unpack('<H', boot_sector[24:26])[0]
info['Number of Heads'] = struct.unpack('<H', boot_sector[26:28])[0]
info['Hidden Sectors'] = struct.unpack('<I', boot_sector[28:32])[0]
info['Total Sectors 32'] = struct.unpack('<I', boot_sector[32:36])[0]
# FAT32 specific
info['Sectors Per FAT'] = struct.unpack('<I', boot_sector[36:40])[0]
info['Flags'] = struct.unpack('<H', boot_sector[40:42])[0]
info['FAT Version'] = struct.unpack('<H', boot_sector[42:44])[0]
info['Root Directory Cluster'] = struct.unpack('<I', boot_sector[44:48])[0]
info['FSInfo Sector'] = struct.unpack('<H', boot_sector[48:50])[0]
info['Backup Boot Sector'] = struct.unpack('<H', boot_sector[50:52])[0]
info['Volume Label'] = boot_sector[71:82].decode('ascii', errors='replace').strip()
info['File System Type'] = boot_sector[82:90].decode('ascii', errors='replace').strip()
return info
def _load_fat_table(self):
"""加载FAT表到内存"""
self.fd.seek(self.fat_start)
fat_size = self.sectors_per_fat * self.sector_size
fat_data = self.fd.read(fat_size)
if len(fat_data) != fat_size:
raise ValueError("读取FAT表失败")
# 解析FAT32表项 (每4字节一个簇)
self.fat = []
for i in range(0, len(fat_data), 4):
# 只取低28位(FAT32实际用28位)
entry = struct.unpack('<I', fat_data[i:i+4])[0] & 0x0FFFFFFF
self.fat.append(entry)
def _find_free_clusters(self):
"""查找所有空闲簇"""
self.free_clusters = []
for cluster_idx in range(2, len(self.fat)): # 簇0和1保留
if self.fat[cluster_idx] == 0:
self.free_clusters.append(cluster_idx)
def get_cluster_chain(self, start_cluster):
"""获取文件的簇链"""
if start_cluster < 2 or start_cluster >= len(self.fat):
return [] # 无效簇号
chain = []
current = start_cluster
visited = set()
# 追踪簇链直到结束
while current < 0x0FFFFFF8: # 文件结束标记
if current in visited:
messagebox.showwarning("警告", f"发现循环簇链!簇 {current} 已被访问过")
break # 防止无限循环
if current >= len(self.fat):
messagebox.showwarning("警告", f"簇链越界!簇 {current} 超出FAT表范围")
break # 越界保护
chain.append(current)
visited.add(current)
next_cluster = self.fat[current]
if next_cluster == 0 or next_cluster >= 0x0FFFFFF8:
break
current = next_cluster
return chain
def get_fat_chain(self, start_cluster):
"""获取文件的FAT链(包含FAT表值)"""
chain = self.get_cluster_chain(start_cluster)
fat_chain = []
for cluster in chain:
if cluster < len(self.fat):
fat_chain.append((cluster, self.fat[cluster]))
return fat_chain
def read_directory(self, cluster):
"""读取目录内容"""
entries = []
chain = self.get_cluster_chain(cluster)
if not chain:
return entries
# 长文件名缓存
lfn_parts = []
for c in chain:
# 计算簇对应的扇区
sector_offset = self.data_start + (c - 2) * self.cluster_size
if sector_offset < 0 or sector_offset > self.get_disk_size():
break
self.fd.seek(sector_offset)
data = self.fd.read(self.cluster_size)
if len(data) < self.cluster_size:
break # 读取不完整
# 解析每个目录项(32字节)
for i in range(0, len(data), 32):
entry = data[i:i+32]
if len(entry) < 32:
continue
if entry[0] == 0x00: # 空闲
lfn_parts = [] # 重置长文件名缓存
continue
if entry[0] == 0xE5: # 删除
lfn_parts = [] # 重置长文件名缓存
continue
attr = entry[11]
if attr == 0x0F: # 长文件名条目
# 解析长文件名片段
seq = entry[0]
name_part = entry[1:11] + entry[14:26] + entry[28:32]
# 移除尾部的0x00
name_part = name_part.split(b'\x00')[0]
try:
name_part = name_part.decode('utf-16le', errors='ignore')
except:
name_part = '?'
lfn_parts.append((seq, name_part))
continue
# 短文件名条目
name = entry[0:8].decode('latin-1', errors='ignore').strip()
ext = entry[8:11].decode('latin-1', errors='ignore').strip()
fullname = name + ('.' + ext if ext else '')
# 如果有长文件名,使用它
if lfn_parts:
# 排序并组合长文件名片段
lfn_parts.sort(key=lambda x: x[0])
long_name = ''.join(part for _, part in reversed(lfn_parts))
fullname = long_name.strip()
lfn_parts = []
# 解析属性
is_dir = bool(attr & 0x10)
is_volume = bool(attr & 0x08)
is_hidden = bool(attr & 0x02)
is_system = bool(attr & 0x04)
is_archive = bool(attr & 0x20)
# 获取起始簇号
start_cluster_hi = struct.unpack('<H', entry[20:22])[0]
start_cluster_lo = struct.unpack('<H', entry[26:28])[0]
start_cluster = (start_cluster_hi << 16) | start_cluster_lo
# 文件大小
size = struct.unpack('<I', entry[28:32])[0]
# 解析时间日期
create_time = struct.unpack('<H', entry[14:16])[0]
create_date = struct.unpack('<H', entry[16:18])[0]
mod_time = struct.unpack('<H', entry[22:24])[0]
mod_date = struct.unpack('<H', entry[24:26])[0]
# 跳过卷标
if is_volume:
continue
# 添加所有条目
entries.append({
'name': fullname,
'is_dir': is_dir,
'is_hidden': is_hidden,
'is_system': is_system,
'is_archive': is_archive,
'start_cluster': start_cluster,
'size': size,
'create_time': self._parse_dos_datetime(create_date, create_time),
'mod_time': self._parse_dos_datetime(mod_date, mod_time)
})
return entries
def _parse_dos_datetime(self, date, time):
"""解析DOS日期时间格式"""
try:
# 解析日期: 7位年(从1980), 4位月, 5位日
year = ((date & 0xFE00) >> 9) + 1980
month = (date & 0x01E0) >> 5
day = date & 0x001F
# 解析时间: 5位时, 6位分, 5位秒(以2秒为单位)
hour = (time & 0xF800) >> 11
minute = (time & 0x07E0) >> 5
second = (time & 0x001F) * 2
return f"{year}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
except:
return "未知时间"
def read_file_content(self, start_cluster, size):
"""读取文件内容"""
content = b''
chain = self.get_cluster_chain(start_cluster)
if not chain:
return content
bytes_remaining = size
for cluster in chain:
if bytes_remaining <= 0:
break
# 计算簇位置
sector_offset = self.data_start + (cluster - 2) * self.cluster_size
if sector_offset < 0 or sector_offset > self.get_disk_size():
break
self.fd.seek(sector_offset)
# 读取簇内容
bytes_to_read = min(bytes_remaining, self.cluster_size)
try:
chunk = self.fd.read(bytes_to_read)
if not chunk:
break
content += chunk
bytes_remaining -= len(chunk)
except Exception as e:
messagebox.showerror("读取错误", f"读取簇 {cluster} 失败: {str(e)}")
break
return content
def get_disk_size(self):
"""获取磁盘大小"""
try:
self.fd.seek(0, 2) # 移动到文件末尾
return self.fd.tell()
except Exception as e:
messagebox.showerror("错误", f"获取磁盘大小失败: {str(e)}")
return 0
def create_file(self, parent_cluster, filename, content):
"""在指定目录创建文件"""
if self.readonly:
raise PermissionError("只读模式不允许写操作")
# 1. 分配簇链
clusters_needed = math.ceil(len(content) / self.cluster_size)
if clusters_needed > len(self.free_clusters):
raise IOError("磁盘空间不足")
allocated_clusters = self.free_clusters[:clusters_needed]
self.free_clusters = self.free_clusters[clusters_needed:]
# 2. 更新FAT表
for i in range(len(allocated_clusters)):
cluster = allocated_clusters[i]
if i == len(allocated_clusters) - 1:
# 最后一个簇标记为EOF
self.fat[cluster] = 0x0FFFFFFF
else:
# 指向下一个簇
self.fat[cluster] = allocated_clusters[i+1]
# 3. 写入FAT表
self._write_fat_table()
# 4. 写入文件内容
for i, cluster in enumerate(allocated_clusters):
offset = self.data_start + (cluster - 2) * self.cluster_size
self.fd.seek(offset)
# 写入当前簇的数据
start = i * self.cluster_size
end = min((i+1) * self.cluster_size, len(content))
self.fd.write(content[start:end])
# 5. 在父目录创建目录项
self._create_directory_entry(parent_cluster, {
'name': filename,
'is_dir': False,
'start_cluster': allocated_clusters[0],
'size': len(content)
})
return allocated_clusters[0]
def create_directory(self, parent_cluster, dirname):
"""在指定目录创建子目录"""
if self.readonly:
raise PermissionError("只读模式不允许写操作")
# 1. 分配一个簇给新目录
if not self.free_clusters:
raise IOError("磁盘空间不足")
cluster = self.free_clusters[0]
self.free_clusters = self.free_clusters[1:]
# 2. 更新FAT表 (目录结束)
self.fat[cluster] = 0x0FFFFFFF
self._write_fat_table()
# 3. 初始化目录内容 (创建 . 和 .. 条目)
self._initialize_directory(cluster, parent_cluster)
# 4. 在父目录创建目录项
self._create_directory_entry(parent_cluster, {
'name': dirname,
'is_dir': True,
'start_cluster': cluster,
'size': 0
})
return cluster
def _initialize_directory(self, cluster, parent_cluster):
"""初始化新目录内容"""
# 计算簇位置
offset = self.data_start + (cluster - 2) * self.cluster_size
# 创建 . 条目
dot_entry = self._create_dir_entry(".", cluster, True)
# 创建 .. 条目
dotdot_entry = self._create_dir_entry("..", parent_cluster, True)
# 写入目录内容
self.fd.seek(offset)
self.fd.write(dot_entry)
self.fd.write(dotdot_entry)
# 填充剩余空间为0
remaining = self.cluster_size - len(dot_entry) - len(dotdot_entry)
self.fd.write(b'\x00' * remaining)
def _create_dir_entry(self, name, cluster, is_dir):
"""创建目录项字节数据"""
# 短文件名格式 (8.3)
if len(name) > 8 and '.' not in name:
base, ext = name[:8], ""
else:
parts = name.split('.')
base = parts[0].upper()[:8]
ext = parts[1].upper()[:3] if len(parts) > 1 else ""
# 填充空格
base = base.ljust(8, ' ')
ext = ext.ljust(3, ' ')
# 创建32字节条目
entry = bytearray(32)
# 文件名 (8字节)
entry[0:8] = base.encode('latin-1')
# 扩展名 (3字节)
entry[8:11] = ext.encode('latin-1')
# 属性 (目录)
entry[11] = 0x10 if is_dir else 0x20 # 目录或存档
# 创建时间和日期 (当前时间)
now = datetime.datetime.now()
create_time = self._to_dos_time(now)
create_date = self._to_dos_date(now)
entry[14:16] = struct.pack('<H', create_time)
entry[16:18] = struct.pack('<H', create_date)
# 修改时间和日期
mod_time = create_time
mod_date = create_date
entry[22:24] = struct.pack('<H', mod_time)
entry[24:26] = struct.pack('<H', mod_date)
# 起始簇号
entry[20:22] = struct.pack('<H', (cluster >> 16) & 0xFFFF) # 高16位
entry[26:28] = struct.pack('<H', cluster & 0xFFFF) # 低16位
# 文件大小 (目录为0)
entry[28:32] = struct.pack('<I', 0)
return entry
def _to_dos_time(self, dt):
"""将datetime转换为DOS时间格式"""
return ((dt.hour << 11) | (dt.minute << 5) | (dt.second // 2))
def _to_dos_date(self, dt):
"""将datetime转换为DOS日期格式"""
return (((dt.year - 1980) << 9) | (dt.month << 5) | dt.day)
def _create_directory_entry(self, parent_cluster, entry_info):
"""在父目录中添加新的目录项"""
# 获取父目录的所有簇
clusters = self.get_cluster_chain(parent_cluster)
if not clusters:
raise IOError("父目录无效")
# 查找空闲目录槽
for cluster in clusters:
offset = self.data_start + (cluster - 2) * self.cluster_size
self.fd.seek(offset)
data = self.fd.read(self.cluster_size)
for i in range(0, len(data), 32):
pos = offset + i
entry = data[i:i+32]
# 找到空闲或已删除的条目
if len(entry) < 32 or entry[0] in [0x00, 0xE5]:
# 创建新条目
new_entry = self._create_dir_entry(
entry_info['name'],
entry_info['start_cluster'],
entry_info['is_dir']
)
# 设置文件大小
if not entry_info['is_dir']:
new_entry[28:32] = struct.pack('<I', entry_info['size'])
# 写入新条目
self.fd.seek(pos)
self.fd.write(new_entry)
return
# 如果没有找到空闲槽,分配新簇
if not self.free_clusters:
raise IOError("磁盘空间不足")
new_cluster = self.free_clusters[0]
self.free_clusters = self.free_clusters[1:]
# 更新FAT表
self.fat[clusters[-1]] = new_cluster # 当前最后一个簇指向新簇
self.fat[new_cluster] = 0x0FFFFFFF # 新簇标记为EOF
self._write_fat_table()
# 初始化新簇
self.fd.seek(self.data_start + (new_cluster - 2) * self.cluster_size)
self.fd.write(b'\x00' * self.cluster_size)
# 写入新条目到新簇的第一个位置
new_entry = self._create_dir_entry(
entry_info['name'],
entry_info['start_cluster'],
entry_info['is_dir']
)
if not entry_info['is_dir']:
new_entry[28:32] = struct.pack('<I', entry_info['size'])
self.fd.seek(self.data_start + (new_cluster - 2) * self.cluster_size)
self.fd.write(new_entry)
def _write_fat_table(self):
"""将FAT表写回磁盘"""
# 更新所有FAT副本
for fat_copy in range(self.num_fats):
offset = self.fat_start + fat_copy * self.sectors_per_fat * self.sector_size
self.fd.seek(offset)
# 构建FAT表数据
fat_data = bytearray()
for entry in self.fat:
# 确保使用32位格式
fat_data += struct.pack('<I', entry)
# 写入FAT表
self.fd.write(fat_data)
def close(self):
"""安全关闭文件句柄"""
if hasattr(self, 'fd') and self.fd:
self.fd.close()
class FAT32Explorer:
def __init__(self, root, device_path, readonly=True):
self.root = root
self.root.title(f"FAT32 文件系统分析工具 - {device_path}")
self.root.geometry("1400x900")
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.device_path = device_path
self.readonly = readonly
self.current_cluster = None
self.current_path = "/"
self.selected_file = None
self.file_content_cache = None
self.canvas = None
self.cluster_canvas = None
# 创建GUI布局
self._create_widgets()
# 在后台加载文件系统
self.loading = True
self.status = tk.StringVar()
self.status.set("正在加载文件系统...")
self.root.after(100, self._load_filesystem)
def _create_widgets(self):
# 创建主框架
main_frame = tk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 顶部工具栏
toolbar = tk.Frame(main_frame)
toolbar.pack(fill=tk.X, pady=(0, 10))
mode_text = "只读模式" if self.readonly else "读写模式"
mode_color = "green" if self.readonly else "red"
tk.Label(toolbar, text=f"模式: {mode_text}", fg=mode_color, font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=10)
# 路径导航
self.path_var = tk.StringVar(value="路径: /")
tk.Label(toolbar, textvariable=self.path_var, font=("Arial", 10)).pack(side=tk.LEFT, padx=10)
# 操作按钮
btn_frame = tk.Frame(toolbar)
btn_frame.pack(side=tk.RIGHT)
tk.Button(btn_frame, text="刷新", command=self.refresh).pack(side=tk.LEFT, padx=2)
tk.Button(btn_frame, text="物理布局", command=self.show_physical_layout).pack(side=tk.LEFT, padx=2)
tk.Button(btn_frame, text="FAT表信息", command=self.show_fat_info).pack(side=tk.LEFT, padx=2)
if not self.readonly:
tk.Button(btn_frame, text="新建文件", command=self.create_file_dialog).pack(side=tk.LEFT, padx=2)
tk.Button(btn_frame, text="新建目录", command=self.create_directory_dialog).pack(side=tk.LEFT, padx=2)
tk.Button(btn_frame, text="写入测试文件", command=self.write_test_file).pack(side=tk.LEFT, padx=2)
# 主分割窗口
self.paned_window = tk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
self.paned_window.pack(fill=tk.BOTH, expand=True)
# 左侧面板 (目录树)
self.left_frame = tk.LabelFrame(self.paned_window, text="目录结构")
self.paned_window.add(self.left_frame, width=300)
# 目录树
self.tree = ttk.Treeview(self.left_frame, show='tree', columns=("size"))
self.tree.heading("#0", text="名称")
self.tree.heading("size", text="大小")
self.tree.column("size", width=80, anchor='e')
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(self.left_frame, orient=tk.VERTICAL, command=self.tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.bind('<<TreeviewSelect>>', self.on_tree_select)
# 右侧面板
self.right_frame = tk.PanedWindow(self.paned_window, orient=tk.VERTICAL)
self.paned_window.add(self.right_frame)
# 上部: 文件列表
file_frame = tk.LabelFrame(self.right_frame, text="当前目录内容")
file_frame.pack(fill=tk.BOTH, expand=True)
# 文件列表表头
columns = ("name", "size", "type", "cluster", "modified", "attributes")
self.file_tree = ttk.Treeview(file_frame, columns=columns, show="headings")
# 设置列
self.file_tree.heading("name", text="名称")
self.file_tree.heading("size", text="大小")
self.file_tree.heading("type", text="类型")
self.file_tree.heading("cluster", text="起始簇")
self.file_tree.heading("modified", text="修改时间")
self.file_tree.heading("attributes", text="属性")
self.file_tree.column("name", width=200)
self.file_tree.column("size", width=80, anchor='e')
self.file_tree.column("type", width=80)
self.file_tree.column("cluster", width=80, anchor='e')
self.file_tree.column("modified", width=150)
self.file_tree.column("attributes", width=80)
self.file_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.file_tree.bind('<<TreeviewSelect>>', self.on_file_select)
self.file_tree.bind('<Double-1>', self.on_file_double_click)
# 文件列表滚动条
file_scrollbar = ttk.Scrollbar(file_frame, orient=tk.VERTICAL, command=self.file_tree.yview)
file_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.file_tree.configure(yscrollcommand=file_scrollbar.set)
# 添加右键菜单
self.file_menu = tk.Menu(self.root, tearoff=0)
self.file_menu.add_command(label="查看内容", command=self.show_file_content)
self.file_menu.add_command(label="查看簇链", command=self.show_cluster_chain)
self.file_menu.add_command(label="计算哈希", command=self.calculate_hash)
self.file_tree.bind("<Button-3>", self.show_context_menu)
# 下部: 簇状态可视化
self.cluster_frame = tk.LabelFrame(self.right_frame, text="簇分配图")
self.cluster_frame.pack(fill=tk.BOTH, expand=True)
# 簇链可视化框架
self.cluster_canvas_frame = tk.Frame(self.cluster_frame)
self.cluster_canvas_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 底部: 状态栏
self.status = tk.StringVar()
self.status.set("就绪" + (" - 只读模式" if self.readonly else " - 读写模式"))
status_bar = tk.Label(self.root, textvariable=self.status, bd=1, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 添加文件内容查看器 (弹出窗口)
self.content_window = None
self.cluster_window = None
def _load_filesystem(self):
"""安全加载文件系统"""
try:
self.parser = FAT32Parser(self.device_path, self.readonly)
self.root_cluster = self.parser.root_cluster
# 加载根目录
self._show_root_directory()
self.status.set(f"就绪 | 总簇数: {len(self.parser.fat)} | 空闲簇: {len(self.parser.free_clusters)}")
self.loading = False
# 更新簇分配图
self._update_cluster_map()
except Exception as e:
self.status.set(f"错误: {str(e)}")
messagebox.showerror("初始化错误", f"加载文件系统失败: {str(e)}")
self.root.after(100, self.root.destroy)
def _show_root_directory(self):
"""显示根目录"""
self.tree.delete(*self.tree.get_children())
self.file_tree.delete(*self.file_tree.get_children())
self.current_path = "/"
self.path_var.set(f"路径: {self.current_path}")
# 添加根节点
root_id = self.tree.insert('', 'end', text="根目录", values=[""], open=True)
self.current_cluster = self.root_cluster
# 加载根目录内容
self._load_directory(root_id, self.root_cluster)
def _load_directory(self, parent_id, cluster):
"""加载目录内容到树视图和列表"""
try:
entries = self.parser.read_directory(cluster)
self.current_entries = entries
# 清空文件列表
self.file_tree.delete(*self.file_tree.get_children())
# 添加到文件列表
for entry in entries:
# 跳过特殊目录项 '.' 和 '..'
if entry['name'] in ['.', '..']:
continue
item_type = "目录" if entry['is_dir'] else "文件"
size = self.format_size(entry['size']) if not entry['is_dir'] else ""
# 构建属性字符串
attributes = []
if entry['is_hidden']: attributes.append("隐藏")
if entry['is_system']: attributes.append("系统")
if entry['is_archive']: attributes.append("存档")
attr_str = ", ".join(attributes)
self.file_tree.insert('', 'end', values=(
entry['name'],
size,
item_type,
entry['start_cluster'],
entry['mod_time'],
attr_str
))
# 添加到树视图
for entry in entries:
# 跳过特殊目录项 '.' 和 '..'
if entry['name'] in ['.', '..']:
continue
if entry['is_dir']:
size = self.format_size(entry['size'])
node_id = self.tree.insert(parent_id, 'end',
text=entry['name'],
values=[entry['start_cluster']],
tags=('dir',))
# 添加一个虚拟节点以便展开
self.tree.insert(node_id, 'end', text="加载中...", tags=('dummy',))
# 配置标签样式
self.tree.tag_configure('dir', foreground='blue')
self.tree.tag_configure('dummy', foreground='gray')
self.status.set(f"显示目录: 簇 {cluster} | 条目: {len(entries)}")
except Exception as e:
self.status.set(f"错误: {str(e)}")
messagebox.showerror("目录错误", f"读取目录失败: {str(e)}")
def _update_cluster_map(self):
"""更新簇状态可视化"""
if not hasattr(self, 'parser'):
return
# 清除现有图表
if self.canvas:
self.canvas.get_tk_widget().destroy()
fig, ax = plt.subplots(figsize=(10, 3))
# 简化的簇状态展示
max_clusters = min(1000, len(self.parser.fat))
# 创建状态数组,每个元素对应一个簇的状态
cluster_status = []
# 收集前max_clusters个簇的状态
for cluster_idx in range(max_clusters):
# 标记特殊簇
if cluster_idx == 0:
cluster_status.append(4) # 特殊簇
elif cluster_idx == 1:
cluster_status.append(4) # 特殊簇
elif self.parser.fat[cluster_idx] == 0:
cluster_status.append(0) # 空闲
elif self.parser.fat[cluster_idx] >= 0x0FFFFFF8:
cluster_status.append(1) # 文件结束
elif self.parser.fat[cluster_idx] == 0x0FFFFFF7:
cluster_status.append(3) # 坏簇
else:
cluster_status.append(2) # 使用中
# 修复:将一维列表转换为二维数组用于热力图
# 计算合适的行数和列数
num_cols = 50 # 每行50个簇
num_rows = (len(cluster_status) + num_cols - 1) // num_cols
# 创建二维数组
heatmap_data = []
for i in range(num_rows):
start_idx = i * num_cols
end_idx = min((i + 1) * num_cols, len(cluster_status))
row = cluster_status[start_idx:end_idx]
# 如果行不满,填充0
if len(row) < num_cols:
row += [0] * (num_cols - len(row))
heatmap_data.append(row)
# 使用不同颜色
cmap = plt.cm.colors.ListedColormap([
'green', # 0: 空闲
'red', # 1: 文件结束
'blue', # 2: 使用中
'black', # 3: 坏簇
'gray', # 4: 特殊簇
])
# 绘制热力图
img = ax.imshow(heatmap_data, cmap=cmap, aspect='auto')
# 添加颜色条
cbar = fig.colorbar(img, ax=ax, ticks=[0, 1, 2, 3, 4])
cbar.ax.set_yticklabels(['空闲', '结束簇', '使用中', '坏簇', '特殊簇'])
ax.set_title(f'簇分配图 (前{max_clusters}个簇)')
ax.set_xlabel('簇号 (每行50个)')
ax.set_ylabel('簇组')
# 添加网格线
ax.grid(which='major', color='gray', linestyle='-', linewidth=0.5)
ax.set_xticks(range(0, num_cols, 5))
ax.set_yticks(range(0, num_rows, 5))
# 嵌入到Canvas
canvas = FigureCanvasTkAgg(fig, master=self.cluster_canvas_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.canvas = canvas
def show_context_menu(self, event):
"""显示右键菜单"""
item = self.file_tree.identify_row(event.y)
if item:
self.file_tree.selection_set(item)
self.file_menu.post(event.x_root, event.y_root)
def on_tree_select(self, event):
"""处理树节点选择事件"""
if self.loading:
return
selected = self.tree.selection()
if not selected:
return
item = self.tree.item(selected[0])
if 'values' in item and item['values']:
# 如果是虚拟节点,跳过
if 'dummy' in self.tree.item(selected[0], "tags"):
return
# 获取簇号(目录节点才有)
if 'dir' in self.tree.item(selected[0], "tags"):
cluster = self.tree.item(selected[0])['values'][0]
if isinstance(cluster, int) or cluster.isdigit():
self.current_cluster = int(cluster)
else:
# 根目录
self.current_cluster = self.parser.root_cluster
# 构建当前路径
path = []
current_item = selected[0]
while current_item:
item_text = self.tree.item(current_item)['text']
if item_text != "根目录":
path.insert(0, item_text)
current_item = self.tree.parent(current_item)
self.current_path = "/" + "/".join(path)
self.path_var.set(f"路径: {self.current_path}")
# 如果节点有子节点但只有一个"加载中"节点,则加载实际内容
children = self.tree.get_children(selected[0])
if children and self.tree.item(children[0])['text'] == "加载中...":
self.tree.delete(children[0])
self._load_directory(selected[0], self.current_cluster)
else:
self._load_directory(selected[0], self.current_cluster)
def on_file_select(self, event):
"""处理文件列表选择事件"""
if self.loading:
return
selected = self.file_tree.selection()
if not selected:
return
item = self.file_tree.item(selected[0])
values = item['values']
if values:
# 查找对应的条目
for entry in self.current_entries:
if entry['name'] == values[0]:
self.selected_file = entry
break
def on_file_double_click(self, event):
"""双击文件事件"""
if self.selected_file and not self.selected_file['is_dir']:
self.show_file_content()
def show_file_content(self):
"""显示文件内容"""
if not self.selected_file or self.selected_file['is_dir']:
return
# 创建弹出窗口
if self.content_window and self.content_window.winfo_exists():
self.content_window.destroy()
self.content_window = tk.Toplevel(self.root)
self.content_window.title(f"文件内容: {self.selected_file['name']}")
self.content_window.geometry("800x600")
# 添加文本编辑器
text_frame = tk.Frame(self.content_window)
text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 添加文本区域
self.content_text = scrolledtext.ScrolledText(text_frame, wrap=tk.WORD)
self.content_text.pack(fill=tk.BOTH, expand=True)
# 添加按钮
btn_frame = tk.Frame(self.content_window)
btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
tk.Button(btn_frame, text="加载内容", command=self.load_file_content).pack(side=tk.LEFT, padx=5)
if not self.readonly:
tk.Button(btn_frame, text="保存修改", command=self.save_file_content).pack(side=tk.LEFT, padx=5)
tk.Button(btn_frame, text="关闭", command=self.content_window.destroy).pack(side=tk.RIGHT, padx=5)
def load_file_content(self):
"""加载文件内容到编辑器"""
if not self.selected_file:
return
try:
content = self.parser.read_file_content(
self.selected_file['start_cluster'],
self.selected_file['size']
)
# 尝试解码为文本
try:
decoded = content.decode('utf-8', errors='replace')
self.content_text.delete(1.0, tk.END)
self.content_text.insert(tk.END, decoded)
self.file_content_cache = content
self.status.set(f"已加载文件: {self.selected_file['name']}")
except UnicodeDecodeError:
# 如果是二进制文件,显示十六进制预览
hex_preview = ' '.join(f'{b:02x}' for b in content[:128])
if len(content) > 128:
hex_preview += " ..."
self.content_text.delete(1.0, tk.END)
self.content_text.insert(tk.END, f"二进制文件 (十六进制预览):\n{hex_preview}")
self.file_content_cache = content
self.status.set(f"已加载二进制文件: {self.selected_file['name']}")
except Exception as e:
self.status.set(f"错误: {str(e)}")
messagebox.showerror("读取错误", f"读取文件内容失败: {str(e)}")
def save_file_content(self):
"""保存修改后的文件内容"""
if self.readonly or not self.selected_file or not self.file_content_cache:
return
# 获取新内容
new_content = self.content_text.get(1.0, tk.END).encode('utf-8')
# 检查内容是否变化
if new_content == self.file_content_cache:
messagebox.showinfo("保存", "内容未更改")
return
# 确认保存
confirm = messagebox.askyesno("确认保存",
f"确定要保存对文件 '{self.selected_file['name']}' 的修改吗?\n"
"此操作将直接写入U盘!")
if not confirm:
return
try:
# 创建新文件 (如果大小变化)
if len(new_content) != len(self.file_content_cache):
# 创建新文件
new_cluster = self.parser.create_file(
self.current_cluster,
self.selected_file['name'],
new_content
)
# 删除旧文件 (标记为删除)
# 在实际应用中应该实现文件删除功能
messagebox.showinfo("保存成功", "文件大小已改变,已创建新文件副本")
else:
# 直接覆盖内容
chain = self.parser.get_cluster_chain(self.selected_file['start_cluster'])
bytes_remaining = len(new_content)
for i, cluster in enumerate(chain):
if bytes_remaining <= 0:
break
offset = self.parser.data_start + (cluster - 2) * self.parser.cluster_size
self.parser.fd.seek(offset)
# 写入当前簇的数据
start = i * self.parser.cluster_size
end = min((i+1) * self.parser.cluster_size, len(new_content))
self.parser.fd.write(new_content[start:end])
bytes_remaining -= (end - start)
self.status.set(f"文件已保存: {self.selected_file['name']}")
messagebox.showinfo("保存成功", "文件内容已更新")
# 刷新目录
self.refresh()
修正代码
最新发布