Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support JNBT string encoding as an option #10

Merged
merged 3 commits into from
Oct 2, 2024
Merged
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
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
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
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