Skip to content
Closed
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
12 changes: 6 additions & 6 deletions ValvePak/ValvePak.Test/MultiChunkWriteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ public async Task WriteMultiChunkPackage()
using (var package = new Package())
{
package.SetFileName(dirPath);
package.WriteChunkSize = 1024;
package.WriteChunkSize = 800;

for (var i = 0; i < 10; i++)
{
var entry = package.AddFile($"files/chunked_{i}.bin", CreateTestData(400, (byte)i), multiChunk: true);

// 400 byte files with a 1024 chunk size, so three files per chunk
// 400 byte files with an 800 byte chunk size: a chunk file at exactly 800 bytes is not full yet, so each one holds three files
await Assert.That(entry.ArchiveIndex).IsEqualTo((ushort)(i / 3));
}

Expand Down Expand Up @@ -274,17 +274,17 @@ public async Task AddFileThrowsWhenExceedingChunkLimit()
package.SetFileName(TempPath("limit_dir.vpk"));
package.WriteChunkSize = 1;

// With a chunk size of 1 every file starts a new chunk
// With a chunk size of 1 every file of 2 bytes starts a new chunk
PackageEntry? entry = null;

for (var i = 0; i < 0x7FFF; i++)
{
entry = package.AddFile($"{i}.bin", [1], multiChunk: true);
entry = package.AddFile($"{i}.bin", [1, 2], multiChunk: true);
}

await Assert.That(entry!.ArchiveIndex).IsEqualTo((ushort)0x7FFE);

await Assert.That(() => package.AddFile("one too many.bin", [1], multiChunk: true)).ThrowsExactly<InvalidOperationException>()
await Assert.That(() => package.AddFile("one too many.bin", [1, 2], multiChunk: true)).ThrowsExactly<InvalidOperationException>()
.WithMessageContaining("maximum amount of chunk files", StringComparison.Ordinal);
}

Expand Down Expand Up @@ -492,7 +492,7 @@ public async Task WritesAgainAfterAddingMoreFiles()
public async Task WriteChunkSizeValidation()
{
using var package = new Package();
await Assert.That(package.WriteChunkSize).IsEqualTo(200 * 1024 * 1024);
await Assert.That(package.WriteChunkSize).IsEqualTo(100 * 1024 * 1024);
await Assert.That(() => package.WriteChunkSize = 0).ThrowsExactly<ArgumentOutOfRangeException>();
await Assert.That(() => package.WriteChunkSize = -1).ThrowsExactly<ArgumentOutOfRangeException>();
}
Expand Down
58 changes: 55 additions & 3 deletions ValvePak/ValvePak.Test/PackageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,44 @@ public async Task InvalidWholeFileChecksumThrows()
await Assert.That(() => package.VerifyHashes()).ThrowsExactly<InvalidDataException>();
}

[Test]
[Arguments(0, "File tree")]
[Arguments(16, "Archive MD5 entries")]
public async Task SectionChecksumMismatchIsReportedWhenWholeFileChecksumMatches(int checksumOffset, string sectionName)
{
var data = CreateSmallPackageBytes();
var otherMD5SectionOffset = GetOtherMD5SectionOffset(data);

data.AsSpan(otherMD5SectionOffset + checksumOffset, 16).Fill(0xAB);
MD5.HashData(data.AsSpan(0, otherMD5SectionOffset + 32)).CopyTo(data, otherMD5SectionOffset + 32);

using var package = new Package();
package.SetFileName("a.vpk");
package.Read(new MemoryStream(data));

var progress = new SynchronousProgress();

await Assert.That(() => package.VerifyHashes(progress)).ThrowsNothing();
await Assert.That(progress.Reports).Count().IsEqualTo(1);
await Assert.That(progress.Reports[0]).StartsWith($"{sectionName} checksum mismatch (", StringComparison.Ordinal);
}

[Test]
public async Task SectionChecksumMismatchThrowsWhenWholeFileChecksumMismatches()
{
var data = CreateSmallPackageBytes();
var otherMD5SectionOffset = GetOtherMD5SectionOffset(data);

data.AsSpan(otherMD5SectionOffset, 16).Fill(0xAB);

using var package = new Package();
package.SetFileName("a.vpk");
package.Read(new MemoryStream(data));

await Assert.That(() => package.VerifyHashes()).ThrowsExactly<InvalidDataException>()
.WithMessageContaining("File tree checksum mismatch", StringComparison.Ordinal);
}

