From 84192c69db8d0d0ca80b5a1b7bd6081a2f1362b2 Mon Sep 17 00:00:00 2001 From: zontreck Date: Thu, 29 Aug 2024 06:55:12 -0700 Subject: [PATCH] Completely rework SNBT Parser (NOTE: ChatGPT Was used for regex only) --- bin/mkfsreport.dart | 1 - lib/consts.dart | 2 +- lib/nbt/SnbtIo.dart | 14 ++- lib/nbt/Stream.dart | 28 +++++- lib/nbt/Tag.dart | 191 +++++++++++++++++++------------------- lib/omv/types/list.dart | 1 - lib/omv/types/string.dart | 2 +- lib/utils/IOTools.dart | 1 - pubspec.yaml | 2 +- test/EL-ReducedList.dat | Bin 0 -> 2202 bytes test/nbt_test.dart | 15 +++ 11 files changed, 150 insertions(+), 107 deletions(-) create mode 100644 test/EL-ReducedList.dat diff --git a/bin/mkfsreport.dart b/bin/mkfsreport.dart index 2825c70..3d6ce49 100644 --- a/bin/mkfsreport.dart +++ b/bin/mkfsreport.dart @@ -4,7 +4,6 @@ This tool will generate a HTML report for the base path specified. */ -import 'dart:ffi'; import 'dart:io'; import 'package:libac_dart/argparse/Args.dart'; diff --git a/lib/consts.dart b/lib/consts.dart index 259f732..67df2da 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -1,3 +1,3 @@ class Constants { - static const VERSION = "1.2.082424+1243"; + static const VERSION = "1.2.082924+0654"; } diff --git a/lib/nbt/SnbtIo.dart b/lib/nbt/SnbtIo.dart index cb823a8..70f1461 100644 --- a/lib/nbt/SnbtIo.dart +++ b/lib/nbt/SnbtIo.dart @@ -5,7 +5,7 @@ import 'Tag.dart'; import 'impl/CompoundTag.dart'; class SnbtIo { - static void writeToFile(String file, CompoundTag tag) { + static Future writeToFile(String file, CompoundTag tag) async { File handle = File(file); if (handle.existsSync()) @@ -13,7 +13,7 @@ class SnbtIo { StringBuilder builder = StringBuilder(); Tag.writeStringifiedNamedTag(tag, builder, 0); - handle.writeAsString(builder.toString()); + await handle.writeAsString(builder.toString()); } static Future readFromFile(String file) async { @@ -21,7 +21,15 @@ class SnbtIo { String data = await fi.readAsString(); StringReader reader = StringReader(data); - return Tag.readStringifiedNamedTag(reader); + Tag tag = CompoundTag(); + try { + tag = Tag.readStringifiedNamedTag(reader); + } catch (E, stack) { + print("FATAL ERROR OCCURED AT LOCATION:\n${reader.getSnapshot()}"); + print(E); + print(stack); + } + return tag; } static String writeToString(CompoundTag tag) { diff --git a/lib/nbt/Stream.dart b/lib/nbt/Stream.dart index 41cdfcd..b6282b9 100644 --- a/lib/nbt/Stream.dart +++ b/lib/nbt/Stream.dart @@ -417,8 +417,12 @@ class StringReader { StringReader(this._buffer); - // Check if there's more to read - bool get canRead => _position < _buffer.length; + /// Check if there's more to read + bool get canRead => _canRead(); + bool _canRead() { + skipWhitespace(); + return _position < _buffer.length; + } // Get the number of chars seeked int get getSeeked => _lastPostion - _position; @@ -433,6 +437,18 @@ class StringReader { } } + /// Generates a snapshot of the text location if applicable + String getSnapshot() { + if (canRead) { + if (_position + 64 < _buffer.length) { + return _buffer.substring(_position, _position + 64); + } else { + return _buffer.substring(_position); + } + } else + return ""; + } + // Peek the next character without advancing the position String peek() { skipWhitespace(); @@ -446,7 +462,7 @@ class StringReader { // Skip any whitespace characters void skipWhitespace() { if (_quotedString) return; // We need whitespace for strings - while (canRead && isWhitespace(_buffer[_position])) { + while ((_position < _buffer.length) && isWhitespace(_buffer[_position])) { _position++; } } @@ -457,10 +473,12 @@ class StringReader { } // Read until a specific character is found - String readUntil(String stopChar) { + String readUntil(String stopChar, int maxChars) { StringBuffer result = StringBuffer(); - while (canRead && peek() != stopChar) { + int index = 0; + while (canRead && peek() != stopChar && index < maxChars) { result.write(next()); + index++; } return result.toString(); } diff --git a/lib/nbt/Tag.dart b/lib/nbt/Tag.dart index 379772d..61f5690 100644 --- a/lib/nbt/Tag.dart +++ b/lib/nbt/Tag.dart @@ -1,3 +1,5 @@ +import 'dart:core'; + import 'Stream.dart'; import 'impl/ByteArrayTag.dart'; import 'impl/ByteTag.dart'; @@ -42,109 +44,112 @@ enum TagType { } static const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - static TagType getStringifiedTagType(StringReader reader) { - reader.startSeek(); + reader.startSeek(); // Enter fake read mode TagType ret = TagType.End; - bool isNumber = true; - bool isAlpha = true; // Is digits only + bool isQuoted = false; + bool isNumeric = true; // Assume true until proven otherwise + bool isAlpha = true; // Assume true until proven otherwise + StringBuffer buffer = StringBuffer(); - // Start to determine the next tag type - while (reader.canRead && ret == TagType.End) { - var val = reader.next().toUpperCase(); - if (ALPHABET.indexOf(val) == -1) isAlpha = false; + while (reader.canRead) { + var val = + reader.peek(); // Peek at the next character without consuming it - switch (val) { - case "{": - { - ret = TagType.Compound; - break; - } - case "[": - { - // Check for a type Prefix - var X = reader.readUntil(";"); - switch (X.toUpperCase()) { - case "B": - { - ret = TagType.ByteArray; - break; - } - case "I": - { - ret = TagType.IntArray; - break; - } - case "L": - { - ret = TagType.LongArray; - break; - } - default: - { - ret = TagType.List; - break; - } - } - break; - } - case "B": - { - ret = TagType.Byte; - break; - } - case "D": - { - ret = TagType.Double; - break; - } - case "F": - { - ret = TagType.Float; - break; - } - case "I": - { - ret = TagType.Int; - break; - } - case "L": - { - ret = TagType.Long; - break; - } - case "S": - { - ret = TagType.Short; - break; - } - case "\"": - { - ret = TagType.String; - break; - } - case ",": - case "\n": - { - if (reader.getSeeked >= 1) ret = TagType.String; - if (isNumber) ret = TagType.Int; + if (val == ',' || val == '\n' || val == "]" || val == "}") { + break; // Stop at comma or newline + } + if (val == '"') { + reader.next(); // Consume the quote character + isQuoted = true; // Toggle quoted state + break; + } + + if (val == '{') { + ret = TagType.Compound; // Detected a CompoundTag + reader.next(); // Consume '{' + reader.endSeek(); // Restore the original stream position + return ret; + } + + if (val == '[') { + reader.next(); // Consume '[' + // Peek ahead to differentiate between List and Array + var prefix = reader.readUntil(";", 2).toUpperCase(); + + switch (prefix) { + case "B": + ret = TagType.ByteArray; break; - } - default: - { - if (!reader.isDigit(val)) { - if (isNumber) isNumber = false; - } + case "I": + ret = TagType.IntArray; break; - } + case "L": + ret = TagType.LongArray; + break; + default: + ret = TagType.List; // No type prefix, it's a List + break; + } + + reader.endSeek(); // Restore the original stream position + return ret; + } + + // Adjusting numeric and alphabetic checks + var current = reader.next(); // Consume the character + + buffer.write(current); + + // Check if current character is a digit or numeric suffix + if (!RegExp(r'^[0-9]$').hasMatch(current) && + !RegExp(r'^[sSbBiIlLfFdD]$').hasMatch(current)) { + isNumeric = false; // Not purely numeric with possible suffix + } + + // Check if current character is purely alphabetical + if (!RegExp(r'^[A-Za-z]$').hasMatch(current)) { + isAlpha = false; // Not purely alphabetical } } - reader.endSeek(); - if (!isNumber && isAlpha) ret = TagType.String; + var input = buffer.toString().trim(); + reader.endSeek(); // Restore the original stream position - return ret; + if (input.isEmpty) { + return TagType.String; // No input detected + } + + if (isQuoted) { + return TagType.String; // Quoted string + } + + if (isNumeric) { + // Check the last character for type indicator (only for numeric input) + var lastChar = input.substring(input.length - 1).toUpperCase(); + switch (lastChar) { + case 'S': + return TagType.Short; + case 'B': + return TagType.Byte; + case 'I': + return TagType.Int; + case 'F': + return TagType.Float; + case 'D': + return TagType.Double; + case 'L': + return TagType.Long; + default: + return TagType + .Int; // Default to Int if purely numeric with no specific suffix + } + } else if (isAlpha && !input.contains(' ')) { + return TagType.String; // Unquoted purely alphabetical string + } else { + return TagType.String; // Unquoted string with mixed content or spaces + } } } @@ -264,7 +269,7 @@ abstract class Tag { bool shouldQuote(String value) { if (value == "") { - return false; + return true; } else { String letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; for (int i = 0; i < value.length; i++) { diff --git a/lib/omv/types/list.dart b/lib/omv/types/list.dart index 533bbd5..a7b1aaa 100644 --- a/lib/omv/types/list.dart +++ b/lib/omv/types/list.dart @@ -1,4 +1,3 @@ -import 'package:libac_dart/nbt/Stream.dart'; class list { List _list = []; diff --git a/lib/omv/types/string.dart b/lib/omv/types/string.dart index 6210461..c1dc459 100644 --- a/lib/omv/types/string.dart +++ b/lib/omv/types/string.dart @@ -28,7 +28,7 @@ class string { String toString() => value ?? ''; // Conversion to bool - bool toBool() => value != null && value.isNotEmpty; + bool toBool() => value.isNotEmpty; // Equality operator @override diff --git a/lib/utils/IOTools.dart b/lib/utils/IOTools.dart index c795af4..95cb3b4 100644 --- a/lib/utils/IOTools.dart +++ b/lib/utils/IOTools.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:libac_dart/consts.dart'; class PathHelper { String pth = ""; diff --git a/pubspec.yaml b/pubspec.yaml index 38997e7..6ef907e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: libac_dart description: "Aria's Creations code library" -version: 1.2.082424+1243 +version: 1.2.082924+0654 homepage: "https://zontreck.com" diff --git a/test/EL-ReducedList.dat b/test/EL-ReducedList.dat new file mode 100644 index 0000000000000000000000000000000000000000..4efb40ab2d8427029255c1e3b242c8e11d8d0f3d GIT binary patch literal 2202 zcmbW3PmJ3{6vih_(m2@yMNkz9M5kPeDs_`=cH<4V?6MLocH0G~>Tx^qJjbJTJnqam z>8@0cNJT>8hCt$v=w6U2aRDKr9*V?`D+k1Z3n!$WIdSN-L&jWdpJv9M-~0Uh=6!Av z(js+;jXG|TDpR6L>Y+@PCWM@;lEzfTsex=paHheRG0$fe(twzaBj6PzJg-5`KzU${ zh$q@1PQtXFOT~%fL#(9&UL`e!+X;Ey$Kixc0f)F+zmtMG#&LiV=87c=C_=`6kP2Bz z3@FK&Z;qo%lhme?>lRA6;E-CJ0h;@1WW;vJRE?YwJd^pu)nCpyq?)iJ^1^j#=x!{G zV39Zhj=MspLxSW&{1WxzcB+k>(k*681ett`eEi$P56U248BWs}%oR<&JUw)zSgZpm z#NJo?Uz8!gGzt}qB6^D{P2W!A3CP&Wz0v&U?=sNWypVA@*Hi}7JAz1RdX4VfH4t;) zmJSd0OC_V`&Ac^BNlu#|mC=|{dY1Cwli`grl=E9ysGz9Fd?fvW9`zvWndRZA<*FjvhTXhA~qF9rUiD3|5{4p ztMB63JYkCCTOTmRCNidbFoOs?fK32D@ zMwtMuccf-Blzwy9vW_m_zq4fB+})S_Fn?$jd&#nOQ0VvjW0%2Tv(p*$+_m-on2!g2 z7PvjPJKoqRS_>hzwd84`y-tTN&TWzW&zo27Ekiy_x2o-J9lURazjyl8hh-?uku;a7 zmvU)Q-~aP*sUmZ7)X*DQ($7y`J}o1vwIFe{9?SanuZusGVJ%(|{lOHdN1y#(2DOsQ zT9|}|Jv{&T&u4+PNP`RQqc7sFR7K>?(3nKO)o%Z{yz0wonhjA$7k#2X_HdgU+ zNNeG-*E8Qcw{usZ_0m0tc3hNPg*3@omSrxt$;z?-ofC-;556tqv+I7}?XKf}%_2jb z-sYg=4hDd3jP5(l93D42GxSX^%rU8TJN|n1tI*UbK9Hi6CA*%-!&z>vjn25c+1bdf lHYeBJ&9&hFSUeO3wjT6+m!Tb31EVXbelB7-6zJF$@)UjxM2P?Z literal 0 HcmV?d00001 diff --git a/test/nbt_test.dart b/test/nbt_test.dart index 4e9c23a..98605c3 100644 --- a/test/nbt_test.dart +++ b/test/nbt_test.dart @@ -108,4 +108,19 @@ void main() { expect(ID.MSB, 0); expect(ID.LSB, 0); }); + + test("Convert real-world NBT to SNBT and back to NBT", () async { + String OriginFile = "${Directory.current.path}/test/EL-ReducedList.dat"; + String OutputSNBT = "${Directory.current.path}/build/el-test.snbt"; + String OutputNBT = "${Directory.current.path}/build/el-test.nbt"; + + CompoundTag tag = await NbtIo.read(OriginFile); + await SnbtIo.writeToFile(OutputSNBT, tag); + + expect(File(OutputSNBT).existsSync(), true); + tag = await SnbtIo.readFromFile(OutputSNBT) as CompoundTag; + + await NbtIo.write(OutputNBT, tag); + expect(File(OutputNBT).existsSync(), true); + }, timeout: Timeout(Duration(hours: 90))); }