Repository: getkin/kin-openapi
Pull request #654 (2022-11-05 21:02:21)

fix panic slice out of range error #652
Fixes panic from the issue #652, that occurs when trying to load specification from file that contains reference to the file insed of parent directory.


Patch:
diff --git a/openapi3/errors.go b/openapi3/errors.go
index b530101..74baab9 100644
--- a/openapi3/errors.go
+++ b/openapi3/errors.go
@@ -10,11 +10,15 @@ import (
 type MultiError []error
 
 func (me MultiError) Error() string {
+	return spliceErr(" | ", me)
+}
+
+func spliceErr(sep string, errs []error) string {
 	buff := &bytes.Buffer{}
-	for i, e := range me {
+	for i, e := range errs {
 		buff.WriteString(e.Error())
-		if i != len(me)-1 {
-			buff.WriteString(" | ")
+		if i != len(errs)-1 {
+			buff.WriteString(sep)
 		}
 	}
 	return buff.String()
@@ -43,3 +47,13 @@ func (me MultiError) As(target interface{}) bool {
 	}
 	return false
 }
+
+type multiErrorForOneOf MultiError
+
+func (meo multiErrorForOneOf) Error() string {
+	return spliceErr(" Or ", meo)
+}
+
+func (meo multiErrorForOneOf) Unwrap() error {
+	return MultiError(meo)
+}
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 1ab693b..2bb9999 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -742,7 +742,10 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 				return err
 			}
 			component.Value = resolved.Value
-			foundPath := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath)
+			foundPath, rerr := loader.getResolvedRefPath(ref, &resolved, documentPath, componentPath)
+			if rerr != nil {
+				return fmt.Errorf("failed to resolve file from reference %q: %w", ref, rerr)
+			}
 			documentPath = loader.documentPathForRecursiveRef(documentPath, foundPath)
 		}
 	}
@@ -790,23 +793,28 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 	return nil
 }
 
