Repository: ogen-go/ogen
Pull request #148 (2022-02-02 08:28:58)

fix: recursive additionalProperties stack overflow


Patch:
diff --git a/internal/ir/recursion.go b/internal/ir/recursion.go
index 437da371..e8062b2b 100644
--- a/internal/ir/recursion.go
+++ b/internal/ir/recursion.go
@@ -10,8 +10,8 @@ func (t *Type) RecursiveTo(target *Type) bool {
 }
 
 func (t *Type) recursive(target *Type, path *walkpath) bool {
-	if t.Is(KindPrimitive, KindPointer, KindMap, KindAny, KindArray, KindEnum) ||
-		target.Is(KindPrimitive, KindPointer, KindMap, KindAny, KindArray, KindEnum) {
+	if t.Is(KindPrimitive, KindPointer, KindAny, KindArray, KindEnum) ||
+		target.Is(KindPrimitive, KindPointer, KindAny, KindArray, KindEnum) {
 		return false
 	}
 
@@ -30,7 +30,7 @@ func (t *Type) recursive(target *Type, path *walkpath) bool {
 		return t.recursive(target.AliasTo, path)
 	case KindGeneric:
 		return t.recursive(target.GenericOf, path)
-	case KindStruct:
+	case KindStruct, KindMap:
 		for _, f := range target.Fields {
 			if !f.Spec.Required {
 				continue
diff --git a/internal/oas/parser/schema_parser.go b/internal/oas/parser/schema_parser.go
index 5f9e40a2..15b85991 100644
--- a/internal/oas/parser/schema_parser.go
+++ b/internal/oas/parser/schema_parser.go
@@ -142,29 +142,23 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return false
 		}
 
-		var (
-			item       *oas.Schema
-			additional bool
-		)
+		s := hook(&oas.Schema{
+			Type:          oas.Object,
+			MaxProperties: schema.MaxProperties,
+			MinProperties: schema.MinProperties,
+		})
+
 		if ap := schema.AdditionalProperties; ap != nil {
-			additional = true
+			s.AdditionalProperties = true
 			if !ap.Bool {
-				sch := ap.Schema
-				prop, err := p.Parse(&sch)
+				item, err := p.Parse(&ap.Schema)
 				if err != nil {
 					return nil, errors.Wrapf(err, "additionalProperties")
 				}
-				item = prop
+				s.Item = item
 			}
 		}
 
-		s := hook(&oas.Schema{
-			Type:                 oas.Object,
-			Item:                 item,
-			AdditionalProperties: additional,
-			MaxProperties:        schema.MaxProperties,
-			MinProperties:        schema.MinProperties,
-		})
 		for _, propSpec := range schema.Properties {
 			prop, err := p.Parse(propSpec.Schema)
 			if err != nil {
diff --git a/internal/sample_api/oas_client_gen.go b/internal/sample_api/oas_client_gen.go
index 1842f39c..530b3027 100644
--- a/internal/sample_api/oas_client_gen.go
+++ b/internal/sample_api/oas_client_gen.go
@@ -1176,6 +1176,45 @@ func (c *Client) PetUploadAvatarByID(ctx context.Context, request PetUploadAvata
 	return result, nil
 }
 
+// RecursiveMapGet invokes  operation.
+//
+// GET /recursiveMap
+func (c *Client) RecursiveMapGet(ctx context.Context) (res RecursiveMap, err error) {
+	startTime := time.Now()
+	ctx, span := c.cfg.Tracer.Start(ctx, `RecursiveMapGet`,
+		trace.WithSpanKind(trace.SpanKindClient),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+			c.errors.Add(ctx, 1)
+		} else {
+			elapsedDuration := time.Since(startTime)
+			c.duration.Record(ctx, elapsedDuration.Microseconds())
+		}
+		span.End()
+	}()
+	c.requests.Add(ctx, 1)
+	u := uri.Clone(c.serverURL)
+	u.Path += "/recursiveMap"
+
+	r := ht.NewRequest(ctx, "GET", u, nil)
+	defer ht.PutRequest(r)
+
+	resp, err := c.cfg.Client.Do(r)
+	if err != nil {
+		return res, errors.Wrap(err, "do request")
+	}
+	defer resp.Body.Close()
+
+	result, err := decodeRecursiveMapGetResponse(resp, span)
+	if err != nil {
+		return res, errors.Wrap(err, "decode response")
+	}
+
+	return result, nil
+}
+
 // TestObjectQueryParameter invokes testObjectQueryParameter operation.
 //
 // GET /testObjectQueryParameter
diff --git a/internal/sample_api/oas_handlers_gen.go b/internal/sample_api/oas_handlers_gen.go
index 38f60a22..1b964685 100644
--- a/internal/sample_api/oas_handlers_gen.go
+++ b/internal/sample_api/oas_handlers_gen.go
@@ -519,6 +519,28 @@ func (s *Server) handlePetUploadAvatarByIDRequest(args [0]string, w http.Respons
 	}
 }
 
+// HandleRecursiveMapGetRequest handles  operation.
+//
+// GET /recursiveMap
+func (s *Server) handleRecursiveMapGetRequest(args [0]string, w http.ResponseWriter, r *http.Request) {
+	ctx, span := s.cfg.Tracer.Start(r.Context(), `RecursiveMapGet`,
+		trace.WithSpanKind(trace.SpanKindServer),
+	)
+	defer span.End()
+
+	response, err := s.h.RecursiveMapGet(ctx)
+	if err != nil {
+		span.RecordError(err)
+		respondError(w, http.StatusInternalServerError, err)
+		return
+	}
+
+	if err := encodeRecursiveMapGetResponse(response, w, span); err != nil {
+		span.RecordError(err)
+		return
+	}
+}
+
 // HandleTestObjectQueryParameterRequest handles testObjectQueryParameter operation.
 //
 // GET /testObjectQueryParameter
diff --git a/internal/sample_api/oas_json_gen.go b/internal/sample_api/oas_json_gen.go
index 62802da4..b1129cff 100644
--- a/internal/sample_api/oas_json_gen.go
+++ b/internal/sample_api/oas_json_gen.go
@@ -3752,6 +3752,79 @@ func (s *PetType) Decode(d *jx.Decoder) error {
 	return nil
 }
 
+// Encode implements json.Marshaler.
+func (s RecursiveMap) Encode(e *jx.Writer) {
+	e.ObjStart()
+	var (
+		first = true
+		_     = first
+	)
+	{
+		{
+			if s.OptionalRecursiveField != nil {
+				if !first {
+					e.Comma()
+				}
+				first = false
+			}
+			if s.OptionalRecursiveField != nil {
+				e.RawStr("\"optional_recursive_field\"" + ":")
+				s.OptionalRecursiveField.Encode(e)
+			}
+		}
+	}
+	for k, elem := range s.AdditionalProps {
+		if !first {
+			e.Comma()
+		}
+		first = true
+		e.Str(k)
+		e.RawStr(`:`)
+
+		elem.Encode(e)
+	}
+	e.ObjEnd()
+}
+
+var jsonFieldsNameOfRecursiveMap = [1]string{
+	0: "optional_recursive_field",
+}
+
+// Decode decodes RecursiveMap from json.
+func (s *RecursiveMap) Decode(d *jx.Decoder) error {
+	if s == nil {
+		return errors.New(`invalid: unable to decode RecursiveMap to nil`)
+	}
+
+	m := s.AdditionalProps
+	if m == nil {
+		m = map[string]RecursiveMap{}
+		s.AdditionalProps = m
+	}
+	if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error {
+		switch string(k) {
+		case "optional_recursive_field":
+			s.OptionalRecursiveField = nil
+			var elem RecursiveMap
+			if err := elem.Decode(d); err != nil {
+				return err
+			}
+			s.OptionalRecursiveField = &elem
+		default:
+			var elem RecursiveMap
+			if err := elem.Decode(d); err != nil {
+				return err
+			}
+			m[string(k)] = elem
+		}
+		return nil
+	}); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // Encode implements json.Marshaler.
 func (s StringMap) Encode(e *jx.Writer) {
 	e.ObjStart()
diff --git a/internal/sample_api/oas_res_dec_gen.go b/internal/sample_api/oas_res_dec_gen.go
index dc015721..d70f5dc6 100644
--- a/internal/sample_api/oas_res_dec_gen.go
+++ b/internal/sample_api/oas_res_dec_gen.go
@@ -604,6 +604,40 @@ func decodePetUploadAvatarByIDResponse(resp *http.Response, span trace.Span) (re
 	}
 }
 
+func decodeRecursiveMapGetResponse(resp *http.Response, span trace.Span) (res RecursiveMap, err error) {
+	switch resp.StatusCode {
+	case 200:
+		switch resp.Header.Get("Content-Type") {
+		case "application/json":
+			buf := getBuf()
+			defer putBuf(buf)
+			if _, err := io.Copy(buf, resp.Body); err != nil {
+				return res, err
+			}
+
+			d := jx.GetDecoder()
+			defer jx.PutDecoder(d)
+			d.ResetBytes(buf.Bytes())
+
+			var response RecursiveMap
+			if err := func() error {
+				if err := response.Decode(d); err != nil {
+					return err
+				}
+				return nil
+			}(); err != nil {
+				return res, err
+			}
+
+			return response, nil
+		default:
+			return res, errors.Errorf("unexpected content-type: %s", resp.Header.Get("Content-Type"))
+		}
+	default:
+		return res, errors.Errorf("unexpected statusCode: %d", resp.StatusCode)
+	}
+}
+
 func decodeTestObjectQueryParameterResponse(resp *http.Response, span trace.Span) (res TestObjectQueryParameterOK, err error) {
 	switch resp.StatusCode {
 	case 200:
diff --git a/internal/sample_api/oas_res_enc_gen.go b/internal/sample_api/oas_res_enc_gen.go
index 72ef19d8..926635a5 100644
--- a/internal/sample_api/oas_res_enc_gen.go
+++ b/internal/sample_api/oas_res_enc_gen.go
@@ -336,6 +336,20 @@ func encodePetUploadAvatarByIDResponse(response PetUploadAvatarByIDRes, w http.R
 	}
 }
 
+func encodeRecursiveMapGetResponse(response RecursiveMap, w http.ResponseWriter, span trace.Span) error {
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(200)
+	e := jx.GetWriter()
+	defer jx.PutWriter(e)
+
+	response.Encode(e)
+	if _, err := e.WriteTo(w); err != nil {
+		return errors.Wrap(err, "write")
+	}
+
+	return nil
+}
+
 func encodeTestObjectQueryParameterResponse(response TestObjectQueryParameterOK, w http.ResponseWriter, span trace.Span) error {
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(200)
diff --git a/internal/sample_api/oas_router_gen.go b/internal/sample_api/oas_router_gen.go
index e6ee79e9..b3c982d1 100644
--- a/internal/sample_api/oas_router_gen.go
+++ b/internal/sample_api/oas_router_gen.go
@@ -321,6 +321,19 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 						return
 					}
 				}
+			case 'r': // Prefix: "recursiveMap"
+				if l := len("recursiveMap"); len(elem) >= l && elem[0:l] == "recursiveMap" {
+					elem = elem[l:]
+				} else {
+					break
+				}
+
+				if len(elem) == 0 {
+					// Leaf: RecursiveMapGet
+					s.handleRecursiveMapGetRequest([0]string{}, w, r)
+
+					return
+				}
 			case 't': // Prefix: "test"
 				if l := len("test"); len(elem) >= l && elem[0:l] == "test" {
 					elem = elem[l:]
@@ -767,6 +780,20 @@ func (s *Server) FindRoute(method, path string) (r Route, _ bool) {
 						return r, true
 					}
 				}
+			case 'r': // Prefix: "recursiveMap"
+				if l := len("recursiveMap"); len(elem) >= l && elem[0:l] == "recursiveMap" {
+					elem = elem[l:]
+				} else {
+					break
+				}
+
+				if len(elem) == 0 {
+					// Leaf: RecursiveMapGet
+					r.name = "RecursiveMapGet"
+					r.args = args
+					r.count = 0
+					return r, true
+				}
 			case 't': // Prefix: "test"
 				if l := len("test"); len(elem) >= l && elem[0:l] == "test" {
 					elem = elem[l:]
diff --git a/internal/sample_api/oas_schemas_gen.go b/internal/sample_api/oas_schemas_gen.go
index 84bff635..bd16cd12 100644
--- a/internal/sample_api/oas_schemas_gen.go
+++ b/internal/sample_api/oas_schemas_gen.go
@@ -1849,6 +1849,12 @@ func (s PetUploadAvatarByIDReq) Read(p []byte) (n int, err error) {
 	return s.Data.Read(p)
 }
 
+// Ref: #/components/schemas/RecursiveMap
+type RecursiveMap struct {
+	OptionalRecursiveField *RecursiveMap `json:"optional_recursive_field"`
+	AdditionalProps        map[string]RecursiveMap
+}
+
 // Ref: #/components/schemas/StringMap
 type StringMap map[string]string
 
diff --git a/internal/sample_api/oas_server_gen.go b/internal/sample_api/oas_server_gen.go
index f575c072..5230260c 100644
--- a/internal/sample_api/oas_server_gen.go
+++ b/internal/sample_api/oas_server_gen.go
@@ -154,6 +154,10 @@ type Handler interface {
 	//
 	// POST /pet/avatar
 	PetUploadAvatarByID(ctx context.Context, req PetUploadAvatarByIDReq, params PetUploadAvatarByIDParams) (PetUploadAvatarByIDRes, error)
+	// RecursiveMapGet implements  operation.
+	//
+	// GET /recursiveMap
+	RecursiveMapGet(ctx context.Context) (RecursiveMap, error)
 	// TestObjectQueryParameter implements testObjectQueryParameter operation.
 	//
 	// GET /testObjectQueryParameter


Test patch:
diff --git a/_testdata/sample.json b/_testdata/sample.json
index 483dbed0..84409834 100644
--- a/_testdata/sample.json
+++ b/_testdata/sample.json
@@ -587,6 +587,21 @@
           }
         }
       }
+    },
+    "/recursiveMap": {
+      "get": {
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/RecursiveMap"
+                }
+              }
+            }
+          }
+        }
+      }
     }
   },
   "components": {
@@ -637,6 +652,17 @@
           "minLength": 1
         }
       },
+      "RecursiveMap": {
+        "type": "object",
+        "additionalProperties": {
+          "$ref": "#/components/schemas/RecursiveMap"
+        },
+        "properties": {
+          "optional_recursive_field": {
+            "$ref": "#/components/schemas/RecursiveMap"
+          }
+        }
+      },
       "NullableEnums": {
         "type": "object",
         "required": [
diff --git a/e2e_test.go b/e2e_test.go
index a8b7e380..c0e3bea1 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -213,6 +213,10 @@ func (s sampleAPIServer) OneofBug(ctx context.Context, req api.OneOfBugs) (api.O
 	panic("implement me")
 }
 
+func (s sampleAPIServer) RecursiveMapGet(ctx context.Context) (api.RecursiveMap, error) {
+	panic("implement me")
+}
+
 var _ api.Handler = (*sampleAPIServer)(nil)
 
 //go:embed _testdata/pet.json