{"record":{"id":"3801dd7e52ed2547","repo":"HMCL-dev/HMCL","slug":"protected-payload-lane-is-not-a-string","errorCode":null,"errorMessage":"Protected payload lane is not a string","messagePattern":"Protected payload lane is not a string","errorType":"validation","errorClass":"JsonParseException","httpStatus":null,"severity":"error","filePath":"HMCL/src/main/java/org/jackhuang/hmcl/setting/ProtectedPayload.java","lineNumber":196,"sourceCode":"            /// Joins Base64 payload lanes from the envelope.\n            ///\n            /// @param envelope the envelope object to read from\n            /// @return the restored Base64 payload\n            /// @throws JsonParseException if the payload lanes are missing or malformed\n            private static String joinObfuscatedPayload(JsonObject envelope) {\n                if (!(envelope.get(PROPERTY_PAYLOAD) instanceof JsonArray lanes)\n                        || lanes.size() < OBFUSCATED_LANE_COUNT) {\n                    throw new JsonParseException(\"Missing payload or payload array is too small\");\n                }\n\n                int effectivePayloadSize = Integer.highestOneBit(lanes.size());\n                String[] laneTexts = new String[OBFUSCATED_LANE_COUNT];\n                int totalLength = 0;\n                for (int i = 0; i < OBFUSCATED_LANE_COUNT; i++) {\n                    int payloadIndex = lanePayloadIndex(i, effectivePayloadSize);\n                    JsonElement lane = lanes.get(payloadIndex);\n                    if (!lane.isJsonPrimitive() || !lane.getAsJsonPrimitive().isString()) {\n                        throw new JsonParseException(\"Protected payload lane is not a string\");\n                    }\n\n                    laneTexts[i] = lane.getAsString();\n                    totalLength += laneTexts[i].length();\n                }\n\n                StringBuilder result = new StringBuilder(totalLength);\n                for (String laneText : laneTexts) {\n                    result.append(laneText);\n                }\n                return result.toString();\n            }\n\n            /// Writes the payload into the given envelope.\n            @Override\n            protected void writePayload(JsonObject envelope, JsonElement payload) {\n                byte[] nonce = new byte[NONCE_SIZE];\n                SECURE_RANDOM.nextBytes(nonce);","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/HMCL-dev/HMCL/blob/24702dc5a0214034f4c27166d5fd30cad08cec19/HMCL/src/main/java/org/jackhuang/hmcl/setting/ProtectedPayload.java#L178-L214","documentation":"OBFUSCATED_V1.joinObfuscatedPayload throws this when one of the 4 lane elements at the computed payload indexes is not a JSON string primitive. Lanes are written as strings (Base64 slices) with JsonNull padding; a non-string at a lane index means the envelope was corrupted or produced by an incompatible writer.","triggerScenarios":"Calling ProtectedPayload.read on an 'hmcl-obfuscated-v1' envelope where lanes.get(lanePayloadIndex(i, effectiveSize)) yields JsonNull, a number, boolean, object, or array instead of a string — i.e. padding leaked into a lane slot or lanes were rewritten with wrong types.","commonSituations":"Hand-editing or programmatic re-serialization that replaced lane strings with nulls; re-encoding the array with wrong element ordering/size so lane indexes land on padding; third-party tools transforming the config JSON.","solutions":["Restore the original envelope (backup or re-download the config) so lane slots hold their string values.","Do not modify the null-padded 256-element array; if editing is needed, use 'plain' protection instead.","Regenerate the setting through HMCL so the obfuscated envelope is rewritten correctly."],"exampleFix":"// before: lane slot overwritten with null\n[... 63 nulls, null, ...]  // lane index holds null\n// after: lane slot holds its Base64 string\n[... 63 nulls, \"QmFzZTY0\", ...]","handlingStrategy":"validation","validationCode":"if (envelope.get(\"payload\") instanceof com.google.gson.JsonArray lanes && lanes.size() >= 4) {\n    for (int i = 0; i < 4; i++) {\n        int idx = (i + 1) * (Integer.highestOneBit(lanes.size()) / 4) - 1;\n        com.google.gson.JsonElement lane = lanes.get(idx);\n        if (!lane.isJsonPrimitive() || !lane.getAsJsonPrimitive().isString()) {\n            throw new IllegalStateException(\"Lane \" + i + \" at index \" + idx + \" is not a string\");\n        }\n    }\n}","typeGuard":"static boolean lanesAreStrings(com.google.gson.JsonObject envelope) {\n    if (!(envelope.get(\"payload\") instanceof com.google.gson.JsonArray lanes)) return false;\n    int size = Integer.highestOneBit(lanes.size());\n    for (int i = 0; i < 4; i++) {\n        com.google.gson.JsonElement lane = lanes.get((i + 1) * (size / 4) - 1);\n        if (!lane.isJsonPrimitive() || !lane.getAsJsonPrimitive().isString()) return false;\n    }\n    return true;\n}","tryCatchPattern":"try {\n    return ProtectedPayload.read(envelope, JsonObject.class);\n} catch (com.google.gson.JsonParseException e) {\n    logger.warning(\"Corrupted payload lanes; restoring from backup\", e);\n    return readFromBackupOrDefaults();\n}","preventionTips":["Treat the obfuscated payload array as opaque; never rewrite its element types.","Use 'plain' protection if you need human-editable config values.","Round-trip config files through JSON serializers carefully — null-padded arrays are easily damaged.","Verify envelope integrity after sync/merge operations."],"tags":["json","envelope-format","type-mismatch"],"backgroundTag":"type-mismatch","analyzedSha":"24702dc5a0214034f4c27166d5fd30cad08cec19","analyzedAt":"2026-09-10T12:36:46.680Z","contentChangedAt":"2026-09-10T12:36:46.680Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}