Skip to content

Commit

Permalink
Support JNBT string encoding as an option
Browse files Browse the repository at this point in the history
Fixes #9
  • Loading branch information
octylFractal committed Oct 2, 2024
1 parent 6947c99 commit 4fc44fa
Show file tree
Hide file tree
Showing 11 changed files with 677 additions and 115 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ikonli-fontawesome5.module = "org.kordamp.ikonli:ikonli-fontawesome5-pack"
junit-bom = "org.junit:junit-bom:5.10.3"
junit-jupiter-api.module = "org.junit.jupiter:junit-jupiter-api"
junit-jupiter-engine.module = "org.junit.jupiter:junit-jupiter-engine"
junit-jupiter-params.module = "org.junit.jupiter:junit-jupiter-params"
truth = "com.google.truth:truth:1.4.4"

[libraries.tinylog-api]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@
import javafx.stage.Stage;
import org.enginehub.linbus.gui.LinBusGui;
import org.enginehub.linbus.gui.util.ErrorReporter;
import org.enginehub.linbus.stream.LinReadOptions;
import org.jspecify.annotations.Nullable;
import org.kordamp.ikonli.fontawesome5.FontAwesomeSolid;
import org.kordamp.ikonli.javafx.FontIcon;

import java.io.File;
import java.io.IOException;
import java.io.UTFDataFormatException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -106,28 +108,7 @@ private MenuItem openFile(Stage stage, ExecutorService backgroundExecutor) {
return;
}
Path path = file.toPath();
backgroundExecutor.submit(new Task<TreeItem<NbtTreeView.TagEntry>>() {

@Override
protected TreeItem<NbtTreeView.TagEntry> call() throws Exception {
return NbtTreeView.loadTreeItem(path);
}

@Override
protected void succeeded() {
openPath.set(path);
TreeItem<NbtTreeView.TagEntry> value = getValue();
originalTag.set(value.getValue());
treeTableView.setRoot(value);
}

@Override
protected void failed() {
ErrorReporter.reportError(
ErrorReporter.Level.INFORM, "Failed to open file " + path, getException()
);
}
});
backgroundExecutor.submit(new LoadTreeItemTask(path, backgroundExecutor, false));
});
return openFile;
}
Expand Down Expand Up @@ -239,15 +220,6 @@ private Button moveEntryDown() {
public final Scene mainScene;

public MainSceneSetup(Stage stage, ExecutorService backgroundExecutor) {
openPath.addListener((__, oldPath, newPath) -> {
if (newPath != null) {
try {
treeTableView.setRoot(NbtTreeView.loadTreeItem(newPath));
} catch (IOException e) {
ErrorReporter.reportError(ErrorReporter.Level.INFORM, "Failed to open file " + newPath, e);
}
}
});
ObservableValue<NbtTreeView.TagEntry> rootTagEntry = treeTableView.rootProperty().flatMap(TreeItem::valueProperty);
isModified = originalTag.isNotEqualTo(
// Promote ObservableValue to ObjectBinding
Expand All @@ -267,4 +239,65 @@ public MainSceneSetup(Stage stage, ExecutorService backgroundExecutor) {
VBox.setVgrow(treeTableView, Priority.ALWAYS);
mainScene = new Scene(mainPane, 900, 600);
}

private class LoadTreeItemTask extends Task<TreeItem<NbtTreeView.TagEntry>> {

private final Path path;
private final ExecutorService backgroundExecutor;
private final boolean tryingLegacyCompat;

public LoadTreeItemTask(Path path, ExecutorService backgroundExecutor, boolean tryingLegacyCompat) {
this.path = path;
this.backgroundExecutor = backgroundExecutor;
this.tryingLegacyCompat = tryingLegacyCompat;
}

@Override
protected TreeItem<NbtTreeView.TagEntry> call() throws Exception {
LinReadOptions.Builder options = LinReadOptions.builder();
if (tryingLegacyCompat) {
options.allowJnbtStringEncoding(true);
}
return NbtTreeView.loadTreeItem(path, options.build());
}

@Override
protected void succeeded() {
TreeItem<NbtTreeView.TagEntry> value = getValue();
openPath.set(path);
originalTag.set(value.getValue());
treeTableView.setRoot(value);
}

@Override
protected void failed() {
Throwable ex = getException();
if (ex instanceof UTFDataFormatException && !tryingLegacyCompat) {
Alert alert = createUtfAlert();
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent() && result.get() == ButtonType.YES) {
backgroundExecutor.submit(new LoadTreeItemTask(path, backgroundExecutor, true));
return;
}
}
ErrorReporter.reportError(
ErrorReporter.Level.INFORM, "Failed to open file " + path, getException()
);
}

private static Alert createUtfAlert() {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(LinBusGui.TITLE_BASE + " - Invalid File");
alert.setHeaderText("File contained invalid modified UTF-8");
alert.setContentText("The file you tried to open contained invalid modified UTF-8, " +
"but may be a legacy JNBT file. Would you like to try opening it with JNBT compatibility?\n" +
"Saving the file will convert it to standard NBT format.");
alert.getButtonTypes().setAll(
ButtonType.YES,
ButtonType.NO
);
return alert;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import org.enginehub.linbus.stream.LinBinaryIO;
import org.enginehub.linbus.stream.LinReadOptions;
import org.enginehub.linbus.tree.LinCompoundTag;
import org.enginehub.linbus.tree.LinListTag;
import org.enginehub.linbus.tree.LinRootEntry;
Expand Down Expand Up @@ -97,10 +98,10 @@ private static TreeTableColumn<TagEntry, TagEntry> createValueColumn() {
return valueCol;
}

public static TreeItem<TagEntry> loadTreeItem(Path file) throws IOException {
public static TreeItem<TagEntry> loadTreeItem(Path file, LinReadOptions options) throws IOException {
LinRootEntry root;
try (var dataInput = new DataInputStream(new GZIPInputStream(Files.newInputStream(file)))) {
root = LinBinaryIO.readUsing(dataInput, LinRootEntry::readFrom);
root = LinBinaryIO.readUsing(dataInput, options, LinRootEntry::readFrom);
}
assert root != null;
return new TagEntryTreeItem(root.name(), root.value());
Expand Down
1 change: 1 addition & 0 deletions stream/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {

testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter.api)
testImplementation(libs.junit.jupiter.params)
testRuntimeOnly(libs.junit.jupiter.engine)

testImplementation(libs.truth) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,22 @@ public class LinBinaryIO {
* @return the stream of NBT tokens
*/
public static LinStream read(DataInput input) {
return new LinNbtReader(input);
return read(input, LinReadOptions.builder().build());
}

/**
* Read a stream of NBT tokens from a {@link DataInput}.
*
* <p>
* The input will not be closed by the iterator. The caller is responsible for managing the lifetime of the input.
* </p>
*
* @param input the input to read from
* @param options the options for reading
* @return the stream of NBT tokens
*/
public static LinStream read(DataInput input, LinReadOptions options) {
return new LinNbtReader(input, options);
}

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

/**
* Read a result using a stream of NBT tokens from a {@link DataInput}.
*
* <p>
* The input will not be closed by this method. The caller is responsible for managing the lifetime of the input.
* </p>
*
* @param input the input to read from
* @param options the options for reading
* @param transform the function to transform the stream of NBT tokens into the result
* @param <R> the type of the result
* @return the result
* @throws IOException if an I/O error occurs ({@link UncheckedIOException} is unwrapped)
*/
public static <R extends @Nullable Object> R readUsing(DataInput input, LinReadOptions options, IOFunction<? super LinStream, ? extends R> transform)
throws IOException {
return transform.apply(read(input, options));
}

/**
* Write a stream of NBT tokens to a {@link DataOutput}.
*
Expand Down
102 changes: 102 additions & 0 deletions stream/src/main/java/org/enginehub/linbus/stream/LinReadOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) EngineHub <https://enginehub.org>
* Copyright (c) contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package org.enginehub.linbus.stream;


/**
* Options for reading NBT streams.
*/
public final class LinReadOptions {

/**
* Create a new builder.
*
* @return a new builder
*/
public static Builder builder() {
return new Builder();
}

/**
* Builder for {@link LinReadOptions}.
*/
public static final class Builder {
private boolean allowJnbtStringEncoding = false;

private Builder() {
}

/**
* Set whether to allow the string encoding used by JNBT. It is not compliant with the NBT specification and
* uses normal UTF-8 encoding instead of the modified UTF-8 encoding of {@link java.io.DataInput}.
*
* <p>
* Note that this option will force checking the bytes to select the correct encoding, which will be slower.
* </p>
*
* @param allowJnbtStringEncoding whether to allow the string encoding used by JNBT
* @return this builder
*/
public Builder allowJnbtStringEncoding(boolean allowJnbtStringEncoding) {
this.allowJnbtStringEncoding = allowJnbtStringEncoding;
return this;
}

/**
* Build the options.
*
* @return the options
*/
public LinReadOptions build() {
return new LinReadOptions(this);
}

@Override
public String toString() {
return "LinReadOptions.Builder{" +
"allowJnbtStringEncoding=" + allowJnbtStringEncoding +
'}';
}
}

private final boolean allowJnbtStringEncoding;

private LinReadOptions(Builder builder) {
this.allowJnbtStringEncoding = builder.allowJnbtStringEncoding;
}

/**
* {@return whether to allow the string encoding used by JNBT} It is not compliant with the NBT specification and
* uses normal UTF-8 encoding instead of the modified UTF-8 encoding of {@link java.io.DataInput}.
*
* <p>
* Note that this option will force checking the bytes to select the correct encoding, which will be slower.
* </p>
*/
public boolean allowJnbtStringEncoding() {
return allowJnbtStringEncoding;
}

@Override
public String toString() {
return "LinReadOptions{" +
"allowJnbtStringEncoding=" + allowJnbtStringEncoding +
'}';
}
}
Loading

0 comments on commit 4fc44fa

Please sign in to comment.