FAT file system-Backup Boot Sector

本文探讨了FAT32文件系统中备份引导扇区(BPB_BkBootSec)的重要性及其如何减少因主引导扇区丢失或损坏导致的数据风险。与FAT16/FAT12不同,FAT32在第6扇区提供了完整的引导记录备份,确保即使主引导扇区无法读取,也能恢复数据。
Another feature on FAT32 volumes that is not present on FAT16/FAT12 is the BPB_BkBootSec field. FAT16/FAT12 volumes can be totally lost if the contents of sector 0 of the volume are overwritten or sector 0 goes bad and cannot be read. This is a “single point of failure” for FAT16 and FAT12 volumes. The BPB_BkBootSec field reduces the severity of this problem for FAT32 volumes, because starting at that sector number on the volume—6—there is a backup copy of the boot sector information including the volume’s BPB.

In the case where the sector 0 information has been accidentally overwritten, all a disk repair utility has to do is restore the boot sector(s) from the backup copy. In the case where sector 0 goes bad, this allows the volume to be mounted so that the user can access data before replacing the disk.

This second case—sector 0 goes bad—is the reason why no value other than 6 should ever be placed in the BPB_BkBootSec field. If sector 0 is unreadable, various operating systems are “hard wired” to check for backup boot sector(s) starting at sector 6 of the FAT32 volume. Note that starting at the BPB_BkBootSec sector is a complete boot record. The Microsoft FAT32 “boot sector” is actually three 512-byte sectors long. There is a copy of all three of these sectors starting at the BPB_BkBootSec sector. A copy of the FSInfo sector is also there, even though the BPB_FSInfo field in this backup boot sector is set to the same value as is stored in the sector 0 BPB.

NOTE: All 3 of these sectors have the 0xAA55 signature in sector offsets 510 and 511, just like the first boot sector does

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() 修正代码
最新发布
06-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值