Ticket #17177: 17177.7.protobuf.patch

File 17177.7.protobuf.patch, 62.3 KB (added by taylor.smock, 3 years ago)

Protobuf only classes and tests (tests cover most everything, including most mutations). Files needed for tests will be uploaded separately.

  • src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.util.ArrayList;
     5import java.util.List;
     6
     7/**
     8 * Parse packed values (only numerical values)
     9 *
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13public class ProtoBufPacked {
     14    private final byte[] bytes;
     15    private final Number[] numbers;
     16    private int location;
     17
     18    /**
     19     * Create a new ProtoBufPacked object
     20     *
     21     * @param bytes The packed bytes
     22     */
     23    public ProtoBufPacked(byte[] bytes) {
     24        this.location = 0;
     25        this.bytes = bytes;
     26        List<Number> numbersT = new ArrayList<>();
     27        while (this.location < bytes.length) {
     28            numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE));
     29        }
     30
     31        this.numbers = new Number[numbersT.size()];
     32        for (int i = 0; i < numbersT.size(); i++) {
     33            this.numbers[i] = numbersT.get(i);
     34        }
     35    }
     36
     37    /**
     38     * Get the parsed number array
     39     *
     40     * @return The number array
     41     */
     42    public Number[] getArray() {
     43        return this.numbers;
     44    }
     45
     46    private byte[] nextVarInt() {
     47        List<Byte> byteList = new ArrayList<>();
     48        while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE)
     49          == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
     50            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     51            byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
     52        }
     53        // The last byte doesn't drop the most significant bit
     54        byteList.add(this.bytes[this.location++]);
     55        byte[] byteArray = new byte[byteList.size()];
     56        for (int i = 0; i < byteList.size(); i++) {
     57            byteArray[i] = byteList.get(i);
     58        }
     59
     60        return byteArray;
     61    }
     62}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.BufferedInputStream;
     5import java.io.ByteArrayInputStream;
     6import java.io.IOException;
     7import java.io.InputStream;
     8import java.util.ArrayList;
     9import java.util.Collection;
     10import java.util.List;
     11
     12import org.openstreetmap.josm.tools.Logging;
     13
     14/**
     15 * A basic Protobuf parser
     16 *
     17 * @author Taylor Smock
     18 * @since xxx
     19 */
     20public class ProtoBufParser implements AutoCloseable {
     21    /**
     22     * The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints)
     23     */
     24    public static final byte BYTE_SIZE = 8;
     25    /**
     26     * The byte size for var ints (since the first byte is just an indicator for if the var int is done)
     27     */
     28    public static final byte VAR_INT_BYTE_SIZE = BYTE_SIZE - 1;
     29    /**
     30     * Used to get the most significant byte
     31     */
     32    static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7);
     33    /**
     34     * Convert a byte array to a number (little endian)
     35     *
     36     * @param bytes    The bytes to convert
     37     * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8.
     38     * @return An appropriate {@link Number} class.
     39     */
     40    public static Number convertByteArray(byte[] bytes, byte byteSize) {
     41        long number = 0;
     42        for (int i = 0; i < bytes.length; i++) {
     43            // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues
     44            number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i);
     45        }
     46        return convertLong(number);
     47    }
     48
     49    /**
     50     * Convert a long to an appropriate {@link Number} class
     51     *
     52     * @param number The long to convert
     53     * @return A {@link Number}
     54     */
     55    public static Number convertLong(long number) {
     56        // TODO deal with booleans
     57        if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) {
     58            return (byte) number;
     59        } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) {
     60            return (short) number;
     61        } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) {
     62            return (int) number;
     63        }
     64        return number;
     65    }
     66
     67    /**
     68     * Decode a zig-zag encoded value
     69     *
     70     * @param signed The value to decode
     71     * @return The decoded value
     72     */
     73    public static Number decodeZigZag(Number signed) {
     74        final long value = signed.longValue();
     75        return convertLong((value >> 1) ^ -(value & 1));
     76    }
     77
     78    /**
     79     * Encode a number to a zig-zag encode value
     80     *
     81     * @param signed The number to encode
     82     * @return The encoded value
     83     */
     84    public static Number encodeZigZag(Number signed) {
     85        final long value = signed.longValue();
     86        // This boundary condition could be >= or <= or both. Tests indicate that it doesn't actually matter.
     87        // The only difference would be the number type returned, except it is always converted to the most basic type.
     88        final int shift = (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE ? Long.BYTES : Integer.BYTES) * 8 - 1;
     89        return convertLong((value << 1) ^ (value >> shift));
     90    }
     91
     92    private final InputStream inputStream;
     93
     94    /**
     95     * Create a new parser
     96     *
     97     * @param bytes The bytes to parse
     98     */
     99    public ProtoBufParser(byte[] bytes) {
     100        this(new ByteArrayInputStream(bytes));
     101    }
     102
     103    /**
     104     * Create a new parser
     105     *
     106     * @param inputStream The InputStream (will be fully read at this time)
     107     */
     108    public ProtoBufParser(InputStream inputStream) {
     109        if (inputStream.markSupported()) {
     110            this.inputStream = inputStream;
     111        } else {
     112            this.inputStream = new BufferedInputStream(inputStream);
     113        }
     114    }
     115
     116    /**
     117     * Read all records
     118     *
     119     * @return A collection of all records
     120     * @throws IOException - if an IO error occurs
     121     */
     122    public Collection<ProtoBufRecord> allRecords() throws IOException {
     123        Collection<ProtoBufRecord> records = new ArrayList<>();
     124        while (this.hasNext()) {
     125            records.add(new ProtoBufRecord(this));
     126        }
     127        return records;
     128    }
     129
     130    @Override
     131    public void close() {
     132        try {
     133            this.inputStream.close();
     134        } catch (IOException e) {
     135            Logging.error(e);
     136        }
     137    }
     138
     139    /**
     140     * Check if there is more data to read
     141     *
     142     * @return {@code true} if there is more data to read
     143     * @throws IOException - if an IO error occurs
     144     */
     145    public boolean hasNext() throws IOException {
     146        return this.inputStream.available() > 0;
     147    }
     148
     149    /**
     150     * Get the "next" WireType
     151     *
     152     * @return {@link WireType} expected
     153     * @throws IOException - if an IO error occurs
     154     */
     155    public WireType next() throws IOException {
     156        this.inputStream.mark(16);
     157        try {
     158            return WireType.values()[this.inputStream.read() << 3];
     159        } finally {
     160            this.inputStream.reset();
     161        }
     162    }
     163
     164    /**
     165     * Get the next byte
     166     *
     167     * @return The next byte
     168     * @throws IOException - if an IO error occurs
     169     */
     170    public int nextByte() throws IOException {
     171        return this.inputStream.read();
     172    }
     173
     174    /**
     175     * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT})
     176     *
     177     * @return a byte array of the next 32 bits (4 bytes)
     178     * @throws IOException - if an IO error occurs
     179     */
     180    public byte[] nextFixed32() throws IOException {
     181        // 4 bytes == 32 bits
     182        return readNextBytes(4);
     183    }
     184
     185    /**
     186     * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     187     *
     188     * @return a byte array of the next 64 bits (8 bytes)
     189     * @throws IOException - if an IO error occurs
     190     */
     191    public byte[] nextFixed64() throws IOException {
     192        // 8 bytes == 64 bits
     193        return readNextBytes(8);
     194    }
     195
     196    /**
     197     * Get the next delimited message ({@link WireType#LENGTH_DELIMITED})
     198     *
     199     * @return The next length delimited message
     200     * @throws IOException - if an IO error occurs
     201     */
     202    public byte[] nextLengthDelimited() throws IOException {
     203        int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue();
     204        return readNextBytes(length);
     205    }
     206
     207    /**
     208     * Get the next var int ({@code WireType#VARINT})
     209     *
     210     * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     211     * @throws IOException - if an IO error occurs
     212     */
     213    public byte[] nextVarInt() throws IOException {
     214        List<Byte> byteList = new ArrayList<>();
     215        int currentByte = this.nextByte();
     216        while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE && currentByte > 0) {
     217            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     218            byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE));
     219            currentByte = this.nextByte();
     220        }
     221        // The last byte doesn't drop the most significant bit
     222        byteList.add((byte) currentByte);
     223        byte[] byteArray = new byte[byteList.size()];
     224        for (int i = 0; i < byteList.size(); i++) {
     225            byteArray[i] = byteList.get(i);
     226        }
     227
     228        return byteArray;
     229    }
     230
     231    /**
     232     * Read an arbitrary number of bytes
     233     *
     234     * @param size The number of bytes to read
     235     * @return a byte array of the specified size, filled with bytes read (unsigned)
     236     * @throws IOException - if an IO error occurs
     237     */
     238    private byte[] readNextBytes(int size) throws IOException {
     239        byte[] bytesRead = new byte[size];
     240        for (int i = 0; i < bytesRead.length; i++) {
     241            bytesRead[i] = (byte) this.nextByte();
     242        }
     243        return bytesRead;
     244    }
     245}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.IOException;
     5import java.nio.charset.StandardCharsets;
     6import java.util.stream.Stream;
     7
     8import org.openstreetmap.josm.tools.Utils;
     9
     10/**
     11 * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it.
     12 *
     13 * @author Taylor Smock
     14 * @since xxx
     15 */
     16public class ProtoBufRecord implements AutoCloseable {
     17    private static final byte[] EMPTY_BYTES = {};
     18    private final WireType type;
     19    private final int field;
     20    private byte[] bytes;
     21
     22    /**
     23     * Create a new Protobuf record
     24     *
     25     * @param parser The parser to use to create the record
     26     * @throws IOException - if an IO error occurs
     27     */
     28    public ProtoBufRecord(ProtoBufParser parser) throws IOException {
     29        Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE);
     30        // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3}
     31        this.field = (int) number.longValue() >> 3;
     32        // 7 is 111 (so last three bits)
     33        byte wireType = (byte) (number.longValue() & 7);
     34        this.type = Stream.of(WireType.values()).filter(wType -> wType.getTypeRepresentation() == wireType).findFirst()
     35          .orElse(WireType.UNKNOWN);
     36
     37        if (this.type == WireType.VARINT) {
     38            this.bytes = parser.nextVarInt();
     39        } else if (this.type == WireType.SIXTY_FOUR_BIT) {
     40            this.bytes = parser.nextFixed64();
     41        } else if (this.type == WireType.THIRTY_TWO_BIT) {
     42            this.bytes = parser.nextFixed32();
     43        } else if (this.type == WireType.LENGTH_DELIMITED) {
     44            this.bytes = parser.nextLengthDelimited();
     45        } else {
     46            this.bytes = EMPTY_BYTES;
     47        }
     48    }
     49
     50    /**
     51     * Get as a double ({@link WireType#SIXTY_FOUR_BIT})
     52     *
     53     * @return the double
     54     */
     55    public double asDouble() {
     56        long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
     57        return Double.longBitsToDouble(doubleNumber);
     58    }
     59
     60    /**
     61     * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT})
     62     *
     63     * @return a byte array of the 32 bits (4 bytes)
     64     */
     65    public byte[] asFixed32() {
     66        // TODO verify, or just assume?
     67        // 4 bytes == 32 bits
     68        return this.bytes;
     69    }
     70
     71    /**
     72     * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     73     *
     74     * @return a byte array of the 64 bits (8 bytes)
     75     */
     76    public byte[] asFixed64() {
     77        // TODO verify, or just assume?
     78        // 8 bytes == 64 bits
     79        return this.bytes;
     80    }
     81
     82    /**
     83     * Get as a float ({@link WireType#THIRTY_TWO_BIT})
     84     *
     85     * @return the float
     86     */
     87    public float asFloat() {
     88        int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
     89        return Float.intBitsToFloat(floatNumber);
     90    }
     91
     92    /**
     93     * Get the signed var int ({@code WireType#VARINT}).
     94     * These are specially encoded so that they take up less space.
     95     *
     96     * @return The signed var int ({@code sint32} or {@code sint64})
     97     */
     98    public Number asSignedVarInt() {
     99        final Number signed = this.asUnsignedVarInt();
     100        return ProtoBufParser.decodeZigZag(signed);
     101    }
     102
     103    /**
     104     * Get as a string ({@link WireType#LENGTH_DELIMITED})
     105     *
     106     * @return The string (encoded as {@link StandardCharsets#UTF_8})
     107     */
     108    public String asString() {
     109        return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8));
     110    }
     111
     112    /**
     113     * Get the var int ({@code WireType#VARINT})
     114     *
     115     * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     116     */
     117    public Number asUnsignedVarInt() {
     118        return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE);
     119    }
     120
     121    @Override
     122    public void close() {
     123        this.bytes = null;
     124    }
     125
     126    /**
     127     * Get the raw bytes for this record
     128     *
     129     * @return The bytes
     130     */
     131    public byte[] getBytes() {
     132        return this.bytes;
     133    }
     134
     135    /**
     136     * Get the field value
     137     *
     138     * @return The field value
     139     */
     140    public int getField() {
     141        return this.field;
     142    }
     143
     144    /**
     145     * Get the WireType of the data
     146     *
     147     * @return The {@link WireType} of the data
     148     */
     149    public WireType getType() {
     150        return this.type;
     151    }
     152}
  • src/org/openstreetmap/josm/data/protobuf/WireType.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4/**
     5 * The WireTypes
     6 *
     7 * @author Taylor Smock
     8 * @since xxx
     9 */
     10public enum WireType {
     11    /**
     12     * int32, int64, uint32, uint64, sing32, sint64, bool, enum
     13     */
     14    VARINT(0),
     15    /**
     16     * fixed64, sfixed64, double
     17     */
     18    SIXTY_FOUR_BIT(1),
     19    /**
     20     * string, bytes, embedded messages, packed repeated fields
     21     */
     22    LENGTH_DELIMITED(2),
     23    /**
     24     * start groups
     25     *
     26     * @deprecated Unknown reason. Deprecated since at least 2012.
     27     */
     28    @Deprecated
     29    START_GROUP(3),
     30    /**
     31     * end groups
     32     *
     33     * @deprecated Unknown reason. Deprecated since at least 2012.
     34     */
     35    @Deprecated
     36    END_GROUP(4),
     37    /**
     38     * fixed32, sfixed32, float
     39     */
     40    THIRTY_TWO_BIT(5),
     41
     42    /**
     43     * For unknown WireTypes
     44     */
     45    UNKNOWN(Byte.MAX_VALUE);
     46
     47    private final byte type;
     48
     49    WireType(int value) {
     50        this.type = (byte) value;
     51    }
     52
     53    /**
     54     * Get the type representation (byte form)
     55     *
     56     * @return The wire type byte representation
     57     */
     58    public byte getTypeRepresentation() {
     59        return this.type;
     60    }
     61}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.util.ArrayList;
     5import java.util.List;
     6
     7/**
     8 * Parse packed values (only numerical values)
     9 *
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13public class ProtoBufPacked {
     14    private final byte[] bytes;
     15    private final Number[] numbers;
     16    private int location;
     17
     18    /**
     19     * Create a new ProtoBufPacked object
     20     *
     21     * @param bytes The packed bytes
     22     */
     23    public ProtoBufPacked(byte[] bytes) {
     24        this.location = 0;
     25        this.bytes = bytes;
     26        List<Number> numbersT = new ArrayList<>();
     27        while (this.location < bytes.length) {
     28            numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE));
     29        }
     30
     31        this.numbers = new Number[numbersT.size()];
     32        for (int i = 0; i < numbersT.size(); i++) {
     33            this.numbers[i] = numbersT.get(i);
     34        }
     35    }
     36
     37    /**
     38     * Get the parsed number array
     39     *
     40     * @return The number array
     41     */
     42    public Number[] getArray() {
     43        return this.numbers;
     44    }
     45
     46    private byte[] nextVarInt() {
     47        List<Byte> byteList = new ArrayList<>();
     48        while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE)
     49          == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
     50            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     51            byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
     52        }
     53        // The last byte doesn't drop the most significant bit
     54        byteList.add(this.bytes[this.location++]);
     55        byte[] byteArray = new byte[byteList.size()];
     56        for (int i = 0; i < byteList.size(); i++) {
     57            byteArray[i] = byteList.get(i);
     58        }
     59
     60        return byteArray;
     61    }
     62}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.BufferedInputStream;
     5import java.io.ByteArrayInputStream;
     6import java.io.IOException;
     7import java.io.InputStream;
     8import java.util.ArrayList;
     9import java.util.Collection;
     10import java.util.List;
     11
     12import org.openstreetmap.josm.tools.Logging;
     13
     14/**
     15 * A basic Protobuf parser
     16 *
     17 * @author Taylor Smock
     18 * @since xxx
     19 */
     20public class ProtoBufParser implements AutoCloseable {
     21    /**
     22     * The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints)
     23     */
     24    public static final byte BYTE_SIZE = 8;
     25    /**
     26     * The byte size for var ints (since the first byte is just an indicator for if the var int is done)
     27     */
     28    public static final byte VAR_INT_BYTE_SIZE = BYTE_SIZE - 1;
     29    /**
     30     * Used to get the most significant byte
     31     */
     32    static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7);
     33    /**
     34     * Convert a byte array to a number (little endian)
     35     *
     36     * @param bytes    The bytes to convert
     37     * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8.
     38     * @return An appropriate {@link Number} class.
     39     */
     40    public static Number convertByteArray(byte[] bytes, byte byteSize) {
     41        long number = 0;
     42        for (int i = 0; i < bytes.length; i++) {
     43            // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues
     44            number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i);
     45        }
     46        return convertLong(number);
     47    }
     48
     49    /**
     50     * Convert a long to an appropriate {@link Number} class
     51     *
     52     * @param number The long to convert
     53     * @return A {@link Number}
     54     */
     55    public static Number convertLong(long number) {
     56        // TODO deal with booleans
     57        if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) {
     58            return (byte) number;
     59        } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) {
     60            return (short) number;
     61        } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) {
     62            return (int) number;
     63        }
     64        return number;
     65    }
     66
     67    /**
     68     * Decode a zig-zag encoded value
     69     *
     70     * @param signed The value to decode
     71     * @return The decoded value
     72     */
     73    public static Number decodeZigZag(Number signed) {
     74        final long value = signed.longValue();
     75        return convertLong((value >> 1) ^ -(value & 1));
     76    }
     77
     78    /**
     79     * Encode a number to a zig-zag encode value
     80     *
     81     * @param signed The number to encode
     82     * @return The encoded value
     83     */
     84    public static Number encodeZigZag(Number signed) {
     85        final long value = signed.longValue();
     86        // This boundary condition could be >= or <= or both. Tests indicate that it doesn't actually matter.
     87        // The only difference would be the number type returned, except it is always converted to the most basic type.
     88        final int shift = (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE ? Long.BYTES : Integer.BYTES) * 8 - 1;
     89        return convertLong((value << 1) ^ (value >> shift));
     90    }
     91
     92    private final InputStream inputStream;
     93
     94    /**
     95     * Create a new parser
     96     *
     97     * @param bytes The bytes to parse
     98     */
     99    public ProtoBufParser(byte[] bytes) {
     100        this(new ByteArrayInputStream(bytes));
     101    }
     102
     103    /**
     104     * Create a new parser
     105     *
     106     * @param inputStream The InputStream (will be fully read at this time)
     107     */
     108    public ProtoBufParser(InputStream inputStream) {
     109        if (inputStream.markSupported()) {
     110            this.inputStream = inputStream;
     111        } else {
     112            this.inputStream = new BufferedInputStream(inputStream);
     113        }
     114    }
     115
     116    /**
     117     * Read all records
     118     *
     119     * @return A collection of all records
     120     * @throws IOException - if an IO error occurs
     121     */
     122    public Collection<ProtoBufRecord> allRecords() throws IOException {
     123        Collection<ProtoBufRecord> records = new ArrayList<>();
     124        while (this.hasNext()) {
     125            records.add(new ProtoBufRecord(this));
     126        }
     127        return records;
     128    }
     129
     130    @Override
     131    public void close() {
     132        try {
     133            this.inputStream.close();
     134        } catch (IOException e) {
     135            Logging.error(e);
     136        }
     137    }
     138
     139    /**
     140     * Check if there is more data to read
     141     *
     142     * @return {@code true} if there is more data to read
     143     * @throws IOException - if an IO error occurs
     144     */
     145    public boolean hasNext() throws IOException {
     146        return this.inputStream.available() > 0;
     147    }
     148
     149    /**
     150     * Get the "next" WireType
     151     *
     152     * @return {@link WireType} expected
     153     * @throws IOException - if an IO error occurs
     154     */
     155    public WireType next() throws IOException {
     156        this.inputStream.mark(16);
     157        try {
     158            return WireType.values()[this.inputStream.read() << 3];
     159        } finally {
     160            this.inputStream.reset();
     161        }
     162    }
     163
     164    /**
     165     * Get the next byte
     166     *
     167     * @return The next byte
     168     * @throws IOException - if an IO error occurs
     169     */
     170    public int nextByte() throws IOException {
     171        return this.inputStream.read();
     172    }
     173
     174    /**
     175     * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT})
     176     *
     177     * @return a byte array of the next 32 bits (4 bytes)
     178     * @throws IOException - if an IO error occurs
     179     */
     180    public byte[] nextFixed32() throws IOException {
     181        // 4 bytes == 32 bits
     182        return readNextBytes(4);
     183    }
     184
     185    /**
     186     * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     187     *
     188     * @return a byte array of the next 64 bits (8 bytes)
     189     * @throws IOException - if an IO error occurs
     190     */
     191    public byte[] nextFixed64() throws IOException {
     192        // 8 bytes == 64 bits
     193        return readNextBytes(8);
     194    }
     195
     196    /**
     197     * Get the next delimited message ({@link WireType#LENGTH_DELIMITED})
     198     *
     199     * @return The next length delimited message
     200     * @throws IOException - if an IO error occurs
     201     */
     202    public byte[] nextLengthDelimited() throws IOException {
     203        int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue();
     204        return readNextBytes(length);
     205    }
     206
     207    /**
     208     * Get the next var int ({@code WireType#VARINT})
     209     *
     210     * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     211     * @throws IOException - if an IO error occurs
     212     */
     213    public byte[] nextVarInt() throws IOException {
     214        List<Byte> byteList = new ArrayList<>();
     215        int currentByte = this.nextByte();
     216        while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE && currentByte > 0) {
     217            // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
     218            byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE));
     219            currentByte = this.nextByte();
     220        }
     221        // The last byte doesn't drop the most significant bit
     222        byteList.add((byte) currentByte);
     223        byte[] byteArray = new byte[byteList.size()];
     224        for (int i = 0; i < byteList.size(); i++) {
     225            byteArray[i] = byteList.get(i);
     226        }
     227
     228        return byteArray;
     229    }
     230
     231    /**
     232     * Read an arbitrary number of bytes
     233     *
     234     * @param size The number of bytes to read
     235     * @return a byte array of the specified size, filled with bytes read (unsigned)
     236     * @throws IOException - if an IO error occurs
     237     */
     238    private byte[] readNextBytes(int size) throws IOException {
     239        byte[] bytesRead = new byte[size];
     240        for (int i = 0; i < bytesRead.length; i++) {
     241            bytesRead[i] = (byte) this.nextByte();
     242        }
     243        return bytesRead;
     244    }
     245}
  • src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import java.io.IOException;
     5import java.nio.charset.StandardCharsets;
     6import java.util.stream.Stream;
     7
     8import org.openstreetmap.josm.tools.Utils;
     9
     10/**
     11 * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it.
     12 *
     13 * @author Taylor Smock
     14 * @since xxx
     15 */
     16public class ProtoBufRecord implements AutoCloseable {
     17    private static final byte[] EMPTY_BYTES = {};
     18    private final WireType type;
     19    private final int field;
     20    private byte[] bytes;
     21
     22    /**
     23     * Create a new Protobuf record
     24     *
     25     * @param parser The parser to use to create the record
     26     * @throws IOException - if an IO error occurs
     27     */
     28    public ProtoBufRecord(ProtoBufParser parser) throws IOException {
     29        Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE);
     30        // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3}
     31        this.field = (int) number.longValue() >> 3;
     32        // 7 is 111 (so last three bits)
     33        byte wireType = (byte) (number.longValue() & 7);
     34        this.type = Stream.of(WireType.values()).filter(wType -> wType.getTypeRepresentation() == wireType).findFirst()
     35          .orElse(WireType.UNKNOWN);
     36
     37        if (this.type == WireType.VARINT) {
     38            this.bytes = parser.nextVarInt();
     39        } else if (this.type == WireType.SIXTY_FOUR_BIT) {
     40            this.bytes = parser.nextFixed64();
     41        } else if (this.type == WireType.THIRTY_TWO_BIT) {
     42            this.bytes = parser.nextFixed32();
     43        } else if (this.type == WireType.LENGTH_DELIMITED) {
     44            this.bytes = parser.nextLengthDelimited();
     45        } else {
     46            this.bytes = EMPTY_BYTES;
     47        }
     48    }
     49
     50    /**
     51     * Get as a double ({@link WireType#SIXTY_FOUR_BIT})
     52     *
     53     * @return the double
     54     */
     55    public double asDouble() {
     56        long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
     57        return Double.longBitsToDouble(doubleNumber);
     58    }
     59
     60    /**
     61     * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT})
     62     *
     63     * @return a byte array of the 32 bits (4 bytes)
     64     */
     65    public byte[] asFixed32() {
     66        // TODO verify, or just assume?
     67        // 4 bytes == 32 bits
     68        return this.bytes;
     69    }
     70
     71    /**
     72     * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT})
     73     *
     74     * @return a byte array of the 64 bits (8 bytes)
     75     */
     76    public byte[] asFixed64() {
     77        // TODO verify, or just assume?
     78        // 8 bytes == 64 bits
     79        return this.bytes;
     80    }
     81
     82    /**
     83     * Get as a float ({@link WireType#THIRTY_TWO_BIT})
     84     *
     85     * @return the float
     86     */
     87    public float asFloat() {
     88        int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
     89        return Float.intBitsToFloat(floatNumber);
     90    }
     91
     92    /**
     93     * Get the signed var int ({@code WireType#VARINT}).
     94     * These are specially encoded so that they take up less space.
     95     *
     96     * @return The signed var int ({@code sint32} or {@code sint64})
     97     */
     98    public Number asSignedVarInt() {
     99        final Number signed = this.asUnsignedVarInt();
     100        return ProtoBufParser.decodeZigZag(signed);
     101    }
     102
     103    /**
     104     * Get as a string ({@link WireType#LENGTH_DELIMITED})
     105     *
     106     * @return The string (encoded as {@link StandardCharsets#UTF_8})
     107     */
     108    public String asString() {
     109        return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8));
     110    }
     111
     112    /**
     113     * Get the var int ({@code WireType#VARINT})
     114     *
     115     * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
     116     */
     117    public Number asUnsignedVarInt() {
     118        return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE);
     119    }
     120
     121    @Override
     122    public void close() {
     123        this.bytes = null;
     124    }
     125
     126    /**
     127     * Get the raw bytes for this record
     128     *
     129     * @return The bytes
     130     */
     131    public byte[] getBytes() {
     132        return this.bytes;
     133    }
     134
     135    /**
     136     * Get the field value
     137     *
     138     * @return The field value
     139     */
     140    public int getField() {
     141        return this.field;
     142    }
     143
     144    /**
     145     * Get the WireType of the data
     146     *
     147     * @return The {@link WireType} of the data
     148     */
     149    public WireType getType() {
     150        return this.type;
     151    }
     152}
  • src/org/openstreetmap/josm/data/protobuf/WireType.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4/**
     5 * The WireTypes
     6 *
     7 * @author Taylor Smock
     8 * @since xxx
     9 */
     10public enum WireType {
     11    /**
     12     * int32, int64, uint32, uint64, sing32, sint64, bool, enum
     13     */
     14    VARINT(0),
     15    /**
     16     * fixed64, sfixed64, double
     17     */
     18    SIXTY_FOUR_BIT(1),
     19    /**
     20     * string, bytes, embedded messages, packed repeated fields
     21     */
     22    LENGTH_DELIMITED(2),
     23    /**
     24     * start groups
     25     *
     26     * @deprecated Unknown reason. Deprecated since at least 2012.
     27     */
     28    @Deprecated
     29    START_GROUP(3),
     30    /**
     31     * end groups
     32     *
     33     * @deprecated Unknown reason. Deprecated since at least 2012.
     34     */
     35    @Deprecated
     36    END_GROUP(4),
     37    /**
     38     * fixed32, sfixed32, float
     39     */
     40    THIRTY_TWO_BIT(5),
     41
     42    /**
     43     * For unknown WireTypes
     44     */
     45    UNKNOWN(Byte.MAX_VALUE);
     46
     47    private final byte type;
     48
     49    WireType(int value) {
     50        this.type = (byte) value;
     51    }
     52
     53    /**
     54     * Get the type representation (byte form)
     55     *
     56     * @return The wire type byte representation
     57     */
     58    public byte getTypeRepresentation() {
     59        return this.type;
     60    }
     61}
  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6import org.junit.jupiter.api.Test;
     7
     8/**
     9 * Test class for {@link ProtoBufParser}
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13class ProtoBufParserTest {
     14    /**
     15     * Check that we are appropriately converting values to the "smallest" type
     16     */
     17    @Test
     18    void testConvertLong() {
     19        // No casting due to auto conversions
     20        assertEquals(Byte.MAX_VALUE, ProtoBufParser.convertLong(Byte.MAX_VALUE));
     21        assertEquals(Byte.MIN_VALUE, ProtoBufParser.convertLong(Byte.MIN_VALUE));
     22        assertEquals(Short.MIN_VALUE, ProtoBufParser.convertLong(Short.MIN_VALUE));
     23        assertEquals(Short.MAX_VALUE, ProtoBufParser.convertLong(Short.MAX_VALUE));
     24        assertEquals(Integer.MAX_VALUE, ProtoBufParser.convertLong(Integer.MAX_VALUE));
     25        assertEquals(Integer.MIN_VALUE, ProtoBufParser.convertLong(Integer.MIN_VALUE));
     26        assertEquals(Long.MIN_VALUE, ProtoBufParser.convertLong(Long.MIN_VALUE));
     27        assertEquals(Long.MAX_VALUE, ProtoBufParser.convertLong(Long.MAX_VALUE));
     28    }
     29
     30    /**
     31     * Check that zig zags are appropriately encoded.
     32     */
     33    @Test
     34    void testEncodeZigZag() {
     35        assertEquals(0, ProtoBufParser.encodeZigZag(0).byteValue());
     36        assertEquals(1, ProtoBufParser.encodeZigZag(-1).byteValue());
     37        assertEquals(2, ProtoBufParser.encodeZigZag(1).byteValue());
     38        assertEquals(3, ProtoBufParser.encodeZigZag(-2).byteValue());
     39        assertEquals(254, ProtoBufParser.encodeZigZag(Byte.MAX_VALUE).shortValue());
     40        assertEquals(255, ProtoBufParser.encodeZigZag(Byte.MIN_VALUE).shortValue());
     41        assertEquals(65_534, ProtoBufParser.encodeZigZag(Short.MAX_VALUE).intValue());
     42        assertEquals(65_535, ProtoBufParser.encodeZigZag(Short.MIN_VALUE).intValue());
     43        // These integers check a possible boundary condition (the boundary between using the 32/64 bit encoding methods)
     44        assertEquals(4_294_967_292L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE - 1).longValue());
     45        assertEquals(4_294_967_293L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE + 1).longValue());
     46        assertEquals(4_294_967_294L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE).longValue());
     47        assertEquals(4_294_967_295L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE).longValue());
     48        assertEquals(4_294_967_296L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE + 1L).longValue());
     49        assertEquals(4_294_967_297L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE - 1L).longValue());
     50    }
     51}
  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6
     7import java.io.IOException;
     8
     9import org.junit.jupiter.api.Test;
     10
     11/**
     12 * Test class for specific {@link ProtoBufRecord} functionality
     13 */
     14class ProtoBufRecordTest {
     15    @Test
     16    void testFixed32() throws IOException {
     17        ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
     18        ProtoBufRecord thirtyTwoBit = new ProtoBufRecord(parser);
     19        assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType());
     20        assertEquals(1f, thirtyTwoBit.asFloat());
     21    }
     22
     23    @Test
     24    void testUnknown() throws IOException {
     25        ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
     26        ProtoBufRecord unknown = new ProtoBufRecord(parser);
     27        assertEquals(WireType.UNKNOWN, unknown.getType());
     28        assertEquals(0, unknown.getBytes().length);
     29    }
     30}
  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertNotNull;
     6import static org.junit.jupiter.api.Assertions.fail;
     7
     8import java.awt.geom.Ellipse2D;
     9import java.io.ByteArrayInputStream;
     10import java.io.File;
     11import java.io.IOException;
     12import java.io.InputStream;
     13import java.nio.file.Paths;
     14import java.text.MessageFormat;
     15import java.util.ArrayList;
     16import java.util.Collection;
     17import java.util.List;
     18import java.util.stream.Collectors;
     19
     20import org.openstreetmap.josm.TestUtils;
     21import org.openstreetmap.josm.data.coor.LatLon;
     22import org.openstreetmap.josm.data.imagery.ImageryInfo;
     23import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     24import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     25import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     26import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     27import org.openstreetmap.josm.data.osm.BBox;
     28import org.openstreetmap.josm.data.osm.Node;
     29import org.openstreetmap.josm.data.osm.Way;
     30import org.openstreetmap.josm.data.vector.VectorDataSet;
     31import org.openstreetmap.josm.data.vector.VectorNode;
     32import org.openstreetmap.josm.data.vector.VectorWay;
     33import org.openstreetmap.josm.io.Compression;
     34import org.openstreetmap.josm.testutils.JOSMTestRules;
     35
     36import org.junit.jupiter.api.Test;
     37import org.junit.jupiter.api.extension.RegisterExtension;
     38
     39/**
     40 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
     41 *
     42 * @author Taylor Smock
     43 * @since xxx
     44 */
     45class ProtoBufTest {
     46    /**
     47     * Convert an int array into a byte array
     48     * @param intArray The int array to convert (NOTE: numbers must be below 255)
     49     * @return A byte array that can be used
     50     */
     51    static byte[] toByteArray(int[] intArray) {
     52        byte[] byteArray = new byte[intArray.length];
     53        for (int i = 0; i < intArray.length; i++) {
     54            if (intArray[i] > Byte.MAX_VALUE - Byte.MIN_VALUE) {
     55                throw new IllegalArgumentException();
     56            }
     57            byteArray[i] = Integer.valueOf(intArray[i]).byteValue();
     58        }
     59        return byteArray;
     60    }
     61
     62    @RegisterExtension
     63    JOSMTestRules josmTestRules = new JOSMTestRules().preferences();
     64
     65    private Number bytesToVarInt(int... bytes) {
     66        byte[] byteArray = new byte[bytes.length];
     67        for (int i = 0; i < bytes.length; i++) {
     68            byteArray[i] = (byte) bytes[i];
     69        }
     70        return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE);
     71    }
     72
     73    /**
     74     * Test reading tile from Mapillary ( 14/3251/6258 )
     75     *
     76     * @throws IOException if there is a problem reading the file
     77     */
     78    @Test
     79    void testRead_14_3251_6258() throws IOException {
     80        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "14", "3251", "6258.mvt").toFile();
     81        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     82        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     83        assertEquals(2, records.size());
     84        List<Layer> layers = new ArrayList<>();
     85        for (ProtoBufRecord record : records) {
     86            if (record.getField() == Layer.LAYER_FIELD) {
     87                layers.add(new Layer(record.getBytes()));
     88            } else {
     89                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     90            }
     91        }
     92        Layer mapillarySequences = layers.get(0);
     93        Layer mapillaryPictures = layers.get(1);
     94        assertEquals("mapillary-sequences", mapillarySequences.getName());
     95        assertEquals("mapillary-images", mapillaryPictures.getName());
     96        assertEquals(2048, mapillarySequences.getExtent());
     97        assertEquals(2048, mapillaryPictures.getExtent());
     98
     99        assertEquals(1,
     100                mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).count());
     101        Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111)
     102                .findAny().orElse(null);
     103        assertEquals("jgxkXqVFM4jepMG3vP5Q9A", testSequence.getTags().get("key"));
     104        assertEquals("C15Ul6qVMfQFlzRcmQCLcA", testSequence.getTags().get("ikey"));
     105        assertEquals("x0hTY8cakpy0m3ui1GaG1A", testSequence.getTags().get("userkey"));
     106        assertEquals(Long.valueOf(1565196718638L), Long.valueOf(testSequence.getTags().get("captured_at")));
     107        assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
     108    }
     109
     110    @Test
     111    void testRead_17_26028_50060() throws IOException {
     112        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "openinframap", "17", "26028", "50060.pbf")
     113                .toFile();
     114        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     115        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     116        List<Layer> layers = new ArrayList<>();
     117        for (ProtoBufRecord record : records) {
     118            if (record.getField() == Layer.LAYER_FIELD) {
     119                layers.add(new Layer(record.getBytes()));
     120            } else {
     121                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     122            }
     123        }
     124        assertEquals(19, layers.size());
     125        List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty())
     126                .collect(Collectors.toList());
     127        // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area
     128        assertEquals(5, dataLayers.size());
     129
     130        // power_generator_area was rendered incorrectly
     131        final Layer powerGeneratorArea = dataLayers.stream()
     132                .filter(layer -> "power_generator_area".equals(layer.getName())).findAny().orElse(null);
     133        assertNotNull(powerGeneratorArea);
     134        final int extent = powerGeneratorArea.getExtent();
     135        // 17/26028/50060 bounds
     136        VectorDataSet vectorDataSet = new VectorDataSet();
     137        MVTTile vectorTile1 = new MVTTile(new MapboxVectorTileSource(new ImageryInfo("Test info", "example.org")),
     138                26028, 50060, 17);
     139        vectorTile1.loadImage(Compression.getUncompressedFileInputStream(vectorTile));
     140        vectorDataSet.addTileData(vectorTile1);
     141        vectorDataSet.setZoom(17);
     142        final Way one = new Way();
     143        one.addNode(new Node(new LatLon(39.0687509, -108.5100816)));
     144        one.addNode(new Node(new LatLon(39.0687509, -108.5095751)));
     145        one.addNode(new Node(new LatLon(39.0687169, -108.5095751)));
     146        one.addNode(new Node(new LatLon(39.0687169, -108.5100816)));
     147        one.addNode(one.getNode(0));
     148        one.setOsmId(666293899, 2);
     149        final BBox searchBBox = one.getBBox();
     150        searchBBox.addPrimitive(one, 0.00001);
     151        final Collection<VectorNode> searchedNodes = vectorDataSet.searchNodes(searchBBox);
     152        final Collection<VectorWay> searchedWays = vectorDataSet.searchWays(searchBBox);
     153        assertEquals(4, searchedNodes.size());
     154    }
     155
     156    @Test
     157    void testReadVarInt() {
     158        assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0));
     159        assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1));
     160        assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f));
     161        // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array
     162        Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07);
     163        assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual,
     164                MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE),
     165                        Long.toBinaryString(actual.longValue())));
     166    }
     167
     168    /**
     169     * Test simple message.
     170     * Check that a simple message is readable
     171     *
     172     * @throws IOException - if an IO error occurs
     173     */
     174    @Test
     175    void testSimpleMessage() throws IOException {
     176        ProtoBufParser parser = new ProtoBufParser(new byte[] { (byte) 0x08, (byte) 0x96, (byte) 0x01 });
     177        ProtoBufRecord record = new ProtoBufRecord(parser);
     178        assertEquals(WireType.VARINT, record.getType());
     179        assertEquals(150, record.asUnsignedVarInt().intValue());
     180    }
     181
     182    @Test
     183    void testSingletonMultiPoint() throws IOException {
     184        Collection<ProtoBufRecord> records = new ProtoBufParser(new ByteArrayInputStream(toByteArray(
     185                new int[] { 0x1a, 0x2c, 0x78, 0x02, 0x0a, 0x03, 0x74, 0x6d, 0x70, 0x28, 0x80, 0x20, 0x1a, 0x04, 0x6e,
     186                        0x61, 0x6d, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65,
     187                        0x12, 0x0d, 0x18, 0x01, 0x12, 0x02, 0x00, 0x00, 0x22, 0x05, 0x09, 0xe0, 0x3e, 0x84, 0x27 })))
     188                                .allRecords();
     189        List<Layer> layers = new ArrayList<>();
     190        for (ProtoBufRecord record : records) {
     191            if (record.getField() == Layer.LAYER_FIELD) {
     192                layers.add(new Layer(record.getBytes()));
     193            } else {
     194                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     195            }
     196        }
     197        assertEquals(1, layers.size());
     198        assertEquals(1, layers.get(0).getGeometry().size());
     199        Ellipse2D shape = (Ellipse2D) layers.get(0).getGeometry().iterator().next().getShapes().iterator().next();
     200        assertEquals(4016, shape.getCenterX());
     201        assertEquals(2498, shape.getCenterY());
     202    }
     203
     204    @Test
     205    void testZigZag() {
     206        assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue());
     207        assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue());
     208        assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue());
     209        assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue());
     210    }
     211}
  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertNotNull;
     6import static org.junit.jupiter.api.Assertions.fail;
     7
     8import java.awt.geom.Ellipse2D;
     9import java.io.ByteArrayInputStream;
     10import java.io.File;
     11import java.io.IOException;
     12import java.io.InputStream;
     13import java.nio.file.Paths;
     14import java.text.MessageFormat;
     15import java.util.ArrayList;
     16import java.util.Collection;
     17import java.util.List;
     18import java.util.stream.Collectors;
     19
     20import org.openstreetmap.josm.TestUtils;
     21import org.openstreetmap.josm.data.coor.LatLon;
     22import org.openstreetmap.josm.data.imagery.ImageryInfo;
     23import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
     24import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
     25import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
     26import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
     27import org.openstreetmap.josm.data.osm.BBox;
     28import org.openstreetmap.josm.data.osm.Node;
     29import org.openstreetmap.josm.data.osm.Way;
     30import org.openstreetmap.josm.data.vector.VectorDataSet;
     31import org.openstreetmap.josm.data.vector.VectorNode;
     32import org.openstreetmap.josm.data.vector.VectorWay;
     33import org.openstreetmap.josm.io.Compression;
     34import org.openstreetmap.josm.testutils.JOSMTestRules;
     35
     36import org.junit.jupiter.api.Test;
     37import org.junit.jupiter.api.extension.RegisterExtension;
     38
     39/**
     40 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
     41 *
     42 * @author Taylor Smock
     43 * @since xxx
     44 */
     45class ProtoBufTest {
     46    /**
     47     * Convert an int array into a byte array
     48     * @param intArray The int array to convert (NOTE: numbers must be below 255)
     49     * @return A byte array that can be used
     50     */
     51    static byte[] toByteArray(int[] intArray) {
     52        byte[] byteArray = new byte[intArray.length];
     53        for (int i = 0; i < intArray.length; i++) {
     54            if (intArray[i] > Byte.MAX_VALUE - Byte.MIN_VALUE) {
     55                throw new IllegalArgumentException();
     56            }
     57            byteArray[i] = Integer.valueOf(intArray[i]).byteValue();
     58        }
     59        return byteArray;
     60    }
     61
     62    @RegisterExtension
     63    JOSMTestRules josmTestRules = new JOSMTestRules().preferences();
     64
     65    private Number bytesToVarInt(int... bytes) {
     66        byte[] byteArray = new byte[bytes.length];
     67        for (int i = 0; i < bytes.length; i++) {
     68            byteArray[i] = (byte) bytes[i];
     69        }
     70        return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE);
     71    }
     72
     73    /**
     74     * Test reading tile from Mapillary ( 14/3251/6258 )
     75     *
     76     * @throws IOException if there is a problem reading the file
     77     */
     78    @Test
     79    void testRead_14_3251_6258() throws IOException {
     80        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "14", "3251", "6258.mvt").toFile();
     81        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     82        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     83        assertEquals(2, records.size());
     84        List<Layer> layers = new ArrayList<>();
     85        for (ProtoBufRecord record : records) {
     86            if (record.getField() == Layer.LAYER_FIELD) {
     87                layers.add(new Layer(record.getBytes()));
     88            } else {
     89                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     90            }
     91        }
     92        Layer mapillarySequences = layers.get(0);
     93        Layer mapillaryPictures = layers.get(1);
     94        assertEquals("mapillary-sequences", mapillarySequences.getName());
     95        assertEquals("mapillary-images", mapillaryPictures.getName());
     96        assertEquals(2048, mapillarySequences.getExtent());
     97        assertEquals(2048, mapillaryPictures.getExtent());
     98
     99        assertEquals(1,
     100                mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).count());
     101        Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111)
     102                .findAny().orElse(null);
     103        assertEquals("jgxkXqVFM4jepMG3vP5Q9A", testSequence.getTags().get("key"));
     104        assertEquals("C15Ul6qVMfQFlzRcmQCLcA", testSequence.getTags().get("ikey"));
     105        assertEquals("x0hTY8cakpy0m3ui1GaG1A", testSequence.getTags().get("userkey"));
     106        assertEquals(Long.valueOf(1565196718638L), Long.valueOf(testSequence.getTags().get("captured_at")));
     107        assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
     108    }
     109
     110    @Test
     111    void testRead_17_26028_50060() throws IOException {
     112        File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "openinframap", "17", "26028", "50060.pbf")
     113                .toFile();
     114        InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
     115        Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
     116        List<Layer> layers = new ArrayList<>();
     117        for (ProtoBufRecord record : records) {
     118            if (record.getField() == Layer.LAYER_FIELD) {
     119                layers.add(new Layer(record.getBytes()));
     120            } else {
     121                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     122            }
     123        }
     124        assertEquals(19, layers.size());
     125        List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty())
     126                .collect(Collectors.toList());
     127        // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area
     128        assertEquals(5, dataLayers.size());
     129
     130        // power_generator_area was rendered incorrectly
     131        final Layer powerGeneratorArea = dataLayers.stream()
     132                .filter(layer -> "power_generator_area".equals(layer.getName())).findAny().orElse(null);
     133        assertNotNull(powerGeneratorArea);
     134        final int extent = powerGeneratorArea.getExtent();
     135        // 17/26028/50060 bounds
     136        VectorDataSet vectorDataSet = new VectorDataSet();
     137        MVTTile vectorTile1 = new MVTTile(new MapboxVectorTileSource(new ImageryInfo("Test info", "example.org")),
     138                26028, 50060, 17);
     139        vectorTile1.loadImage(Compression.getUncompressedFileInputStream(vectorTile));
     140        vectorDataSet.addTileData(vectorTile1);
     141        vectorDataSet.setZoom(17);
     142        final Way one = new Way();
     143        one.addNode(new Node(new LatLon(39.0687509, -108.5100816)));
     144        one.addNode(new Node(new LatLon(39.0687509, -108.5095751)));
     145        one.addNode(new Node(new LatLon(39.0687169, -108.5095751)));
     146        one.addNode(new Node(new LatLon(39.0687169, -108.5100816)));
     147        one.addNode(one.getNode(0));
     148        one.setOsmId(666293899, 2);
     149        final BBox searchBBox = one.getBBox();
     150        searchBBox.addPrimitive(one, 0.00001);
     151        final Collection<VectorNode> searchedNodes = vectorDataSet.searchNodes(searchBBox);
     152        final Collection<VectorWay> searchedWays = vectorDataSet.searchWays(searchBBox);
     153        assertEquals(4, searchedNodes.size());
     154    }
     155
     156    @Test
     157    void testReadVarInt() {
     158        assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0));
     159        assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1));
     160        assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f));
     161        // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array
     162        Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07);
     163        assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual,
     164                MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE),
     165                        Long.toBinaryString(actual.longValue())));
     166    }
     167
     168    /**
     169     * Test simple message.
     170     * Check that a simple message is readable
     171     *
     172     * @throws IOException - if an IO error occurs
     173     */
     174    @Test
     175    void testSimpleMessage() throws IOException {
     176        ProtoBufParser parser = new ProtoBufParser(new byte[] { (byte) 0x08, (byte) 0x96, (byte) 0x01 });
     177        ProtoBufRecord record = new ProtoBufRecord(parser);
     178        assertEquals(WireType.VARINT, record.getType());
     179        assertEquals(150, record.asUnsignedVarInt().intValue());
     180    }
     181
     182    @Test
     183    void testSingletonMultiPoint() throws IOException {
     184        Collection<ProtoBufRecord> records = new ProtoBufParser(new ByteArrayInputStream(toByteArray(
     185                new int[] { 0x1a, 0x2c, 0x78, 0x02, 0x0a, 0x03, 0x74, 0x6d, 0x70, 0x28, 0x80, 0x20, 0x1a, 0x04, 0x6e,
     186                        0x61, 0x6d, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65,
     187                        0x12, 0x0d, 0x18, 0x01, 0x12, 0x02, 0x00, 0x00, 0x22, 0x05, 0x09, 0xe0, 0x3e, 0x84, 0x27 })))
     188                                .allRecords();
     189        List<Layer> layers = new ArrayList<>();
     190        for (ProtoBufRecord record : records) {
     191            if (record.getField() == Layer.LAYER_FIELD) {
     192                layers.add(new Layer(record.getBytes()));
     193            } else {
     194                fail(MessageFormat.format("Invalid field {0}", record.getField()));
     195            }
     196        }
     197        assertEquals(1, layers.size());
     198        assertEquals(1, layers.get(0).getGeometry().size());
     199        Ellipse2D shape = (Ellipse2D) layers.get(0).getGeometry().iterator().next().getShapes().iterator().next();
     200        assertEquals(4016, shape.getCenterX());
     201        assertEquals(2498, shape.getCenterY());
     202    }
     203
     204    @Test
     205    void testZigZag() {
     206        assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue());
     207        assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue());
     208        assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue());
     209        assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue());
     210    }
     211}
  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6import org.junit.jupiter.api.Test;
     7
     8/**
     9 * Test class for {@link ProtoBufParser}
     10 * @author Taylor Smock
     11 * @since xxx
     12 */
     13class ProtoBufParserTest {
     14    /**
     15     * Check that we are appropriately converting values to the "smallest" type
     16     */
     17    @Test
     18    void testConvertLong() {
     19        // No casting due to auto conversions
     20        assertEquals(Byte.MAX_VALUE, ProtoBufParser.convertLong(Byte.MAX_VALUE));
     21        assertEquals(Byte.MIN_VALUE, ProtoBufParser.convertLong(Byte.MIN_VALUE));
     22        assertEquals(Short.MIN_VALUE, ProtoBufParser.convertLong(Short.MIN_VALUE));
     23        assertEquals(Short.MAX_VALUE, ProtoBufParser.convertLong(Short.MAX_VALUE));
     24        assertEquals(Integer.MAX_VALUE, ProtoBufParser.convertLong(Integer.MAX_VALUE));
     25        assertEquals(Integer.MIN_VALUE, ProtoBufParser.convertLong(Integer.MIN_VALUE));
     26        assertEquals(Long.MIN_VALUE, ProtoBufParser.convertLong(Long.MIN_VALUE));
     27        assertEquals(Long.MAX_VALUE, ProtoBufParser.convertLong(Long.MAX_VALUE));
     28    }
     29
     30    /**
     31     * Check that zig zags are appropriately encoded.
     32     */
     33    @Test
     34    void testEncodeZigZag() {
     35        assertEquals(0, ProtoBufParser.encodeZigZag(0).byteValue());
     36        assertEquals(1, ProtoBufParser.encodeZigZag(-1).byteValue());
     37        assertEquals(2, ProtoBufParser.encodeZigZag(1).byteValue());
     38        assertEquals(3, ProtoBufParser.encodeZigZag(-2).byteValue());
     39        assertEquals(254, ProtoBufParser.encodeZigZag(Byte.MAX_VALUE).shortValue());
     40        assertEquals(255, ProtoBufParser.encodeZigZag(Byte.MIN_VALUE).shortValue());
     41        assertEquals(65_534, ProtoBufParser.encodeZigZag(Short.MAX_VALUE).intValue());
     42        assertEquals(65_535, ProtoBufParser.encodeZigZag(Short.MIN_VALUE).intValue());
     43        // These integers check a possible boundary condition (the boundary between using the 32/64 bit encoding methods)
     44        assertEquals(4_294_967_292L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE - 1).longValue());
     45        assertEquals(4_294_967_293L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE + 1).longValue());
     46        assertEquals(4_294_967_294L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE).longValue());
     47        assertEquals(4_294_967_295L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE).longValue());
     48        assertEquals(4_294_967_296L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE + 1L).longValue());
     49        assertEquals(4_294_967_297L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE - 1L).longValue());
     50    }
     51}
  • test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.protobuf;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5
     6
     7import java.io.IOException;
     8
     9import org.junit.jupiter.api.Test;
     10
     11/**
     12 * Test class for specific {@link ProtoBufRecord} functionality
     13 */
     14class ProtoBufRecordTest {
     15    @Test
     16    void testFixed32() throws IOException {
     17        ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
     18        ProtoBufRecord thirtyTwoBit = new ProtoBufRecord(parser);
     19        assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType());
     20        assertEquals(1f, thirtyTwoBit.asFloat());
     21    }
     22
     23    @Test
     24    void testUnknown() throws IOException {
     25        ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
     26        ProtoBufRecord unknown = new ProtoBufRecord(parser);
     27        assertEquals(WireType.UNKNOWN, unknown.getType());
     28        assertEquals(0, unknown.getBytes().length);
     29    }
     30}