簇 扇区

本文深入探讨了在计算机存储领域中,FAT、FAT32和NTFS三种文件系统的运作机制及磁盘空间管理原理。了解每种文件系统的特性、簇的概念及其对文件大小和数量的限制,有助于优化存储使用效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文件占用磁盘空间时,基本单位不是字节而是簇。簇的大小与磁盘的规格有关,一般情况下,软盘每簇是1个扇区,硬盘每簇的扇区数与硬盘的总容量大小有关,可能是4、8、16、32、64……             通常在Windows平台下使用的3种文件系统是FAT(文件分区表),FAT32(32位文件分区表)和NTFS(NT文件系统)。在FAT文件系统下,每一个磁盘被分成固定大小的簇。簇最少为512个字节,其大小可以成倍增长,最大为32K。每个簇都是由唯一的索引号——一个16位二进制数来标识。因为16位二进制数最大为65536,所以FAT分区所拥有的簇的数量不可能超过65536个。簇的数量和大小的限制,就是FAT分区为什么不能超过2GB的原因。     FAT中的入口连接着组成一个文件的各个簇,文件的目录入口包含其第一个簇的索引号,而该簇在FAT中的入口又包含着下一个簇的索引号,依此类推。一个文件的最后一簇对应的FAT入口则包含着一个特殊的文件终止符,未使用的簇和损坏的簇也会用特殊代码标识出来。FAT32文件的原理几乎与此相同,但它的簇更小,而且由于FAT32入口是32位,所以其容量理论上可以超过40亿个字节。     NTFS是一个相当高级的文件系统。它的主文件表(MFT)是一个非常完整的数据库,它负责对磁盘上的每个文件进行索引。每个MFT的入口通常为1K大小,其中记录了大量的文件信息。NTFS可以在文件的MFT入口中存储非常小的文件的全部内容;对于大一些的文件,这些入口会标识出包含文件数据的簇。
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
typedef struct{BYTE NoUsed[11];//为了后面好编程,保留这个Bytes,分别是跳转指令和OEM代号WORD BPBBytesPerSector;//0x0B 每扇区字节数BYTE BPBSectorsPerClusters;   //0x0D 每扇区数BYTE BPBUnused1[2];            //0x0E 保留扇区数BYTE BPBUnused2[3];         //0x10 总为BYTE BPBUnused3[2];         //0x13 未用BYTE BPBMedia;//0x15 介质描述符BYTE BPBUnused4[2];//0x16 未用WORD BPBSectorPerTruck;//0x18 每磁道扇区数WORD BPBNumberOfHeads;//0x1A 磁头数DWORD BPBHideSector;//0x1C 隐藏扇区BYTE BPBUnused5[4];//0x20 保留BYTE BPBUnused6[4];//0x24 每FAT扇区数DWORD64 BPBTotalSector;//0x28  扇区总数DWORD64 BPBMFTStartCluster;//0x30   $MFT 起始号DWORD64 BPBMFTMirStartCluster;//0x38 $MFTMir 起始号BYTE BPBFileSize;//0x40文件记录的大小描述BYTE BPBUnused7[3];//0x41 未用BYTE BPBIndexBufferSize;//0x44 索引缓冲区大小描述BYTE BPBUnused8[3];              //45 未用BYTE BPBVolumeName[8];//0x48 卷序列号BYTE BPBCheckSum[4];//0x52 校验和 }NTFSBPB,*PNTFSBPB;NTFS的MFT文件记录头结构体如下:typedef struct{char MFTHSignature[4];   //0x00 MFT标志,一定是"FILE"WORD MFTHUSNOffset;       //0x04 USN偏移WORD MFTHUSNSize;    //0x06 USN大小与数组LONGLONG MFTHLSN;                //0x08 LSNWORD MFTHSequenceNumber;//0x10 序列号 用于记录主文件表记录被重复使用的次数WORD MFTHHardLinkCount;//0x12 硬链接数,有多少个目录指向该文件WORD MFTHFirstAttributeOffset;   //0x14  第一个属性的偏移地址WORD MFTHFlags;//0x16 标志00文件被删除;01文件正在使用 02目录被删除  03目录正在使用DWORD MFTHRealRecSize;   //0x18  文件记录的实际长度DWORD MFTHAllocatedRecSize;//0x1C  文件记录的分配长度LONGLONG MFTHBasicFileRec;    //0x20  基本文件记录中的文件索引号WORD MFTHNextAttributeID;//0x28  下一个属性IDWORD MFTHBoundary;       //0x2A  边界DWORD MFTHReferenceNumber;//0x2C  文件记录参考号WORD       MFTHUSN;                //0x30  更新序列号BYTE       MFTHUSA[4];                //0x32  更新数组 }NTFSMFTFILERECHEAD,*PNTFSMFTFILERECHEAD;用于构建文件目录树的结构体如下:文件链表 struct NTFSFILELINK{DWORD64 MFTReferenceNumber;  //文件记录参考号DWORD64 MFTParentReferenceNumber; //父目录文件参考号,使用的时候要注意,只有前个字节是父目录的参考号//可以学着先和xFFFFFFFFFFFF相与,然后再右移位WORD FileFlag; //标志,和MFT中定义的一样,00删除文件,02被删除目录WORD FileName[255];//文件名WORD FileExtend[3];//扩展名DWORD64 FileSize;//文件实际大小DWORD64 AllFileSize; //文件分配大小DWORD64 FileCreateTime;//文件创建时间DWORD64 FileModifyTime;//修改时间NTFSFILELINK *subDir;//子目录指针NTFSFILELINK *Next;//下一个节点指针 };//NTFS 文件记录属性,如果是常驻,则RESIDENT,否则NONRESIDENT typedef struct   {DWORD AttibuteType;//0x00  属性类型DWORD ThisAttributeSize;//0x04  本属性的长度BYTE IfResident;//0x08 是否为常驻BYTE AttributeNameLength;//0x09  属性名长度WORD AttributeNameOffset;//0x0A  属性名的开始偏移WORD AttributeFlags;//0x0C  标志WORD AttributeID; //0x0E  属性ID//这里使用联合,sizeof是以最大的为准,如果是小的赋了最大值,后面的忽略就好了union ATTR{//常驻struct RESIDENT{DWORD AttributeVolumeLength; //0x10  属性体的长度WORD AttributeVolumeOffset;  //0x14  属性体的开始偏移BYTE IndexFlags; //0x16  索引标志BYTE Padding;  //0x17 填充无意义} Resident;//非常驻struct NONRESIDENT{DWORD64 StartVCN;  //0x10 属性体的起始虚拟号(VCN)DWORD64 EndVCN; //0x18  属性体的结束虚拟号WORD DatarunOffset; //0x20  Data List偏移地址WORD CompressionSize; //0x22  压缩单位大小BYTE Padding[4]; //0x24  无意义DWORD64 AllocSize;//0x28  属性体的分配大小DWORD64 RealSize; //0x30  属性体的实际大小DWORD64 StreamSize;//0x38  属性体的初始大小// data runs...}NonResident;}Attr; } NTFSFILERECATTRIBUTE,*PNTFSFILERECATTRIBUTE;
04-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值