Files
Wand-Enhancer-k1tbyte/AsarSharp/PickleTools/Pickle.cs
T
kitbyte 20956c3228 fix(asar): correct archive tree lookups and fail loudly on unreadable input
InsertFile resolved the grandparent node instead of the parent.
Reads no longer create phantom directories in the header.
Bound symlink traversal and skip reparse points when crawling.
Locked or unreadable files now abort packing instead of being dropped.
Read headers and integrity blocks with a full-read loop.
Assert the header keeps its size before overwriting the placeholder.
Validate Pickle buffer sizes, payload overflow and negative lengths.
Check CreateSymbolicLink and external tool exit codes.
Drop unused Pickle accessors, TransformedFile and FilesystemFilesAndLinks.Links.
2026-08-29 16:55:55 +03:00

225 lines
7.2 KiB
C#

using System;
using System.IO;
using System.Text;
namespace AsarSharp.PickleTools
{
public class Pickle
{
public const int SIZE_INT32 = 4;
public const int SIZE_UINT32 = 4;
public const int SIZE_INT64 = 8;
public const int SIZE_UINT64 = 8;
public const int SIZE_FLOAT = 4;
public const int SIZE_DOUBLE = 8;
// Initial payload allocation. Bumped from 64 — large headers used to
// realloc many times when growing geometrically from 64.
public const int PAYLOAD_UNIT = 4096;
public const long CAPACITY_READ_ONLY = 9007199254740992;
private byte[] _header;
private int _headerSize;
private long _capacityAfterHeader;
private int _writeOffset;
private Pickle(byte[] buffer = null)
{
if (buffer != null)
{
if (buffer.Length < SIZE_UINT32)
throw new ArgumentException("Buffer is too small.", nameof(buffer));
_header = buffer;
int payloadSize = GetPayloadSize();
if (payloadSize > buffer.Length)
throw new ArgumentException("Payload size exceeds buffer length.", nameof(buffer));
_headerSize = buffer.Length - payloadSize;
_capacityAfterHeader = CAPACITY_READ_ONLY;
_writeOffset = 0;
if (_headerSize != AlignInt(_headerSize, SIZE_UINT32))
{
_headerSize = 0;
}
if (_headerSize == 0)
{
_header = new byte[0];
}
}
else
{
_header = new byte[0];
_headerSize = SIZE_UINT32;
_capacityAfterHeader = 0;
_writeOffset = 0;
Resize(PAYLOAD_UNIT);
SetPayloadSize(0);
}
}
public static Pickle CreateEmpty() => new Pickle();
public static Pickle CreateFromBuffer(byte[] buffer) => new Pickle(buffer);
public byte[] GetHeader() => _header;
public int GetHeaderSize() => _headerSize;
public PickleIterator CreateIterator() => new PickleIterator(this);
/// <summary>Total byte length of the serialised pickle (header + payload).</summary>
public int GetTotalSize() => _headerSize + GetPayloadSize();
/// <summary>Materialise the pickle into a fresh byte array (allocates).</summary>
public byte[] ToBuffer()
{
int resultSize = GetTotalSize();
byte[] result = new byte[resultSize];
Buffer.BlockCopy(_header, 0, result, 0, resultSize);
return result;
}
/// <summary>Write the serialised pickle straight to <paramref name="stream"/> — no extra copy.</summary>
public void WriteTo(Stream stream)
{
stream.Write(_header, 0, GetTotalSize());
}
public bool WriteInt(int value)
{
const int dataLength = SIZE_INT32; // already 4-byte aligned
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
}
WriteInt32LE(value, _headerSize + _writeOffset);
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
}
public bool WriteUInt32(uint value)
{
const int dataLength = SIZE_UINT32;
int newSize = _writeOffset + dataLength;
if (newSize > _capacityAfterHeader)
{
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
}
WriteUInt32LE(value, _headerSize + _writeOffset);
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
}
public bool WriteString(string value)
{
int byteLen = Encoding.UTF8.GetByteCount(value);
if (!WriteInt(byteLen))
{
return false;
}
int aligned = AlignInt(byteLen, SIZE_UINT32);
int newSize = _writeOffset + aligned;
if (newSize > _capacityAfterHeader)
{
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
}
int writeStart = _headerSize + _writeOffset;
Encoding.UTF8.GetBytes(value, 0, value.Length, _header, writeStart);
// zero alignment padding
for (int i = writeStart + byteLen; i < writeStart + aligned; i++)
{
_header[i] = 0;
}
SetPayloadSize(newSize);
_writeOffset = newSize;
return true;
}
public void SetPayloadSize(int payloadSize)
{
WriteUInt32LE((uint)payloadSize, 0);
}
public int GetPayloadSize()
{
uint size = ReadUInt32LE(0);
if (size > int.MaxValue)
throw new InvalidOperationException("Payload size exceeds maximum allowed (2GB).");
return (int)size;
}
private void Resize(int newCapacity)
{
newCapacity = AlignInt(newCapacity, PAYLOAD_UNIT);
// The backing array must hold the header plus the full advertised
// payload capacity (matches Chromium's realloc(header_size_ + new_capacity)).
// Sizing it from _header.Length under-allocates by _headerSize on the
// first growth (when _header is still empty), leaving the payload region
// _headerSize bytes short of _capacityAfterHeader and overrunning the
// buffer when a write fills the payload.
byte[] newHeader = new byte[_headerSize + newCapacity];
Buffer.BlockCopy(_header, 0, newHeader, 0, Math.Min(_header.Length, newHeader.Length));
_header = newHeader;
_capacityAfterHeader = newCapacity;
}
public static int AlignInt(int i, int alignment)
{
return i + ((alignment - (i % alignment)) % alignment);
}
#region Auxiliary methods for reading/writing values in Little Endian
private uint ReadUInt32LE(int offset)
{
// _header is allocated by us so always little-endian-friendly when on LE host.
return (uint)(_header[offset] |
(_header[offset + 1] << 8) |
(_header[offset + 2] << 16) |
(_header[offset + 3] << 24));
}
private void WriteInt32LE(int value, int offset)
{
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
}
private void WriteUInt32LE(uint value, int offset)
{
_header[offset] = (byte)value;
_header[offset + 1] = (byte)(value >> 8);
_header[offset + 2] = (byte)(value >> 16);
_header[offset + 3] = (byte)(value >> 24);
}
#endregion
}
}