{"record":{"id":"054468e74c613363","repo":"Anuken/Mindustry","slug":"could-not-skip-bytes-expected-length-actual","errorCode":null,"errorMessage":"Could not skip bytes. Expected length: {}; Actual length: {}","messagePattern":"Could not skip bytes\\. Expected length: (.+?); Actual length: (.+?)","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"core/src/mindustry/io/SaveFileReader.java","lineNumber":154,"sourceCode":"        int length = input.readInt();\n        runner.accept(input, length);\n        return length;\n    }\n\n    /** Reads a chunk of some length. Use the runner for reading to catch more descriptive errors. */\n    public int readChunkReads(DataInput input, IORunnerLength<Reads> runner) throws IOException{\n        return readChunk(input, (in, length) -> {\n            chunkReads.input = in;\n            runner.accept(chunkReads, length);\n        });\n    }\n\n    /** Skip a chunk completely, discarding the bytes. */\n    public void skipChunk(DataInput input) throws IOException{\n        int length = readChunk(input, (t, len) -> {});\n        int skipped = input.skipBytes(length);\n        if(length != skipped){\n            throw new IOException(\"Could not skip bytes. Expected length: \" + length + \"; Actual length: \" + skipped);\n        }\n    }\n\n    /** Reads a legacy chunk where the length is only 2 bytes. */\n    public int readLegacyShortChunk(DataInput input, IORunnerLength<Reads> runner) throws IOException{\n        int length = input.readUnsignedShort();\n        chunkReads.input = input;\n        runner.accept(chunkReads, length);\n        return length;\n    }\n\n    /** Skip a legacy chunk completely, discarding the bytes. */\n    public void skipLegacyShortChunk(DataInput input) throws IOException{\n        int length = readLegacyShortChunk(input, (t, len) -> {});\n        int skipped = input.skipBytes(length);\n        if(length != skipped){\n            throw new IOException(\"Could not skip bytes. Expected length: \" + length + \"; Actual length: \" + skipped);\n        }","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/Anuken/Mindustry/blob/f695ad7e60323ebced984fa26d0bcf0bc54296b4/core/src/mindustry/io/SaveFileReader.java#L136-L172","documentation":"Thrown by SaveFileReader.skipChunk after reading a 4-byte chunk length and calling DataInput.skipBytes(length). The Java DataInput contract allows skipBytes to skip fewer bytes than requested (it is best-effort and returns the number actually skipped); this guard fires when the returned count does not match the declared chunk length. The mismatch almost always means the underlying stream is truncated or the chunk header is corrupt rather than a transient skip failure.","triggerScenarios":"Called from SaveVersion.readMap (SaveVersion.java:373) when a tile had an entity in the save but the block no longer has Building IO code, so the entity region must be discarded. Also fires anywhere skipChunk is invoked on a stream whose actual remaining bytes are fewer than the length prefix indicates.","commonSituations":"Loading a save from an incompatible game version where chunk layout shifted; a truncated .msav (disk write interrupted / partial download); a corrupt deflated stream that decompressed to fewer bytes than the length prefix claimed; manual editing of a save file that left the chunk length stale.","solutions":["Treat the save as corrupt: call SaveIO.isSaveValid(file) first, and on failure fall back to the auto-generated backup via SaveIO.getBackupStream / SaveIO.load(file) which already retries the -backup file.","If loading a custom DataInput, verify the source stream has at least length bytes remaining before invoking skipChunk (use a CounterInputStream or available()/mark).","Re-save the game in the current version so the chunk layout matches, instead of loading an older/cross-version file.","If you control the writer, ensure writeChunk always writes a length prefix that exactly equals the buffered output size (it does in SaveFileReader.writeChunk)."],"exampleFix":"// before\ntry {\n    SaveIO.load(file);\n} catch (SaveException e) {\n    // surface raw error\n}\n\n// after\nif (SaveIO.isSaveValid(file)) {\n    SaveIO.load(file);\n} else {\n    ui.showInfo(\"This save file is corrupt or from an incompatible version.\");\n}","handlingStrategy":"validation","validationCode":"// Before loading, confirm the save parses end-to-end.\nif (!SaveIO.isSaveValid(file)) {\n    throw new IllegalArgumentException(\"Save file is corrupt or truncated: \" + file);\n}\n// SaveIO.load(Fi) already retries the -backup file on SaveException.","typeGuard":"null","tryCatchPattern":"try {\n    SaveIO.load(file);\n} catch (SaveException e) {\n    // both primary and backup failed; report to user\n    ui.showInfo(bundle.get(\"save.corrupt\"));\n}","preventionTips":["Always load via SaveIO.load(Fi) so the auto-backup retry applies.","Call SaveIO.isSaveValid before offering a Load button on a slot.","Never hand-edit chunk length prefixes in save files."],"tags":["save-io","deserialization","data-corruption","stream"],"backgroundTag":null,"analyzedSha":"f695ad7e60323ebced984fa26d0bcf0bc54296b4","analyzedAt":"2026-08-14T04:31:16.262Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}