Skip to content
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
7 changes: 3 additions & 4 deletions ValvePak/ValvePak.Test/MemoryMapTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Security.Cryptography;
using System.Threading.Tasks;

Expand All @@ -20,7 +19,7 @@ public async Task ReturnsCorrectStreamsForSplitPackages()
package.Read(path);

using var stream = package.GetMemoryMappedStreamIfPossible(package.FindEntry("kitten.jpg")!);
await Assert.That(stream).IsAssignableTo<MemoryMappedViewStream>();
await Assert.That(stream).IsNotAssignableTo<MemoryStream>();
await VerifyKitten(stream);

using var stream2 = package.GetMemoryMappedStreamIfPossible(package.FindEntry("steammessages_base.proto")!);
Expand All @@ -37,7 +36,7 @@ public async Task ReturnsCorrectStreams()
package.Read(path);

using var stream = package.GetMemoryMappedStreamIfPossible(package.FindEntry("kitten.jpg")!);
await Assert.That(stream).IsAssignableTo<MemoryMappedViewStream>();
await Assert.That(stream).IsNotAssignableTo<MemoryStream>();
await VerifyKitten(stream);

using var stream2 = package.GetMemoryMappedStreamIfPossible(package.FindEntry("steammessages_base.proto")!);
Expand All @@ -56,7 +55,7 @@ public async Task ReturnsCorrectStreamsWhenUsingFileStream()
package.Read(fileStream);

using var stream = package.GetMemoryMappedStreamIfPossible(package.FindEntry("kitten.jpg")!);
await Assert.That(stream).IsAssignableTo<MemoryMappedViewStream>();
await Assert.That(stream).IsNotAssignableTo<MemoryStream>();
await VerifyKitten(stream);

using var stream2 = package.GetMemoryMappedStreamIfPossible(package.FindEntry("steammessages_base.proto")!);
Expand Down
57 changes: 57 additions & 0 deletions ValvePak/ValvePak.Test/PackageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,63 @@ public async Task ExtractIntoUserProvidedByteArray()
await Assert.That(() => package.ReadEntry(entry, smallBuffer)).ThrowsExactly<ArgumentOutOfRangeException>();
}

[Test]
public async Task ThrowsWhenEntryExtendsPastEndOfArchive()
{
var directory = Path.Combine(Path.GetTempPath(), "ValvePakTest_" + Path.GetRandomFileName());
Directory.CreateDirectory(directory);

try
{
// Cut off the last entry (steammessages_clientserver.proto, 39177 bytes) and the end of the one before it (steammessages_base.proto, 2563 bytes)
const int Cut = 39177 + 100;

File.Copy(Path.Combine(AppContext.BaseDirectory, "Files", "test_dir.vpk"), Path.Combine(directory, "test_dir.vpk"));
var chunk = await File.ReadAllBytesAsync(Path.Combine(AppContext.BaseDirectory, "Files", "test_000.vpk"));
await File.WriteAllBytesAsync(Path.Combine(directory, "test_000.vpk"), chunk[..^Cut]);

var single = await File.ReadAllBytesAsync(Path.Combine(AppContext.BaseDirectory, "Files", "test_single.vpk"));
await File.WriteAllBytesAsync(Path.Combine(directory, "test_single.vpk"), single[..^Cut]);

foreach (var (name, fromMemory) in new[] { ("test_dir.vpk", false), ("test_single.vpk", false), ("test_single.vpk", true) })
{
var path = Path.Combine(directory, name);

using var package = new Package();

if (fromMemory)
{
package.SetFileName(path);
package.Read(new MemoryStream(await File.ReadAllBytesAsync(path)));
}
else
{
package.Read(path);
}

var kitten = package.FindEntry("kitten.jpg")!;
await Assert.That(() => package.ReadEntry(kitten, out _, validateCrc: true)).ThrowsNothing();

foreach (var truncatedName in new[] { "steammessages_base.proto", "steammessages_clientserver.proto" })
{
var truncated = package.FindEntry(truncatedName)!;

using (Assert.Multiple())
{
await Assert.That(() => package.ReadEntry(truncated, out _, validateCrc: false)).ThrowsExactly<EndOfStreamException>();
await Assert.That(() => package.ReadEntry(truncated, out _, validateCrc: true)).ThrowsExactly<EndOfStreamException>();
await Assert.That(() => package.ReadEntry(truncated, new byte[truncated.TotalLength], validateCrc: false)).ThrowsExactly<EndOfStreamException>();
await Assert.That(() => package.GetMemoryMappedStreamIfPossible(truncated)).ThrowsExactly<EndOfStreamException>();
}
}
}
}
finally
{
Directory.Delete(directory, recursive: true);
}
}

[Test]
public async Task TestFileChecksums()
{
Expand Down
108 changes: 108 additions & 0 deletions ValvePak/ValvePak/ArchiveFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading;
using Microsoft.Win32.SafeHandles;

namespace ValvePak
{
/// <summary>
/// A file containing archive data of a package, kept open and memory mapped on first use.
/// </summary>
internal sealed class ArchiveFile : IDisposable
{
private readonly bool OwnsHandle;
private readonly Lock MappingLock = new();
private bool IsMapped;
private MemoryMappedFile? Mapping;

/// <summary>
/// Initializes a new instance of the <see cref="ArchiveFile"/> class.
/// </summary>
/// <param name="handle">Handle of the file, opened for reading.</param>
/// <param name="ownsHandle">Whether the handle is closed when this archive is disposed.</param>
public ArchiveFile(SafeFileHandle handle, bool ownsHandle)
{
Handle = handle;
OwnsHandle = ownsHandle;
}

/// <summary>
/// Gets the file handle.
/// </summary>
public SafeFileHandle Handle { get; }

/// <summary>
/// Gets the length in bytes of the file at the time it was mapped.
/// </summary>
public long MappedLength { get; private set; }

/// <summary>
/// Gets the view of the whole mapped file, only created in 64-bit processes.
/// </summary>
public MemoryMappedViewAccessor? View { get; private set; }

/// <summary>
/// Gets the memory mapping of the file, creating it on first use.
/// </summary>
/// <returns>The mapping, or null if the file is empty.</returns>
public MemoryMappedFile? GetMapping()
{
if (!Volatile.Read(ref IsMapped))
{
lock (MappingLock)
{
if (!IsMapped)
{
CreateMapping();
Volatile.Write(ref IsMapped, true);
}
}
}

return Mapping;
}

private void CreateMapping()
{
var length = RandomAccess.GetLength(Handle);

if (length == 0)
{
return;
}

var mapping = MemoryMappedFile.CreateFromFile(Handle, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, leaveOpen: true);

if (Environment.Is64BitProcess)
{
try
{
View = mapping.CreateViewAccessor(0, length, MemoryMappedFileAccess.Read);
}
catch
{
mapping.Dispose();
throw;
}
}

Mapping = mapping;
MappedLength = length;
}

/// <summary>
/// Releases the view and the mapping, and closes the handle if it is owned.
/// </summary>
public void Dispose()
{
View?.Dispose();
Mapping?.Dispose();

if (OwnsHandle)
{
Handle.Dispose();
}
}
}
}
Loading