3030import com .google .common .collect .Multimaps ;
3131import com .google .common .io .ByteSource ;
3232import com .google .common .io .Files ;
33+ import com .sk89q .jnbt .NBTConstants ;
34+ import com .sk89q .jnbt .NBTInputStream ;
3335import com .sk89q .worldedit .LocalConfiguration ;
3436import com .sk89q .worldedit .WorldEdit ;
3537import com .sk89q .worldedit .extension .platform .Actor ;
38+ import com .sk89q .worldedit .internal .util .LogManagerCompat ;
3639import com .sk89q .worldedit .util .formatting .text .TextComponent ;
40+ import it .unimi .dsi .fastutil .io .FastBufferedInputStream ;
41+ import org .apache .logging .log4j .Logger ;
3742
3843import javax .annotation .Nullable ;
44+ import java .io .DataInputStream ;
45+ import java .io .EOFException ;
3946import java .io .File ;
4047import java .io .IOException ;
4148import java .io .InputStream ;
5562import java .util .Locale ;
5663import java .util .Map ;
5764import java .util .Map .Entry ;
65+ import java .util .Set ;
5866import java .util .regex .Pattern ;
67+ import java .util .zip .GZIPInputStream ;
5968import java .util .zip .ZipEntry ;
69+ import java .util .zip .ZipException ;
6070import java .util .zip .ZipInputStream ;
6171
6272import static com .google .common .base .Preconditions .checkNotNull ;
@@ -70,6 +80,22 @@ public class ClipboardFormats {
7080 // FAWE end
7181 private static final List <ClipboardFormat > registeredFormats = new ArrayList <>();
7282
83+ // FAWE start - provide logger instance + track fast-search formats
84+ private static final Logger LOGGER = LogManagerCompat .getLogger ();
85+ // a list of all schematic formats which are handled by the faster detection algorithm.
86+ // contains all builtin formats as of the time of writing, but in case we forget updating the algorithm after introducing a
87+ // new format, we keep track manually to avoid breaking something.
88+ @ SuppressWarnings ("deprecation" )
89+ private static final Set <ClipboardFormat > FAST_SEARCH_BUILTIN_FORMATS = Set .of (
90+ BuiltInClipboardFormat .PNG , BuiltInClipboardFormat .MCEDIT_SCHEMATIC , BuiltInClipboardFormat .MINECRAFT_STRUCTURE ,
91+ BuiltInClipboardFormat .SPONGE_V1_SCHEMATIC , BuiltInClipboardFormat .FAST_V2 , BuiltInClipboardFormat .FAST_V3 ,
92+ // the following formats are not explicitly supported, but either don't support reading or
93+ // are handled by previous formats (e.g. SPONGE_V2_SCHEMATIC -> FAST_V2)
94+ BuiltInClipboardFormat .SPONGE_V2_SCHEMATIC , BuiltInClipboardFormat .SPONGE_V3_SCHEMATIC ,
95+ BuiltInClipboardFormat .BROKENENTITY
96+ );
97+ // FAWE end
98+
7399 public static void registerClipboardFormat (ClipboardFormat format ) {
74100 checkNotNull (format );
75101
@@ -112,24 +138,110 @@ public static ClipboardFormat findByAlias(String alias) {
112138 return aliasMap .get (alias .toLowerCase (Locale .ROOT ).trim ());
113139 }
114140
141+ //FAWE start - optimize format detection for builtin / known formats
142+
115143 /**
116144 * Detect the format of given a file.
117145 *
118146 * @param file the file
119147 * @return the format, otherwise null if one cannot be detected
120148 */
149+ @ SuppressWarnings ("removal" ) // NBTInputStream + NBTConstants
121150 @ Nullable
122151 public static ClipboardFormat findByFile (File file ) {
123152 checkNotNull (file );
124153
154+ if (file .getName ().toLowerCase ().endsWith (".png" )) {
155+ return BuiltInClipboardFormat .PNG ;
156+ }
157+
158+ /* Conditions for known formats
159+ FAST_V3: Compound_Tag("") -> Compound_Tag("Schematic") -> Int_Tag("Version") == 3
160+ MINECRAFT_STRUCTURE: Compound_Tag("") -> len(Nbt_List_Tag("size"))
161+ FAST_V2: Compound_Tag("Schematic") -> Int_Tag("Version") == 2
162+ SPONGE_V1: Compound_Tag("Schematic") -> Int_Tag("Version") == 1
163+ MC_EDIT: Compound_Tag("Schematic") -> exist(Byte_Array_Tag("Blocks") || String_Tag("Materials"))
164+ */
165+ try (final DataInputStream inputStream = new DataInputStream (new GZIPInputStream (new FastBufferedInputStream (java .nio .file .Files .newInputStream (
166+ file .toPath ()))));
167+ final NBTInputStream nbtInputStream = new NBTInputStream (inputStream )) {
168+ if (inputStream .readByte () != NBTConstants .TYPE_COMPOUND ) {
169+ return findByFileInExternalFormats (file );
170+ }
171+ final int rootNameTagLength = inputStream .readShort () & 0xFFFF ;
172+ if (rootNameTagLength != 0 && rootNameTagLength != 9 ) { // Only allow "" and "Schematic"
173+ return findByFileInExternalFormats (file );
174+ }
175+ final String rootName = new String (inputStream .readNBytes (rootNameTagLength ));
176+ if (rootName .isEmpty ()) {
177+ // Only FAST_V3 and MINECRAFT_STRUCTURE use empty named root compound tags
178+ // FAST_V3 only contains a single child component - if that's not present only MINECRAFT_STRUCTURE is possible
179+ do {
180+ byte type = inputStream .readByte ();
181+ if (type == NBTConstants .TYPE_END ) {
182+ return findByFileInExternalFormats (file );
183+ }
184+ String name = nbtInputStream .readNamedTagName (type );
185+ if (type == NBTConstants .TYPE_COMPOUND && name .equals ("Schematic" )) {
186+ // unwrap inner schematic compound for general processing below
187+ break ;
188+ }
189+ // search for almost all known compound children for a fast return path (lowercase is specific enough for now)
190+ if (type == NBTConstants .TYPE_LIST && (name .equals ("size" ) || name .equals ("palette" ) || name .equals ("blocks" ) || name .equals ("entities" ))) {
191+ return BuiltInClipboardFormat .MINECRAFT_STRUCTURE ;
192+ }
193+ nbtInputStream .readTagPayloadLazy (type , 0 ); // skip unwanted tags and continue search
194+ } while (true );
195+ }
196+
197+ do {
198+ byte type = inputStream .readByte ();
199+ if (type == NBTConstants .TYPE_END ) {
200+ return findByFileInExternalFormats (file );
201+ }
202+ String name = nbtInputStream .readNamedTagName (type );
203+ if ((type == NBTConstants .TYPE_BYTE_ARRAY && name .equals ("Blocks" )) || (type == NBTConstants .TYPE_STRING && name .equals ("Materials" ))) {
204+ return BuiltInClipboardFormat .MCEDIT_SCHEMATIC ;
205+ }
206+ if (type == NBTConstants .TYPE_INT && name .equals ("Version" )) {
207+ int version = inputStream .readInt ();
208+ return switch (version ) {
209+ case 1 -> BuiltInClipboardFormat .SPONGE_V1_SCHEMATIC ;
210+ case 2 -> BuiltInClipboardFormat .FAST_V2 ;
211+ case 3 -> BuiltInClipboardFormat .FAST_V3 ;
212+ default -> findByFileInExternalFormats (file );
213+ };
214+ }
215+ nbtInputStream .readTagPayloadLazy (type , 0 ); // skip unwanted tags and continue search
216+ } while (true );
217+ } catch (ZipException | EOFException ignored ) {
218+ // ignore gzip errors and EOFs - the file format might not use gzip, or we expected more data from known formats
219+ // all other builtin formats use gzip compression
220+ } catch (IOException e ) {
221+ // other IO errors (non gzip-related) should be logged
222+ LOGGER .error ("Failed determining clipboard format for file {}" , file .getAbsolutePath (), e );
223+ return null ;
224+ }
225+
226+ // no builtin format seems to match - test the remaining registered formats (added by other plugins, for example)
227+ return findByFileInExternalFormats (file );
228+ }
229+
230+ private static ClipboardFormat findByFileInExternalFormats (File file ) {
231+ if (registeredFormats .size () == FAST_SEARCH_BUILTIN_FORMATS .size ()) {
232+ return null ;
233+ }
125234 for (ClipboardFormat format : registeredFormats ) {
235+ if (FAST_SEARCH_BUILTIN_FORMATS .contains (format )) {
236+ continue ;
237+ }
126238 if (format .isFormat (file )) {
127239 return format ;
128240 }
129241 }
130-
131242 return null ;
132243 }
244+ //FAWE end
133245
134246 /**
135247 * A mapping from extensions to formats.
0 commit comments