Add PARAM.SFO parser for PlayStation 3 game metadata

This commit is contained in:
2026-03-15 20:07:52 +00:00
parent 04f9204a25
commit 8bbda5bf2b
6 changed files with 445 additions and 0 deletions

View File

@@ -46,6 +46,7 @@ set(TEST_DATA_FILES
cdmode2_v1.aif
cdmode2.aif
BLES-00905.ird
ps3_param.sfo
)
# Create data directory in build tree
@@ -92,10 +93,12 @@ add_executable(tests_run
ps3_crypto.cpp
ps3_encryption_map.cpp
ps3_ird.cpp
ps3_sfo.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/aes128.c
${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/ps3_crypto.c
${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/ps3_encryption_map.c
${CMAKE_CURRENT_SOURCE_DIR}/../tool/ird.c
${CMAKE_CURRENT_SOURCE_DIR}/../tool/sfo.c
)
# Set up include directories using modern target-specific approach

BIN
tests/data/ps3_param.sfo Normal file

Binary file not shown.

177
tests/ps3_sfo.cpp Normal file
View File

@@ -0,0 +1,177 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* PARAM.SFO parser tests using a synthetic test file.
*/
#include <climits>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "gtest/gtest.h"
extern "C"
{
#include "../tool/sfo.h"
}
class SfoFixture : public ::testing::Test
{
public:
SfoFixture() = default;
protected:
uint8_t *sfo_data = nullptr;
uint32_t sfo_data_len = 0;
SfoFile sfo;
void SetUp() override
{
memset(&sfo, 0, sizeof(sfo));
char path[PATH_MAX];
char cwd[PATH_MAX];
getcwd(cwd, PATH_MAX);
snprintf(path, PATH_MAX, "%s/data/ps3_param.sfo", cwd);
FILE *fp = fopen(path, "rb");
ASSERT_NE(nullptr, fp) << "Cannot open " << path;
fseek(fp, 0, SEEK_END);
sfo_data_len = (uint32_t)ftell(fp);
rewind(fp);
sfo_data = static_cast<uint8_t *>(malloc(sfo_data_len));
ASSERT_NE(nullptr, sfo_data);
ASSERT_EQ(sfo_data_len, fread(sfo_data, 1, sfo_data_len, fp));
fclose(fp);
}
void TearDown() override
{
ps3_free_sfo(&sfo);
free(sfo_data);
}
~SfoFixture() override = default;
};
/* Test: parse succeeds */
TEST_F(SfoFixture, ParseSucceeds)
{
int32_t ret = ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
ASSERT_EQ(0, ret);
EXPECT_EQ(8u, sfo.entry_count);
}
/* Test: TITLE key */
TEST_F(SfoFixture, Title)
{
ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
const char *val = ps3_sfo_get_string(&sfo, "TITLE");
ASSERT_NE(nullptr, val);
EXPECT_STREQ("Test Game Title", val);
}
/* Test: TITLE_ID key */
TEST_F(SfoFixture, TitleId)
{
ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
const char *val = ps3_sfo_get_string(&sfo, "TITLE_ID");
ASSERT_NE(nullptr, val);
EXPECT_STREQ("BLES99999", val);
}
/* Test: CATEGORY key */
TEST_F(SfoFixture, Category)
{
ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
const char *val = ps3_sfo_get_string(&sfo, "CATEGORY");
ASSERT_NE(nullptr, val);
EXPECT_STREQ("DG", val);
}
/* Test: APP_VER key */
TEST_F(SfoFixture, AppVer)
{
ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
const char *val = ps3_sfo_get_string(&sfo, "APP_VER");
ASSERT_NE(nullptr, val);
EXPECT_STREQ("01.00", val);
}
/* Test: VERSION key */
TEST_F(SfoFixture, Version)
{
ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
const char *val = ps3_sfo_get_string(&sfo, "VERSION");
ASSERT_NE(nullptr, val);
EXPECT_STREQ("02.50", val);
}
/* Test: PS3_SYSTEM_VER key */
TEST_F(SfoFixture, SystemVer)
{
ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
const char *val = ps3_sfo_get_string(&sfo, "PS3_SYSTEM_VER");
ASSERT_NE(nullptr, val);
EXPECT_STREQ("04.21", val);
}
/* Test: non-existent key returns NULL */
TEST_F(SfoFixture, NonExistentKey)
{
ps3_parse_sfo(sfo_data, sfo_data_len, &sfo);
const char *val = ps3_sfo_get_string(&sfo, "NONEXISTENT");
EXPECT_EQ(nullptr, val);
}
/* Test: NULL arguments return error */
TEST(SfoErrors, NullArgs)
{
SfoFile sfo;
uint8_t dummy[20] = {0};
EXPECT_LT(ps3_parse_sfo(NULL, 10, &sfo), 0);
EXPECT_LT(ps3_parse_sfo(dummy, 10, NULL), 0);
}
/* Test: too-small buffer returns error */
TEST(SfoErrors, TooSmall)
{
SfoFile sfo;
uint8_t buf[10] = {0};
EXPECT_LT(ps3_parse_sfo(buf, 10, &sfo), 0);
}
/* Test: wrong magic returns error */
TEST(SfoErrors, WrongMagic)
{
SfoFile sfo;
uint8_t buf[20];
memset(buf, 0, 20);
buf[0] = 0xFF; /* bad magic */
EXPECT_LT(ps3_parse_sfo(buf, 20, &sfo), 0);
}
/* Test: free on zeroed struct is safe */
TEST(SfoErrors, FreeZeroed)
{
SfoFile sfo;
memset(&sfo, 0, sizeof(sfo));
ps3_free_sfo(&sfo); /* should not crash */
ps3_free_sfo(NULL); /* should not crash */
}
/* Test: get_string with NULL sfo returns NULL */
TEST(SfoErrors, GetStringNull)
{
EXPECT_EQ(nullptr, ps3_sfo_get_string(NULL, "TITLE"));
SfoFile sfo;
memset(&sfo, 0, sizeof(sfo));
EXPECT_EQ(nullptr, ps3_sfo_get_string(&sfo, NULL));
}

View File

@@ -30,6 +30,8 @@ add_executable(aaruformattool
inject_media_tag.c
ird.c
ird.h
sfo.c
sfo.h
termbox2.h
)

181
tool/sfo.c Normal file
View File

@@ -0,0 +1,181 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* PARAM.SFO parser for PlayStation 3 game metadata.
*
* SFO binary format (all fields little-endian):
*
* Header (20 bytes):
* [4B] magic: 0x00505346 ("\0PSF")
* [4B] version: e.g. 0x00000101
* [4B] key_table_offset: offset to key string table
* [4B] data_table_offset: offset to data value table
* [4B] entry_count: number of index entries
*
* Index table (entry_count × 16 bytes each):
* [2B] key_offset: offset into key table for this entry's name
* [2B] data_format: 0x0004=UTF-8, 0x0404=UTF-8(special), 0x0204=int32
* [4B] data_len: actual data length
* [4B] data_maxlen: maximum data length (padded)
* [4B] data_offset: offset into data table for this entry's value
*
* Key table: null-terminated ASCII strings back-to-back
* Data table: values packed per index entries
*/
#include "sfo.h"
#include <stdlib.h>
#include <string.h>
#define SFO_MAGIC 0x46535000 /* \"\\0PSF\" as little-endian uint32 (bytes: 00 50 53 46) */
#define SFO_FORMAT_UTF8 0x0004
#define SFO_FORMAT_UTF8_SPECIAL 0x0204
#define SFO_FORMAT_INT32 0x0404
static inline uint16_t read_le16(const uint8_t *p) { return (uint16_t)p[0] | ((uint16_t)p[1] << 8); }
static inline uint32_t read_le32(const uint8_t *p)
{ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); }
int32_t ps3_parse_sfo(const uint8_t *data, uint32_t length, SfoFile *sfo)
{
if(data == NULL || sfo == NULL) return -1;
memset(sfo, 0, sizeof(*sfo));
/* Need at least the 20-byte header */
if(length < 20) return -2;
uint32_t magic = read_le32(data);
if(magic != SFO_MAGIC) return -3;
/* uint32_t version = read_le32(data + 4); — not needed */
uint32_t key_table_offset = read_le32(data + 8);
uint32_t data_table_offset = read_le32(data + 12);
uint32_t entry_count = read_le32(data + 16);
if(entry_count == 0) return 0;
/* Validate that the index table fits */
uint32_t index_end = 20 + entry_count * 16;
if(index_end > length || key_table_offset > length || data_table_offset > length) return -2;
sfo->entries = calloc(entry_count, sizeof(SfoEntry));
if(sfo->entries == NULL) return -4;
sfo->entry_count = entry_count;
for(uint32_t i = 0; i < entry_count; i++)
{
const uint8_t *idx = data + 20 + i * 16;
uint16_t key_offset = read_le16(idx);
uint16_t data_format = read_le16(idx + 2);
uint32_t data_len = read_le32(idx + 4);
/* uint32_t data_maxlen = read_le32(idx + 8); — not needed */
uint32_t data_offset = read_le32(idx + 12);
sfo->entries[i].format = data_format;
/* Read key from key table (null-terminated) */
uint32_t abs_key = key_table_offset + key_offset;
if(abs_key < length)
{
/* Find null terminator, bounded by end of buffer */
const char *key_start = (const char *)(data + abs_key);
size_t max_len = length - abs_key;
size_t key_len = strnlen(key_start, max_len);
sfo->entries[i].key = malloc(key_len + 1);
if(sfo->entries[i].key != NULL)
{
memcpy(sfo->entries[i].key, key_start, key_len);
sfo->entries[i].key[key_len] = '\0';
}
}
/* Read value from data table */
uint32_t abs_data = data_table_offset + data_offset;
if(abs_data < length && data_len > 0)
{
uint32_t avail = length - abs_data;
if(data_len > avail) data_len = avail;
if(data_format == SFO_FORMAT_UTF8 || data_format == SFO_FORMAT_UTF8_SPECIAL)
{
/* String value: copy ensuring null termination */
sfo->entries[i].value = malloc(data_len + 1);
if(sfo->entries[i].value != NULL)
{
memcpy(sfo->entries[i].value, data + abs_data, data_len);
sfo->entries[i].value[data_len] = '\0';
/* Trim trailing nulls for clean string */
size_t slen = strlen(sfo->entries[i].value);
if(slen < data_len) sfo->entries[i].value[slen] = '\0';
}
}
else if(data_format == SFO_FORMAT_INT32 && data_len >= 4)
{
sfo->entries[i].int_value = (int32_t)read_le32(data + abs_data);
}
}
}
return 0;
}
const char *ps3_sfo_get_string(const SfoFile *sfo, const char *key)
{
if(sfo == NULL || key == NULL || sfo->entries == NULL) return NULL;
for(uint32_t i = 0; i < sfo->entry_count; i++)
if(sfo->entries[i].key != NULL && strcmp(sfo->entries[i].key, key) == 0) return sfo->entries[i].value;
return NULL;
}
void ps3_free_sfo(SfoFile *sfo)
{
if(sfo == NULL) return;
if(sfo->entries != NULL)
{
for(uint32_t i = 0; i < sfo->entry_count; i++)
{
free(sfo->entries[i].key);
free(sfo->entries[i].value);
}
free(sfo->entries);
sfo->entries = NULL;
}
sfo->entry_count = 0;
}

82
tool/sfo.h Normal file
View File

@@ -0,0 +1,82 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* PARAM.SFO parser for PlayStation 3 game metadata.
*/
#ifndef LIBAARUFORMAT_TOOL_SFO_H
#define LIBAARUFORMAT_TOOL_SFO_H
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
#endif
/** A single key-value entry from a PARAM.SFO file. */
typedef struct SfoEntry
{
char *key; /**< Parameter name (malloc'd, null-terminated). */
char *value; /**< UTF-8 string value (malloc'd, null-terminated). Only for string-type entries. */
int32_t int_value; /**< Integer value (for integer-type entries). */
uint16_t format; /**< Data format: 0x0004 = UTF-8 string, 0x0204 = UTF-8 string (special), 0x0404 = int32. */
} SfoEntry;
/** Parsed PARAM.SFO file. */
typedef struct SfoFile
{
SfoEntry *entries; /**< Array of entries (malloc'd). */
uint32_t entry_count; /**< Number of entries. */
} SfoFile;
/**
* @brief Parse a PARAM.SFO from an in-memory buffer.
*
* @param data Pointer to the SFO file data.
* @param length Length of the data buffer.
* @param sfo Output: parsed SFO structure. Must be freed with ps3_free_sfo().
* @return 0 on success, negative on error:
* -1: NULL argument
* -2: buffer too small or invalid header
* -3: invalid magic
* -4: memory allocation failure
*/
int32_t ps3_parse_sfo(const uint8_t *data, uint32_t length, SfoFile *sfo);
/**
* @brief Look up a string value by key in a parsed SFO.
*
* @param sfo Parsed SFO structure.
* @param key Key to search for.
* @return Pointer to the value string (owned by the SfoFile), or NULL if not found.
*/
const char *ps3_sfo_get_string(const SfoFile *sfo, const char *key);
/**
* @brief Free all dynamically allocated fields in an SfoFile.
*
* @param sfo Pointer to SfoFile to clean up.
*/
void ps3_free_sfo(SfoFile *sfo);
#ifdef __cplusplus
}
#endif
#endif /* LIBAARUFORMAT_TOOL_SFO_H */