From c874b7b457c8df9de690e5d010251aa8f1e0fa05 Mon Sep 17 00:00:00 2001 From: Samuel Stark Date: Sun, 7 Jan 2024 20:26:50 +0000 Subject: [PATCH 1/5] Make ParArchiveReader special-case zero-length pars --- ParLibrary/Converter/ParArchiveReader.cs | 13 +++++++++++++ ParLibrary/Converter/ParArchiveReaderParameters.cs | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/ParLibrary/Converter/ParArchiveReader.cs b/ParLibrary/Converter/ParArchiveReader.cs index 8a3967c..aff4c05 100644 --- a/ParLibrary/Converter/ParArchiveReader.cs +++ b/ParLibrary/Converter/ParArchiveReader.cs @@ -6,6 +6,7 @@ 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..ba67eda 100644 --- a/ParLibrary/Converter/ParArchiveReaderParameters.cs +++ b/ParLibrary/Converter/ParArchiveReaderParameters.cs @@ -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; } } } From e834141a70a2f72e883fb1c346185745faef5cea Mon Sep 17 00:00:00 2001 From: Samuel Stark Date: Sun, 7 Jan 2024 20:45:07 +0000 Subject: [PATCH 2/5] Make Program.{Add,Extract,List} handle zero-length pars .Add throws an error if it's the top-level par, because then it can't infer whether to IncludeDots. .Add tolerates zero-length pars inside the top-level par. .Extract and .List warn the user if the top-level par is zero-length, but otherwise permit it. This includes creating empty directories when extracting zero-length PARs recursively. I haven't added command-line flags to change this behaviour, because any error that gets thrown inside Add, Extract, or List on encountering a 0B .par won't have enough information to help the user. I don't think the ParArchiveReader can throw an error including the filepath the BinarySource came from, so any error would just indicate "somewhere inside this .par there's an 0b file" and that's not useful. --- ParTool/Program.Add.cs | 14 +++++++++++++- ParTool/Program.Extract.cs | 11 +++++++++++ ParTool/Program.List.cs | 11 +++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/ParTool/Program.Add.cs b/ParTool/Program.Add.cs index e9f14b3..e1119ee 100644 --- a/ParTool/Program.Add.cs +++ b/ParTool/Program.Add.cs @@ -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..b878e65 100644 --- a/ParTool/Program.Extract.cs +++ b/ParTool/Program.Extract.cs @@ -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..b074a95 100644 --- a/ParTool/Program.List.cs +++ b/ParTool/Program.List.cs @@ -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)) From 63db79401e3bc534466ca9e6a2131e2ae6a6b1ac Mon Sep 17 00:00:00 2001 From: Samuel Stark Date: Sun, 7 Jan 2024 20:45:37 +0000 Subject: [PATCH 3/5] Add unit tests to document intended behaviour on 0b files --- .../ParLib.UnitTests/ParLib.UnitTests.csproj | 6 + Tests/ParLib.UnitTests/ZeroLengthPar.cs | 113 ++++++++++++++++++ .../test_par_containing_0b_par.par | Bin 0 -> 2048 bytes 3 files changed, 119 insertions(+) create mode 100644 Tests/ParLib.UnitTests/ZeroLengthPar.cs create mode 100644 Tests/ParLib.UnitTests/test_par_containing_0b_par.par 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..ee88099 --- /dev/null +++ b/Tests/ParLib.UnitTests/ZeroLengthPar.cs @@ -0,0 +1,113 @@ +namespace ParLib.UnitTests +{ + using NUnit.Framework; + using ParLibrary.Converter; + using System; + using System.IO; + 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 0000000000000000000000000000000000000000..548fac15c5a1e8e61534f989fc51e67a4d306298 GIT binary patch literal 2048 zcmWG=402{-WME)mVgQjK0HPN_=>vK|1|^^*wYVhSAUi3(AhAdfh{-ey Date: Sun, 7 Jan 2024 21:05:58 +0000 Subject: [PATCH 4/5] Copyright --- ParLibrary/Converter/ParArchiveReader.cs | 2 +- ParLibrary/Converter/ParArchiveReaderParameters.cs | 2 +- ParTool/Program.Add.cs | 2 +- ParTool/Program.Extract.cs | 2 +- ParTool/Program.List.cs | 2 +- Tests/ParLib.UnitTests/ZeroLengthPar.cs | 8 +++++--- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ParLibrary/Converter/ParArchiveReader.cs b/ParLibrary/Converter/ParArchiveReader.cs index aff4c05..27d3a0c 100644 --- a/ParLibrary/Converter/ParArchiveReader.cs +++ b/ParLibrary/Converter/ParArchiveReader.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 { diff --git a/ParLibrary/Converter/ParArchiveReaderParameters.cs b/ParLibrary/Converter/ParArchiveReaderParameters.cs index ba67eda..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 { diff --git a/ParTool/Program.Add.cs b/ParTool/Program.Add.cs index e1119ee..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 { diff --git a/ParTool/Program.Extract.cs b/ParTool/Program.Extract.cs index b878e65..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 { diff --git a/ParTool/Program.List.cs b/ParTool/Program.List.cs index b074a95..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 { diff --git a/Tests/ParLib.UnitTests/ZeroLengthPar.cs b/Tests/ParLib.UnitTests/ZeroLengthPar.cs index ee88099..3ca209c 100644 --- a/Tests/ParLib.UnitTests/ZeroLengthPar.cs +++ b/Tests/ParLib.UnitTests/ZeroLengthPar.cs @@ -1,9 +1,11 @@ -namespace ParLib.UnitTests +// ------------------------------------------------------- +// © Samuel W. Stark (TheTurboTurnip). Licensed under MIT. See LICENSE for details. +// ------------------------------------------------------- +namespace ParLib.UnitTests { + using System.IO; using NUnit.Framework; using ParLibrary.Converter; - using System; - using System.IO; using Yarhl.FileSystem; public class ZeroLengthPar From 778be68dc853da6ff8a675059cad165cc5ccbee6 Mon Sep 17 00:00:00 2001 From: Samuel Stark Date: Sun, 7 Jan 2024 21:06:17 +0000 Subject: [PATCH 5/5] Make .Remove handle 0b pars --- ParTool/Program.Remove.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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);