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 ;
4451import java .net .URL ;
4552import java .nio .channels .Channels ;
4653import java .nio .channels .ReadableByteChannel ;
54+ import java .nio .charset .StandardCharsets ;
4755import java .util .ArrayList ;
4856import java .util .Arrays ;
4957import java .util .Collection ;
5563import java .util .Locale ;
5664import java .util .Map ;
5765import java .util .Map .Entry ;
66+ import java .util .Set ;
5867import java .util .regex .Pattern ;
68+ import java .util .zip .GZIPInputStream ;
5969import java .util .zip .ZipEntry ;
70+ import java .util .zip .ZipException ;
6071import java .util .zip .ZipInputStream ;
6172
6273import 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.
0 commit comments