PFS: Difference between revisions
mNo edit summary |
m (const) |
||
Line 157: | Line 157: | ||
The hashes are sorted in ascending order in the file. The hash function is given below: | The hashes are sorted in ascending order in the file. The hash function is given below: | ||
<source lang="c"> | <source lang="c"> | ||
uint32_t fptbl_hash(char* filename){ | uint32_t fptbl_hash(const char* filename){ | ||
int i; | int i; | ||
uint32_t hash = 0; | uint32_t hash = 0; | ||
for(i = 0; filename[i]; i++) { | for(i = 0; filename[i]; i++) { | ||
// convert character to uppercase | |||
char c = (filename[i] >= 'a' && filename[i] <= 'z') ? (filename[i] - 32) : filename[i]; | |||
hash = c + 31 * hash; | |||
} | } | ||
return hash; | return hash; |
Revision as of 23:20, 28 January 2017
PFS (Playstation File System) is the file system used by (at least) downloadable content and games on the PS4. It is loosely based on the UFS (Unix File System) used in FreeBSD. PFS typically uses a 64kB block size, although the block size is configurable and specified in the file system header.
Structure
There are four main sections in a Playstation File System:
- Header (superblock)
- Inode blocks
- Directory blocks
- Data blocks
Header/Superblock
Offset | Value | Size | Notes |
---|---|---|---|
0x00 | version | 0x8 | Should be 1 |
0x08 | magic | 0x8 | Should be 20130315 |
0x10 | id | 0x8 | |
0x18 | fmode | 0x1 | |
0x19 | clean | 0x1 | |
0x1A | ronly | 0x1 | If 1, this is a readonly filesystem |
0x1B | rsv | 0x1 | |
0x1C | mode | 0x2 | |
0x1E | unknown | 0x2 | |
0x20 | blocksz | 0x4 | The size of each block in the filesystem. |
0x24 | nbackup | 0x4 | |
0x28 | nblock | 0x8 | |
0x30 | ndinode | 0x8 | Number of inodes in the inode blocks |
0x38 | ndblock | 0x8 | Number of data blocks |
0x40 | ndinodeblock | 0x8 | Number of inode blocks |
0x48 | superroot_ino | 0x8 |
typedef struct {
int64 version;
int64 magic;
int32 id[2];
char fmode;
char clean;
char ronly;
char rsv;
int16 mode;
int16 unk1;
int32 blocksz;
int32 nbackup;
int64 nblock;
int64 ndinode;
int64 ndblock;
int64 ndinodeblock;
int64 superroot_ino;
} PFS_HDR;
Inodes
Inode table starts at the second block and continues for header.ndinodeblock blocks. There are only header.blocksz / sizeof(di_d32) inodes per block. (inodes will never cross a block boundary)
For an explanation of inodes, direct and indirect blocks, see inode pointer structure on Wikipedia. Most PFS images from PKGs have just one direct block pointer per file, and the file's data just takes up consecutive blocks. But if there is fragmentation on the file system, you will have to follow the direct and indirect block pointers.
Offset | Value | Size | Notes |
---|---|---|---|
0x00 | mode | 0x2 | Inode mode (bitwise OR of flags; file=0x8000, directory=0x4000) |
0x02 | nlink | 0x2 | |
0x04 | flags | 0x4 | |
0x08 | size | 0x8 | |
0x10 | unknown | 0x38 | |
0x48 | uid | 0x4 | |
0x4C | gid | 0x4 | |
0x50 | unknown | 0x10 | |
0x60 | blocks | 0x4 | Number of blocks occupied |
0x64 | db | 0x30 | Direct blocks |
0x94 | ib | 0x14 | Indirect blocks |
typedef struct {
uint16 mode;
uint16 nlink;
uint16 flags[2];
uint64 size;
char unk1[56];
uint32 uid;
uint32 gid;
uint64 unk2[2];
uint32 blocks;
int32 db[12];
int32 ib[5];
} di_d32;
Dirents
Each inode with a mode OR'd with 0x4000 points to block(s) of dirents. Dirents contain the name and type of files, directories, symlinks, etc. Each directory will have an associated dirent block containing at least the '.' and '..' special files, along with all other files and sub-directories in that directory. Dirents are 8-byte aligned. The entsize value will say the total length of this dirent. There is typically padding after name which can just be skipped.
If the type of the dirent is 3, it is a directory. Its ino value indicates the inode number (0 being the first inode). That inode will point to the dirent block for that directory so you can continue down the file system tree.
If the type of the dirent is 2, it is a file. Its inode will point you to the location(s) of the file data.
Offset | Value | Size | Notes |
---|---|---|---|
0x00 | ino | 0x4 | Inode index |
0x04 | type | 0x4 | Type of entry. 2=file, 3=directory |
0x08 | namelen | 0x4 | Length of filename (add 1 for 0-terminator) |
0x0C | entsize | 0x4 | Size of this whole struct, in bytes |
0x10 | name | namelen + 1 | Filename and 0-terminator |
0x11 + namelen | padding | variable | Padding so this structure is exactly entsize bytes. |
typedef struct {
int32 ino;
int32 type;
int32 namelen;
int32 entsize;
char name[namelen+1];
} dirent;
Finding the root
The filesystem tree starts with the superroot, a sort of meta-directory that contains the root directory within it, along with something called a "flat_path_table". The superroot's inode is typically the first (zeroeth) inode entry in the table, but you should check PFS_HDR.superroot_ino for the actual index. The true root of the filesystem is the "uroot" directory within the super root.
flat_path_table
The flat_path_table is a file located above the root directory on every PFS image. It is simply a mapping of filename hashes to inode number, to increase the lookup speed for files:
typedef struct {
uint32 filename_hash;
uint32 inode;
} path_table_entry;
The hashes are sorted in ascending order in the file. The hash function is given below:
uint32_t fptbl_hash(const char* filename){
int i;
uint32_t hash = 0;
for(i = 0; filename[i]; i++) {
// convert character to uppercase
char c = (filename[i] >= 'a' && filename[i] <= 'z') ? (filename[i] - 32) : filename[i];
hash = c + 31 * hash;
}
return hash;
}
Patent explaining the flat path table
Tools
- GameArchives/ArchiveExplorer C# library/tool that supports opening and extracting from PFS images