Skip to content

Commit 4fc44fa

Browse files
committed
Support JNBT string encoding as an option
Fixes #9
1 parent 6947c99 commit 4fc44fa

File tree

11 files changed

+677
-115
lines changed

11 files changed

+677
-115
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ikonli-fontawesome5.module = "org.kordamp.ikonli:ikonli-fontawesome5-pack"
1313
junit-bom = "org.junit:junit-bom:5.10.3"
1414
junit-jupiter-api.module = "org.junit.jupiter:junit-jupiter-api"
1515
junit-jupiter-engine.module = "org.junit.jupiter:junit-jupiter-engine"
16+
junit-jupiter-params.module = "org.junit.jupiter:junit-jupiter-params"
1617
truth = "com.google.truth:truth:1.4.4"
1718

1819
[libraries.tinylog-api]

gui/src/main/java/org/enginehub/linbus/gui/javafx/MainSceneSetup.java

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@
4747
import javafx.stage.Stage;
4848
import org.enginehub.linbus.gui.LinBusGui;
4949
import org.enginehub.linbus.gui.util.ErrorReporter;
50+
import org.enginehub.linbus.stream.LinReadOptions;
5051
import org.jspecify.annotations.Nullable;
5152
import org.kordamp.ikonli.fontawesome5.FontAwesomeSolid;
5253
import org.kordamp.ikonli.javafx.FontIcon;
5354

