mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
Add sim parsing for Steam2 sim/sid installers (#95)
* Add model for sim file * Add sim parsing for steam2 installer set * First round of fixes
This commit is contained in:
committed by
GitHub
parent
a602686551
commit
79ca9f617e
10
SabreTools.Data.Models/Steam2Installer/Constants.cs
Normal file
10
SabreTools.Data.Models/Steam2Installer/Constants.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SabreTools.Data.Models.Steam2Installer
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public static readonly byte[] SimSignatureBytes = [0x1F, 0x4C, 0xD0, 0x3F];
|
||||
|
||||
/// <remarks>All other values in the structure are little endian, assuming this is too</remarks>
|
||||
public const uint SimSignatureUInt32 = 0x3FD04C1F;
|
||||
}
|
||||
}
|
||||
71
SabreTools.Data.Models/Steam2Installer/FileEntry.cs
Normal file
71
SabreTools.Data.Models/Steam2Installer/FileEntry.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SabreTools.Data.Models.Steam2Installer
|
||||
{
|
||||
|
||||
public sealed class FileEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Offset of the filename in the string bytes array
|
||||
/// </summary>
|
||||
public uint NameOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset of the path name in the string bytes array
|
||||
/// </summary>
|
||||
public uint PathOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Steam depot id of the depot the file is a part of
|
||||
/// </summary>
|
||||
public uint DepotId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset of the file within its sid volume
|
||||
/// </summary>
|
||||
public ulong Offset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the file, in bytes
|
||||
/// </summary>
|
||||
public ulong Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which # installer disc the file is on
|
||||
/// </summary>
|
||||
public byte DiscNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which # sid volume the file is in
|
||||
/// </summary>
|
||||
public byte VolumeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the file is encrypted or not
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This flag isn't entirely reliable, as sometimes the file is encrypted in the sid without this
|
||||
/// flag being set in the sid. The reverse is not currently known to be true, though, so there at
|
||||
/// least shouldn't be any false positives when using this flag.
|
||||
/// </remarks>
|
||||
public bool Encrypted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unknown 1-byte value, likely just padding
|
||||
/// </summary>
|
||||
public byte Unknown { get; set; }
|
||||
|
||||
// The filename and path aren't stored in the fileentry structure in the .sim, but the offsets are,
|
||||
// so it just makes sense to also parse and store the strings for the file here too.
|
||||
|
||||
/// <summary>
|
||||
/// The filename
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The file path
|
||||
/// </summary>
|
||||
public string Path { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
30
SabreTools.Data.Models/Steam2Installer/Header.cs
Normal file
30
SabreTools.Data.Models/Steam2Installer/Header.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SabreTools.Data.Models.Steam2Installer
|
||||
{
|
||||
/// <summary>
|
||||
/// Header from the .sim file.
|
||||
/// </summary>
|
||||
public sealed class Header
|
||||
{
|
||||
/// <summary>
|
||||
/// .sim file magic value
|
||||
/// </summary>
|
||||
public uint Magic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version (0x00000001)
|
||||
/// </summary>
|
||||
public uint Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of discs
|
||||
/// </summary>
|
||||
public uint Discs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the section containing file and path strings.
|
||||
/// </summary>
|
||||
public uint StringsSize { get; set; }
|
||||
}
|
||||
}
|
||||
31
SabreTools.Data.Models/Steam2Installer/SimFile.cs
Normal file
31
SabreTools.Data.Models/Steam2Installer/SimFile.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace SabreTools.Data.Models.Steam2Installer
|
||||
{
|
||||
/// <summary>
|
||||
/// .sim file for a .sim/.sid archive
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// While it is not known for sure what .sim stands for, it's assumed to mean Steam Installer Manifest.
|
||||
/// This contains information about the .sid data volumes. It's used for retail installer discs for
|
||||
/// Steam2 (Steam pre-Steam3/SteamPipe). Preceded by GCF (GCF and NCF are still used for actual game
|
||||
/// installs on Steam2, sim/sid just replaced GCF for retail discs. NCF was never directly stored on
|
||||
/// retail discs since sim/sid had already come out). Succeeded by csm/csd for Steam3/SteamPipe.
|
||||
/// Code is based on NickNine's python-based sim/sid extractor at https://pastebin.com/sj6F64XP
|
||||
/// While this was not referenced for any of the code in SabreTools, https://codeberg.org/CYBERDEV/SIDEx
|
||||
/// provides additional documentation if it is needed.
|
||||
/// </remarks>
|
||||
public sealed class SimFile
|
||||
{
|
||||
/// <summary>
|
||||
/// .sim file header
|
||||
/// </summary>
|
||||
public Header Header { get; set; } = new();
|
||||
|
||||
// The string bytes array would go here, but it gets parsed out into FileEntries during deserialization,
|
||||
// so there's no point storing it in the model. Same with the file entries size and count.
|
||||
|
||||
/// <summary>
|
||||
/// Array of file entries
|
||||
/// </summary>
|
||||
public FileEntry[] FileEntries { get; set; } = [];
|
||||
}
|
||||
}
|
||||
15
SabreTools.Data.Models/Steam2Installer/Steam2InstallerSet.cs
Normal file
15
SabreTools.Data.Models/Steam2Installer/Steam2InstallerSet.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace SabreTools.Data.Models.Steam2Installer
|
||||
{
|
||||
public class Steam2InstallerSet
|
||||
{
|
||||
/// <summary>
|
||||
/// The sim file
|
||||
/// </summary>
|
||||
public SimFile Sim { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The set of sid file volumes
|
||||
/// </summary>
|
||||
// public SidFile[] Sid { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Serialization.Readers.Test
|
||||
{
|
||||
public class Steam2InstallerTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var deserializer = new Steam2Installer();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var deserializer = new Steam2Installer();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var deserializer = new Steam2Installer();
|
||||
|
||||
var actual = deserializer.Deserialize(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var deserializer = new Steam2Installer();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var deserializer = new Steam2Installer();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var deserializer = new Steam2Installer();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
SabreTools.Serialization.Readers/Steam2Installer.cs
Normal file
172
SabreTools.Serialization.Readers/Steam2Installer.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.Steam2Installer;
|
||||
using SabreTools.IO.Extensions;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
using SabreTools.Text.Extensions;
|
||||
|
||||
namespace SabreTools.Serialization.Readers
|
||||
{
|
||||
public class Steam2Installer : BaseBinaryReader<Steam2InstallerSet>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Steam2InstallerSet? Deserialize(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is null || !data.CanRead)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Cache the current offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
// Create a new Steam2 Installer set to populate
|
||||
var si = new Steam2InstallerSet();
|
||||
|
||||
// Try to parse the .sim file
|
||||
var sim = ParseSim(data);
|
||||
|
||||
// Make sure the sim was parsed properly
|
||||
if (sim == null)
|
||||
return null;
|
||||
|
||||
// Set the sim file
|
||||
si.Sim = sim;
|
||||
|
||||
return si;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore the actual error
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Sim file
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Sim on success, null on error</returns>
|
||||
public static SimFile? ParseSim(Stream data)
|
||||
{
|
||||
// Create a new Sim file to populate
|
||||
var si = new SimFile();
|
||||
|
||||
// Try to parse the .sim file header
|
||||
var header = ParseHeader(data);
|
||||
|
||||
// Make sure the magic is correct
|
||||
if (header.Magic != Constants.SimSignatureUInt32)
|
||||
return null;
|
||||
|
||||
// Make sure the version is correct
|
||||
if (header.Version != 1)
|
||||
return null;
|
||||
|
||||
// Make sure string bytes section doesn't go beyond EOF
|
||||
if (header.StringsSize > data.Length - data.Position)
|
||||
return null;
|
||||
|
||||
// Set the header
|
||||
si.Header = header;
|
||||
|
||||
// Read the string section bytes
|
||||
byte[] stringsBytes = data.ReadBytes((int)header.StringsSize);
|
||||
|
||||
// Read the size and number of file entries
|
||||
uint fileEntriesSize = data.ReadUInt32LittleEndian();
|
||||
uint fileEntriesCount = data.ReadUInt32LittleEndian();
|
||||
|
||||
// Make sure reading the file entries won't go beyond EOF
|
||||
if (fileEntriesSize > data.Length - data.Position)
|
||||
return null;
|
||||
|
||||
// Try to parse the entry table
|
||||
var table = ParseTable(data, fileEntriesCount, stringsBytes);
|
||||
if (table is null || table.Length != fileEntriesCount)
|
||||
return null;
|
||||
|
||||
// Set the entry table
|
||||
si.FileEntries = table;
|
||||
|
||||
return si;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Header on success, null on error</returns>
|
||||
public static Header ParseHeader(Stream data)
|
||||
{
|
||||
var obj = new Header();
|
||||
|
||||
obj.Magic = data.ReadUInt32LittleEndian();
|
||||
obj.Version = data.ReadUInt32LittleEndian();
|
||||
obj.Discs = data.ReadUInt32LittleEndian();
|
||||
obj.StringsSize = data.ReadUInt32LittleEndian();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an entry table
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <param name="count">Number of entries to parse</param>
|
||||
/// <returns>Filled entry table on success, null on error</returns>
|
||||
public static FileEntry[]? ParseTable(Stream data, uint count, byte[] stringsBytes)
|
||||
{
|
||||
if (count == 0)
|
||||
return [];
|
||||
|
||||
var obj = new FileEntry[count];
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
var fileEntry = ParseFileEntry(data, stringsBytes);
|
||||
if (fileEntry is null)
|
||||
return null;
|
||||
|
||||
obj[i] = fileEntry;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a FileEntry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled FileEntry on success, null on error</returns>
|
||||
public static FileEntry? ParseFileEntry(Stream data, byte[] stringsBytes)
|
||||
{
|
||||
var obj = new FileEntry();
|
||||
|
||||
obj.NameOffset = data.ReadUInt32LittleEndian();
|
||||
obj.PathOffset = data.ReadUInt32LittleEndian();
|
||||
obj.DepotId = data.ReadUInt32LittleEndian();
|
||||
obj.Offset = data.ReadUInt64LittleEndian();
|
||||
obj.Size = data.ReadUInt64LittleEndian();
|
||||
obj.DiscNumber = data.ReadByteValue();
|
||||
obj.VolumeNumber = data.ReadByteValue();
|
||||
obj.Encrypted = data.ReadByte() != 0;
|
||||
obj.Unknown = data.ReadByteValue();
|
||||
|
||||
// Read file path and name strings
|
||||
int nameOffset = (int)obj.NameOffset;
|
||||
string? name = stringsBytes.ReadNullTerminatedAnsiString(ref nameOffset); // Unsure on encoding, assumed ascii/ansi
|
||||
if (name is null)
|
||||
return null;
|
||||
|
||||
obj.Name = name;
|
||||
int pathOffset = (int)obj.PathOffset;
|
||||
string? path = stringsBytes.ReadNullTerminatedAnsiString(ref pathOffset); // Unsure on encoding, assumed ascii/ansi
|
||||
if (path is null)
|
||||
return null;
|
||||
|
||||
obj.Path = path;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
SabreTools.Wrappers.Test/Steam2InstallerTests.cs
Normal file
60
SabreTools.Wrappers.Test/Steam2InstallerTests.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Wrappers.Test
|
||||
{
|
||||
public class Steam2InstallerTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var actual = Steam2Installer.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var actual = Steam2Installer.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var actual = Steam2Installer.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var actual = Steam2Installer.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var actual = Steam2Installer.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var actual = Steam2Installer.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
SabreTools.Wrappers/Steam2Installer.cs
Normal file
97
SabreTools.Wrappers/Steam2Installer.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.Steam2Installer;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class Steam2Installer : WrapperBase<Steam2InstallerSet>
|
||||
{
|
||||
#region Descriptive Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DescriptionString => "Steam2 Installer Set";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extension Properties
|
||||
|
||||
/// <inheritdoc cref="Steam2InstallerSet.Sim"/>
|
||||
public SimFile Sim => Model.Sim;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Steam2Installer(Steam2InstallerSet model, byte[] data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Steam2Installer(Steam2InstallerSet model, byte[] data, int offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Steam2Installer(Steam2InstallerSet model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Steam2Installer(Steam2InstallerSet model, Stream data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Steam2Installer(Steam2InstallerSet model, Stream data, long offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Steam2Installer(Steam2InstallerSet model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Create a Steam2 Installer Set from a byte array and offset
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array representing the sim file</param>
|
||||
/// <param name="offset">Offset within the array to parse</param>
|
||||
/// <returns>A Steam2 Installer Set wrapper on success, null on failure</returns>
|
||||
public static Steam2Installer? Create(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is null || data.Length == 0)
|
||||
return null;
|
||||
|
||||
// If the offset is out of bounds
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Create a memory stream and use that
|
||||
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
||||
return Create(dataStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Steam2 Installer Set from a Stream
|
||||
/// </summary>
|
||||
/// <param name="data">Stream representing the sim file</param>
|
||||
/// <returns>A Steam2 Installer Set wrapper on success, null on failure</returns>
|
||||
public static Steam2Installer? Create(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data is null || !data.CanRead)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
var model = new Serialization.Readers.Steam2Installer().Deserialize(data);
|
||||
if (model is null)
|
||||
return null;
|
||||
|
||||
return new Steam2Installer(model, data, currentOffset);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ namespace SabreTools.Wrappers
|
||||
WrapperType.SkuSis => SkuSis.Create(data),
|
||||
WrapperType.SFFS => SFFS.Create(data),
|
||||
WrapperType.SGA => SGA.Create(data),
|
||||
WrapperType.Steam2Installer => Steam2Installer.Create(data),
|
||||
WrapperType.STFS => STFS.Create(data),
|
||||
WrapperType.TapeArchive => TapeArchive.Create(data),
|
||||
WrapperType.VBSP => VBSP.Create(data),
|
||||
@@ -938,6 +939,13 @@ namespace SabreTools.Wrappers
|
||||
|
||||
#endregion
|
||||
|
||||
#region Steam2Installer
|
||||
|
||||
if (magic.StartsWith(Data.Models.Steam2Installer.Constants.SimSignatureBytes))
|
||||
return WrapperType.Steam2Installer;
|
||||
|
||||
#endregion
|
||||
|
||||
#region STFS
|
||||
|
||||
// LIVE
|
||||
|
||||
@@ -257,6 +257,11 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
SkuSis,
|
||||
|
||||
/// <summary>
|
||||
/// Steam2 Installer set (sim/sid)
|
||||
/// </summary>
|
||||
Steam2Installer,
|
||||
|
||||
/// <summary>
|
||||
/// Secure Transacted File System
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user