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
33 changes: 26 additions & 7 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 @@ -43,9 +45,14 @@ public final class SNBTParser {
private @Nullable Token lookahead;

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 +81,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 +117,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 +159,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 @@ -307,8 +322,12 @@ <T extends Token> T nextToken(Class<T> expected) {
}

Token peekToken() {
return peekToken(false);
}

Token peekToken(boolean tokenizeNewLine) {
if (lookahead == null) {
lookahead = readNextToken();
lookahead = readNextToken(tokenizeNewLine);
}
return lookahead;
}
Expand Down Expand Up @@ -400,8 +419,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);
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 +451,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
29 changes: 27 additions & 2 deletions src/main/java/org/glavo/nbt/io/SNBTCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,29 @@ public static SNBTCodec ofCompact() {
private final EscapeStrategy escapeStrategy;
private final QuoteStrategy nameQuoteStrategy;
private final QuoteStrategy valueQuoteStrategy;
private final boolean allowNewLineAsSeparator;

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

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 Down Expand Up @@ -245,6 +260,16 @@ public SNBTCodec withValueQuoteStrategy(QuoteStrategy quoteStrategy) {
return new SNBTCodec(lineBreakStrategy, indentation, surroundingSpaces, escapeStrategy, nameQuoteStrategy, quoteStrategy);
}

@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.
///
/// @throws IOException if the input is not a valid Stringified NBT data.
Expand All @@ -261,7 +286,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
62 changes: 62 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,62 @@
/*
* 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"));
var list = assertInstanceOf(ListTag.class, compound.get("list"));
assert list != null : "ensured by assertInstanceOf";
assertEquals(TagType.INT, list.getElementType());
var expected = new ListTag<>(TagType.INT);
expected.setName("list");
expected.addTags(new IntTag(1), new IntTag(2), new IntTag(3), new IntTag(4));
Assertions.assertEquals(expected, list);
}
}

}
9 changes: 9 additions & 0 deletions src/test/resources/assets/nbt/newline_separated.snbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
name: "Foo"
list: [
1
2
3
4
]
}