fix(validation): reduce log spam + add OOM guard + check correct mesh for WEIGHTS_0

- DataDrivenItemParser: downgrade animation_bones absence log from INFO to
  DEBUG (was spamming for every item without the optional field)
- GlbValidator: read 12-byte GLB header first and reject files declaring
  totalLength > 50 MB before allocating (prevents OOM on malformed GLBs)
- GlbValidator: WEIGHTS_0 check now uses the same mesh selection logic as
  GlbParser (prefer "Item", fallback to last non-Player) instead of
  blindly checking the first mesh
This commit is contained in:
NotEvil
2026-04-17 02:12:47 +02:00
parent 3f6e04edb0
commit 9dfd2d1724
3 changed files with 88 additions and 36 deletions

View File

@@ -92,6 +92,9 @@ public final class GlbValidator {
// Header + JSON chunk extraction //
// ------------------------------------------------------------------ //
/** Maximum GLB file size the validator will accept (50 MB). */
private static final long MAX_GLB_SIZE = 50L * 1024 * 1024;
/**
* Parse the GLB header and extract the JSON chunk root object.
* Returns null and adds ERROR diagnostics on failure.
@@ -101,20 +104,21 @@ public final class GlbValidator {
ResourceLocation source,
List<GlbDiagnostic> diagnostics
) throws Exception {
byte[] allBytes = input.readAllBytes();
if (allBytes.length < 12) {
// Read the 12-byte header first to check totalLength before
// committing to readAllBytes (OOM guard for malformed GLBs).
byte[] header = input.readNBytes(12);
if (header.length < 12) {
diagnostics.add(new GlbDiagnostic(
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
"File too small to contain a GLB header (" + allBytes.length + " bytes)"
"File too small to contain a GLB header (" + header.length + " bytes)"
));
return null;
}
ByteBuffer buf = ByteBuffer.wrap(allBytes).order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer headerBuf = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
// -- Header (12 bytes) --
int magic = buf.getInt();
int magic = headerBuf.getInt();
if (magic != GLB_MAGIC) {
diagnostics.add(new GlbDiagnostic(
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
@@ -123,7 +127,7 @@ public final class GlbValidator {
return null;
}
int version = buf.getInt();
int version = headerBuf.getInt();
if (version != GLB_VERSION) {
diagnostics.add(new GlbDiagnostic(
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
@@ -132,8 +136,31 @@ public final class GlbValidator {
return null;
}
@SuppressWarnings("unused")
int totalLength = buf.getInt();
int totalLength = headerBuf.getInt();
// OOM guard: reject files that declare a size exceeding the cap.
// totalLength is a signed int, so treat negative values as > 2 GB.
if (totalLength < 0 || Integer.toUnsignedLong(totalLength) > MAX_GLB_SIZE) {
diagnostics.add(new GlbDiagnostic(
source, null, Severity.ERROR, "GLB_TOO_LARGE",
"GLB declares totalLength=" + Integer.toUnsignedLong(totalLength)
+ " bytes which exceeds the " + (MAX_GLB_SIZE / (1024 * 1024))
+ " MB safety cap — aborting validation"
));
return null;
}
// Now read the remainder (totalLength includes the 12-byte header)
int remaining = totalLength - 12;
if (remaining < 0) {
diagnostics.add(new GlbDiagnostic(
source, null, Severity.ERROR, "INVALID_GLB_HEADER",
"GLB totalLength (" + totalLength + ") is smaller than the header itself"
));
return null;
}
byte[] restBytes = input.readNBytes(remaining);
ByteBuffer buf = ByteBuffer.wrap(restBytes).order(ByteOrder.LITTLE_ENDIAN);
// -- First chunk must be JSON --
if (buf.remaining() < 8) {
@@ -285,22 +312,42 @@ public final class GlbValidator {
));
}
// Check WEIGHTS_0 on the first mesh's first primitive
if (meshes.size() > 0) {
JsonObject firstMesh = meshes.get(0).getAsJsonObject();
if (firstMesh.has("primitives")) {
JsonArray primitives = firstMesh.getAsJsonArray("primitives");
if (primitives.size() > 0) {
JsonObject firstPrimitive = primitives.get(0).getAsJsonObject();
if (firstPrimitive.has("attributes")) {
JsonObject attributes = firstPrimitive.getAsJsonObject("attributes");
if (!attributes.has("WEIGHTS_0")) {
diagnostics.add(new GlbDiagnostic(
source, null, Severity.WARNING, "NO_WEIGHTS",
"First mesh's first primitive has no WEIGHTS_0 attribute "
+ "— skinning will not work correctly"
));
}
// Check WEIGHTS_0 on the mesh that GlbParser would actually select:
// 1) mesh named "Item", 2) last non-Player mesh.
JsonObject targetMesh = null;
String targetMeshName = null;
for (int mi = 0; mi < meshes.size(); mi++) {
JsonObject mesh = meshes.get(mi).getAsJsonObject();
String meshName = mesh.has("name")
? mesh.get("name").getAsString()
: "";
if ("Item".equals(meshName)) {
targetMesh = mesh;
targetMeshName = meshName;
break; // Convention match — same as GlbParser
}
if (!"Player".equals(meshName)) {
targetMesh = mesh;
targetMeshName = meshName;
}
}
if (targetMesh != null && targetMesh.has("primitives")) {
JsonArray primitives = targetMesh.getAsJsonArray("primitives");
if (primitives.size() > 0) {
JsonObject firstPrimitive = primitives.get(0).getAsJsonObject();
if (firstPrimitive.has("attributes")) {
JsonObject attributes = firstPrimitive.getAsJsonObject("attributes");
if (!attributes.has("WEIGHTS_0")) {
String meshLabel = targetMeshName != null
? "'" + targetMeshName + "'"
: "(unnamed)";
diagnostics.add(new GlbDiagnostic(
source, null, Severity.WARNING, "NO_WEIGHTS",
"Selected mesh " + meshLabel
+ " first primitive has no WEIGHTS_0 attribute "
+ "— skinning will not work correctly"
));
}
}
}

View File

@@ -267,7 +267,7 @@ public final class DataDrivenItemParser {
if (!root.has("animation_bones")) {
// Not an error — absent means "permissive" (all owned bones, all animations)
animationBones = null;
LOGGER.info(
LOGGER.debug(
"[DataDrivenItems] {}: animation_bones not declared — all owned bones enabled for all animations",
fileId
);