package ar.com.companeros.identity; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Locale; import java.util.UUID; /** Identificadores de cuentas y texturas oficiales; jamás rutas, credenciales ni URLs arbitrarias. */ public final class SkinSources { private SkinSources() { } // Steve viene con Minecraft. El operador puede elegirlo sin resolver ninguna cuenta en la web. public static String parasiteOverride(String choice) { if (choice == null) throw invalid(); return switch (choice.toLowerCase(Locale.ROOT)) { case "steve", "default" -> "steve"; case "host" -> ""; default -> throw invalid(); }; } public static String parasiteSource(String override, UUID host, String hostTexture) { if ("steve".equals(override)) return "steve"; if (!"".equals(override)) throw invalid(); if (hostTexture != null && !hostTexture.isEmpty()) return texture(hostTexture); // La infección natural conserva incluso la skin vanilla asignada por UUID al bot. return host == null ? "steve" : host.toString(); } // Una cuenta puede cambiar la apariencia sin cambiar la identidad del bot. public static String profile(String input) { if (input == null || input.length() > 36 || !input.equals(input.strip())) throw invalid(); if (input.equalsIgnoreCase("default")) return "default"; if (input.matches("[A-Za-z0-9_]{1,16}")) return input; if (input.matches("[a-fA-F0-9]{32}")) return uuid(input).toString(); if (input.matches("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}")) return UUID.fromString(input).toString(); throw invalid(); } public static UUID uuid(String input) { if (input == null || !input.matches("[a-fA-F0-9]{32}")) throw invalid(); return UUID.fromString(input.substring(0, 8) + "-" + input.substring(8, 12) + "-" + input.substring(12, 16) + "-" + input.substring(16, 20) + "-" + input.substring(20)); } // Mojang todavía puede devolver HTTP en el documento firmado. El transporte siempre usa HTTPS. public static String texture(String value) { if (value == null || value.length() > 160) throw invalid(); URI uri; try { uri = URI.create(value); } catch (RuntimeException rejected) { throw invalid(); } if ((!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme())) || !"textures.minecraft.net".equals(uri.getHost()) || uri.getPort() != -1 || uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null || !uri.getRawPath().matches("/texture/[a-fA-F0-9]{64}")) throw invalid(); return "https://textures.minecraft.net" + uri.getRawPath().toLowerCase(Locale.ROOT); } public static String textureFromProperty(String property) { if (property == null || property.length() > 12_000) throw invalid(); byte[] decoded; try { decoded = Base64.getDecoder().decode(property); } catch (RuntimeException rejected) { throw invalid(); } if (decoded.length > 8_192) throw invalid(); try { JsonObject root = JsonParser.parseString(new String(decoded, StandardCharsets.UTF_8)).getAsJsonObject(); return texture(root.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString()); } catch (RuntimeException rejected) { throw invalid(); } } // Verificar IHDR antes de decodificar evita imágenes comprimidas con dimensiones desmedidas. public static boolean playerPng(byte[] bytes) { if (bytes == null || bytes.length < 33 || bytes.length > 131_072) return false; byte[] signature = {(byte)137, 80, 78, 71, 13, 10, 26, 10}; for (int index = 0; index < signature.length; index++) if (bytes[index] != signature[index]) return false; if (bytes[12] != 'I' || bytes[13] != 'H' || bytes[14] != 'D' || bytes[15] != 'R') return false; int width = java.nio.ByteBuffer.wrap(bytes, 16, 8).getInt(); int height = java.nio.ByteBuffer.wrap(bytes, 20, 4).getInt(); return width == 64 && (height == 64 || height == 32); } private static IllegalArgumentException invalid() { return new IllegalArgumentException("Cuenta o textura de Minecraft inválida"); } }