Files
gmipf 1663345195 Add a UDF reader and wrapper (#100)
* Add a UDF reader

Reads the Volume Recognition Sequence, the Anchor Volume Descriptor
Pointer (sector 256, then the end of the volume), the Main or Reserve
Volume Descriptor Sequence, the File Set Descriptor and the directory
hierarchy. Type 1 and Metadata Partitions are mapped, Virtual and
Sparable partitions are not.

File Entries, Allocation Extent Descriptors and directories are read in
ascending sector order with wrap-around, adjacent sectors merged into
one request. FindFileEntry follows one path and reads only the
directories on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add a UDF wrapper

Prints the volume and file set structures and the directory hierarchy,
and extracts the files. CreateDiscImageWrapper tries UDF after ISO 9660
for a stream at offset 0, so discs with both file systems still open as
ISO 9660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Treat the stream position as the start of the UDF volume

The UDF reader addresses logical sectors from the start of the stream,
so the wrapper hands it a view that starts at the current position and
keeps that offset, like the other wrappers. CreateDiscImageWrapper no
longer skips UDF for a stream that is not at offset 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address review comments on the UDF code

- Move the descriptor tag check to UDFExtensions, with tests
- Parse Volume Structure Descriptors in their own method
- Use the existing extensions for numbers, the partition type identifier
  and printing entity identifiers
- Use Array.Exists in IsSafeName
- Make Extract non-virtual and remove the printing test
- Use collection expressions and string interpolation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Parse File Identifier Descriptors in their own method

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add a TODO to use the CRC from Hashing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:18:23 +01:00

1229 lines
44 KiB
C#

using System;
using System.IO;
using SabreTools.Data.Extensions;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Numerics.Extensions;
namespace SabreTools.Wrappers
{
public static class WrapperFactory
{
/// <summary>
/// Create an instance of a wrapper based on file type
/// </summary>
public static IWrapper? CreateWrapper(WrapperType fileType, Stream? data)
{
return fileType switch
{
WrapperType.AACSMediaKeyBlock => AACSMediaKeyBlock.Create(data),
WrapperType.Atari7800Cart => Atari7800Cart.Create(data),
WrapperType.AtariLynxCart => AtariLynxCart.Create(data),
WrapperType.BDPlusSVM => BDPlusSVM.Create(data),
WrapperType.BFPK => BFPK.Create(data),
WrapperType.BSP => BSP.Create(data),
WrapperType.BZip2 => BZip2.Create(data),
WrapperType.CDROM => CreateCDROMImageWrapper(data),
WrapperType.CFB => CFB.Create(data),
WrapperType.CHD => CHD.Create(data),
WrapperType.CIA => CIA.Create(data),
WrapperType.Executable => CreateExecutableWrapper(data),
WrapperType.FDS => FDS.Create(data),
WrapperType.GCF => GCF.Create(data),
WrapperType.GCZ => GCZ.Create(data),
WrapperType.GZip => GZip.Create(data),
WrapperType.InstallShieldArchiveV3 => InstallShieldArchiveV3.Create(data),
WrapperType.InstallShieldCAB => InstallShieldCabinet.Create(data),
WrapperType.IRD => IRD.Create(data),
WrapperType.ISO9660 => CreateDiscImageWrapper(data),
WrapperType.LDSCRYPT => LDSCRYPT.Create(data),
WrapperType.LZKWAJ => LZKWAJ.Create(data),
WrapperType.LZQBasic => LZQBasic.Create(data),
WrapperType.LZSZDD => LZSZDD.Create(data),
WrapperType.MicrosoftCAB => MicrosoftCabinet.Create(data),
WrapperType.MoPaQ => MoPaQ.Create(data),
WrapperType.N3DS => N3DS.Create(data),
WrapperType.NCF => NCF.Create(data),
WrapperType.NESCart => NESCart.Create(data),
WrapperType.Nitro => Nitro.Create(data),
WrapperType.NintendoDisc => NintendoDisc.Create(data),
WrapperType.PAK => PAK.Create(data),
WrapperType.PFF => PFF.Create(data),
WrapperType.PIC => PIC.Create(data),
WrapperType.PKZIP => PKZIP.Create(data),
WrapperType.PlayJAudioFile => PlayJAudioFile.Create(data),
WrapperType.PlayJPlaylist => PlayJPlaylist.Create(data),
WrapperType.QD => QD.Create(data),
WrapperType.Quantum => Quantum.Create(data),
WrapperType.RAR => RAR.Create(data),
WrapperType.RealArcadeInstaller => RealArcadeInstaller.Create(data),
WrapperType.RealArcadeMezzanine => RealArcadeMezzanine.Create(data),
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.Steam2Installer => Steam2Installer.Create(data),
WrapperType.STFS => STFS.Create(data),
WrapperType.TapeArchive => TapeArchive.Create(data),
WrapperType.VBSP => VBSP.Create(data),
WrapperType.VPK => VPK.Create(data),
WrapperType.WAD => WAD3.Create(data),
WrapperType.WiseOverlayHeader => WiseOverlayHeader.Create(data),
WrapperType.WiseScript => WiseScript.Create(data),
WrapperType.WIA => WIA.Create(data),
WrapperType.RVZ => WIA.Create(data),
WrapperType.XboxExecutable => XboxExecutable.Create(data),
WrapperType.XDVDFS => XDVDFS.Create(data),
WrapperType.XenonExecutable => XenonExecutable.Create(data),
WrapperType.XRD => XRD.Create(data),
WrapperType.XZ => XZ.Create(data),
WrapperType.XZP => XZP.Create(data),
WrapperType.ZArchive => ZArchive.Create(data),
WrapperType.ZSTD => ZSTD.Create(data),
// Unimplemented
WrapperType.UNKNOWN => null,
WrapperType.IniFile => null,// TODO: Implement wrapper
WrapperType.Textfile => null,// TODO: Implement wrapper
_ => null,
};
}
/// <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;
// Attempt to open standard ISO9660 CDROM wrapper
var cdromWrapper = CDROM.Create(stream);
if (cdromWrapper is not null)
return cdromWrapper;
// Reset position in stream
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
// 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);
// Try to get a PC Engine CDROM wrapper
// This reads a lot for detection, do this step last
var pcEngineDiscImageWrapper = PCEngineDiscImage.Create(stream);
if (pcEngineDiscImageWrapper is not null)
return pcEngineDiscImageWrapper;
// No known filesystems found
return null;
}
/// <summary>
/// Create an instance of a wrapper based on the disc image type
/// </summary>
/// <param name="stream">Stream data to parse</param>
/// <returns>IWrapper representing the disc image, null on error</returns>
public static IWrapper? CreateDiscImageWrapper(Stream? stream)
{
// If we have no stream
if (stream is null)
return null;
// Cache the current offset
long initialOffset = stream.Position;
// Try to get an Xbox disc image wrapper (must be before ISO9660)
var xboxWrapper = XboxISO.Create(stream);
if (xboxWrapper is not null)
return xboxWrapper;
// Reset position in stream
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
// Try to standard ISO9660 wrapper
var isoWrapper = ISO9660.Create(stream);
if (isoWrapper is not null)
return isoWrapper;
// Reset position in stream
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
// Try to get a UDF wrapper (discs without an ISO9660 file system)
var udfWrapper = UDF.Create(stream);
if (udfWrapper is not null)
return udfWrapper;
// Reset position in stream
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);
// Try to get a PC Engine CDROM disc image wrapper
// This reads a lot for detection, do this step later
var pcEngineCDROMWrapper = PCEngineCDROM.Create(stream);
if (pcEngineCDROMWrapper is not null)
return pcEngineCDROMWrapper;
// Reset position in stream
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
// Try NintendoDisc (GameCube / Wii)
// This has to be later due to slight possibility of false positives
var nintendoWrapper = NintendoDisc.Create(stream);
if (nintendoWrapper is not null)
return nintendoWrapper;
// No known filesystems found
return null;
}
/// <summary>
/// Create an instance of a wrapper based on the executable type
/// </summary>
/// <param name="stream">Stream data to parse</param>
/// <returns>IWrapper representing the executable, null on error</returns>
public static IWrapper? CreateExecutableWrapper(Stream? stream)
{
// If we have no stream
if (stream is null)
return null;
// Cache the current offset
long initialOffset = stream.Position;
// Try to get an MS-DOS wrapper first
var wrapper = MSDOS.Create(stream);
if (wrapper is null || wrapper is not MSDOS msdos)
return null;
// Check for a valid new executable address
if (initialOffset + msdos.Model.Header.NewExeHeaderAddr >= stream.Length)
return wrapper;
// Try to read the executable info
stream.SeekIfPossible(initialOffset + msdos.Model.Header.NewExeHeaderAddr, SeekOrigin.Begin);
var magic = stream.ReadBytes(4);
// If we didn't get valid data at the offset
if (magic is null)
{
return wrapper;
}
// New Executable
else if (magic.StartsWith(Data.Models.NewExecutable.Constants.SignatureBytes))
{
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
return NewExecutable.Create(stream);
}
// Linear Executable
else if (magic.StartsWith(Data.Models.LinearExecutable.Constants.LESignatureBytes)
|| magic.StartsWith(Data.Models.LinearExecutable.Constants.LXSignatureBytes))
{
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
return LinearExecutable.Create(stream);
}
// Portable Executable
else if (magic.StartsWith(Data.Models.PortableExecutable.Constants.SignatureBytes))
{
stream.SeekIfPossible(initialOffset, SeekOrigin.Begin);
return PortableExecutable.Create(stream);
}
// Everything else fails
return null;
}
/// <summary>
/// Get the supported file type for a magic string and an extension
/// </summary>
/// <remarks>Recommend sending in 16 bytes to check</remarks>
public static WrapperType GetFileType(byte[]? magic, string? extension)
{
// If we have an invalid magic byte array and extension
if (magic is null || magic.Length == 0 || extension is null)
return WrapperType.UNKNOWN;
// Normalize the extension
extension = extension.TrimStart('.').Trim();
// TODO: Use constants from Models here
#region AACSMediaKeyBlock
// Block starting with verify media key record
if (magic.StartsWith([0x81, 0x00, 0x00, 0x14]))
return WrapperType.AACSMediaKeyBlock;
// Block starting with type and version record
if (magic.StartsWith([0x10, 0x00, 0x00, 0x0C]))
return WrapperType.AACSMediaKeyBlock;
// Shares an extension with INF setup information so it can't be used accurately
// Blu-ray
// if (extension.Equals("inf", StringComparison.OrdinalIgnoreCase))
// return WrapperType.AACSMediaKeyBlock;
// HD-DVD
if (extension.Equals("aacs", StringComparison.OrdinalIgnoreCase))
return WrapperType.AACSMediaKeyBlock;
#endregion
// TODO: Figure out how to use constants here
#region Atari7800Cart
// Version 1 header
if (magic.StartsWith([0x01, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
// Version 2 header
if (magic.StartsWith([0x02, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
// Version 3 header
if (magic.StartsWith([0x03, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
// Version 4 header
if (magic.StartsWith([0x04, 0x41, 0x54, 0x41, 0x52, 0x49, 0x37, 0x38, 0x30, 0x30]))
return WrapperType.Atari7800Cart;
if (extension.Equals("a78", StringComparison.OrdinalIgnoreCase))
return WrapperType.Atari7800Cart;
#endregion
#region AtariLynxCart
// LNX header
if (magic.StartsWith(Data.Models.AtariLynx.Constants.MagicBytes))
return WrapperType.AtariLynxCart;
if (extension.Equals("lnx", StringComparison.OrdinalIgnoreCase))
return WrapperType.AtariLynxCart;
#endregion
#region BDPlusSVM
if (magic.StartsWith(Data.Models.BDPlus.Constants.SignatureBytes))
return WrapperType.BDPlusSVM;
if (extension.Equals("svm", StringComparison.OrdinalIgnoreCase))
return WrapperType.BDPlusSVM;
#endregion
#region BFPK
if (magic.StartsWith(Data.Models.BFPK.Constants.SignatureBytes))
return WrapperType.BFPK;
#endregion
// TODO: Use constants from Models here
#region BSP
// Shares a first 4 bytes with some .mc files
// Shares an extension with VBSP
if (magic.StartsWith([0x1d, 0x00, 0x00, 0x00]) && extension.Equals("bsp", StringComparison.OrdinalIgnoreCase))
return WrapperType.BSP;
// Shares a first 4 bytes with some .mc files
// Shares an extension with VBSP
if (magic.StartsWith([0x1e, 0x00, 0x00, 0x00]) && extension.Equals("bsp", StringComparison.OrdinalIgnoreCase))
return WrapperType.BSP;
#endregion
#region BZip2
if (magic.StartsWith(Data.Models.BZip2.Constants.SignatureBytes))
return WrapperType.BZip2;
if (extension.Equals("bz2", StringComparison.OrdinalIgnoreCase))
return WrapperType.BZip2;
#endregion
#region CDROM
// 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("skeleton", StringComparison.OrdinalIgnoreCase)))
{
return WrapperType.CDROM;
}
// Some CD-ROM images have no sync bytes in pregap
if (magic.StartsWith([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
&& (extension.Equals("bin", StringComparison.OrdinalIgnoreCase)
|| extension.Equals("skeleton", StringComparison.OrdinalIgnoreCase)))
{
return WrapperType.CDROM;
}
#endregion
#region CFB
if (magic.StartsWith(Data.Models.CFB.Constants.SignatureBytes))
return WrapperType.CFB;
// Installer package
if (extension.Equals("msi", StringComparison.OrdinalIgnoreCase))
return WrapperType.CFB;
// Merge module
else if (extension.Equals("msm", StringComparison.OrdinalIgnoreCase))
return WrapperType.CFB;
// Patch Package
else if (extension.Equals("msp", StringComparison.OrdinalIgnoreCase))
return WrapperType.CFB;
// Transform
else if (extension.Equals("mst", StringComparison.OrdinalIgnoreCase))
return WrapperType.CFB;
// Patch Creation Properties
else if (extension.Equals("pcp", StringComparison.OrdinalIgnoreCase))
return WrapperType.CFB;
#endregion
#region CHD
if (magic.StartsWith(Data.Models.CHD.Constants.SignatureBytes))
return WrapperType.CHD;
#endregion
#region CIA
if (extension.Equals("cia", StringComparison.OrdinalIgnoreCase))
return WrapperType.CIA;
#endregion
#region Executable
// DOS MZ executable file format (and descendants)
if (magic.StartsWith(Data.Models.MSDOS.Constants.SignatureBytes))
return WrapperType.Executable;
// None of the following are supported yet
/*
// Executable and Linkable Format
if (magic.StartsWith([0x7f, 0x45, 0x4c, 0x46]))
return WrapperType.Executable;
// Mach-O binary (32-bit)
if (magic.StartsWith([0xfe, 0xed, 0xfa, 0xce]))
return WrapperType.Executable;
// Mach-O binary (32-bit, reverse byte ordering scheme)
if (magic.StartsWith([0xce, 0xfa, 0xed, 0xfe]))
return WrapperType.Executable;
// Mach-O binary (64-bit)
if (magic.StartsWith([0xfe, 0xed, 0xfa, 0xcf]))
return WrapperType.Executable;
// Mach-O binary (64-bit, reverse byte ordering scheme)
if (magic.StartsWith([0xcf, 0xfa, 0xed, 0xfe]))
return WrapperType.Executable;
// Prefrred Executable File Format
if (magic.StartsWith([0x4a, 0x6f, 0x79, 0x21, 0x70, 0x65, 0x66, 0x66]))
return WrapperType.Executable;
*/
// DOS MZ executable file format (and descendants)
if (extension.Equals("exe", StringComparison.OrdinalIgnoreCase))
return WrapperType.Executable;
// DOS MZ library file format (and descendants)
if (extension.Equals("dll", StringComparison.OrdinalIgnoreCase))
return WrapperType.Executable;
#endregion
#region FDS
// fwNES FDS file with header
if (magic.StartsWith(Data.Models.NES.Constants.FDSSignatureBytes))
return WrapperType.FDS;
// fwNES FDS file with header
if (extension.Equals("fds", StringComparison.OrdinalIgnoreCase))
return WrapperType.FDS;
#endregion
// TODO: Use constants from Models here
#region GCF
if (magic.StartsWith([0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]))
return WrapperType.GCF;
if (extension.Equals("gcf", StringComparison.OrdinalIgnoreCase))
return WrapperType.GCF;
#endregion
#region GCZ
// GCZ magic cookie (0xB10BC001 stored little-endian)
if (magic.StartsWith([0x01, 0xC0, 0x0B, 0xB1]))
return WrapperType.GCZ;
if (extension.Equals("gcz", StringComparison.OrdinalIgnoreCase))
return WrapperType.GCZ;
#endregion
#region GZip
if (magic.StartsWith(Data.Models.GZIP.Constants.SignatureBytes))
return WrapperType.GZip;
if (extension.Equals("gz", StringComparison.OrdinalIgnoreCase))
return WrapperType.GZip;
#endregion
#region IniFile
if (extension.Equals("ini", StringComparison.OrdinalIgnoreCase))
return WrapperType.IniFile;
#endregion
#region InstallShieldArchiveV3
if (magic.StartsWith(Data.Models.InstallShieldArchiveV3.Constants.HeaderSignatureBytes))
return WrapperType.InstallShieldArchiveV3;
if (extension.Equals("z", StringComparison.OrdinalIgnoreCase))
return WrapperType.InstallShieldArchiveV3;
#endregion
#region InstallShieldCAB
if (magic.StartsWith(Data.Models.InstallShieldCabinet.Constants.SignatureBytes))
return WrapperType.InstallShieldCAB;
// Both InstallShieldCAB and MicrosoftCAB share the same extension
#endregion
#region IRD
if (magic.StartsWith([0x33, 0x49, 0x52, 0x44]))
return WrapperType.IRD;
if (extension.Equals("ird", StringComparison.OrdinalIgnoreCase))
return WrapperType.IRD;
#endregion
#region ISO9660
if (extension.Equals("iso", StringComparison.OrdinalIgnoreCase))
return WrapperType.ISO9660;
#endregion
#region LDSCRYPT
if (magic.StartsWith(Data.Models.LDSCRYPT.Constants.SignatureBytes))
return WrapperType.LDSCRYPT;
#endregion
#region LZ
if (magic.StartsWith(Data.Models.LZ.Constants.KWAJSignatureBytes))
return WrapperType.LZKWAJ;
if (magic.StartsWith(Data.Models.LZ.Constants.QBasicSignatureBytes))
return WrapperType.LZQBasic;
if (magic.StartsWith(Data.Models.LZ.Constants.SZDDSignatureBytes))
return WrapperType.LZSZDD;
#endregion
#region MicrosoftCAB
if (magic.StartsWith(Data.Models.MicrosoftCabinet.Constants.SignatureBytes))
return WrapperType.MicrosoftCAB;
// Both InstallShieldCAB and MicrosoftCAB share the same extension
#endregion
#region MoPaQ
if (magic.StartsWith(Data.Models.MoPaQ.Constants.ArchiveHeaderSignatureBytes))
return WrapperType.MoPaQ;
if (magic.StartsWith(Data.Models.MoPaQ.Constants.UserDataSignatureBytes))
return WrapperType.MoPaQ;
if (extension.Equals("mpq", StringComparison.OrdinalIgnoreCase))
return WrapperType.MoPaQ;
#endregion
#region N3DS
// 3DS cart image
if (extension.Equals("3ds", StringComparison.OrdinalIgnoreCase))
return WrapperType.N3DS;
else if (extension.Equals("cci", StringComparison.OrdinalIgnoreCase))
return WrapperType.N3DS;
// CIA package -- Not currently supported
// else if (extension.Equals("cia", StringComparison.OrdinalIgnoreCase))
// return WrapperType.N3DS;
#endregion
// TODO: Use constants from Models here
#region NCF
if (magic.StartsWith([0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]))
return WrapperType.NCF;
if (extension.Equals("ncf", StringComparison.OrdinalIgnoreCase))
return WrapperType.NCF;
#endregion
#region NESCart
// NES 1.0 and 2.0 image with header
if (magic.StartsWith(Data.Models.NES.Constants.CartSignatureBytes))
return WrapperType.NESCart;
// NES 1.0 and 2.0 image
if (extension.Equals("nes", StringComparison.OrdinalIgnoreCase))
return WrapperType.NESCart;
#endregion
#region NintendoDisc
// Wii disc magic at offset 0x018 (0x5D1C9EA3 stored big-endian on disc)
if (magic.Length > 0x1B && magic[0x18] == 0x5D && magic[0x19] == 0x1C && magic[0x1A] == 0x9E && magic[0x1B] == 0xA3)
return WrapperType.NintendoDisc;
// GameCube disc magic at offset 0x01C (0xC2339F3D stored big-endian on disc)
if (magic.Length > 0x1F && magic[0x1C] == 0xC2 && magic[0x1D] == 0x33 && magic[0x1E] == 0x9F && magic[0x1F] == 0x3D)
return WrapperType.NintendoDisc;
// GameCube/Wii disc by GameId prefix: first byte is a known title type code,
// bytes 1-2 are ASCII letters (region + developer), bytes 3-4 are ASCII digits or letters (title code),
// byte 5 is an ASCII digit (disc number). Covers redump ISOs that lack magic words.
if (magic.Length > 5
&& magic[0].IsNintendoDiscTitleType()
&& magic[1] >= 'A' && magic[1] <= 'Z'
&& ((magic[2] >= '0' && magic[2] <= '9') || (magic[2] >= 'A' && magic[2] <= 'Z'))
&& ((magic[3] >= '0' && magic[3] <= '9') || (magic[3] >= 'A' && magic[3] <= 'Z'))
&& ((magic[4] >= '0' && magic[4] <= '9') || (magic[4] >= 'A' && magic[4] <= 'Z'))
&& magic[5] >= '0' && magic[5] <= '9'
&& (extension.Equals("iso", StringComparison.OrdinalIgnoreCase)
|| extension.Equals("gcm", StringComparison.OrdinalIgnoreCase)))
{
return WrapperType.NintendoDisc;
}
// .gcm files are always GameCube disc images
if (extension.Equals("gcm", StringComparison.OrdinalIgnoreCase))
return WrapperType.NintendoDisc;
#endregion
#region Nitro
// DS cart image
if (extension.Equals("nds", StringComparison.OrdinalIgnoreCase))
return WrapperType.Nitro;
// DS development cart image
else if (extension.Equals("srl", StringComparison.OrdinalIgnoreCase))
return WrapperType.Nitro;
// DSi cart image
else if (extension.Equals("dsi", StringComparison.OrdinalIgnoreCase))
return WrapperType.Nitro;
// iQue DS cart image
else if (extension.Equals("ids", StringComparison.OrdinalIgnoreCase))
return WrapperType.Nitro;
#endregion
#region PAK
if (magic.StartsWith(Data.Models.PAK.Constants.SignatureBytes))
return WrapperType.PAK;
// Both PAK and Quantum share one extension
// if (extension.Equals("pak", StringComparison.OrdinalIgnoreCase))
// return WrapperType.PAK;
#endregion
// TODO: Use constants from Models here
#region PFF
// Version 0
if (magic.StartsWith([0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x30]))
return WrapperType.PFF;
// Version 2
if (magic.StartsWith([0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x32]))
return WrapperType.PFF;
// Version 3
if (magic.StartsWith([0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x33]))
return WrapperType.PFF;
// Version 4
if (magic.StartsWith([0x14, 0x00, 0x00, 0x00, 0x50, 0x46, 0x46, 0x34]))
return WrapperType.PFF;
if (extension.Equals("pff", StringComparison.OrdinalIgnoreCase))
return WrapperType.PFF;
#endregion
// TODO: Use constants from Models here
#region PKZIP
// PKZIP (Unknown)
if (magic.StartsWith([0x50, 0x4b, 0x00, 0x00]))
return WrapperType.PKZIP;
// PKZIP
if (magic.StartsWith(Data.Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
return WrapperType.PKZIP;
// PKZIP (Empty Archive)
if (magic.StartsWith(Data.Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
return WrapperType.PKZIP;
else if (magic.StartsWith(Data.Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
return WrapperType.PKZIP;
// PKZIP (Spanned Archive)
if (magic.StartsWith(Data.Models.PKZIP.Constants.DataDescriptorSignatureBytes))
return WrapperType.PKZIP;
// PKZIP
if (extension.Equals("zip", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Android package
if (extension.Equals("apk", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Java archive
if (extension.Equals("jar", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Google Earth saved working session file
if (extension.Equals("kmz", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// KWord document
if (extension.Equals("kwd", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Microsoft Office Open XML Format (OOXML) Document
if (extension.Equals("docx", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Microsoft Office Open XML Format (OOXML) Presentation
if (extension.Equals("pptx", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Microsoft Office Open XML Format (OOXML) Spreadsheet
if (extension.Equals("xlsx", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// OpenDocument text document
if (extension.Equals("odt", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// OpenDocument presentation
if (extension.Equals("odp", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// OpenDocument text document template
if (extension.Equals("ott", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Microsoft Open XML paper specification file
if (extension.Equals("oxps", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// OpenOffice spreadsheet
if (extension.Equals("sxc", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// OpenOffice drawing
if (extension.Equals("sxd", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// OpenOffice presentation
if (extension.Equals("sxi", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// OpenOffice word processing
if (extension.Equals("sxw", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// StarOffice spreadsheet
if (extension.Equals("sxc", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Windows Media compressed skin file
if (extension.Equals("wmz", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// Mozilla Browser Archive
if (extension.Equals("xpi", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// XML paper specification file
if (extension.Equals("xps", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
// eXact Packager Models
if (extension.Equals("xpt", StringComparison.OrdinalIgnoreCase))
return WrapperType.PKZIP;
#endregion
#region PLJ
// https://www.iana.org/assignments/media-types/audio/vnd.everad.plj
if (magic.StartsWith(Data.Models.PlayJ.Constants.SignatureBytes))
return WrapperType.PlayJAudioFile;
// https://www.iana.org/assignments/media-types/audio/vnd.everad.plj
if (extension.Equals("plj", StringComparison.OrdinalIgnoreCase))
return WrapperType.PlayJAudioFile;
#endregion
#region QD
if (extension.Equals("qd", StringComparison.OrdinalIgnoreCase))
return WrapperType.QD;
#endregion
#region Quantum
if (magic.StartsWith(Data.Models.Quantum.Constants.SignatureBytes))
return WrapperType.Quantum;
if (extension.Equals("q", StringComparison.OrdinalIgnoreCase))
return WrapperType.Quantum;
// Both PAK and Quantum share one extension
// if (extension.Equals("pak", StringComparison.OrdinalIgnoreCase))
// return WrapperType.Quantum;
#endregion
#region RAR
// RAR archive version 1.50 onwards
if (magic.StartsWith(Data.Models.RAR.Constants.OldSignatureBytes))
return WrapperType.RAR;
// RAR archive version 5.0 onwards
if (magic.StartsWith(Data.Models.RAR.Constants.NewSignatureBytes))
return WrapperType.RAR;
if (extension.Equals("rar", StringComparison.OrdinalIgnoreCase))
return WrapperType.RAR;
#endregion
#region RealArcade
// RASGI2.0
// Found in the ".rgs files in IA item "Nova_RealArcadeCD_USA".
if (magic.StartsWith(Data.Models.RealArcade.Constants.RgsSignatureBytes))
return WrapperType.RealArcadeInstaller;
// XZip2.0
// Found in the ".mez" files in IA item "Nova_RealArcadeCD_USA".
if (magic.StartsWith(Data.Models.RealArcade.Constants.MezzanineSignatureBytes))
return WrapperType.RealArcadeMezzanine;
#endregion
#region SecuROM DFA
if (magic.StartsWith(Data.Models.SecuROM.Constants.DFAMagicBytes))
return WrapperType.SecuROMDFA;
#endregion
#region SevenZip
if (magic.StartsWith(Data.Models.SevenZip.Constants.SignatureBytes))
return WrapperType.SevenZip;
if (extension.Equals("7z", StringComparison.OrdinalIgnoreCase))
return WrapperType.SevenZip;
#endregion
#region SFFS
// Found in Redump entry 81756, confirmed to be "StarForce Filesystem" by PiD.
if (magic.StartsWith(Data.Models.StarForce.Constants.SignatureBytes))
return WrapperType.SFFS;
#endregion
#region SkuSis
if (magic.StartsWith(Data.Models.VDF.Constants.SteamSimSidSisSignatureBytes))
return WrapperType.SkuSis;
if (magic.StartsWith(Data.Models.VDF.Constants.SteamCsmCsdSisSignatureBytes))
return WrapperType.SkuSis;
#endregion
#region SGA
if (magic.StartsWith(Data.Models.SGA.Constants.SignatureBytes))
return WrapperType.SGA;
if (extension.Equals("sga", StringComparison.OrdinalIgnoreCase))
return WrapperType.SGA;
#endregion
#region Skeleton
if (extension.Equals("skeleton", StringComparison.OrdinalIgnoreCase))
return WrapperType.Skeleton;
#endregion
#region Steam2Installer
if (magic.StartsWith(Data.Models.Steam2Installer.Constants.SimSignatureBytes))
return WrapperType.Steam2Installer;
#endregion
#region STFS
// LIVE
if (magic.StartsWith(Data.Models.STFS.Constants.MagicBytesLIVE))
return WrapperType.STFS;
// PIRS
if (magic.StartsWith(Data.Models.STFS.Constants.MagicBytesPIRS))
return WrapperType.STFS;
// "CON "
if (magic.StartsWith(Data.Models.STFS.Constants.MagicBytesCON))
return WrapperType.STFS;
#endregion
// TODO: Use constants from Models here
#region TapeArchive
if (magic.StartsWith([0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30]))
return WrapperType.TapeArchive;
if (magic.StartsWith([0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00]))
return WrapperType.TapeArchive;
if (extension.Equals("tar", StringComparison.OrdinalIgnoreCase))
return WrapperType.TapeArchive;
#endregion
// TODO: Use constants from Models here
#region Textfile
// Not all textfiles can be determined through magic number
// HTML
if (magic.StartsWith([0x3c, 0x68, 0x74, 0x6d, 0x6c]))
return WrapperType.Textfile;
// HTML and XML
if (magic.StartsWith([0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45]))
return WrapperType.Textfile;
// InstallShield Compiled Rules
if (magic.StartsWith([0x61, 0x4C, 0x75, 0x5A]))
return WrapperType.Textfile;
// Microsoft Office File (old)
if (magic.StartsWith([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]))
return WrapperType.Textfile;
// Rich Text File
if (magic.StartsWith([0x7b, 0x5c, 0x72, 0x74, 0x66, 0x31]))
return WrapperType.Textfile;
// Windows Help File
if (magic.StartsWith([0x3F, 0x5F, 0x03, 0x00]))
return WrapperType.Textfile;
// XML
// "<?xml"
if (magic.StartsWith([0x3C, 0x3F, 0x78, 0x6D, 0x6C]))
return WrapperType.Textfile;
// "<?xml" in UTF-16 encoding
if (magic.StartsWith([0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
return WrapperType.Textfile;
// "<?xml" in UTF-16 encoding with byte order marks
if (magic.StartsWith([0xFF, 0xFE, 0x3C, 0x00, 0x3F, 0x00, 0x78, 0x00, 0x6D, 0x00, 0x6C, 0x00]))
return WrapperType.Textfile;
// "Description in Zip"
if (extension.Equals("diz", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// Generic textfile (no header)
if (extension.Equals("txt", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// HTML
if (extension.Equals("htm", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
if (extension.Equals("html", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// InstallShield Script
if (extension.Equals("ins", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// Microsoft Office File (old)
if (extension.Equals("doc", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// Property list
if (extension.Equals("plist", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// Rich Text File
if (extension.Equals("rtf", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// Setup information
if (extension.Equals("inf", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// Windows Help File
if (extension.Equals("hlp", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// WZC
if (extension.Equals("wzc", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
// XML
if (extension.Equals("xml", StringComparison.OrdinalIgnoreCase))
return WrapperType.Textfile;
#endregion
#region VBSP
if (magic.StartsWith(Data.Models.BSP.Constants.SignatureBytes))
return WrapperType.VBSP;
// Shares an extension with BSP
if (extension.Equals("bsp", StringComparison.OrdinalIgnoreCase))
return WrapperType.VBSP;
#endregion
#region VPK
if (magic.StartsWith(Data.Models.VPK.Constants.SignatureBytes))
return WrapperType.VPK;
// Common extension so this cannot be used accurately
// if (extension.Equals("vpk", StringComparison.OrdinalIgnoreCase))
// return WrapperType.VPK;
#endregion
#region WAD
if (magic.StartsWith(Data.Models.WAD3.Constants.SignatureBytes))
return WrapperType.WAD;
// Common extension so this cannot be used accurately
// if (extension.Equals("wad", StringComparison.OrdinalIgnoreCase))
// return WrapperType.WAD;
#endregion
#region WIA
// WIA magic ("WIA\x01" stored little-endian: 0x01414957)
if (magic.StartsWith([0x57, 0x49, 0x41, 0x01]))
return WrapperType.WIA;
// RVZ magic ("RVZ\x01" stored little-endian: 0x015A5652)
if (magic.StartsWith([0x52, 0x56, 0x5A, 0x01]))
return WrapperType.RVZ;
if (extension.Equals("wia", StringComparison.OrdinalIgnoreCase))
return WrapperType.WIA;
if (extension.Equals("rvz", StringComparison.OrdinalIgnoreCase))
return WrapperType.RVZ;
#endregion
#region XboxExecutable
if (magic.StartsWith(Data.Models.XboxExecutable.Constants.MagicBytes))
return WrapperType.XboxExecutable;
if (extension.Equals("xbe", StringComparison.OrdinalIgnoreCase))
return WrapperType.XboxExecutable;
#endregion
#region XDVDFS
// TODO: Overload .iso wrapper to call both ISO and XDVDFS wrappers if present
if (extension.Equals("xiso", StringComparison.OrdinalIgnoreCase))
return WrapperType.XDVDFS;
if (extension.Equals("xdvdfs", StringComparison.OrdinalIgnoreCase))
return WrapperType.XDVDFS;
#endregion
#region XenonExecutable
if (magic.StartsWith(Data.Models.XenonExecutable.Constants.MagicBytes))
return WrapperType.XenonExecutable;
if (extension.Equals("xex", StringComparison.OrdinalIgnoreCase))
return WrapperType.XenonExecutable;
#endregion
#region XRD
if (magic.StartsWith(Data.Models.XRD.Constants.MagicBytes))
return WrapperType.XRD;
if (extension.Equals("xrd", StringComparison.OrdinalIgnoreCase))
return WrapperType.XRD;
#endregion
#region XZ
if (magic.StartsWith(Data.Models.XZ.Constants.HeaderSignatureBytes))
return WrapperType.XZ;
if (extension.Equals("xz", StringComparison.OrdinalIgnoreCase))
return WrapperType.XZ;
#endregion
#region XZP
if (magic.StartsWith(Data.Models.XZP.Constants.HeaderSignatureBytes))
return WrapperType.XZP;
if (extension.Equals("xzp", StringComparison.OrdinalIgnoreCase))
return WrapperType.XZP;
#endregion
#region ZSTD
// ZArchive magic is the final 4 bytes of the file: [0x16, 0x9F, 0x52, 0xD6]
if (extension.Equals("zar", StringComparison.OrdinalIgnoreCase))
return WrapperType.ZArchive;
#endregion
#region ZSTD
if (magic.StartsWith([null, 0xB5, 0x2F, 0xFD]))
return WrapperType.ZSTD;
#endregion
// We couldn't find a supported match
return WrapperType.UNKNOWN;
}
/// <summary>
/// Get a file wrapper based on an offset within a stream
/// </summary>
public static IWrapper? GetFileWrapper(Stream? data, long offset, string filename)
{
// Null streams can't create wrappers
if (data is null)
return null;
data.Seek(offset, SeekOrigin.Begin);
byte[] magic = data.PeekBytes(16);
string extension = Path.GetExtension(filename).TrimStart('.');
WrapperType ft = GetFileType(magic, extension);
return CreateWrapper(ft, data);
}
}
}