diff --git a/ParLibrary/Converter/ParArchiveReader.cs b/ParLibrary/Converter/ParArchiveReader.cs index 8a3967c..27d3a0c 100644 --- a/ParLibrary/Converter/ParArchiveReader.cs +++ b/ParLibrary/Converter/ParArchiveReader.cs @@ -1,11 +1,12 @@ // ------------------------------------------------------- -// © Kaplas. Licensed under MIT. See LICENSE for details. +// © Kaplas, Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. // ------------------------------------------------------- namespace ParLibrary.Converter { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; + using System.IO; using System.Text; using Yarhl.FileFormat; using Yarhl.FileSystem; @@ -43,6 +44,18 @@ public NodeContainerFormat Convert(BinaryFormat source) var result = new NodeContainerFormat(); + if (source.Stream.Length == 0) + { + if (this.parameters.AllowZeroLengthPars) + { + return result; + } + else + { + throw new InvalidDataException("PAR stream is zero bytes long and cannot be read."); + } + } + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var reader = new DataReader(source.Stream) diff --git a/ParLibrary/Converter/ParArchiveReaderParameters.cs b/ParLibrary/Converter/ParArchiveReaderParameters.cs index ea881ef..4c51a72 100644 --- a/ParLibrary/Converter/ParArchiveReaderParameters.cs +++ b/ParLibrary/Converter/ParArchiveReaderParameters.cs @@ -1,5 +1,5 @@ // ------------------------------------------------------- -// © Kaplas. Licensed under MIT. See LICENSE for details. +// © Kaplas, Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. // ------------------------------------------------------- namespace ParLibrary.Converter { @@ -12,5 +12,10 @@ public class ParArchiveReaderParameters /// Gets or sets a value indicating whether the reading is recursive. /// public bool Recursive { get; set; } + + /// + /// Gets or sets a value indicating whether zero-length PARs cause an error or not. + /// + public bool AllowZeroLengthPars { get; set; } } } diff --git a/ParTool/Program.Add.cs b/ParTool/Program.Add.cs index e9f14b3..53c4cc4 100644 --- a/ParTool/Program.Add.cs +++ b/ParTool/Program.Add.cs @@ -1,5 +1,5 @@ // ------------------------------------------------------- -// © Kaplas. Licensed under MIT. See LICENSE for details. +// © Kaplas, Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. // ------------------------------------------------------- namespace ParTool { @@ -46,6 +46,9 @@ private static void Add(Options.Add opts) var readerParameters = new ParArchiveReaderParameters { Recursive = true, + + // If we encounter a zero-length PAR at any point below the top level, we treat it as an empty directory. + AllowZeroLengthPars = true, }; var writerParameters = new ParArchiveWriterParameters @@ -56,8 +59,17 @@ private static void Add(Options.Add opts) Console.Write("Reading PAR file... "); Node par = NodeFactory.FromFile(opts.InputParArchivePath, Yarhl.IO.FileOpenMode.Read); + + // Warn the user if the top-level PAR they're using is a zero-length file. + // If it is, we can't infer the IncludeDots parameter. + if (par.Stream.Length == 0) + { + Console.WriteLine($"ERROR: \"{opts.InputParArchivePath}\" is an empty file, and contains no data. Use `ParTool.exe create` instead."); + return; + } + par.TransformWith(readerParameters); - writerParameters.IncludeDots = par.Children[0].Name == "."; + writerParameters.IncludeDots = (par.Children.Count > 0) && par.Children[0].Name == "."; Console.WriteLine("DONE!"); Console.Write("Reading input directory... "); diff --git a/ParTool/Program.Extract.cs b/ParTool/Program.Extract.cs index 561a538..367dfaf 100644 --- a/ParTool/Program.Extract.cs +++ b/ParTool/Program.Extract.cs @@ -1,5 +1,5 @@ // ------------------------------------------------------- -// © Kaplas. Licensed under MIT. See LICENSE for details. +// © Kaplas, Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. // ------------------------------------------------------- namespace ParTool { @@ -42,9 +42,20 @@ private static void Extract(Options.Extract opts) var parameters = new ParArchiveReaderParameters { Recursive = opts.Recursive, + + // If we encounter a zero-length PAR at any point, we treat it as an empty directory. + AllowZeroLengthPars = true, }; using Node par = NodeFactory.FromFile(opts.ParArchivePath, Yarhl.IO.FileOpenMode.Read); + + // For convenience, warn the user if the top-level PAR they're using is a zero-length file. + // We still use the AllowZeroLengthPARs parameter, in case a non-zero-length PAR contains a zero-length PAR and we're reading in recursive mode. + if (par.Stream.Length == 0) + { + Console.WriteLine($"WARNING: \"{opts.ParArchivePath}\" is an empty file, and contains no data."); + } + par.TransformWith(parameters); Extract(par, opts.OutputDirectory); diff --git a/ParTool/Program.List.cs b/ParTool/Program.List.cs index 551df0a..0c442db 100644 --- a/ParTool/Program.List.cs +++ b/ParTool/Program.List.cs @@ -1,5 +1,5 @@ // ------------------------------------------------------- -// © Kaplas. Licensed under MIT. See LICENSE for details. +// © Kaplas, Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. // ------------------------------------------------------- namespace ParTool { @@ -27,9 +27,20 @@ private static void List(Options.List opts) var parameters = new ParArchiveReaderParameters { Recursive = opts.Recursive, + + // If we encounter a zero-length PAR at any point, we treat it as an empty directory. + AllowZeroLengthPars = true, }; using Node par = NodeFactory.FromFile(opts.ParArchivePath, Yarhl.IO.FileOpenMode.Read); + + // For convenience, warn the user if the top-level PAR they're using is a zero-length file. + // We still use the AllowZeroLengthPARs parameter, in case a non-zero-length PAR contains a zero-length PAR and we're reading in recursive mode. + if (par.Stream.Length == 0) + { + Console.WriteLine($"WARNING: \"{opts.ParArchivePath}\" is an empty file, and contains no data."); + } + par.TransformWith(parameters); foreach (Node node in Navigator.IterateNodes(par)) diff --git a/ParTool/Program.Remove.cs b/ParTool/Program.Remove.cs index 0b10edf..4924dc5 100644 --- a/ParTool/Program.Remove.cs +++ b/ParTool/Program.Remove.cs @@ -1,5 +1,5 @@ // ------------------------------------------------------- -// © Kaplas. Licensed under MIT. See LICENSE for details. +// © Kaplas, Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. // ------------------------------------------------------- namespace ParTool { @@ -40,6 +40,9 @@ private static void Remove(Options.Remove opts) var readerParameters = new ParArchiveReaderParameters { Recursive = true, + + // If we encounter a zero-length PAR at any point, we treat it as an empty directory. + AllowZeroLengthPars = true, }; using Node par = NodeFactory.FromFile(opts.InputParArchivePath, Yarhl.IO.FileOpenMode.Read); diff --git a/Tests/ParLib.UnitTests/ParLib.UnitTests.csproj b/Tests/ParLib.UnitTests/ParLib.UnitTests.csproj index 6210552..fdce2c7 100644 --- a/Tests/ParLib.UnitTests/ParLib.UnitTests.csproj +++ b/Tests/ParLib.UnitTests/ParLib.UnitTests.csproj @@ -19,4 +19,10 @@ + + + Always + + + diff --git a/Tests/ParLib.UnitTests/ZeroLengthPar.cs b/Tests/ParLib.UnitTests/ZeroLengthPar.cs new file mode 100644 index 0000000..3ca209c --- /dev/null +++ b/Tests/ParLib.UnitTests/ZeroLengthPar.cs @@ -0,0 +1,115 @@ +// ------------------------------------------------------- +// © Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. +// ------------------------------------------------------- +namespace ParLib.UnitTests +{ + using System.IO; + using NUnit.Framework; + using ParLibrary.Converter; + using Yarhl.FileSystem; + + public class ZeroLengthPar + { + /// + /// Test that reading a 0B PAR file returns an empty node when using AllowZeroLengthPars = true. + /// + [Test] + public void ZeroLengthParIsEmptyNodeWhenAllowed() + { + var readerParameters = new ParArchiveReaderParameters + { + Recursive = true, + + // If we encounter a zero-length PAR at any point, we treat it as an empty directory. + AllowZeroLengthPars = true, + }; + + // This creates an empty BinaryStream for the Node, so it's a 0b file + Node test_0b_par = NodeFactory.FromMemory("test_0b_par.par"); + test_0b_par.TransformWith(readerParameters); + + Assert.AreEqual(0, test_0b_par.Children.Count); + } + + /// + /// Test that reading a 0B PAR file throws an exception when using AllowZeroLengthPars = false. + /// + [Test] + public void ZeroLengthParThrowsWhenNotAllowed() + { + var readerParameters = new ParArchiveReaderParameters + { + Recursive = true, + + AllowZeroLengthPars = false, + }; + + // This creates an empty BinaryStream for the Node, so it's a 0b file + Node test_0b_par = NodeFactory.FromMemory("test_0b_par.par"); + + Assert.Throws(() => test_0b_par.TransformWith(readerParameters)); + } + + /// + /// Test that when recursively reading a PAR that *contains* a 0B PAR file, that the 0B par is treated as an empty directory with the correct name. + /// + [Test] + public void ParContainingZeroLengthParHasNodeWhenAllowed() + { + var readerParameters = new ParArchiveReaderParameters + { + Recursive = true, + + AllowZeroLengthPars = true, + }; + + // This loads the file stored in Tests/ParLib.UnitTests + Node test_par_containing_0b_par = NodeFactory.FromFile("test_par_containing_0b_par.par", Yarhl.IO.FileOpenMode.Read); + test_par_containing_0b_par.TransformWith(readerParameters); + + // The toplevel par should have one child (it's a directory) + Assert.AreEqual(1, test_par_containing_0b_par.Children.Count); + + // That child should be '.', which should *also* have one child. + // (I exported this test file with IncludeDots = true). + Assert.AreEqual(".", test_par_containing_0b_par.Children[0].Name); + Assert.AreEqual(1, test_par_containing_0b_par.Children[0].Children.Count); + + // That child should be test_0kb_par.par + var test_0kb_par = test_par_containing_0b_par.Children[0].Children[0]; + Assert.AreEqual("test_0kb_par.par", test_0kb_par.Name); + + // test_0kb_par.par should have no children + Assert.AreEqual(0, test_0kb_par.Children.Count); + + // Overall the listing is + // /test_par_containing_0b_par.par + // /test_par_containing_0b_par.par/./ + // /test_par_containing_0b_par.par/./test_0kb_par/ + } + + /// + /// Test that reading a PAR *containing* 0B PAR file throws an exception when using AllowZeroLengthPars = false. + /// + /// This is not necessarily desirable behaviour, and at time of writing there isn't a nice way to surface the error to the user. + /// "test_par_containing_0b_par.par is fine, but it contains test_0b_par.par and that is bad!" + /// + /// This test is meant to document the current behaviour, and if the behaviour is improved then it should be changed or removed. + /// + [Test] + public void ParContainingZeroLengthParThrowsWhenNotAllowed() + { + var readerParameters = new ParArchiveReaderParameters + { + Recursive = true, + + AllowZeroLengthPars = false, + }; + + // This loads the file stored in Tests/ParLib.UnitTests + Node test_par_containing_0b_par = NodeFactory.FromFile("test_par_containing_0b_par.par", Yarhl.IO.FileOpenMode.Read); + + Assert.Throws(() => test_par_containing_0b_par.TransformWith(readerParameters)); + } + } +} diff --git a/Tests/ParLib.UnitTests/test_par_containing_0b_par.par b/Tests/ParLib.UnitTests/test_par_containing_0b_par.par new file mode 100644 index 0000000..548fac1 Binary files /dev/null and b/Tests/ParLib.UnitTests/test_par_containing_0b_par.par differ