Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions src/main/java/org/glavo/nbt/internal/snbt/SNBTParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
public final class SNBTParser {
private final CharSequence input;
private final int endIndex;
/// Whether allows New Line (\n) as separators in Compound and List. Used in FTB-flavored SNBT.
private final boolean allowNewLineAsSeparator;

private boolean parsingPath = false;

Expand All @@ -41,11 +43,22 @@ public final class SNBTParser {
private @Nullable StringBuilder buffer;

private @Nullable Token lookahead;
/// the tokenizeNewLine flag of the corresponding lookahead value, used to check if a re-read is needed.
/// the nullability is same to the lookahead, which when lookahead is null, this is null.
private @Nullable Boolean lookaheadNewLineParam;
/// the cursor to roll back to when the lookahead need to be re-read.
/// the nullability is same to the lookahead, which when lookahead is null, this is null.
private @Nullable Integer lookaheadCursor;

public SNBTParser(CharSequence input, int beginIndex, int endIndex) {
this(input, beginIndex, endIndex, false);
}

public SNBTParser(CharSequence input, int beginIndex, int endIndex, boolean allowNewLineAsSeparator) {
Objects.checkFromToIndex(beginIndex, endIndex, input.length());
this.input = input;
this.endIndex = endIndex;
this.allowNewLineAsSeparator = allowNewLineAsSeparator;

this.cursor = beginIndex;
}
Expand Down Expand Up @@ -74,9 +87,12 @@ private StringBuilder getBuilder() {
return buffer;
}

private void skipWhiteSpace() {
private void skipWhiteSpace(boolean keepNewLine) {
while (cursor < endIndex) {
int ch = getCodePoint();
if (allowNewLineAsSeparator && keepNewLine && ch == '\n') {
break;
}
if (Character.isWhitespace(ch)) {
cursor += Character.charCount(ch);
} else {
Expand Down Expand Up @@ -107,7 +123,11 @@ private boolean isUnquotedStringPart(int ch) {
}

Token readNextToken() {
skipWhiteSpace();
return readNextToken(false);
}

Token readNextToken(boolean tokenizeNewLine) {
skipWhiteSpace(tokenizeNewLine);

if (cursor >= endIndex) {
return Token.SimpleToken.EOF;
Expand Down Expand Up @@ -145,6 +165,7 @@ Token readNextToken() {
case '.' -> cursor >= endIndex || !TextUtils.isAsciiDigit(input.charAt(cursor))
? Token.SimpleToken.DOT
: null; // Floating point number
case '\n' -> Token.SimpleToken.NEW_LINE; // '\n' will be skipped above unless allowed
default -> null;
};

Expand Down Expand Up @@ -285,6 +306,8 @@ Token nextToken() {
if (lookahead != null) {
Token token = lookahead;
lookahead = null;
lookaheadNewLineParam = null;
lookaheadCursor = null;
return token;
}
return readNextToken();
Expand All @@ -307,8 +330,24 @@ <T extends Token> T nextToken(Class<T> expected) {
}

Token peekToken() {
if (lookahead == null) {
lookahead = readNextToken();
return peekToken(false);
}

Token peekToken(boolean tokenizeNewLine) {
// nullability of lookaheadNewLineParam is same to lookahead.
assert (lookahead != null) == (lookaheadNewLineParam != null);
assert (lookahead != null) == (lookaheadCursor != null);

if(lookahead == null) {
// preserved the rollback point
lookaheadCursor = cursor;
lookahead = readNextToken(tokenizeNewLine);
lookaheadNewLineParam = tokenizeNewLine;
} else if(lookaheadNewLineParam != tokenizeNewLine) {
// the flag doesn't match, do the rollback
cursor = lookaheadCursor;
lookahead = readNextToken(tokenizeNewLine);
lookaheadNewLineParam = tokenizeNewLine;
}
return lookahead;
}
Expand All @@ -318,6 +357,8 @@ void discardPeekedToken(Token token) {
throw new AssertionError("Expected " + token + ", but got " + lookahead);
}
lookahead = null;
lookaheadNewLineParam = null;
lookaheadCursor = null;
}

public @Nullable Tag nextTag() throws IllegalArgumentException {
Expand Down Expand Up @@ -400,8 +441,8 @@ private CompoundTag nextCompoundTag(boolean shareEmpty) throws IllegalArgumentEx

tag.addTag(nameToken.value(), value);

Token peek = peekToken();
if (peek == Token.SimpleToken.COMMA) {
Token peek = peekToken(true);
Comment thread
Taskeren marked this conversation as resolved.
if (peek == Token.SimpleToken.COMMA || peek == Token.SimpleToken.NEW_LINE) {
discardPeekedToken(peek);
} else if (peek == Token.SimpleToken.RIGHT_BRACE) {
discardPeekedToken(peek);
Expand Down Expand Up @@ -432,8 +473,8 @@ private ListTag<?> nextListTag() throws IllegalArgumentException {
}
tag.addAnyTag(value);

peek = peekToken();
if (peek == Token.SimpleToken.COMMA) {
peek = peekToken(true);
if (peek == Token.SimpleToken.COMMA || peek == Token.SimpleToken.NEW_LINE) {
discardPeekedToken(peek);
} else if (peek == Token.SimpleToken.RIGHT_BRACKET) {
discardPeekedToken(peek);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/glavo/nbt/internal/snbt/Token.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum SimpleToken implements Token {
COMMA, // ,
COLON, // :
DOT, // .
NEW_LINE, // \n
EOF
}

Expand Down
44 changes: 29 additions & 15 deletions src/main/java/org/glavo/nbt/io/SNBTCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,17 @@ public final class SNBTCodec {
SurroundingSpaces.COMPACT,
EscapeStrategy.defaultStrategy(),
QuoteStrategy.defaultNameStrategy(),
QuoteStrategy.defaultValueStrategy()
);
QuoteStrategy.defaultValueStrategy(),
false);

private static final SNBTCodec PRETTY = new SNBTCodec(
LineBreakStrategy.defaultStrategy(),
" ", // 4 spaces
SurroundingSpaces.PRETTY,
EscapeStrategy.defaultStrategy(),
QuoteStrategy.defaultNameStrategy(),
QuoteStrategy.defaultValueStrategy()
);
QuoteStrategy.defaultValueStrategy(),
false);

public static SNBTCodec of() {
return PRETTY;
Expand All @@ -110,14 +110,17 @@ public static SNBTCodec ofCompact() {
private final EscapeStrategy escapeStrategy;
private final QuoteStrategy nameQuoteStrategy;
private final QuoteStrategy valueQuoteStrategy;
private final boolean allowNewLineAsSeparator;
Comment thread
Taskeren marked this conversation as resolved.

private SNBTCodec(LineBreakStrategy lineBreakStrategy, String indentation, SurroundingSpaces surroundingSpaces, EscapeStrategy escapeStrategy, QuoteStrategy nameQuoteStrategy, QuoteStrategy valueQuoteStrategy) {
private SNBTCodec(LineBreakStrategy lineBreakStrategy, String indentation, SurroundingSpaces surroundingSpaces,
EscapeStrategy escapeStrategy, QuoteStrategy nameQuoteStrategy, QuoteStrategy valueQuoteStrategy, boolean allowNewLineAsSeparator) {
this.lineBreakStrategy = lineBreakStrategy;
this.indentation = indentation;
this.surroundingSpaces = surroundingSpaces;
this.escapeStrategy = escapeStrategy;
this.nameQuoteStrategy = nameQuoteStrategy;
this.valueQuoteStrategy = valueQuoteStrategy;
this.allowNewLineAsSeparator = allowNewLineAsSeparator;
}

/// Returns the line break strategy for all parent tags.
Expand All @@ -136,7 +139,7 @@ public LineBreakStrategy getLineBreakStrategy() {
@Contract(value = "_ -> new", pure = true)
public SNBTCodec withLineBreakStrategy(LineBreakStrategy strategy) {
Objects.requireNonNull(strategy, "strategy");
return new SNBTCodec(strategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy);
return new SNBTCodec(strategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);
}

/// Returns the indentation string before each line.
Expand All @@ -160,7 +163,7 @@ public SNBTCodec withIndentation(String indentation) {
}
}

return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy);
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);

}

Expand All @@ -172,7 +175,7 @@ public SNBTCodec withIndentation(String indentation) {
/// @see #withIndentation(String)
@Contract(value = "_ -> new", pure = true)
public SNBTCodec withIndentation(int spaces) {
return new SNBTCodec(lineBreakStrategy, " ".repeat(spaces), surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy);
return new SNBTCodec(lineBreakStrategy, " ".repeat(spaces), surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);
}

/// Returns the surrounding spaces for SNBT.
Expand All @@ -185,7 +188,7 @@ public SurroundingSpaces getSurroundingSpaces() {
@Contract(value = "_ -> new", pure = true)
public SNBTCodec withSurroundingSpaces(SurroundingSpaces surroundingSpaces) {
Objects.requireNonNull(surroundingSpaces, "surroundingSpaces");
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy);
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);
}

/// Returns the escape strategy for SNBT.
Expand All @@ -204,7 +207,7 @@ public EscapeStrategy getEscapeStrategy() {
@Contract(value = "_ -> new", pure = true)
public SNBTCodec withEscapeStrategy(EscapeStrategy escapeStrategy) {
Objects.requireNonNull(escapeStrategy, "escapeStrategy");
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy);
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);
}

/// Returns the quote strategy for SNBT tag names.
Expand All @@ -223,7 +226,7 @@ public QuoteStrategy getNameQuoteStrategy() {
@Contract(value = "_ -> new", pure = true)
public SNBTCodec withNameQuoteStrategy(QuoteStrategy quoteStrategy) {
Objects.requireNonNull(quoteStrategy, "quoteStrategy");
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, quoteStrategy, valueQuoteStrategy);
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, quoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);
}

/// Returns the quote strategy for SNBT tag values.
Expand All @@ -242,7 +245,17 @@ public QuoteStrategy getValueQuoteStrategy() {
@Contract(value = "_ -> new", pure = true)
public SNBTCodec withValueQuoteStrategy(QuoteStrategy quoteStrategy) {
Objects.requireNonNull(quoteStrategy, "quoteStrategy");
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, quoteStrategy);
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, quoteStrategy, allowNewLineAsSeparator);
}

@Contract(pure = true)
public boolean getAllowNewLineAsSeparator() {
return allowNewLineAsSeparator;
}

@Contract(pure = true)
public SNBTCodec withAllowNewLineAsSeparator(boolean allowNewLineAsSeparator) {
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);
}

/// Reads a NBT tag from the Stringified NBT data.
Expand All @@ -261,7 +274,7 @@ public Tag readTag(CharSequence input) throws IOException {
public Tag readTag(CharSequence input, int startInclusive, int endExclusive) throws IOException {
Tag tag;
try {
tag = new SNBTParser(input, startInclusive, endExclusive).nextTag();
tag = new SNBTParser(input, startInclusive, endExclusive, allowNewLineAsSeparator).nextTag();
} catch (IllegalArgumentException e) {
throw new IOException(e);
}
Expand Down Expand Up @@ -324,12 +337,13 @@ public boolean equals(Object o) {
&& this.surroundingSpaces.equals(that.surroundingSpaces)
&& this.escapeStrategy.equals(that.escapeStrategy)
&& this.nameQuoteStrategy.equals(that.nameQuoteStrategy)
&& this.valueQuoteStrategy.equals(that.valueQuoteStrategy);
&& this.valueQuoteStrategy.equals(that.valueQuoteStrategy)
&& this.allowNewLineAsSeparator == that.allowNewLineAsSeparator;
}

@Override
public int hashCode() {
return Objects.hash(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy);
return Objects.hash(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, valueQuoteStrategy, allowNewLineAsSeparator);
}

@Override
Expand Down
74 changes: 74 additions & 0 deletions src/test/java/org/glavo/nbt/io/SNBTCodecTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2026 Taskeren
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.glavo.nbt.io;

import org.glavo.nbt.TestResources;
import org.glavo.nbt.tag.CompoundTag;
import org.glavo.nbt.tag.IntTag;
import org.glavo.nbt.tag.ListTag;
import org.glavo.nbt.tag.TagType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;

public final class SNBTCodecTest {

@Test
void testNewLineSeparatedSNBT() throws IOException {
Path resource = TestResources.getResource("/assets/nbt/newline_separated.snbt");
// expected failure
// the vanilla-flavored SNBT should not split compounds and lists with '\n'.
Assertions.assertThrows(
IOException.class, () -> {
try (var reader = Files.newBufferedReader(resource, StandardCharsets.UTF_8)) {
SNBTCodec.of().readTag(reader);
}
});

try (var reader = Files.newBufferedReader(resource, StandardCharsets.UTF_8)) {
var tag = SNBTCodec.of().withAllowNewLineAsSeparator(true).readTag(reader);
var compound = assertInstanceOf(CompoundTag.class, tag);
assertEquals("Foo", compound.getString("name"));
// check list
var list = assertInstanceOf(ListTag.class, compound.get("list"));
assert list != null : "ensured by assertInstanceOf";
assertEquals(TagType.INT, list.getElementType());
assertEquals(new ListTag<>(TagType.INT).setName("list")
.addTag(new IntTag(1))
.addTag(new IntTag(2))
.addTag(new IntTag(3))
.addTag(new IntTag(4)), list);
// check compound
var strDict = assertInstanceOf(CompoundTag.class, compound.get("str_dict"));
assertEquals(new CompoundTag()
.setName("str_dict")
.setString("a", "foo")
.setString("b", "bar")
.setString("c", "baz")
.setString("d", "Hello\nWorld")
.setIntArray("e", new int[] {-886784530, 1488270925, -1459824571, -372109282})
.setBoolean("f", true), strDict);
}
}

}
18 changes: 18 additions & 0 deletions src/test/resources/assets/nbt/newline_separated.snbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
name: "Foo"
list: [
1
2
3
4
]
str_dict: {
a: foo
b: bar
c: baz
d: "Hello
World"
e: uuid(cb24bdee-58b5-364d-a8fc-d845e9d2101e)
f: bool(1)
}
}