#!/usr/bin/env python3
# Usage ./class_format_fixer.py <list of jar files>
#
# A (relatively) simple Python script that parses class files in given JARs
# and creates new JARs with dots in field and method names replaced with
# underscores.
# It's needed because some old JVMs (?) allowed dots there despite the JVM spec
# forbidding them - newer (?, maybe Oracle vs OpenJDK thing) JVMs refuse to
# load such classes, which this script does fix.
#
# Main reason of creation is to run a game called Starsector on NixOS
#
# Author: Anton Bulakh <
[email protected]>
import sys
import struct
import shutil
from zipfile import ZipFile
from io import BytesIO
CONST_SIZES = [0, 0, 0, 4, 4, 8, 8, 2, 2, 4, 4, 4, 4, 0, 0, 3, 2, 4, 4, 2, 2]
UTF_8 = 1
LONG = 5
DOUBLE = 6
NAME_AND_TYPE = 12
def read(format, stream):
data = struct.unpack_from(format, stream.getbuffer(), stream.tell())
stream.seek(struct.calcsize(format), 1)
return data[0] if len(data) == 1 else data
def fix_jar(filename):
temp = filename + '.temp'
with ZipFile(temp, 'w') as output, \
ZipFile(filename) as zip:
for info in zip.infolist():
data, fixed = zip.read(info), None
if info.filename.endswith('.class'):
fixed = fix_class(data)
output.writestr(info, fixed or data)
if fixed:
print(f'changed {info.filename}')
shutil.move(temp, filename)
def fix_class(bytecode):
stream = BytesIO(bytecode)
magic = read('>Ixxxx', stream)
if magic != 0xCAFEBABE:
raise RuntimeError('Bad magic number')
constant_pool = read_constants(stream)
stream.seek(2 * read('>xxxxxxH', stream), 1)
constants_to_fix = []
# fields and methods
for _ in range(2):
for _ in range(read('>H', stream)):
constants_to_fix.append(read('>xxHxx', stream))
for _ in range(read('>H', stream)):
stream.seek(read('>xxI', stream), 1)
for type, constant in constant_pool:
if type == 2:
constants_to_fix.append(constant)
offsets_to_fix = []
for constant in constants_to_fix:
type, constant = constant_pool[constant]
if type != 1:
raise RuntimeError(f'Constant #{constant} is not a Utf_info')
pos, data = constant
local_offset = data.find(b'.')
if local_offset != -1:
offsets_to_fix.append((data, pos + local_offset))
if len(offsets_to_fix) != 0:
bytecode = bytearray(bytecode)
for string, offset in offsets_to_fix:
if bytecode[offset] == 46:
bytecode[offset] = 95
print(f'fixed "{str(string)[2:-1]}"')
return bytecode
def read_constants(stream):
constant_pool_count = read('>H', stream)
constant_pool = [(0, None)]
while len(constant_pool) < constant_pool_count:
tag = read('>B', stream)
if tag > len(CONST_SIZES) or tag == 0 or tag == 2:
raise RuntimeError(f'Unknown constant tag: {tag}')
if tag == UTF_8:
length = read('>H', stream)
constant_pool.append((1, (stream.tell(), stream.read(length))))
elif tag == NAME_AND_TYPE:
constant_pool.append((2, read('>Hxx', stream)))
else:
stream.seek(CONST_SIZES[tag], 1)
if tag in (LONG, DOUBLE):
constant_pool.append((0, None))
constant_pool.append((0, None))
return constant_pool
if __name__ == '__main__':
for f in sys.argv[1:]:
fix_jar(f)