-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add class for server favicon parsing
- Loading branch information
1 parent
9fdf877
commit 0131c08
Showing
8 changed files
with
267 additions
and
1 deletion.
There are no files selected for viewing
125 changes: 125 additions & 0 deletions
125
minecord-bot/src/main/java/com/tisawesomeness/minecord/mc/Favicon.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package com.tisawesomeness.minecord.mc; | ||
|
||
import com.tisawesomeness.minecord.common.util.Either; | ||
import com.tisawesomeness.minecord.util.type.Dimensions; | ||
import lombok.*; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.DataInput; | ||
import java.io.DataInputStream; | ||
import java.util.Base64; | ||
import java.util.Optional; | ||
import java.util.regex.Pattern; | ||
|
||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) | ||
public class Favicon { | ||
|
||
/** The expected width and height of a favicon. Older clients may not display favicons with different sizes. */ | ||
public static final int EXPECTED_SIZE = 64; | ||
|
||
private static final String PREAMBLE = "data:image/png;base64,"; | ||
private static final Pattern NEWLINES_PATTERN = Pattern.compile("[\r\n]"); | ||
private static final int MIN_LENGTH = 24; | ||
private static final long PNG_SIGNATURE = 0x89_504E47_0D0A_1A_0AL; | ||
private static final int IHDR_LENGTH = 13; | ||
private static final int IHDR_TYPE = ('I' << 24) + ('H' << 16) + ('D' << 8) + 'R'; | ||
|
||
/** The image data for the favicon. */ | ||
@Getter private final byte[] data; | ||
private final boolean usesNewlines; | ||
|
||
/** | ||
* Creates a favicon from image bytes. | ||
* @param data byte array representing a raw PNG image | ||
* @return new favicon | ||
*/ | ||
public static Favicon from(byte[] data) { | ||
return new Favicon(data, false); | ||
} | ||
|
||
/** | ||
* Parses a base64-encoded favicon, according to the server ping | ||
* <a href="https://wiki.vg/Server_List_Ping#Status_Response">specification</a>. Newlines are accepted and trimmed | ||
* before parsing, use {@link #usesNewlines()} to check if newlines were used. | ||
* @param str the string to parse | ||
* @return the favicon, or empty if the input is malformed | ||
*/ | ||
public static Optional<Favicon> parse(@NonNull String str) { | ||
if (!str.startsWith(PREAMBLE)) { | ||
return Optional.empty(); | ||
} | ||
String imageData = str.substring(PREAMBLE.length()); | ||
boolean usesNewlines = false; | ||
if (imageData.indexOf('\r') == -1 || imageData.indexOf('\n') == -1) { | ||
imageData = NEWLINES_PATTERN.matcher(imageData).replaceAll(""); | ||
usesNewlines = true; | ||
} | ||
try { | ||
byte[] decoded = Base64.getDecoder().decode(imageData); | ||
return Optional.of(new Favicon(decoded, usesNewlines)); | ||
} catch (IllegalArgumentException ignored) { | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
/** | ||
* If this favicon was created through {@link #parse(String)}, returns whether newlines were in the original input. | ||
* Favicons with newlines no longer work since 1.13. | ||
* @return whether newlines were used to create this favicon | ||
*/ | ||
public boolean usesNewlines() { | ||
return usesNewlines; | ||
} | ||
|
||
/** | ||
* Checks if the favicon is a valid PNG image. If the PNG can be read, returns the width and height. | ||
* This method only checks if a PNG can be <strong>read</strong>, not necessarily displayed. | ||
* <br>This method can return any {@link PngError PngError}. | ||
* @return the image dimensions if the PNG is valid, or a {@link PngError PngError} otherwise | ||
*/ | ||
@SneakyThrows // IOE, not possible with ByteArrayInputStream | ||
public Either<PngError, Dimensions> validate() { | ||
if (data.length < MIN_LENGTH) { | ||
return Either.left(PngError.TOO_SHORT); | ||
} | ||
DataInput is = new DataInputStream(new ByteArrayInputStream(data)); | ||
if (is.readLong() != PNG_SIGNATURE) { | ||
return Either.left(PngError.BAD_SIGNATURE); | ||
} | ||
if (is.readInt() != IHDR_LENGTH) { | ||
return Either.left(PngError.BAD_IHDR_LENGTH); | ||
} | ||
if (is.readInt() != IHDR_TYPE) { | ||
return Either.left(PngError.BAD_IHDR_TYPE); | ||
} | ||
int width = is.readInt(); | ||
if (width < 0) { | ||
return Either.left(PngError.NEGATIVE_WIDTH); | ||
} | ||
int height = is.readInt(); | ||
if (height < 0) { | ||
return Either.left(PngError.NEGATIVE_WIDTH); | ||
} | ||
return Either.right(new Dimensions(width, height)); | ||
} | ||
|
||
/** | ||
* An error that can occur when validating a PNG image. See the | ||
* <a href="https://en.wikipedia.org/wiki/PNG#File_format">Wikipedia page</a> for more details. | ||
*/ | ||
public enum PngError { | ||
/** Image data is too small to be a valid PNG */ | ||
TOO_SHORT, | ||
/** Does not contain PNG file header */ | ||
BAD_SIGNATURE, | ||
/** IHDR chunk length is invalid */ | ||
BAD_IHDR_LENGTH, | ||
/** First PNG chunk is not IHDR */ | ||
BAD_IHDR_TYPE, | ||
/** Width of PNG is negative (or overflow) */ | ||
NEGATIVE_WIDTH, | ||
/** Height of PNG is negative (or overflow) */ | ||
NEGATIVE_HEIGHT | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
minecord-bot/src/main/java/com/tisawesomeness/minecord/util/type/Dimensions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.tisawesomeness.minecord.util.type; | ||
|
||
import lombok.Value; | ||
|
||
@Value | ||
public class Dimensions { | ||
int width; | ||
int height; | ||
|
||
/** | ||
* Creates a new Dimensions object | ||
* @param width width | ||
* @param height height | ||
* @throws IllegalArgumentException if width or height are negative | ||
*/ | ||
public Dimensions(int width, int height) { | ||
if (width < 0) { | ||
throw new IllegalArgumentException("width cannot be negative but was " + width); | ||
} | ||
if (height < 0) { | ||
throw new IllegalArgumentException("height cannot be negative but was " + height); | ||
} | ||
this.width = width; | ||
this.height = height; | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
minecord-bot/src/test/java/com/tisawesomeness/minecord/mc/FaviconTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package com.tisawesomeness.minecord.mc; | ||
|
||
import com.tisawesomeness.minecord.common.util.IO; | ||
import com.tisawesomeness.minecord.util.Strings; | ||
import com.tisawesomeness.minecord.util.type.Dimensions; | ||
import org.assertj.core.api.InstanceOfAssertFactories; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.util.Optional; | ||
|
||
import static com.tisawesomeness.minecord.testutil.assertion.CustomAssertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
public class FaviconTest { | ||
|
||
private static final String HYPIXEL_FAVICON = IO.loadResource("hypixelFavicon.txt", FaviconTest.class); | ||
|
||
@Test | ||
public void testFromEmpty() { | ||
Favicon icon = Favicon.from(new byte[0]); | ||
assertThat(icon.getData()).isEmpty(); | ||
assertThat(icon.usesNewlines()).isFalse(); | ||
assertThat(icon.validate()).asLeft().isEqualTo(Favicon.PngError.TOO_SHORT); | ||
} | ||
|
||
@Test | ||
public void testParse() { | ||
Optional<Favicon> iconOpt = Favicon.parse(HYPIXEL_FAVICON); | ||
assertThat(iconOpt).isNotEmpty(); | ||
Favicon icon = iconOpt.get(); | ||
assertThat(icon.validate()) | ||
.asRight(InstanceOfAssertFactories.type(Dimensions.class)) | ||
.extracting(Dimensions::getWidth, Dimensions::getHeight) | ||
.containsExactly(Favicon.EXPECTED_SIZE, Favicon.EXPECTED_SIZE); | ||
} | ||
@Test | ||
public void testParseEmpty() { | ||
assertThat(Favicon.parse("")).isEmpty(); | ||
} | ||
@Test | ||
public void testParseInvalid() { | ||
assertThat(Favicon.parse("data:image/png;base64,!")).isEmpty(); | ||
} | ||
@Test | ||
public void testParseBadSignature() { | ||
String modified = Strings.replaceCharAt(HYPIXEL_FAVICON, 22, 'j'); | ||
Optional<Favicon> iconOpt = Favicon.parse(modified); | ||
assertThat(iconOpt).isNotEmpty(); | ||
Favicon icon = iconOpt.get(); | ||
assertThat(icon.validate()) | ||
.asLeft() | ||
.isEqualTo(Favicon.PngError.BAD_SIGNATURE); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
minecord-bot/src/test/java/com/tisawesomeness/minecord/util/type/DimensionsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.tisawesomeness.minecord.util.type; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
||
public class DimensionsTest { | ||
|
||
@Test | ||
public void test() { | ||
assertThat(new Dimensions(2, 1)) | ||
.extracting(Dimensions::getWidth, Dimensions::getHeight) | ||
.containsExactly(2, 1); | ||
} | ||
@Test | ||
public void testZero() { | ||
assertThat(new Dimensions(0, 0)) | ||
.extracting(Dimensions::getWidth, Dimensions::getHeight) | ||
.containsExactly(0, 0); | ||
} | ||
@Test | ||
public void testNegative() { | ||
assertThatThrownBy(() -> new Dimensions(1, -1)) | ||
.isInstanceOf(IllegalArgumentException.class); | ||
} | ||
@Test | ||
public void testNegative2() { | ||
assertThatThrownBy(() -> new Dimensions(-1, 1)) | ||
.isInstanceOf(IllegalArgumentException.class); | ||
} | ||
|
||
} |
Oops, something went wrong.