All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.maxmind.db.Decoder Maven / Gradle / Ivy

package com.maxmind.db;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
 * Decoder for MaxMind DB data.
 *
 * This class CANNOT be shared between threads
 */
final class Decoder {

    private static final Charset UTF_8 = StandardCharsets.UTF_8;

    private static final int[] POINTER_VALUE_OFFSETS = {0, 0, 1 << 11, (1 << 19) + ((1) << 11), 0};

    // XXX - This is only for unit testings. We should possibly make a
    // constructor to set this
    boolean POINTER_TEST_HACK = false;

    private final NodeCache cache;

    private final long pointerBase;

    private final CharsetDecoder utfDecoder = UTF_8.newDecoder();

    private final ByteBuffer buffer;

    private final ConcurrentHashMap constructors;

    Decoder(NodeCache cache, ByteBuffer buffer, long pointerBase) {
        this(
                cache,
                buffer,
                pointerBase,
                new ConcurrentHashMap<>()
        );
    }

    Decoder(
            NodeCache cache,
            ByteBuffer buffer,
            long pointerBase,
            ConcurrentHashMap constructors
    ) {
        this.cache = cache;
        this.pointerBase = pointerBase;
        this.buffer = buffer;
        this.constructors = constructors;
    }

    private final NodeCache.Loader cacheLoader = this::decode;

    public  T decode(int offset, Class cls) throws IOException {
        if (offset >= this.buffer.capacity()) {
            throw new InvalidDatabaseException(
                    "The MaxMind DB file's data section contains bad data: "
                            + "pointer larger than the database.");
        }

        this.buffer.position(offset);
        return cls.cast(decode(cls, null).getValue());
    }

    private  DecodedValue decode(CacheKey key) throws IOException {
        int offset = key.getOffset();
        if (offset >= this.buffer.capacity()) {
            throw new InvalidDatabaseException(
                    "The MaxMind DB file's data section contains bad data: "
                            + "pointer larger than the database.");
        }

        this.buffer.position(offset);
        Class cls = key.getCls();
        return decode(cls, key.getType());
    }

    private  DecodedValue decode(Class cls, java.lang.reflect.Type genericType)
            throws IOException {
        int ctrlByte = 0xFF & this.buffer.get();

        Type type = Type.fromControlByte(ctrlByte);

        // Pointers are a special case, we don't read the next 'size' bytes, we
        // use the size to determine the length of the pointer and then follow
        // it.
        if (type.equals(Type.POINTER)) {
            int pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
            int base = pointerSize == 4 ? (byte) 0 : (byte) (ctrlByte & 0x7);
            int packed = this.decodeInteger(base, pointerSize);
            long pointer = packed + this.pointerBase + POINTER_VALUE_OFFSETS[pointerSize];

            // for unit testing
            if (this.POINTER_TEST_HACK) {
                return new DecodedValue(pointer);
            }

            int targetOffset = (int) pointer;
            int position = buffer.position();

            CacheKey key = new CacheKey(targetOffset, cls, genericType);
            DecodedValue o = cache.get(key, cacheLoader);

            buffer.position(position);
            return o;
        }

        if (type.equals(Type.EXTENDED)) {
            int nextByte = this.buffer.get();

            int typeNum = nextByte + 7;

            if (typeNum < 8) {
                throw new InvalidDatabaseException(
                        "Something went horribly wrong in the decoder. An extended type "
                                + "resolved to a type number < 8 (" + typeNum
                                + ")");
            }

            type = Type.get(typeNum);
        }

        int size = ctrlByte & 0x1f;
        if (size >= 29) {
            switch (size) {
                case 29:
                    size = 29 + (0xFF & buffer.get());
                    break;
                case 30:
                    size = 285 + decodeInteger(2);
                    break;
                default:
                    size = 65821 + decodeInteger(3);
            }
        }

        return new DecodedValue(this.decodeByType(type, size, cls, genericType));
    }