5455
import java.io.File;
5556
import java.io.IOException;
57+
import java.io.UTFDataFormatException;
5658
import java.nio.file.Path;
5759
import java.util.Optional;
5860
import java.util.concurrent.ExecutorService;
@@ -106,28 +108,7 @@ private MenuItem openFile(Stage stage, ExecutorService backgroundExecutor) {
106108
return;
107109
}
108110
Path path = file.toPath();
109-
backgroundExecutor.submit(new Task<TreeItem<NbtTreeView.TagEntry>>() {
110-
111-
@Override
112-
protected TreeItem<NbtTreeView.TagEntry> call() throws Exception {
113-
return NbtTreeView.loadTreeItem(path);
114-
}
115-
116-
@Override
117-
protected void succeeded() {
118-
openPath.set(path);
119-
TreeItem<NbtTreeView.TagEntry> value = getValue();
120-
originalTag.set(value.getValue());
121-
treeTableView.setRoot(value);
122-
}
123-
124-
@Override
125-
protected void failed() {
126-
ErrorReporter.reportError(
127-
ErrorReporter.Level.INFORM, "Failed to open file " + path, getException()
128-
);
129-
}
130-
});
111+
backgroundExecutor.submit(new LoadTreeItemTask(path, backgroundExecutor, false));
131112
});
132113
return openFile;
133114
}
@@ -239,15 +220,6 @@ private Button moveEntryDown() {
239220
public final Scene mainScene;
240221

241222
public MainSceneSetup(Stage stage, ExecutorService backgroundExecutor) {
242-
openPath.addListener((__, oldPath, newPath) -> {
243-
if (newPath != null) {
244-
try {
245-
treeTableView.setRoot(NbtTreeView.loadTreeItem(newPath));
246-
} catch (IOException e) {
247-
ErrorReporter.reportError(ErrorReporter.Level.INFORM, "Failed to open file " + newPath, e);
248-
}
249-
}
250-
});
251223
ObservableValue<NbtTreeView.TagEntry> rootTagEntry = treeTableView.rootProperty().flatMap(TreeItem::valueProperty);
252224
isModified = originalTag.isNotEqualTo(
253225
// Promote ObservableValue to ObjectBinding
@@ -267,4 +239,65 @@ public MainSceneSetup(Stage stage, ExecutorService backgroundExecutor) {
267239
VBox.setVgrow(treeTableView, Priority.ALWAYS);
268240
mainScene = new Scene(mainPane, 900, 600);
269241
}
242+
243+
private class LoadTreeItemTask extends Task<TreeItem<NbtTreeView.TagEntry>> {
244+
245+
private final Path path;
246+
private final ExecutorService backgroundExecutor;
247+
private final boolean tryingLegacyCompat;
248+
249+
public LoadTreeItemTask(Path path, ExecutorService backgroundExecutor, boolean tryingLegacyCompat) {
250+
this.path = path;
251+
this.backgroundExecutor = backgroundExecutor;
252+
this.tryingLegacyCompat = tryingLegacyCompat;
253+
}
254+
255+
@Override
256+
protected TreeItem<NbtTreeView.TagEntry> call() throws Exception {
257+
LinReadOptions.Builder options = LinReadOptions.builder();
258+
if (tryingLegacyCompat) {
259+
options.allowJnbtStringEncoding(true);
260+
}
261+
return NbtTreeView.loadTreeItem(path, options.build());
262+
}
263+
264+
@Override
265+
protected void succeeded() {
266+
TreeItem<NbtTreeView.TagEntry> value = getValue();
267+
openPath.set(path);
268+
originalTag.set(value.getValue());
269+
treeTableView.setRoot(value);
270+
}
271+
272+
@Override
273+
protected void failed() {
274+
Throwable ex = getException();
275+
if (ex instanceof UTFDataFormatException && !tryingLegacyCompat) {
276+
Alert alert = createUtfAlert();
277+
Optional<ButtonType> result = alert.showAndWait();
278+
if (result.isPresent() && result.get() == ButtonType.YES) {
279+
backgroundExecutor.submit(new LoadTreeItemTask(path, backgroundExecutor, true));
280+
return;
281+
}
282+
}
283+
ErrorReporter.reportError(
284+
ErrorReporter.Level.INFORM, "Failed to open file " + path, getException()
285+
);
286+
}
287+
288+
private static Alert createUtfAlert() {
289+
Alert alert = new Alert(Alert.AlertType.WARNING);
290+
alert.setTitle(LinBusGui.TITLE_BASE + " - Invalid File");
291+
alert.setHeaderText("File contained invalid modified UTF-8");
292+
alert.setContentText("The file you tried to open contained invalid modified UTF-8, " +
293+
"but may be a legacy JNBT file. Would you like to try opening it with JNBT compatibility?\n" +
294+
"Saving the file will convert it to standard NBT format.");
295+
alert.getButtonTypes().setAll(
296+
ButtonType.YES,
297+
ButtonType.NO
298+
);
299+
return alert;
300+
}
301+
}
302+
270303
}

gui/src/main/java/org/enginehub/linbus/gui/javafx/NbtTreeView.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import javafx.scene.control.TreeTableRow;
2727
import javafx.scene.control.TreeTableView;
2828
import org.enginehub.linbus.stream.LinBinaryIO;
29+
import org.enginehub.linbus.stream.LinReadOptions;
2930
import org.enginehub.linbus.tree.LinCompoundTag;
3031
import org.enginehub.linbus.tree.LinListTag;
3132
import org.enginehub.linbus.tree.LinRootEntry;
@@ -97,10 +98,10 @@ private static TreeTableColumn<TagEntry, TagEntry> createValueColumn() {
9798
return valueCol;
9899
}
99100

100-
public static TreeItem<TagEntry> loadTreeItem(Path file) throws IOException {
101+
public static TreeItem<TagEntry> loadTreeItem(Path file, LinReadOptions options) throws IOException {
101102
LinRootEntry root;
102103
try (var dataInput = new DataInputStream(new GZIPInputStream(Files.newInputStream(file)))) {
103-
root = LinBinaryIO.readUsing(dataInput, LinRootEntry::readFrom);
104+
root = LinBinaryIO.readUsing(dataInput, options, LinRootEntry::readFrom);
104105
}
105106
assert root != null;
106107
return new TagEntryTreeItem(root.name(), root.value());

stream/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies {
1212

1313
testImplementation(platform(libs.junit.bom))
1414
testImplementation(libs.junit.jupiter.api)
15+
testImplementation(libs.junit.jupiter.params)
1516
testRuntimeOnly(libs.junit.jupiter.engine)
1617

1718
testImplementation(libs.truth) {

stream/src/main/java/org/enginehub/linbus/stream/LinBinaryIO.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,22 @@ public class LinBinaryIO {
5050
* @return the stream of NBT tokens
5151
*/
5252
public static LinStream read(DataInput input) {
53-
return new LinNbtReader(input);
53+
return read(input, LinReadOptions.builder().build());
54+
}
55+
56+
/**
57+
* Read a stream of NBT tokens from a {@link DataInput}.
58+
*
59+
* <p>
60+
* The input will not be closed by the iterator. The caller is responsible for managing the lifetime of the input.
61+
* </p>
62+
*
63+
* @param input the input to read from
64+
* @param options the options for reading
65+
* @return the stream of NBT tokens
66+
*/
67+
public static LinStream read(DataInput input, LinReadOptions options) {
68+
return new LinNbtReader(input, options);
5469
}
5570

5671
/**
@@ -71,6 +86,25 @@ public static LinStream read(DataInput input) {
7186
return transform.apply(read(input));
7287
}
7388

89+
/**
90+
* Read a result using a stream of NBT tokens from a {@link DataInput}.
91+
*
92+
* <p>
93+
* The input will not be closed by this method. The caller is responsible for managing the lifetime of the input.
94+
* </p>
95+
*
96+
* @param input the input to read from
97+
* @param options the options for reading
98+
* @param transform the function to transform the stream of NBT tokens into the result
99+
* @param <R> the type of the result
100+
* @return the result
101+
* @throws IOException if an I/O error occurs ({@link UncheckedIOException} is unwrapped)
102+
*/
103+
public static <R extends @Nullable Object> R readUsing(DataInput input, LinReadOptions options, IOFunction<? super LinStream, ? extends R> transform)
104+
throws IOException {
105+
return transform.apply(read(input, options));
106+
}
107+
74108
/**
75109
* Write a stream of NBT tokens to a {@link DataOutput}.
76110
*
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (c) EngineHub <https://enginehub.org>
3+
* Copyright (c) contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.enginehub.linbus.stream;
20+
21+
22+
/**
23+
* Options for reading NBT streams.
24+
*/
25+
public final class LinReadOptions {
26+
27+
/**
28+
* Create a new builder.
29+
*
30+
* @return a new builder
31+
*/
32+
public static Builder builder() {
33+
return new Builder();
34+
}
35+
36+
/**
37+
* Builder for {@link LinReadOptions}.
38+
*/
39+
public static final class Builder {
40+
private boolean allowJnbtStringEncoding = false;
41+
42+
private Builder() {
43+
}
44+
45+
/**
46+
* Set whether to allow the string encoding used by JNBT. It is not compliant with the NBT specification and
47+
* uses normal UTF-8 encoding instead of the modified UTF-8 encoding of {@link java.io.DataInput}.
48+
*
49+
* <p>
50+
* Note that this option will force checking the bytes to select the correct encoding, which will be slower.
51+
* </p>
52+
*
53+
* @param allowJnbtStringEncoding whether to allow the string encoding used by JNBT
54+
* @return this builder
55+
*/
56+
public Builder allowJnbtStringEncoding(boolean allowJnbtStringEncoding) {
57+
this.allowJnbtStringEncoding = allowJnbtStringEncoding;
58+
return this;
59+
}
60+
61+
/**
62+
* Build the options.
63+
*
64+
* @return the options
65+
*/
66+
public LinReadOptions build() {
67+
return new LinReadOptions(this);
68+
}
69+
70+
@Override
71+
public String toString() {
72+
return "LinReadOptions.Builder{" +
73+
"allowJnbtStringEncoding=" + allowJnbtStringEncoding +
74+
'}';
75+
}
76+
}
77+
78+
private final boolean allowJnbtStringEncoding;
79+
80+
private LinReadOptions(Builder builder) {
81+
this.allowJnbtStringEncoding = builder.allowJnbtStringEncoding;
82+
}
83+
84+
/**
85+
* {@return whether to allow the string encoding used by JNBT} It is not compliant with the NBT specification and
86+
* uses normal UTF-8 encoding instead of the modified UTF-8 encoding of {@link java.io.DataInput}.
87+
*
88+
* <p>
89+
* Note that this option will force checking the bytes to select the correct encoding, which will be slower.
90+
* </p>
91+
*/
92+
public boolean allowJnbtStringEncoding() {
93+
return allowJnbtStringEncoding;
94+
}
95+
96+
@Override
97+
public String toString() {
98+
return "LinReadOptions{" +
99+
"allowJnbtStringEncoding=" + allowJnbtStringEncoding +
100+
'}';
101+
}
102+
}

0 commit comments

Comments
 (0)