[Test]
public async Task InvalidSignatureFails()
{
Expand Down Expand Up @@ -629,7 +667,7 @@ public async Task ThrowsWhenReadTwice()
}

[Test]
public async Task VerifyHashesThrowsOnVersion1()
public async Task VerifyHashesSkipsVersion1()
{
using var package = new Package();
package.SetFileName("a.vpk");
Expand All @@ -638,8 +676,10 @@ public async Task VerifyHashesThrowsOnVersion1()
using var ms = new MemoryStream([0x34, 0x12, 0xAA, 0x55, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
package.Read(ms);

await Assert.That(() => package.VerifyHashes()).ThrowsExactly<InvalidDataException>()
.WithMessage("Only version 2 is supported.", StringComparison.Ordinal);
var progress = new SynchronousProgress();

await Assert.That(() => package.VerifyHashes(progress)).ThrowsNothing();
await Assert.That(progress.Reports).IsEquivalentTo(["Version 1 packages have no checksums to verify."]);
}

[Test]
Expand Down Expand Up @@ -870,8 +910,10 @@ public async Task ThrowsOnDirectoryTreeEndingInTheMiddleOfString(string tree)
}

private const int TreeSizeOffset = 8;
private const int FileDataSectionSizeOffset = 12;
private const int ArchiveMD5SectionSizeOffset = 16;
private const int SignatureSectionSizeOffset = 24;
private const int HeaderSize = 28;
private const int EmbeddedPrefixLength = 1000;
private const int EmbeddedSuffixLength = 50;

Expand All @@ -886,6 +928,16 @@ private static byte[] CreateSmallPackageBytes()
return output.ToArray();
}

private static int GetOtherMD5SectionOffset(byte[] package)
{
var span = package.AsSpan();
var treeSize = BinaryPrimitives.ReadInt32LittleEndian(span[TreeSizeOffset..]);
var fileDataSectionSize = BinaryPrimitives.ReadInt32LittleEndian(span[FileDataSectionSizeOffset..]);
var archiveMD5SectionSize = BinaryPrimitives.ReadInt32LittleEndian(span[ArchiveMD5SectionSizeOffset..]);

return HeaderSize + treeSize + fileDataSectionSize + archiveMD5SectionSize;
}

/// <summary>
/// Creates a package whose signature section holds the given little-endian integers.
/// </summary>
Expand Down
73 changes: 40 additions & 33 deletions ValvePak/ValvePak.Test/WriteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ public async Task CreateNewPackage()
output.Position = 0;

// Verify
using var packageWritten = new Package();
packageWritten.SetFileName("test.vpk");
packageWritten.Read(output);
using var packageWritten = ReadBack(output);
packageWritten.VerifyHashes();

var newEntry = packageWritten.FindEntry(newName);
Expand Down Expand Up @@ -76,9 +74,7 @@ public async Task WriteManyFiles()
output.Position = 0;

// Verify
using var packageWritten = new Package();
packageWritten.SetFileName("test.vpk");
packageWritten.Read(output);
using var packageWritten = ReadBack(output);
packageWritten.VerifyHashes();