    private  Object decodeByType(
            Type type,
            int size,
            Class cls,
            java.lang.reflect.Type genericType
    ) throws IOException {
        switch (type) {
            case MAP:
                return this.decodeMap(size, cls, genericType);
            case ARRAY:
                Class elementClass = Object.class;
                if (genericType instanceof ParameterizedType) {
                    ParameterizedType pType = (ParameterizedType) genericType;
                    java.lang.reflect.Type[] actualTypes
                        = pType.getActualTypeArguments();
                    if (actualTypes.length == 1) {
                        elementClass = (Class) actualTypes[0];
                    }
                }
                return this.decodeArray(size, cls, elementClass);
            case BOOLEAN:
                return Decoder.decodeBoolean(size);
            case UTF8_STRING:
                return this.decodeString(size);
            case DOUBLE:
                return this.decodeDouble(size);
            case FLOAT:
                return this.decodeFloat(size);
            case BYTES:
                return this.getByteArray(size);
            case UINT16:
                return this.decodeUint16(size);
            case UINT32:
                return this.decodeUint32(size);
            case INT32:
                return this.decodeInt32(size);
            case UINT64:
            case UINT128:
                return this.decodeBigInteger(size);
            default:
                throw new InvalidDatabaseException(
                        "Unknown or unexpected type: " + type.name());
        }
    }

    private String decodeString(int size) throws CharacterCodingException {
        int oldLimit = buffer.limit();
        buffer.limit(buffer.position() + size);
        String s = utfDecoder.decode(buffer).toString();
        buffer.limit(oldLimit);
        return s;
    }

    private int decodeUint16(int size) {
        return this.decodeInteger(size);
    }

    private int decodeInt32(int size) {
        return this.decodeInteger(size);
    }

    private long decodeLong(int size) {
        long integer = 0;
        for (int i = 0; i < size; i++) {
            integer = (integer << 8) | (this.buffer.get() & 0xFF);
        }
        return integer;
    }

    private long decodeUint32(int size) {
        return this.decodeLong(size);
    }

    private int decodeInteger(int size) {
        return this.decodeInteger(0, size);
    }

    private int decodeInteger(int base, int size) {
        return Decoder.decodeInteger(this.buffer, base, size);
    }

    static int decodeInteger(ByteBuffer buffer, int base, int size) {
        int integer = base;
        for (int i = 0; i < size; i++) {
            integer = (integer << 8) | (buffer.get() & 0xFF);
        }
        return integer;
    }

    private BigInteger decodeBigInteger(int size) {
        byte[] bytes = this.getByteArray(size);
        return new BigInteger(1, bytes);
    }

    private double decodeDouble(int size) throws InvalidDatabaseException {
        if (size != 8) {
            throw new InvalidDatabaseException(
                    "The MaxMind DB file's data section contains bad data: "
                            + "invalid size of double.");
        }
        return this.buffer.getDouble();
    }

    private float decodeFloat(int size) throws InvalidDatabaseException {
        if (size != 4) {
            throw new InvalidDatabaseException(
                    "The MaxMind DB file's data section contains bad data: "
                            + "invalid size of float.");
        }
        return this.buffer.getFloat();
    }

    private static boolean decodeBoolean(int size)
            throws InvalidDatabaseException {
        switch (size) {
            case 0:
                return false;
            case 1:
                return true;
            default:
                throw new InvalidDatabaseException(
                        "The MaxMind DB file's data section contains bad data: "
                                + "invalid size of boolean.");
        }
    }

    private  List decodeArray(
            int size,
            Class cls,
            Class elementClass
    ) throws IOException {
        if (!List.class.isAssignableFrom(cls) && !cls.equals(Object.class)) {
            throw new DeserializationException("Unable to deserialize an array into an " + cls);
        }

        List array;
        if (cls.equals(List.class) || cls.equals(Object.class)) {
            array = new ArrayList<>(size);
        } else {
            Constructor constructor;
            try {
                constructor = cls.getConstructor(Integer.TYPE);
            } catch (NoSuchMethodException e) {
                throw new DeserializationException("No constructor found for the List: " + e);
            }
            Object[] parameters = {size};
            try {
                @SuppressWarnings("unchecked")
                List array2 = (List) constructor.newInstance(parameters);
                array = array2;
            } catch (InstantiationException |
                    IllegalAccessException |
                    InvocationTargetException e) {
                throw new DeserializationException("Error creating list: " + e);
            }
        }

        for (int i = 0; i < size; i++) {
            Object e = this.decode(elementClass, null).getValue();
            array.add(elementClass.cast(e));
        }

        return array;
    }

