mirror of
https://github.com/aaru-dps/libaaruformat.git
synced 2026-07-08 18:06:18 +00:00
Add PARAM.SFO parser for PlayStation 3 game metadata
This commit is contained in:
@@ -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
BIN
tests/data/ps3_param.sfo
Normal file
Binary file not shown.
177
tests/ps3_sfo.cpp
Normal file
177
tests/ps3_sfo.cpp
Normal 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));
|
||||
}
|
||||
@@ -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
181
tool/sfo.c
Normal 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
82
tool/sfo.h
Normal 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 */
|
||||
Reference in New Issue
Block a user