mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-04-20 05:03:11 +00:00
Xbox ISO Support (#81)
* Xbox ISO support * Fixes * Print XGDType * Review
This commit is contained in:
@@ -18,6 +18,13 @@ namespace SabreTools.Data.Models.XDVDFS
|
||||
/// </summary>
|
||||
public const int MinimumRecordLength = 14;
|
||||
|
||||
/// <summary>
|
||||
/// Volume Descriptor magic bytes at start of sector 32
|
||||
/// </summary>
|
||||
public static readonly byte[] VolumeDescriptorMagic = [0x4D, 0x49, 0x43, 0x52, 0x4F, 0x53, 0x4F, 0x46,
|
||||
0x54, 0x2A, 0x58, 0x42, 0x4F, 0x58, 0x2A, 0x4D,
|
||||
0x45, 0x44, 0x49, 0x41];
|
||||
|
||||
/// <summary>
|
||||
/// Volume Descriptor signature at start of sector 32
|
||||
/// </summary>
|
||||
|
||||
37
SabreTools.Data.Models/XboxISO/Constants.cs
Normal file
37
SabreTools.Data.Models/XboxISO/Constants.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace SabreTools.Data.Models.XboxISO
|
||||
{
|
||||
/// <see href="https://github.dev/Deterous/XboxKit/"/>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Known redump ISO lengths
|
||||
/// 0 = XGD1
|
||||
/// 1/2/3/4 = XGD2
|
||||
/// 5 = XGD2-Hybrid
|
||||
/// 6/7 = XGD3
|
||||
/// </summary>
|
||||
/// <see href="https://github.dev/Deterous/XboxKit/"/>
|
||||
public static readonly long[] RedumpIsoLengths = [0x1D26A8000, 0x1D3301800, 0x1D2FEF800, 0x1D3082000,
|
||||
0x1D3390000, 0x1D31A0000, 0x208E05800, 0x208E03800];
|
||||
|
||||
/// <summary>
|
||||
/// Known XISO offsets into redump ISOs
|
||||
/// 0 = XGD1
|
||||
/// 1 = XGD2
|
||||
/// 2 = XGD2-Hybrid
|
||||
/// 3 = XGD3
|
||||
/// </summary>
|
||||
/// <see href="https://github.dev/Deterous/XboxKit/"/>
|
||||
public static readonly long[] XisoOffsets = [0x18300000, 0xFD90000, 0x89D80000, 0x2080000];
|
||||
|
||||
/// <summary>
|
||||
/// Known XISO lengths from redump ISOs
|
||||
/// 0 = XGD1
|
||||
/// 1 = XGD2
|
||||
/// 2 = XGD2-Hybrid
|
||||
/// 3 = XGD3
|
||||
/// </summary>
|
||||
/// <see href="https://github.dev/Deterous/XboxKit/"/>
|
||||
public static readonly long[] XisoLengths = [0x1A2DB0000, 0x1B3880000, 0xBF8A0000, 0x204510000];
|
||||
}
|
||||
}
|
||||
29
SabreTools.Data.Models/XboxISO/XboxISO.cs
Normal file
29
SabreTools.Data.Models/XboxISO/XboxISO.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace SabreTools.Data.Models.XboxISO
|
||||
{
|
||||
/// <summary>
|
||||
/// Xbox / Xbox 360 disc image with a video partition (ISO9660) and a game partition (XDVDFS)
|
||||
/// There exists zeroed gaps before/after the start/end of the game partition
|
||||
/// </summary>
|
||||
public class DiscImage
|
||||
{
|
||||
/// <summary>
|
||||
/// ISO9660 Video parititon, split across start and end of Disc Image
|
||||
/// </summary>
|
||||
public SabreTools.Data.Models.ISO9660.Volume VideoPartition { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// XGD type present in game partition
|
||||
/// 0 = XGD1
|
||||
/// 1 = XGD2
|
||||
/// 2 = XGD2-Hybrid
|
||||
/// 3 = XGD3
|
||||
/// </summary>
|
||||
/// <remarks>This field is not actually present, but is an abstract representation of the offset/length of the game partition</remarks>
|
||||
public int XGDType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// XDVDFS Game partition, present in middle of Disc Image
|
||||
/// </summary>
|
||||
public SabreTools.Data.Models.XDVDFS.Volume GamePartition { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ namespace SabreTools.Serialization.Readers
|
||||
|
||||
data.SeekIfPossible(initialOffset + (((long)offset) * Constants.SectorSize), SeekOrigin.Begin);
|
||||
long curPosition;
|
||||
while (size > data.Position - (((long)offset) * Constants.SectorSize))
|
||||
while (size > data.Position - (initialOffset + ((long)offset) * Constants.SectorSize))
|
||||
{
|
||||
curPosition = data.Position;
|
||||
var dr = ParseDirectoryRecord(data);
|
||||
@@ -206,9 +206,9 @@ namespace SabreTools.Serialization.Readers
|
||||
records.Add(dr);
|
||||
|
||||
// If invalid record read or next descriptor cannot fit in the current sector, skip ahead
|
||||
if (dr is null || data.Position % Constants.SectorSize > (Constants.SectorSize - Constants.MinimumRecordLength))
|
||||
if (dr is null || (data.Position - initialOffset) % Constants.SectorSize > (Constants.SectorSize - Constants.MinimumRecordLength))
|
||||
{
|
||||
data.Position += Constants.SectorSize - (int)(data.Position % Constants.SectorSize);
|
||||
data.SeekIfPossible(Constants.SectorSize - (int)((data.Position - initialOffset) % Constants.SectorSize), SeekOrigin.Current);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
60
SabreTools.Wrappers.Test/XboxISOTests.cs
Normal file
60
SabreTools.Wrappers.Test/XboxISOTests.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.Wrappers.Test
|
||||
{
|
||||
public class XboxISOTests
|
||||
{
|
||||
[Fact]
|
||||
public void NullArray_Null()
|
||||
{
|
||||
byte[]? data = null;
|
||||
int offset = 0;
|
||||
var actual = XboxISO.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyArray_Null()
|
||||
{
|
||||
byte[]? data = [];
|
||||
int offset = 0;
|
||||
var actual = XboxISO.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidArray_Null()
|
||||
{
|
||||
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||
int offset = 0;
|
||||
var actual = XboxISO.Create(data, offset);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullStream_Null()
|
||||
{
|
||||
Stream? data = null;
|
||||
var actual = XboxISO.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([]);
|
||||
var actual = XboxISO.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStream_Null()
|
||||
{
|
||||
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||
var actual = XboxISO.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace SabreTools.Wrappers
|
||||
WrapperType.InstallShieldArchiveV3 => InstallShieldArchiveV3.Create(data),
|
||||
WrapperType.InstallShieldCAB => InstallShieldCabinet.Create(data),
|
||||
WrapperType.IRD => IRD.Create(data),
|
||||
WrapperType.ISO9660 => ISO9660.Create(data),
|
||||
WrapperType.ISO9660 => CreateDiscImageWrapper(data),
|
||||
WrapperType.LDSCRYPT => LDSCRYPT.Create(data),
|
||||
WrapperType.LZKWAJ => LZKWAJ.Create(data),
|
||||
WrapperType.LZQBasic => LZQBasic.Create(data),
|
||||
@@ -99,13 +99,20 @@ namespace SabreTools.Wrappers
|
||||
// Cache the current offset
|
||||
long initialOffset = stream.Position;
|
||||
|
||||
// Try to get an ISO-9660 wrapper first
|
||||
var wrapper = ISO9660.Create(stream);
|
||||
if (wrapper is null || wrapper is not ISO9660 iso)
|
||||
// Try to get an Xbox ISO wrapper first
|
||||
var xboxWrapper = XboxISO.Create(stream);
|
||||
if (xboxWrapper is not null && xboxWrapper is XboxISO xboxISO)
|
||||
return xboxWrapper;
|
||||
|
||||
// Reset position in stream
|
||||
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
|
||||
|
||||
// Fallback to standard ISO9660 wrapper
|
||||
var isoWrapper = ISO9660.Create(stream);
|
||||
if (isoWrapper is null || isoWrapper is not ISO9660 iso)
|
||||
return null;
|
||||
|
||||
// TODO: Fill in the rest
|
||||
return wrapper;
|
||||
return isoWrapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
27
SabreTools.Wrappers/XboxISO.Extraction.cs
Normal file
27
SabreTools.Wrappers/XboxISO.Extraction.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.XboxISO;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class XboxISO : IExtractable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Extract(string outputDirectory, bool includeDebug)
|
||||
{
|
||||
long initialOffset = _dataSource.Position;
|
||||
|
||||
// Extract all files from the video partition
|
||||
var videoWrapper = new SabreTools.Wrappers.ISO9660(VideoPartition, _dataSource, initialOffset, _dataSource.Length);
|
||||
bool success = videoWrapper?.Extract(outputDirectory, includeDebug) ?? false;
|
||||
|
||||
// Extract all files from the game partition
|
||||
var gameWrapper = new SabreTools.Wrappers.XDVDFS(GamePartition, _dataSource, initialOffset + Constants.XisoOffsets[XGDType], Constants.XisoLengths[XGDType]);
|
||||
success |= gameWrapper?.Extract(outputDirectory, includeDebug) ?? false;
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
SabreTools.Wrappers/XboxISO.Printing.cs
Normal file
49
SabreTools.Wrappers/XboxISO.Printing.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.XboxISO;
|
||||
using SabreTools.Text.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class XboxISO : IPrintable
|
||||
{
|
||||
#if NETCOREAPP
|
||||
/// <inheritdoc/>
|
||||
public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void PrintInformation(StringBuilder builder)
|
||||
{
|
||||
builder.AppendLine("Xbox / Xbox 360 Disc Image Information:");
|
||||
builder.AppendLine("-------------------------");
|
||||
builder.AppendLine();
|
||||
|
||||
// Custom XGD type string
|
||||
string xgdType = "XGD?";
|
||||
if (XGDType == 0)
|
||||
xgdType = "XGD1 (Xbox)";
|
||||
else if (XGDType == 1)
|
||||
xgdType = "XGD2 (Xbox 360)";
|
||||
else if (XGDType == 2)
|
||||
xgdType = "XGD2 (Xbox 360 / DVD-Video Hybrid)";
|
||||
else if (XGDType == 3)
|
||||
xgdType = "XGD3 (Xbox 360)";
|
||||
|
||||
builder.AppendLine(xgdType, "XGD Type");
|
||||
builder.AppendLine();
|
||||
|
||||
long initialOffset = _dataSource.Position;
|
||||
|
||||
// Print all information of video partition model
|
||||
var videoWrapper = new SabreTools.Wrappers.ISO9660(VideoPartition, _dataSource, initialOffset, _dataSource.Length);
|
||||
if (videoWrapper is not null)
|
||||
videoWrapper.PrintInformation(builder);
|
||||
|
||||
// Print all information of game partition model
|
||||
var gameWrapper = new SabreTools.Wrappers.XDVDFS(GamePartition, _dataSource, initialOffset + Constants.XisoOffsets[XGDType], Constants.XisoLengths[XGDType]);
|
||||
if (gameWrapper is not null)
|
||||
gameWrapper.PrintInformation(builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
SabreTools.Wrappers/XboxISO.cs
Normal file
154
SabreTools.Wrappers/XboxISO.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.XboxISO;
|
||||
using SabreTools.IO;
|
||||
using SabreTools.IO.Extensions;
|
||||
using SabreTools.Matching;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class XboxISO : WrapperBase<DiscImage>
|
||||
{
|
||||
#region Descriptive Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DescriptionString => "Xbox / Xbox 360 Disc Image";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extension Properties
|
||||
|
||||
/// <inheritdoc cref="DiscImage.VideoPartition"/>
|
||||
public SabreTools.Data.Models.ISO9660.Volume VideoPartition => Model.VideoPartition;
|
||||
|
||||
/// <inheritdoc cref="DiscImage.XGDType"/>
|
||||
public int XGDType => Model.XGDType;
|
||||
|
||||
/// <inheritdoc cref="DiscImage.GamePartition"/>
|
||||
public SabreTools.Data.Models.XDVDFS.Volume GamePartition => Model.GamePartition;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XboxISO(DiscImage model, byte[] data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XboxISO(DiscImage model, byte[] data, int offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XboxISO(DiscImage model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XboxISO(DiscImage model, Stream data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XboxISO(DiscImage model, Stream data, long offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XboxISO(DiscImage model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Create an XboxISO DiscImage from a byte array and offset
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array representing the XboxISO DiscImage</param>
|
||||
/// <param name="offset">Offset within the array to parse</param>
|
||||
/// <returns>An XboxISO DiscImage on success, null on failure</returns>
|
||||
public static XboxISO? 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 an XboxISO DiscImage from a Stream
|
||||
/// </summary>
|
||||
/// <param name="data">Stream representing the XboxISO DiscImage</param>
|
||||
/// <returns>An XboxISO DiscImage DiscImage on success, null on failure</returns>
|
||||
public static XboxISO? 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;
|
||||
|
||||
// Create new model to fill in
|
||||
var model = new DiscImage();
|
||||
|
||||
// Try to detect XDVDFS partition
|
||||
int redumpType = Array.IndexOf(Constants.RedumpIsoLengths, data.Length);
|
||||
if (redumpType < 0)
|
||||
return null;
|
||||
|
||||
// Determine location/size of game partition based on total ISO size
|
||||
model.XGDType = redumpType switch
|
||||
{
|
||||
0 => 0, // XGD1
|
||||
1 or 2 or 3 or 4 => 1, // XGD2
|
||||
5 => 2, // XGD2 (Hybrid)
|
||||
6 or 7 => 3, // XGD3
|
||||
_ => -1,
|
||||
};
|
||||
|
||||
// Validate XGDType
|
||||
if (model.XGDType < 0)
|
||||
return null;
|
||||
|
||||
// Verify that XDVDFS game parition exists
|
||||
long magicOffset = Constants.XisoOffsets[model.XGDType] + Data.Models.XDVDFS.Constants.SectorSize * Data.Models.XDVDFS.Constants.ReservedSectors;
|
||||
data.SeekIfPossible(currentOffset + magicOffset, SeekOrigin.Begin);
|
||||
var magic = data.ReadBytes(20);
|
||||
if (magic is null)
|
||||
return null;
|
||||
if (!magic.EqualsExactly(Data.Models.XDVDFS.Constants.VolumeDescriptorMagic))
|
||||
return null;
|
||||
|
||||
// Parse the game partition first, more likely to fail
|
||||
data.SeekIfPossible(currentOffset + Constants.XisoOffsets[model.XGDType], SeekOrigin.Begin);
|
||||
var gamePartition = new Serialization.Readers.XDVDFS().Deserialize(data);
|
||||
if (gamePartition is null)
|
||||
return null;
|
||||
|
||||
model.GamePartition = gamePartition;
|
||||
|
||||
// Reset stream position
|
||||
data.SeekIfPossible(currentOffset, SeekOrigin.Begin);
|
||||
|
||||
// Parse the video partition last, more likely to pass
|
||||
var videoPartition = new Serialization.Readers.ISO9660().Deserialize(data);
|
||||
if (videoPartition is null)
|
||||
return null;
|
||||
|
||||
model.VideoPartition = videoPartition;
|
||||
|
||||
return new XboxISO(model, data, currentOffset);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user