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

  1. Compare the reported byte with the Java serialization tag table to identify what is actually encoded there.
  2. Verify the top-level serialized type in the FreeOTP export is java.util.HashMap as the parser requires.
  3. Re-export from FreeOTP and check the file is not truncated — corruption right after the header produces this.
  4. 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

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


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)