mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
Add Opera FileSystem support (#90)
* Add Opera FileSystem support * Fix tests
This commit is contained in:
@@ -60,6 +60,6 @@ namespace SabreTools.Data.Extensions.Test
|
|||||||
Assert.Equal(expected, actual);
|
Assert.Equal(expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Figure out how to add ISO9660Stream tests
|
// TODO: Figure out how to add UserDataStream tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
SabreTools.Data.Extensions.Test/OperaFSExtensionsTests.cs
Normal file
27
SabreTools.Data.Extensions.Test/OperaFSExtensionsTests.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using SabreTools.Data.Models.OperaFS;
|
||||||
|
using SabreTools.Numerics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SabreTools.Data.Extensions.Test
|
||||||
|
{
|
||||||
|
public class OperaFSExtensionsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DirectoryDescriptor_EqualsExactly_Empty()
|
||||||
|
{
|
||||||
|
var dir1 = new DirectoryDescriptor();
|
||||||
|
var dir2 = new DirectoryDescriptor();
|
||||||
|
bool actual = dir1.EqualsExactly(dir2);
|
||||||
|
Assert.True(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DirectoryRecord_EqualsExactly_Empty()
|
||||||
|
{
|
||||||
|
var dr1 = new DirectoryRecord();
|
||||||
|
var dr2 = new DirectoryRecord();
|
||||||
|
bool actual = dr1.EqualsExactly(dr2);
|
||||||
|
Assert.True(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ namespace SabreTools.Data.Extensions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a stream that provides only the user data of a CDROM stream
|
/// Creates a stream that provides only the user data of a CDROM stream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ISO9660Stream : Stream
|
public class UserDataStream : Stream
|
||||||
{
|
{
|
||||||
// Base CDROM stream (2352-byte sector)
|
// Base CDROM stream (2352-byte sector)
|
||||||
private readonly Stream _baseStream;
|
private readonly Stream _baseStream;
|
||||||
@@ -121,7 +121,7 @@ namespace SabreTools.Data.Extensions
|
|||||||
private long _isoSectorSize = Constants.Mode1DataSize;
|
private long _isoSectorSize = Constants.Mode1DataSize;
|
||||||
#pragma warning restore IDE0044
|
#pragma warning restore IDE0044
|
||||||
|
|
||||||
public ISO9660Stream(Stream inputStream)
|
public UserDataStream(Stream inputStream)
|
||||||
{
|
{
|
||||||
if (!inputStream.CanSeek || !inputStream.CanRead)
|
if (!inputStream.CanSeek || !inputStream.CanRead)
|
||||||
throw new ArgumentException("Stream must be readable and seekable.", nameof(inputStream));
|
throw new ArgumentException("Stream must be readable and seekable.", nameof(inputStream));
|
||||||
|
|||||||
79
SabreTools.Data.Extensions/OperaFSExtensions.cs
Normal file
79
SabreTools.Data.Extensions/OperaFSExtensions.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using SabreTools.Data.Models.OperaFS;
|
||||||
|
using SabreTools.Matching;
|
||||||
|
using SabreTools.Numerics;
|
||||||
|
|
||||||
|
namespace SabreTools.Data.Extensions
|
||||||
|
{
|
||||||
|
public static class OperaFSExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Compare two DirectoryDescriptor objects
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dir1">The provided DirectoryDescriptor</param>
|
||||||
|
/// <param name="dir2">The DirectoryDescriptor to compare against</param>
|
||||||
|
/// <returns>True if all fields are equal, otherwise false</returns>
|
||||||
|
public static bool EqualsExactly(this DirectoryDescriptor dir1, DirectoryDescriptor dir2)
|
||||||
|
{
|
||||||
|
if (dir1.NextBlock != dir2.NextBlock)
|
||||||
|
return false;
|
||||||
|
if (dir1.PreviousBlock != dir2.PreviousBlock)
|
||||||
|
return false;
|
||||||
|
if (dir1.Flags != dir2.Flags)
|
||||||
|
return false;
|
||||||
|
if (dir1.FirstFreeByte != dir2.FirstFreeByte)
|
||||||
|
return false;
|
||||||
|
if (dir1.FirstEntryOffset != dir2.FirstEntryOffset)
|
||||||
|
return false;
|
||||||
|
if (dir1.DirectoryRecords.Length != dir2.DirectoryRecords.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (int i = 0; i < dir1.DirectoryRecords.Length; i++)
|
||||||
|
{
|
||||||
|
if (!dir1.DirectoryRecords[i].EqualsExactly(dir2.DirectoryRecords[i]))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compare two DirectoryRecord objects
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dir1">The provided DirectoryRecord</param>
|
||||||
|
/// <param name="dir2">The DirectoryRecord to compare against</param>
|
||||||
|
/// <returns>True if all fields are equal, otherwise false</returns>
|
||||||
|
public static bool EqualsExactly(this DirectoryRecord dr1, DirectoryRecord dr2)
|
||||||
|
{
|
||||||
|
if ((byte)dr1.DirectoryRecordFlags != (byte)dr2.DirectoryRecordFlags)
|
||||||
|
return false;
|
||||||
|
if (!dr1.UniqueIdentifier.EqualsExactly(dr2.UniqueIdentifier))
|
||||||
|
return false;
|
||||||
|
if (!dr1.Type.EqualsExactly(dr2.Type))
|
||||||
|
return false;
|
||||||
|
if (dr1.BlockSize != dr2.BlockSize)
|
||||||
|
return false;
|
||||||
|
if (dr1.ByteCount != dr2.ByteCount)
|
||||||
|
return false;
|
||||||
|
if (dr1.BlockCount != dr2.BlockCount)
|
||||||
|
return false;
|
||||||
|
if (dr1.Burst != dr2.Burst)
|
||||||
|
return false;
|
||||||
|
if (dr1.Gap != dr2.Gap)
|
||||||
|
return false;
|
||||||
|
if (!dr1.Filename.EqualsExactly(dr2.Filename))
|
||||||
|
return false;
|
||||||
|
if (dr1.LastAvatarIndex != dr2.LastAvatarIndex)
|
||||||
|
return false;
|
||||||
|
if (dr1.AvatarList.Length != dr2.AvatarList.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (int i = 0; i < dr1.AvatarList.Length; i++)
|
||||||
|
{
|
||||||
|
if (dr1.AvatarList[i] != dr2.AvatarList[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SabreTools.Data.Models/OperaFS/Constants.cs
Normal file
23
SabreTools.Data.Models/OperaFS/Constants.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace SabreTools.Data.Models.OperaFS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// OperaFS constant values and arrays
|
||||||
|
/// </summary>
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Standard block size for OperaFS disc images
|
||||||
|
/// </summary>
|
||||||
|
public static readonly int SectorSize = 2048;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start of a standard OperaFS image
|
||||||
|
/// </summary>
|
||||||
|
public static readonly byte[] MagicBytes = [0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Padding bytes within a OperaFS FileSystem, "iamaduck"
|
||||||
|
/// </summary>
|
||||||
|
public static readonly byte[] PaddingBytes = [0x69, 0x61, 0x6D, 0x61, 0x64, 0x75, 0x63, 0x6B];
|
||||||
|
}
|
||||||
|
}
|
||||||
41
SabreTools.Data.Models/OperaFS/DirectoryDescriptor.cs
Normal file
41
SabreTools.Data.Models/OperaFS/DirectoryDescriptor.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
namespace SabreTools.Data.Models.OperaFS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// OperaFS Directory Descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://groups.google.com/g/rec.games.video.3do/c/1U3qrmLSYMQ"/>
|
||||||
|
public class DirectoryDescriptor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Offset of the next block
|
||||||
|
/// 0xFFFFFFFF implies this is the last block
|
||||||
|
/// </summary>
|
||||||
|
public int NextBlock { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offset of the previous block
|
||||||
|
/// 0xFFFFFFFF implies this is the first block
|
||||||
|
/// </summary>
|
||||||
|
public int PreviousBlock { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should be zeroed
|
||||||
|
/// </summary>
|
||||||
|
public uint Flags { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First free byte
|
||||||
|
/// </summary>
|
||||||
|
public uint FirstFreeByte { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First entry offset
|
||||||
|
/// </summary>
|
||||||
|
public uint FirstEntryOffset { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directory records in this directory
|
||||||
|
/// </summary>
|
||||||
|
public DirectoryRecord[] DirectoryRecords { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
66
SabreTools.Data.Models/OperaFS/DirectoryRecord.cs
Normal file
66
SabreTools.Data.Models/OperaFS/DirectoryRecord.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
namespace SabreTools.Data.Models.OperaFS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// OperaFS Directory Record
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://groups.google.com/g/rec.games.video.3do/c/1U3qrmLSYMQ"/>
|
||||||
|
public class DirectoryRecord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Flags about this directory record
|
||||||
|
/// </summary>
|
||||||
|
public DirectoryRecordFlags DirectoryRecordFlags { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hash or random value to identify this record
|
||||||
|
/// </summary>
|
||||||
|
public byte[] UniqueIdentifier { get; set; } = new byte[4];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of record, ASCII
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Type { get; set; } = new byte[4];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sector size for this record
|
||||||
|
/// Should be 0x800
|
||||||
|
/// </summary>
|
||||||
|
public uint BlockSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of bytes in this record
|
||||||
|
/// </summary>
|
||||||
|
public uint ByteCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of blocks allocated to this record
|
||||||
|
/// </summary>
|
||||||
|
public uint BlockCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Burst, usually 0x1
|
||||||
|
/// </summary>
|
||||||
|
public uint Burst { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gap, usually 0x0
|
||||||
|
/// </summary>
|
||||||
|
public uint Gap { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filename of record
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Filename { get; set; } = new byte[32];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of duplicates of the file/directory provided
|
||||||
|
/// </summary>
|
||||||
|
public uint LastAvatarIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offset of the file/directories provided
|
||||||
|
/// Length of array is LastAvatarIndex + 1
|
||||||
|
/// </summary>
|
||||||
|
public uint[] AvatarList { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
82
SabreTools.Data.Models/OperaFS/Enums.cs
Normal file
82
SabreTools.Data.Models/OperaFS/Enums.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SabreTools.Data.Models.OperaFS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Volume descriptor flag values
|
||||||
|
/// Formerly mapped 0x01 to DATA_DISC but this was not used by 3DO, so was remapped for M2 discs
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum VolumeFlags : byte
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates it is a data disc, system does not need rebooting
|
||||||
|
/// Should not be set for any retail 3DO discs
|
||||||
|
/// </summary>
|
||||||
|
M1_DATA_DISC = 0x01,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates M2 compatible disc?
|
||||||
|
/// Should be set for all Konami M2 discs
|
||||||
|
/// </summary>
|
||||||
|
M2 = 0x01,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates disc is only compatible with M2 ?
|
||||||
|
/// </summary>
|
||||||
|
M2_ONLY = 0x02,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates it is a data disc, system does not need rebooting
|
||||||
|
/// </summary>
|
||||||
|
M2_DATA_DISC = 0x04,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Blessed" volume
|
||||||
|
/// Should be set for all retail Konami M2 discs
|
||||||
|
/// </summary>
|
||||||
|
M2_SIGNED = 0x08,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mask for bits that are reserved
|
||||||
|
/// </summary>
|
||||||
|
RESERVED_MASK = 0xF0,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directory record flag values
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum DirectoryRecordFlags : uint
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Record is a directory
|
||||||
|
/// </summary>
|
||||||
|
DIRECTORY = 0x01,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Record is read-only
|
||||||
|
/// </summary>
|
||||||
|
READ_ONLY = 0x02,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Record is for filesystem use only
|
||||||
|
/// </summary>
|
||||||
|
SYSTEM = 0x04,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Final record in this block
|
||||||
|
/// </summary>
|
||||||
|
BLOCK_FINAL = 0x40000000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Final record in this directory
|
||||||
|
/// </summary>
|
||||||
|
DIRECTORY_FINAL = 0x80000000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mask for bits that are reserved
|
||||||
|
/// </summary>
|
||||||
|
RESERVED_MASK = 0x3FFFFFF8,
|
||||||
|
}
|
||||||
|
}
|
||||||
24
SabreTools.Data.Models/OperaFS/FileSystem.cs
Normal file
24
SabreTools.Data.Models/OperaFS/FileSystem.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace SabreTools.Data.Models.OperaFS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Opera Filesystem (or user-data-only disc image) present on 3DO and M2 discs
|
||||||
|
/// Usually contained within a CDROM disc image (2352-byte bin file)
|
||||||
|
/// All fields are Big-Endian
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://groups.google.com/g/rec.games.video.3do/c/1U3qrmLSYMQ"/>
|
||||||
|
public sealed class FileSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Volume Descriptor
|
||||||
|
/// </summary>
|
||||||
|
public VolumeDescriptor VolumeDescriptor { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map of all directories in filesystem, and their offsets
|
||||||
|
/// Duplicate directories exist at different offsets
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<uint, DirectoryDescriptor> Directories { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
112
SabreTools.Data.Models/OperaFS/VolumeDescriptor.cs
Normal file
112
SabreTools.Data.Models/OperaFS/VolumeDescriptor.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
namespace SabreTools.Data.Models.OperaFS
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// OperaFS Volume Descriptor, first sector of filesystem
|
||||||
|
/// </summary>
|
||||||
|
/// <see href="https://groups.google.com/g/rec.games.video.3do/c/1U3qrmLSYMQ"/>
|
||||||
|
public class VolumeDescriptor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Should be 0x01
|
||||||
|
/// </summary>
|
||||||
|
public byte RecordType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "ZZZZZ"
|
||||||
|
/// </summary>
|
||||||
|
public byte[] VolumeSyncBytes { get; set; } = new byte[5];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should be 0x01
|
||||||
|
/// </summary>
|
||||||
|
public byte StructureVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should be 0x00 for all 3DO discs
|
||||||
|
/// Is used by M2 discs?
|
||||||
|
/// </summary>
|
||||||
|
public VolumeFlags VolumeFlags { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Usually zeroed
|
||||||
|
/// </summary>
|
||||||
|
public byte[] VolumeCommentary { get; set; } = new byte[32];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ASCII "CD-ROM"
|
||||||
|
/// </summary>
|
||||||
|
public byte[] VolumeIdentifier { get; set; } = new byte[32];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hash or just a random value to identify disc
|
||||||
|
/// </summary>
|
||||||
|
public uint VolumeUniqueIdentifier { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sector size in volume
|
||||||
|
/// Usually 0x800 (2048)
|
||||||
|
/// </summary>
|
||||||
|
public uint VolumeBlockSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of sectors in volume
|
||||||
|
/// Usually size of disc image minus 300
|
||||||
|
/// </summary>
|
||||||
|
public uint VolumeBlockCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hash or just a random value to identify root directory
|
||||||
|
/// </summary>
|
||||||
|
public uint RootUniqueIdentifier { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of sectors for root directory
|
||||||
|
/// Usually 0x01
|
||||||
|
/// </summary>
|
||||||
|
public uint RootDirectoryBlockCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sector size in root directory
|
||||||
|
/// Usually 0x800
|
||||||
|
/// </summary>
|
||||||
|
public uint RootDirectoryBlockSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of duplicates of the root directory provided
|
||||||
|
/// Should be between 0 and 7
|
||||||
|
/// </summary>
|
||||||
|
public uint RootDirectoryLastAvatarIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Array of 8 offsets pointing to the root directory
|
||||||
|
/// Contents of each root directory should be identical
|
||||||
|
/// If RootDirectoryLastAvatarIndex is less than 7, remaining values are zeroed
|
||||||
|
/// </summary>
|
||||||
|
public uint[] RootDirectoryAvatarList { get; set; } = new uint[8];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rom tag count
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Extended volume data, present on M2 discs only</remarks>
|
||||||
|
public uint? RomTagCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application ID
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Extended volume data, present on M2 discs only</remarks>
|
||||||
|
public uint? ApplicationID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 36 reserved (zeroed) bytes
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Extended volume data, present on M2 discs only</remarks>
|
||||||
|
public byte[]? Reserved { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "iamaduck" repeated, aligned to each QWORD (0x0 or 0x8)
|
||||||
|
/// i.e. if padding starts at offset ending in 0x4 or 0xC, then it begins with "duck"
|
||||||
|
/// Is 0x77C long for M1 discs, and 0x750 long for M2 discs
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Padding { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
72
SabreTools.Serialization.Readers.Test/OperaFSTests.cs
Normal file
72
SabreTools.Serialization.Readers.Test/OperaFSTests.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SabreTools.Serialization.Readers.Test
|
||||||
|
{
|
||||||
|
public class OperaFSTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NullArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = null;
|
||||||
|
int offset = 0;
|
||||||
|
var deserializer = new OperaFS();
|
||||||
|
|
||||||
|
var actual = deserializer.Deserialize(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = [];
|
||||||
|
int offset = 0;
|
||||||
|
var deserializer = new OperaFS();
|
||||||
|
|
||||||
|
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 OperaFS();
|
||||||
|
|
||||||
|
var actual = deserializer.Deserialize(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = null;
|
||||||
|
var deserializer = new OperaFS();
|
||||||
|
|
||||||
|
var actual = deserializer.Deserialize(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = new MemoryStream([]);
|
||||||
|
var deserializer = new OperaFS();
|
||||||
|
|
||||||
|
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 OperaFS();
|
||||||
|
|
||||||
|
var actual = deserializer.Deserialize(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
237
SabreTools.Serialization.Readers/OperaFS.cs
Normal file
237
SabreTools.Serialization.Readers/OperaFS.cs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using SabreTools.Data.Extensions;
|
||||||
|
using SabreTools.Data.Models.OperaFS;
|
||||||
|
using SabreTools.IO.Extensions;
|
||||||
|
using SabreTools.Matching;
|
||||||
|
using SabreTools.Numerics.Extensions;
|
||||||
|
|
||||||
|
namespace SabreTools.Serialization.Readers
|
||||||
|
{
|
||||||
|
public class OperaFS : BaseBinaryReader<FileSystem>
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="Deserialize(Stream?)" />
|
||||||
|
public override FileSystem? Deserialize(Stream? data)
|
||||||
|
{
|
||||||
|
// If the data is invalid
|
||||||
|
if (data is null || !data.CanRead)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Simple check for a valid stream length
|
||||||
|
if (data.Length - data.Position < Constants.SectorSize)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Simple check on first bytes
|
||||||
|
var signature = data.PeekBytes(7);
|
||||||
|
if (!signature.EqualsExactly(Constants.MagicBytes))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Cache the current offset
|
||||||
|
long initialOffset = data.Position;
|
||||||
|
|
||||||
|
// Create a new FileSystem to fill
|
||||||
|
var volume = new FileSystem();
|
||||||
|
|
||||||
|
var volumeDescriptor = ParseVolumeDescriptor(data);
|
||||||
|
if (volumeDescriptor is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
volume.VolumeDescriptor = volumeDescriptor;
|
||||||
|
|
||||||
|
var directories = ParseRootDirectory(data, volumeDescriptor, initialOffset);
|
||||||
|
if (directories is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
volume.Directories = directories;
|
||||||
|
|
||||||
|
return volume;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore the actual error
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a Stream into an OperaFS Volume Descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Stream to parse</param>
|
||||||
|
/// <returns>Filled VolumeDescriptor on success, null on error</returns>
|
||||||
|
public static VolumeDescriptor ParseVolumeDescriptor(Stream data)
|
||||||
|
{
|
||||||
|
var volumeDescriptor = new VolumeDescriptor();
|
||||||
|
|
||||||
|
volumeDescriptor.RecordType = data.ReadByteValue();
|
||||||
|
volumeDescriptor.VolumeSyncBytes = data.ReadBytes(5);
|
||||||
|
volumeDescriptor.StructureVersion = data.ReadByteValue();
|
||||||
|
volumeDescriptor.VolumeFlags = (VolumeFlags)data.ReadByteValue();
|
||||||
|
volumeDescriptor.VolumeCommentary = data.ReadBytes(32);
|
||||||
|
volumeDescriptor.VolumeIdentifier = data.ReadBytes(32);
|
||||||
|
volumeDescriptor.VolumeUniqueIdentifier = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.VolumeBlockSize = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.VolumeBlockCount = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.RootUniqueIdentifier = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.RootDirectoryBlockCount = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.RootDirectoryBlockSize = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.RootDirectoryLastAvatarIndex = data.ReadUInt32BigEndian();
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
volumeDescriptor.RootDirectoryAvatarList[i] = data.ReadUInt32BigEndian();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((volumeDescriptor.VolumeFlags & VolumeFlags.M2) == VolumeFlags.M2)
|
||||||
|
{
|
||||||
|
volumeDescriptor.RomTagCount = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.ApplicationID = data.ReadUInt32BigEndian();
|
||||||
|
volumeDescriptor.Reserved = data.ReadBytes(36);
|
||||||
|
volumeDescriptor.Padding = data.ReadBytes(0x750);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
volumeDescriptor.Padding = data.ReadBytes(0x77C);
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumeDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a Stream into an map of OperaFS directories from a volume descriptor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Stream to parse</param>
|
||||||
|
/// <returns>Filled map of directories on success, null on error</returns>
|
||||||
|
public static Dictionary<uint, DirectoryDescriptor> ParseRootDirectory(Stream data, VolumeDescriptor volumeDescriptor, long initialOffset)
|
||||||
|
{
|
||||||
|
var directories = new Dictionary<uint, DirectoryDescriptor>();
|
||||||
|
|
||||||
|
data.SeekIfPossible(initialOffset + volumeDescriptor.RootDirectoryAvatarList[0] * Constants.SectorSize, SeekOrigin.Begin);
|
||||||
|
var rootDirectory = ParseDirectory(data);
|
||||||
|
for (int i = 0; i <= volumeDescriptor.RootDirectoryLastAvatarIndex; i++)
|
||||||
|
{
|
||||||
|
directories.Add(volumeDescriptor.RootDirectoryAvatarList[i], rootDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var childDirectories = ParseChildDirectories(data, rootDirectory, initialOffset);
|
||||||
|
foreach (var kvp in childDirectories)
|
||||||
|
{
|
||||||
|
if (!directories.ContainsKey(kvp.Key))
|
||||||
|
directories.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a Stream into an map of OperaFS directories from a directory
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Stream to parse</param>
|
||||||
|
/// <returns>Filled map of directories on success, null on error</returns>
|
||||||
|
public static Dictionary<uint, DirectoryDescriptor> ParseChildDirectories(Stream data, DirectoryDescriptor parent, long initialOffset)
|
||||||
|
{
|
||||||
|
var directories = new Dictionary<uint, DirectoryDescriptor>();
|
||||||
|
|
||||||
|
foreach (var dr in parent.DirectoryRecords)
|
||||||
|
{
|
||||||
|
// Skip file records
|
||||||
|
if ((dr.DirectoryRecordFlags & DirectoryRecordFlags.DIRECTORY) == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
data.SeekIfPossible(initialOffset + dr.AvatarList[0] * Constants.SectorSize, SeekOrigin.Begin);
|
||||||
|
var directory = ParseDirectory(data);
|
||||||
|
directories.Add(dr.AvatarList[0], directory);
|
||||||
|
for (int i = 1; i <= dr.LastAvatarIndex; i++)
|
||||||
|
{
|
||||||
|
// Read avatar
|
||||||
|
data.SeekIfPossible(initialOffset + dr.AvatarList[i] * Constants.SectorSize, SeekOrigin.Begin);
|
||||||
|
var avatar = ParseDirectory(data);
|
||||||
|
|
||||||
|
// Add reference to original directory if avatar is identical
|
||||||
|
// TODO: Check equality of all previously added unique avatars too
|
||||||
|
if (avatar.EqualsExactly(directory))
|
||||||
|
directories.Add(dr.AvatarList[i], directory);
|
||||||
|
else
|
||||||
|
directories.Add(dr.AvatarList[i], avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
var childDirectories = ParseChildDirectories(data, directory, initialOffset);
|
||||||
|
foreach (var kvp in childDirectories)
|
||||||
|
{
|
||||||
|
if (!directories.ContainsKey(kvp.Key))
|
||||||
|
directories.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a Stream into an OperaFS Directory
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Stream to parse</param>
|
||||||
|
/// <returns>Filled Directory on success, null on error</returns>
|
||||||
|
public static DirectoryDescriptor ParseDirectory(Stream data)
|
||||||
|
{
|
||||||
|
var directory = new DirectoryDescriptor();
|
||||||
|
|
||||||
|
directory.NextBlock = data.ReadInt32BigEndian();
|
||||||
|
directory.PreviousBlock = data.ReadInt32BigEndian();
|
||||||
|
directory.Flags = data.ReadUInt32BigEndian();
|
||||||
|
directory.FirstFreeByte = data.ReadUInt32BigEndian();
|
||||||
|
directory.FirstEntryOffset = data.ReadUInt32BigEndian();
|
||||||
|
|
||||||
|
var directoryRecords = new List<DirectoryRecord>();
|
||||||
|
|
||||||
|
long startPosition = data.Position;
|
||||||
|
while (data.Position < startPosition + directory.FirstFreeByte)
|
||||||
|
{
|
||||||
|
var directoryRecord = ParseDirectoryRecord(data);
|
||||||
|
directoryRecords.Add(directoryRecord);
|
||||||
|
|
||||||
|
if ((directoryRecord.DirectoryRecordFlags & DirectoryRecordFlags.DIRECTORY_FINAL) != 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if ((directoryRecord.DirectoryRecordFlags & DirectoryRecordFlags.BLOCK_FINAL) != 0)
|
||||||
|
{
|
||||||
|
long nextBlock = Constants.SectorSize - ((data.Position - startPosition) % Constants.SectorSize);
|
||||||
|
data.SeekIfPossible(nextBlock, SeekOrigin.Current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
directory.DirectoryRecords = directoryRecords.ToArray();
|
||||||
|
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a Stream into an OperaFS Directory Record
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Stream to parse</param>
|
||||||
|
/// <returns>Filled Directory Record on success, null on error</returns>
|
||||||
|
public static DirectoryRecord ParseDirectoryRecord(Stream data)
|
||||||
|
{
|
||||||
|
var directoryRecord = new DirectoryRecord();
|
||||||
|
|
||||||
|
directoryRecord.DirectoryRecordFlags = (DirectoryRecordFlags)data.ReadUInt32BigEndian();
|
||||||
|
directoryRecord.UniqueIdentifier = data.ReadBytes(4);
|
||||||
|
directoryRecord.Type = data.ReadBytes(4);
|
||||||
|
directoryRecord.BlockSize = data.ReadUInt32BigEndian();
|
||||||
|
directoryRecord.ByteCount = data.ReadUInt32BigEndian();
|
||||||
|
directoryRecord.BlockCount = data.ReadUInt32BigEndian();
|
||||||
|
directoryRecord.Burst = data.ReadUInt32BigEndian();
|
||||||
|
directoryRecord.Gap = data.ReadUInt32BigEndian();
|
||||||
|
directoryRecord.Filename = data.ReadBytes(32);
|
||||||
|
directoryRecord.LastAvatarIndex = data.ReadUInt32BigEndian();
|
||||||
|
|
||||||
|
directoryRecord.AvatarList = new uint[directoryRecord.LastAvatarIndex + 1];
|
||||||
|
for (int i = 0; i <= directoryRecord.LastAvatarIndex; i++)
|
||||||
|
{
|
||||||
|
directoryRecord.AvatarList[i] = data.ReadUInt32BigEndian();
|
||||||
|
}
|
||||||
|
|
||||||
|
return directoryRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
SabreTools.Wrappers.Test/OperaDiscImageTests.cs
Normal file
60
SabreTools.Wrappers.Test/OperaDiscImageTests.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SabreTools.Wrappers.Test
|
||||||
|
{
|
||||||
|
public class OperaDiscImageTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NullArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = null;
|
||||||
|
int offset = 0;
|
||||||
|
var actual = OperaDiscImage.Create(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = [];
|
||||||
|
int offset = 0;
|
||||||
|
var actual = OperaDiscImage.Create(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||||
|
int offset = 0;
|
||||||
|
var actual = OperaDiscImage.Create(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = null;
|
||||||
|
var actual = OperaDiscImage.Create(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = new MemoryStream([]);
|
||||||
|
var actual = OperaDiscImage.Create(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||||
|
var actual = OperaDiscImage.Create(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
SabreTools.Wrappers.Test/OperaFSTests.cs
Normal file
60
SabreTools.Wrappers.Test/OperaFSTests.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SabreTools.Wrappers.Test
|
||||||
|
{
|
||||||
|
public class OperaFSTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NullArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = null;
|
||||||
|
int offset = 0;
|
||||||
|
var actual = OperaFS.Create(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = [];
|
||||||
|
int offset = 0;
|
||||||
|
var actual = OperaFS.Create(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidArray_Null()
|
||||||
|
{
|
||||||
|
byte[]? data = [.. Enumerable.Repeat<byte>(0xFF, 1024)];
|
||||||
|
int offset = 0;
|
||||||
|
var actual = OperaFS.Create(data, offset);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NullStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = null;
|
||||||
|
var actual = OperaFS.Create(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = new MemoryStream([]);
|
||||||
|
var actual = OperaFS.Create(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidStream_Null()
|
||||||
|
{
|
||||||
|
Stream? data = new MemoryStream([.. Enumerable.Repeat<byte>(0xFF, 1024)]);
|
||||||
|
var actual = OperaFS.Create(data);
|
||||||
|
Assert.Null(actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ namespace SabreTools.Wrappers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create user data sub-stream
|
// Create user data sub-stream
|
||||||
var userData = new Data.Extensions.CDROMExtensions.ISO9660Stream(data);
|
var userData = new Data.Extensions.CDROMExtensions.UserDataStream(data);
|
||||||
|
|
||||||
// Cache the current offset
|
// Cache the current offset
|
||||||
long currentOffset = userData.Position;
|
long currentOffset = userData.Position;
|
||||||
|
|||||||
97
SabreTools.Wrappers/OperaDiscImage.cs
Normal file
97
SabreTools.Wrappers/OperaDiscImage.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using System.IO;
|
||||||
|
using SabreTools.Data.Models.OperaFS;
|
||||||
|
|
||||||
|
namespace SabreTools.Wrappers
|
||||||
|
{
|
||||||
|
public partial class OperaDiscImage : OperaFS
|
||||||
|
{
|
||||||
|
#region Descriptive Properties
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string DescriptionString => "3DO / M2 (Opera) Disc Image";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaDiscImage(FileSystem model, byte[] data) : base(model, data) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaDiscImage(FileSystem model, byte[] data, int offset) : base(model, data, offset) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaDiscImage(FileSystem model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaDiscImage(FileSystem model, Stream data) : base(model, data) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaDiscImage(FileSystem model, Stream data, long offset) : base(model, data, offset) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaDiscImage(FileSystem model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Static Constructors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a OperaDiscImage filesystem from a byte array and offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Byte array representing the OperaDiscImage filesystem</param>
|
||||||
|
/// <param name="offset">Offset within the array to parse</param>
|
||||||
|
/// <returns>A OperaDiscImage filesystem wrapper on success, null on failure</returns>
|
||||||
|
public static new OperaDiscImage? 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 OperaDiscImage filesystem from a Stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Seekable Stream representing the OperaDiscImage filesystem</param>
|
||||||
|
/// <returns>A OperaDiscImage filesystem wrapper on success, null on failure</returns>
|
||||||
|
public static new OperaDiscImage? Create(Stream? data)
|
||||||
|
{
|
||||||
|
// If the data is invalid
|
||||||
|
if (data is null || !data.CanRead || !data.CanSeek)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create user data sub-stream
|
||||||
|
var userData = new Data.Extensions.CDROMExtensions.UserDataStream(data);
|
||||||
|
|
||||||
|
// Cache the current offset
|
||||||
|
long currentOffset = userData.Position;
|
||||||
|
|
||||||
|
// Deserialize just the sub-stream
|
||||||
|
var model = new Serialization.Readers.OperaFS().Deserialize(userData);
|
||||||
|
if (model is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Reset stream
|
||||||
|
userData.Seek(currentOffset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
return new OperaDiscImage(model, userData, userData.Position);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
159
SabreTools.Wrappers/OperaFS.Extraction.cs
Normal file
159
SabreTools.Wrappers/OperaFS.Extraction.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using SabreTools.Data.Extensions;
|
||||||
|
using SabreTools.Data.Models.OperaFS;
|
||||||
|
using SabreTools.IO.Extensions;
|
||||||
|
using SabreTools.Matching;
|
||||||
|
using SabreTools.Numerics.Extensions;
|
||||||
|
|
||||||
|
namespace SabreTools.Wrappers
|
||||||
|
{
|
||||||
|
public partial class OperaFS : IExtractable
|
||||||
|
{
|
||||||
|
#region Extraction State
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of extracted files by their sector offset
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<uint> extractedFiles = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of extracted directories
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<DirectoryDescriptor> extractedDirectories = [];
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public virtual bool Extract(string outputDirectory, bool includeDebug)
|
||||||
|
{
|
||||||
|
// Clear the extraction state
|
||||||
|
extractedFiles.Clear();
|
||||||
|
extractedDirectories.Clear();
|
||||||
|
|
||||||
|
bool allExtracted = true;
|
||||||
|
|
||||||
|
for (int i = 0; i <= VolumeDescriptor.RootDirectoryLastAvatarIndex; i++)
|
||||||
|
{
|
||||||
|
var rootDirectory = Directories[VolumeDescriptor.RootDirectoryAvatarList[i]];
|
||||||
|
if (extractedDirectories.Contains(rootDirectory))
|
||||||
|
{
|
||||||
|
if (includeDebug) Console.WriteLine($"Root directory duplicate at sector {VolumeDescriptor.RootDirectoryAvatarList[i]}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDebug) Console.WriteLine($"Extracting from root directory at sector {VolumeDescriptor.RootDirectoryAvatarList[i]}");
|
||||||
|
allExtracted |= ExtractDirectory(outputDirectory, includeDebug, rootDirectory);
|
||||||
|
extractedDirectories.Add(rootDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allExtracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ExtractDirectory(string outputDirectory, bool includeDebug, DirectoryDescriptor dir)
|
||||||
|
{
|
||||||
|
// Create output directory if it does not exist
|
||||||
|
if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
|
||||||
|
Directory.CreateDirectory(outputDirectory);
|
||||||
|
|
||||||
|
bool allExtracted = true;
|
||||||
|
foreach (var dr in dir.DirectoryRecords)
|
||||||
|
{
|
||||||
|
var filename = Encoding.UTF8.GetString(dr.Filename).TrimEnd('\0');
|
||||||
|
if ((dr.DirectoryRecordFlags & DirectoryRecordFlags.DIRECTORY) == 0)
|
||||||
|
{
|
||||||
|
// Skip filesystem only files (e.g. Volume Descriptor "Disc Label")
|
||||||
|
if ((dr.DirectoryRecordFlags & DirectoryRecordFlags.SYSTEM) != 0)
|
||||||
|
{
|
||||||
|
if (includeDebug) Console.WriteLine($"Skipping filesystem object {filename}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath = Path.Combine(outputDirectory, filename);
|
||||||
|
for (int i = 0; i <= dr.LastAvatarIndex; i++)
|
||||||
|
{
|
||||||
|
uint fileOffset = (uint)Constants.SectorSize * dr.AvatarList[i];
|
||||||
|
|
||||||
|
// TODO: Check that all avatars are identical?
|
||||||
|
if (extractedFiles.Contains(fileOffset))
|
||||||
|
{
|
||||||
|
if (includeDebug) Console.WriteLine($"File duplicate at sector {dr.AvatarList[i]}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const uint chunkSize = 2048 * 1024;
|
||||||
|
lock (_dataSourceLock)
|
||||||
|
{
|
||||||
|
_dataSource.SeekIfPossible(fileOffset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// Get the length, and make sure it won't EOF
|
||||||
|
uint length = dr.ByteCount;
|
||||||
|
if (length > _dataSource.Length - _dataSource.Position)
|
||||||
|
{
|
||||||
|
allExtracted = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the output file
|
||||||
|
if (includeDebug) Console.WriteLine($"Extracting file {filename} at sector {dr.AvatarList[i]}");
|
||||||
|
using var fs = File.Open(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
|
||||||
|
while (length > 0)
|
||||||
|
{
|
||||||
|
int bytesToRead = (int)Math.Min(length, chunkSize);
|
||||||
|
|
||||||
|
byte[] buffer = _dataSource.ReadBytes(bytesToRead);
|
||||||
|
fs.Write(buffer, 0, bytesToRead);
|
||||||
|
fs.Flush();
|
||||||
|
|
||||||
|
length -= (uint)bytesToRead;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractedFiles.Add(fileOffset);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
allExtracted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Iterate over all avatars, in case they're not identical
|
||||||
|
for (int i = 0; i <= dr.LastAvatarIndex; i++)
|
||||||
|
{
|
||||||
|
// Check whether directory is already extracted
|
||||||
|
var childDir = Directories[dr.AvatarList[i]];
|
||||||
|
if (extractedDirectories.Contains(childDir))
|
||||||
|
{
|
||||||
|
if (includeDebug) Console.WriteLine($"Directory duplicate at sector {dr.AvatarList[i]}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (includeDebug) Console.WriteLine($"Extracting directory {filename} at sector {dr.AvatarList[i]}");
|
||||||
|
var outputPath = Path.Combine(outputDirectory, filename);
|
||||||
|
allExtracted |= ExtractDirectory(outputPath, includeDebug, childDir);
|
||||||
|
extractedDirectories.Add(childDir);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
allExtracted = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allExtracted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
SabreTools.Wrappers/OperaFS.Printing.cs
Normal file
151
SabreTools.Wrappers/OperaFS.Printing.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using SabreTools.Data.Models.OperaFS;
|
||||||
|
using SabreTools.Text.Extensions;
|
||||||
|
|
||||||
|
namespace SabreTools.Wrappers
|
||||||
|
{
|
||||||
|
public partial class OperaFS : IPrintable
|
||||||
|
{
|
||||||
|
#if NETCOREAPP
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map of printed directories to their sector offset
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<DirectoryDescriptor, uint> printedDirectories = [];
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void PrintInformation(StringBuilder builder)
|
||||||
|
{
|
||||||
|
builder.AppendLine("3DO / M2 (Opera) Filesystem Information:");
|
||||||
|
builder.AppendLine("-------------------------");
|
||||||
|
builder.AppendLine();
|
||||||
|
|
||||||
|
Print(builder, VolumeDescriptor);
|
||||||
|
foreach (var kvp in Directories)
|
||||||
|
{
|
||||||
|
if (printedDirectories.ContainsKey(kvp.Value))
|
||||||
|
{
|
||||||
|
builder.AppendLine($" Directory Descriptor (Sector {kvp.Key}):");
|
||||||
|
builder.AppendLine(" -------------------------");
|
||||||
|
builder.AppendLine($" Duplicate of Descriptor at Sector {printedDirectories[kvp.Value]}");
|
||||||
|
builder.AppendLine();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Print(builder, kvp.Key, kvp.Value);
|
||||||
|
printedDirectories.Add(kvp.Value, kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Print(StringBuilder builder, VolumeDescriptor vd)
|
||||||
|
{
|
||||||
|
builder.AppendLine(" Volume Descriptor:");
|
||||||
|
builder.AppendLine(" -------------------------");
|
||||||
|
|
||||||
|
builder.AppendLine(vd.RecordType, " Record Type");
|
||||||
|
builder.AppendLine(vd.VolumeSyncBytes, " Volume Sync Bytes");
|
||||||
|
builder.AppendLine(vd.StructureVersion, " Structure Version");
|
||||||
|
|
||||||
|
builder.AppendLine((byte)vd.VolumeFlags, " Volume Flags");
|
||||||
|
if ((byte)vd.VolumeFlags != 0)
|
||||||
|
{
|
||||||
|
builder.AppendLine(" Volume Flags (Parsed)");
|
||||||
|
builder.AppendLine((vd.VolumeFlags & VolumeFlags.M2) == VolumeFlags.M2, " M2 Disc (or M1 Data Disc)");
|
||||||
|
builder.AppendLine((vd.VolumeFlags & VolumeFlags.M2_ONLY) == VolumeFlags.M2_ONLY, " M2 Only");
|
||||||
|
builder.AppendLine((vd.VolumeFlags & VolumeFlags.M2_DATA_DISC) == VolumeFlags.M2_DATA_DISC, " M2 Data Disc");
|
||||||
|
builder.AppendLine((vd.VolumeFlags & VolumeFlags.M2_SIGNED) == VolumeFlags.M2_SIGNED, " M2 Signed");
|
||||||
|
builder.AppendLine((vd.VolumeFlags & VolumeFlags.RESERVED_MASK) != 0, " Reserved Bits Set");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(Encoding.UTF8.GetString(vd.VolumeCommentary), " Volume Commentary");
|
||||||
|
builder.AppendLine(Encoding.UTF8.GetString(vd.VolumeIdentifier), " Volume Identifier");
|
||||||
|
builder.AppendLine(vd.VolumeUniqueIdentifier, " Volume Unique Identifier");
|
||||||
|
builder.AppendLine(vd.VolumeBlockSize, " Volume Block Size");
|
||||||
|
builder.AppendLine(vd.VolumeBlockCount, " Volume Block Count");
|
||||||
|
builder.AppendLine(vd.RootUniqueIdentifier, " Root Unique Identifier");
|
||||||
|
builder.AppendLine(vd.RootDirectoryBlockCount, " Root Directory Block Count");
|
||||||
|
builder.AppendLine(vd.RootDirectoryBlockSize, " Root Directory Block Size");
|
||||||
|
builder.AppendLine(vd.RootDirectoryLastAvatarIndex, " Root Directory Last Avatar Index");
|
||||||
|
builder.AppendLine(vd.RootDirectoryAvatarList, " Root Directory Avatar List");
|
||||||
|
|
||||||
|
if ((vd.VolumeFlags & VolumeFlags.M2) == VolumeFlags.M2)
|
||||||
|
{
|
||||||
|
builder.AppendLine(vd.RomTagCount, " Rom Tag Count");
|
||||||
|
builder.AppendLine(vd.ApplicationID, " Application ID");
|
||||||
|
if (vd.Reserved is null)
|
||||||
|
builder.AppendLine(vd.Reserved, " Reserved Bytes");
|
||||||
|
else if (Array.TrueForAll(vd.Reserved, b => b == 0))
|
||||||
|
builder.AppendLine($"Zeroed", " Reserved Bytes");
|
||||||
|
else
|
||||||
|
builder.AppendLine($"Not Zeroed", " Reserved Bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = Array.IndexOf(Constants.PaddingBytes, vd.Padding[0]);
|
||||||
|
int index = 0;
|
||||||
|
bool isDuck = (offset >= 0) && Array.TrueForAll(vd.Padding, b => b == Constants.PaddingBytes[(index++ + offset) % Constants.PaddingBytes.Length]);
|
||||||
|
if (isDuck)
|
||||||
|
builder.AppendLine("Expected data (iamaduck)", " Padding");
|
||||||
|
else
|
||||||
|
builder.AppendLine("Unexpected data", " Padding");
|
||||||
|
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Print(StringBuilder builder, uint sector, DirectoryDescriptor dir)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" Directory Descriptor (Sector {sector}):");
|
||||||
|
builder.AppendLine(" -------------------------");
|
||||||
|
|
||||||
|
builder.AppendLine(dir.NextBlock, " Next Block");
|
||||||
|
builder.AppendLine(dir.PreviousBlock, " Previous Block");
|
||||||
|
builder.AppendLine(dir.Flags, " Flags");
|
||||||
|
builder.AppendLine(dir.FirstFreeByte, " First Free Byte");
|
||||||
|
builder.AppendLine(dir.FirstEntryOffset, " First Entry Offset");
|
||||||
|
|
||||||
|
foreach (var dr in dir.DirectoryRecords)
|
||||||
|
{
|
||||||
|
Print(builder, dr);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Print(StringBuilder builder, DirectoryRecord dr)
|
||||||
|
{
|
||||||
|
builder.AppendLine(" Directory Record:");
|
||||||
|
builder.AppendLine(" -------------------------");
|
||||||
|
|
||||||
|
builder.AppendLine((byte)dr.DirectoryRecordFlags, " Directory Record Flags");
|
||||||
|
if ((byte)dr.DirectoryRecordFlags != 0)
|
||||||
|
{
|
||||||
|
builder.AppendLine(" Directory Record Flags (Parsed)");
|
||||||
|
builder.AppendLine((dr.DirectoryRecordFlags & DirectoryRecordFlags.DIRECTORY) == DirectoryRecordFlags.DIRECTORY, " Directory");
|
||||||
|
builder.AppendLine((dr.DirectoryRecordFlags & DirectoryRecordFlags.READ_ONLY) == DirectoryRecordFlags.READ_ONLY, " Read-only");
|
||||||
|
builder.AppendLine((dr.DirectoryRecordFlags & DirectoryRecordFlags.SYSTEM) == DirectoryRecordFlags.SYSTEM, " Filesystem Use");
|
||||||
|
builder.AppendLine((dr.DirectoryRecordFlags & DirectoryRecordFlags.BLOCK_FINAL) == DirectoryRecordFlags.BLOCK_FINAL, " Final in Block");
|
||||||
|
builder.AppendLine((dr.DirectoryRecordFlags & DirectoryRecordFlags.DIRECTORY_FINAL) == DirectoryRecordFlags.DIRECTORY_FINAL, " Final in Directory");
|
||||||
|
builder.AppendLine((dr.DirectoryRecordFlags & DirectoryRecordFlags.RESERVED_MASK) != 0, " Reserved Bits Set");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(dr.UniqueIdentifier, " Unique Identifier");
|
||||||
|
builder.AppendLine(dr.Type, " Type");
|
||||||
|
builder.AppendLine(Encoding.UTF8.GetString(dr.Type), " Type (Parsed)");
|
||||||
|
builder.AppendLine(dr.BlockSize, " BlockSize");
|
||||||
|
builder.AppendLine(dr.ByteCount, " ByteCount");
|
||||||
|
builder.AppendLine(dr.BlockCount, " BlockCount");
|
||||||
|
builder.AppendLine(dr.Burst, " Burst");
|
||||||
|
builder.AppendLine(dr.Gap, " Gap");
|
||||||
|
builder.AppendLine(dr.Filename, " Filename");
|
||||||
|
builder.AppendLine(Encoding.UTF8.GetString(dr.Filename), " Filename (Parsed)");
|
||||||
|
builder.AppendLine(dr.LastAvatarIndex, " LastAvatarIndex");
|
||||||
|
builder.AppendLine(dr.AvatarList, " AvatarList");
|
||||||
|
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
SabreTools.Wrappers/OperaFS.cs
Normal file
101
SabreTools.Wrappers/OperaFS.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using SabreTools.Data.Models.OperaFS;
|
||||||
|
|
||||||
|
namespace SabreTools.Wrappers
|
||||||
|
{
|
||||||
|
public partial class OperaFS : WrapperBase<FileSystem>
|
||||||
|
{
|
||||||
|
#region Descriptive Properties
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string DescriptionString => "3DO / M2 (Opera) Filesystem";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Extension Properties
|
||||||
|
|
||||||
|
/// <inheritdoc cref="FileSystem.VolumeDescriptor"/>
|
||||||
|
public VolumeDescriptor VolumeDescriptor => Model.VolumeDescriptor;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="FileSystem.Directories"/>
|
||||||
|
public Dictionary<uint, DirectoryDescriptor> Directories => Model.Directories;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaFS(FileSystem model, byte[] data) : base(model, data) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaFS(FileSystem model, byte[] data, int offset) : base(model, data, offset) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaFS(FileSystem model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaFS(FileSystem model, Stream data) : base(model, data) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaFS(FileSystem model, Stream data, long offset) : base(model, data, offset) { }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public OperaFS(FileSystem model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Static Constructors
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an OperaFS FileSystem from a byte array and offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Byte array representing the OperaFS FileSystem</param>
|
||||||
|
/// <param name="offset">Offset within the array to parse</param>
|
||||||
|
/// <returns>An OperaFS FileSystem wrapper on success, null on failure</returns>
|
||||||
|
public static OperaFS? 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 OperaFS FileSystem from a Stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Stream representing the OperaFS FileSystem</param>
|
||||||
|
/// <returns>An OperaFS FileSystem wrapper on success, null on failure</returns>
|
||||||
|
public static OperaFS? 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.OperaFS().Deserialize(data);
|
||||||
|
if (model is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new OperaFS(model, data, currentOffset);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ namespace SabreTools.Wrappers
|
|||||||
WrapperType.BFPK => BFPK.Create(data),
|
WrapperType.BFPK => BFPK.Create(data),
|
||||||
WrapperType.BSP => BSP.Create(data),
|
WrapperType.BSP => BSP.Create(data),
|
||||||
WrapperType.BZip2 => BZip2.Create(data),
|
WrapperType.BZip2 => BZip2.Create(data),
|
||||||
WrapperType.CDROM => CDROM.Create(data),
|
WrapperType.CDROM => CreateCDROMImageWrapper(data),
|
||||||
WrapperType.CFB => CFB.Create(data),
|
WrapperType.CFB => CFB.Create(data),
|
||||||
WrapperType.CHD => CHD.Create(data),
|
WrapperType.CHD => CHD.Create(data),
|
||||||
WrapperType.CIA => CIA.Create(data),
|
WrapperType.CIA => CIA.Create(data),
|
||||||
@@ -85,6 +85,36 @@ namespace SabreTools.Wrappers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an instance of a wrapper based on the CDROM image type
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">Stream data to parse</param>
|
||||||
|
/// <returns>IWrapper representing the CDROM image, null on error</returns>
|
||||||
|
public static IWrapper? CreateCDROMImageWrapper(Stream? stream)
|
||||||
|
{
|
||||||
|
// If we have no stream
|
||||||
|
if (stream is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Cache the current offset
|
||||||
|
long initialOffset = stream.Position;
|
||||||
|
|
||||||
|
// Try to get a 3DO / M2 disc image wrapper
|
||||||
|
var operaDiscImageWrapper = OperaDiscImage.Create(stream);
|
||||||
|
if (operaDiscImageWrapper is not null)
|
||||||
|
return operaDiscImageWrapper;
|
||||||
|
|
||||||
|
// Reset position in stream
|
||||||
|
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// Fallback to standard CDROM wrapper
|
||||||
|
var cdromWrapper = CDROM.Create(stream);
|
||||||
|
if (cdromWrapper is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return cdromWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an instance of a wrapper based on the disc image type
|
/// Create an instance of a wrapper based on the disc image type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -99,7 +129,7 @@ namespace SabreTools.Wrappers
|
|||||||
// Cache the current offset
|
// Cache the current offset
|
||||||
long initialOffset = stream.Position;
|
long initialOffset = stream.Position;
|
||||||
|
|
||||||
// Try to get an Xbox ISO wrapper first
|
// Try to get an Xbox disc image wrapper
|
||||||
var xboxWrapper = XboxISO.Create(stream);
|
var xboxWrapper = XboxISO.Create(stream);
|
||||||
if (xboxWrapper is not null)
|
if (xboxWrapper is not null)
|
||||||
return xboxWrapper;
|
return xboxWrapper;
|
||||||
@@ -107,6 +137,14 @@ namespace SabreTools.Wrappers
|
|||||||
// Reset position in stream
|
// Reset position in stream
|
||||||
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
|
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// Try to get a 3DO / M2 disc image wrapper
|
||||||
|
var operaFSWrapper = OperaFS.Create(stream);
|
||||||
|
if (operaFSWrapper is not null)
|
||||||
|
return operaFSWrapper;
|
||||||
|
|
||||||
|
// Reset position in stream
|
||||||
|
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
|
||||||
|
|
||||||
// Fallback to standard ISO9660 wrapper
|
// Fallback to standard ISO9660 wrapper
|
||||||
var isoWrapper = ISO9660.Create(stream);
|
var isoWrapper = ISO9660.Create(stream);
|
||||||
if (isoWrapper is null)
|
if (isoWrapper is null)
|
||||||
@@ -288,7 +326,9 @@ namespace SabreTools.Wrappers
|
|||||||
|
|
||||||
#region CDROM
|
#region CDROM
|
||||||
|
|
||||||
if (magic.StartsWith([0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
// Must come before skeleton extension check for ISO9660
|
||||||
|
// CDROM skeletons have same extension as ISO9660 skeletons
|
||||||
|
if (magic.StartsWith([0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00])
|
||||||
&& (extension.Equals("bin", StringComparison.OrdinalIgnoreCase)
|
&& (extension.Equals("bin", StringComparison.OrdinalIgnoreCase)
|
||||||
|| extension.Equals("skeleton", StringComparison.OrdinalIgnoreCase)))
|
|| extension.Equals("skeleton", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user