    private  Object decodeMap(
            int size,
            Class cls,
            java.lang.reflect.Type genericType
    ) throws IOException {
        if (Map.class.isAssignableFrom(cls) || cls.equals(Object.class)) {
            Class valueClass = Object.class;
            if (genericType instanceof ParameterizedType) {
                ParameterizedType pType = (ParameterizedType) genericType;
                java.lang.reflect.Type[] actualTypes
                    = pType.getActualTypeArguments();
                if (actualTypes.length == 2) {
                    Class keyClass = (Class) actualTypes[0];
                    if (!keyClass.equals(String.class)) {
                        throw new DeserializationException("Map keys must be strings.");
                    }

                    valueClass = (Class) actualTypes[1];
                }
            }
            return this.decodeMapIntoMap(cls, size, valueClass);
        }

        return this.decodeMapIntoObject(size, cls);
    }

    private  Map decodeMapIntoMap(
            Class cls,
            int size,
            Class valueClass
    ) throws IOException {
        Map map;
        if (cls.equals(Map.class) || cls.equals(Object.class)) {
            map = new HashMap<>(size);
        } else {
            Constructor constructor;
            try {
                constructor = cls.getConstructor(Integer.TYPE);
            } catch (NoSuchMethodException e) {
                throw new DeserializationException("No constructor found for the Map: " + e);
            }
            Object[] parameters = {size};
            try {
                @SuppressWarnings("unchecked")
                Map map2 = (Map) constructor.newInstance(parameters);
                map = map2;
            } catch (InstantiationException |
                    IllegalAccessException |
                    InvocationTargetException e) {
                throw new DeserializationException("Error creating map: " + e);
            }
        }

        for (int i = 0; i < size; i++) {
            String key = (String) this.decode(String.class, null).getValue();
            Object value = this.decode(valueClass, null).getValue();
            map.put(key, valueClass.cast(value));
        }

        return map;
    }

    private  Object decodeMapIntoObject(int size, Class cls)
            throws IOException {
        CachedConstructor cachedConstructor = this.constructors.get(cls);
        Constructor constructor;
        Class[] parameterTypes;
        java.lang.reflect.Type[] parameterGenericTypes;
        Map parameterIndexes;
        if (cachedConstructor == null) {
            constructor = this.findConstructor(cls);

            parameterTypes = constructor.getParameterTypes();

            parameterGenericTypes = constructor.getGenericParameterTypes();

            parameterIndexes = new HashMap<>();
            Annotation[][] annotations = constructor.getParameterAnnotations();
            for (int i = 0; i < constructor.getParameterCount(); i++) {
                String parameterName = this.getParameterName(cls, i, annotations[i]);
                parameterIndexes.put(parameterName, i);
            }

            this.constructors.put(
                    cls,
                    new CachedConstructor(
                        constructor,
                        parameterTypes,
                        parameterGenericTypes,
                        parameterIndexes
                    )
            );
        } else {
            constructor = cachedConstructor.getConstructor();
            parameterTypes = cachedConstructor.getParameterTypes();
            parameterGenericTypes = cachedConstructor.getParameterGenericTypes();
            parameterIndexes = cachedConstructor.getParameterIndexes();
        }

        Object[] parameters = new Object[parameterTypes.length];
        for (int i = 0; i < size; i++) {
            String key = (String) this.decode(String.class, null).getValue();

            Integer parameterIndex = parameterIndexes.get(key);
            if (parameterIndex == null) {
                int offset = this.nextValueOffset(this.buffer.position(), 1);
                this.buffer.position(offset);
                continue;
            }

            parameters[parameterIndex] = this.decode(
                parameterTypes[parameterIndex],
                parameterGenericTypes[parameterIndex]
            ).getValue();
        }

        try {
            return constructor.newInstance(parameters);
        } catch (InstantiationException |
                IllegalAccessException |
                InvocationTargetException e) {
            throw new DeserializationException("Error creating object: " + e);
        }
    }

