diff --git a/.gitignore b/.gitignore index f1baa3a..1887b4c 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,7 @@ hs_err_pid* # Gradle .idea/**/gradle.xml .idea/**/libraries +.gradle/ # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, diff --git a/README.md b/README.md index 1eac0c7..a5c1684 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ ## Head Extractor -Head Extractor is a tool to extract the player profile from the player heads in a Minecraft world. +Head Extractor is a tool to extract the skin hashes from the player heads in a Minecraft world. This is accomplished somewhat inefficiently by searching chunk NBT, player data NBT, and entity NBT for lists of Compound tags that contain a String tag named `Value`.\ -In addition, `mcfunction` and `json` files in data packs are scanned for Base64 encoded player profiles. +In addition, `mcfunction` and `json` files in data packs are scanned for Base64 encoded player profiles. + +The skin hash is then extracted from the Base64 encoded player profile. ### Compiling 1. Clone the repository @@ -14,4 +16,4 @@ In addition, `mcfunction` and `json` files in data packs are scanned for Base64 ### Running `java -jar HeadExtractor--all.jar ` -Player profiles are sent line by line to standard output. +Skin hashes are saved to `custom-skulls.yml`. diff --git a/build.gradle.kts b/build.gradle.kts index 16d43d0..4b46b67 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,4 +24,5 @@ dependencies { implementation("com.fasterxml.jackson.core", "jackson-core", "2.14.1") implementation("com.fasterxml.jackson.core", "jackson-databind", "2.14.1") + implementation("com.fasterxml.jackson.dataformat", "jackson-dataformat-yaml", "2.14.1") } \ No newline at end of file diff --git a/src/main/java/me/amberichu/headextractor/HeadExtractor.java b/src/main/java/me/amberichu/headextractor/HeadExtractor.java index 18342c7..40620f1 100644 --- a/src/main/java/me/amberichu/headextractor/HeadExtractor.java +++ b/src/main/java/me/amberichu/headextractor/HeadExtractor.java @@ -24,8 +24,11 @@ package me.amberichu.headextractor; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.github.steveice10.opennbt.NBTIO; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.ListTag; @@ -52,6 +55,7 @@ public class HeadExtractor { private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); // Adapted from https://stackoverflow.com/a/475217 private static final Pattern BASE64_PATTERN = Pattern.compile("\\\\?[\"']((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))\\\\?[\"']"); @@ -61,8 +65,19 @@ public static void main(String[] args) throws IOException { System.out.println("Please specify one world folder."); System.exit(-1); } + Set heads = extractHeads(Path.of(args[0])); - heads.forEach(System.out::println); + YAML_MAPPER.writeValue(new File("custom-skulls.yml"), new SkinHashesConfig(heads)); + } + + static class SkinHashesConfig { + @JsonProperty("skin-hashes") + @JsonAlias("skin-hashes") + private Set skinHashes = new HashSet<>(); + + SkinHashesConfig(Set heads) { + this.skinHashes = heads; + } } private static Set extractHeads(Path worldPath) throws IOException { @@ -72,8 +87,9 @@ private static Set extractHeads(Path worldPath) throws IOException { List> tasks = new ArrayList<>(); Consumer headConsumer = head -> { - if (validateHead(head)) { - heads.add(head); + String url = validatedHeadHash(head); + if (url != null) { + heads.add(url); } }; @@ -178,7 +194,13 @@ private static void processMCA(Path mcaPath, Consumer headConsumer) { MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); buffer.order(ByteOrder.BIG_ENDIAN); for (int i = 0; i < 1024; i++) { - int location = buffer.getInt(4 * i); + int location; + try { + location = buffer.getInt(4 * i); + } catch (IndexOutOfBoundsException e) { + // Chunk is empty + continue; + } if (location == 0) { // Chunk is not present continue; @@ -240,27 +262,36 @@ private static void processString(String string, Consumer headConsumer) } } - private static boolean validateHead(String head) { + private static String validatedHeadHash(String head) { try { JsonNode node = MAPPER.readTree(Base64.getDecoder().decode(head)); if (!node.isObject()) { - return false; + return null; } JsonNode textures = node.get("textures"); if (textures == null || !textures.isObject()) { - return false; + return null; } JsonNode skin = textures.get("SKIN"); if (skin == null || !textures.isObject()) { - return false; + return null; } JsonNode url = skin.get("url"); - return url != null && url.isTextual(); + if (url != null && url.isTextual()) { + String hashPattern = "/([a-fA-F0-9]+)$"; + Pattern pattern = Pattern.compile(hashPattern); + Matcher matcher = pattern.matcher(url.asText()); + if (matcher.find()) { + return matcher.group(1); + } + } + + return null; } catch (Exception e) { - return false; + return null; } } } \ No newline at end of file