commit d2a1cabe35027e4008a147bf1eed18d4c180b9f2 Author: Hervé BECHER Date: Fri Mar 15 14:44:21 2024 +0100 Initial sync diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..222bc53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.vs +/.vscode + +bin/ +obj/ + +/Nbt/Properties \ No newline at end of file diff --git a/NamedBinaryTag.sln b/NamedBinaryTag.sln new file mode 100755 index 0000000..6bc5e9c --- /dev/null +++ b/NamedBinaryTag.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32228.430 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{B064142B-B2BD-4F0A-B37C-EF8DEF542368}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nbt", "Nbt\Nbt.csproj", "{431C4EC2-37FD-4DBD-8966-DF757431346B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B064142B-B2BD-4F0A-B37C-EF8DEF542368}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B064142B-B2BD-4F0A-B37C-EF8DEF542368}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B064142B-B2BD-4F0A-B37C-EF8DEF542368}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B064142B-B2BD-4F0A-B37C-EF8DEF542368}.Release|Any CPU.Build.0 = Release|Any CPU + {431C4EC2-37FD-4DBD-8966-DF757431346B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {431C4EC2-37FD-4DBD-8966-DF757431346B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {431C4EC2-37FD-4DBD-8966-DF757431346B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {431C4EC2-37FD-4DBD-8966-DF757431346B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0B021450-A24B-44BB-A85D-D334EE7F9BA1} + EndGlobalSection +EndGlobal diff --git a/Nbt/Exceptions/NbtException.cs b/Nbt/Exceptions/NbtException.cs new file mode 100755 index 0000000..78359e0 --- /dev/null +++ b/Nbt/Exceptions/NbtException.cs @@ -0,0 +1,10 @@ +namespace Nbt; + +public class NbtException : Exception +{ + public NbtException() { } + + public NbtException(string? message) : base(message) { } + + public NbtException(string? message, Exception? innerException) : base(message, innerException) { } +} diff --git a/Nbt/Exceptions/TagNotFoundException.cs b/Nbt/Exceptions/TagNotFoundException.cs new file mode 100755 index 0000000..caeacce --- /dev/null +++ b/Nbt/Exceptions/TagNotFoundException.cs @@ -0,0 +1,22 @@ +// using Nbt.Tag; + +// namespace Nbt; + +// public class TagNotFoundException : NbtException +// { +// public TagNotFoundException(INbtTag parentTag, INbtPathElement tagPathElement) +// { +// ParentTag = parentTag; +// TagPathElement = tagPathElement; +// } + +// public TagNotFoundException(INbtTag parentTag, INbtPathElement tagPathElement, string? message) : base(message) +// { +// ParentTag = parentTag; +// TagPathElement = tagPathElement; +// } + +// public INbtTag ParentTag { get; } + +// public INbtPathElement TagPathElement { get; } +// } diff --git a/Nbt/Exceptions/UnknownCompressionSchemeException.cs b/Nbt/Exceptions/UnknownCompressionSchemeException.cs new file mode 100755 index 0000000..07d2690 --- /dev/null +++ b/Nbt/Exceptions/UnknownCompressionSchemeException.cs @@ -0,0 +1,16 @@ +namespace Nbt; + +public class UnknownCompressionSchemeException : NbtException +{ + public int CompressionMode { get; } + + public UnknownCompressionSchemeException(int mode) + { + CompressionMode = mode; + } + + public UnknownCompressionSchemeException(int mode, string? message) : base(message) + { + CompressionMode = mode; + } +} diff --git a/Nbt/Exceptions/UnknownTagTypeException.cs b/Nbt/Exceptions/UnknownTagTypeException.cs new file mode 100755 index 0000000..52944ba --- /dev/null +++ b/Nbt/Exceptions/UnknownTagTypeException.cs @@ -0,0 +1,16 @@ +namespace Nbt; + +public class UnknownTagTypeException : NbtException +{ + public UnknownTagTypeException(NbtTagType unknownTagType) : base() + { + UnknownTagType = unknownTagType; + } + + public UnknownTagTypeException(NbtTagType unknownTagType, string? message) : base(message) + { + UnknownTagType = unknownTagType; + } + + public NbtTagType UnknownTagType { get; } +} \ No newline at end of file diff --git a/Nbt/Exceptions/UnsupportedPathElementException.cs b/Nbt/Exceptions/UnsupportedPathElementException.cs new file mode 100755 index 0000000..11dc710 --- /dev/null +++ b/Nbt/Exceptions/UnsupportedPathElementException.cs @@ -0,0 +1,8 @@ +namespace Nbt; + +public class UnsupportedPathElementException : NbtException +{ + public UnsupportedPathElementException() : base() { } + + public UnsupportedPathElementException(string? message) : base(message) { } +} \ No newline at end of file diff --git a/Nbt/Exceptions/WrongTagTypeException.cs b/Nbt/Exceptions/WrongTagTypeException.cs new file mode 100755 index 0000000..fbaf2f3 --- /dev/null +++ b/Nbt/Exceptions/WrongTagTypeException.cs @@ -0,0 +1,8 @@ +namespace Nbt; + +public class WrongTagTypeException : NbtException +{ + public WrongTagTypeException() : base() { } + + public WrongTagTypeException(string? message) : base(message) { } +} \ No newline at end of file diff --git a/Nbt/Nbt.csproj b/Nbt/Nbt.csproj new file mode 100755 index 0000000..ee4f76e --- /dev/null +++ b/Nbt/Nbt.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + Nbt + 1.0.0 + Hervé BECHER + HbShare + Nbt + Nbt library + + + + + + + diff --git a/Nbt/NbtPath.cs b/Nbt/NbtPath.cs new file mode 100755 index 0000000..9ea5350 --- /dev/null +++ b/Nbt/NbtPath.cs @@ -0,0 +1,139 @@ +using System.Collections; + +namespace Nbt; + +public static class NbtPath +{ + public static readonly INbtPath Empty = new EmptyNbtPathImpl(); + + public static INbtPath CreatePath(INbtPathElement pathElement) => new NbtPathImpl([pathElement]); + + public static INbtPath CreatePath(params INbtPathElement[] pathElements) => CreatePath(pathElements.AsEnumerable()); + + public static INbtPath CreatePath(IEnumerable pathElements) => new NbtPathImpl(pathElements); + + public static INbtPath CreatePath(params object[] pathElements) => CreatePath(pathElements.AsEnumerable()); + + public static INbtPath CreatePath(IEnumerable pathElements) => CreatePath(pathElements.Select(WrapPathElement)); + + private static INbtPathElement WrapPathElement(object pathElement) => pathElement switch + { + null => throw new ArgumentNullException(nameof(pathElement)), + string name => new NbtName(name), + int index => new NbtIndex(index), + _ => throw new ArgumentException($"Invalid path element type: {pathElement.GetType().Name}", nameof(pathElement)) + }; +} + +public interface INbtPath : IEnumerable +{ + int Count { get; } + + bool IsEmpty { get; } + + INbtPathElement PopFirst(out INbtPath rest); + + INbtPathElement this[int index] { get; } + + INbtPath Combine(INbtPath other); + + INbtPath Append(INbtPathElement pathElement); + + INbtPath Insert(int index, INbtPathElement pathElement); + + INbtPath Replace(int index, INbtPathElement pathElement); +} + +file class NbtPathImpl(IEnumerable pathElements) : INbtPath +{ + private readonly IList _pathElements = pathElements.AsIList(); + + public int Count => _pathElements.Count; + + public bool IsEmpty => Count == 0; + + public INbtPathElement PopFirst(out INbtPath rest) + { + var count = Count; + + if (count < 1) + { + throw new IndexOutOfRangeException(); + } + + rest = count > 1 ? new NbtPathImpl(_pathElements.Skip(1)) : NbtPath.Empty; + return _pathElements[0]; + } + + public INbtPathElement this[int index] => _pathElements[index]; + + public INbtPath Append(INbtPathElement pathElement) => new NbtPathImpl(_pathElements.Append(pathElement)); + + public INbtPath Insert(int index, INbtPathElement pathElement) => new NbtPathImpl(Utils.Insert(_pathElements, index, pathElement)); + + public INbtPath Replace(int index, INbtPathElement pathElement) => new NbtPathImpl(Utils.Replace(_pathElements, index, pathElement)); + + public INbtPath Combine(INbtPath other) => new NbtPathImpl(_pathElements.Concat(other)); + + public IEnumerator GetEnumerator() => _pathElements.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +file class EmptyNbtPathImpl : INbtPath +{ + public int Count => 0; + + public bool IsEmpty => true; + + public INbtPathElement PopFirst(out INbtPath rest) => throw new IndexOutOfRangeException(); + + public INbtPathElement this[int index] => throw new IndexOutOfRangeException(); + + public INbtPath Append(INbtPathElement pathElement) => new NbtPathImpl(pathElement.Yield()); + + public INbtPath Insert(int index, INbtPathElement pathElement) + { + ArgumentOutOfRangeException.ThrowIfNotEqual(index, 0); + + return new NbtPathImpl(pathElement.Yield()); + } + + public INbtPath Replace(int index, INbtPathElement pathElement) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + public INbtPath Combine(INbtPath other) => other; + + public IEnumerator GetEnumerator() => Enumerable.Empty().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +public interface INbtPathElement +{ + object Key { get; } +} + +public readonly struct NbtName(string name) : INbtPathElement +{ + public string Name { get; } = name; + + object INbtPathElement.Key => Name; + + public static implicit operator NbtName(string tagName) => new(tagName); + + public static implicit operator string(NbtName nbtName) => nbtName.Name; +} + +public readonly struct NbtIndex(int index) : INbtPathElement +{ + public int Index { get; } = index; + + object INbtPathElement.Key => Index; + + public static implicit operator NbtIndex(int tagIndex) => new(tagIndex); + + public static implicit operator int(NbtIndex nbtIndex) => nbtIndex.Index; +} diff --git a/Nbt/NbtTagType.cs b/Nbt/NbtTagType.cs new file mode 100755 index 0000000..7e052b3 --- /dev/null +++ b/Nbt/NbtTagType.cs @@ -0,0 +1,19 @@ +namespace Nbt; + +public enum NbtTagType : byte +{ + Unknown = 0xFF, + End = 0x0, + Byte = 0x1, + Short = 0x2, + Int = 0x3, + Long = 0x4, + Float = 0x5, + Double = 0x6, + ByteArray = 0x7, + String = 0x8, + List = 0x9, + Compound = 0xA, + IntArray = 0xB, + LongArray = 0xC +} diff --git a/Nbt/NbtUtils.cs b/Nbt/NbtUtils.cs new file mode 100755 index 0000000..89bb13e --- /dev/null +++ b/Nbt/NbtUtils.cs @@ -0,0 +1,160 @@ +using System.Collections; +using System.Text; +using Nbt.Type; + +namespace Nbt; + +internal static class NbtUtils +{ + public const char ESCAPE_CHAR = '\\'; + public const char SINGLE_QUOTE_CHAR = '\''; + public const char DOUBLE_QUOTE_CHAR = '"'; + + public static readonly INbtRegistry Registry = new NbtRegistry(); + + static NbtUtils() + { + Registry.AddAll(NbtByteType.Value, NbtShortType.Value, NbtIntType.Value, NbtLongType.Value, NbtFloatType.Value, NbtDoubleType.Value, NbtStringType.Value, NbtListType.Value, NbtCompoundType.Value, NbtEndType.Value, NbtByteArrayType.Value, NbtIntArrayType.Value, NbtLongArrayType.Value); + } + + public static INbtType GetTagType(NbtTagType tagType) => Registry.TryGet(tagType, out var value) ? value : throw new UnknownTagTypeException(tagType, $"Unknown tag type: {tagType}"); + + public static NbtTagType GetValueTagType() => GetValueTagType(typeof(T)); + + public static NbtTagType GetValueTagType(System.Type type) => + type == typeof(sbyte) ? NbtTagType.Byte : + type == typeof(short) ? NbtTagType.Short : + type == typeof(int) ? NbtTagType.Int : + type == typeof(long) ? NbtTagType.Long : + type == typeof(float) ? NbtTagType.Float : + type == typeof(double) ? NbtTagType.Double : + type == typeof(string) ? NbtTagType.String : + NbtTagType.Unknown; + + public static NbtTagType GetArrayTagType() => GetArrayTagType(typeof(T)); + + public static NbtTagType GetArrayTagType(System.Type type) => + type == typeof(sbyte) ? NbtTagType.ByteArray : + type == typeof(int) ? NbtTagType.IntArray : + type == typeof(long) ? NbtTagType.LongArray : + NbtTagType.Unknown; + + public static NbtTagType EnsureValueType() + { + var type = typeof(T); + var tagType = GetValueTagType(type); + return tagType is NbtTagType.Unknown ? throw new UnknownTagTypeException(tagType, $"Unknown value type: {type.Name}") : tagType; + } + + public static NbtTagType EnsureArrayType() + { + var type = typeof(T); + var tagType = GetArrayTagType(type); + return tagType is NbtTagType.Unknown ? throw new UnknownTagTypeException(tagType, $"Unknown array type: {type.Name}") : tagType; + } + + public static NbtTagType ToArrayType(this NbtTagType tagType) => tagType switch + { + NbtTagType.Byte => NbtTagType.ByteArray, + NbtTagType.Int => NbtTagType.IntArray, + NbtTagType.Long => NbtTagType.LongArray, + _ => NbtTagType.Unknown + }; + + public static NbtTagType ToValueType(this NbtTagType tagType) => tagType switch + { + NbtTagType.ByteArray => NbtTagType.Byte, + NbtTagType.IntArray => NbtTagType.Int, + NbtTagType.LongArray => NbtTagType.Long, + _ => NbtTagType.Unknown + }; + + public static string QuoteString(string s, bool onlyIfNeeded = false) + { + if (s.Length == 0) + { + return $"{DOUBLE_QUOTE_CHAR}{DOUBLE_QUOTE_CHAR}"; + } + + var sb = new StringBuilder(); + var needQuotes = false; + var quoteChar = '\0'; + + foreach (var c in s) + { + needQuotes |= QuoteString(sb, c, ref quoteChar); + } + + if (!onlyIfNeeded || needQuotes) + { + quoteChar = quoteChar == DOUBLE_QUOTE_CHAR ? SINGLE_QUOTE_CHAR : DOUBLE_QUOTE_CHAR; + + sb.Insert(0, quoteChar); + sb.Append(quoteChar); + } + + return sb.ToString(); + } + + private static bool QuoteString(StringBuilder sb, char c, ref char quoteChar) + { + var needQuotes = false; + var escape = false; + + switch (c) + { + case '\t': + escape = true; + c = 't'; + break; + + case '\r': + escape = true; + c = 'r'; + break; + + case '\n': + escape = true; + c = 'n'; + break; + + case ESCAPE_CHAR: + escape = true; + break; + + case SINGLE_QUOTE_CHAR: + case DOUBLE_QUOTE_CHAR: + if (quoteChar == '\0') + { + quoteChar = c; + } + else if (c != quoteChar) + { + escape = true; + } + + needQuotes = true; + break; + + case '.': + case '_': + case '-': + case '+': + case >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9': + break; + + default: + needQuotes = true; + break; + } + + if (escape) + { + sb.Append(ESCAPE_CHAR); + } + + sb.Append(c); + + return needQuotes || escape; + } +} diff --git a/Nbt/Serialization/Compression.cs b/Nbt/Serialization/Compression.cs new file mode 100755 index 0000000..efe5c26 --- /dev/null +++ b/Nbt/Serialization/Compression.cs @@ -0,0 +1,11 @@ +namespace Nbt.Serialization; + +public enum Compression : byte +{ + Auto = byte.MaxValue, + None = 0, + Uncompressed = None, + GZip, + ZLib, + LZ4 +} diff --git a/Nbt/Serialization/JsonNbt.cs b/Nbt/Serialization/JsonNbt.cs new file mode 100755 index 0000000..7cff8c5 --- /dev/null +++ b/Nbt/Serialization/JsonNbt.cs @@ -0,0 +1,640 @@ +using System.Text; +using System.Text.Json; +using Nbt.Tag; + +namespace Nbt.Serialization; + +public static class JsonNbt +{ + private static readonly JsonEncodedText TypeProperty = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText ValueProperty = JsonEncodedText.Encode("value"); + + #region Serialize lossless + + public static string Serialize(INbtTag tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtList tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtCompound tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag) => Serialize(serializer => serializer.Serialize(tag)); + + public static void Serialize(TextWriter writer, INbtTag tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtList tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtCompound tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag) => Serialize(writer, serializer => serializer.Serialize(tag)); + + public static void Serialize(Stream stream, INbtTag tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtList tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtCompound tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag) => Serialize(stream, serializer => serializer.Serialize(tag)); + + private static string Serialize(Action serialize) + { + using var writer = new StringWriter(); + Serialize(writer, serialize); + return writer.ToString(); + } + + private static void Serialize(TextWriter writer, Action serialize) + { + using var stream = new MemoryStream(); + Serialize(stream, serialize); + stream.Position = 0L; + + using var reader = new StreamReader(stream, encoding: Encoding.UTF8, leaveOpen: true); + + while (true) + { + var i = reader.Read(); + + if (i < 0) + { + break; + } + + writer.Write((char)i); + } + } + + private static void Serialize(Stream stream, Action serialize) + { + using var serializer = new Serializer(stream); + serialize(serializer); + } + + private sealed class Serializer(Stream stream) : IDisposable + { + private readonly Utf8JsonWriter writer = new(stream, new() { Indented = true, SkipValidation = true }); + + public void Serialize(INbtTag tag) + { + switch (tag) + { + case INbtArray arrayTag: + Serialize(arrayTag); + break; + + case INbtValue valueTag: + Serialize(valueTag); + break; + + case INbtList listTag: + Serialize(listTag); + break; + + case INbtCompound compoundTag: + Serialize(compoundTag); + break; + } + } + + public void Serialize(INbtList tag) + { + using (new NbtObjectMeta(writer, tag.Type)) + { + writer.WriteStartArray(); + + foreach (var entry in tag) + { + Serialize(entry); + } + + writer.WriteEndArray(); + } + } + + public void Serialize(INbtCompound tag) + { + using (new NbtObjectMeta(writer, tag.Type)) + { + writer.WriteStartObject(); + + foreach (var entry in tag as IEnumerable) + { + writer.WritePropertyName(entry.Name); + Serialize(entry.Tag); + } + + writer.WriteEndObject(); + } + } + + public void Serialize(INbtArray tag) + { + switch (tag) + { + case INbtArray byteArrayTag: + Serialize(byteArrayTag); + break; + + case INbtArray intArrayTag: + Serialize(intArrayTag); + break; + + case INbtArray longArrayTag: + Serialize(longArrayTag); + break; + } + } + + public void Serialize(INbtValue valueTag) + { + switch (valueTag) + { + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + } + } + + public void Serialize(INbtArray tag) => Serialize(tag, Serialize); + public void Serialize(INbtArray tag) => Serialize(tag, Serialize); + public void Serialize(INbtArray tag) => Serialize(tag, Serialize); + + public void Serialize(INbtValue tag) => Serialize(tag, Serialize); + public void Serialize(INbtValue tag) => Serialize(tag, Serialize); + public void Serialize(INbtValue tag) => Serialize(tag, Serialize); + public void Serialize(INbtValue tag) => Serialize(tag, Serialize); + public void Serialize(INbtValue tag) => Serialize(tag, Serialize); + public void Serialize(INbtValue tag) => Serialize(tag, Serialize); + public void Serialize(INbtValue tag) => Serialize(tag, Serialize); + + private void Serialize(INbtArray tag, Action writeValue) where T : notnull + { + using (new NbtObjectMeta(writer, tag.Type)) + { + writer.WriteStartArray(); + + foreach (var e in tag) + { + writeValue(e); + } + + writer.WriteEndArray(); + } + } + + private void Serialize(INbtValue tag, Action writeValue) where T : notnull + { + using (new NbtObjectMeta(writer, tag.Type)) + { + writeValue(tag.Value); + } + } + + private void Serialize(sbyte value) => writer.WriteNumberValue(value); + private void Serialize(short value) => writer.WriteNumberValue(value); + private void Serialize(int value) => writer.WriteNumberValue(value); + private void Serialize(long value) => writer.WriteNumberValue(value); + private void Serialize(float value) => writer.WriteNumberValue(value); + private void Serialize(double value) => writer.WriteNumberValue(value); + private void Serialize(string value) => writer.WriteStringValue(value); + + public void Dispose() + { + writer.Dispose(); + + GC.SuppressFinalize(this); + } + + private readonly struct NbtObjectMeta : IDisposable + { + private readonly Utf8JsonWriter writer; + + public NbtObjectMeta(Utf8JsonWriter writer, NbtTagType tagType) + { + this.writer = writer; + + writer.WriteStartObject(); + writer.WriteNumber(TypeProperty, (int)tagType); + writer.WritePropertyName(ValueProperty); + } + + public void Dispose() + { + writer.WriteEndObject(); + } + } + } + + #endregion + + #region Serialize lossy + + public static string ToJson(INbtTag tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtList tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtCompound tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtArray tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtArray tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtArray tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtArray tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + public static string ToJson(INbtValue tag) => ToJson(serializer => serializer.ToJson(tag)); + + public static void ToJson(TextWriter writer, INbtTag tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtList tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtCompound tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtArray tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtArray tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtArray tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtArray tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + public static void ToJson(TextWriter writer, INbtValue tag) => ToJson(writer, serializer => serializer.ToJson(tag)); + + public static void ToJson(Stream stream, INbtTag tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtList tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtCompound tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtArray tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtArray tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtArray tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtArray tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + public static void ToJson(Stream stream, INbtValue tag) => ToJson(stream, serializer => serializer.ToJson(tag)); + + private static string ToJson(Action serialize) + { + using var writer = new StringWriter(); + ToJson(writer, serialize); + return writer.ToString(); + } + + private static void ToJson(TextWriter writer, Action serialize) + { + using var stream = new MemoryStream(); + ToJson(stream, serialize); + stream.Position = 0L; + + using var reader = new StreamReader(stream, encoding: Encoding.UTF8, leaveOpen: true); + + while (true) + { + var i = reader.Read(); + + if (i < 0) + { + break; + } + + writer.Write((char)i); + } + } + + private static void ToJson(Stream stream, Action serialize) + { + using var serializer = new LossySerializer(stream); + serialize(serializer); + } + + private sealed class LossySerializer(Stream stream) : IDisposable + { + private readonly Utf8JsonWriter writer = new(stream, new() { Indented = true, SkipValidation = true }); + + public void ToJson(INbtTag tag) + { + switch (tag) + { + case INbtArray arrayTag: + ToJson(arrayTag); + break; + + case INbtValue valueTag: + ToJson(valueTag); + break; + + case INbtList listTag: + ToJson(listTag); + break; + + case INbtCompound compoundTag: + ToJson(compoundTag); + break; + } + } + + public void ToJson(INbtList tag) + { + writer.WriteStartArray(); + + foreach (var entry in tag) + { + ToJson(entry); + } + + writer.WriteEndArray(); + } + + public void ToJson(INbtCompound tag) + { + writer.WriteStartObject(); + + foreach (var entry in tag as IEnumerable) + { + writer.WritePropertyName(entry.Name); + ToJson(entry.Tag); + } + + writer.WriteEndObject(); + } + + public void ToJson(INbtArray tag) + { + switch (tag) + { + case INbtArray byteArrayTag: + ToJson(byteArrayTag); + break; + + case INbtArray intArrayTag: + ToJson(intArrayTag); + break; + + case INbtArray longArrayTag: + ToJson(longArrayTag); + break; + } + } + + public void ToJson(INbtValue valueTag) + { + switch (valueTag) + { + case INbtValue tag: + ToJson(tag); + break; + + case INbtValue tag: + ToJson(tag); + break; + + case INbtValue tag: + ToJson(tag); + break; + + case INbtValue tag: + ToJson(tag); + break; + + case INbtValue tag: + ToJson(tag); + break; + + case INbtValue tag: + ToJson(tag); + break; + + case INbtValue tag: + ToJson(tag); + break; + } + } + + public void ToJson(INbtArray tag) => ToJson(tag, ToJson); + public void ToJson(INbtArray tag) => ToJson(tag, ToJson); + public void ToJson(INbtArray tag) => ToJson(tag, ToJson); + + public void ToJson(INbtValue tag) => ToJson(tag, ToJson); + public void ToJson(INbtValue tag) => ToJson(tag, ToJson); + public void ToJson(INbtValue tag) => ToJson(tag, ToJson); + public void ToJson(INbtValue tag) => ToJson(tag, ToJson); + public void ToJson(INbtValue tag) => ToJson(tag, ToJson); + public void ToJson(INbtValue tag) => ToJson(tag, ToJson); + public void ToJson(INbtValue tag) => ToJson(tag, ToJson); + + private void ToJson(INbtArray tag, Action writeValue) where T : notnull + { + writer.WriteStartArray(); + + foreach (var e in tag) + { + writeValue(e); + } + + writer.WriteEndArray(); + } + + private static void ToJson(INbtValue tag, Action writeValue) where T : notnull => writeValue(tag.Value); + + private void ToJson(sbyte value) => writer.WriteNumberValue(value); + private void ToJson(short value) => writer.WriteNumberValue(value); + private void ToJson(int value) => writer.WriteNumberValue(value); + private void ToJson(long value) => writer.WriteNumberValue(value); + private void ToJson(float value) => writer.WriteNumberValue(value); + private void ToJson(double value) => writer.WriteNumberValue(value); + private void ToJson(string value) => writer.WriteStringValue(value); + + public void Dispose() + { + writer.Dispose(); + + GC.SuppressFinalize(this); + } + } + + #endregion + + #region Deserialize + + public static INbtTag Deserialize(string s) + { + using var reader = new StringReader(s); + return Deserialize(reader); + } + + public static INbtTag Deserialize(Stream stream) + { + var doc = JsonDocument.Parse(stream); + return Deserialize(doc.RootElement); + } + + public static INbtTag Deserialize(TextReader reader) + { + using var stream = new MemoryStream(); + + using (var writer = new StreamWriter(stream, encoding: Encoding.UTF8, leaveOpen: true)) + { + while (true) + { + var i = reader.Read(); + + if (i < 0) + { + break; + } + + writer.Write((char)i); + } + } + + stream.Position = 0L; + + return Deserialize(stream); + } + + private static INbtTag Deserialize(JsonElement serialized) + { + var tagType = (NbtTagType)serialized.GetProperty(TypeProperty.Value).GetByte(); + var value = serialized.GetProperty(ValueProperty.Value); + + switch (tagType) + { + case NbtTagType.End: + throw new NbtException($"Invalid tag type: {Enum.GetName(tagType)}"); + + case NbtTagType.Byte: + return new NbtByte(value.GetSByte()); + case NbtTagType.Short: + return new NbtShort(value.GetInt16()); + case NbtTagType.Int: + return new NbtInt(value.GetInt32()); + case NbtTagType.Long: + return new NbtLong(value.GetInt64()); + case NbtTagType.Float: + return new NbtFloat(value.GetSingle()); + case NbtTagType.Double: + return new NbtDouble(value.GetDouble()); + case NbtTagType.String: + return new NbtString(value.GetString() ?? throw new NbtException("Null string value")); + + case NbtTagType.ByteArray: + { + var bytes = new sbyte[value.GetArrayLength()]; + var i = 0; + + foreach (var element in value.EnumerateArray()) + { + bytes[i++] = element.GetSByte(); + } + + return new NbtByteArray(bytes); + } + + case NbtTagType.IntArray: + { + var ints = new int[value.GetArrayLength()]; + var i = 0; + + foreach (var element in value.EnumerateArray()) + { + ints[i++] = element.GetInt32(); + } + + return new NbtIntArray(ints); + } + + case NbtTagType.LongArray: + { + var longs = new long[value.GetArrayLength()]; + var i = 0; + + foreach (var element in value.EnumerateArray()) + { + longs[i++] = element.GetInt64(); + } + + return new NbtLongArray(longs); + } + + case NbtTagType.List: + { + var list = new NbtList(); + + foreach (var element in value.EnumerateArray()) + { + list.Add(Deserialize(element)); + } + + return list; + } + + case NbtTagType.Compound: + { + var compound = new NbtCompound(); + + foreach (var property in value.EnumerateObject()) + { + compound.Add(property.Name, Deserialize(property.Value)); + } + + return compound; + } + + default: + throw new UnknownTagTypeException(tagType); + } + } + + #endregion +} \ No newline at end of file diff --git a/Nbt/Serialization/NbtReader.cs b/Nbt/Serialization/NbtReader.cs new file mode 100755 index 0000000..fb215af --- /dev/null +++ b/Nbt/Serialization/NbtReader.cs @@ -0,0 +1,249 @@ +using System.Buffers; +using System.Text; +using Nbt.Tag; + +namespace Nbt.Serialization; + +public interface INbtReader : IDisposable +{ + INbtTag ReadTag(); + + NamedTag ReadNamedTag(); + + NbtTagType ReadTagType(); + + sbyte ReadSByte(); + + short ReadShort(); + + int ReadInt(); + + long ReadLong(); + + float ReadFloat(); + + double ReadDouble(); + + string ReadString(); +} + +public sealed class NbtReader(Stream stream, bool leaveOpen = false) : INbtReader +{ + private readonly Stream stream = stream; + private readonly bool leaveOpen = leaveOpen; + + public Stream BaseStream => stream; + + public static NbtReader Create(Stream stream, Compression compression = Compression.Auto, bool leaveOpen = false) => compression switch + { + Compression.Auto => CreateInflater(stream, leaveOpen), + Compression.GZip => new(Utils.CreateGZipInflater(stream, leaveOpen), false), + Compression.ZLib => new(Utils.CreateZLibInflater(stream, leaveOpen), false), + Compression.LZ4 => new(Utils.CreateLZ4Deflater(stream, leaveOpen), false), + _ => new(stream, leaveOpen) + }; + + private static NbtReader CreateInflater(Stream stream, bool leaveOpen) + { + var cmf = stream.ReadByte(); + + if (cmf < 0) + { + throw new EndOfStreamException(); + } + + stream.Seek(-1, SeekOrigin.Current); + + return cmf switch + { + 0x1F => new(Utils.CreateGZipInflater(stream, leaveOpen), false), + 0x78 => new(Utils.CreateZLibInflater(stream, leaveOpen), false), + _ => throw new UnknownCompressionSchemeException(cmf) + }; + } + + private int ReadBytes(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count); + + private int ReadBytes(byte[] buffer) => ReadBytes(buffer, 0, buffer.Length); + + private int ReadBytes(Span buffer) => stream.Read(buffer); + + private void ReadAllBytes(byte[] buffer, int offset, int count) + { + if (count == 0) + { + return; + } + + var bytesLeft = count; + + do + { + var bytesRead = ReadBytes(buffer, offset + count - bytesLeft, bytesLeft); + + if (bytesRead == 0) + { + throw new EndOfStreamException(); + } + + bytesLeft -= bytesRead; + } while (bytesLeft > 0); + } + + private void ReadAllBytes(byte[] buffer) => ReadAllBytes(buffer, 0, buffer.Length); + + private void ReadAllBytes(Span buffer) + { + var count = buffer.Length; + + if (count == 0) + { + return; + } + + var bytesLeft = count; + + do + { + var bytesRead = ReadBytes(buffer.Slice(count - bytesLeft, bytesLeft)); + + if (bytesRead == 0) + { + throw new EndOfStreamException(); + } + + bytesLeft -= bytesRead; + } while (bytesLeft > 0); + } + + private int Read() => stream.ReadByte(); + + private int ReadAndThrowIfEndOfStream() + { + var b = Read(); + return b < 0 ? throw new EndOfStreamException() : b; + } + + private void ReadEndian(Span buffer) + { + ReadAllBytes(buffer); + + if (BitConverter.IsLittleEndian) + { + buffer.Reverse(); + } + } + + private byte ReadByte() => (byte)ReadAndThrowIfEndOfStream(); + + public sbyte ReadSByte() => (sbyte)ReadAndThrowIfEndOfStream(); + + private ushort ReadUShort() + { + Span buffer = stackalloc byte[sizeof(ushort)]; + ReadEndian(buffer); + return BitConverter.ToUInt16(buffer); + } + + public short ReadShort() + { + Span buffer = stackalloc byte[sizeof(short)]; + ReadEndian(buffer); + return BitConverter.ToInt16(buffer); + } + + public int ReadInt() + { + Span buffer = stackalloc byte[sizeof(int)]; + ReadEndian(buffer); + return BitConverter.ToInt32(buffer); + } + + public long ReadLong() + { + Span buffer = stackalloc byte[sizeof(long)]; + ReadEndian(buffer); + return BitConverter.ToInt64(buffer); + } + + public float ReadFloat() + { + Span buffer = stackalloc byte[sizeof(float)]; + ReadEndian(buffer); + return BitConverter.ToSingle(buffer); + } + + public double ReadDouble() + { + Span buffer = stackalloc byte[sizeof(double)]; + ReadEndian(buffer); + return BitConverter.ToDouble(buffer); + } + + public string ReadString() + { + var len = ReadUShort(); + + if (len == 0) + { + return string.Empty; + } + + var buffer = ArrayPool.Shared.Rent(len); + + try + { + ReadAllBytes(buffer, 0, len); + return Encoding.UTF8.GetString(buffer, 0, len); + } + finally + { + Array.Fill(buffer, (byte)0, 0, len); + ArrayPool.Shared.Return(buffer); + } + } + + public NbtTagType ReadTagType() => (NbtTagType)ReadByte(); + + public INbtTag ReadTag() + { + var tagType = NbtUtils.GetTagType(ReadTagType()); + return tagType.Read(this); + } + + public NamedTag ReadNamedTag() + { + var tagType = NbtUtils.GetTagType(ReadTagType()); + var tagName = ReadString(); + var tag = tagType.Read(this); + return new(tagName, tag); + } + + #region IDisposable + + private bool _disposedValue; + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + if (!leaveOpen) + { + stream.Dispose(); + } + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion +} diff --git a/Nbt/Serialization/NbtWriter.cs b/Nbt/Serialization/NbtWriter.cs new file mode 100755 index 0000000..bda72e6 --- /dev/null +++ b/Nbt/Serialization/NbtWriter.cs @@ -0,0 +1,185 @@ +using System.Text; +using Nbt.Tag; + +namespace Nbt.Serialization; + +public interface INbtWriter : IDisposable +{ + void WriteNamedTag(NamedTag namedTag); + + void WriteTag(INbtTag tag); + + void WriteTagType(NbtTagType tagType); + + void WriteSByte(sbyte b); + + void WriteShort(short s); + + void WriteInt(int i); + + void WriteLong(long l); + + void WriteFloat(float f); + + void WriteDouble(double d); + + void WriteString(string s); +} + +public sealed class NbtWriter(Stream stream, bool leaveOpen = false) : INbtWriter +{ + private readonly Stream stream = stream; + private readonly bool leaveOpen = leaveOpen; + + public Stream BaseStream => stream; + + public static NbtWriter Create(Stream stream, Compression compression = Compression.None, bool leaveOpen = false) => compression switch + { + Compression.GZip => new(Utils.CreateGZipDeflater(stream, leaveOpen), false), + Compression.ZLib => new(Utils.CreateZLibDeflater(stream, leaveOpen), false), + Compression.LZ4 => new(Utils.CreateLZ4Deflater(stream, leaveOpen), false), + Compression.Auto => throw new ArgumentException($"{nameof(Compression.Auto)} is invalid for a writer", nameof(compression)), + _ => new(stream, leaveOpen) + }; + + public void Flush() => stream.Flush(); + + public void WriteBytes(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count); + + public void WriteBytes(byte[] buffer) => stream.Write(buffer, 0, buffer.Length); + + public void WriteBytes(ReadOnlySpan buffer) => stream.Write(buffer); + + public void WriteByte(byte b) => stream.WriteByte(b); + + private void WriteEndian(Span buffer) + { + if (BitConverter.IsLittleEndian) + { + for (var i = buffer.Length - 1; i >= 0; i--) + { + WriteByte(buffer[i]); + } + } + else + { + WriteBytes(buffer); + } + } + + public void WriteSByte(sbyte b) + { + WriteByte((byte)b); + } + + public void WriteUShort(ushort s) + { + Span buffer = stackalloc byte[sizeof(ushort)]; + BitConverter.TryWriteBytes(buffer, s); + WriteEndian(buffer); + } + + public void WriteShort(short s) + { + Span buffer = stackalloc byte[sizeof(short)]; + BitConverter.TryWriteBytes(buffer, s); + WriteEndian(buffer); + } + + public void WriteUInt(uint i) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BitConverter.TryWriteBytes(buffer, i); + WriteEndian(buffer); + } + + public void WriteInt(int i) + { + Span buffer = stackalloc byte[sizeof(int)]; + BitConverter.TryWriteBytes(buffer, i); + WriteEndian(buffer); + } + + public void WriteULong(ulong l) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + BitConverter.TryWriteBytes(buffer, l); + WriteEndian(buffer); + } + + public void WriteLong(long l) + { + Span buffer = stackalloc byte[sizeof(long)]; + BitConverter.TryWriteBytes(buffer, l); + WriteEndian(buffer); + } + + public void WriteFloat(float f) + { + Span buffer = stackalloc byte[sizeof(float)]; + BitConverter.TryWriteBytes(buffer, f); + WriteEndian(buffer); + } + + public void WriteDouble(double d) + { + Span buffer = stackalloc byte[sizeof(double)]; + BitConverter.TryWriteBytes(buffer, d); + WriteEndian(buffer); + } + + public void WriteString(string s) + { + WriteUShort((ushort)s.Length); + + var buffer = Encoding.UTF8.GetBytes(s); + WriteBytes(buffer); + } + + public void WriteNamedTag(NamedTag namedTag) => WriteNamedTag(namedTag.Name, namedTag.Tag); + + public void WriteNamedTag(string tagName, INbtTag tag) + { + var tagType = NbtUtils.GetTagType(tag.Type); + WriteTagType(tag.Type); + WriteString(tagName); + tagType.Write(this, tag); + } + + public void WriteTag(INbtTag tag) + { + var tagType = NbtUtils.GetTagType(tag.Type); + WriteTagType(tag.Type); + tagType.Write(this, tag); + } + + public void WriteTagType(NbtTagType tagType) => WriteByte((byte)tagType); + + #region IDisposable + + private bool _disposedValue; + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + if (!leaveOpen) + { + stream.Dispose(); + } + } + + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion +} \ No newline at end of file diff --git a/Nbt/Serialization/SNbt.cs b/Nbt/Serialization/SNbt.cs new file mode 100755 index 0000000..9e604b8 --- /dev/null +++ b/Nbt/Serialization/SNbt.cs @@ -0,0 +1,401 @@ +namespace Nbt.Serialization; + +using System.Globalization; +using Nbt.Tag; + +public static class SNbt +{ + #region Serialize + + public enum SerializationStyle + { + Compact, Spaced, Indented + } + + public class SerializerOptions + { + public static readonly SerializerOptions Default = new(); + + public SerializationStyle Style { get; init; } = SerializationStyle.Compact; + public bool AlwaysQuoteTagNames { get; init; } = false; + // public bool AlwaysQuoteStringTags { get; init; } = false; + } + + public static string Serialize(INbtTag tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(NamedTag tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtList tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtCompound tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtArray tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + public static string Serialize(INbtValue tag, SerializerOptions? options = null) => Serialize(options, serializer => serializer.Serialize(tag)); + + public static void Serialize(TextWriter writer, INbtTag tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, NamedTag tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtList tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtCompound tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtArray tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + public static void Serialize(TextWriter writer, INbtValue tag, SerializerOptions? options = null) => Serialize(writer, options, serializer => serializer.Serialize(tag)); + + public static void Serialize(Stream stream, INbtTag tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, NamedTag tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtList tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtCompound tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtArray tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + public static void Serialize(Stream stream, INbtValue tag, SerializerOptions? options = null) => Serialize(stream, options, serializer => serializer.Serialize(tag)); + + private static void Serialize(TextWriter writer, SerializerOptions? options, Action serialize) + { + var serializer = new Serializer(writer, options); + serialize(serializer); + } + + private static void Serialize(Stream stream, SerializerOptions? options, Action serialize) + { + using var writer = new StreamWriter(stream, leaveOpen: true); + Serialize(writer, options, serialize); + } + + private static string Serialize(SerializerOptions? options, Action serialize) + { + using var writer = new StringWriter(); + Serialize(writer, options, serialize); + return writer.ToString(); + } + + private sealed class Serializer(TextWriter writer, SerializerOptions? options) + { + private readonly TextWriter writer = writer; + private readonly SerializerOptions options = options ?? SerializerOptions.Default; + private uint depth = 0u; + + private void WriteIndent() + { + for (var i = 0u; i < depth; i++) + { + writer.Write(" "); + } + } + + public void Serialize(INbtTag tag) + { + switch (tag) + { + case INbtArray arrayTag: + Serialize(arrayTag); + break; + + case INbtValue valueTag: + Serialize(valueTag); + break; + + case INbtList listTag: + Serialize(listTag); + break; + + case INbtCompound compoundTag: + Serialize(compoundTag); + break; + } + } + + public void Serialize(NamedTag namedTag) + { + if (!string.IsNullOrEmpty(namedTag.Name)) + { + writer.Write(NbtUtils.QuoteString(namedTag.Name, onlyIfNeeded: !options.AlwaysQuoteTagNames)); + writer.Write(':'); + if (options.Style is not SerializationStyle.Compact) + { + writer.Write(' '); + } + } + + Serialize(namedTag.Tag); + } + + public void Serialize(INbtList tag) + { + writer.Write('['); + + depth++; + + var style = options.Style; + var isSpaced = style is SerializationStyle.Spaced; + var isIndented = style is SerializationStyle.Indented; + + var b = false; + + foreach (var entry in tag) + { + if (b) + { + writer.Write(','); + if (isSpaced) + { + writer.Write(' '); + } + } + else + { + b = true; + } + + if (isIndented) + { + writer.WriteLine(); + WriteIndent(); + } + + Serialize(entry); + } + + depth--; + + if (b && isIndented) + { + writer.WriteLine(); + WriteIndent(); + } + + writer.Write(']'); + } + + public void Serialize(INbtCompound tag) + { + writer.Write('{'); + + depth++; + + var style = options.Style; + var isSpaced = style is SerializationStyle.Spaced; + var isIndented = style is SerializationStyle.Indented; + + var b = false; + + foreach (var entry in tag as IEnumerable) + { + if (b) + { + writer.Write(','); + if (isSpaced) + { + writer.Write(' '); + } + } + else + { + b = true; + } + + if (isIndented) + { + writer.WriteLine(); + WriteIndent(); + } + + Serialize(entry); + } + + depth--; + + if (b && isIndented) + { + writer.WriteLine(); + WriteIndent(); + } + + writer.Write('}'); + } + + public void Serialize(INbtArray tag) + { + switch (tag) + { + case INbtArray byteArrayTag: + Serialize(byteArrayTag); + break; + + case INbtArray intArrayTag: + Serialize(intArrayTag); + break; + + case INbtArray longArrayTag: + Serialize(longArrayTag); + break; + } + } + + public void Serialize(INbtValue valueTag) + { + switch (valueTag) + { + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + + case INbtValue tag: + Serialize(tag); + break; + } + } + + public void Serialize(INbtArray tag) => Serialize(tag, 'B', Serialize); + + public void Serialize(INbtArray tag) => Serialize(tag, 'I', Serialize); + + public void Serialize(INbtArray tag) => Serialize(tag, 'L', Serialize); + + public void Serialize(INbtValue tag) => Serialize(tag.Value); + + public void Serialize(INbtValue tag) => Serialize(tag.Value); + + public void Serialize(INbtValue tag) => Serialize(tag.Value); + + public void Serialize(INbtValue tag) => Serialize(tag.Value); + + public void Serialize(INbtValue tag) => Serialize(tag.Value); + + public void Serialize(INbtValue tag) => Serialize(tag.Value); + + public void Serialize(INbtValue tag) => Serialize(tag.Value); + + private void Serialize(INbtArray tag, char decorator, Action writeValue) where T : notnull + { + writer.Write('['); + writer.Write(decorator); + writer.Write(';'); + + if (tag.Value.Length > 0) + { + var isNotCompact = options.Style is not SerializationStyle.Compact; + + if (isNotCompact) + { + writer.Write(' '); + } + + var b = false; + + foreach (var e in tag) + { + if (b) + { + writer.Write(','); + if (isNotCompact) + { + writer.Write(' '); + } + } + else + { + b = true; + } + + writeValue(e); + } + } + + writer.Write(']'); + } + + private void Serialize(sbyte value) + { + writer.Write(value.ToString(CultureInfo.InvariantCulture)); + writer.Write('b'); + } + + private void Serialize(short value) + { + writer.Write(value.ToString(CultureInfo.InvariantCulture)); + writer.Write('s'); + } + + private void Serialize(int value) + { + writer.Write(value.ToString(CultureInfo.InvariantCulture)); + } + + private void Serialize(long value) + { + writer.Write(value.ToString(CultureInfo.InvariantCulture)); + writer.Write('L'); + } + + private void Serialize(float value) + { + writer.Write(value.ToString(CultureInfo.InvariantCulture)); + + if (float.IsFinite(value)) + { + writer.Write('F'); + } + } + + private void Serialize(double value) + { + writer.Write(value.ToString(CultureInfo.InvariantCulture)); + + if (double.IsFinite(value)) + { + writer.Write('D'); + } + } + + private void Serialize(string value) + { + // Utils.Quote(writer, value, NbtUtils.DOUBLE_QUOTE_CHAR, NbtUtils.ESCAPE_CHAR); + writer.Write(NbtUtils.QuoteString(value, onlyIfNeeded: false)); + } + } + + #endregion +} diff --git a/Nbt/Tag/Array/NbtByteArray.cs b/Nbt/Tag/Array/NbtByteArray.cs new file mode 100755 index 0000000..97e9a07 --- /dev/null +++ b/Nbt/Tag/Array/NbtByteArray.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtByteArray(sbyte[] value) : NbtArray(value) +{ + public override NbtTagType Type => NbtTagType.ByteArray; + + protected override NbtByteArray NewInstance(sbyte[] value) => new(value); +} diff --git a/Nbt/Tag/Array/NbtIntArray.cs b/Nbt/Tag/Array/NbtIntArray.cs new file mode 100755 index 0000000..d68c009 --- /dev/null +++ b/Nbt/Tag/Array/NbtIntArray.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtIntArray(int[] value) : NbtArray(value) +{ + public override NbtTagType Type => NbtTagType.IntArray; + + protected override NbtIntArray NewInstance(int[] value) => new(value); +} diff --git a/Nbt/Tag/Array/NbtLongArray.cs b/Nbt/Tag/Array/NbtLongArray.cs new file mode 100755 index 0000000..27a00ee --- /dev/null +++ b/Nbt/Tag/Array/NbtLongArray.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtLongArray(long[] value) : NbtArray(value) +{ + public override NbtTagType Type => NbtTagType.LongArray; + + protected override NbtLongArray NewInstance(long[] value) => new(value); +} diff --git a/Nbt/Tag/NamedTag.cs b/Nbt/Tag/NamedTag.cs new file mode 100755 index 0000000..f504761 --- /dev/null +++ b/Nbt/Tag/NamedTag.cs @@ -0,0 +1,3 @@ +namespace Nbt.Tag; + +public readonly record struct NamedTag(string Name, INbtTag Tag) { } diff --git a/Nbt/Tag/NbtArray.cs b/Nbt/Tag/NbtArray.cs new file mode 100755 index 0000000..ff4603a --- /dev/null +++ b/Nbt/Tag/NbtArray.cs @@ -0,0 +1,28 @@ +using System.Collections; + +namespace Nbt.Tag; + +public interface INbtArray : INbtValue, IEnumerable +{ + new Array Value { get; set; } +} + +public interface INbtArray : INbtArray, INbtValue, IEnumerable where T : notnull +{ + new T[] Value { get; set; } +} + +public abstract class NbtArray(T[] value) : NbtValue(value), INbtArray where T : notnull +{ + Array INbtArray.Value + { + get => Value; + set => Value = (T[])value; + } + + protected override T[] CopyValue() => (T[])value.Clone(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Value).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => Value.GetEnumerator(); +} diff --git a/Nbt/Tag/NbtCompound.cs b/Nbt/Tag/NbtCompound.cs new file mode 100755 index 0000000..c36f6e8 --- /dev/null +++ b/Nbt/Tag/NbtCompound.cs @@ -0,0 +1,117 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace Nbt.Tag; + +public interface INbtCompound : INbtTag, IEnumerable +{ + int Count { get; } + + new INbtTag this[string tagName] { get; set; } + + void Add(NamedTag namedTag); + void Add(string tagName, INbtTag tag); + + void Set(NamedTag namedTag); + void Set(string tagName, INbtTag tag); + + INbtTag Get(string tagName); + T Get(string tagName) where T : INbtTag; + + bool TryGet(string tagName, [MaybeNullWhen(false)] out INbtTag tag); + bool TryGet(string tagName, [MaybeNullWhen(false)] out T tag) where T : INbtTag; + + bool ContainsKey(string tagName); + bool Contains(NamedTag namedTag); + bool Contains(string tagName, INbtTag tag); + + bool Remove(string tagName); + bool Remove(NamedTag namedTag); + bool Remove(string tagName, INbtTag tag); + bool Remove(string tagName, [MaybeNullWhen(false)] out INbtTag tag); + + void Clear(); +} + +public class NbtCompound : INbtCompound +{ + private readonly Dictionary entries = []; + + private static string EnsureName(string tagName) => tagName ?? throw new ArgumentNullException(nameof(tagName)); + + private static INbtTag EnsureValue(INbtTag tag) => tag ?? throw new ArgumentNullException(nameof(tag)); + + public NbtTagType Type => NbtTagType.Compound; + + INbtCompound INbtTag.AsCompound() => this; + + INbtTag INbtTag.Copy() => Copy(); + public NbtCompound Copy() + { + var copy = new NbtCompound(); + + foreach (var e in entries) + { + copy.entries[e.Key] = e.Value.Copy(); + } + + return copy; + } + + public int Count => entries.Count; + + public INbtTag this[string tagName] + { + get => entries[tagName]; + set => entries[EnsureName(tagName)] = EnsureValue(value); + } + + INbtTag INbtTag.this[INbtPathElement tagPath] + { + get => tagPath is NbtName tagName ? this[tagName] : throw new UnsupportedPathElementException(); + set => this[tagPath is NbtName tagName ? tagName : throw new UnsupportedPathElementException()] = value; + } + + public void Add(NamedTag namedTag) => Add(namedTag.Name, namedTag.Tag); + public void Add(string tagName, INbtTag tag) => entries.Add(EnsureName(tagName), EnsureValue(tag)); + + public void Set(NamedTag namedTag) => Set(namedTag.Name, namedTag.Tag); + public void Set(string tagName, INbtTag tag) => this[tagName] = tag; + + public INbtTag Get(string tagName) => this[tagName]; + public T Get(string tagName) where T : INbtTag => (T)Get(tagName); + + public bool TryGet(string tagName, [MaybeNullWhen(false)] out INbtTag tag) => entries.TryGetValue(tagName, out tag); + public bool TryGet(string tagName, [MaybeNullWhen(false)] out T tag) where T : INbtTag + { + if (TryGet(tagName, out var t)) + { + tag = (T)t; + return true; + } + + tag = default; + return false; + } + + public bool ContainsKey(string tagName) => entries.ContainsKey(tagName); + public bool Contains(NamedTag namedTag) => Contains(namedTag.Name, namedTag.Tag); + public bool Contains(string tagName, INbtTag tag) => entries.Contains(KeyValuePair.Create(tagName, tag)); + + public bool Remove(string tagName) => entries.Remove(tagName); + public bool Remove(NamedTag namedTag) => Remove(namedTag.Name, namedTag.Tag); + public bool Remove(string tagName, INbtTag tag) => ((IDictionary)entries).Remove(KeyValuePair.Create(tagName, tag)); + public bool Remove(string tagName, [MaybeNullWhen(false)] out INbtTag tag) => entries.Remove(tagName, out tag); + + public void Clear() => entries.Clear(); + + public bool Equals(INbtTag? other) => ReferenceEquals(other, this) || other is NbtCompound o && Utils.DictionaryEquals(entries, o.entries); + + public override bool Equals(object? obj) => ReferenceEquals(obj, this) || obj is INbtCompound other && Equals(other); + + public override int GetHashCode() => entries.GetHashCode(); + + public IEnumerator GetEnumerator() => entries.Select(e => new NamedTag(e.Key, e.Value)).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)entries).GetEnumerator(); +} diff --git a/Nbt/Tag/NbtEnd.cs b/Nbt/Tag/NbtEnd.cs new file mode 100755 index 0000000..6e7c2ac --- /dev/null +++ b/Nbt/Tag/NbtEnd.cs @@ -0,0 +1,19 @@ +namespace Nbt.Tag; + +public sealed class NbtEnd : INbtTag +{ + public static readonly NbtEnd Value = new(); + + private NbtEnd() { } + + public NbtTagType Type => NbtTagType.End; + + INbtTag INbtTag.Copy() => this; + + public bool Equals(INbtTag? other) => this == other; + + public override bool Equals(object? obj) => this == obj; + + public override int GetHashCode() => 0; + +} \ No newline at end of file diff --git a/Nbt/Tag/NbtList.cs b/Nbt/Tag/NbtList.cs new file mode 100755 index 0000000..0e671cc --- /dev/null +++ b/Nbt/Tag/NbtList.cs @@ -0,0 +1,227 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using Nbt.Type; + +namespace Nbt.Tag; + +public interface INbtList : INbtTag, IEnumerable +{ + NbtTagType ElementType { get; } + + bool IsElementTypeLocked { get; } + + int Count { get; } + + new INbtTag this[int tagIndex] { get; set; } + + void Add(INbtTag tag); + void Insert(int tagIndex, INbtTag tag); + void Set(int tagIndex, INbtTag tag); + + INbtTag GetAt(int tagIndex); + T GetAt(int tagIndex) where T : INbtTag; + + bool TryGetAt(int tagIndex, [MaybeNullWhen(false)] out INbtTag tag); + bool TryGetAt(int tagIndex, [MaybeNullWhen(false)] out T tag) where T : INbtTag; + + int IndexOf(INbtTag tag); + bool Contains(INbtTag tag); + + INbtTag RemoveAt(int tagIndex); + bool Remove(INbtTag tag); + + void Clear(); +} + +public class NbtList : INbtList +{ + private readonly List entries = []; + + public NbtList() : this(NbtTagType.Unknown, false) { } + + public NbtList(NbtTagType elementType) : this(elementType, elementType is not NbtTagType.Unknown or NbtTagType.End) { } + + private NbtList(NbtTagType elementType, bool lockedType) + { + ElementType = elementType; + IsElementTypeLocked = lockedType; + } + + public NbtTagType Type => NbtTagType.List; + + INbtList INbtTag.AsList() => this; + + INbtTag INbtTag.Copy() => Copy(); + public NbtList Copy() + { + var copy = new NbtList(ElementType, IsElementTypeLocked); + + foreach (var e in entries) + { + copy.entries.Add(e.Copy()); + } + + return copy; + } + + private INbtTag EnsureType(INbtTag tag) + { + ArgumentNullException.ThrowIfNull(tag); + + if (tag.Type is NbtTagType.End) + { + throw new WrongTagTypeException($"The {nameof(NbtTagType.End)} tag cannot appear in a list"); + } + + if ((Count > 0 || IsElementTypeLocked) && tag.Type != ElementType) + { + throw new WrongTagTypeException(); + } + + return tag; + } + + private void OnItemAdded(INbtTag tag) + { + if (!IsElementTypeLocked && Count == 1) + { + ElementType = tag.Type; + } + } + + private void OnItemRemoved() + { + if (!IsElementTypeLocked && Count == 0) + { + ElementType = NbtTagType.Unknown; + } + } + + public NbtTagType ElementType { get; private set; } + + public bool IsElementTypeLocked { get; } + + public int Count => entries.Count; + + public INbtTag this[int tagIndex] + { + get => entries[tagIndex]; + set => entries[tagIndex] = EnsureType(value); + } + + INbtTag INbtTag.this[INbtPathElement tagPath] + { + get => tagPath is NbtIndex tagIndex ? this[tagIndex] : throw new UnsupportedPathElementException(); + set => this[tagPath is NbtIndex tagIndex ? tagIndex : throw new UnsupportedPathElementException()] = value; + } + + public void Add(INbtTag tag) + { + entries.Add(EnsureType(tag)); + OnItemAdded(tag); + } + + public void Insert(int index, INbtTag tag) + { + entries.Insert(index, EnsureType(tag)); + OnItemAdded(tag); + } + + public void Set(int tagIndex, INbtTag tag) => this[tagIndex] = tag; + + public INbtTag GetAt(int tagIndex) => this[tagIndex]; + + public T GetAt(int tagIndex) where T : INbtTag => (T)this[tagIndex]; + + public bool TryGetAt(int tagIndex, [MaybeNullWhen(false)] out INbtTag tag) + { + if (tagIndex >= 0 && tagIndex < Count) + { + tag = this[tagIndex]; + return true; + } + + tag = default; + return false; + } + + public bool TryGetAt(int tagIndex, [MaybeNullWhen(false)] out T tag) where T : INbtTag + { + if (TryGetAt(tagIndex, out var result)) + { + tag = (T)result; + return true; + } + + tag = default; + return false; + } + + public int IndexOf(INbtTag tag) => entries.IndexOf(tag); + + public bool Contains(INbtTag tag) => entries.Contains(tag); + + public INbtTag RemoveAt(int index) + { + var e = entries[index]; + + entries.RemoveAt(index); + OnItemRemoved(); + + return e; + } + + public bool Remove(INbtTag tag) + { + var b = entries.Remove(tag); + + if (b) + { + OnItemRemoved(); + } + + return b; + } + + public void Clear() + { + entries.Clear(); + OnItemRemoved(); + } + + public bool Equals(INbtTag? other) => ReferenceEquals(other, this) || other is NbtList o && Utils.EnumerableEquals(entries, o.entries); + + public override bool Equals(object? obj) => ReferenceEquals(obj, this) || obj is INbtList other && Equals(other); + + public override int GetHashCode() => entries.GetHashCode(); + + IEnumerator IEnumerable.GetEnumerator() => entries.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)entries).GetEnumerator(); + + public static INbtList FromValues(params T[] values) where T : notnull + { + var elementType = NbtUtils.EnsureValueType(); + var t = (NbtValueType)NbtUtils.GetTagType(elementType); + + var result = new NbtList(elementType); + + foreach (var value in values) + { + result.Add(t.CreateTag(value)); + } + + return result; + } + + public static INbtList FromTags(NbtTagType elementType, params T[] tags) where T : INbtTag + { + var result = new NbtList(elementType); + + foreach (var tag in tags) + { + result.Add(tag); + } + + return result; + } +} diff --git a/Nbt/Tag/NbtTag.cs b/Nbt/Tag/NbtTag.cs new file mode 100755 index 0000000..a70468a --- /dev/null +++ b/Nbt/Tag/NbtTag.cs @@ -0,0 +1,68 @@ +namespace Nbt.Tag; + +public interface INbtTag : IEquatable +{ + NbtTagType Type { get; } + + INbtTag this[string tagName] { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + INbtTag this[int tagIndex] { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + INbtTag this[INbtPathElement tagPath] { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + INbtTag this[INbtPath tagPath] + { + get + { + var count = tagPath.Count; + + if (count == 0) + { + return this; + } + + var pathElt = tagPath.PopFirst(out var pathRest); + var childTag = this[pathElt]; + + return childTag[pathRest]; + } + + set + { + var count = tagPath.Count; + + if (count == 0) + { + throw new ArgumentException("Path cannot be empty", nameof(value)); + } + + var pathElt = tagPath.PopFirst(out var pathRest); + + if (count == 1) + { + this[pathElt] = value; + } + else + { + var childTag = this[pathElt]; + childTag[pathRest] = value; + } + } + } + + INbtList AsList() => (INbtList)this; + + INbtCompound AsCompound() => (INbtCompound)this; + + INbtValue AsValue() => (INbtValue)this; + + INbtValue AsValue() where T : notnull => (INbtValue)this; + + INbtArray AsArray() => (INbtArray)this; + + INbtArray AsArray() where T : notnull => (INbtArray)this; + + T As() where T : INbtTag => (T)this; + + INbtTag Copy(); +} \ No newline at end of file diff --git a/Nbt/Tag/NbtValue.cs b/Nbt/Tag/NbtValue.cs new file mode 100755 index 0000000..6f5288c --- /dev/null +++ b/Nbt/Tag/NbtValue.cs @@ -0,0 +1,64 @@ +namespace Nbt.Tag; + +public interface INbtValue : INbtTag +{ + object Value { get; set; } +} + +public interface INbtValue : INbtValue where T : notnull +{ + new T Value { get; set; } +} + +public abstract class NbtValue : INbtValue where T : notnull +{ + private static readonly EqualityComparer ValueComparer = EqualityComparer.Default; + + protected T value; + + internal protected NbtValue(T value) : base() => this.value = value; + + public abstract NbtTagType Type { get; } + + INbtTag INbtTag.this[string tagName] { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + INbtTag INbtTag.this[int tagIndex] { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + INbtTag INbtTag.this[INbtPathElement tagPath] { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + INbtList INbtTag.AsList() => throw new NotSupportedException(); + + INbtCompound INbtTag.AsCompound() => throw new NotSupportedException(); + + INbtValue INbtTag.AsValue() => this; + + INbtValue INbtTag.AsValue() => (INbtValue)this; + + INbtArray INbtTag.AsArray() => (INbtArray)this; + + INbtArray INbtTag.AsArray() => (INbtArray)this; + + INbtTag INbtTag.Copy() => Copy(); + public NbtValue Copy() => NewInstance(CopyValue()); + + object INbtValue.Value + { + get => Value; + set => Value = (T)Convert.ChangeType(value, typeof(T)); + } + + public T Value + { + get => value; + set => this.value = value ?? throw new ArgumentNullException(nameof(value)); + } + + protected virtual T CopyValue() => value; + protected abstract NbtValue NewInstance(T value); + + public bool Equals(INbtTag? other) => ReferenceEquals(other, this) || other is NbtValue o && ValueComparer.Equals(value, o.value); + + public override bool Equals(object? obj) => ReferenceEquals(obj, this) || obj is NbtValue other && ValueComparer.Equals(value, other.value); + + public override int GetHashCode() => value.GetHashCode(); +} diff --git a/Nbt/Tag/Value/NbtByte.cs b/Nbt/Tag/Value/NbtByte.cs new file mode 100755 index 0000000..bd277d4 --- /dev/null +++ b/Nbt/Tag/Value/NbtByte.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtByte(sbyte value) : NbtValue(value) +{ + public override NbtTagType Type => NbtTagType.Byte; + + protected override NbtByte NewInstance(sbyte value) => new(value); +} diff --git a/Nbt/Tag/Value/NbtDouble.cs b/Nbt/Tag/Value/NbtDouble.cs new file mode 100755 index 0000000..33b406b --- /dev/null +++ b/Nbt/Tag/Value/NbtDouble.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtDouble(double value) : NbtValue(value) +{ + public override NbtTagType Type => NbtTagType.Double; + + protected override NbtDouble NewInstance(double value) => new(value); +} diff --git a/Nbt/Tag/Value/NbtFloat.cs b/Nbt/Tag/Value/NbtFloat.cs new file mode 100755 index 0000000..4ac6e5d --- /dev/null +++ b/Nbt/Tag/Value/NbtFloat.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtFloat(float value) : NbtValue(value) +{ + public override NbtTagType Type => NbtTagType.Float; + + protected override NbtFloat NewInstance(float value) => new(value); +} diff --git a/Nbt/Tag/Value/NbtInt.cs b/Nbt/Tag/Value/NbtInt.cs new file mode 100755 index 0000000..451cebe --- /dev/null +++ b/Nbt/Tag/Value/NbtInt.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtInt(int value) : NbtValue(value) +{ + public override NbtTagType Type => NbtTagType.Int; + + protected override NbtInt NewInstance(int value) => new(value); +} diff --git a/Nbt/Tag/Value/NbtLong.cs b/Nbt/Tag/Value/NbtLong.cs new file mode 100755 index 0000000..f22567c --- /dev/null +++ b/Nbt/Tag/Value/NbtLong.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtLong(long value) : NbtValue(value) +{ + public override NbtTagType Type => NbtTagType.Long; + + protected override NbtLong NewInstance(long value) => new(value); +} diff --git a/Nbt/Tag/Value/NbtShort.cs b/Nbt/Tag/Value/NbtShort.cs new file mode 100755 index 0000000..c547b61 --- /dev/null +++ b/Nbt/Tag/Value/NbtShort.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtShort(short value) : NbtValue(value) +{ + public override NbtTagType Type => NbtTagType.Short; + + protected override NbtShort NewInstance(short value) => new(value); +} diff --git a/Nbt/Tag/Value/NbtString.cs b/Nbt/Tag/Value/NbtString.cs new file mode 100755 index 0000000..d095fb3 --- /dev/null +++ b/Nbt/Tag/Value/NbtString.cs @@ -0,0 +1,8 @@ +namespace Nbt.Tag; + +public class NbtString(string value) : NbtValue(value) +{ + public override NbtTagType Type => NbtTagType.String; + + protected override NbtString NewInstance(string value) => new(value); +} diff --git a/Nbt/Type/Array/NbtByteArrayType.cs b/Nbt/Type/Array/NbtByteArrayType.cs new file mode 100755 index 0000000..45089f5 --- /dev/null +++ b/Nbt/Type/Array/NbtByteArrayType.cs @@ -0,0 +1,14 @@ +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtByteArrayType : NbtArrayType +{ + public static readonly NbtByteArrayType Value = new(); + + private NbtByteArrayType() : base(NbtByteType.Value) { } + + public override NbtTagType Type => NbtTagType.ByteArray; + + public override NbtByteArray CreateTag(sbyte[] values) => new(values); +} diff --git a/Nbt/Type/Array/NbtIntArrayType.cs b/Nbt/Type/Array/NbtIntArrayType.cs new file mode 100755 index 0000000..f31c005 --- /dev/null +++ b/Nbt/Type/Array/NbtIntArrayType.cs @@ -0,0 +1,14 @@ +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtIntArrayType : NbtArrayType +{ + public static readonly NbtIntArrayType Value = new(); + + private NbtIntArrayType() : base(NbtIntType.Value) { } + + public override NbtTagType Type => NbtTagType.IntArray; + + public override NbtIntArray CreateTag(int[] values) => new(values); +} diff --git a/Nbt/Type/Array/NbtLongArray.cs b/Nbt/Type/Array/NbtLongArray.cs new file mode 100755 index 0000000..97e4255 --- /dev/null +++ b/Nbt/Type/Array/NbtLongArray.cs @@ -0,0 +1,14 @@ +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtLongArrayType : NbtArrayType +{ + public static readonly NbtLongArrayType Value = new(); + + private NbtLongArrayType() : base(NbtLongType.Value) { } + + public override NbtTagType Type => NbtTagType.LongArray; + + public override NbtLongArray CreateTag(long[] values) => new(values); +} diff --git a/Nbt/Type/NbtArrayType.cs b/Nbt/Type/NbtArrayType.cs new file mode 100755 index 0000000..a2cdb26 --- /dev/null +++ b/Nbt/Type/NbtArrayType.cs @@ -0,0 +1,43 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal abstract class NbtArrayType(NbtValueType elementType) : NbtType> where T : notnull +{ + private readonly NbtValueType elementType = elementType; + + protected virtual T ReadValue(INbtReader reader) => elementType.ReadValue(reader); + public abstract INbtArray CreateTag(T[] values); + public virtual INbtArray CreateEmptyTag() => CreateTag([]); + protected virtual void WriteValue(INbtWriter writer, T value) => elementType.WriteValue(writer, value); + + public override INbtArray Read(INbtReader reader) + { + var length = reader.ReadInt(); + + if (length == 0) + { + return CreateEmptyTag(); + } + + var arr = new T[length]; + + for (var i = 0; i < length; i++) + { + arr[i] = ReadValue(reader); + } + + return CreateTag(arr); + } + + public override void Write(INbtWriter writer, INbtArray tag) + { + writer.WriteInt(tag.Value.Length); + + foreach (var e in tag) + { + WriteValue(writer, e); + } + } +} diff --git a/Nbt/Type/NbtCompoundType.cs b/Nbt/Type/NbtCompoundType.cs new file mode 100755 index 0000000..5ee6878 --- /dev/null +++ b/Nbt/Type/NbtCompoundType.cs @@ -0,0 +1,45 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtCompoundType : NbtType +{ + public static readonly NbtCompoundType Value = new(); + + private NbtCompoundType() { } + + public override NbtTagType Type => NbtTagType.Compound; + + public override INbtCompound Read(INbtReader reader) + { + var compound = new NbtCompound(); + + while (true) + { + var tagType = NbtUtils.GetTagType(reader.ReadTagType()); + + if (tagType == NbtEndType.Value) + { + break; + } + + var tagName = reader.ReadString(); + var tag = tagType.Read(reader); + + compound.Add(tagName, tag); + } + + return compound; + } + + public override void Write(INbtWriter writer, INbtCompound tag) + { + foreach (var entry in (IEnumerable)tag) + { + writer.WriteNamedTag(entry); + } + + writer.WriteTag(NbtEnd.Value); + } +} diff --git a/Nbt/Type/NbtEndType.cs b/Nbt/Type/NbtEndType.cs new file mode 100755 index 0000000..5f638fa --- /dev/null +++ b/Nbt/Type/NbtEndType.cs @@ -0,0 +1,17 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtEndType : NbtType +{ + public static readonly NbtEndType Value = new(); + + private NbtEndType() { } + + public override NbtTagType Type => NbtTagType.End; + + public override NbtEnd Read(INbtReader reader) => NbtEnd.Value; + + public override void Write(INbtWriter writer, NbtEnd tag) { } +} diff --git a/Nbt/Type/NbtListType.cs b/Nbt/Type/NbtListType.cs new file mode 100755 index 0000000..52b84aa --- /dev/null +++ b/Nbt/Type/NbtListType.cs @@ -0,0 +1,38 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtListType : NbtType +{ + public static readonly NbtListType Value = new(); + + private NbtListType() { } + + public override NbtTagType Type => NbtTagType.List; + + public override NbtList Read(INbtReader reader) + { + var tagType = NbtUtils.GetTagType(reader.ReadTagType()); + var length = reader.ReadInt(); + + var list = new NbtList(tagType.Type); + + for (var i = 0; i < length; i++) + { + var tag = tagType.Read(reader); + + list.Add(tag); + } + + return list; + } + + public override void Write(INbtWriter writer, INbtList tag) + { + foreach (var entry in tag) + { + writer.WriteTag(entry); + } + } +} diff --git a/Nbt/Type/NbtRegistry.cs b/Nbt/Type/NbtRegistry.cs new file mode 100755 index 0000000..fd02344 --- /dev/null +++ b/Nbt/Type/NbtRegistry.cs @@ -0,0 +1,98 @@ +using System.Diagnostics.CodeAnalysis; +using Nbt.Tag; + +namespace Nbt.Type; + +internal interface INbtRegistry +{ + int Count { get; } + + IEnumerable Entries { get; } + + bool Contains(NbtTagType type); + + bool Contains(INbtType type); + + INbtType Get(NbtTagType type); + + INbtType Get(NbtTagType type) where T : INbtTag; + + bool TryGet(NbtTagType type, [MaybeNullWhen(false)] out INbtType value); + + bool TryGet(NbtTagType type, [MaybeNullWhen(false)] out INbtType value) where T : INbtTag; + + void Clear(); + + void Add(INbtType t); + + void AddAll(params INbtType[] t); + + void AddRange(IEnumerable t); + + bool Remove(NbtTagType type); + + bool Remove(INbtType type); + + INbtType this[NbtTagType type] { get; set; } +} + +internal class NbtRegistry : INbtRegistry +{ + private readonly Dictionary _registry = []; + + public int Count => _registry.Count; + + public IEnumerable Entries => _registry.Values; + + public void Clear() => _registry.Clear(); + + public void Add(INbtType t) => _registry.Add(t.Type, t); + + public void AddAll(params INbtType[] ts) + { + foreach (var t in ts) + { + Add(t); + } + } + + public void AddRange(IEnumerable e) + { + foreach (var t in e) + { + Add(t); + } + } + + public bool Remove(NbtTagType type) => _registry.Remove(type); + + public bool Remove(INbtType type) => _registry.Remove(type.Type); + + public bool Contains(NbtTagType type) => _registry.ContainsKey(type); + + public bool Contains(INbtType type) => Contains(type.Type); + + public INbtType Get(NbtTagType type) => _registry[type]; + + public INbtType Get(NbtTagType type) where T : INbtTag => (INbtType)Get(type); + + public bool TryGet(NbtTagType type, [MaybeNullWhen(false)] out INbtType value) => _registry.TryGetValue(type, out value); + + public bool TryGet(NbtTagType type, [MaybeNullWhen(false)] out INbtType value) where T : INbtTag + { + if (_registry.TryGetValue(type, out var v)) + { + value = (INbtType)v; + return true; + } + + value = default; + return false; + } + + public INbtType this[NbtTagType type] + { + get => _registry[type]; + set => _registry[type] = value ?? throw new ArgumentNullException(nameof(value)); + } +} diff --git a/Nbt/Type/NbtType.cs b/Nbt/Type/NbtType.cs new file mode 100755 index 0000000..d141425 --- /dev/null +++ b/Nbt/Type/NbtType.cs @@ -0,0 +1,33 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal interface INbtType +{ + NbtTagType Type { get; } + + INbtTag Read(INbtReader reader); + + void Write(INbtWriter writer, INbtTag tag); +} + +internal interface INbtType : INbtType where T : INbtTag +{ + new T Read(INbtReader reader); + + void Write(INbtWriter writer, T tag); +} + +internal abstract class NbtType : INbtType where T : INbtTag +{ + public abstract NbtTagType Type { get; } + + public abstract T Read(INbtReader reader); + + public abstract void Write(INbtWriter writer, T tag); + + INbtTag INbtType.Read(INbtReader reader) => Read(reader); + + void INbtType.Write(INbtWriter writer, INbtTag tag) => Write(writer, (T)tag); +} \ No newline at end of file diff --git a/Nbt/Type/NbtValueType.cs b/Nbt/Type/NbtValueType.cs new file mode 100755 index 0000000..0d43bf9 --- /dev/null +++ b/Nbt/Type/NbtValueType.cs @@ -0,0 +1,15 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal abstract class NbtValueType : NbtType> where T : notnull +{ + protected internal abstract T ReadValue(INbtReader reader); + public abstract INbtValue CreateTag(T value); + protected internal abstract void WriteValue(INbtWriter writer, T value); + + public override INbtValue Read(INbtReader reader) => CreateTag(ReadValue(reader)); + + public override void Write(INbtWriter writer, INbtValue tag) => WriteValue(writer, tag.Value); +} diff --git a/Nbt/Type/Value/NbtByteType.cs b/Nbt/Type/Value/NbtByteType.cs new file mode 100755 index 0000000..6f530c5 --- /dev/null +++ b/Nbt/Type/Value/NbtByteType.cs @@ -0,0 +1,19 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtByteType : NbtValueType +{ + public static readonly NbtByteType Value = new(); + + private NbtByteType() { } + + public override NbtTagType Type => NbtTagType.Byte; + + protected internal override sbyte ReadValue(INbtReader reader) => reader.ReadSByte(); + + public override NbtByte CreateTag(sbyte value) => new(value); + + protected internal override void WriteValue(INbtWriter writer, sbyte value) => writer.WriteSByte(value); +} diff --git a/Nbt/Type/Value/NbtDoubleType.cs b/Nbt/Type/Value/NbtDoubleType.cs new file mode 100755 index 0000000..b58cbfc --- /dev/null +++ b/Nbt/Type/Value/NbtDoubleType.cs @@ -0,0 +1,19 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtDoubleType : NbtValueType +{ + public static readonly NbtDoubleType Value = new(); + + private NbtDoubleType() { } + + public override NbtTagType Type => NbtTagType.Double; + + protected internal override double ReadValue(INbtReader reader) => reader.ReadDouble(); + + public override NbtDouble CreateTag(double value) => new(value); + + protected internal override void WriteValue(INbtWriter writer, double value) => writer.WriteDouble(value); +} diff --git a/Nbt/Type/Value/NbtFloatType.cs b/Nbt/Type/Value/NbtFloatType.cs new file mode 100755 index 0000000..a90647d --- /dev/null +++ b/Nbt/Type/Value/NbtFloatType.cs @@ -0,0 +1,19 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtFloatType : NbtValueType +{ + public static readonly NbtFloatType Value = new(); + + private NbtFloatType() { } + + public override NbtTagType Type => NbtTagType.Float; + + protected internal override float ReadValue(INbtReader reader) => reader.ReadFloat(); + + public override NbtFloat CreateTag(float value) => new(value); + + protected internal override void WriteValue(INbtWriter writer, float value) => writer.WriteFloat(value); +} diff --git a/Nbt/Type/Value/NbtIntType.cs b/Nbt/Type/Value/NbtIntType.cs new file mode 100755 index 0000000..2f985c3 --- /dev/null +++ b/Nbt/Type/Value/NbtIntType.cs @@ -0,0 +1,19 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtIntType : NbtValueType +{ + public static readonly NbtIntType Value = new(); + + private NbtIntType() { } + + public override NbtTagType Type => NbtTagType.Int; + + protected internal override int ReadValue(INbtReader reader) => reader.ReadInt(); + + public override NbtInt CreateTag(int value) => new(value); + + protected internal override void WriteValue(INbtWriter writer, int value) => writer.WriteInt(value); +} diff --git a/Nbt/Type/Value/NbtLongType.cs b/Nbt/Type/Value/NbtLongType.cs new file mode 100755 index 0000000..ea9515c --- /dev/null +++ b/Nbt/Type/Value/NbtLongType.cs @@ -0,0 +1,19 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtLongType : NbtValueType +{ + public static readonly NbtLongType Value = new(); + + private NbtLongType() { } + + public override NbtTagType Type => NbtTagType.Long; + + protected internal override long ReadValue(INbtReader reader) => reader.ReadLong(); + + public override NbtLong CreateTag(long value) => new(value); + + protected internal override void WriteValue(INbtWriter writer, long value) => writer.WriteLong(value); +} diff --git a/Nbt/Type/Value/NbtShortType.cs b/Nbt/Type/Value/NbtShortType.cs new file mode 100755 index 0000000..ebd5bb4 --- /dev/null +++ b/Nbt/Type/Value/NbtShortType.cs @@ -0,0 +1,19 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtShortType : NbtValueType +{ + public static readonly NbtShortType Value = new(); + + private NbtShortType() { } + + public override NbtTagType Type => NbtTagType.Short; + + protected internal override short ReadValue(INbtReader reader) => reader.ReadShort(); + + public override NbtShort CreateTag(short value) => new(value); + + protected internal override void WriteValue(INbtWriter writer, short value) => writer.WriteShort(value); +} diff --git a/Nbt/Type/Value/NbtStringType.cs b/Nbt/Type/Value/NbtStringType.cs new file mode 100755 index 0000000..18409cb --- /dev/null +++ b/Nbt/Type/Value/NbtStringType.cs @@ -0,0 +1,19 @@ +using Nbt.Serialization; +using Nbt.Tag; + +namespace Nbt.Type; + +internal class NbtStringType : NbtValueType +{ + public static readonly NbtStringType Value = new(); + + private NbtStringType() { } + + public override NbtTagType Type => NbtTagType.String; + + protected internal override string ReadValue(INbtReader reader) => reader.ReadString(); + + public override NbtString CreateTag(string value) => new(value); + + protected internal override void WriteValue(INbtWriter writer, string value) => writer.WriteString(value); +} diff --git a/Nbt/Util/IndentedWriter.cs b/Nbt/Util/IndentedWriter.cs new file mode 100755 index 0000000..0902fd7 --- /dev/null +++ b/Nbt/Util/IndentedWriter.cs @@ -0,0 +1,91 @@ +using System.Text; + +namespace Nbt; + +public sealed class IndentedWriter(TextWriter baseWriter, string indent, bool leaveOpen = false) : TextWriter +{ + private readonly TextWriter baseWriter = baseWriter; + private readonly string indent = indent; + private readonly bool leaveOpen = leaveOpen; + private uint depth = 0u; + + public IndentedWriter(StringBuilder sb, string indent) : this(new StringWriter(sb), indent) { } + + public override Encoding Encoding => baseWriter.Encoding; + + public IndentationLevel NewIndentationLevel() => new(this); + + public override void Write(char value) => baseWriter.Write(value); + + public override void WriteLine() + { + baseWriter.WriteLine(); + + for (var i = 0u; i < depth; i++) + { + baseWriter.Write(indent); + } + } + + public override async Task WriteAsync(char value) => await baseWriter.WriteAsync(value); + + public override async Task WriteLineAsync() + { + await baseWriter.WriteLineAsync(); + + for (var i = 0u; i < depth; i++) + { + await baseWriter.WriteAsync(indent); + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (!leaveOpen) + { + baseWriter.Dispose(); + } + } + } + finally + { + base.Dispose(disposing); + } + } + + public override async ValueTask DisposeAsync() + { + try + { + if (!leaveOpen) + { + await baseWriter.DisposeAsync(); + } + } + finally + { + await base.DisposeAsync(); + } + } + + public readonly struct IndentationLevel : IDisposable + { + private readonly IndentedWriter writer; + + public IndentationLevel(IndentedWriter writer) + { + this.writer = writer; + + writer.depth++; + } + + public void Dispose() + { + writer.depth--; + } + } +} \ No newline at end of file diff --git a/Nbt/Util/QuotedWriter.cs b/Nbt/Util/QuotedWriter.cs new file mode 100755 index 0000000..0537d53 --- /dev/null +++ b/Nbt/Util/QuotedWriter.cs @@ -0,0 +1,128 @@ +using System.Text; + +namespace Nbt; + +public sealed class QuotedWriter(TextWriter baseWriter, char quoteChar, char escapeChar, bool leaveOpen = false) : TextWriter +{ + private readonly TextWriter baseWriter = baseWriter; + private readonly char quoteChar = quoteChar; + private readonly char escapeChar = escapeChar; + private readonly bool leaveOpen = leaveOpen; + private bool hasWrittenOneChar = false; + + public QuotedWriter(StringBuilder sb, char quoteChar, char escapeChar) : this(new StringWriter(sb), quoteChar, escapeChar) { } + + public override Encoding Encoding => baseWriter.Encoding; + + public override void Write(char value) + { + if (!hasWrittenOneChar) + { + baseWriter.Write(quoteChar); + hasWrittenOneChar = true; + } + + foreach (var c in Escape(value)) + { + baseWriter.Write(c); + } + } + + public override async Task WriteAsync(char value) + { + if (!hasWrittenOneChar) + { + await baseWriter.WriteAsync(quoteChar); + hasWrittenOneChar = true; + } + + foreach (var c in Escape(value)) + { + await baseWriter.WriteAsync(c); + } + } + + private IEnumerable Escape(char c) + { + var escape = false; + + switch (c) + { + case '\t': + escape = true; + c = 't'; + break; + + case '\r': + escape = true; + c = 'r'; + break; + + case '\n': + escape = true; + c = 'n'; + break; + + default: + if (c == quoteChar || c == escapeChar) + { + escape = true; + } + break; + } + + if (escape) + { + yield return escapeChar; + } + + yield return c; + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (!hasWrittenOneChar) + { + baseWriter.Write(quoteChar); + } + + baseWriter.Write(quoteChar); + + if (!leaveOpen) + { + baseWriter.Dispose(); + } + } + } + finally + { + base.Dispose(disposing); + } + } + + public override async ValueTask DisposeAsync() + { + try + { + if (!hasWrittenOneChar) + { + await baseWriter.WriteAsync(quoteChar); + } + + await baseWriter.WriteAsync(quoteChar); + + if (!leaveOpen) + { + await baseWriter.DisposeAsync(); + } + } + finally + { + await base.DisposeAsync(); + } + } +} \ No newline at end of file diff --git a/Nbt/Util/Utils.cs b/Nbt/Util/Utils.cs new file mode 100755 index 0000000..6531629 --- /dev/null +++ b/Nbt/Util/Utils.cs @@ -0,0 +1,251 @@ +using System.IO.Compression; +using System.Text; +using K4os.Compression.LZ4.Streams; + +namespace Nbt; + +internal static class Utils +{ + #region conversion stuff + + public static TTo Convert(this TFrom value, Converter converter) => value is TTo sameValue ? sameValue : converter(value); + + #endregion + + #region enumerable stuff + + public static T[] AsArray(this IEnumerable enumerable) => Convert(enumerable, Enumerable.ToArray); + + public static IList AsIList(this IEnumerable enumerable) => Convert, IList>(enumerable, Enumerable.ToList); + + public static List AsList(this IEnumerable enumerable) => Convert(enumerable, Enumerable.ToList); + + public static IEnumerable Insert(this IEnumerable e, int index, T value) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (e.TryGetNonEnumeratedCount(out var count)) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(index, count); + } + + var i = 0; + + foreach (var o in e) + { + if (i == index) + { + yield return value; + } + + yield return o; + + i++; + } + + if (i == index) + { + yield return value; + } + } + + public static IEnumerable Replace(this IEnumerable e, int index, T value) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (e.TryGetNonEnumeratedCount(out var count)) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, count); + } + + var i = 0; + + foreach (var o in e) + { + yield return i == index ? value : o; + + i++; + } + } + + public static IEnumerable Yield(this T value) + { + yield return value; + } + + #endregion + + #region streams, readers and writers + + public static GZipStream CreateGZipDeflater(Stream stream, bool leaveOpen) => new(stream, CompressionMode.Compress, leaveOpen); + public static ZLibStream CreateZLibDeflater(Stream stream, bool leaveOpen) => new(stream, CompressionMode.Compress, leaveOpen); + public static LZ4EncoderStream CreateLZ4Deflater(Stream stream, bool leaveOpen) => LZ4Stream.Encode(stream, leaveOpen: leaveOpen); + + public static GZipStream CreateGZipInflater(Stream stream, bool leaveOpen) => new(stream, CompressionMode.Decompress, leaveOpen); + public static ZLibStream CreateZLibInflater(Stream stream, bool leaveOpen) => new(stream, CompressionMode.Decompress, leaveOpen); + public static LZ4DecoderStream CreateLZ4Inflater(Stream stream, bool leaveOpen) => LZ4Stream.Decode(stream, leaveOpen: leaveOpen); + + public static char SkipWhiteSpaces(TextReader reader) + { + while (true) + { + var i = reader.Read(); + + if (i < 0) + { + throw new EndOfStreamException(); + // return i; + } + + var c = (char)i; + + if (char.IsWhiteSpace(c)) + { + continue; + } + + return c; + } + } + + #endregion + + #region string and char quotation and whatnot + + public static string Repeat(this string s, int c) => c switch + { + < 0 => throw new ArgumentException("Count must be positive.", nameof(c)), + 0 => string.Empty, + 1 => s, + _ => string.IsNullOrEmpty(s) ? s : string.Create(s.Length * c, (s, c), static (span, state) => + { + var (s, c) = state; + var n = s.Length; + + for (var i = 0; i < c; i++) + { + var ss = span.Slice(n * i, n); + s.CopyTo(ss); + } + }) + }; + + public static string Unquote(string s, char quoteChar, char escapeChar) + { + var length = s.Length; + + if (length < 2 || s[0] != quoteChar || s[^1] != quoteChar) + { + throw new ArgumentException("Invalid quoted string", nameof(s)); + } + + var sb = new StringBuilder(); + + var escape = false; + + for (var i = 1; i < length - 1; i++) + { + var c = s[i]; + + if (escape) + { + escape = false; + + switch (c) + { + case 't': + c = '\t'; + break; + case 'r': + c = '\r'; + break; + case 'n': + c = '\n'; + break; + } + } + else if (c == quoteChar) + { + throw new ArgumentException("Invalid quoted string", nameof(s)); + } + else if (c == escapeChar) + { + escape = true; + continue; + } + + sb.Append(c); + } + + return sb.ToString(); + } + + public static string Quote(string s, char quoteChar, char escapeChar) + { + using var writer = new StringWriter(); + + Quote(writer, s, quoteChar, escapeChar); + + return writer.ToString(); + } + + public static void Quote(TextWriter writer, string s, char quoteChar, char escapeChar) + { + if (s.Length == 0) + { + writer.Write(quoteChar); + writer.Write(quoteChar); + + return; + } + + using var quoter = new QuotedWriter(writer, quoteChar, escapeChar, true); + + quoter.Write(s); + } + + #endregion + + #region equalities and comparisons + + public static bool EnumerableEquals(IEnumerable a, IEnumerable b, IEqualityComparer? elementComparer = null) => ReferenceEquals(a, b) || a is not null && b is not null && a.SequenceEqual(b, elementComparer); + + public static bool DictionaryEquals(IDictionary a, IDictionary b, IEqualityComparer? valueComparer = null) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + if (a is null || b is null) + { + return false; + } + + var n = a.Count; + + if (n != b.Count) + { + return false; + } + + if (n is 0) + { + return true; + } + + valueComparer ??= EqualityComparer.Default; + + foreach (var entry in a) + { + if (!b.TryGetValue(entry.Key, out var value) || !valueComparer.Equals(entry.Value, value)) + { + return false; + } + } + + return true; + } + + #endregion +} diff --git a/Test/MapReader.cs b/Test/MapReader.cs new file mode 100755 index 0000000..3fd3974 --- /dev/null +++ b/Test/MapReader.cs @@ -0,0 +1,144 @@ +// using System.Drawing; +// using Nbt.Serialization; +// using Nbt.Tag; + +// namespace Nbt.Test; + +// public static class MapReader +// { +// private static readonly Color[] colorTable = [ +// Color.Transparent, +// Color.FromArgb(127, 178, 56), +// Color.FromArgb(247, 233, 163), +// Color.FromArgb(199, 199, 199), +// Color.FromArgb(255, 0, 0), +// Color.FromArgb(160, 160, 255), +// Color.FromArgb(167, 167, 167), +// Color.FromArgb(0, 124, 0), +// Color.FromArgb(255, 255, 255), +// Color.FromArgb(164, 168, 184), +// Color.FromArgb(151, 109, 77), +// Color.FromArgb(112, 112, 112), +// Color.FromArgb(64, 64, 255), +// Color.FromArgb(143, 119, 72), +// Color.FromArgb(255, 252, 245), +// Color.FromArgb(216, 127, 51), +// Color.FromArgb(178, 76, 216), +// Color.FromArgb(102, 153, 216), +// Color.FromArgb(229, 229, 51), +// Color.FromArgb(127, 204, 25), +// Color.FromArgb(242, 127, 165), +// Color.FromArgb(76, 76, 76), +// Color.FromArgb(153, 153, 153), +// Color.FromArgb(76, 127, 153), +// Color.FromArgb(127, 63, 178), +// Color.FromArgb(51, 76, 178), +// Color.FromArgb(102, 76, 51), +// Color.FromArgb(102, 127, 51), +// Color.FromArgb(153, 51, 51), +// Color.FromArgb(25, 25, 25), +// Color.FromArgb(250, 238, 77), +// Color.FromArgb(92, 219, 213), +// Color.FromArgb(74, 128, 255), +// Color.FromArgb(0, 217, 58), +// Color.FromArgb(129, 86, 49), +// Color.FromArgb(112, 2, 0), +// Color.FromArgb(209, 177, 161), +// Color.FromArgb(159, 82, 36), +// Color.FromArgb(149, 87, 108), +// Color.FromArgb(112, 108, 138), +// Color.FromArgb(186, 133, 36), +// Color.FromArgb(103, 117, 53), +// Color.FromArgb(160, 77, 78), +// Color.FromArgb(57, 41, 35), +// Color.FromArgb(135, 107, 98), +// Color.FromArgb(87, 92, 92), +// Color.FromArgb(122, 73, 88), +// Color.FromArgb(76, 62, 92), +// Color.FromArgb(76, 50, 35), +// Color.FromArgb(76, 82, 42), +// Color.FromArgb(142, 60, 46), +// Color.FromArgb(37, 22, 16), +// Color.FromArgb(189, 48, 49), +// Color.FromArgb(148, 63, 97), +// Color.FromArgb(92, 25, 29), +// Color.FromArgb(22, 126, 134), +// Color.FromArgb(58, 142, 140), +// Color.FromArgb(86, 44, 62), +// Color.FromArgb(20, 180, 133), +// Color.FromArgb(100, 100, 100), +// Color.FromArgb(216, 175, 147), +// Color.FromArgb(127, 167, 150) +// ]; + +// private static readonly double[] multiplierTable = [ +// 180.0 / 255.0, +// 220.0 / 255.0, +// 1.0, +// 135.0 / 255.0 +// ]; + +// public static void Main(string[] args) +// { +// const string inputDir = @"C:\Users\hb68\Desktop\a"; +// const string outputDir = @"C:\Users\hb68\Desktop\b"; +// const int width = 128, height = 128; +// const int pixelSize = 8; + +// Parallel.ForEach(Directory.EnumerateFiles(inputDir), static file => +// { +// INbtTag rootTag; +// using (var inputStream = File.OpenRead(file)) +// { +// var reader = NbtReader.Create(inputStream); + +// rootTag = reader.ReadNamedTag().Tag; +// } + +// var colors = rootTag["data"]["colors"].AsArray().Value; + +// using var image = new Bitmap(width * pixelSize, height * pixelSize); + +// var k = 0; +// for (var i = 0; i < height; i++) +// { +// for (var j = 0; j < width; j++) +// { +// var colorId = (byte)colors[k]; +// var baseColorId = colorId / 4; +// var colorOffset = colorId % 4; + +// var baseColor = colorTable[baseColorId]; +// var multiplier = multiplierTable[colorOffset]; + +// var color = Color.FromArgb(baseColor.A, (int)(baseColor.R * multiplier), (int)(baseColor.G * multiplier), (int)(baseColor.B * multiplier)); + +// if (pixelSize > 1) +// { +// var imgI = i * pixelSize; +// var imgJ = j * pixelSize; + +// for (var xOff = 0; xOff < pixelSize; xOff++) +// { +// for (var yOff = 0; yOff < pixelSize; yOff++) +// { +// image.SetPixel(imgJ + xOff, imgI + yOff, color); +// } +// } +// } +// else +// { +// image.SetPixel(j, i, color); +// } + +// k++; +// } +// } + +// var fileName = Path.GetFileName(file); +// image.Save(Path.Combine(outputDir, $"{fileName}.png"), System.Drawing.Imaging.ImageFormat.Png); + +// Console.WriteLine(fileName); +// }); +// } +// } \ No newline at end of file diff --git a/Test/Program.cs b/Test/Program.cs new file mode 100755 index 0000000..677bb1d --- /dev/null +++ b/Test/Program.cs @@ -0,0 +1,128 @@ +namespace Nbt.Test; + +using System.IO.Compression; +using K4os.Compression.LZ4.Streams; +using Nbt.Serialization; +using Nbt.Tag; + +public static class Program +{ + public static void Main(string[] args) + { + WorldFileReading(); + } + + private static void Test() + { + const string filePath = @"D:\Minecraft\game\profiles\test\saves\creatif\players\hbdu68.dat"; + + INbtTag rootTag; + + using (var inputStream = File.OpenRead(filePath)) + { + using var reader = NbtReader.Create(inputStream); + + rootTag = reader.ReadNamedTag().Tag; + } + + // SNbt.Serialize(Console.Out, rootTag, new() { Style = SNbt.SerializationStyle.Indented }); + var s = JsonNbt.Serialize(rootTag); + var t = JsonNbt.Deserialize(s); + + Console.WriteLine(rootTag.Equals(t)); + } + + private static void WorldFileReading() + { + const string inputFile = @"/home/hbecher/Téléchargements/sc-murder/region/r.-3.0.mca"; + + // chunk coordinates and last modified timestamps + // two 4kiB tables (1024 ints) + + var allOfTheShite = new NbtCompound(); + + Span buf4 = stackalloc byte[4]; + + using var inputStream = File.OpenRead(inputFile); + + for (var chunkX = 0; chunkX < 32; chunkX++) + { + for (var chunkZ = 0; chunkZ < 32; chunkZ++) + { + var chunkOffset = (chunkX + (chunkZ << 5)) << 2; + + inputStream.Position = chunkOffset; + + inputStream.Read(buf4); + buf4.Reverse(); + var rawLoc = BitConverter.ToUInt32(buf4); + var offset = (int)((rawLoc & ~0xFFu) << 4); + var size = (int)((rawLoc & 0xFFu) << 12); + + if (offset == 0 && size == 0) + { + continue; + } + + using (inputStream.Mark()) + { + inputStream.Position += 1024 * 4; + inputStream.Read(buf4); + } + + buf4.Reverse(); + var timestamp = BitConverter.ToInt32(buf4); + + using (inputStream.Mark()) + { + inputStream.Position = offset; + + inputStream.Read(buf4); + buf4.Reverse(); + + var length = BitConverter.ToInt32(buf4); + var compressionMode = inputStream.ReadByte(); + + Stream inflaterStream; + bool leaveOpen = false; + + switch (compressionMode) + { + case 1: + inflaterStream = new GZipStream(inputStream, CompressionMode.Decompress, true); + break; + + case 2: + inflaterStream = new ZLibStream(inputStream, CompressionMode.Decompress, true); + break; + + case 3: + inflaterStream = inputStream; + leaveOpen = true; + break; + + case 4: + inflaterStream = LZ4Stream.Decode(inputStream, leaveOpen: true); + break; + + default: + continue; + } + + INbtTag tag; + using (var nbtReader = new NbtReader(inflaterStream, leaveOpen)) + { + tag = nbtReader.ReadNamedTag().Tag; + } + + allOfTheShite[$"{chunkX}-{chunkZ}"] = tag; + } + } + } + + using var outputStream = File.CreateText(@"/home/hbecher/Téléchargements/a.snbt"); + + // SNbt.Serialize(outputStream, allOfTheShite, new() { Style = SNbt.SerializationStyle.Indented }); + JsonNbt.ToJson(outputStream, allOfTheShite); + } +} diff --git a/Test/StreamExtensions.cs b/Test/StreamExtensions.cs new file mode 100755 index 0000000..c606ad6 --- /dev/null +++ b/Test/StreamExtensions.cs @@ -0,0 +1,6 @@ +namespace Nbt.Test; + +public static class StreamExtensions +{ + public static StreamMark Mark(this Stream stream) => new(stream); +} diff --git a/Test/StreamMark.cs b/Test/StreamMark.cs new file mode 100755 index 0000000..95ed77b --- /dev/null +++ b/Test/StreamMark.cs @@ -0,0 +1,9 @@ +namespace Nbt.Test; + +public readonly struct StreamMark(Stream stream) : IDisposable +{ + private readonly Stream _stream = stream; + private readonly long _pos = stream.Position; + + public void Dispose() => _stream.Position = _pos; +} \ No newline at end of file diff --git a/Test/Test.csproj b/Test/Test.csproj new file mode 100755 index 0000000..5d06c5b --- /dev/null +++ b/Test/Test.csproj @@ -0,0 +1,15 @@ + + + Exe + net8.0 + enable + + enable + + + + + + + + \ No newline at end of file