Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle 0byte .pars gracefully #17

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion ParLibrary/Converter/ParArchiveReader.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion ParLibrary/Converter/ParArchiveReaderParameters.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -12,5 +12,10 @@ public class ParArchiveReaderParameters
/// Gets or sets a value indicating whether the reading is recursive.
/// </summary>
public bool Recursive { get; set; }

/// <summary>
/// Gets or sets a value indicating whether zero-length PARs cause an error or not.
/// </summary>
public bool AllowZeroLengthPars { get; set; }
}
}
16 changes: 14 additions & 2 deletions ParTool/Program.Add.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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
Expand All @@ -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<ParArchiveReader, ParArchiveReaderParameters>(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... ");
Expand Down
13 changes: 12 additions & 1 deletion ParTool/Program.Extract.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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<ParArchiveReader, ParArchiveReaderParameters>(parameters);

Extract(par, opts.OutputDirectory);
Expand Down
13 changes: 12 additions & 1 deletion ParTool/Program.List.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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<ParArchiveReader, ParArchiveReaderParameters>(parameters);

foreach (Node node in Navigator.IterateNodes(par))
Expand Down
5 changes: 4 additions & 1 deletion ParTool/Program.Remove.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions Tests/ParLib.UnitTests/ParLib.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@
<ProjectReference Include="..\..\ParLibrary\ParLibrary.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="test_par_containing_0b_par.par">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
115 changes: 115 additions & 0 deletions Tests/ParLib.UnitTests/ZeroLengthPar.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Test that reading a 0B PAR file returns an empty node when using AllowZeroLengthPars = true.
/// </summary>
[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<ParArchiveReader, ParArchiveReaderParameters>(readerParameters);

Assert.AreEqual(0, test_0b_par.Children.Count);
}

/// <summary>
/// Test that reading a 0B PAR file throws an exception when using AllowZeroLengthPars = false.
/// </summary>
[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<InvalidDataException>(() => test_0b_par.TransformWith<ParArchiveReader, ParArchiveReaderParameters>(readerParameters));
}

/// <summary>
/// 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.
/// </summary>
[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<ParArchiveReader, ParArchiveReaderParameters>(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/
}

/// <summary>
/// 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.
/// </summary>
[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<InvalidDataException>(() => test_par_containing_0b_par.TransformWith<ParArchiveReader, ParArchiveReaderParameters>(readerParameters));
}
}
}
Binary file not shown.