beemdevelopment/Aegis · error · ParseException
Expected a class desc, found:
Error message
Expected a class desc, found:
What it means
FreeOtpImporter.parse expects the byte after TC_OBJECT to be TC_CLASSDESC (0x72), the class-descriptor marker for the serialized HashMap. When the byte differs, it throws this ParseException including the unexpected byte value, indicating the serialized stream does not follow the exact object/classdesc ordering the manual parser implements.
Solutions
- Compare the reported byte with the Java serialization tag table to identify what is actually encoded there.
- Verify the top-level serialized type in the FreeOTP export is java.util.HashMap as the parser requires.
- Re-export from FreeOTP and check the file is not truncated — corruption right after the header produces this.
- Align FreeOTP and Aegis versions so the serialization layout matches what the importer supports.
Example fix
// before: a stream whose top-level object is not HashMap
Object root = someNonHashMapSerializedStream;
// after: ensure the export is the expected HashMap token store
byte[] data = readAllBytes(f);
if (!isHashMapSerializedStore(data)) { // checks TC_OBJECT then TC_CLASSDESC with java.util.HashMap
throw new ParseException("FreeOTP export does not contain the expected HashMap structure");
} Defensive patterns
Strategy: validation
Validate before calling
static void validateClassDescTag(byte[] data) {
// header(4) + TC_OBJECT(1) must be followed by TC_CLASSDESC (0x72)
if (data.length >= 6 && data[5] != 0x72) {
throw new ParseException(String.format("Expected TC_CLASSDESC, found 0x%02X at offset 5", data[5]));
}
} Type guard
static boolean hasClassDescAfterObject(byte[] data) {
return data != null && data.length >= 6 && data[4] == 0x73 && data[5] == 0x72;
} Try / catch
try {
importer.parse(stream);
} catch (ParseException e) {
if (e.getMessage().startsWith("Expected a class desc, found:")) {
// top-level object is not backed by a plain class descriptor (e.g. TC_NULL/TC_REFERENCE)
throw new ImportException("Unsupported FreeOTP serialization layout", e);
}
} Prevention
- Validate the byte pattern AC ED 00 05 73 72 before parsing.
- Confirm the top-level serialized type is java.util.HashMap, not a wrapper or null.
- Check for truncation/corruption right after the header when files arrive over unreliable channels.
- Regenerate the export rather than attempting to repair a corrupt stream.
When it happens
Trigger: A Java-serialized stream where the object tag is followed by something other than a class descriptor — e.g. TC_NULL, TC_REFERENCE, TC_PROXYCLASSDESC, or an object written with custom serialization that omits the classdesc — or byte corruption between the two tags.
Common situations: FreeOTP exports written by a different JVM serialization configuration, streams that serialize a non-HashMap top-level type, or files corrupted in transit (partial download, encoding conversion like base64 round-trips gone wrong).
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- Not a serialized Java Object
- Expected an object, found:
- Unexpected class name
- Unable to find nonce in parameters
- Unable to decode stream to bitmap
AI-assisted analysis of beemdevelopment/Aegis@d6f4e5925a (2026-09-08).
Data as JSON: /api/errors/e14b1e45b011a83f.
Report an issue: GitHub.
Appendix: source
Thrown at app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java:388
public static Map<String, String> parse(DataInputStream inStream)
throws IOException, ParseException {
Map<String, String> map = new HashMap<>();
// Read/validate the magic number and version
int magic = inStream.readUnsignedShort();
int version = inStream.readUnsignedShort();
if (magic != MAGIC || version != VERSION) {
throw new ParseException("Not a serialized Java Object");
}
// Read the class descriptor info for HashMap
byte b = inStream.readByte();
if (b != TC_OBJECT) {
throw new ParseException("Expected an object, found: " + b);
}
b = inStream.readByte();
if (b != TC_CLASSDESC) {
throw new ParseException("Expected a class desc, found: " + b);
}
parseClassDescriptor(inStream);
// Not interested in the capacity of the map
inStream.readInt();
// Read the number of elements in the HashMap
int size = inStream.readInt();
// Parse each key-value pair in the map
for (int i = 0; i < size; i++) {
String key = parseStringObject(inStream);
String value = parseStringObject(inStream);
map.put(key, value);
}
return map;
}
View on GitHub (pinned to d6f4e5925a)