beemdevelopment/Aegis · error · ParseException

Unexpected class name

Error message

Unexpected class name: %s

What it means

parseClassDescriptor reads the UTF class name from the Java-serialized class descriptor and requires it to be exactly java.util.HashMap; any other class name triggers this ParseException with the found name. The FreeOTP importer only supports the specific HashMap-at-top-level serialization layout, so unexpected classes mean the file is not the token store format it can decode.

Solutions

  1. Check the class name printed in the message — it tells you what the file actually contains.
  2. Re-export the token store from FreeOTP, making sure to pick the token backup, not another serialized file.
  3. Ensure the FreeOTP version matches the format Aegis's importer expects (update Aegis if FreeOTP is newer).
  4. Verify the class descriptor's serialVersionUID also matches after fixing the class name — the parser checks it next.

Example fix

// before: importing a wrong serialized file
File f = new File("freeotp_prefs.bin"); // serializes SharedPreferences, not HashMap
// after: select the actual token store
File f = new File("freeotp_tokens.bin"); // serializes java.util.HashMap
def parseTokenStore(File f) { parse(f); }
Defensive patterns

Strategy: validation

Validate before calling

static String peekSerializedClassName(byte[] data) {
    // header(4) + TC_OBJECT + TC_CLASSDESC + 2-byte length -> then UTF class name
    if (data == null || data.length < 8) return null;
    int nameLen = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
    if (data.length < 8 + nameLen) return null;
    return new String(data, 8, nameLen, StandardCharsets.UTF_8);
}
// usage: require "java.util.HashMap" before importing

Type guard

static boolean isHashMapClassDescriptor(byte[] data) {
    String name = peekSerializedClassName(data);
    return "java.util.HashMap".equals(name);
}

Try / catch

try {
    importer.parse(stream);
} catch (ParseException e) {
    if (e.getMessage().startsWith("Unexpected class name:")) {
        String actual = e.getMessage().substring(e.getMessage().indexOf(':') + 2);
        throw new ImportException("File contains " + actual + " instead of a FreeOTP HashMap token store", e);
    }
}

Prevention

When it happens

Trigger: A Java serialization stream whose class descriptor names any class other than java.util.HashMap — e.g. java.util.Hashtable, a custom FreeOTP token class, or a serialized object from a different app placed in the same file.

Common situations: Importing an export from a different authenticator app, a FreeOTP version that changed its storage layout, or mixing up files (settings store vs token store) both of which are Java-serialized.

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/98568fe77d085202. Report an issue: GitHub.

Appendix: source

Thrown at app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java:412

            // 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;
        }

        private static void parseClassDescriptor(DataInputStream inputStream)
                throws IOException, ParseException {
            // Check whether we're dealing with a HashMap and a version we support
            String className = parseUTF(inputStream);
            if (!className.equals(HashMap.class.getName())) {
                throw new ParseException(String.format("Unexpected class name: %s", className));
            }
            long serialVersionUID = inputStream.readLong();
            if (serialVersionUID != SERIAL_VERSION_UID) {
                throw new ParseException(String.format("Unexpected serial version UID: %d", serialVersionUID));
            }

            // Read past all of the fields in the class
            byte fieldDescriptor = inputStream.readByte();
            if (fieldDescriptor == TC_NULL) {
                return;
            }
            int totalFieldSkip = 0;
            int fieldCount = inputStream.readUnsignedShort();
            for (int i = 0; i < fieldCount; i++) {
                char fieldType = (char) inputStream.readByte();
                parseUTF(inputStream);
                switch (fieldType) {
                    case 'F': // float (4 bytes)

View on GitHub (pinned to d6f4e5925a)