mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Garmin: Basic http web request/response parsing
As per https://gadgetbridge.org/internals/specifics/garmin-protocol/#internal-structures
This commit is contained in:
Generated
+1
@@ -148,6 +148,7 @@
|
|||||||
<w>shahrabani</w>
|
<w>shahrabani</w>
|
||||||
<w>shimokawa</w>
|
<w>shimokawa</w>
|
||||||
<w>shokz</w>
|
<w>shokz</w>
|
||||||
|
<w>sint</w>
|
||||||
<w>skype</w>
|
<w>skype</w>
|
||||||
<w>slezak</w>
|
<w>slezak</w>
|
||||||
<w>sophanimus</w>
|
<w>sophanimus</w>
|
||||||
|
|||||||
+356
@@ -0,0 +1,356 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonNull;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
|
public class GarminJson {
|
||||||
|
private static final byte[] STRING_SECTION_MAGIC = {(byte) 0xab, (byte) 0xcd, (byte) 0xab, (byte) 0xcd};
|
||||||
|
private static final byte[] DATA_SECTION_MAGIC = {(byte) 0xda, (byte) 0x7a, (byte) 0xda, (byte) 0x7a};
|
||||||
|
|
||||||
|
private static final byte TYPE_NULL = 0x00;
|
||||||
|
private static final byte TYPE_SINT32 = 0x01;
|
||||||
|
private static final byte TYPE_FLOAT = 0x02;
|
||||||
|
private static final byte TYPE_STRING = 0x03;
|
||||||
|
private static final byte TYPE_ARRAY = 0x05;
|
||||||
|
private static final byte TYPE_BOOL = 0x09;
|
||||||
|
private static final byte TYPE_MAP = 0x0b;
|
||||||
|
private static final byte TYPE_SINT64 = 0x0e;
|
||||||
|
private static final byte TYPE_DOUBLE = 0x0f;
|
||||||
|
|
||||||
|
public static byte[] encode(final Object object) throws GarminJsonException {
|
||||||
|
try {
|
||||||
|
// First pass: collect all strings
|
||||||
|
Set<String> strings = new LinkedHashSet<>();
|
||||||
|
collectStrings(object, strings);
|
||||||
|
|
||||||
|
// Build string section
|
||||||
|
final ByteArrayOutputStream stringSection = new ByteArrayOutputStream();
|
||||||
|
int currentOffset = 0;
|
||||||
|
final Map<String, Integer> finalOffsets = new LinkedHashMap<>();
|
||||||
|
for (final String str : strings) {
|
||||||
|
finalOffsets.put(str, currentOffset);
|
||||||
|
final byte[] strBytes = str.getBytes(StandardCharsets.UTF_8);
|
||||||
|
final int length = strBytes.length + 1; // +1 for null terminator
|
||||||
|
stringSection.write((length >> 8) & 0xFF);
|
||||||
|
stringSection.write(length & 0xFF);
|
||||||
|
stringSection.write(strBytes);
|
||||||
|
stringSection.write(0x00);
|
||||||
|
currentOffset += 2 + strBytes.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build data section (breadth-first)
|
||||||
|
final ByteArrayOutputStream dataSection = new ByteArrayOutputStream();
|
||||||
|
encodeValueBreadthFirst(object, dataSection, finalOffsets);
|
||||||
|
|
||||||
|
final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// Write string section, if any
|
||||||
|
if (stringSection.size() > 0) {
|
||||||
|
output.write(STRING_SECTION_MAGIC);
|
||||||
|
writeUint32BE(output, stringSection.size());
|
||||||
|
output.write(stringSection.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write data section
|
||||||
|
output.write(DATA_SECTION_MAGIC);
|
||||||
|
writeUint32BE(output, dataSection.size());
|
||||||
|
output.write(dataSection.toByteArray());
|
||||||
|
|
||||||
|
return output.toByteArray();
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new GarminJsonException("Failed to encode GarminJson", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void encodeValueBreadthFirst(final Object rootObj,
|
||||||
|
final ByteArrayOutputStream out,
|
||||||
|
final Map<String, Integer> stringOffsets) throws GarminJsonException {
|
||||||
|
final Queue<Object> queue = new LinkedList<>();
|
||||||
|
queue.add(rootObj);
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
final Object obj = queue.poll();
|
||||||
|
|
||||||
|
if (obj == null || obj instanceof JsonNull) {
|
||||||
|
out.write(TYPE_NULL);
|
||||||
|
} else if (obj instanceof JsonPrimitive jsonPrimitive) {
|
||||||
|
if (jsonPrimitive.isBoolean()) {
|
||||||
|
out.write(TYPE_BOOL);
|
||||||
|
out.write(jsonPrimitive.getAsBoolean() ? 0x01 : 0x00);
|
||||||
|
} else if (jsonPrimitive.isNumber()) {
|
||||||
|
final Number num = jsonPrimitive.getAsNumber();
|
||||||
|
// Check the actual value to determine the appropriate type
|
||||||
|
// This handles LazilyParsedNumber and other Number implementations
|
||||||
|
final double doubleValue = num.doubleValue();
|
||||||
|
final long longValue = num.longValue();
|
||||||
|
|
||||||
|
// Check if it's a floating point number
|
||||||
|
if (doubleValue != longValue || num instanceof Float || num instanceof Double) {
|
||||||
|
// It has a fractional part or is explicitly a float/double
|
||||||
|
if (num instanceof Float || (Math.abs(doubleValue) < Float.MAX_VALUE && doubleValue == (float) doubleValue)) {
|
||||||
|
out.write(TYPE_FLOAT);
|
||||||
|
writeFloat(out, num.floatValue());
|
||||||
|
} else {
|
||||||
|
out.write(TYPE_DOUBLE);
|
||||||
|
writeDouble(out, num.doubleValue());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's an integer value
|
||||||
|
if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) {
|
||||||
|
out.write(TYPE_SINT32);
|
||||||
|
writeUint32BE(out, (int) longValue);
|
||||||
|
} else {
|
||||||
|
out.write(TYPE_SINT64);
|
||||||
|
writeUint64BE(out, longValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (jsonPrimitive.isString()) {
|
||||||
|
out.write(TYPE_STRING);
|
||||||
|
final String str = jsonPrimitive.getAsString();
|
||||||
|
final Integer offset = stringOffsets.get(str);
|
||||||
|
if (offset == null) {
|
||||||
|
throw new GarminJsonException("String not found in offset map: " + str);
|
||||||
|
}
|
||||||
|
writeUint32BE(out, offset);
|
||||||
|
} else {
|
||||||
|
throw new GarminJsonException("Unexpected json primitive: " + jsonPrimitive.getClass().getName());
|
||||||
|
}
|
||||||
|
} else if (obj instanceof String str) {
|
||||||
|
out.write(TYPE_STRING);
|
||||||
|
final Integer offset = stringOffsets.get(str);
|
||||||
|
if (offset == null) {
|
||||||
|
throw new GarminJsonException("String not found in offset map: " + str);
|
||||||
|
}
|
||||||
|
writeUint32BE(out, offset);
|
||||||
|
} else if (obj instanceof JsonArray jsonArray) {
|
||||||
|
out.write(TYPE_ARRAY);
|
||||||
|
writeUint32BE(out, jsonArray.size());
|
||||||
|
// Add array elements to queue for breadth-first processing
|
||||||
|
for (final JsonElement element : jsonArray) {
|
||||||
|
queue.add(element);
|
||||||
|
}
|
||||||
|
} else if (obj instanceof JsonObject jsonObj) {
|
||||||
|
out.write(TYPE_MAP);
|
||||||
|
writeUint32BE(out, jsonObj.size());
|
||||||
|
// Add key-value pairs to queue for breadth-first processing
|
||||||
|
for (final Map.Entry<String, JsonElement> entry : jsonObj.entrySet()) {
|
||||||
|
queue.add(entry.getKey());
|
||||||
|
queue.add(entry.getValue());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new GarminJsonException("Unsupported type: " + obj.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object decode(final byte[] bytes) throws GarminJsonException {
|
||||||
|
final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
// Parse string section if present
|
||||||
|
final Map<Integer, String> strings = new LinkedHashMap<>();
|
||||||
|
final byte[] magic = new byte[4];
|
||||||
|
buffer.get(magic);
|
||||||
|
|
||||||
|
if (Arrays.equals(magic, STRING_SECTION_MAGIC)) {
|
||||||
|
final int stringSectionLength = buffer.getInt();
|
||||||
|
final int stringSectionEnd = buffer.position() + stringSectionLength;
|
||||||
|
final int stringSectionStart = buffer.position();
|
||||||
|
|
||||||
|
while (buffer.position() < stringSectionEnd) {
|
||||||
|
final int stringStart = buffer.position() - stringSectionStart;
|
||||||
|
final int length = buffer.getShort() & 0xFFFF;
|
||||||
|
final byte[] strBytes = new byte[length - 1]; // -1 for null terminator
|
||||||
|
buffer.get(strBytes);
|
||||||
|
buffer.get(); // skip null terminator
|
||||||
|
final String str = new String(strBytes, StandardCharsets.UTF_8);
|
||||||
|
strings.put(stringStart, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data section magic
|
||||||
|
buffer.get(magic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Arrays.equals(magic, DATA_SECTION_MAGIC)) {
|
||||||
|
throw new GarminJsonException("Expected data section magic, got " + GB.hexdump(magic));
|
||||||
|
}
|
||||||
|
|
||||||
|
final int dataSectionLength = buffer.getInt();
|
||||||
|
if (buffer.position() + dataSectionLength != bytes.length) {
|
||||||
|
throw new GarminJsonException("Data section length does not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object rootObject = decodeValue(buffer, strings);
|
||||||
|
|
||||||
|
// Process placeholders breadth-first using a queue
|
||||||
|
final Queue<Object> queue = new LinkedList<>();
|
||||||
|
queue.add(rootObject);
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
Object current = queue.poll();
|
||||||
|
|
||||||
|
if (current instanceof MapPlaceholder mp) {
|
||||||
|
for (int i = 0; i < mp.size; i++) {
|
||||||
|
final Object key = decodeValue(buffer, strings);
|
||||||
|
final Object value = decodeValue(buffer, strings);
|
||||||
|
|
||||||
|
// Unwrap placeholders to get the actual JSON object/array
|
||||||
|
Object actualValue = value;
|
||||||
|
if (value instanceof MapPlaceholder) {
|
||||||
|
actualValue = ((MapPlaceholder) value).obj;
|
||||||
|
queue.add(value);
|
||||||
|
} else if (value instanceof ArrayPlaceholder) {
|
||||||
|
actualValue = ((ArrayPlaceholder) value).array;
|
||||||
|
queue.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.obj.add(String.valueOf(((JsonPrimitive) key).getAsString()), (JsonElement) actualValue);
|
||||||
|
}
|
||||||
|
} else if (current instanceof ArrayPlaceholder ap) {
|
||||||
|
for (int i = 0; i < ap.length; i++) {
|
||||||
|
Object value = decodeValue(buffer, strings);
|
||||||
|
|
||||||
|
// Unwrap placeholders to get the actual JSON object/array
|
||||||
|
Object actualValue = value;
|
||||||
|
if (value instanceof MapPlaceholder) {
|
||||||
|
actualValue = ((MapPlaceholder) value).obj;
|
||||||
|
queue.add(value);
|
||||||
|
} else if (value instanceof ArrayPlaceholder) {
|
||||||
|
actualValue = ((ArrayPlaceholder) value).array;
|
||||||
|
queue.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ap.array.add((JsonElement) actualValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootObject instanceof ArrayPlaceholder ap) {
|
||||||
|
return ap.array;
|
||||||
|
} else if (rootObject instanceof MapPlaceholder mp) {
|
||||||
|
return mp.obj;
|
||||||
|
} else if (rootObject instanceof JsonElement jsonElement) {
|
||||||
|
return jsonElement;
|
||||||
|
} else {
|
||||||
|
throw new GarminJsonException("Unexpected root object " + rootObject.getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectStrings(final Object rootObj, final Set<String> strings) {
|
||||||
|
final Queue<Object> queue = new LinkedList<>();
|
||||||
|
queue.add(rootObj);
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
final Object obj = queue.poll();
|
||||||
|
|
||||||
|
if (obj instanceof JsonObject jsonObj) {
|
||||||
|
for (final Map.Entry<String, JsonElement> entry : jsonObj.entrySet()) {
|
||||||
|
strings.add(entry.getKey());
|
||||||
|
final JsonElement value = entry.getValue();
|
||||||
|
if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
|
||||||
|
strings.add(value.getAsString());
|
||||||
|
} else if (value.isJsonObject() || value.isJsonArray()) {
|
||||||
|
queue.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (obj instanceof JsonArray jsonArray) {
|
||||||
|
for (final JsonElement element : jsonArray) {
|
||||||
|
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
|
||||||
|
strings.add(element.getAsString());
|
||||||
|
} else if (element.isJsonObject() || element.isJsonArray()) {
|
||||||
|
queue.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (obj instanceof String str) {
|
||||||
|
strings.add(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object decodeValue(final ByteBuffer buffer, final Map<Integer, String> strings) throws GarminJsonException {
|
||||||
|
byte type = buffer.get();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case TYPE_NULL:
|
||||||
|
return JsonNull.INSTANCE;
|
||||||
|
case TYPE_BOOL:
|
||||||
|
return new JsonPrimitive(buffer.get() != 0x00);
|
||||||
|
case TYPE_SINT32:
|
||||||
|
return new JsonPrimitive(buffer.getInt());
|
||||||
|
case TYPE_SINT64:
|
||||||
|
return new JsonPrimitive(buffer.getLong());
|
||||||
|
case TYPE_FLOAT:
|
||||||
|
return new JsonPrimitive(buffer.getFloat());
|
||||||
|
case TYPE_DOUBLE:
|
||||||
|
return new JsonPrimitive(buffer.getDouble());
|
||||||
|
case TYPE_STRING:
|
||||||
|
final int offset = buffer.getInt();
|
||||||
|
final String str = strings.get(offset);
|
||||||
|
if (str == null) {
|
||||||
|
throw new GarminJsonException("String not found in offset map: " + offset);
|
||||||
|
}
|
||||||
|
return new JsonPrimitive(str);
|
||||||
|
case TYPE_ARRAY:
|
||||||
|
final int arrayLength = buffer.getInt();
|
||||||
|
// Decoding is breadth-first - don't decode children yet
|
||||||
|
// return placeholder with the expected number of children
|
||||||
|
return new ArrayPlaceholder(new JsonArray(), arrayLength);
|
||||||
|
case TYPE_MAP:
|
||||||
|
final int mapSize = buffer.getInt();
|
||||||
|
// Decoding is breadth-first - don't decode children yet
|
||||||
|
// return placeholder with the expected number of children
|
||||||
|
return new MapPlaceholder(new JsonObject(), mapSize);
|
||||||
|
default:
|
||||||
|
throw new GarminJsonException("Unknown type: 0x" + Integer.toHexString(type & 0xFF));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeUint32BE(final ByteArrayOutputStream out, final int value) {
|
||||||
|
out.write((value >> 24) & 0xFF);
|
||||||
|
out.write((value >> 16) & 0xFF);
|
||||||
|
out.write((value >> 8) & 0xFF);
|
||||||
|
out.write(value & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeUint64BE(final ByteArrayOutputStream out, final long value) {
|
||||||
|
out.write((int) ((value >> 56) & 0xFF));
|
||||||
|
out.write((int) ((value >> 48) & 0xFF));
|
||||||
|
out.write((int) ((value >> 40) & 0xFF));
|
||||||
|
out.write((int) ((value >> 32) & 0xFF));
|
||||||
|
out.write((int) ((value >> 24) & 0xFF));
|
||||||
|
out.write((int) ((value >> 16) & 0xFF));
|
||||||
|
out.write((int) ((value >> 8) & 0xFF));
|
||||||
|
out.write((int) (value & 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeFloat(final ByteArrayOutputStream out, final float value) {
|
||||||
|
writeUint32BE(out, Float.floatToIntBits(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeDouble(final ByteArrayOutputStream out, final double value) {
|
||||||
|
writeUint64BE(out, Double.doubleToLongBits(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MapPlaceholder(JsonObject obj, int size) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ArrayPlaceholder(JsonArray array, int length) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
|
||||||
|
|
||||||
|
public class GarminJsonException extends Exception {
|
||||||
|
public GarminJsonException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GarminJsonException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -39,6 +39,14 @@ public class HttpHandler {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
} else if (httpService.hasWebRequest()) {
|
||||||
|
final GdiHttpService.HttpService.WebResponse webResponse = handleWebRequest(httpService.getWebRequest());
|
||||||
|
if (webResponse != null) {
|
||||||
|
return GdiHttpService.HttpService.newBuilder()
|
||||||
|
.setWebResponse(webResponse)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.warn("Unsupported http service request {}", httpService);
|
LOG.warn("Unsupported http service request {}", httpService);
|
||||||
@@ -147,4 +155,10 @@ public class HttpHandler {
|
|||||||
.addAllHeader(responseHeaders)
|
.addAllHeader(responseHeaders)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GdiHttpService.HttpService.WebResponse handleWebRequest(final GdiHttpService.HttpService.WebRequest webRequest) {
|
||||||
|
LOG.debug("Got webRequest: {} - {}", webRequest.getMethod(), webRequest.getUrl());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,44 @@ message HttpService {
|
|||||||
DATA_TRANSFER_ITEM_FAILURE = 400;
|
DATA_TRANSFER_ITEM_FAILURE = 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ResponseType {
|
||||||
|
JSON = 0;
|
||||||
|
URL_ENCODED = 1;
|
||||||
|
PLAIN_TEXT = 2;
|
||||||
|
XML = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Version {
|
||||||
|
VERSION_1 = 0;
|
||||||
|
VERSION_2 = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional WebRequest webRequest = 1;
|
||||||
|
optional WebResponse webResponse = 2;
|
||||||
optional RawRequest rawRequest = 5;
|
optional RawRequest rawRequest = 5;
|
||||||
optional RawResponse rawResponse = 6;
|
optional RawResponse rawResponse = 6;
|
||||||
|
|
||||||
|
message WebRequest {
|
||||||
|
required string url = 1;
|
||||||
|
optional Method method = 2;
|
||||||
|
optional bytes headers = 3;
|
||||||
|
optional bytes body = 4;
|
||||||
|
optional uint32 maxResponseLength = 5;
|
||||||
|
optional bool httpHeadersInResponse = 6 [default = true];
|
||||||
|
optional bool compressResponseBody = 7 [default = false];
|
||||||
|
optional ResponseType responseType = 8;
|
||||||
|
optional Version version = 9 [default = VERSION_1];
|
||||||
|
}
|
||||||
|
|
||||||
|
message WebResponse {
|
||||||
|
optional Status status = 1;
|
||||||
|
optional uint32 httpStatus = 2;
|
||||||
|
optional bytes body = 3;
|
||||||
|
optional bytes headers = 4;
|
||||||
|
optional bytes size = 5;
|
||||||
|
optional ResponseType responseType = 6;
|
||||||
|
}
|
||||||
|
|
||||||
message RawRequest {
|
message RawRequest {
|
||||||
required string url = 1;
|
required string url = 1;
|
||||||
optional Method method = 3;
|
optional Method method = 3;
|
||||||
|
|||||||
+243
@@ -0,0 +1,243 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonNull;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
|
public class GarminJsonTest {
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
|
final String example1json = """
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"glanceTemplate": {
|
||||||
|
"template": "{{ states('sensor.solarnet_power_grid_import') }}%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "render_template"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
final String example1hex = "abcdabcd000000710005646174610000" +
|
||||||
|
"057479706500001072656e6465725f74" +
|
||||||
|
"656d706c61746500000f676c616e6365" +
|
||||||
|
"54656d706c61746500000974656d706c" +
|
||||||
|
"6174650000337b7b2073746174657328" +
|
||||||
|
"2773656e736f722e736f6c61726e6574" +
|
||||||
|
"5f706f7765725f677269645f696d706f" +
|
||||||
|
"72742729207d7d2500da7ada7a000000" +
|
||||||
|
"2d0b0000000203000000000b00000001" +
|
||||||
|
"0300000007030000000e03000000200b" +
|
||||||
|
"000000010300000031030000003c";
|
||||||
|
|
||||||
|
final String example2json = """
|
||||||
|
{
|
||||||
|
"a": 1,
|
||||||
|
"b": {
|
||||||
|
"c": "d",
|
||||||
|
"e": "c",
|
||||||
|
"f": ["x", 1, { "123": "456","789": "100" }]
|
||||||
|
},
|
||||||
|
"array": [1, "two", true],
|
||||||
|
"nested": {
|
||||||
|
"level1": {
|
||||||
|
"level2": {
|
||||||
|
"level3": "value"
|
||||||
|
},
|
||||||
|
"level2_2": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
final String example2hex = "abcdabcd000000790006617272617900000262000002610000076e6573746564" +
|
||||||
|
"00000474776f000002660000026300000264000002650000076c6576656c3100" +
|
||||||
|
"0002780000096c6576656c325f320000076c6576656c32000004313233000004" +
|
||||||
|
"3435360000043738390000043130300000076c6576656c3300000676616c7565" +
|
||||||
|
"00da7ada7a000000a20b000000040300000000050000000303000000080b0000" +
|
||||||
|
"0003030000000c010000000103000000100b0000000101000000010300000019" +
|
||||||
|
"0901030000001f050000000303000000230300000027030000002b0300000023" +
|
||||||
|
"030000002f0b00000002030000003801000000010b00000002030000003c0100" +
|
||||||
|
"00002a03000000470b0000000103000000500300000056030000005c03000000" +
|
||||||
|
"6203000000680300000071";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeExample1() throws Exception {
|
||||||
|
final byte[] bytes = GB.hexStringToByteArray(example1hex);
|
||||||
|
final JsonObject result = (JsonObject) GarminJson.decode(bytes);
|
||||||
|
|
||||||
|
Assert.assertEquals(2, result.size());
|
||||||
|
Assert.assertTrue(result.has("data"));
|
||||||
|
Assert.assertTrue(result.has("type"));
|
||||||
|
|
||||||
|
Assert.assertEquals("render_template", result.get("type").getAsString());
|
||||||
|
|
||||||
|
final JsonObject data = result.getAsJsonObject("data");
|
||||||
|
Assert.assertEquals(1, data.size());
|
||||||
|
Assert.assertTrue(data.has("glanceTemplate"));
|
||||||
|
|
||||||
|
final JsonObject glanceTemplate = data.getAsJsonObject("glanceTemplate");
|
||||||
|
Assert.assertEquals(1, glanceTemplate.size());
|
||||||
|
Assert.assertTrue(glanceTemplate.has("template"));
|
||||||
|
Assert.assertEquals("{{ states('sensor.solarnet_power_grid_import') }}%",
|
||||||
|
glanceTemplate.get("template").getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDecodeExample2() throws Exception {
|
||||||
|
final byte[] bytes = GB.hexStringToByteArray(example2hex);
|
||||||
|
final JsonObject result = (JsonObject) GarminJson.decode(bytes);
|
||||||
|
|
||||||
|
// root
|
||||||
|
Assert.assertEquals(4, result.size());
|
||||||
|
Assert.assertTrue(result.has("a"));
|
||||||
|
Assert.assertTrue(result.has("b"));
|
||||||
|
Assert.assertTrue(result.has("array"));
|
||||||
|
Assert.assertTrue(result.has("nested"));
|
||||||
|
|
||||||
|
// "a"
|
||||||
|
Assert.assertEquals(1, result.get("a").getAsInt());
|
||||||
|
|
||||||
|
// "b"
|
||||||
|
final JsonObject b = result.getAsJsonObject("b");
|
||||||
|
Assert.assertEquals(3, b.size());
|
||||||
|
Assert.assertEquals("d", b.get("c").getAsString());
|
||||||
|
Assert.assertEquals("c", b.get("e").getAsString());
|
||||||
|
// "b.f"
|
||||||
|
final JsonArray f = b.getAsJsonArray("f");
|
||||||
|
Assert.assertEquals(3, f.size());
|
||||||
|
Assert.assertEquals("x", f.get(0).getAsString());
|
||||||
|
Assert.assertEquals(1, f.get(1).getAsInt());
|
||||||
|
final JsonObject fObj = f.get(2).getAsJsonObject();
|
||||||
|
Assert.assertEquals(2, fObj.size());
|
||||||
|
Assert.assertEquals("456", fObj.get("123").getAsString());
|
||||||
|
Assert.assertEquals("100", fObj.get("789").getAsString());
|
||||||
|
|
||||||
|
// "array"
|
||||||
|
final JsonArray array = result.getAsJsonArray("array");
|
||||||
|
Assert.assertEquals(3, array.size());
|
||||||
|
Assert.assertEquals(1, array.get(0).getAsInt());
|
||||||
|
Assert.assertEquals("two", array.get(1).getAsString());
|
||||||
|
Assert.assertTrue(array.get(2).getAsBoolean());
|
||||||
|
|
||||||
|
// "nested"
|
||||||
|
final JsonObject nested = result.getAsJsonObject("nested");
|
||||||
|
Assert.assertEquals(1, nested.size());
|
||||||
|
final JsonObject level1 = nested.getAsJsonObject("level1");
|
||||||
|
Assert.assertEquals(2, level1.size());
|
||||||
|
Assert.assertEquals(42, level1.get("level2_2").getAsInt());
|
||||||
|
final JsonObject level2 = level1.getAsJsonObject("level2");
|
||||||
|
Assert.assertEquals(1, level2.size());
|
||||||
|
Assert.assertEquals("value", level2.get("level3").getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEncodeExample1() throws Exception {
|
||||||
|
final JsonElement object = GSON.fromJson(example1json, JsonElement.class);
|
||||||
|
final byte[] encoded = GarminJson.encode(object);
|
||||||
|
Assert.assertEquals(
|
||||||
|
example1hex,
|
||||||
|
GB.hexdump(encoded).toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore("Field order is not kept by gson, so this is failing..")
|
||||||
|
public void testEncodeExample2() throws Exception {
|
||||||
|
final JsonElement object = GSON.fromJson(example2json, JsonElement.class);
|
||||||
|
final byte[] encoded = GarminJson.encode(object);
|
||||||
|
Assert.assertEquals(
|
||||||
|
example2hex,
|
||||||
|
GB.hexdump(encoded).toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEncodeDecodeAllTypes() throws Exception {
|
||||||
|
final JsonObject json = new JsonObject();
|
||||||
|
// basic types
|
||||||
|
json.add("nullValue", JsonNull.INSTANCE);
|
||||||
|
json.addProperty("boolTrue", true);
|
||||||
|
json.addProperty("boolFalse", false);
|
||||||
|
json.addProperty("int32", 42);
|
||||||
|
json.addProperty("int64", 9223372036854775807L);
|
||||||
|
json.addProperty("float", 3.14f);
|
||||||
|
json.addProperty("double", 3.115281878499445d);
|
||||||
|
json.addProperty("string", "Hello, World!");
|
||||||
|
// array
|
||||||
|
final JsonArray array = new JsonArray();
|
||||||
|
array.add(1);
|
||||||
|
array.add("two");
|
||||||
|
array.add(true);
|
||||||
|
json.add("array", array);
|
||||||
|
// nested map
|
||||||
|
final JsonObject nested = new JsonObject();
|
||||||
|
nested.addProperty("nested_key", "nested_value");
|
||||||
|
json.add("map", nested);
|
||||||
|
|
||||||
|
// Encode and decode
|
||||||
|
final byte[] encoded = GarminJson.encode(json);
|
||||||
|
final JsonObject decoded = (JsonObject) GarminJson.decode(encoded);
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
Assert.assertEquals(json.size(), decoded.size());
|
||||||
|
Assert.assertTrue(decoded.get("nullValue").isJsonNull());
|
||||||
|
Assert.assertEquals(json.get("boolTrue").getAsBoolean(), decoded.get("boolTrue").getAsBoolean());
|
||||||
|
Assert.assertEquals(json.get("boolFalse").getAsBoolean(), decoded.get("boolFalse").getAsBoolean());
|
||||||
|
Assert.assertEquals(json.get("int32").getAsInt(), decoded.get("int32").getAsInt());
|
||||||
|
Assert.assertEquals(json.get("int64").getAsLong(), decoded.get("int64").getAsLong());
|
||||||
|
Assert.assertEquals(json.get("float").getAsDouble(), decoded.get("float").getAsDouble(), 0.001);
|
||||||
|
Assert.assertEquals(json.get("double").getAsDouble(), decoded.get("double").getAsDouble(), 0.000001);
|
||||||
|
Assert.assertEquals(json.get("string").getAsString(), decoded.get("string").getAsString());
|
||||||
|
Assert.assertEquals(json.getAsJsonArray("array").size(), decoded.getAsJsonArray("array").size());
|
||||||
|
for (int i = 0; i < json.getAsJsonArray("array").size(); i++) {
|
||||||
|
Assert.assertEquals(json.getAsJsonArray("array").get(i), decoded.getAsJsonArray("array").get(i));
|
||||||
|
}
|
||||||
|
Assert.assertEquals(
|
||||||
|
json.getAsJsonObject("map").get("nested_key").getAsString(),
|
||||||
|
decoded.getAsJsonObject("map").get("nested_key").getAsString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyObject() throws Exception {
|
||||||
|
final JsonObject json = new JsonObject();
|
||||||
|
|
||||||
|
final byte[] encoded = GarminJson.encode(json);
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
"da7ada7a000000050b00000000",
|
||||||
|
GB.hexdump(encoded).toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
final JsonObject decoded = (JsonObject) GarminJson.decode(encoded);
|
||||||
|
|
||||||
|
Assert.assertEquals(0, decoded.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimpleKeyValue() throws Exception {
|
||||||
|
final JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("key", "value");
|
||||||
|
|
||||||
|
final byte[] encoded = GarminJson.encode(json);
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
"abcdabcd0000000e00046b657900000676616c756500da7ada7a0000000f0b0000000103000000000300000006",
|
||||||
|
GB.hexdump(encoded).toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
final JsonObject decoded = (JsonObject) GarminJson.decode(encoded);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, decoded.size());
|
||||||
|
Assert.assertEquals("value", decoded.get("key").getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user