    private static  Constructor findConstructor(Class cls)
            throws ConstructorNotFoundException {
        Constructor[] constructors = cls.getConstructors();
        for (Constructor constructor : constructors) {
            if (constructor.getAnnotation(MaxMindDbConstructor.class) == null) {
                continue;
            }
            @SuppressWarnings("unchecked")
            Constructor constructor2 = (Constructor) constructor;
            return constructor2;
        }

        throw new ConstructorNotFoundException("No constructor on class " + cls.getName() + " with the MaxMindDbConstructor annotation was found.");
    }

    private static  String getParameterName(
            Class cls,
            int index,
            Annotation[] annotations
    ) throws ParameterNotFoundException {
        for (Annotation annotation : annotations) {
            if (!annotation.annotationType().equals(MaxMindDbParameter.class)) {
                continue;
            }
            MaxMindDbParameter paramAnnotation = (MaxMindDbParameter) annotation;
            return paramAnnotation.name();
        }
        throw new ParameterNotFoundException("Constructor parameter " + index + " on class " + cls.getName() + " is not annotated with MaxMindDbParameter.");
    }

    private int nextValueOffset(int offset, int numberToSkip)
        throws InvalidDatabaseException {
        if (numberToSkip == 0) {
            return offset;
        }

        CtrlData ctrlData = this.getCtrlData(offset);
        int ctrlByte = ctrlData.getCtrlByte();
        int size = ctrlData.getSize();
        offset = ctrlData.getOffset();

        Type type = ctrlData.getType();
        switch (type) {
        case POINTER:
            int pointerSize = ((ctrlByte >>> 3) & 0x3) + 1;
            offset += pointerSize;
            break;
        case MAP:
            numberToSkip += 2 * size;
            break;
        case ARRAY:
            numberToSkip += size;
            break;
        case BOOLEAN:
            break;
        default:
            offset += size;
            break;
        }

        return nextValueOffset(offset, numberToSkip - 1);
    }

    private CtrlData getCtrlData(int offset)
        throws InvalidDatabaseException {
        if (offset >= this.buffer.capacity()) {
            throw new InvalidDatabaseException(
                    "The MaxMind DB file's data section contains bad data: "
                            + "pointer larger than the database.");
        }

        this.buffer.position(offset);
        int ctrlByte = 0xFF & this.buffer.get();
        offset++;

        Type type = Type.fromControlByte(ctrlByte);

        if (type.equals(Type.EXTENDED)) {
            int nextByte = this.buffer.get();

            int typeNum = nextByte + 7;

            if (typeNum < 8) {
                throw new InvalidDatabaseException(
                        "Something went horribly wrong in the decoder. An extended type "
                                + "resolved to a type number < 8 (" + typeNum
                                + ")");
            }

            type = Type.get(typeNum);
            offset++;
        }

        int size = ctrlByte & 0x1f;
        if (size >= 29) {
            int bytesToRead = size - 28;
            offset += bytesToRead;
            switch (size) {
                case 29:
                    size = 29 + (0xFF & buffer.get());
                    break;
                case 30:
                    size = 285 + decodeInteger(2);
                    break;
                default:
                    size = 65821 + decodeInteger(3);
            }
        }

        return new CtrlData(type, ctrlByte, offset, size);
    }

    private byte[] getByteArray(int length) {
        return Decoder.getByteArray(this.buffer, length);
    }

    private static byte[] getByteArray(ByteBuffer buffer, int length) {
        byte[] bytes = new byte[length];
        buffer.get(bytes);
        return bytes;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy