More work on firmware detection, recognition and validation #234
Should be as robust as possible now.
This commit is contained in:
parent
1933e2bf10
commit
424d9cd142
|
@ -29,3 +29,5 @@ proguard/
|
|||
*.iml
|
||||
|
||||
MPChartLib
|
||||
|
||||
fw.dirs
|
||||
|
|
|
@ -5,18 +5,37 @@ import android.support.annotation.NonNull;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
|
||||
/**
|
||||
* Some helper methods for Mi1 and Mi1A firmware.
|
||||
*/
|
||||
public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractMi1FirmwareInfo.class);
|
||||
|
||||
private static final byte[] SINGLE_FW_HEADER = new byte[] {
|
||||
0,
|
||||
(byte)0x98,
|
||||
0,
|
||||
(byte)0x20,
|
||||
(byte)0x89,
|
||||
4,
|
||||
0,
|
||||
(byte)0x20
|
||||
};
|
||||
private static final int SINGLE_FW_HEADER_OFFSET = 0;
|
||||
|
||||
private static final int MI1_FW_BASE_OFFSET = 1056;
|
||||
|
||||
protected AbstractMi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) {
|
||||
super(wholeFirmwareBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleMiBandFirmware() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmwareOffset() {
|
||||
return 0;
|
||||
|
@ -51,11 +70,11 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
|||
|
||||
@Override
|
||||
protected boolean isGenerallySupportedFirmware() {
|
||||
if (!isSingleMiBandFirmware()) {
|
||||
LOG.warn("not a single firmware");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!isHeaderValid()) {
|
||||
LOG.info("unrecognized header");
|
||||
return false;
|
||||
}
|
||||
int majorVersion = getFirmwareVersionMajor();
|
||||
if (majorVersion == getSupportedMajorVersion()) {
|
||||
return true;
|
||||
|
@ -70,5 +89,19 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected boolean isHeaderValid() {
|
||||
// TODO: not sure if this is a correct check!
|
||||
return ArrayUtils.equals(SINGLE_FW_HEADER, wholeFirmwareBytes, SINGLE_FW_HEADER_OFFSET, SINGLE_FW_HEADER_OFFSET + SINGLE_FW_HEADER.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
super.checkValid();
|
||||
|
||||
if (wholeFirmwareBytes.length < SINGLE_FW_HEADER.length) {
|
||||
throw new IllegalArgumentException("firmware too small: " + wholeFirmwareBytes.length);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract int getSupportedMajorVersion();
|
||||
}
|
||||
|
|
|
@ -15,4 +15,9 @@ public abstract class AbstractMi1SFirmwareInfo extends AbstractMiFirmwareInfo {
|
|||
public boolean isGenerallyCompatibleWith(GBDevice device) {
|
||||
return MiBandConst.MI_1S.equals(device.getHardwareVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleMiBandFirmware() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
|
|||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.Arrays;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
|
||||
public abstract class AbstractMiFirmwareInfo {
|
||||
|
||||
/**
|
||||
* @param wholeFirmwareBytes
|
||||
* @return
|
||||
|
@ -20,6 +23,7 @@ public abstract class AbstractMiFirmwareInfo {
|
|||
throw new IllegalArgumentException("Unsupported data (maybe not even a firmware?).");
|
||||
}
|
||||
if (candidates.length == 1) {
|
||||
candidates[0].checkValid();
|
||||
return candidates[0];
|
||||
}
|
||||
throw new IllegalArgumentException("don't know for which device the firmware is, matches multiple devices");
|
||||
|
@ -58,6 +62,8 @@ public abstract class AbstractMiFirmwareInfo {
|
|||
|
||||
protected abstract boolean isGenerallySupportedFirmware();
|
||||
|
||||
protected abstract boolean isHeaderValid();
|
||||
|
||||
public abstract boolean isGenerallyCompatibleWith(GBDevice device);
|
||||
|
||||
public @NonNull byte[] getFirmwareBytes() {
|
||||
|
@ -72,12 +78,9 @@ public abstract class AbstractMiFirmwareInfo {
|
|||
throw new IllegalArgumentException("bad firmware version: " + version);
|
||||
}
|
||||
|
||||
public boolean isSingleMiBandFirmware() {
|
||||
// TODO: not sure if this is a correct check!
|
||||
if ((wholeFirmwareBytes[7] & 255) != 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public abstract boolean isSingleMiBandFirmware();
|
||||
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
}
|
||||
|
||||
public AbstractMiFirmwareInfo getFirst() {
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.support.annotation.Nullable;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
|
||||
/**
|
||||
* FW1 is Mi Band firmware
|
||||
* FW2 is heartrate firmware
|
||||
|
@ -12,6 +14,14 @@ import org.slf4j.LoggerFactory;
|
|||
public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfo.class);
|
||||
|
||||
private static final byte[] DOUBLE_FW_HEADER = new byte[] {
|
||||
(byte)0x78,
|
||||
(byte)0x75,
|
||||
(byte)0x63,
|
||||
(byte)0x6b
|
||||
};
|
||||
private static final int DOUBLE_FW_HEADER_OFFSET = 0;
|
||||
|
||||
private final Mi1SFirmwareInfoFW1 fw1Info;
|
||||
private final Mi1SFirmwareInfoFW2 fw2Info;
|
||||
|
||||
|
@ -21,6 +31,33 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
|
|||
fw2Info = new Mi1SFirmwareInfoFW2(wholeFirmwareBytes);
|
||||
}
|
||||
|
||||
protected boolean isHeaderValid() {
|
||||
// TODO: not sure if this is a correct check!
|
||||
return ArrayUtils.equals(DOUBLE_FW_HEADER, wholeFirmwareBytes, DOUBLE_FW_HEADER_OFFSET, DOUBLE_FW_HEADER_OFFSET + DOUBLE_FW_HEADER.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValid() throws IllegalArgumentException {
|
||||
super.checkValid();
|
||||
int firstEndIndex = getFirst().getFirmwareOffset() + getFirst().getFirmwareLength();
|
||||
if (getSecond().getFirmwareOffset() < firstEndIndex) {
|
||||
throw new IllegalArgumentException("Invalid firmware offsets/lengths: " + getLengthsOffsetsString());
|
||||
}
|
||||
int secondEndIndex = getSecond().getFirmwareOffset();
|
||||
if (wholeFirmwareBytes.length < firstEndIndex || wholeFirmwareBytes.length < secondEndIndex) {
|
||||
throw new IllegalArgumentException("Invalid firmware size, or invalid offsets/lengths: " + getLengthsOffsetsString());
|
||||
}
|
||||
if (getSecond().getFirmwareOffset() < firstEndIndex) {
|
||||
throw new IllegalArgumentException("Invalid firmware, second fw starts before first fw ends: " + firstEndIndex + "," + getSecond().getFirmwareOffset());
|
||||
}
|
||||
}
|
||||
|
||||
protected String getLengthsOffsetsString() {
|
||||
return getFirst().getFirmwareOffset() + "," + getFirst().getFirmwareLength()
|
||||
+ "; "
|
||||
+ getSecond().getFirmwareOffset() + "," + getSecond().getFirmwareLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractMiFirmwareInfo getFirst() {
|
||||
return fw1Info;
|
||||
|
@ -44,10 +81,11 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
|
|||
|
||||
@Override
|
||||
protected boolean isGenerallySupportedFirmware() {
|
||||
if (isSingleMiBandFirmware()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!isHeaderValid()) {
|
||||
LOG.info("unrecognized header");
|
||||
return false;
|
||||
}
|
||||
return fw1Info.isGenerallySupportedFirmware()
|
||||
&& fw2Info.isGenerallySupportedFirmware()
|
||||
&& fw1Info.getFirmwareBytes().length > 0
|
||||
|
|
|
@ -17,6 +17,11 @@ public class Mi1SFirmwareInfoFW1 extends AbstractMi1SFirmwareInfo {
|
|||
super(wholeFirmwareBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isHeaderValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmwareOffset() {
|
||||
return (wholeFirmwareBytes[12] & 255) << 24
|
||||
|
|
|
@ -16,6 +16,11 @@ public class Mi1SFirmwareInfoFW2 extends AbstractMi1SFirmwareInfo {
|
|||
super(wholeFirmwareBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isHeaderValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirmwareOffset() {
|
||||
return (wholeFirmwareBytes[26] & 255) << 24
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
public class ArrayUtils {
|
||||
/**
|
||||
* Checks the two given arrays for equality, but comparing only a subset of the second
|
||||
* array with the whole first array.
|
||||
* @param first the whole array to compare against
|
||||
* @param second the array, of which a subset shall be compared against the whole first array
|
||||
* @param secondStartIndex the start index (inclusive) of the second array from which to start the comparison
|
||||
* @param secondEndIndex the end index (exclusive) of the second array until which to compare
|
||||
* @return whether the first byte array is equal to the specified subset of the second byte array
|
||||
* @throws IllegalArgumentException when one of the arrays is null or start and end index are wrong
|
||||
*/
|
||||
public static boolean equals(byte[] first, byte[] second, int secondStartIndex, int secondEndIndex) {
|
||||
if (first == null) {
|
||||
throw new IllegalArgumentException("first must not be null");
|
||||
}
|
||||
if (second == null) {
|
||||
throw new IllegalArgumentException("second must not be null");
|
||||
}
|
||||
if (secondStartIndex >= secondEndIndex) {
|
||||
throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex");
|
||||
}
|
||||
if (second.length < secondEndIndex) {
|
||||
throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex");
|
||||
}
|
||||
if (first.length < secondEndIndex) {
|
||||
return false;
|
||||
}
|
||||
int len = secondEndIndex - secondStartIndex;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (first[i] != second[secondStartIndex + i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -12,7 +13,7 @@ import java.util.Arrays;
|
|||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
@Ignore("Disabled for travis -- needs vm parameter -DMiFirmwareDir=/path/to/firmware/directory/")
|
||||
//@Ignore("Disabled for travis -- needs vm parameter -DMiFirmwareDir=/path/to/firmware/directory/")
|
||||
public class FirmwareTest {
|
||||
|
||||
private static final long MAX_FILE_SIZE_BYTES = 1024 * 1024; // 1MB
|
||||
|
@ -24,6 +25,11 @@ public class FirmwareTest {
|
|||
private static final int SINGLE = 1;
|
||||
private static final int DOUBLE = 2;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupSuite() {
|
||||
getFirmwareDir(); // throws if firmware directory not available
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirmwareMi1() throws Exception {
|
||||
byte[] wholeFw = getFirmwareMi();
|
||||
|
@ -124,11 +130,11 @@ public class FirmwareTest {
|
|||
return info;
|
||||
}
|
||||
|
||||
private File getFirmwareDir() {
|
||||
private static File getFirmwareDir() {
|
||||
String path = System.getProperty("MiFirmwareDir");
|
||||
Assert.assertNotNull(path);
|
||||
Assert.assertNotNull("You must run this test with -DMiFirmwareDir=/path/to/directory/with/miband/firmwarefiles/", path);
|
||||
File dir = new File(path);
|
||||
Assert.assertTrue(dir.isDirectory());
|
||||
Assert.assertTrue("System property MiFirmwareDir should point to a directory continaing the Mi Band firmware files", dir.isDirectory());
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue