Compare commits

..

5 Commits
1.1.1 ... 1.1.2

Author SHA1 Message Date
Matt Nadareski
327fd68f04 Bump version 2023-09-13 15:31:24 -04:00
Matt Nadareski
f12d48861f Port XMID/XeMID serialization from MPF 2023-09-13 15:30:32 -04:00
Matt Nadareski
5a613be9bf Port PIC serialization from MPF 2023-09-13 15:11:44 -04:00
Matt Nadareski
226031f3bd Port cuesheet serialization from MPF 2023-09-13 14:55:52 -04:00
Matt Nadareski
e5edf43624 Update Models to 1.1.2 2023-09-13 13:58:42 -04:00
13 changed files with 1736 additions and 2 deletions

View File

@@ -0,0 +1,625 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using SabreTools.Models.CueSheets;
namespace SabreTools.Serialization.Files
{
public partial class CueSheet : IFileSerializer<Models.CueSheets.CueSheet>
{
/// <inheritdoc/>
#if NET48
public Models.CueSheets.CueSheet Deserialize(string path)
#else
public Models.CueSheets.CueSheet? Deserialize(string? path)
#endif
{
// Check that the file exists
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return null;
// Check the extension
string ext = Path.GetExtension(path).TrimStart('.');
if (!string.Equals(ext, "cue", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(ext, "txt", StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Create the holding objects
var cueSheet = new Models.CueSheets.CueSheet();
var cueFiles = new List<CueFile>();
// Open the file and begin reading
string[] cueLines = File.ReadAllLines(path);
for (int i = 0; i < cueLines.Length; i++)
{
string line = cueLines[i].Trim();
// http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes
string[] splitLine = Regex
.Matches(line, @"[^\s""]+|""[^""]*""")
.Cast<Match>()
.Select(m => m.Groups[0].Value)
.ToArray();
// If we have an empty line, we skip
if (string.IsNullOrWhiteSpace(line))
continue;
switch (splitLine[0])
{
// Read comments
case "REM":
// We ignore all comments for now
break;
// Read MCN
case "CATALOG":
if (splitLine.Length < 2)
throw new FormatException($"CATALOG line malformed: {line}");
cueSheet.Catalog = splitLine[1];
break;
// Read external CD-Text file path
case "CDTEXTFILE":
if (splitLine.Length < 2)
throw new FormatException($"CDTEXTFILE line malformed: {line}");
cueSheet.CdTextFile = splitLine[1];
break;
// Read CD-Text enhanced performer
case "PERFORMER":
if (splitLine.Length < 2)
throw new FormatException($"PERFORMER line malformed: {line}");
cueSheet.Performer = splitLine[1];
break;
// Read CD-Text enhanced songwriter
case "SONGWRITER":
if (splitLine.Length < 2)
throw new FormatException($"SONGWRITER line malformed: {line}");
cueSheet.Songwriter = splitLine[1];
break;
// Read CD-Text enhanced title
case "TITLE":
if (splitLine.Length < 2)
throw new FormatException($"TITLE line malformed: {line}");
cueSheet.Title = splitLine[1];
break;
// Read file information
case "FILE":
if (splitLine.Length < 3)
throw new FormatException($"FILE line malformed: {line}");
var file = CreateCueFile(splitLine[1], splitLine[2], cueLines, ref i);
if (file == default)
throw new FormatException($"FILE line malformed: {line}");
cueFiles.Add(file);
break;
}
}
cueSheet.Files = cueFiles.ToArray();
return cueSheet;
}
/// <summary>
/// Fill a FILE from an array of lines
/// </summary>
/// <param name="fileName">File name to set</param>
/// <param name="fileType">File type to set</param>
/// <param name="cueLines">Lines array to pull from</param>
/// <param name="i">Reference to index in array</param>
#if NET48
private static CueFile CreateCueFile(string fileName, string fileType, string[] cueLines, ref int i)
#else
private static CueFile? CreateCueFile(string fileName, string fileType, string[]? cueLines, ref int i)
#endif
{
// Check the required parameters
if (cueLines == null)
throw new ArgumentNullException(nameof(cueLines));
else if (i < 0 || i > cueLines.Length)
throw new IndexOutOfRangeException();
// Create the holding objects
var cueFile = new CueFile();
var cueTracks = new List<CueTrack>();
// Set the current fields
cueFile.FileName = fileName.Trim('"');
cueFile.FileType = GetFileType(fileType);
// Increment to start
i++;
for (; i < cueLines.Length; i++)
{
string line = cueLines[i].Trim();
string[] splitLine = line.Split(' ');
// If we have an empty line, we skip
if (string.IsNullOrWhiteSpace(line))
continue;
switch (splitLine[0])
{
// Read comments
case "REM":
// We ignore all comments for now
break;
// Read track information
case "TRACK":
if (splitLine.Length < 3)
throw new FormatException($"TRACK line malformed: {line}");
var track = CreateCueTrack(splitLine[1], splitLine[2], cueLines, ref i);
if (track == default)
throw new FormatException($"TRACK line malformed: {line}");
cueTracks.Add(track);
break;
// Default means return
default:
i--;
return null;
}
}
cueFile.Tracks = cueTracks.ToArray();
return cueFile;
}
/// <summary>
/// Fill a TRACK from an array of lines
/// </summary>
/// <param name="number">Number to set</param>
/// <param name="dataType">Data type to set</param>
/// <param name="cueLines">Lines array to pull from</param>
/// <param name="i">Reference to index in array</param>
#if NET48
private static CueTrack CreateCueTrack(string number, string dataType, string[] cueLines, ref int i)
#else
private static CueTrack? CreateCueTrack(string number, string dataType, string[]? cueLines, ref int i)
#endif
{
// Check the required parameters
if (cueLines == null)
throw new ArgumentNullException(nameof(cueLines));
else if (i < 0 || i > cueLines.Length)
throw new IndexOutOfRangeException();
// Set the current fields
if (!int.TryParse(number, out int parsedNumber))
throw new ArgumentException($"Number was not a number: {number}");
else if (parsedNumber < 1 || parsedNumber > 99)
throw new IndexOutOfRangeException($"Index must be between 1 and 99: {parsedNumber}");
// Create the holding objects
var cueTrack = new CueTrack();
var cueIndices = new List<CueIndex>();
cueTrack.Number = parsedNumber;
cueTrack.DataType = GetDataType(dataType);
// Increment to start
i++;
for (; i < cueLines.Length; i++)
{
string line = cueLines[i].Trim();
string[] splitLine = line.Split(' ');
// If we have an empty line, we skip
if (string.IsNullOrWhiteSpace(line))
continue;
switch (splitLine[0])
{
// Read comments
case "REM":
// We ignore all comments for now
break;
// Read flag information
case "FLAGS":
if (splitLine.Length < 2)
throw new FormatException($"FLAGS line malformed: {line}");
cueTrack.Flags = GetFlags(splitLine);
break;
// Read International Standard Recording Code
case "ISRC":
if (splitLine.Length < 2)
throw new FormatException($"ISRC line malformed: {line}");
cueTrack.ISRC = splitLine[1];
break;
// Read CD-Text enhanced performer
case "PERFORMER":
if (splitLine.Length < 2)
throw new FormatException($"PERFORMER line malformed: {line}");
cueTrack.Performer = splitLine[1];
break;
// Read CD-Text enhanced songwriter
case "SONGWRITER":
if (splitLine.Length < 2)
throw new FormatException($"SONGWRITER line malformed: {line}");
cueTrack.Songwriter = splitLine[1];
break;
// Read CD-Text enhanced title
case "TITLE":
if (splitLine.Length < 2)
throw new FormatException($"TITLE line malformed: {line}");
cueTrack.Title = splitLine[1];
break;
// Read pregap information
case "PREGAP":
if (splitLine.Length < 2)
throw new FormatException($"PREGAP line malformed: {line}");
var pregap = CreatePreGap(splitLine[1]);
if (pregap == default)
throw new FormatException($"PREGAP line malformed: {line}");
cueTrack.PreGap = pregap;
break;
// Read index information
case "INDEX":
if (splitLine.Length < 3)
throw new FormatException($"INDEX line malformed: {line}");
var index = CreateCueIndex(splitLine[1], splitLine[2]);
if (index == default)
throw new FormatException($"INDEX line malformed: {line}");
cueIndices.Add(index);
break;
// Read postgap information
case "POSTGAP":
if (splitLine.Length < 2)
throw new FormatException($"POSTGAP line malformed: {line}");
var postgap = CreatePostGap(splitLine[1]);
if (postgap == default)
throw new FormatException($"POSTGAP line malformed: {line}");
cueTrack.PostGap = postgap;
break;
// Default means return
default:
i--;
return null;
}
}
cueTrack.Indices = cueIndices.ToArray();
return cueTrack;
}
/// <summary>
/// Create a PREGAP from a mm:ss:ff length
/// </summary>
/// <param name="length">String to get length information from</param>
#if NET48
private static PreGap CreatePreGap(string length)
#else
private static PreGap CreatePreGap(string? length)
#endif
{
// Ignore empty lines
if (string.IsNullOrWhiteSpace(length))
throw new ArgumentException("Length was null or whitespace");
// Ignore lines that don't contain the correct information
if (length.Length != 8 || length.Count(c => c == ':') != 2)
throw new FormatException($"Length was not in a recognized format: {length}");
// Split the line
string[] splitLength = length.Split(':');
if (splitLength.Length != 3)
throw new FormatException($"Length was not in a recognized format: {length}");
// Parse the lengths
int[] lengthSegments = new int[3];
// Minutes
if (!int.TryParse(splitLength[0], out lengthSegments[0]))
throw new FormatException($"Minutes segment was not a number: {splitLength[0]}");
else if (lengthSegments[0] < 0)
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
// Seconds
if (!int.TryParse(splitLength[1], out lengthSegments[1]))
throw new FormatException($"Seconds segment was not a number: {splitLength[1]}");
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
// Frames
if (!int.TryParse(splitLength[2], out lengthSegments[2]))
throw new FormatException($"Frames segment was not a number: {splitLength[2]}");
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
// Set the values
var preGap = new PreGap
{
Minutes = lengthSegments[0],
Seconds = lengthSegments[1],
Frames = lengthSegments[2],
};
return preGap;
}
/// <summary>
/// Fill a INDEX from an array of lines
/// </summary>
/// <param name="index">Index to set</param>
/// <param name="startTime">Start time to set</param>
#if NET48
private static CueIndex CreateCueIndex(string index, string startTime)
#else
private static CueIndex CreateCueIndex(string? index, string? startTime)
#endif
{
// Set the current fields
if (!int.TryParse(index, out int parsedIndex))
throw new ArgumentException($"Index was not a number: {index}");
else if (parsedIndex < 0 || parsedIndex > 99)
throw new IndexOutOfRangeException($"Index must be between 0 and 99: {parsedIndex}");
// Ignore empty lines
if (string.IsNullOrWhiteSpace(startTime))
throw new ArgumentException("Start time was null or whitespace");
// Ignore lines that don't contain the correct information
if (startTime.Length != 8 || startTime.Count(c => c == ':') != 2)
throw new FormatException($"Start time was not in a recognized format: {startTime}");
// Split the line
string[] splitTime = startTime.Split(':');
if (splitTime.Length != 3)
throw new FormatException($"Start time was not in a recognized format: {startTime}");
// Parse the lengths
int[] lengthSegments = new int[3];
// Minutes
if (!int.TryParse(splitTime[0], out lengthSegments[0]))
throw new FormatException($"Minutes segment was not a number: {splitTime[0]}");
else if (lengthSegments[0] < 0)
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
// Seconds
if (!int.TryParse(splitTime[1], out lengthSegments[1]))
throw new FormatException($"Seconds segment was not a number: {splitTime[1]}");
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
// Frames
if (!int.TryParse(splitTime[2], out lengthSegments[2]))
throw new FormatException($"Frames segment was not a number: {splitTime[2]}");
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
// Set the values
var cueIndex = new CueIndex
{
Index = parsedIndex,
Minutes = lengthSegments[0],
Seconds = lengthSegments[1],
Frames = lengthSegments[2],
};
return cueIndex;
}
/// <summary>
/// Create a POSTGAP from a mm:ss:ff length
/// </summary>
/// <param name="length">String to get length information from</param>
#if NET48
private static PostGap CreatePostGap(string length)
#else
private static PostGap CreatePostGap(string? length)
#endif
{
// Ignore empty lines
if (string.IsNullOrWhiteSpace(length))
throw new ArgumentException("Length was null or whitespace");
// Ignore lines that don't contain the correct information
if (length.Length != 8 || length.Count(c => c == ':') != 2)
throw new FormatException($"Length was not in a recognized format: {length}");
// Split the line
string[] splitLength = length.Split(':');
if (splitLength.Length != 3)
throw new FormatException($"Length was not in a recognized format: {length}");
// Parse the lengths
int[] lengthSegments = new int[3];
// Minutes
if (!int.TryParse(splitLength[0], out lengthSegments[0]))
throw new FormatException($"Minutes segment was not a number: {splitLength[0]}");
else if (lengthSegments[0] < 0)
throw new IndexOutOfRangeException($"Minutes segment must be 0 or greater: {lengthSegments[0]}");
// Seconds
if (!int.TryParse(splitLength[1], out lengthSegments[1]))
throw new FormatException($"Seconds segment was not a number: {splitLength[1]}");
else if (lengthSegments[1] < 0 || lengthSegments[1] > 60)
throw new IndexOutOfRangeException($"Seconds segment must be between 0 and 60: {lengthSegments[1]}");
// Frames
if (!int.TryParse(splitLength[2], out lengthSegments[2]))
throw new FormatException($"Frames segment was not a number: {splitLength[2]}");
else if (lengthSegments[2] < 0 || lengthSegments[2] > 75)
throw new IndexOutOfRangeException($"Frames segment must be between 0 and 75: {lengthSegments[2]}");
// Set the values
var postGap = new PostGap
{
Minutes = lengthSegments[0],
Seconds = lengthSegments[1],
Frames = lengthSegments[2],
};
return postGap;
}
#region Helpers
/// <summary>
/// Get the file type from a given string
/// </summary>
/// <param name="fileType">String to get value from</param>
/// <returns>CueFileType, if possible</returns>
#if NET48
private static CueFileType GetFileType(string fileType)
#else
private static CueFileType GetFileType(string? fileType)
#endif
{
switch (fileType?.ToLowerInvariant())
{
case "binary":
return CueFileType.BINARY;
case "motorola":
return CueFileType.MOTOROLA;
case "aiff":
return CueFileType.AIFF;
case "wave":
return CueFileType.WAVE;
case "mp3":
return CueFileType.MP3;
default:
return CueFileType.BINARY;
}
}
/// <summary>
/// Get the data type from a given string
/// </summary>
/// <param name="dataType">String to get value from</param>
/// <returns>CueTrackDataType, if possible (default AUDIO)</returns>
#if NET48
private static CueTrackDataType GetDataType(string dataType)
#else
private static CueTrackDataType GetDataType(string? dataType)
#endif
{
switch (dataType?.ToLowerInvariant())
{
case "audio":
return CueTrackDataType.AUDIO;
case "cdg":
return CueTrackDataType.CDG;
case "mode1/2048":
return CueTrackDataType.MODE1_2048;
case "mode1/2352":
return CueTrackDataType.MODE1_2352;
case "mode2/2336":
return CueTrackDataType.MODE2_2336;
case "mode2/2352":
return CueTrackDataType.MODE2_2352;
case "cdi/2336":
return CueTrackDataType.CDI_2336;
case "cdi/2352":
return CueTrackDataType.CDI_2352;
default:
return CueTrackDataType.AUDIO;
}
}
/// <summary>
/// Get the flag value for an array of strings
/// </summary>
/// <param name="flagStrings">Possible flags as strings</param>
/// <returns>CueTrackFlag value representing the strings, if possible</returns>
#if NET48
private static CueTrackFlag GetFlags(string[] flagStrings)
#else
private static CueTrackFlag GetFlags(string?[]? flagStrings)
#endif
{
CueTrackFlag flag = 0;
if (flagStrings == null)
return flag;
#if NET48
foreach (string flagString in flagStrings)
#else
foreach (string? flagString in flagStrings)
#endif
{
switch (flagString?.ToLowerInvariant())
{
case "flags":
// No-op since this is the start of the line
break;
case "dcp":
flag |= CueTrackFlag.DCP;
break;
case "4ch":
flag |= CueTrackFlag.FourCH;
break;
case "pre":
flag |= CueTrackFlag.PRE;
break;
case "scms":
flag |= CueTrackFlag.SCMS;
break;
case "data":
flag |= CueTrackFlag.DATA;
break;
}
}
return flag;
}
#endregion
}
}

View File

@@ -0,0 +1,30 @@
using System.IO;
namespace SabreTools.Serialization.Files
{
public partial class CueSheet : IFileSerializer<Models.CueSheets.CueSheet>
{
/// <inheritdoc/>
#if NET48
public bool Serialize(Models.CueSheets.CueSheet obj, string path)
#else
public bool Serialize(Models.CueSheets.CueSheet? obj, string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return false;
using (var stream = new Streams.CueSheet().Serialize(obj))
{
if (stream == null)
return false;
using (var fs = File.OpenWrite(path))
{
stream.CopyTo(fs);
return true;
}
}
}
}
}

20
Files/PIC.Deserializer.cs Normal file
View File

@@ -0,0 +1,20 @@
using SabreTools.Models.PIC;
namespace SabreTools.Serialization.Files
{
public partial class PIC : IFileSerializer<DiscInformation>
{
/// <inheritdoc/>
#if NET48
public DiscInformation Deserialize(string path)
#else
public DiscInformation? Deserialize(string? path)
#endif
{
using (var stream = PathProcessor.OpenStream(path))
{
return new Streams.PIC().Deserialize(stream);
}
}
}
}

30
Files/PIC.Serializer.cs Normal file
View File

@@ -0,0 +1,30 @@
using SabreTools.Models.PIC;
namespace SabreTools.Serialization.Files
{
public partial class PIC : IFileSerializer<DiscInformation>
{
/// <inheritdoc/>
#if NET48
public bool Serialize(DiscInformation obj, string path)
#else
public bool Serialize(DiscInformation? obj, string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return false;
using (var stream = new Streams.PIC().Serialize(obj))
{
if (stream == null)
return false;
using (var fs = System.IO.File.OpenWrite(path))
{
stream.CopyTo(fs);
return true;
}
}
}
}
}

214
Files/XMID.Deserializer.cs Normal file
View File

@@ -0,0 +1,214 @@
namespace SabreTools.Serialization.Files
{
public partial class XMID : IFileSerializer<Models.Xbox.XMID>
{
/// <inheritdoc/>
/// <remarks>This treats the input path like a parseable string</remarks>
#if NET48
public Models.Xbox.XMID Deserialize(string path)
#else
public Models.Xbox.XMID? Deserialize(string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return null;
string xmid = path.TrimEnd('\0');
if (string.IsNullOrWhiteSpace(xmid))
return null;
return ParseXMID(xmid);
}
/// <summary>
/// Parse an XGD2/3 XMID string
/// </summary>
/// <param name="xmidString">XMID string to attempt to parse</param>
/// <returns>Filled XMID on success, null on error</returns>
#if NET48
private static Models.Xbox.XMID ParseXMID(string xmidString)
#else
private static Models.Xbox.XMID? ParseXMID(string? xmidString)
#endif
{
if (xmidString == null || xmidString.Length != 8)
return null;
var xmid = new Models.Xbox.XMID();
xmid.PublisherIdentifier = xmidString.Substring(0, 2);
if (string.IsNullOrEmpty(PublisherName(xmid)))
return null;
xmid.GameID = xmidString.Substring(2, 3);
xmid.VersionNumber = xmidString.Substring(5, 2);
xmid.RegionIdentifier = xmidString[7];
if (InternalRegion(xmid) == null)
return null;
return xmid;
}
#region Helpers
/// <summary>
/// Human-readable name derived from the publisher identifier
/// </summary>
#if NET48
public static string PublisherName(Models.Xbox.XMID xmid) => GetPublisher(xmid.PublisherIdentifier);
#else
public static string? PublisherName(Models.Xbox.XMID xmid) => GetPublisher(xmid.PublisherIdentifier);
#endif
/// <summary>
/// Internally represented region
/// </summary>
#if NET48
public static string InternalRegion(Models.Xbox.XMID xmid) => GetRegion(xmid.RegionIdentifier);
#else
public static string? InternalRegion(Models.Xbox.XMID xmid) => GetRegion(xmid.RegionIdentifier);
#endif
/// <summary>
/// Get the full name of the publisher from the 2-character identifier
/// </summary>
/// <param name="publisherIdentifier">Case-sensitive 2-character identifier</param>
/// <returns>Publisher name, if possible</returns>
/// <see cref="https://xboxdevwiki.net/Xbe#Title_ID"/>
#if NET48
private static string GetPublisher(string publisherIdentifier)
#else
private static string? GetPublisher(string? publisherIdentifier)
#endif
{
switch (publisherIdentifier)
{
case "AC": return "Acclaim Entertainment";
case "AH": return "ARUSH Entertainment";
case "AQ": return "Aqua System";
case "AS": return "ASK";
case "AT": return "Atlus";
case "AV": return "Activision";
case "AY": return "Aspyr Media";
case "BA": return "Bandai";
case "BL": return "Black Box";
case "BM": return "BAM! Entertainment";
case "BR": return "Broccoli Co.";
case "BS": return "Bethesda Softworks";
case "BU": return "Bunkasha Co.";
case "BV": return "Buena Vista Games";
case "BW": return "BBC Multimedia";
case "BZ": return "Blizzard";
case "CC": return "Capcom";
case "CK": return "Kemco Corporation"; // TODO: Confirm
case "CM": return "Codemasters";
case "CV": return "Crave Entertainment";
case "DC": return "DreamCatcher Interactive";
case "DX": return "Davilex";
case "EA": return "Electronic Arts (EA)";
case "EC": return "Encore inc";
case "EL": return "Enlight Software";
case "EM": return "Empire Interactive";
case "ES": return "Eidos Interactive";
case "FI": return "Fox Interactive";
case "FS": return "From Software";
case "GE": return "Genki Co.";
case "GV": return "Groove Games";
case "HE": return "Tru Blu (Entertainment division of Home Entertainment Suppliers)";
case "HP": return "Hip games";
case "HU": return "Hudson Soft";
case "HW": return "Highwaystar";
case "IA": return "Mad Catz Interactive";
case "IF": return "Idea Factory";
case "IG": return "Infogrames";
case "IL": return "Interlex Corporation";
case "IM": return "Imagine Media";
case "IO": return "Ignition Entertainment";
case "IP": return "Interplay Entertainment";
case "IX": return "InXile Entertainment"; // TODO: Confirm
case "JA": return "Jaleco";
case "JW": return "JoWooD";
case "KB": return "Kemco"; // TODO: Confirm
case "KI": return "Kids Station Inc."; // TODO: Confirm
case "KN": return "Konami";
case "KO": return "KOEI";
case "KU": return "Kobi and / or GAE (formerly Global A Entertainment)"; // TODO: Confirm
case "LA": return "LucasArts";
case "LS": return "Black Bean Games (publishing arm of Leader S.p.A.)";
case "MD": return "Metro3D";
case "ME": return "Medix";
case "MI": return "Microïds";
case "MJ": return "Majesco Entertainment";
case "MM": return "Myelin Media";
case "MP": return "MediaQuest"; // TODO: Confirm
case "MS": return "Microsoft Game Studios";
case "MW": return "Midway Games";
case "MX": return "Empire Interactive"; // TODO: Confirm
case "NK": return "NewKidCo";
case "NL": return "NovaLogic";
case "NM": return "Namco";
case "OX": return "Oxygen Interactive";
case "PC": return "Playlogic Entertainment";
case "PL": return "Phantagram Co., Ltd.";
case "RA": return "Rage";
case "SA": return "Sammy";
case "SC": return "SCi Games";
case "SE": return "SEGA";
case "SN": return "SNK";
case "SS": return "Simon & Schuster";
case "SU": return "Success Corporation";
case "SW": return "Swing! Deutschland";
case "TA": return "Takara";
case "TC": return "Tecmo";
case "TD": return "The 3DO Company (or just 3DO)";
case "TK": return "Takuyo";
case "TM": return "TDK Mediactive";
case "TQ": return "THQ";
case "TS": return "Titus Interactive";
case "TT": return "Take-Two Interactive Software";
case "US": return "Ubisoft";
case "VC": return "Victor Interactive Software";
case "VN": return "Vivendi Universal (just took Interplays publishing rights)"; // TODO: Confirm
case "VU": return "Vivendi Universal Games";
case "VV": return "Vivendi Universal Games"; // TODO: Confirm
case "WE": return "Wanadoo Edition";
case "WR": return "Warner Bros. Interactive Entertainment"; // TODO: Confirm
case "XI": return "XPEC Entertainment and Idea Factory";
case "XK": return "Xbox kiosk disk?"; // TODO: Confirm
case "XL": return "Xbox special bundled or live demo disk?"; // TODO: Confirm
case "XM": return "Evolved Games"; // TODO: Confirm
case "XP": return "XPEC Entertainment";
case "XR": return "Panorama";
case "YB": return "YBM Sisa (South-Korea)";
case "ZD": return "Zushi Games (formerly Zoo Digital Publishing)";
default: return null;
}
}
/// <summary>
/// Determine the region based on the XGD serial character
/// </summary>
/// <param name="region">Character denoting the region</param>
/// <returns>Region, if possible</returns>
#if NET48
private static string GetRegion(char region)
#else
private static string? GetRegion(char region)
#endif
{
switch (region)
{
case 'W': return "World";
case 'A': return "USA";
case 'J': return "Japan / Asia";
case 'E': return "Europe";
case 'K': return "USA / Japan";
case 'L': return "USA / Europe";
case 'H': return "Japan / Europe";
default: return null;
}
}
#endregion
}
}

14
Files/XMID.Serializer.cs Normal file
View File

@@ -0,0 +1,14 @@
using System;
namespace SabreTools.Serialization.Files
{
public partial class XMID : IFileSerializer<Models.Xbox.XMID>
{
/// <inheritdoc/>
#if NET48
public bool Serialize(Models.Xbox.XMID obj, string path) => throw new NotImplementedException();
#else
public bool Serialize(Models.Xbox.XMID? obj, string? path) => throw new NotImplementedException();
#endif
}
}

273
Files/XeMID.Deserializer.cs Normal file
View File

@@ -0,0 +1,273 @@
namespace SabreTools.Serialization.Files
{
public partial class XeMID : IFileSerializer<Models.Xbox.XeMID>
{
/// <inheritdoc/>
/// <remarks>This treats the input path like a parseable string</remarks>
#if NET48
public Models.Xbox.XeMID Deserialize(string path)
#else
public Models.Xbox.XeMID? Deserialize(string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return null;
string xemid = path.TrimEnd('\0');
if (string.IsNullOrWhiteSpace(xemid))
return null;
return ParseXeMID(xemid);
}
/// <summary>
/// Parse an XGD2/3 XeMID string
/// </summary>
/// <param name="xemidString">XeMID string to attempt to parse</param>
/// <returns>Filled XeMID on success, null on error</returns>
#if NET48
private static Models.Xbox.XeMID ParseXeMID(string xemidString)
#else
private static Models.Xbox.XeMID? ParseXeMID(string? xemidString)
#endif
{
if (xemidString == null
|| (xemidString.Length != 13 && xemidString.Length != 14
&& xemidString.Length != 21 && xemidString.Length != 22))
return null;
var xemid = new Models.Xbox.XeMID();
xemid.PublisherIdentifier = xemidString.Substring(0, 2);
if (string.IsNullOrEmpty(PublisherName(xemid)))
return null;
xemid.PlatformIdentifier = xemidString[2];
if (xemid.PlatformIdentifier != '2')
return null;
xemid.GameID = xemidString.Substring(3, 3);
xemid.SKU = xemidString.Substring(6, 2);
xemid.RegionIdentifier = xemidString[8];
if (InternalRegion(xemid) == null)
return null;
if (xemidString.Length == 13 || xemidString.Length == 21)
{
xemid.BaseVersion = xemidString.Substring(9, 1);
xemid.MediaSubtypeIdentifier = xemidString[10];
if (string.IsNullOrEmpty(MediaSubtype(xemid)))
return null;
xemid.DiscNumberIdentifier = xemidString.Substring(11, 2);
}
else if (xemidString.Length == 14 || xemidString.Length == 22)
{
xemid.BaseVersion = xemidString.Substring(9, 2);
xemid.MediaSubtypeIdentifier = xemidString[11];
if (string.IsNullOrEmpty(MediaSubtype(xemid)))
return null;
xemid.DiscNumberIdentifier = xemidString.Substring(12, 2);
}
if (xemidString.Length == 21)
xemid.CertificationSubmissionIdentifier = xemidString.Substring(13);
else if (xemidString.Length == 22)
xemid.CertificationSubmissionIdentifier = xemidString.Substring(14);
return xemid;
}
#region Helpers
/// <summary>
/// Human-readable name derived from the publisher identifier
/// </summary>
#if NET48
public static string PublisherName(Models.Xbox.XeMID xemid) => GetPublisher(xemid.PublisherIdentifier);
#else
public static string? PublisherName(Models.Xbox.XeMID xemid) => GetPublisher(xemid.PublisherIdentifier);
#endif
/// <summary>
/// Internally represented region
/// </summary>
#if NET48
public static string InternalRegion(Models.Xbox.XeMID xemid) => GetRegion(xemid.RegionIdentifier);
#else
public static string? InternalRegion(Models.Xbox.XeMID xemid) => GetRegion(xemid.RegionIdentifier);
#endif
/// <summary>
/// Human-readable subtype derived from the media identifier
/// </summary>
#if NET48
public static string MediaSubtype(Models.Xbox.XeMID xemid) => GetMediaSubtype(xemid.MediaSubtypeIdentifier);
#else
public static string? MediaSubtype(Models.Xbox.XeMID xemid) => GetMediaSubtype(xemid.MediaSubtypeIdentifier);
#endif
/// <summary>
/// Determine the XGD type based on the XGD2/3 media type identifier character
/// </summary>
/// <param name="mediaTypeIdentifier">Character denoting the media type</param>
/// <returns>Media subtype as a string, if possible</returns>
#if NET48
private static string GetMediaSubtype(char mediaTypeIdentifier)
#else
private static string? GetMediaSubtype(char mediaTypeIdentifier)
#endif
{
switch (mediaTypeIdentifier)
{
case 'F': return "XGD3";
case 'X': return "XGD2";
case 'Z': return "Games on Demand / Marketplace Demo";
default: return null;
}
}
/// <summary>
/// Get the full name of the publisher from the 2-character identifier
/// </summary>
/// <param name="publisherIdentifier">Case-sensitive 2-character identifier</param>
/// <returns>Publisher name, if possible</returns>
/// <see cref="https://xboxdevwiki.net/Xbe#Title_ID"/>
#if NET48
private static string GetPublisher(string publisherIdentifier)
#else
private static string? GetPublisher(string? publisherIdentifier)
#endif
{
switch (publisherIdentifier)
{
case "AC": return "Acclaim Entertainment";
case "AH": return "ARUSH Entertainment";
case "AQ": return "Aqua System";
case "AS": return "ASK";
case "AT": return "Atlus";
case "AV": return "Activision";
case "AY": return "Aspyr Media";
case "BA": return "Bandai";
case "BL": return "Black Box";
case "BM": return "BAM! Entertainment";
case "BR": return "Broccoli Co.";
case "BS": return "Bethesda Softworks";
case "BU": return "Bunkasha Co.";
case "BV": return "Buena Vista Games";
case "BW": return "BBC Multimedia";
case "BZ": return "Blizzard";
case "CC": return "Capcom";
case "CK": return "Kemco Corporation"; // TODO: Confirm
case "CM": return "Codemasters";
case "CV": return "Crave Entertainment";
case "DC": return "DreamCatcher Interactive";
case "DX": return "Davilex";
case "EA": return "Electronic Arts (EA)";
case "EC": return "Encore inc";
case "EL": return "Enlight Software";
case "EM": return "Empire Interactive";
case "ES": return "Eidos Interactive";
case "FI": return "Fox Interactive";
case "FS": return "From Software";
case "GE": return "Genki Co.";
case "GV": return "Groove Games";
case "HE": return "Tru Blu (Entertainment division of Home Entertainment Suppliers)";
case "HP": return "Hip games";
case "HU": return "Hudson Soft";
case "HW": return "Highwaystar";
case "IA": return "Mad Catz Interactive";
case "IF": return "Idea Factory";
case "IG": return "Infogrames";
case "IL": return "Interlex Corporation";
case "IM": return "Imagine Media";
case "IO": return "Ignition Entertainment";
case "IP": return "Interplay Entertainment";
case "IX": return "InXile Entertainment"; // TODO: Confirm
case "JA": return "Jaleco";
case "JW": return "JoWooD";
case "KB": return "Kemco"; // TODO: Confirm
case "KI": return "Kids Station Inc."; // TODO: Confirm
case "KN": return "Konami";
case "KO": return "KOEI";
case "KU": return "Kobi and / or GAE (formerly Global A Entertainment)"; // TODO: Confirm
case "LA": return "LucasArts";
case "LS": return "Black Bean Games (publishing arm of Leader S.p.A.)";
case "MD": return "Metro3D";
case "ME": return "Medix";
case "MI": return "Microïds";
case "MJ": return "Majesco Entertainment";
case "MM": return "Myelin Media";
case "MP": return "MediaQuest"; // TODO: Confirm
case "MS": return "Microsoft Game Studios";
case "MW": return "Midway Games";
case "MX": return "Empire Interactive"; // TODO: Confirm
case "NK": return "NewKidCo";
case "NL": return "NovaLogic";
case "NM": return "Namco";
case "OX": return "Oxygen Interactive";
case "PC": return "Playlogic Entertainment";
case "PL": return "Phantagram Co., Ltd.";
case "RA": return "Rage";
case "SA": return "Sammy";
case "SC": return "SCi Games";
case "SE": return "SEGA";
case "SN": return "SNK";
case "SS": return "Simon & Schuster";
case "SU": return "Success Corporation";
case "SW": return "Swing! Deutschland";
case "TA": return "Takara";
case "TC": return "Tecmo";
case "TD": return "The 3DO Company (or just 3DO)";
case "TK": return "Takuyo";
case "TM": return "TDK Mediactive";
case "TQ": return "THQ";
case "TS": return "Titus Interactive";
case "TT": return "Take-Two Interactive Software";
case "US": return "Ubisoft";
case "VC": return "Victor Interactive Software";
case "VN": return "Vivendi Universal (just took Interplays publishing rights)"; // TODO: Confirm
case "VU": return "Vivendi Universal Games";
case "VV": return "Vivendi Universal Games"; // TODO: Confirm
case "WE": return "Wanadoo Edition";
case "WR": return "Warner Bros. Interactive Entertainment"; // TODO: Confirm
case "XI": return "XPEC Entertainment and Idea Factory";
case "XK": return "Xbox kiosk disk?"; // TODO: Confirm
case "XL": return "Xbox special bundled or live demo disk?"; // TODO: Confirm
case "XM": return "Evolved Games"; // TODO: Confirm
case "XP": return "XPEC Entertainment";
case "XR": return "Panorama";
case "YB": return "YBM Sisa (South-Korea)";
case "ZD": return "Zushi Games (formerly Zoo Digital Publishing)";
default: return null;
}
}
/// <summary>
/// Determine the region based on the XGD serial character
/// </summary>
/// <param name="region">Character denoting the region</param>
/// <returns>Region, if possible</returns>
#if NET48
private static string GetRegion(char region)
#else
private static string? GetRegion(char region)
#endif
{
switch (region)
{
case 'W': return "World";
case 'A': return "USA";
case 'J': return "Japan / Asia";
case 'E': return "Europe";
case 'K': return "USA / Japan";
case 'L': return "USA / Europe";
case 'H': return "Japan / Europe";
default: return null;
}
}
#endregion
}
}

14
Files/XeMID.Serializer.cs Normal file
View File

@@ -0,0 +1,14 @@
using System;
namespace SabreTools.Serialization.Files
{
public partial class XeMID : IFileSerializer<Models.Xbox.XeMID>
{
/// <inheritdoc/>
#if NET48
public bool Serialize(Models.Xbox.XeMID obj, string path) => throw new NotImplementedException();
#else
public bool Serialize(Models.Xbox.XeMID? obj, string? path) => throw new NotImplementedException();
#endif
}
}

View File

@@ -4,7 +4,7 @@
<!-- Assembly Properties -->
<TargetFrameworks>net48;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<Version>1.1.1</Version>
<Version>1.1.2</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Package Properties -->
@@ -29,7 +29,7 @@
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="1.1.1" />
<PackageReference Include="SabreTools.Models" Version="1.1.1" />
<PackageReference Include="SabreTools.Models" Version="1.1.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using System;
using System.IO;
namespace SabreTools.Serialization.Streams
{
public partial class CueSheet : IStreamSerializer<Models.CueSheets.CueSheet>
{
/// <inheritdoc/>
#if NET48
public Models.CueSheets.CueSheet Deserialize(Stream data) => throw new NotImplementedException();
#else
public Models.CueSheets.CueSheet? Deserialize(Stream? data) => throw new NotImplementedException();
#endif
}
}

View File

@@ -0,0 +1,281 @@
using System;
using System.IO;
using System.Text;
using SabreTools.Models.CueSheets;
namespace SabreTools.Serialization.Streams
{
public partial class CueSheet : IStreamSerializer<Models.CueSheets.CueSheet>
{
/// <inheritdoc/>
#if NET48
public Stream Serialize(Models.CueSheets.CueSheet obj)
#else
public Stream? Serialize(Models.CueSheets.CueSheet? obj)
#endif
{
// If the cuesheet is null
if (obj == null)
return null;
// If we don't have any files, it's invalid
if (obj?.Files == null)
throw new ArgumentNullException(nameof(obj.Files));
else if (obj.Files.Length == 0)
throw new ArgumentException("No files provided to write");
// Setup the writer and output
var stream = new MemoryStream();
var writer = new StreamWriter(stream, Encoding.ASCII, 1024, true);
// Write the file
WriteCueSheet(obj, writer);
// Return the stream
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
/// <summary>
/// Write the cuesheet out to a stream
/// </summary>
/// <param name="cueSheet">CueSheet to write</param>
/// <param name="sw">StreamWriter to write to</param>
private static void WriteCueSheet(Models.CueSheets.CueSheet cueSheet, StreamWriter sw)
{
// If we don't have any files, it's invalid
if (cueSheet.Files == null)
throw new ArgumentNullException(nameof(cueSheet.Files));
else if (cueSheet.Files.Length == 0)
throw new ArgumentException("No files provided to write");
if (!string.IsNullOrEmpty(cueSheet.Catalog))
sw.WriteLine($"CATALOG {cueSheet.Catalog}");
if (!string.IsNullOrEmpty(cueSheet.CdTextFile))
sw.WriteLine($"CDTEXTFILE {cueSheet.CdTextFile}");
if (!string.IsNullOrEmpty(cueSheet.Performer))
sw.WriteLine($"PERFORMER {cueSheet.Performer}");
if (!string.IsNullOrEmpty(cueSheet.Songwriter))
sw.WriteLine($"SONGWRITER {cueSheet.Songwriter}");
if (!string.IsNullOrEmpty(cueSheet.Title))
sw.WriteLine($"TITLE {cueSheet.Title}");
foreach (var cueFile in cueSheet.Files)
{
WriteCueFile(cueFile, sw);
}
}
/// <summary>
/// Write the FILE out to a stream
/// </summary>
/// <param name="cueFile">CueFile to write</param>
/// <param name="sw">StreamWriter to write to</param>
#if NET48
private static void WriteCueFile(CueFile cueFile, StreamWriter sw)
#else
private static void WriteCueFile(CueFile? cueFile, StreamWriter sw)
#endif
{
// If we don't have any tracks, it's invalid
if (cueFile?.Tracks == null)
throw new ArgumentNullException(nameof(cueFile.Tracks));
else if (cueFile.Tracks.Length == 0)
throw new ArgumentException("No tracks provided to write");
sw.WriteLine($"FILE \"{cueFile.FileName}\" {FromFileType(cueFile.FileType)}");
foreach (var track in cueFile.Tracks)
{
WriteCueTrack(track, sw);
}
}
/// <summary>
/// Write the TRACK out to a stream
/// </summary>
/// <param name="cueFile">CueFile to write</param>
/// <param name="sw">StreamWriter to write to</param>
#if NET48
private static void WriteCueTrack(CueTrack cueTrack, StreamWriter sw)
#else
private static void WriteCueTrack(CueTrack? cueTrack, StreamWriter sw)
#endif
{
// If we don't have any indices, it's invalid
if (cueTrack?.Indices == null)
throw new ArgumentNullException(nameof(cueTrack.Indices));
else if (cueTrack.Indices.Length == 0)
throw new ArgumentException("No indices provided to write");
sw.WriteLine($" TRACK {cueTrack.Number:D2} {FromDataType(cueTrack.DataType)}");
if (cueTrack.Flags != 0)
sw.WriteLine($" FLAGS {FromFlags(cueTrack.Flags)}");
if (!string.IsNullOrEmpty(cueTrack.ISRC))
sw.WriteLine($" ISRC {cueTrack.ISRC}");
if (!string.IsNullOrEmpty(cueTrack.Performer))
sw.WriteLine($" PERFORMER {cueTrack.Performer}");
if (!string.IsNullOrEmpty(cueTrack.Songwriter))
sw.WriteLine($" SONGWRITER {cueTrack.Songwriter}");
if (!string.IsNullOrEmpty(cueTrack.Title))
sw.WriteLine($" TITLE {cueTrack.Title}");
if (cueTrack.PreGap != null)
WritePreGap(cueTrack.PreGap, sw);
foreach (var index in cueTrack.Indices)
{
WriteCueIndex(index, sw);
}
if (cueTrack.PostGap != null)
WritePostGap(cueTrack.PostGap, sw);
}
/// <summary>
/// Write the PREGAP out to a stream
/// </summary>
/// <param name="preGap">PreGap to write</param>
/// <param name="sw">StreamWriter to write to</param>
private static void WritePreGap(PreGap preGap, StreamWriter sw)
{
sw.WriteLine($" PREGAP {preGap.Minutes:D2}:{preGap.Seconds:D2}:{preGap.Frames:D2}");
}
/// <summary>
/// Write the INDEX out to a stream
/// </summary>
/// <param name="cueIndex">CueIndex to write</param>
/// <param name="sw">StreamWriter to write to</param>
#if NET48
private static void WriteCueIndex(CueIndex cueIndex, StreamWriter sw)
#else
private static void WriteCueIndex(CueIndex? cueIndex, StreamWriter sw)
#endif
{
if (cueIndex == null)
throw new ArgumentNullException(nameof(cueIndex));
sw.WriteLine($" INDEX {cueIndex.Index:D2} {cueIndex.Minutes:D2}:{cueIndex.Seconds:D2}:{cueIndex.Frames:D2}");
}
/// <summary>
/// Write the POSTGAP out to a stream
/// </summary>
/// <param name="postGap">PostGap to write</param>
/// <param name="sw">StreamWriter to write to</param>
private static void WritePostGap(PostGap postGap, StreamWriter sw)
{
sw.WriteLine($" POSTGAP {postGap.Minutes:D2}:{postGap.Seconds:D2}:{postGap.Frames:D2}");
}
#region Helpers
/// <summary>
/// Get the string from a given file type
/// </summary>
/// <param name="fileType">CueFileType to get value from</param>
/// <returns>String, if possible (default BINARY)</returns>
private static string FromFileType(CueFileType fileType)
{
switch (fileType)
{
case CueFileType.BINARY:
return "BINARY";
case CueFileType.MOTOROLA:
return "MOTOROLA";
case CueFileType.AIFF:
return "AIFF";
case CueFileType.WAVE:
return "WAVE";
case CueFileType.MP3:
return "MP3";
default:
return string.Empty;
}
}
/// <summary>
/// Get the string from a given data type
/// </summary>
/// <param name="dataType">CueTrackDataType to get value from</param>
/// <returns>string, if possible</returns>
private static string FromDataType(CueTrackDataType dataType)
{
switch (dataType)
{
case CueTrackDataType.AUDIO:
return "AUDIO";
case CueTrackDataType.CDG:
return "CDG";
case CueTrackDataType.MODE1_2048:
return "MODE1/2048";
case CueTrackDataType.MODE1_2352:
return "MODE1/2352";
case CueTrackDataType.MODE2_2336:
return "MODE2/2336";
case CueTrackDataType.MODE2_2352:
return "MODE2/2352";
case CueTrackDataType.CDI_2336:
return "CDI/2336";
case CueTrackDataType.CDI_2352:
return "CDI/2352";
default:
return string.Empty;
}
}
/// <summary>
/// Get the string value for a set of track flags
/// </summary>
/// <param name="flags">CueTrackFlag to get value from</param>
/// <returns>String value representing the CueTrackFlag, if possible</returns>
private static string FromFlags(CueTrackFlag flags)
{
string outputFlagString = string.Empty;
if (flags.HasFlag(CueTrackFlag.DCP))
outputFlagString += "DCP ";
if (flags.HasFlag(CueTrackFlag.FourCH))
outputFlagString += "4CH ";
if (flags.HasFlag(CueTrackFlag.PRE))
outputFlagString += "PRE ";
if (flags.HasFlag(CueTrackFlag.SCMS))
outputFlagString += "SCMS ";
if (flags.HasFlag(CueTrackFlag.DATA))
outputFlagString += "DATA ";
return outputFlagString.Trim();
}
#endregion
}
}

202
Streams/PIC.Deserializer.cs Normal file
View File

@@ -0,0 +1,202 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO;
using SabreTools.Models.PIC;
using static SabreTools.Models.PIC.Constants;
namespace SabreTools.Serialization.Streams
{
public partial class PIC : IStreamSerializer<DiscInformation>
{
/// <inheritdoc/>
#if NET48
public DiscInformation Deserialize(Stream data)
#else
public DiscInformation? Deserialize(Stream? data)
#endif
{
// If the data is invalid
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
return null;
// If the offset is out of bounds
if (data.Position < 0 || data.Position >= data.Length)
return null;
var di = new DiscInformation();
// Read the initial disc information
di.DataStructureLength = data.ReadUInt16BigEndian();
di.Reserved0 = data.ReadByteValue();
di.Reserved1 = data.ReadByteValue();
// Create a list for the units
var diUnits = new List<DiscInformationUnit>();
// Loop and read all available units
for (int i = 0; i < 32; i++)
{
var unit = ParseDiscInformationUnit(data);
if (unit == null)
continue;
diUnits.Add(unit);
}
// Assign the units and return
di.Units = diUnits.ToArray();
return di;
}
/// <summary>
/// Parse a Stream into a disc information unit
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled disc information unit on success, null on error</returns>
#if NET48
private static DiscInformationUnit ParseDiscInformationUnit(Stream data)
#else
private static DiscInformationUnit? ParseDiscInformationUnit(Stream data)
#endif
{
// TODO: Use marshalling here instead of building
var unit = new DiscInformationUnit();
#region Header
// Try to parse the header
var header = ParseDiscInformationUnitHeader(data);
if (header == null)
return null;
// Set the information unit header
unit.Header = header;
#endregion
#region Body
// Try to parse the body
var body = ParseDiscInformationUnitBody(data);
if (body == null)
return null;
// Set the information unit body
unit.Body = body;
#endregion
#region Trailer
if (unit.Body.DiscTypeIdentifier == DiscTypeIdentifierReWritable || unit.Body.DiscTypeIdentifier == DiscTypeIdentifierRecordable)
{
// Try to parse the trailer
var trailer = ParseDiscInformationUnitTrailer(data);
if (trailer == null)
return null;
// Set the information unit trailer
unit.Trailer = trailer;
}
#endregion
return unit;
}
/// <summary>
/// Parse a Stream into a disc information unit header
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled disc information unit header on success, null on error</returns>
#if NET48
private static DiscInformationUnitHeader ParseDiscInformationUnitHeader(Stream data)
#else
private static DiscInformationUnitHeader? ParseDiscInformationUnitHeader(Stream data)
#endif
{
// TODO: Use marshalling here instead of building
var header = new DiscInformationUnitHeader();
// We only accept Disc Information units, not Emergency Brake or other
#if NET48
byte[] dic = data.ReadBytes(2);
#else
byte[]? dic = data.ReadBytes(2);
#endif
if (dic == null)
return null;
header.DiscInformationIdentifier = Encoding.ASCII.GetString(dic);
if (header.DiscInformationIdentifier != "DI")
return null;
header.DiscInformationFormat = data.ReadByteValue();
header.NumberOfUnitsInBlock = data.ReadByteValue();
header.Reserved0 = data.ReadByteValue();
header.SequenceNumber = data.ReadByteValue();
header.BytesInUse = data.ReadByteValue();
header.Reserved1 = data.ReadByteValue();
return header;
}
/// <summary>
/// Parse a Stream into a disc information unit body
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled disc information unit body on success, null on error</returns>
#if NET48
private static DiscInformationUnitBody ParseDiscInformationUnitBody(Stream data)
#else
private static DiscInformationUnitBody? ParseDiscInformationUnitBody(Stream data)
#endif
{
// TODO: Use marshalling here instead of building
var body = new DiscInformationUnitBody();
#if NET48
byte[] dti = data.ReadBytes(3);
#else
byte[]? dti = data.ReadBytes(3);
#endif
if (dti == null)
return null;
body.DiscTypeIdentifier = Encoding.ASCII.GetString(dti);
body.DiscSizeClassVersion = data.ReadByteValue();
switch (body.DiscTypeIdentifier)
{
case DiscTypeIdentifierROM:
case DiscTypeIdentifierROMUltra:
body.FormatDependentContents = data.ReadBytes(52);
break;
case DiscTypeIdentifierReWritable:
case DiscTypeIdentifierRecordable:
body.FormatDependentContents = data.ReadBytes(100);
break;
}
return body;
}
/// <summary>
/// Parse a Stream into a disc information unit trailer
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled disc information unit trailer on success, null on error</returns>
private static DiscInformationUnitTrailer ParseDiscInformationUnitTrailer(Stream data)
{
// TODO: Use marshalling here instead of building
var trailer = new DiscInformationUnitTrailer();
trailer.DiscManufacturerID = data.ReadBytes(6);
trailer.MediaTypeID = data.ReadBytes(3);
trailer.TimeStamp = data.ReadUInt16();
trailer.ProductRevisionNumber = data.ReadByteValue();
return trailer;
}
}
}

16
Streams/PIC.Serializer.cs Normal file
View File

@@ -0,0 +1,16 @@
using System;
using System.IO;
using SabreTools.Models.PIC;
namespace SabreTools.Serialization.Streams
{
public partial class PIC : IStreamSerializer<DiscInformation>
{
/// <inheritdoc/>
#if NET48
public Stream Serialize(DiscInformation obj) => throw new NotImplementedException();
#else
public Stream? Serialize(DiscInformation? obj) => throw new NotImplementedException();
#endif
}
}