More work on firmware detection, recognition and validation #234

Should be as robust as possible now.
master
cpfeiffer 2016-03-21 23:41:37 +01:00
parent 1933e2bf10
commit 424d9cd142
9 changed files with 152 additions and 17 deletions

2
.gitignore vendored
View File

@ -29,3 +29,5 @@ proguard/
*.iml
MPChartLib
fw.dirs

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}