Skip to content

Commit e94a3b5

Browse files
fix: schematic format detection (#3289)
* fix: schematic format detection * chore: adjust filename limitation for sponge v1 check * fix: adjust paper dev bundle version * feat: make schematic format detection faster * chore/test: add tests for new clipboard format detection algorithm * chore: cleanup * chore: specify charset for rootName
1 parent a6da595 commit e94a3b5

File tree

10 files changed

+229
-5
lines changed

10 files changed

+229
-5
lines changed

worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/BuiltInClipboardFormat.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.anarres.parallelgzip.ParallelGZIPOutputStream;
4141
import org.enginehub.linbus.stream.LinBinaryIO;
4242
import org.enginehub.linbus.stream.LinReadOptions;
43-
import org.enginehub.linbus.tree.LinCompoundTag;
4443
import org.enginehub.linbus.tree.LinRootEntry;
4544

4645
import java.io.BufferedInputStream;
@@ -261,7 +260,11 @@ public boolean isFormat(InputStream inputStream) {
261260

262261
@Override
263262
public boolean isFormat(File file) {
264-
return MCEDIT_SCHEMATIC.isFormat(file);
263+
String name = file.getName().toLowerCase(Locale.ROOT);
264+
if (name.endsWith(".mcedit") || name.endsWith(".mce")) {
265+
return false;
266+
}
267+
return super.isFormat(file);
265268
}
266269

267270
@Override
@@ -272,7 +275,7 @@ public Set<String> getExplicitFileExtensions() {
272275

273276
/**
274277
* @deprecated Slow, resource intensive, but sometimes safer than using the recommended
275-
* {@link BuiltInClipboardFormat#FAST}.
278+
* {@link BuiltInClipboardFormat#FAST_V2}.
276279
* Avoid using with any large schematics/clipboards for reading/writing.
277280
*/
278281
@Deprecated

worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormats.java

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@
3030
import com.google.common.collect.Multimaps;
3131
import com.google.common.io.ByteSource;
3232
import com.google.common.io.Files;
33+
import com.sk89q.jnbt.NBTConstants;
34+
import com.sk89q.jnbt.NBTInputStream;
3335
import com.sk89q.worldedit.LocalConfiguration;
3436
import com.sk89q.worldedit.WorldEdit;
3537
import com.sk89q.worldedit.extension.platform.Actor;
38+
import com.sk89q.worldedit.internal.util.LogManagerCompat;
3639
import com.sk89q.worldedit.util.formatting.text.TextComponent;
40+
import it.unimi.dsi.fastutil.io.FastBufferedInputStream;
41+
import org.apache.logging.log4j.Logger;
3742

3843
import javax.annotation.Nullable;
44+
import java.io.DataInputStream;
45+
import java.io.EOFException;
3946
import java.io.File;
4047
import java.io.IOException;
4148
import java.io.InputStream;
@@ -44,6 +51,7 @@
4451
import java.net.URL;
4552
import java.nio.channels.Channels;
4653
import java.nio.channels.ReadableByteChannel;
54+
import java.nio.charset.StandardCharsets;
4755
import java.util.ArrayList;
4856
import java.util.Arrays;
4957
import java.util.Collection;
@@ -55,8 +63,11 @@
5563
import java.util.Locale;
5664
import java.util.Map;
5765
import java.util.Map.Entry;
66+
import java.util.Set;
5867
import java.util.regex.Pattern;
68+
import java.util.zip.GZIPInputStream;
5969
import java.util.zip.ZipEntry;
70+
import java.util.zip.ZipException;
6071
import java.util.zip.ZipInputStream;
6172

6273
import static com.google.common.base.Preconditions.checkNotNull;
@@ -70,6 +81,22 @@ public class ClipboardFormats {
7081
// FAWE end
7182
private static final List<ClipboardFormat> registeredFormats = new ArrayList<>();
7283

84+
// FAWE start - provide logger instance + track fast-search formats
85+
private static final Logger LOGGER = LogManagerCompat.getLogger();
86+
// a list of all schematic formats which are handled by the faster detection algorithm.
87+
// contains all builtin formats as of the time of writing, but in case we forget updating the algorithm after introducing a
88+
// new format, we keep track manually to avoid breaking something.
89+
@SuppressWarnings("deprecation")
90+
private static final Set<ClipboardFormat> FAST_SEARCH_BUILTIN_FORMATS = Set.of(
91+
BuiltInClipboardFormat.PNG, BuiltInClipboardFormat.MCEDIT_SCHEMATIC, BuiltInClipboardFormat.MINECRAFT_STRUCTURE,
92+
BuiltInClipboardFormat.SPONGE_V1_SCHEMATIC, BuiltInClipboardFormat.FAST_V2, BuiltInClipboardFormat.FAST_V3,
93+
// the following formats are not explicitly supported, but either don't support reading or
94+
// are handled by previous formats (e.g. SPONGE_V2_SCHEMATIC -> FAST_V2)
95+
BuiltInClipboardFormat.SPONGE_V2_SCHEMATIC, BuiltInClipboardFormat.SPONGE_V3_SCHEMATIC,
96+
BuiltInClipboardFormat.BROKENENTITY
97+
);
98+
// FAWE end
99+
73100
public static void registerClipboardFormat(ClipboardFormat format) {
74101
checkNotNull(format);
75102

@@ -112,24 +139,118 @@ public static ClipboardFormat findByAlias(String alias) {
112139
return aliasMap.get(alias.toLowerCase(Locale.ROOT).trim());
113140
}
114141

142+
//FAWE start - optimize format detection for builtin / known formats
143+
115144
/**
116-
* Detect the format of given a file.
145+
* Detect the format of a given file.
117146
*
118147
* @param file the file
119148
* @return the format, otherwise null if one cannot be detected
120149
*/
150+
@SuppressWarnings("removal") // NBTInputStream + NBTConstants
121151
@Nullable
122152
public static ClipboardFormat findByFile(File file) {
123153
checkNotNull(file);
124154

155+
if (file.getName().toLowerCase().endsWith(".png")) {
156+
return BuiltInClipboardFormat.PNG;
157+
}
158+
159+
/* Conditions for known formats
160+
FAST_V3: Compound_Tag("") -> Compound_Tag("Schematic") -> Int_Tag("Version") == 3
161+
MINECRAFT_STRUCTURE: Compound_Tag("") -> exist(Nbt_List_Tag("size" || "palette" || "blocks" || "entities"))
162+
FAST_V2: Compound_Tag("Schematic") -> Int_Tag("Version") == 2
163+
SPONGE_V1: Compound_Tag("Schematic") -> Int_Tag("Version") == 1
164+
MC_EDIT: Compound_Tag("Schematic") -> exist(Byte_Array_Tag("Blocks") || String_Tag("Materials"))
165+
*/
166+
try (final DataInputStream inputStream = new DataInputStream(new GZIPInputStream(new FastBufferedInputStream(java.nio.file.Files.newInputStream(
167+
file.toPath()))));
168+
final NBTInputStream nbtInputStream = new NBTInputStream(inputStream)) {
169+
if (inputStream.readByte() != NBTConstants.TYPE_COMPOUND) {
170+
return findByFileInExternalFormats(file);
171+
}
172+
final int rootNameTagLength = inputStream.readShort() & 0xFFFF;
173+
if (rootNameTagLength != 0 && rootNameTagLength != 9) { // Only allow "" and "Schematic"
174+
return findByFileInExternalFormats(file);
175+
}
176+
final String rootName = new String(inputStream.readNBytes(rootNameTagLength), StandardCharsets.UTF_8);
177+
if (rootName.isEmpty()) {
178+
// Only FAST_V3 and MINECRAFT_STRUCTURE use empty named root compound tags
179+
// FAST_V3 only contains a single child component - if that's not present, only MINECRAFT_STRUCTURE is possible
180+
do {
181+
byte type = inputStream.readByte();
182+
if (type == NBTConstants.TYPE_END) {
183+
return findByFileInExternalFormats(file);
184+
}
185+
String name = nbtInputStream.readNamedTagName(type);
186+
if (type == NBTConstants.TYPE_COMPOUND && name.equals("Schematic")) {
187+
// unwrap inner schematic compound for general processing below
188+
break;
189+
}
190+
// search for almost all known compound children for a fast return path (lowercase is specific enough for now)
191+
if (type == NBTConstants.TYPE_LIST &&
192+
(name.equals("size") || name.equals("palette") || name.equals("blocks") || name.equals("entities"))) {
193+
return BuiltInClipboardFormat.MINECRAFT_STRUCTURE;
194+
}
195+
nbtInputStream.readTagPayloadLazy(type, 0); // skip unwanted tags and continue search
196+
} while (true);
197+
}
198+
199+
do {
200+
byte type = inputStream.readByte();
201+
if (type == NBTConstants.TYPE_END) {
202+
return findByFileInExternalFormats(file);
203+
}
204+
String name = nbtInputStream.readNamedTagName(type);
205+
if ((type == NBTConstants.TYPE_BYTE_ARRAY && name.equals("Blocks")) ||
206+
(type == NBTConstants.TYPE_STRING && name.equals("Materials"))) {
207+
return BuiltInClipboardFormat.MCEDIT_SCHEMATIC;
208+
}
209+
if (type == NBTConstants.TYPE_INT && name.equals("Version")) {
210+
int version = inputStream.readInt();
211+
return switch (version) {
212+
case 1 -> BuiltInClipboardFormat.SPONGE_V1_SCHEMATIC;
213+
case 2 -> BuiltInClipboardFormat.FAST_V2;
214+
case 3 -> BuiltInClipboardFormat.FAST_V3;
215+
default -> findByFileInExternalFormats(file);
216+
};
217+
}
218+
nbtInputStream.readTagPayloadLazy(type, 0); // skip unwanted tags and continue search
219+
} while (true);
220+
} catch (ZipException | EOFException ignored) {
221+
// ignore gzip errors and EOFs - the file format might not use gzip, or we expected more data from known formats
222+
// all other builtin formats use gzip compression
223+
} catch (IOException e) {
224+
// other IO errors (non gzip-related) should be logged
225+
LOGGER.error("Failed determining clipboard format for file {}", file.getAbsolutePath(), e);
226+
return null;
227+
}
228+
229+
// no builtin format seems to match - test the remaining registered formats (added by other plugins, for example)
230+
return findByFileInExternalFormats(file);
231+
}
232+
233+
/**
234+
* Detect the clipboard format for a specified file while skipping optimized builtin formats
235+
*
236+
* @param file the file
237+
* @return the format or {@code null}
238+
*/
239+
private static ClipboardFormat findByFileInExternalFormats(File file) {
240+
if (registeredFormats.size() == FAST_SEARCH_BUILTIN_FORMATS.size()) {
241+
return null;
242+
}
125243
for (ClipboardFormat format : registeredFormats) {
244+
if (FAST_SEARCH_BUILTIN_FORMATS.contains(format)) {
245+
continue;
246+
}
126247
if (format.isFormat(file)) {
127248
return format;
128249
}
129250
}
130-
131251
return null;
132252
}
253+
//FAWE end
133254

134255
/**
135256
* A mapping from extensions to formats.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.sk89q.worldedit.extent.clipboard.io;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.io.File;
7+
import java.io.InputStream;
8+
import java.io.OutputStream;
9+
import java.nio.file.Path;
10+
import java.util.Set;
11+
12+
class ClipboardFormatsTest {
13+
14+
@Test
15+
void findByFile() {
16+
Assertions.assertSame(
17+
BuiltInClipboardFormat.SPONGE_V1_SCHEMATIC,
18+
ClipboardFormats.findByFile(getTestSchematic("sponge1.schem"))
19+
);
20+
Assertions.assertSame(
21+
BuiltInClipboardFormat.FAST_V2,
22+
ClipboardFormats.findByFile(getTestSchematic("sponge2.schem"))
23+
);
24+
Assertions.assertSame(
25+
BuiltInClipboardFormat.FAST_V3,
26+
ClipboardFormats.findByFile(getTestSchematic("sponge3.schem"))
27+
);
28+
Assertions.assertSame(
29+
BuiltInClipboardFormat.MCEDIT_SCHEMATIC,
30+
ClipboardFormats.findByFile(getTestSchematic("mcedit.mce"))
31+
);
32+
Assertions.assertSame(
33+
BuiltInClipboardFormat.MINECRAFT_STRUCTURE,
34+
ClipboardFormats.findByFile(getTestSchematic("minecraft_structure.nbt"))
35+
);
36+
Assertions.assertSame(
37+
BuiltInClipboardFormat.PNG,
38+
ClipboardFormats.findByFile(getTestSchematic("1x1.png"))
39+
);
40+
41+
Assertions.assertNull(
42+
ClipboardFormats.findByFile(getTestSchematic("custom_format.xyz"))
43+
);
44+
ClipboardFormats.registerClipboardFormat(new CustomTestingClipboardFormat());
45+
Assertions.assertInstanceOf(
46+
CustomTestingClipboardFormat.class,
47+
ClipboardFormats.findByFile(getTestSchematic("custom_format.xyz"))
48+
);
49+
}
50+
51+
private static File getTestSchematic(String name) {
52+
return Path.of("src", "test", "resources", "fastasyncworldedit", "schematics", name).toFile();
53+
}
54+
55+
private static final class CustomTestingClipboardFormat implements ClipboardFormat {
56+
57+
@Override
58+
public String getName() {
59+
return "Custom Testing Format";
60+
}
61+
62+
@Override
63+
public boolean isFormat(final File file) {
64+
return file.getName().endsWith(".xyz");
65+
}
66+
67+
@Override
68+
public Set<String> getAliases() {
69+
return Set.of();
70+
}
71+
72+
@Override
73+
public ClipboardReader getReader(final InputStream inputStream) {
74+
return null;
75+
}
76+
77+
@Override
78+
public ClipboardWriter getWriter(final OutputStream outputStream) {
79+
return null;
80+
}
81+
82+
@Override
83+
public String getPrimaryFileExtension() {
84+
return "";
85+
}
86+
87+
@Override
88+
public Set<String> getFileExtensions() {
89+
return Set.of();
90+
}
91+
92+
@Override
93+
public Set<String> getExplicitFileExtensions() {
94+
return Set.of();
95+
}
96+
97+
}
98+
99+
}
95 Bytes
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hi!
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)