sharpemu/tests/SharpEmu.Libs.Tests/FileSystem/ZArchiveTestWriter.cs

204 lines
6.7 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
using ZstdSharp;
namespace SharpEmu.Libs.Tests.FileSystem;
// Builds .zar archives the way the ZArchive writer lays them out: file data appended in order and
// cut into 64 KiB blocks (the last padded with zeros, a block that does not shrink stored raw), the
// data section padded to 8 bytes, then offset records, names, a breadth-first file tree with each
// directory's children sorted without case, empty metadata and the footer.
internal static class ZArchiveTestWriter
{
private const int BlockSize = 64 * 1024;
private sealed class PathNode(string name, bool isFile)
{
public string Name { get; } = name;
public bool IsFile { get; } = isFile;
public List<PathNode> Children { get; } = [];
public ulong FileOffset { get; set; }
public ulong FileSize { get; set; }
public uint NameOffset { get; set; }
public uint FirstChild { get; set; }
}
public static byte[] Build(IEnumerable<(string Path, byte[] Data)> files)
{
var root = new PathNode(string.Empty, isFile: false);
var data = new MemoryStream();
foreach (var (path, bytes) in files)
{
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
var directory = root;
foreach (var part in parts[..^1])
{
var next = directory.Children.Find(child => string.Equals(child.Name, part, StringComparison.OrdinalIgnoreCase));
if (next is null)
{
next = new PathNode(part, isFile: false);
directory.Children.Add(next);
}
directory = next;
}
directory.Children.Add(new PathNode(parts[^1], isFile: true) { FileOffset = (ulong)data.Length, FileSize = (ulong)bytes.Length });
data.Write(bytes);
}
if (data.Length % BlockSize != 0)
{
data.Write(new byte[BlockSize - data.Length % BlockSize]);
}
var output = new MemoryStream();
var records = new List<(ulong BaseOffset, ushort[] Sizes)>();
using var compressor = new Compressor(6);
var raw = data.ToArray();
for (var block = 0; block < raw.Length / BlockSize; block++)
{
var input = raw.AsSpan(block * BlockSize, BlockSize);
var compressed = compressor.Wrap(input).ToArray();
var offset = (ulong)output.Length;
int size;
if (compressed.Length >= BlockSize)
{
output.Write(input);
size = BlockSize;
}
else
{
output.Write(compressed);
size = compressed.Length;
}
if (block % 16 == 0)
{
records.Add((offset, new ushort[16]));
}
records[^1].Sizes[block % 16] = (ushort)(size - 1);
}
var dataSize = (ulong)output.Length;
while (output.Length % 8 != 0)
{
output.WriteByte(0);
}
var sections = new (ulong Offset, ulong Size)[6];
sections[0] = (0, dataSize);
var start = (ulong)output.Length;
foreach (var (baseOffset, sizes) in records)
{
WriteU64(output, baseOffset);
foreach (var size in sizes)
{
WriteU16(output, size);
}
}
sections[1] = (start, (ulong)output.Length - start);
// Names in breadth-first order, as the writer interns them.
var ordered = new List<PathNode>();
var queue = new Queue<PathNode>();
queue.Enqueue(root);
var nextIndex = 1u;
while (queue.TryDequeue(out var node))
{
ordered.Add(node);
if (node.IsFile)
{
continue;
}
node.Children.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
node.FirstChild = nextIndex;
nextIndex += (uint)node.Children.Count;
foreach (var child in node.Children)
{
queue.Enqueue(child);
}
}
start = (ulong)output.Length;
foreach (var node in ordered.Skip(1))
{
node.NameOffset = (uint)((ulong)output.Length - start);
var name = Encoding.UTF8.GetBytes(node.Name);
if (name.Length >= 0x80)
{
output.WriteByte((byte)((name.Length & 0x7F) | 0x80));
output.WriteByte((byte)(name.Length >> 7));
}
else
{
output.WriteByte((byte)name.Length);
}
output.Write(name);
}
sections[2] = (start, (ulong)output.Length - start);
start = (ulong)output.Length;
foreach (var node in ordered)
{
var nameOffset = ReferenceEquals(node, root) ? 0x7FFFFFFFu : node.NameOffset;
WriteU32(output, nameOffset | (node.IsFile ? 0x80000000u : 0));
if (node.IsFile)
{
WriteU32(output, (uint)node.FileOffset);
WriteU32(output, (uint)node.FileSize);
WriteU32(output, (uint)((node.FileOffset >> 32) & 0xFFFF) | (uint)((node.FileSize >> 16) & 0xFFFF0000));
}
else
{
WriteU32(output, node.FirstChild);
WriteU32(output, (uint)node.Children.Count);
WriteU32(output, 0);
}
}
sections[3] = (start, (ulong)output.Length - start);
sections[4] = ((ulong)output.Length, 0);
sections[5] = ((ulong)output.Length, 0);
foreach (var (offset, size) in sections)
{
WriteU64(output, offset);
WriteU64(output, size);
}
output.Write(new byte[32]);
WriteU64(output, (ulong)output.Length + 8 + 4 + 4);
WriteU32(output, 0x61BF3A01);
WriteU32(output, 0x169F52D6);
return output.ToArray();
}
private static void WriteU16(Stream stream, ushort value)
{
Span<byte> bytes = stackalloc byte[2];
BinaryPrimitives.WriteUInt16BigEndian(bytes, value);
stream.Write(bytes);
}
private static void WriteU32(Stream stream, uint value)
{
Span<byte> bytes = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(bytes, value);
stream.Write(bytes);
}
private static void WriteU64(Stream stream, ulong value)
{
Span<byte> bytes = stackalloc byte[8];
BinaryPrimitives.WriteUInt64BigEndian(bytes, value);
stream.Write(bytes);
}
}