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