await Assert.That(packageWritten.Entries!["txt"]).Count().IsEqualTo(1000);
Expand Down Expand Up @@ -118,22 +114,16 @@ await Assert.That(package.RemoveFile(new PackageEntry
public async Task SetsSpaces()
{
using var package = new Package();
var file = package.AddFile("", []);
using (Assert.Multiple())
{
await Assert.That(file.TypeName).IsEqualTo(" ");
await Assert.That(file.DirectoryName).IsEqualTo(" ");
await Assert.That(file.FileName).IsEqualTo("");
await Assert.That(package.Entries!.ContainsKey(" ")).IsTrue();
await Assert.That(package.Entries[" "][0]).IsEqualTo(file);
}
await Assert.That(() => package.AddFile("", [])).ThrowsExactly<ArgumentException>();

var file2 = package.AddFile("hello", []);
using (Assert.Multiple())
{
await Assert.That(file2.TypeName).IsEqualTo(" ");
await Assert.That(file2.DirectoryName).IsEqualTo(" ");
await Assert.That(file2.FileName).IsEqualTo("hello");
await Assert.That(package.Entries!.ContainsKey(" ")).IsTrue();
await Assert.That(package.Entries[" "][0]).IsEqualTo(file2);
}

var file3 = package.AddFile("hello.txt", []);
Expand Down Expand Up @@ -194,9 +184,7 @@ public async Task WritesToNonSeekableStream(uint version)
}

using var output = new MemoryStream(nonSeekable.ToArray());
using var readBack = new Package();
readBack.SetFileName("test.vpk");
readBack.Read(output);
using var readBack = ReadBack(output);

if (version >= 2)
{
Expand Down Expand Up @@ -259,9 +247,7 @@ public async Task WriteAndVerifyRoundTrip()

output.Position = 0;

using var readBack = new Package();
readBack.SetFileName("test.vpk");
readBack.Read(output);
using var readBack = ReadBack(output);

using (Assert.Multiple())
{
Expand Down Expand Up @@ -292,9 +278,7 @@ public async Task WriteVersion1Package()

output.Position = 0;

using var readBack = new Package();
readBack.SetFileName("test.vpk");
readBack.Read(output);
using var readBack = ReadBack(output);

using (Assert.Multiple())
{
Expand Down Expand Up @@ -327,18 +311,14 @@ public async Task WritePreservesVersionOfReadPackage()

output.Position = 0;

using var readBack = new Package();
readBack.SetFileName("test.vpk");
readBack.Read(output);
using var readBack = ReadBack(output);

using var output2 = new MemoryStream();
readBack.Write(output2);

output2.Position = 0;

using var readBack2 = new Package();
readBack2.SetFileName("test.vpk");
readBack2.Read(output2);
using var readBack2 = ReadBack(output2);

await Assert.That(readBack2.Version).IsEqualTo(1u);

Expand Down Expand Up @@ -428,9 +408,7 @@ public async Task WriteIntoLongerStream(int position, uint version)

output.Position = position;

using var readBack = new Package();
readBack.SetFileName("test.vpk");
readBack.Read(output);
using var readBack = ReadBack(output);

await Assert.That(readBack.Version).IsEqualTo(version);

Expand Down Expand Up @@ -517,6 +495,35 @@ public async Task AddedFilesCanBeFoundAfterOptimizingForBinarySearch(StringCompa
await Assert.That(package.FindEntry("addons/chess/missing.vdf")).IsNull();
}

[Test]
public async Task RenameFile()
{
using var package = new Package();
package.OptimizeEntriesForBinarySearch(StringComparison.OrdinalIgnoreCase);
var entry = package.AddFile("kitten.jpg", Encoding.UTF8.GetBytes("meow"));
package.AddFile("other.txt", []);

package.RenameFile(entry, "/Path\\To/Cat.PNG");

using (Assert.Multiple())
{
await Assert.That(entry.GetFullPath()).IsEqualTo("path/to/cat.png");
await Assert.That(package.FindEntry("kitten.jpg")).IsNull();
await Assert.That(package.FindEntry("path/to/cat.png")).IsEqualTo(entry);
await Assert.That(package.Entries!.ContainsKey("jpg")).IsFalse();
await Assert.That(() => package.RenameFile(entry, "other.txt")).ThrowsExactly<ArgumentException>();
}
}

private static Package ReadBack(Stream stream)
{
var package = new Package();
package.SetFileName("test.vpk");
package.Read(stream);

return package;
}

/// <summary>
/// Write only stream that can not seek, collecting the written bytes.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions ValvePak/ValvePak/Archive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ protected Archive(long origin, long dataOffset)
/// </summary>
protected long Origin { get; }

/// <summary>
/// Gets the size in bytes of the archive.
/// </summary>
public abstract long Length { get; }

/// <summary>
/// Reads data at the given offset until the output is full or the end of the archive is reached.
/// This method is safe to call from multiple threads.
Expand Down
2 changes: 2 additions & 0 deletions ValvePak/ValvePak/FileArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public FileArchive(SafeFileHandle handle, bool ownsHandle, long origin, long dat
OwnsHandle = ownsHandle;
}

public override long Length => RandomAccess.GetLength(Handle) - Origin;

public override int Read(long offset, Span<byte> output)
{
var totalRead = 0;
Expand Down
Loading
Loading