From cfc53f66a892fcb63ea068dc218e3d9584326f08 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 2 Apr 2024 15:30:43 -0400 Subject: [PATCH] Fix cuesheet serialization --- .../Files/CueSheet.Deserializer.cs | 589 +--------------- .../Streams/CueSheet.Deserializer.cs | 645 +++++++++++++++++- .../Streams/CueSheet.Serializer.cs | 2 - 3 files changed, 646 insertions(+), 590 deletions(-) diff --git a/SabreTools.Serialization/Files/CueSheet.Deserializer.cs b/SabreTools.Serialization/Files/CueSheet.Deserializer.cs index 51146c3b..4223650b 100644 --- a/SabreTools.Serialization/Files/CueSheet.Deserializer.cs +++ b/SabreTools.Serialization/Files/CueSheet.Deserializer.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using SabreTools.Models.CueSheets; using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Files @@ -13,587 +7,8 @@ namespace SabreTools.Serialization.Files /// public Models.CueSheets.CueSheet? Deserialize(string? path) { - // Check that the file exists - if (string.IsNullOrEmpty(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(); - - // 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() - .Select(m => m.Groups[0].Value) - .ToArray(); - - // If we have an empty line, we skip - if (string.IsNullOrEmpty(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; + using var stream = PathProcessor.OpenStream(path); + return new Streams.CueSheet().Deserialize(stream); } - - /// - /// Fill a FILE from an array of lines - /// - /// File name to set - /// File type to set - /// Lines array to pull from - /// Reference to index in array - private static CueFile? CreateCueFile(string fileName, string fileType, string[]? cueLines, ref int i) - { - // 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(); - - // 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.IsNullOrEmpty(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; - - // Next file found, return - case "FILE": - i--; - cueFile.Tracks = cueTracks.ToArray(); - return cueFile; - - // Default means return - default: - i--; - return null; - } - } - - cueFile.Tracks = cueTracks.ToArray(); - return cueFile; - } - - /// - /// Fill a TRACK from an array of lines - /// - /// Number to set - /// Data type to set - /// Lines array to pull from - /// Reference to index in array - private static CueTrack? CreateCueTrack(string number, string dataType, string[]? cueLines, ref int i) - { - // 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(); - - 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.IsNullOrEmpty(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; - - // Next track or file found, return - case "TRACK": - case "FILE": - i--; - cueTrack.Indices = cueIndices.ToArray(); - return cueTrack; - - // Default means return - default: - i--; - return null; - } - } - - cueTrack.Indices = cueIndices.ToArray(); - return cueTrack; - } - - /// - /// Create a PREGAP from a mm:ss:ff length - /// - /// String to get length information from - private static PreGap CreatePreGap(string? length) - { - // Ignore empty lines - if (string.IsNullOrEmpty(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; - } - - /// - /// Fill a INDEX from an array of lines - /// - /// Index to set - /// Start time to set - private static CueIndex CreateCueIndex(string? index, string? startTime) - { - // 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.IsNullOrEmpty(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; - } - - /// - /// Create a POSTGAP from a mm:ss:ff length - /// - /// String to get length information from - private static PostGap CreatePostGap(string? length) - { - // Ignore empty lines - if (string.IsNullOrEmpty(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 - - /// - /// Get the file type from a given string - /// - /// String to get value from - /// CueFileType, if possible - private static CueFileType GetFileType(string? fileType) - { - 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; - } - } - - /// - /// Get the data type from a given string - /// - /// String to get value from - /// CueTrackDataType, if possible (default AUDIO) - private static CueTrackDataType GetDataType(string? dataType) - { - 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; - } - } - - /// - /// Get the flag value for an array of strings - /// - /// Possible flags as strings - /// CueTrackFlag value representing the strings, if possible - private static CueTrackFlag GetFlags(string?[]? flagStrings) - { - CueTrackFlag flag = 0; - if (flagStrings == null) - return flag; - - foreach (string? flagString in flagStrings) - { - 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 } } diff --git a/SabreTools.Serialization/Streams/CueSheet.Deserializer.cs b/SabreTools.Serialization/Streams/CueSheet.Deserializer.cs index 8b3384b5..fb258be4 100644 --- a/SabreTools.Serialization/Streams/CueSheet.Deserializer.cs +++ b/SabreTools.Serialization/Streams/CueSheet.Deserializer.cs @@ -1,5 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using SabreTools.IO; +using SabreTools.Models.CueSheets; using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Streams @@ -7,6 +12,644 @@ namespace SabreTools.Serialization.Streams public partial class CueSheet : IStreamSerializer { /// - public Models.CueSheets.CueSheet? Deserialize(Stream? data) => throw new NotImplementedException(); + public Models.CueSheets.CueSheet? Deserialize(Stream? data) + { + // 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; + + // Cache the current offset + int initialOffset = (int)data.Position; + + // Create a new cuesheet to fill + var cueSheet = new Models.CueSheets.CueSheet(); + var cueFiles = new List(); + + // Read the next line from the input + string? lastLine = null; + while (true) + { + string? line = lastLine ?? ReadLine(data); + lastLine = null; + + // If we have a null line, break from the loop + if (line == null) + break; + + // If we have an empty line, we skip + if (string.IsNullOrEmpty(line)) + continue; + + // http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes + string[] splitLine = Regex + .Matches(line, @"[^\s""]+|""[^""]*""") + .Cast() + .Select(m => m.Groups[0].Value) + .ToArray(); + + 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].Trim('"'); + break; + + // Read external CD-Text file path + case "CDTEXTFILE": + if (splitLine.Length < 2) + throw new FormatException($"CDTEXTFILE line malformed: {line}"); + + cueSheet.CdTextFile = splitLine[1].Trim('"'); + break; + + // Read CD-Text enhanced performer + case "PERFORMER": + if (splitLine.Length < 2) + throw new FormatException($"PERFORMER line malformed: {line}"); + + cueSheet.Performer = splitLine[1].Trim('"'); + break; + + // Read CD-Text enhanced songwriter + case "SONGWRITER": + if (splitLine.Length < 2) + throw new FormatException($"SONGWRITER line malformed: {line}"); + + cueSheet.Songwriter = splitLine[1].Trim('"'); + break; + + // Read CD-Text enhanced title + case "TITLE": + if (splitLine.Length < 2) + throw new FormatException($"TITLE line malformed: {line}"); + + cueSheet.Title = splitLine[1].Trim('"'); + break; + + // Read file information + case "FILE": + if (splitLine.Length < 3) + throw new FormatException($"FILE line malformed: {line}"); + + var file = CreateCueFile(splitLine[1], splitLine[2], data, out lastLine); + if (file == default) + throw new FormatException($"FILE line malformed: {line}"); + + cueFiles.Add(file); + break; + } + } + + cueSheet.Files = [.. cueFiles]; + return cueSheet; + } + + /// + /// Fill a FILE from an array of lines + /// + /// File name to set + /// File type to set + /// Stream to pull from + private static CueFile? CreateCueFile(string fileName, string fileType, Stream data, out string? lastLine) + { + // Check the required parameters + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + throw new ArgumentNullException(nameof(data)); + if (data.Position < 0 || data.Position >= data.Length) + throw new IndexOutOfRangeException(); + + // Create the holding objects + lastLine = null; + var cueFile = new CueFile(); + var cueTracks = new List(); + + // Set the current fields + cueFile.FileName = fileName.Trim('"'); + cueFile.FileType = GetFileType(fileType); + + while (true) + { + string? line = lastLine ?? ReadLine(data); + lastLine = null; + + // If we have a null line, break from the loop + if (line == null) + break; + + // If we have an empty line, we skip + if (string.IsNullOrEmpty(line)) + continue; + + // http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes + string[] splitLine = Regex + .Matches(line, @"[^\s""]+|""[^""]*""") + .Cast() + .Select(m => m.Groups[0].Value) + .ToArray(); + + 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], data, out lastLine); + if (track == default) + throw new FormatException($"TRACK line malformed: {line}"); + + cueTracks.Add(track); + break; + + // Next file found, return + case "FILE": + lastLine = line; + cueFile.Tracks = [.. cueTracks]; + return cueFile; + + // Default means return + default: + lastLine = line; + cueFile.Tracks = [.. cueTracks]; + return cueFile; + } + } + + cueFile.Tracks = [.. cueTracks]; + return cueFile; + } + + /// + /// Fill a TRACK from an array of lines + /// + /// Number to set + /// Data type to set + /// Stream to pull from + private static CueTrack? CreateCueTrack(string number, string dataType, Stream data, out string? lastLine) + { + // Check the required parameters + if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead) + throw new ArgumentNullException(nameof(data)); + if (data.Position < 0 || data.Position >= data.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 + lastLine = null; + var cueTrack = new CueTrack(); + var cueIndices = new List(); + + cueTrack.Number = parsedNumber; + cueTrack.DataType = GetDataType(dataType); + + while (true) + { + string? line = lastLine ?? ReadLine(data); + lastLine = null; + + // If we have a null line, break from the loop + if (line == null) + break; + + // If we have an empty line, we skip + if (string.IsNullOrEmpty(line)) + continue; + + // http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes + string[] splitLine = Regex + .Matches(line, @"[^\s""]+|""[^""]*""") + .Cast() + .Select(m => m.Groups[0].Value) + .ToArray(); + + 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].Trim('"'); + break; + + // Read CD-Text enhanced performer + case "PERFORMER": + if (splitLine.Length < 2) + throw new FormatException($"PERFORMER line malformed: {line}"); + + cueTrack.Performer = splitLine[1].Trim('"'); + break; + + // Read CD-Text enhanced songwriter + case "SONGWRITER": + if (splitLine.Length < 2) + throw new FormatException($"SONGWRITER line malformed: {line}"); + + cueTrack.Songwriter = splitLine[1].Trim('"'); + break; + + // Read CD-Text enhanced title + case "TITLE": + if (splitLine.Length < 2) + throw new FormatException($"TITLE line malformed: {line}"); + + cueTrack.Title = splitLine[1].Trim('"'); + 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; + + // Next track or file found, return + case "TRACK": + case "FILE": + lastLine = line; + cueTrack.Indices = [.. cueIndices]; + return cueTrack; + + // Default means return + default: + lastLine = line; + cueTrack.Indices = [.. cueIndices]; + return cueTrack; + } + } + + cueTrack.Indices = [.. cueIndices]; + return cueTrack; + } + + /// + /// Create a PREGAP from a mm:ss:ff length + /// + /// String to get length information from + private static PreGap CreatePreGap(string length) + { + // Ignore empty lines + if (string.IsNullOrEmpty(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; + } + + /// + /// Fill a INDEX from an array of lines + /// + /// Index to set + /// Start time to set + private static CueIndex CreateCueIndex(string index, string startTime) + { + // 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.IsNullOrEmpty(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; + } + + /// + /// Create a POSTGAP from a mm:ss:ff length + /// + /// String to get length information from + private static PostGap CreatePostGap(string length) + { + // Ignore empty lines + if (string.IsNullOrEmpty(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 + + /// + /// Read a line from the input file, where a line is either terminated by a newline character + /// or by an end quote + /// + private static string? ReadLine(Stream data) + { + // If we are at the end of data + if (data.Position >= data.Length) + return null; + + var bytes = new List(); + bool openQuote = false; + while (data.Position < data.Length) + { + // Read the byte value + byte b = data.ReadByteValue(); + + // If we have a quote, flip the flag + if (b == (byte)'"') + openQuote = !openQuote; + + // If we have a newline not in a quoted string, exit the loop + else if (b == (byte)'\n' && !openQuote) + break; + + // Add the byte to the set + bytes.Add(b); + } + + var line = new string(bytes.Select(b => (char)b).ToArray()); + return line.TrimEnd(); + } + + /// + /// Get the file type from a given string + /// + /// String to get value from + /// CueFileType, if possible + private static CueFileType GetFileType(string? fileType) + { + 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; + } + } + + /// + /// Get the data type from a given string + /// + /// String to get value from + /// CueTrackDataType, if possible (default AUDIO) + private static CueTrackDataType GetDataType(string? dataType) + { + 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; + } + } + + /// + /// Get the flag value for an array of strings + /// + /// Possible flags as strings + /// CueTrackFlag value representing the strings, if possible + private static CueTrackFlag GetFlags(string?[]? flagStrings) + { + CueTrackFlag flag = 0; + if (flagStrings == null) + return flag; + + foreach (string? flagString in flagStrings) + { + 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 } } \ No newline at end of file diff --git a/SabreTools.Serialization/Streams/CueSheet.Serializer.cs b/SabreTools.Serialization/Streams/CueSheet.Serializer.cs index c377be3c..06bda4d4 100644 --- a/SabreTools.Serialization/Streams/CueSheet.Serializer.cs +++ b/SabreTools.Serialization/Streams/CueSheet.Serializer.cs @@ -15,8 +15,6 @@ namespace SabreTools.Serialization.Streams 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));