SharpNBT/SharpNBT/TagReader.cs

468 lines
20 KiB
C#

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace SharpNBT
{
/// <summary>
/// Provides methods for reading NBT data from a stream.
/// </summary>
[PublicAPI]
public class TagReader : TagIO
{
/// <summary>
/// Occurs when a tag has been fully deserialized from the stream.
/// </summary>
public event TagReaderCallback<TagEventArgs> TagRead;
public event TagReaderCallback<TagHandledEventArgs> TagEncountered;
/// <summary>
/// Gets the underlying stream this <see cref="TagReader"/> is operating on.
/// </summary>
[NotNull]
protected Stream BaseStream { get; }
private readonly bool leaveOpen;
/// <summary>
/// Creates a new instance of the <see cref="TagReader"/> class from the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">A <see cref="Stream"/> instance that the reader will be reading from.</param>
/// <param name="options">Bitwise flags to configure how data should be handled for compatibility between different specifications.</param>
/// <param name="leaveOpen">
/// <paramref langword="true"/> to leave the <paramref name="stream"/> object open after disposing the <see cref="TagReader"/>
/// object; otherwise, <see langword="false"/>.</param>
public TagReader([NotNull] Stream stream, FormatOptions options, bool leaveOpen = false) : base(options)
{
BaseStream = stream ?? throw new ArgumentNullException(nameof(stream));
if (!stream.CanRead)
throw new IOException("Stream is not opened for reading.");
this.leaveOpen = leaveOpen;
}
/// <summary>
/// Reads a <see cref="ByteTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="ByteTag"/> instance.</returns>
public ByteTag ReadByte(bool named = true)
{
var name = named ? ReadUTF8String() : null;
return new ByteTag(name, (byte) BaseStream.ReadByte());
}
/// <summary>
/// Reads a <see cref="ShortTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="ShortTag"/> instance.</returns>
public ShortTag ReadShort(bool named = true)
{
var name = named ? ReadUTF8String() : null;
short value;
if (UseVarInt)
{
value = (short)VarInt.Read(BaseStream, ZigZagEncoding);
}
else
{
Span<byte> buffer = stackalloc byte[sizeof(short)];
BaseStream.Read(buffer);
value = BitConverter.ToInt16(buffer);
if (SwapEndian)
value = value.SwapEndian();
}
return new ShortTag(name, value);
}
/// <summary>
/// Reads a <see cref="IntTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="IntTag"/> instance.</returns>
public IntTag ReadInt(bool named = true)
{
var name = named ? ReadUTF8String() : null;
return new IntTag(name, UseVarInt ? VarInt.Read(BaseStream, ZigZagEncoding) : ReadInt32());
}
/// <summary>
/// Reads a <see cref="LongTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="LongTag"/> instance.</returns>
public LongTag ReadLong(bool named = true)
{
var name = named ? ReadUTF8String() : null;
long value;
if (UseVarInt)
{
value = VarLong.Read(BaseStream, ZigZagEncoding);
}
else
{
Span<byte> buffer = stackalloc byte[sizeof(long)];
BaseStream.Read(buffer);
value = BitConverter.ToInt64(buffer);
if (SwapEndian)
value = value.SwapEndian();
}
return new LongTag(name, value);
}
/// <summary>
/// Reads a <see cref="FloatTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="FloatTag"/> instance.</returns>
public FloatTag ReadFloat(bool named = true)
{
var name = named ? ReadUTF8String() : null;
var buffer = new byte[sizeof(float)];
BaseStream.Read(buffer, 0, sizeof(float));
if (SwapEndian)
Array.Reverse(buffer);
return new FloatTag( name, BitConverter.ToSingle(buffer));
}
/// <summary>
/// Reads a <see cref="DoubleTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="DoubleTag"/> instance.</returns>
public DoubleTag ReadDouble(bool named = true)
{
var name = named ? ReadUTF8String() : null;
var buffer = new byte[sizeof(double)];
BaseStream.Read(buffer, 0, buffer.Length);
if (SwapEndian)
Array.Reverse(buffer);
return new DoubleTag( name, BitConverter.ToDouble(buffer, 0));
}
/// <summary>
/// Reads a <see cref="StringTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="StringTag"/> instance.</returns>
public StringTag ReadString(bool named = true)
{
var name = named ? ReadUTF8String() : null;
var value = ReadUTF8String();
return new StringTag(name, value);
}
/// <summary>
/// Reads a <see cref="ByteArrayTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="ByteArrayTag"/> instance.</returns>
public ByteArrayTag ReadByteArray(bool named = true)
{
var name = named ? ReadUTF8String() : null;
var count = ReadCount();
var buffer = new byte[count];
BaseStream.Read(buffer, 0, count);
return new ByteArrayTag(name, buffer);
}
/// <summary>
/// Reads a <see cref="IntArrayTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="IntArrayTag"/> instance.</returns>
public IntArrayTag ReadIntArray(bool named = true)
{
const int INT_SIZE = sizeof(int);
var name = named ? ReadUTF8String() : null;
var count = ReadCount();
if (UseVarInt)
{
var array = new int[count];
for (var i = 0; i < count; i++)
array[i] = VarInt.Read(BaseStream, ZigZagEncoding);
return new IntArrayTag(name, array);
}
var buffer = new byte[count * INT_SIZE];
BaseStream.Read(buffer, 0, count * INT_SIZE);
Span<int> values = MemoryMarshal.Cast<byte, int>(buffer);
if (SwapEndian)
{
for (var i = 0; i < count; i++)
values[i] = values[i].SwapEndian();
}
return new IntArrayTag(name, values);
}
/// <summary>
/// Reads a <see cref="LongArrayTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="LongArrayTag"/> instance.</returns>
public LongArrayTag ReadLongArray(bool named = true)
{
const int LONG_SIZE = sizeof(long);
var name = named ? ReadUTF8String() : null;
var count = ReadCount();
if (UseVarInt)
{
var array = new long[count];
for (var i = 0; i < count; i++)
array[i] = VarLong.Read(BaseStream, ZigZagEncoding);
return new LongArrayTag(name, array);
}
var buffer = new byte[count * LONG_SIZE];
BaseStream.Read(buffer, 0, count * LONG_SIZE);
Span<long> values = MemoryMarshal.Cast<byte, long>(buffer);
if (SwapEndian)
{
for (var i = 0; i < count; i++)
values[i] = values[i].SwapEndian();
}
return new LongArrayTag(name, values);
}
/// <summary>
/// Reads a <see cref="ListTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="ListTag"/> instance.</returns>
public ListTag ReadList(bool named = true)
{
var name = named ? ReadUTF8String() : null;
var childType = ReadType();
var count = ReadCount();
if (childType == TagType.End && count > 0)
throw new FormatException("An EndTag is not a valid child type for a non-empty ListTag.");
var list = new ListTag(name, childType);
while (count-- > 0)
{
list.Add(ReadTag(childType, false));
}
return list;
}
/// <summary>
/// Reads a <see cref="CompoundTag"/> from the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <remarks>It is assumed that the stream is positioned at the beginning of the tag payload.</remarks>
/// <returns>The deserialized <see cref="CompoundTag"/> instance.</returns>
public CompoundTag ReadCompound(bool named = true)
{
var name = named ? ReadUTF8String() : null;
var compound = new CompoundTag(name);
while (true)
{
var type = ReadType();
if (type == TagType.End)
break;
compound.Add(ReadTag(type, true));
}
return compound;
}
/// <summary>
/// Reads a <see cref="Tag"/> from the current position in the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <returns>The tag instance that was read from the stream.</returns>
public Tag ReadTag(bool named = true)
{
var type = ReadType();
return ReadTag(type, named);
}
/// <summary>
/// Asynchronously reads a <see cref="Tag"/> from the current position in the stream.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <returns>The tag instance that was read from the stream.</returns>
public async Task<Tag> ReadTagAsync(bool named = true)
{
return await Task.Run(() => ReadTag(named));
}
/// <summary>
/// Convenience method to read a tag and cast it automatically.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <typeparam name="T">The tag type that is being read from the stream.</typeparam>
/// <returns>The tag instance that was read from the stream.</returns>
/// <remarks>This is typically only used when reading the top-level <see cref="CompoundTag"/> of a document where the type is already known.</remarks>
public T ReadTag<T>(bool named = true) where T : Tag
{
return (T)ReadTag(named);
}
/// <summary>
/// Convenience method to asynchronously read a tag and cast it automatically.
/// </summary>
/// <param name="named">Flag indicating if this tag is named, only <see langowrd="false"/> when a tag is a direct child of a <see cref="ListTag"/>.</param>
/// <typeparam name="T">The tag type that is being read from the stream.</typeparam>
/// <returns>The tag instance that was read from the stream.</returns>
/// <remarks>This is typically only used when reading the top-level <see cref="CompoundTag"/> of a document where the type is already known.</remarks>
public async Task<T> ReadTagAsync<T>(bool named = true) where T : Tag
{
var tag = await ReadTagAsync(named);
return (T)tag;
}
[NotNull]
private Tag ReadTag(TagType type, bool named)
{
var result = OnTagEncountered(type, named);
if (result != null)
{
OnTagRead(result);
return result;
}
Tag tag = type switch
{
TagType.End => new EndTag(),
TagType.Byte => ReadByte(named),
TagType.Short => ReadShort(named),
TagType.Int => ReadInt(named),
TagType.Long => ReadLong(named),
TagType.Float => ReadFloat(named),
TagType.Double => ReadDouble(named),
TagType.ByteArray => ReadByteArray(named),
TagType.String => ReadString(named),
TagType.List => ReadList(named),
TagType.Compound => ReadCompound(named),
TagType.IntArray => ReadIntArray(named),
TagType.LongArray => ReadLongArray(named),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
OnTagRead(tag);
return tag;
}
private TagType ReadType()
{
try
{
return (TagType)BaseStream.ReadByte();
}
catch (EndOfStreamException)
{
return TagType.End;
}
}
/// <summary>
/// Reads a length-prefixed UTF-8 string from the stream.
/// </summary>
/// <returns>The deserialized string instance.</returns>
[CanBeNull]
protected string ReadUTF8String()
{
int length;
if (UseVarInt)
length = VarInt.Read(BaseStream);
else
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
BaseStream.Read(buffer);
var uint16 = BitConverter.ToUInt16(buffer);
length = SwapEndian ? uint16.SwapEndian() : uint16;
}
if (length == 0)
return null;
var utf8 = new byte[length];
BaseStream.Read(utf8, 0, length);
return Encoding.UTF8.GetString(utf8);
}
private int ReadCount() => UseVarInt ? VarInt.Read(BaseStream, ZigZagEncoding) : ReadInt32();
/// <summary>
/// Reads a 64-bit signed (big-endian) integer from the stream, converting to native endian when necessary.
/// </summary>
/// <returns>The deserialized value.</returns>
private int ReadInt32()
{
Span<byte> buffer = stackalloc byte[sizeof(int)];
BaseStream.Read(buffer);
var value = BitConverter.ToInt32(buffer);
return SwapEndian ? value.SwapEndian() : value;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public override void Dispose()
{
if (!leaveOpen)
BaseStream.Dispose();
}
/// <summary>
/// Asynchronously releases the unmanaged resources used by the <see cref="TagReader"/>.
/// </summary>
public override async ValueTask DisposeAsync()
{
if (!leaveOpen)
await BaseStream.DisposeAsync();
}
/// <summary>
/// Invokes the <see cref="TagRead"/> event when a tag has been fully deserialized from the <see cref="BaseStream"/>.
/// </summary>
/// <param name="tag">The deserialized <see cref="Tag"/> instance.</param>
protected virtual void OnTagRead(Tag tag) => TagRead?.Invoke(this, new TagEventArgs(tag.Type, tag));
[CanBeNull]
protected virtual Tag OnTagEncountered(TagType type, bool named)
{
// Early out if no subscribers.
if (TagEncountered is null)
return null;
var args = new TagHandledEventArgs(type, named, BaseStream);
TagEncountered.Invoke(this, args);
return args.Handled ? args.Result : null;
}
}
}