Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions SabreTools.Serialization.Test/Readers/SkuSisTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Readers;
using Xunit;

namespace SabreTools.Serialization.Test.Readers
{
public class SkuSisTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var deserializer = new SkuSis();

var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}

[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var deserializer = new SkuSis();

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 SkuSis();

var actual = deserializer.Deserialize(data, offset);
Assert.Null(actual);
}

[Fact]
public void NullStream_Null()
{
Stream? data = null;
var deserializer = new SkuSis();

var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}

[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var deserializer = new SkuSis();

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 SkuSis();

var actual = deserializer.Deserialize(data);
Assert.Null(actual);
}
}
}
61 changes: 61 additions & 0 deletions SabreTools.Serialization.Test/Wrappers/SkuSisTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.IO;
using System.Linq;
using SabreTools.Serialization.Wrappers;
using Xunit;

namespace SabreTools.Serialization.Test.Wrappers
{
public class SkuSisTests
{
[Fact]
public void NullArray_Null()
{
byte[]? data = null;
int offset = 0;
var actual = SkuSis.Create(data, offset);
Assert.Null(actual);
}

[Fact]
public void EmptyArray_Null()
{
byte[]? data = [];
int offset = 0;
var actual = SkuSis.Create(data, offset);
Assert.Null(actual);
}

[Fact]
public void InvalidArray_Null()
{
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
int offset = 0;
var actual = SkuSis.Create(data, offset);
Assert.Null(actual);
}

[Fact]
public void NullStream_Null()
{
Stream? data = null;
var actual = SkuSis.Create(data);
Assert.Null(actual);
}

[Fact]
public void EmptyStream_Null()
{
Stream? data = new MemoryStream([]);
var actual = SkuSis.Create(data);
Assert.Null(actual);
}

[Fact]
public void InvalidStream_Null()
{
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
var actual = SkuSis.Create(data);
Assert.Null(actual);
}
}
}
19 changes: 19 additions & 0 deletions SabreTools.Serialization/Models/VDF/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace SabreTools.Data.Models.VDF
{
public static class Constants
{
/// <summary>
/// Top-level item (and thus also first 5 bytes) of Steam2 (sis/sim/sid) retail installers
/// </summary>
public static readonly byte[] Steam2SisSignatureBytes = [0x22, 0x53, 0x4B, 0x55, 0x22]; // "SKU"

public static readonly string Steam2SisSignatureString = "\"SKU\"";

/// <summary>
/// Top-level item (and thus also first 5 bytes) of Steam3 (sis/csm/csd) retail installers
/// </summary>
public static readonly byte[] Steam3SisSignatureBytes = [0x22, 0x73, 0x6B, 0x75, 0x22]; // "sku"

public static readonly string Steam3SisSignatureString = "\"sku\"";
}
}
25 changes: 25 additions & 0 deletions SabreTools.Serialization/Models/VDF/File.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Newtonsoft.Json.Linq;

namespace SabreTools.Data.Models.VDF
{
/// <summary>
/// Valve Data File
/// </summary>
/// <remarks>
/// Valve's json-like format, used for a variety of things across Steam.
/// </remarks>
/// <see href="https://github.com/ValveResourceFormat/ValveKeyValue"/>
/// <see href="https://developer.valvesoftware.com/wiki/VDF"/>
public class File
{
/// <summary>
/// A byte array representing the signature/top level item.
/// </summary>
public byte[]? Signature { get; set; }

/// <summary>
/// A JSON Object representing the VDF structure.
/// </summary>
public JObject? VDFObject { get; set; }
}
}
114 changes: 114 additions & 0 deletions SabreTools.Serialization/Readers/SkuSis.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using File = SabreTools.Data.Models.VDF.File;
using static SabreTools.Data.Models.VDF.Constants;
using System.Text;
using Newtonsoft.Json.Linq;

namespace SabreTools.Serialization.Readers
{
/// <remarks>
/// The VDF file format was used for a very wide scope of functions on steam. At the moment, VDF file support is
/// only needed when it comes to parsing retail sku sis files, so the current parser is only aimed at supporting
/// these files, as they're overall very consistent, and trying to test every usage of VDF files would be extremely
/// time-consuming for little benefit. If parsing other usages of VDF files ever becomes necessary, this should be
/// replaced with a general-purpose VDF parser.
/// Most observations about sku sis files described here probably also apply to VDF files.
/// </remarks>
public class SkuSis : BaseBinaryReader<File>
{
/// <inheritdoc/>
public override File? Deserialize(Stream? data)
{
// If the data is invalid
if (data == null || !data.CanRead)
return null;

try
{
// Cache the current offset
long initialOffset = data.Position;

// Check if file contains the top level sku value, otherwise return null
var signatureBytes = data.ReadBytes(5);
if (!signatureBytes.EqualsExactly(Steam2SisSignatureBytes)
&& !signatureBytes.EqualsExactly(Steam3SisSignatureBytes))
return null;

data.SeekIfPossible(initialOffset, SeekOrigin.Begin);

var skuSis = ParseSkuSis(data);
if (skuSis == null)
return null;

if (skuSis.VDFObject == null)
return null;

skuSis.Signature = signatureBytes;

return skuSis;
}
catch
{
// Ignore the actual error
return null;
}
}

/// <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 File? ParseSkuSis(Stream data)
{
var obj = new File();

string json = "{\n"; // Sku sis files have no surrounding curly braces, which json doesn't allow
string delimiter = "\"\t\t\""; // KVPs are always quoted, and are delimited by two tabs
string? line;
var reader = new StreamReader(data, Encoding.ASCII);

while (!reader.EndOfStream)
{
line = reader.ReadLine();
if (line == null)
continue;

// Curly braces are always on their own lines
if (line.Contains("{"))
{
json += "{\n";
continue;
}
else if (line.Contains("}"))
{
json += line;
json += ",\n";
continue;
}

int index = line.IndexOf(delimiter, StringComparison.Ordinal);

// If the delimiter isn't found, this is the start of an object with multiple KVPs and the next line
// will be an opening curly brace line.
if (index <= -1)
{
json += line;
json += ": ";
}
else // If the delimiter is found, it's just a normal KVP
{
json += line.Replace(delimiter, "\": \"");
json += ",\n";
}
}

json += "\n}";
obj.VDFObject = JObject.Parse(json);

return obj;
}
}
}
10 changes: 10 additions & 0 deletions SabreTools.Serialization/WrapperFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public static class WrapperFactory
WrapperType.SecuROMDFA => SecuROMDFA.Create(data),
WrapperType.SevenZip => SevenZip.Create(data),
WrapperType.Skeleton => Skeleton.Create(data),
WrapperType.SkuSis => SkuSis.Create(data),
WrapperType.SFFS => SFFS.Create(data),
WrapperType.SGA => SGA.Create(data),
WrapperType.TapeArchive => TapeArchive.Create(data),
Expand Down Expand Up @@ -671,6 +672,15 @@ public static WrapperType GetFileType(byte[]? magic, string? extension)
return WrapperType.SFFS;

#endregion

#region SkuSis

// TODO: add description
if (magic.StartsWith(Data.Models.VDF.Constants.Steam2SisSignatureBytes)
|| magic.StartsWith(Data.Models.VDF.Constants.Steam3SisSignatureBytes))
return WrapperType.SkuSis;

#endregion

#region SGA

Expand Down
Loading