从PS2存储卡中导出游戏存档

从PS2存储卡中导出游戏存档

上一篇文章中我们解析了PS2存储卡的文件系统,这次直接实战,编写python代码导出指定的游戏存档。

01 解析SuperBlock

SuperBlock结构如下,大小为340字节:

c++ 复制代码
struct SuperBlock {
    char magic[28];
    char version[12];
    uint16 page_size;
    uint16 pages_per_cluster;
    uint16 pages_per_block;
    uint16 unknown; // ignore
    uint32 clusters_per_card;
    uint32 alloc_offset;
    uint32 alloc_end;
    uint32 rootdir_cluster;
    uint32 backup_block1; // ignore
    uint32 backup_block2; // ignore
    uint32 unknown[2]; // ignore
    uint32 ifc_list[32];
    uint32 bad_block_list[32]; // ignore
    byte card_type;
    byte card_flags;
    byte unknown; // ignore
    byte unknown; // ignore
};

使用struct.unpack()解包:

python 复制代码
struct.Struct("<28s12sHHH2xLLLL4x4x8x128s128xbbxx").unpack(byte_val)

得到page_sizepages_per_cluster

02 读取pagecluster

根据公式计算pagecluster大小:

python 复制代码
self.spare_size = (self.page_size // 128) * 4 # 备用区域字节数
self.raw_page_size = self.page_size + self.spare_size # 算上备用区域的page字节数
self.cluster_size = self.page_size * self.pages_per_cluster # 簇字节数

读取pageclusterspare area里的内容是被舍弃掉的:

python 复制代码
def read_page(self, n): # n为page编号
    offset = self.raw_page_size * n
    return self.byte_val[offset: offset + self.page_size]

def read_cluster(self, n): # n为cluster编号
    page_index = n * self.pages_per_cluster
    byte_buffer = bytearray()
    for i in range(self.pages_per_cluster):
        byte_buffer += self.read_page(page_index + i)
    return bytes(byte_buffer)

03 构建FAT矩阵

从上一篇文章知道FAT矩阵的构建方式如下:

python 复制代码
def __build_fat_matrix(self):
    indirect_fat_matrix = self.__build_matrix(self.ifc_list) # 从ifc_list构建间接FAT
    indirect_fat_matrix = indirect_fat_matrix.reshape(indirect_fat_matrix.size) # 间接FAT是个一维数组
    indirect_fat_matrix = [x for x in indirect_fat_matrix if x != Fat.UNALLOCATED] # 排除掉0xFFFFFFFF这种未分配的
    fat_matrix = self.__build_matrix(indirect_fat_matrix) # 从间接FAT构建直接FAT
    return fat_matrix

def __build_matrix(self, cluster_list):
    matrix = np.zeros((len(cluster_list), self.fat_per_cluster), np.uint32) # 初始化矩阵
    for index, v in enumerate(cluster_list): # 遍历cluster
        cluster_value = self.read_cluster(v) # 读出每个cluster的256个FAT
        cluster_value_unpacked = np.frombuffer(cluster_value, np.uint32)
        for index0, v0 in enumerate(cluster_value_unpacked):
            matrix[index, index0] = v0 # 给矩阵赋值
    return matrix

def get_fat_value(self, n): # 给出簇编号n,找到其对应的FAT的值
    value = self.fat_matrix[(n // self.fat_per_cluster) % self.fat_per_cluster, n % self.fat_per_cluster]
    return value ^ Fat.ALLOCATED_BIT if value & Fat.ALLOCATED_BIT > 0 else value # 最高位为8代表正常使用的簇,其它值代表簇未分配,最高位为8时,取低31位的整形值

04 条目数据结构

条目是所有文件和目录的元数据,条目的数据结构如下:

c++ 复制代码
struct Entry {
    uint16 mode;
    uint16 unknown; // ignore
    uint32 length;
    char created[8];
    uint32 cluster;
    uint32 dir_entry; // ignore
    char modified[8];
    uint32 attr; // ignore
    char padding[28]; // ignore
    char name[32];
    char padding[416]; // ignore
};

使用struct.unpack()解包:

python 复制代码
struct.Struct("<H2xL8sL4x8s4x28x32s416x").unpack(byte_val)

每个条目的大小为512字节,条目里最重要的字段是cluster,标识了该条目对应的文件或目录的簇编号。如果本条目是目录,则对应的簇编号是"条目簇";如果本条目是文件,则对应的簇编号是"文件簇"。另一个重要字段是length,如果本条目是目录,则对应的是目录下的条目数;如果本条目是文件,则对应的是文件的字节数。

05 解析"条目簇"和"数据簇"

python 复制代码
def read_entry_cluster(self, cluster_offset): # 读取条目,条目是512字节,一个簇可以包含多个条目
    cluster_value = self.read_cluster(cluster_offset + self.alloc_offset)
    return Entry.build(cluster_value)

def read_data_cluster(self, entry): # 读取数据,要从第一个簇开始读取到文件结束
    byte_buffer = bytearray()
    chain_start = entry.cluster
    bytes_read = 0
    while chain_start != Fat.CHAIN_END:
        to_read = min(entry.length - bytes_read, self.cluster_size)
        byte_buffer += self.read_cluster(chain_start + self.alloc_offset)[:to_read]
        bytes_read += to_read
        chain_start = self.get_fat_value(chain_start)
    return bytes(byte_buffer)

def build(byte_val):
    entry_count = len(byte_val) // Entry.__size
    entries = []
    for i in range(entry_count):
        entries.append(Entry(byte_val[i * Entry.__size: i * Entry.__size + Entry.__size]))
    return entries

06 读取存储卡中的所有文件

上一篇文章说过,根目录没有条目,它的首个"条目簇"在超级块的rootdir_cluster中,它的"包含条目数"在.这个条目中。

要读取存储卡中的所有文件,第一步是解析根目录下所有条目,再解析条目下所有文件。因此只要循环调用以下方法:

python 复制代码
def find_sub_entries(self, parent_entry):
    chain_start = parent_entry.cluster
    sub_entries = []
    while chain_start != Fat.CHAIN_END:
        entries = self.read_entry_cluster(chain_start)
        for e in entries:
            if len(sub_entries) < parent_entry.length:
                sub_entries.append(e.unpack())
        chain_start = self.get_fat_value(chain_start)
    return [x for x in sub_entries if not x.name.startswith('.')]

结果如下:

css 复制代码
BISCPS-15119sv01
    GameData
    BISCPS-15119sv01
    icon00.ico
    icon.sys
BISCPS-15116sv01
    GameData
    BISCPS-15116sv01
    icon00.ico
    icon.sys
BASLUS-21441DBZT2
    icon.sys
    dbzsn.ico
    BASLUS-21441DBZT2
...

07 导出游戏存档

既然所有文件条目都已经读取出来了,我们只要写个方法,根据输入的游戏名称,即可导出目录下的所有文件。

python 复制代码
def export(self, name, dest):
    dir_path = dest + os.sep + name
    if not os.path.exists(dir_path):
        os.mkdir(dir_path)
    entries = self.lookup_entry_by_name(name)
    for e in entries:
        if e.is_file():
            with open(dir_path + os.sep + e.name, 'wb') as f:
                f.write(self.ps2mc.read_data_cluster(e))

08 结尾

至此,我们已经可以把一个游戏的存档从存储卡中导出来了。项目完整代码可以访问:github.com/caol64/ps2m...。下一篇我们将分析一下每个存档文件里的icon.sysxxx.ico文件,这两个文件是存档3d特效的数据文件。

09 参考文献

相关推荐
玄同76511 小时前
Python Random 模块深度解析:从基础 API 到 AI / 大模型工程化实践
人工智能·笔记·python·学习·算法·语言模型·llm
AIFarmer11 小时前
在EV3上运行Python语言——环境设置
python·ev3
yunsr11 小时前
python作业3
开发语言·python
历程里程碑11 小时前
普通数组-----除了自身以外数组的乘积
大数据·javascript·python·算法·elasticsearch·搜索引擎·flask
曦月逸霜11 小时前
Python快速入门——学习笔记(持续更新中~)
笔记·python·学习
喵手11 小时前
Python爬虫实战:采集菜谱网站的“分类/列表页”(例如“家常菜”或“烘焙”频道)数据,构建高可用的美食菜谱数据采集流水线(附CSV导出)!
爬虫·python·爬虫实战·零基础python爬虫教学·采集菜谱网站数据·家常菜或烘焙频道·构建高可用食谱数据采集系统
喵手11 小时前
Python爬虫实战:硬核解析 Google Chrome 官方更新日志(正则+文本清洗篇)(附 CSV 导出)!
爬虫·python·爬虫实战·零基础python爬虫教学·csv导出·监控谷歌版本发布历史·获取稳定版更新日志
小邓睡不饱耶11 小时前
实战|W餐饮平台智能化菜品推荐方案(含Spark实操+算法选型+完整流程)
python·ai·ai编程·ai写作
草莓熊Lotso11 小时前
Qt 主窗口核心组件实战:菜单栏、工具栏、状态栏、浮动窗口全攻略
运维·开发语言·人工智能·python·qt·ui
aiguangyuan11 小时前
基于BiLSTM-CRF的命名实体识别模型:原理剖析与实现详解
人工智能·python·nlp