569 lines
20 KiB
C#
569 lines
20 KiB
C#
/* Copyright (c) 2015 - 2018 TheDarkporgramer
|
|
*
|
|
* This was originally done by Jappi88 (Jappi88 at Gmail dot com) https://github.com/Jappi88
|
|
* All modifications have been TheDarkporgramer (save sfo ext ext ) https://github.com/xXxTheDarkprogramerxXx
|
|
*
|
|
* This(software Is provided) 'as-is', without any express or implied
|
|
* warranty. In no event will the authors be held liable for any damages arising from the use of this software.
|
|
*
|
|
* Permission is granted to anyone to use this software for any purpose,
|
|
* including commercial applications*, and to alter it and redistribute it
|
|
* freely, subject to the following restrictions:
|
|
*
|
|
* 1. The origin of this software must not be misrepresented; you must not
|
|
* claim that you wrote the original software. If you use this software
|
|
* in a product, an acknowledge in the product documentation is required.
|
|
*
|
|
* 2. Altered source versions must be plainly marked as such, and must not
|
|
* be misrepresented as being the original software.
|
|
*
|
|
* 3. This notice may not be removed or altered from any source distribution.
|
|
*
|
|
* *Contact must be made to discuses permission and terms.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Linq;
|
|
|
|
namespace Param_SFO
|
|
{
|
|
public class PARAM_SFO
|
|
{
|
|
#region << Enums >>
|
|
public enum DataTypes : uint
|
|
{
|
|
PSN_Game = 18248,
|
|
GameData = 0x4744,
|
|
SaveData = 0x5344,
|
|
AppPhoto = 0x4150,
|
|
AppMusic = 0x414D,
|
|
AppVideo = 0x4156,
|
|
BroadCastVideo = 0x4256,
|
|
AppleTV = 4154,
|
|
WebTV = 5754,
|
|
CellBE = 0x4342,
|
|
Home = 0x484D,
|
|
StoreFronted = 0x5346,
|
|
HDDGame = 0x4847,
|
|
DiscGame = 0x4447,
|
|
AutoInstallRoot = 0x4152,
|
|
DiscPackage = 0x4450,
|
|
ExtraRoot = 0x5852,
|
|
VideoRoot = 0x5652,
|
|
ThemeRoot = 0x5452,
|
|
DiscMovie = 0x444D,
|
|
Game_Digital_Application = 0x4081AC0,//GD
|
|
PS4_Game_Application_Patch = 28775,
|
|
Additional_Content = 25441,//PSvita PS4
|
|
GameContent = 25447,//PSVITA
|
|
Blu_Ray_Disc = 25698,//PS4
|
|
None
|
|
}
|
|
|
|
public enum FMT : ushort
|
|
{
|
|
UTF_8 = 0x0004,
|
|
ASCII = 0x0402,
|
|
Utf8Null = 0x0204,
|
|
UINT32 = 0x0404,
|
|
}
|
|
|
|
#endregion << Enums >>
|
|
|
|
#region << Vars>>
|
|
public List<Table> Tables { get; set; }
|
|
|
|
#endregion << Vars>>
|
|
|
|
#region << Example Of Calling Functions >>
|
|
//ypu can use this as SFO.Atribute
|
|
public string Attribute
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return "";
|
|
foreach (Table t in Tables)
|
|
{
|
|
if (t.Name == "ATTRIBUTE")
|
|
return t.Value;
|
|
}
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public DataTypes DataType
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return DataTypes.None;
|
|
foreach (Table t in Tables)
|
|
if (t.Name == "CATEGORY")
|
|
return ((DataTypes)BitConverter.ToUInt16(Encoding.UTF8.GetBytes(t.Value), 0));
|
|
return DataTypes.None;
|
|
}
|
|
}
|
|
|
|
public string APP_VER
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return "";
|
|
foreach (Table t in Tables)
|
|
{
|
|
if (t.Name == "APP_VER")
|
|
return t.Value;
|
|
}
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public string Detail
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return "";
|
|
foreach (Table t in Tables)
|
|
if (t.Name == "DETAIL")
|
|
return t.Value;
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public string ContentID
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return "";
|
|
foreach (Table t in Tables)
|
|
if (t.Name == "CONTENT_ID")
|
|
return t.Value;
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public string GetValue(string tagName)
|
|
{
|
|
foreach (Table t in Tables)
|
|
if (t.Name == tagName)
|
|
return t.Value;
|
|
return "";
|
|
}
|
|
public string TITLEID
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return "";
|
|
foreach (Table t in Tables)
|
|
if (t.Name == "TITLE_ID")
|
|
return t.Value;
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public string TitleID
|
|
{
|
|
get
|
|
{
|
|
string name = TITLEID;
|
|
if (name == "")
|
|
return "";
|
|
return name.Split('-')[0];
|
|
}
|
|
}
|
|
|
|
public string Title
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return "";
|
|
foreach (Table t in Tables)
|
|
if (t.Name == "TITLE")
|
|
return t.Value;
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public string Category
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return "";
|
|
foreach (Table t in Tables)
|
|
if (t.Name == "CATEGORY")
|
|
return t.Value;
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public enum Playstation
|
|
{
|
|
ps3 = 0,
|
|
psvita = 1,
|
|
ps4 = 2,
|
|
psp = 3,
|
|
unknown = 4,//there will be a time i no longer support the scene this will be for ps5+ most probabbly
|
|
}
|
|
|
|
public Playstation PlaystationVersion
|
|
{
|
|
get
|
|
{
|
|
if (Tables == null)
|
|
return Playstation.unknown;
|
|
foreach (Table t in Tables)
|
|
{
|
|
if (t.Name == "PS3_SYSTEM_VER")
|
|
return Playstation.ps3;//this is the unique offset for ps3
|
|
if (t.Name == "PSP2_SYSTEM_VER")
|
|
{
|
|
return Playstation.psvita;//this is the only flag that tells us its a psvita
|
|
}
|
|
if (t.Name == "PSP_SYSTEM_VER")
|
|
{
|
|
return Playstation.psp;//this is how we know its a psp
|
|
}
|
|
if (t.Name == "SYSTEM_VER")//i believe this to be only ps4
|
|
{
|
|
return Playstation.ps4;
|
|
}
|
|
}
|
|
return Playstation.unknown;
|
|
}
|
|
}
|
|
|
|
|
|
#endregion << Example Of Calling Functions >>
|
|
|
|
#region Param.SFO Struct
|
|
|
|
public struct Header
|
|
{
|
|
public static byte[] Magic = { 0, 0x50, 0x53, 0x46 };
|
|
public static byte[] version = { 01, 01, 0, 0 };
|
|
public static uint KeyTableStart = 0;
|
|
public static uint DataTableStart = 0;
|
|
public static uint IndexTableEntries = 0;
|
|
|
|
private static byte[] Buffer
|
|
{
|
|
get
|
|
{
|
|
var header = new byte[20];
|
|
Array.Copy(Magic, 0, header, 0, 4);
|
|
Array.Copy(version, 0, header, 4, 4);
|
|
Array.Copy(BitConverter.GetBytes(KeyTableStart), 0, header, 8, 4);
|
|
Array.Copy(BitConverter.GetBytes(DataTableStart), 0, header, 12, 4);
|
|
Array.Copy(BitConverter.GetBytes(IndexTableEntries), 0, header, 16, 4);
|
|
return header;
|
|
}
|
|
}
|
|
|
|
public static void Read(BinaryReader input)
|
|
{
|
|
input.BaseStream.Seek(0, SeekOrigin.Begin);
|
|
input.Read(Magic, 0, 4);
|
|
input.Read(version, 0, 4);
|
|
KeyTableStart = input.ReadUInt32();
|
|
DataTableStart = input.ReadUInt32();
|
|
IndexTableEntries = input.ReadUInt32();
|
|
}
|
|
|
|
}
|
|
[Serializable]
|
|
public struct Table : IComparable
|
|
{
|
|
public index_table Indextable;
|
|
public string Name;
|
|
public string Value;
|
|
public int index;
|
|
|
|
public byte[] NameBuffer
|
|
{
|
|
get
|
|
{
|
|
var buffer = new byte[Name.Length + 1];
|
|
Array.Copy(Encoding.UTF8.GetBytes(Name), 0, buffer, 0, Encoding.UTF8.GetBytes(Name).Length);
|
|
return buffer;
|
|
}
|
|
}
|
|
|
|
public byte[] ValueBuffer
|
|
{
|
|
get
|
|
{
|
|
byte[] buffer;
|
|
switch (Indextable.param_data_fmt)
|
|
{
|
|
case FMT.ASCII:
|
|
buffer = new byte[Indextable.param_data_max_len];
|
|
Array.Copy(Encoding.ASCII.GetBytes(Value), 0, buffer, 0, Encoding.UTF8.GetBytes(Value).Length);
|
|
return buffer;
|
|
case FMT.UINT32:
|
|
return BitConverter.GetBytes(uint.Parse(Value));
|
|
case FMT.UTF_8:
|
|
buffer = new byte[Indextable.param_data_max_len];
|
|
Array.Copy(Encoding.UTF8.GetBytes(Value), 0, buffer, 0, Encoding.UTF8.GetBytes(Value).Length);
|
|
return buffer;
|
|
case FMT.Utf8Null:
|
|
buffer = new byte[Indextable.param_data_max_len];
|
|
Array.Copy(Encoding.UTF8.GetBytes(Value), 0, buffer, 0, Encoding.UTF8.GetBytes(Value).Length);/*write the length of the array*/
|
|
return buffer;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public int CompareTo(object obj)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
[Serializable]
|
|
public struct index_table
|
|
{
|
|
public FMT param_data_fmt; /* param_data data type */
|
|
public uint param_data_len; /* param_data used bytes */
|
|
public uint param_data_max_len; /* param_data total reserved bytes */
|
|
public uint param_data_offset; /* param_data offset (relative to start offset of data_table) */
|
|
public ushort param_key_offset; /* param_key offset (relative to start offset of key_table) */
|
|
|
|
|
|
private byte[] Buffer
|
|
{
|
|
get
|
|
{
|
|
var data = new byte[16];
|
|
Array.Copy(BitConverter.GetBytes(param_key_offset), 0, data, 0, 2);
|
|
Array.Copy(BitConverter.GetBytes(((ushort)param_data_fmt)), 0, data, 2, 2);
|
|
Array.Copy(BitConverter.GetBytes(param_data_len), 0, data, 4, 4);
|
|
Array.Copy(BitConverter.GetBytes(param_data_max_len), 0, data, 8, 4);
|
|
Array.Copy(BitConverter.GetBytes(param_data_offset), 0, data, 12, 4);
|
|
return data;
|
|
}
|
|
}
|
|
|
|
public void Read(BinaryReader input)
|
|
{
|
|
param_key_offset = input.ReadUInt16();
|
|
param_data_fmt = (FMT)input.ReadUInt16();
|
|
param_data_len = input.ReadUInt32();
|
|
param_data_max_len = input.ReadUInt32();
|
|
param_data_offset = input.ReadUInt32();
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
private enum DATA_TYPE : byte
|
|
{
|
|
BinaryData = 0,
|
|
Utf8Text = 2,
|
|
Si32Integer = 4
|
|
}
|
|
|
|
#endregion Param.SFO Struct
|
|
|
|
#region << Methods >>
|
|
|
|
|
|
public PARAM_SFO()
|
|
{
|
|
|
|
}
|
|
|
|
public PARAM_SFO(string filepath)
|
|
{
|
|
Init(new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.Read));
|
|
}
|
|
|
|
public PARAM_SFO(byte[] inputdata)
|
|
{
|
|
Init(new MemoryStream(inputdata));
|
|
}
|
|
|
|
public PARAM_SFO(Stream input)
|
|
{
|
|
Init(input);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// This is the SaveSFO Function for PS3/PS4/PSVita/And PSP no longer needed for Sony's CMD
|
|
/// </summary>
|
|
/// <param name="psfo">SFO That has been opened</param>
|
|
/// <param name="filename">Save Location</param>
|
|
public void SaveSFO(PARAM_SFO psfo, string filename)
|
|
{
|
|
//we start by opening the stream to the file
|
|
using (var stream = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.Read))
|
|
{
|
|
if (!stream.CanSeek)
|
|
throw new ArgumentException("Stream must be seekable");//throw this error we cant seek the stream
|
|
|
|
var utf8 = new UTF8Encoding(false);//encoding
|
|
using (var writer = new BinaryWriter(stream, utf8))//start binary reader
|
|
{
|
|
|
|
#region << Header Info (DevWiki) >>
|
|
/*
|
|
Header
|
|
* 0x00 0x04 magic PSF
|
|
0x04 0x04 version 01 01 00 00 1.01
|
|
0x08 0x04 key_table_start 24 00 00 00 Absolute start offset of key_table = 0x24
|
|
0x0C 0x04 data_table_start 30 00 00 00 Absolute start offset of data_table = 0x30
|
|
0x10 0x04 tables_entries 01 00 00 00 Number of entries in index_table, key_table, and data_table = 1
|
|
*/
|
|
|
|
#endregion <<Header Info >>
|
|
|
|
//so lets start writing the info
|
|
writer.Write(Header.Magic);//write magic "\0PSF"
|
|
writer.Write(Header.version);//write version info this is mayjor and minor (01 01 00 00 1.01)
|
|
Header.KeyTableStart = 0x14 + Header.IndexTableEntries * 0x10;/*we can write all this lovely info from the tables back*/
|
|
writer.Write(Header.KeyTableStart);
|
|
|
|
Header.DataTableStart = Convert.ToUInt32(Header.KeyTableStart + Tables.Sum(i => i.Name.Length + 1));//needs to be Uint
|
|
if (Header.DataTableStart % 4 != 0)
|
|
Header.DataTableStart = (Header.DataTableStart / 4 + 1) * 4;
|
|
writer.Write(Header.DataTableStart);
|
|
Header.IndexTableEntries = Convert.ToUInt32(Tables.Count);
|
|
writer.Write(Header.IndexTableEntries);
|
|
|
|
int lastKeyOffset = Convert.ToInt32(Header.KeyTableStart);
|
|
int lastValueOffset = Convert.ToInt32(Header.DataTableStart);
|
|
for (var i = 0; i < Tables.Count; i++)
|
|
{
|
|
var entry = Tables[i];
|
|
|
|
writer.BaseStream.Seek(0x14 + i * 0x10, SeekOrigin.Begin);
|
|
writer.Write((ushort)(lastKeyOffset - Header.KeyTableStart));
|
|
|
|
|
|
writer.Write((ushort)entry.Indextable.param_data_fmt);
|
|
|
|
writer.Write(entry.Indextable.param_data_len);
|
|
writer.Write(entry.Indextable.param_data_max_len);
|
|
writer.Write(lastValueOffset - Header.DataTableStart);
|
|
|
|
writer.BaseStream.Seek(lastKeyOffset, SeekOrigin.Begin);
|
|
writer.Write(utf8.GetBytes(entry.Name));
|
|
writer.Write((byte)0);
|
|
lastKeyOffset = (int)writer.BaseStream.Position;
|
|
|
|
writer.BaseStream.Seek(lastValueOffset, SeekOrigin.Begin);
|
|
writer.Write(entry.ValueBuffer);
|
|
lastValueOffset = (int)writer.BaseStream.Position;
|
|
}
|
|
|
|
//I'm doing this to just rewrite the first item (Some Cleanup will be needed)
|
|
//Or maybe not as when I checked this gives a 1 - 1 match with how the Sony tool works
|
|
//we need to rewrite that first item (PS4/PS3/PSV should be APP-VER)
|
|
lastKeyOffset = Convert.ToInt32(Header.KeyTableStart);
|
|
lastValueOffset = Convert.ToInt32(Header.DataTableStart);
|
|
|
|
var tableentry = Tables[0];
|
|
|
|
writer.BaseStream.Seek(lastKeyOffset, SeekOrigin.Begin);
|
|
writer.Write(utf8.GetBytes(tableentry.Name));
|
|
writer.Write((byte)0);
|
|
lastKeyOffset = (int)writer.BaseStream.Position;
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private string ReadValue(BinaryReader br, index_table table)
|
|
{
|
|
br.BaseStream.Position = ((Header.DataTableStart) + table.param_data_offset);
|
|
switch (table.param_data_fmt)
|
|
{
|
|
case FMT.ASCII:
|
|
//return Encoding.GetEncoding(1252).GetString(br.ReadBytes((int) table.param_data_max_len)).Replace("\0", "");
|
|
return Encoding.UTF8.GetString(br.ReadBytes((int)table.param_data_max_len)).Replace("\0", "");
|
|
case FMT.UINT32:
|
|
return br.ReadUInt32().ToString();
|
|
case FMT.UTF_8:
|
|
return Encoding.UTF8.GetString(br.ReadBytes((int)table.param_data_max_len)).Replace("\0", "");
|
|
case FMT.Utf8Null:
|
|
return Encoding.UTF8.GetString(br.ReadBytes((int)table.param_data_max_len)).Replace("\0", "");
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string ReadValueSpecialChars(BinaryReader br, index_table table)
|
|
{
|
|
br.BaseStream.Position = ((Header.DataTableStart) + table.param_data_offset);
|
|
switch (table.param_data_fmt)
|
|
{
|
|
case FMT.ASCII:
|
|
return Encoding.UTF8.GetString(br.ReadBytes((int)table.param_data_max_len)).Replace("\0", "");
|
|
case FMT.UINT32:
|
|
return br.ReadUInt32().ToString();
|
|
case FMT.UTF_8:
|
|
return Encoding.UTF8.GetString(br.ReadBytes((int)table.param_data_max_len)).Replace("\0", "");
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string ReadName(BinaryReader br, index_table table)
|
|
{
|
|
br.BaseStream.Position = (Header.KeyTableStart + table.param_key_offset);
|
|
string name = "";
|
|
while (((byte)br.PeekChar()) != 0)
|
|
name += br.ReadChar();
|
|
br.BaseStream.Position++;
|
|
return name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start Reading the Parameter file
|
|
/// </summary>
|
|
/// <param name="input">Input Stream</param>
|
|
private void Init(Stream input)
|
|
{
|
|
using (var br = new BinaryReader(input))
|
|
{
|
|
Header.Read(br);
|
|
var tables = new List<index_table>();
|
|
for (int i = 0; i < Header.IndexTableEntries; i++)
|
|
{
|
|
var t = new index_table();
|
|
t.Read(br);
|
|
tables.Add(t);
|
|
}
|
|
var xtables = new List<Table>();
|
|
int count = 0;
|
|
foreach (index_table t in tables)
|
|
{
|
|
var x = new Table();
|
|
x.index = count;
|
|
x.Indextable = t;
|
|
x.Name = ReadName(br, t);
|
|
x.Value = ReadValue(br, t);
|
|
count++;
|
|
xtables.Add(x);
|
|
}
|
|
Tables = xtables;
|
|
br.Close();
|
|
}
|
|
}
|
|
|
|
|
|
#endregion << Methods >>
|
|
|
|
}
|
|
} |