-func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) string {
+func (loader *Loader) getResolvedRefPath(ref string, resolved *SchemaRef, cur, found *url.URL) (string, error) {
 	if referencedFilename := strings.Split(ref, "#")[0]; referencedFilename == "" {
 		if cur != nil {
 			if loader.rootDir != "" && strings.HasPrefix(cur.Path, loader.rootDir) {
-				return cur.Path[len(loader.rootDir)+1:]
+				return cur.Path[len(loader.rootDir)+1:], nil
 			}
 
-			return path.Base(cur.Path)
+			return path.Base(cur.Path), nil
 		}
-		return ""
+		return "", nil
 	}
 	// ref. to external file
 	if resolved.Ref != "" {
-		return resolved.Ref
+		return resolved.Ref, nil
 	}
+
+	if loader.rootDir == "" {
+		return found.Path, nil
+	}
+
 	// found dest spec. file
-	return path.Dir(found.Path)[len(loader.rootDir):]
+	return filepath.Rel(loader.rootDir, found.Path)
 }
 
 func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) {
diff --git a/openapi3/schema.go b/openapi3/schema.go
index ba846d0..e5cded8 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -926,7 +926,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 
 		var (
 			ok               = 0
-			validationErrors = []error{}
+			validationErrors = multiErrorForOneOf{}
 			matchedOneOfIdx  = 0
 			tempValue        = value
 		)
@@ -955,14 +955,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 
 		if ok != 1 {
 			if len(validationErrors) > 1 {
-				errorMessage := ""
-				for _, err := range validationErrors {
-					if errorMessage != "" {
-						errorMessage += " Or "
-					}
-					errorMessage += err.Error()
-				}
-				return errors.New("doesn't match schema due to: " + errorMessage)
+				return fmt.Errorf("doesn't match schema due to: %w", validationErrors)
 			}
 			if settings.failfast {
 				return errSchema
diff --git a/openapi3filter/req_resp_encoder.go b/openapi3filter/req_resp_encoder.go
index dd410f5..36b7db6 100644
--- a/openapi3filter/req_resp_encoder.go
+++ b/openapi3filter/req_resp_encoder.go
@@ -16,8 +16,34 @@ func encodeBody(body interface{}, mediaType string) ([]byte, error) {
 	return encoder(body)
 }
 
-type bodyEncoder func(body interface{}) ([]byte, error)
+type BodyEncoder func(body interface{}) ([]byte, error)
 
-var bodyEncoders = map[string]bodyEncoder{
+var bodyEncoders = map[string]BodyEncoder{
 	"application/json": json.Marshal,
 }
+
+func RegisterBodyEncoder(contentType string, encoder BodyEncoder) {
+	if contentType == "" {
+		panic("contentType is empty")
+	}
+	if encoder == nil {
+		panic("encoder is not defined")
+	}
+	bodyEncoders[contentType] = encoder
+}
+
+// This call is not thread-safe: body encoders should not be created/destroyed by multiple goroutines.
+func UnregisterBodyEncoder(contentType string) {
+	if contentType == "" {
+		panic("contentType is empty")
+	}
+	delete(bodyEncoders, contentType)
+}
+
+// RegisteredBodyEncoder returns the registered body encoder for the given content type.
+//
+// If no encoder was registered for the given content type, nil is returned.
+// This call is not thread-safe: body encoders should not be created/destroyed by multiple goroutines.
+func RegisteredBodyEncoder(contentType string) BodyEncoder {
+	return bodyEncoders[contentType]
+}


Test patch:
diff --git a/openapi3/issue652_test.go b/openapi3/issue652_test.go
new file mode 100644
index 0000000..f36e920
--- /dev/null
+++ b/openapi3/issue652_test.go
@@ -0,0 +1,29 @@
+package openapi3_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+func TestIssue652(t *testing.T) {
+	loader := openapi3.NewLoader()
+	loader.IsExternalRefsAllowed = true
+
+	// Test checks that no slice bounds out of range error occurs while loading
+	// from file that contains reference to file in the parent directory.
+	require.NotPanics(t, func() {
+		const schemaName = "ReferenceToParentDirectory"
+
+		spec, err := loader.LoadFromFile("testdata/issue652/nested/schema.yml")
+		require.NoError(t, err)
+		require.Contains(t, spec.Components.Schemas, schemaName)
+
+		schema := spec.Components.Schemas[schemaName]
+		assert.Equal(t, schema.Ref, "../definitions.yml#/components/schemas/TestSchema")
+		assert.Equal(t, schema.Value.Type, "string")
+	})
+}
diff --git a/openapi3/issue657_test.go b/openapi3/issue657_test.go
new file mode 100644
index 0000000..195ccd1
--- /dev/null
+++ b/openapi3/issue657_test.go
@@ -0,0 +1,79 @@
+package openapi3_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+func TestOneOf_Warning_Errors(t *testing.T) {
+	t.Parallel()
+
+	loader := openapi3.NewLoader()
+	spec := `
+components:
+  schemas:
+    Something:
+      type: object
+      properties:
+        field:
+          title: Some field
+          oneOf:
+            - title: First rule
+              type: string
+              minLength: 10
+              maxLength: 10
+            - title: Second rule
+              type: string
+              minLength: 15
+              maxLength: 15
+`[1:]
+
+	doc, err := loader.LoadFromData([]byte(spec))
+	require.NoError(t, err)
+
+	tests := [...]struct {
+		name     string
+		value    string
+		checkErr require.ErrorAssertionFunc
+	}{
+		{
+			name:     "valid value",
+			value:    "ABCDE01234",
+			checkErr: require.NoError,
+		},
+		{
+			name:     "valid value",
+			value:    "ABCDE0123456789",
+			checkErr: require.NoError,
+		},
+		{
+			name:  "no valid value",
+			value: "ABCDE",
+			checkErr: func(t require.TestingT, err error, i ...interface{}) {
+				require.Equal(t, "doesn't match schema due to: minimum string length is 10\nSchema:\n  {\n    \"maxLength\": 10,\n    \"minLength\": 10,\n    \"title\": \"First rule\",\n    \"type\": \"string\"\n  }\n\nValue:\n  \"ABCDE\"\n Or minimum string length is 15\nSchema:\n  {\n    \"maxLength\": 15,\n    \"minLength\": 15,\n    \"title\": \"Second rule\",\n    \"type\": \"string\"\n  }\n\nValue:\n  \"ABCDE\"\n", err.Error())
+
+				wErr := &openapi3.MultiError{}
+				require.ErrorAs(t, err, wErr)
+
+				require.Len(t, *wErr, 2)
+
+				require.Equal(t, "minimum string length is 10", (*wErr)[0].(*openapi3.SchemaError).Reason)
+				require.Equal(t, "minimum string length is 15", (*wErr)[1].(*openapi3.SchemaError).Reason)
+			},
+		},
+	}
+
+	for _, test := range tests {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+
+			err = doc.Components.Schemas["Something"].Value.Properties["field"].Value.VisitJSON(test.value)
+
+			test.checkErr(t, err)
+		})
+	}
+}
diff --git a/openapi3/testdata/issue652/definitions.yml b/openapi3/testdata/issue652/definitions.yml
new file mode 100644
index 0000000..98ef692
--- /dev/null
+++ b/openapi3/testdata/issue652/definitions.yml
@@ -0,0 +1,4 @@
+components:
+    schemas:
+        TestSchema:
+            type: string
diff --git a/openapi3/testdata/issue652/nested/schema.yml b/openapi3/testdata/issue652/nested/schema.yml
new file mode 100644
index 0000000..ef321a1
--- /dev/null
+++ b/openapi3/testdata/issue652/nested/schema.yml
@@ -0,0 +1,4 @@
+components:
+    schemas:
+        ReferenceToParentDirectory:
+            $ref: "../definitions.yml#/components/schemas/TestSchema"
diff --git a/openapi3filter/req_resp_encoder_test.go b/openapi3filter/req_resp_encoder_test.go
new file mode 100644
index 0000000..11fe2af
--- /dev/null
+++ b/openapi3filter/req_resp_encoder_test.go
@@ -0,0 +1,43 @@
+package openapi3filter
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestRegisterAndUnregisterBodyEncoder(t *testing.T) {
+	var encoder BodyEncoder
+	encoder = func(body interface{}) (data []byte, err error) {
+		return []byte(strings.Join(body.([]string), ",")), nil
+	}
+	contentType := "text/csv"
+	h := make(http.Header)
+	h.Set(headerCT, contentType)
+
+	originalEncoder := RegisteredBodyEncoder(contentType)
+	require.Nil(t, originalEncoder)
+
+	RegisterBodyEncoder(contentType, encoder)
+	require.Equal(t, fmt.Sprintf("%v", encoder), fmt.Sprintf("%v", RegisteredBodyEncoder(contentType)))
+
+	body := []string{"foo", "bar"}
+	got, err := encodeBody(body, contentType)
+
+	require.NoError(t, err)
+	require.Equal(t, []byte("foo,bar"), got)
+
+	UnregisterBodyEncoder(contentType)
+
+	originalEncoder = RegisteredBodyEncoder(contentType)
+	require.Nil(t, originalEncoder)
+
+	_, err = encodeBody(body, contentType)
+	require.Equal(t, &ParseError{
+		Kind:   KindUnsupportedFormat,
+		Reason: prefixUnsupportedCT + ` "text/csv"`,
+	}, err)
+}