Repository: blevesearch/bleve
Pull request #1704 (2022-06-15 15:06:02)

Adding input validations for spatial coordinates.


Patch:
diff --git a/geo/parse.go b/geo/parse.go
index 7725da0a..87ac091b 100644
--- a/geo/parse.go
+++ b/geo/parse.go
@@ -229,7 +229,10 @@ func extract2DCoordinates(thing interface{}) [][]float64 {
 		for j := 0; j < thingVal.Len(); j++ {
 			edges := thingVal.Index(j).Interface()
 			if es, ok := edges.([]interface{}); ok {
-				rv = append(rv, extractCoordinates(es))
+				v := extractCoordinates(es)
+				if len(v) == 2 {
+					rv = append(rv, v)
+				}
 			}
 		}
 
@@ -272,29 +275,38 @@ func extract4DCoordinates(thing interface{}) (rv [][][][]float64) {
 	return rv
 }
 
-func extractGeoShape(thing interface{}) ([][][][]float64, string, bool) {
+func ParseGeoShapeField(thing interface{}) (interface{}, string, error) {
 	thingVal := reflect.ValueOf(thing)
 	if !thingVal.IsValid() {
-		return nil, "", false
+		return nil, "", nil
 	}
 
+	var shape string
 	var coordValue interface{}
-	var typ string
+
 	if thingVal.Kind() == reflect.Map {
 		iter := thingVal.MapRange()
 		for iter.Next() {
 			if iter.Key().String() == "type" {
-				typ = iter.Value().Interface().(string)
-				// make the shape type case insensitive.
-				typ = strings.ToLower(typ)
+				shape = iter.Value().Interface().(string)
 				continue
 			}
+
 			if iter.Key().String() == "coordinates" {
 				coordValue = iter.Value().Interface()
 			}
 		}
 	}
 
+	return coordValue, strings.ToLower(shape), nil
+}
+
+func extractGeoShape(thing interface{}) ([][][][]float64, string, bool) {
+	coordValue, typ, err := ParseGeoShapeField(thing)
+	if err != nil {
+		return nil, "", false
+	}
+
 	return ExtractGeoShapeCoordinates(coordValue, typ)
 }
 
@@ -383,23 +395,68 @@ func ExtractGeoShapeCoordinates(coordValue interface{},
 	typ string) ([][][][]float64, string, bool) {
 	var rv [][][][]float64
 	if typ == PointType {
-		rv = [][][][]float64{{{extractCoordinates(coordValue)}}}
+		point := extractCoordinates(coordValue)
+
+		// ignore the contents with invalid entry.
+		if len(point) < 2 {
+			return nil, typ, false
+		}
+
+		rv = [][][][]float64{{{point}}}
 		return rv, typ, true
 	}
 
 	if typ == MultiPointType || typ == LineStringType ||
 		typ == EnvelopeType {
-		rv = [][][][]float64{{extract2DCoordinates(coordValue)}}
+		coords := extract2DCoordinates(coordValue)
+
+		// ignore the contents with invalid entry.
+		if len(coords) == 0 {
+			return nil, typ, false
+		}
+
+		if typ == EnvelopeType && len(coords) != 2 {
+			return nil, typ, false
+		}
+
+		if typ == LineStringType && len(coords) < 2 {
+			return nil, typ, false
+		}
+
+		rv = [][][][]float64{{coords}}
 		return rv, typ, true
 	}
 
 	if typ == PolygonType || typ == MultiLineStringType {
-		rv = [][][][]float64{extract3DCoordinates(coordValue)}
+		coords := extract3DCoordinates(coordValue)
+
+		// ignore the contents with invalid entry.
+		if len(coords) == 0 {
+			return nil, typ, false
+		}
+
+		if typ == PolygonType && len(coords[0]) < 3 ||
+			typ == MultiLineStringType && len(coords[0]) < 2 {
+			return nil, typ, false
+		}
+
+		rv = [][][][]float64{coords}
 		return rv, typ, true
 	}
 
 	if typ == MultiPolygonType {
 		rv = extract4DCoordinates(coordValue)
+
+		// ignore the contents with invalid entry.
+		if len(rv) == 0 || len(rv[0]) == 0 {
+			return nil, typ, false
+
+		}
+
+		if len(rv[0][0]) < 3 {
+			return nil, typ, false
+		}
+
 		return rv, typ, true
 	}
 
diff --git a/mapping/field.go b/mapping/field.go
index 962696eb..7a7fc82f 100644
--- a/mapping/field.go
+++ b/mapping/field.go
@@ -18,8 +18,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"net"
-	"reflect"
-	"strings"
 	"time"
 
 	"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
@@ -315,36 +313,9 @@ func (fm *FieldMapping) processIP(ip net.IP, pathString string, path []string, i
 	}
 }
 
-func parseGeoShapeField(thing interface{}) (interface{}, string, error) {
-	thingVal := reflect.ValueOf(thing)
-	if !thingVal.IsValid() {
-		return nil, "", nil
-	}
-
-	var shape string
-	var coordValue interface{}
-
-	if thingVal.Kind() == reflect.Map {
-		iter := thingVal.MapRange()
-		for iter.Next() {
-			if iter.Key().String() == "type" {
-				shape = iter.Value().Interface().(string)
-				continue
-
-			}
-
-			if iter.Key().String() == "coordinates" {
-				coordValue = iter.Value().Interface()
-			}
-		}
-	}
-
-	return coordValue, strings.ToLower(shape), nil
-}
-
 func (fm *FieldMapping) processGeoShape(propertyMightBeGeoShape interface{},
 	pathString string, path []string, indexes []uint64, context *walkContext) {
-	coordValue, shape, err := parseGeoShapeField(propertyMightBeGeoShape)
+	coordValue, shape, err := geo.ParseGeoShapeField(propertyMightBeGeoShape)
 	if err != nil {
 		return
 	}


Test patch:
diff --git a/geo/parse_test.go b/geo/parse_test.go
index 1b46d8c8..56145564 100644
--- a/geo/parse_test.go
+++ b/geo/parse_test.go
@@ -14,7 +14,10 @@
 
 package geo
 
-import "testing"
+import (
+	"reflect"
+	"testing"
+)
 
 func TestExtractGeoPoint(t *testing.T) {
 
@@ -199,3 +202,154 @@ func (s *s12) Lng() float64 {
 func (s *s12) Lat() float64 {
 	return s.lat
 }
+
+func TestExtractGeoShape(t *testing.T) {
+	tests := []struct {
+		in      interface{}
+		resTyp  string
+		result  [][][][]float64
+		success bool
+	}{
+		// valid point slice
+		{
+			in: map[string]interface{}{
+				"coordinates": []interface{}{3.4, 5.9},
+				"type":        "Point",
+			},
+			resTyp:  "point",
+			result:  [][][][]float64{{{{3.4, 5.9}}}},
+			success: true,
+		},
+		// invalid point slice
+		{
+			in: map[string]interface{}{
+				"coordinates": []interface{}{3.4},
+				"type":        "point"},
+
+			resTyp:  "point",
+			result:  nil,
+			success: false,
+		},
+		// valid multipoint slice containing single point
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 5.9}},
+				"type":        "multipoint"},
+			resTyp:  "multipoint",
+			result:  [][][][]float64{{{{3.4, 5.9}}}},
+			success: true,
+		},
+		// valid multipoint slice
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 5.9}, {6.7, 9.8}},
+				"type":        "multipoint"},
+			resTyp:  "multipoint",
+			result:  [][][][]float64{{{{3.4, 5.9}, {6.7, 9.8}}}},
+			success: true,
+		},
+		// valid multipoint slice containing one invalid entry
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 5.9}, {6.7}},
+				"type":        "multipoint"},
+			resTyp:  "multipoint",
+			result:  [][][][]float64{{{{3.4, 5.9}}}},
+			success: true,
+		},
+		// invalid multipoint slice
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4}},
+				"type":        "multipoint"},
+			resTyp:  "multipoint",
+			result:  nil,
+			success: false,
+		},
+		// valid linestring slice
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}},
+				"type":        "linestring"},
+			resTyp:  "linestring",
+			result:  [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}}}},
+			success: true,
+		},
+		// valid linestring slice
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}, {10.1, 12.3}},
+				"type":        "linestring"},
+			resTyp:  "linestring",
+			result:  [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}, {10.1, 12.3}}}},
+			success: true,
+		},
+		// invalid linestring slice with single entry
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 4.4}},
+				"type":        "linestring"},
+			resTyp:  "linestring",
+			result:  nil,
+			success: false,
+		},
+		// invalid linestring slice with wrong paranthesis
+		{
+			in: map[string]interface{}{
+				"coordinates": [][][]interface{}{{{3.4, 4.4}, {8.4, 9.4}}},
+				"type":        "linestring"},
+			resTyp:  "linestring",
+			result:  nil,
+			success: false,
+		},
+		// valid envelope
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}},
+				"type":        "envelope"},
+			resTyp:  "envelope",
+			result:  [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}}}},
+			success: true,
+		},
+		// invalid envelope
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 4.4}},
+				"type":        "envelope"},
+			resTyp:  "envelope",
+			result:  nil,
+			success: false,
+		},
+		// invalid envelope
+		{
+			in: map[string]interface{}{
+				"coordinates": [][][]interface{}{{{3.4, 4.4}, {8.4, 9.4}}},
+				"type":        "envelope"},
+			resTyp:  "envelope",
+			result:  nil,
+			success: false,
+		},
+		// invalid envelope with >2 vertices
+		{
+			in: map[string]interface{}{
+				"coordinates": [][]interface{}{{3.4, 4.4}, {5.6, 6.4}, {7.4, 7.4}},
+				"type":        "envelope"},
+			resTyp:  "envelope",
+			result:  nil,
+			success: false,
+		},
+	}
+
+	for _, test := range tests {
+		result, shapeType, success := extractGeoShape(test.in)
+		if success != test.success {
+			t.Errorf("expected extract geo point: %t, got: %t for: %v", test.success, success, test.in)
+		}
+		if shapeType != test.resTyp {
+			t.Errorf("expected shape type: %v, got: %v for input: %v", test.resTyp, shapeType, test.in)
+		}
+		if !reflect.DeepEqual(test.result, result) {
+			t.Errorf("expected result %+v, got %+v for %v", test.result, result, test.in)
+		}
+	}
+}