Volver al índice

src/main/java/ar/com/companeros/identity/SkinSources.java

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"); }
}