discovery/ble: avoid panic for invalid characteristics

  Sometimes Gatt reader reads invalid characteristics causing panic.
  I couldn't figure out what the root cause was. It can be a bug in
  our code or a bug in Android.

  This CL added more checks for invalid characteristics to avoid panic.

Change-Id: I54ad09dacd31181e84ab683ed68204e5a32daaa4
diff --git a/lib/discovery/plugins/ble/encoding.go b/lib/discovery/plugins/ble/encoding.go
index eb9f3fa..3b59a8e 100644
--- a/lib/discovery/plugins/ble/encoding.go
+++ b/lib/discovery/plugins/ble/encoding.go
@@ -108,8 +108,9 @@
 	return buf.Bytes(), nil
 }
 
-func unpackFromCharacteristics(cs map[string][]byte) [][]byte {
+func unpackFromCharacteristics(cs map[string][]byte) ([][]byte, error) {
 	var unpacked [][]byte
+	used := 0
 	for i := 0; i < maxNumPackedServices; i++ {
 		if _, ok := cs[cUuid(i, 0)]; !ok {
 			break
@@ -117,11 +118,13 @@
 
 		var splitted [][]byte
 		for j := 0; j < maxNumPackedCharacteristicsPerService; j++ {
-			c, ok := cs[cUuid(i, j)]
+			uuid := cUuid(i, j)
+			c, ok := cs[uuid]
 			if !ok {
 				break
 			}
 			splitted = append(splitted, c)
+			used++
 		}
 
 		if len(splitted) == 1 {
@@ -129,7 +132,10 @@
 			unpacked = append(unpacked, splitted[0])
 		} else {
 			n := 0
-			for _, d := range splitted {
+			for j, d := range splitted {
+				if j < len(splitted)-1 && len(d) != maxCharacteristicValueLen {
+					return nil, fmt.Errorf("invalid characteristics: %v", cs)
+				}
 				n += len(d)
 			}
 			merged := make([]byte, n)
@@ -140,7 +146,10 @@
 			unpacked = append(unpacked, merged)
 		}
 	}
-	return unpacked
+	if len(cs) != used {
+		return nil, fmt.Errorf("invalid characteristics: %v", cs)
+	}
+	return unpacked, nil
 }
 
 func decodeAdInfo(encoded []byte) (*idiscovery.AdInfo, error) {
diff --git a/lib/discovery/plugins/ble/encoding_test.go b/lib/discovery/plugins/ble/encoding_test.go
index 1d5bee3..86c7224 100644
--- a/lib/discovery/plugins/ble/encoding_test.go
+++ b/lib/discovery/plugins/ble/encoding_test.go
@@ -5,6 +5,7 @@
 package ble
 
 import (
+	"encoding/hex"
 	"math/rand"
 	"reflect"
 	"testing"
@@ -87,7 +88,11 @@
 		}
 
 		cs := packToCharacteristics(encodedAdinfos)
-		unpacked := unpackFromCharacteristics(cs)
+		unpacked, err := unpackFromCharacteristics(cs)
+		if err != nil {
+			t.Errorf("unpack failed: %v", err)
+			continue
+		}
 
 		for _, encoded := range unpacked {
 			adinfo, err := decodeAdInfo(encoded)
@@ -102,3 +107,38 @@
 		}
 	}
 }
+
+func TestDecodeInvalid(t *testing.T) {
+	tests := []map[string]string{
+		{"1234": "00"},                                 // Invalid uuid.
+		{"31ca10d5-0195-54fa-9344-25fcd7072f00": "00"}, // Invalid characteristic uuid.
+		{"31ca10d5-0195-54fa-9344-25fcd7072e10": "00"}, // Invalid characteristic uuid sequence.
+		{ //  Invalid characteristic split.
+			"31ca10d5-0195-54fa-9344-25fcd7072e00": "08eff4d54d979febed94a1b551b0316733762e696f2f782f7265662f73657276696365732f73796e63626173652f7365727665722f696e74657266616365732f53796e63fd01d4ce4d4ec33010c57171205c3b76c633b3321b2e8158f8b30db471459cf4442c90e00270390a4854e204416ff7167ffd5eaf360e5c68ee46322906622296926fe5069c030c9474510a7456bdf15afa10256934600329e92697f222163154ae0c12c92894d8f5706df4d2ca98729b2c3d967ee7230e797e6815515b0029fcf138896daddb7d9ea7fc14ebd8f2d844ac074efe348ac58f3e0df3c16d0f7ed87ffdcebd7c6b4fd3cea9ce0a799e627deee1faa86f172a7542010a044176addce75fee1db3ba5f2bf3e3c22c1925b3c13eb189a570c9bab0eda85b2dfefd2f1e2044feb11bc3d123fc1f7bc8bde5dc81e292348309b85afb67000000ffff01050164026462026462536465762e762e696f3a6f3a3630383934313830383235362d34337674666e6465747337396b6635686163386965756a746f383833373636302e617070732e676f6f676c6575736572636f6e74656e742e636f6d01730c73675f3535313632333837380273626b6465762e762e696f3a6f3a3630383934313830383235362d34337674666e6465747337396b6635686163386965756a746f383833373636302e617070732e676f6f676c6575736572636f6e74",
+			"31ca10d5-0195-54fa-9344-25fcd7072e01": "656e742e636f6d3a6461776e2e76616e616469756d40676d61696c2e636f6d0376697390016465762e762e696f3a6f3a3630383934313830383235362d34337674666e6465747337396b6635686163386965756a746f383833373636302e617070732e676f6f676c6575736572636f6e74656e742e636f6d3a6461776e2e76616e616469756d40676d61696c2e636f6d2c6465762e762e696f3a753a616c657866616e647269616e746f40676f6f676c652e636f6d005cb840d035a10ceaa1e9b7f238c057140000",
+		},
+		{ // Invalid characteristic value.
+			"31ca10d5-0195-54fa-9344-25fcd7072e00": "7337396b6635686163386965756a746f383833373636302e617070732e676f6f676c6575736572636f6e74656e742e636f6d3a6461776e2e76616e616469756d40676d61696c2e636f6d0164026462026462536465762e762e696f3a6f3a3630383934313830383235362d34337674666e6465747337396b6635686163386965756a746f383833373636302e617070732e676f6f676c6575736572636f6e74656e742e636f6d01732c6c6973745f6c697374735f4a4d786659727655694d364f596d4f706b356b797350425048324e366b77536f480013c6163e316e5359a9d3ca49fbbc57140000",
+		},
+	}
+
+	for i, test := range tests {
+		cs := make(map[string][]byte)
+		for k, v := range test {
+			c, err := hex.DecodeString(v)
+			if err != nil {
+				t.Fatal(err)
+			}
+			cs[k] = c
+		}
+
+		if unpacked, err := unpackFromCharacteristics(cs); err == nil {
+			for _, encoded := range unpacked {
+				_, err := decodeAdInfo(encoded)
+				if err == nil {
+					t.Errorf("[%d]: expect an error; but got none", i)
+				}
+			}
+		}
+	}
+}
diff --git a/lib/discovery/plugins/ble/scanner.go b/lib/discovery/plugins/ble/scanner.go
index bf0bd43..4f47040 100644
--- a/lib/discovery/plugins/ble/scanner.go
+++ b/lib/discovery/plugins/ble/scanner.go
@@ -145,11 +145,16 @@
 
 func (s *scanner) OnDiscovered(uuid string, characteristics map[string][]byte, rssi int) {
 	// TODO(jhahn): Add rssi to adinfo.
-	unpacked := unpackFromCharacteristics(characteristics)
+	unpacked, err := unpackFromCharacteristics(characteristics)
+	if err != nil {
+		s.ctx.Errorf("failed to unpack characteristics for %v: %v", uuid, err)
+		return
+	}
+
 	for _, encoded := range unpacked {
 		adinfo, err := decodeAdInfo(encoded)
 		if err != nil {
-			s.ctx.Error(err)
+			s.ctx.Errorf("failed to decode characteristics for %v: %v", uuid, err)
 			continue
 		}