From ab9c24a5fe6fbe349fa13e13a535c3be695f472c Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Thu, 10 Apr 2025 08:35:52 +0100 Subject: [PATCH 1/4] chore: make wasm work --- datamodel/low/v3/create_document.go | 2 +- index/rolodex.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/datamodel/low/v3/create_document.go b/datamodel/low/v3/create_document.go index 80929b3f7..4d75696b6 100644 --- a/datamodel/low/v3/create_document.go +++ b/datamodel/low/v3/create_document.go @@ -57,7 +57,7 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur doc.Rolodex = rolodex // If basePath is provided, add a local filesystem to the rolodex. - if idxConfig.BasePath != "" || config.AllowFileReferences { + if idxConfig.BasePath != "" && config.AllowFileReferences { var cwd string cwd, _ = filepath.Abs(config.BasePath) // if a supplied local filesystem is provided, add it to the rolodex. diff --git a/index/rolodex.go b/index/rolodex.go index 2d0cdfd97..cc9d440d8 100644 --- a/index/rolodex.go +++ b/index/rolodex.go @@ -357,7 +357,7 @@ func (r *Rolodex) IndexTheRolodex() error { // which does not exist, but is used to formulate the absolute path to root references correctly. if r.indexConfig.BasePath != "" && r.indexConfig.BaseURL == nil { basePath := r.indexConfig.BasePath - if !filepath.IsAbs(basePath) { + if !filepath.IsAbs(basePath) && r.indexConfig.AllowFileLookup { basePath, _ = filepath.Abs(basePath) } From e6cb73d5f74a5f0f42b9b5189cb50c92503998bb Mon Sep 17 00:00:00 2001 From: David Alberto Adler Date: Thu, 24 Jul 2025 14:51:51 +0100 Subject: [PATCH 2/4] fix: nil pointer deref --- what-changed/model/schema.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/what-changed/model/schema.go b/what-changed/model/schema.go index a4528683d..99731e122 100644 --- a/what-changed/model/schema.go +++ b/what-changed/model/schema.go @@ -349,8 +349,12 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { lHash := l.Schema().Hash() rHash := r.Schema().Hash() if lHash != rHash { + var rContentNode *yaml.Node = r.GetValueNode() + if len(r.GetValueNode().Content) > 1 { + rContentNode = r.GetValueNode().Content[1] + } CreateChange(&changes, Modified, v3.RefLabel, - l.GetValueNode(), r.GetValueNode().Content[1], true, l, r.GetReference()) + l.GetValueNode(), rContentNode, true, l, r.GetReference()) sc.PropertyChanges = NewPropertyChanges(changes) return sc // we're done here } @@ -363,8 +367,12 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { lHash := l.Schema().Hash() rHash := r.Schema().Hash() if lHash != rHash { + var lContentNode *yaml.Node = l.GetValueNode() + if len(l.GetValueNode().Content) > 1 { + lContentNode = l.GetValueNode().Content[1] + } CreateChange(&changes, Modified, v3.RefLabel, - l.GetValueNode().Content[1], r.GetValueNode(), true, l.GetReference(), r) + lContentNode, r.GetValueNode(), true, l.GetReference(), r) sc.PropertyChanges = NewPropertyChanges(changes) return sc // done, nothing else to do. } From 7820d16949a70cb18b010f177862cf6f42e80c73 Mon Sep 17 00:00:00 2001 From: Ash Godfrey Date: Thu, 2 Jul 2026 12:36:53 +0100 Subject: [PATCH 3/4] fix: nil-guard SchemaProxy.Schema() results in CompareSchemas A property $ref pointing at a ref-to-ref alias chain inside a reference cycle leaves a SchemaProxy whose Schema() returns nil. CompareSchemas hashed that nil schema in the inline<->ref branches and dereferenced it in the deep-compare walk, crashing with a nil-receiver SIGSEGV in Schema.Hash(). Treat an unresolvable proxy as a modification instead, and make Schema.Hash() nil-safe as a backstop. GEN-3129 Co-Authored-By: Claude Fable 5 --- datamodel/low/base/schema.go | 4 ++ datamodel/low/base/schema_test.go | 8 ++++ what-changed/model/schema.go | 28 +++++++++++--- what-changed/model/schema_test.go | 64 +++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/datamodel/low/base/schema.go b/datamodel/low/base/schema.go index 1ddff501c..f94fe4909 100644 --- a/datamodel/low/base/schema.go +++ b/datamodel/low/base/schema.go @@ -162,6 +162,10 @@ func (s *Schema) GetContext() context.Context { // Hash will calculate a SHA256 hash from the values of the schema, This allows equality checking against // Schemas defined inside an OpenAPI document. The only way to know if a schema has changed, is to hash it. func (s *Schema) Hash() [32]byte { + // a nil schema (e.g. from a SchemaProxy that failed to resolve) hashes like an empty one. + if s == nil { + return sha256.Sum256([]byte{}) + } // calculate a hash from every property in the schema. var d []string if !s.SchemaTypeRef.IsEmpty() { diff --git a/datamodel/low/base/schema_test.go b/datamodel/low/base/schema_test.go index e65e4a9b1..cd67198de 100644 --- a/datamodel/low/base/schema_test.go +++ b/datamodel/low/base/schema_test.go @@ -1970,3 +1970,11 @@ func TestExtractSchema_CheckExampleNodesExtracted(t *testing.T) { t.Fail() } } + +func TestSchema_Hash_NilReceiver(t *testing.T) { + // a nil schema (e.g. from a SchemaProxy that failed to resolve) must not panic + // and hashes like an empty schema. + var s *Schema + empty := &Schema{} + assert.Equal(t, empty.Hash(), s.Hash()) +} diff --git a/what-changed/model/schema.go b/what-changed/model/schema.go index 99731e122..416929597 100644 --- a/what-changed/model/schema.go +++ b/what-changed/model/schema.go @@ -346,9 +346,11 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { if !l.IsReference() && r.IsReference() { // check if the referenced schema matches or not // https://github.com/pb33f/libopenapi/issues/218 - lHash := l.Schema().Hash() - rHash := r.Schema().Hash() - if lHash != rHash { + // if either proxy cannot be resolved (e.g. an unresolvable circular reference), + // equality cannot be proven, so report the change instead of hashing a nil schema. + lSchema := l.Schema() + rSchema := r.Schema() + if lSchema == nil || rSchema == nil || lSchema.Hash() != rSchema.Hash() { var rContentNode *yaml.Node = r.GetValueNode() if len(r.GetValueNode().Content) > 1 { rContentNode = r.GetValueNode().Content[1] @@ -364,9 +366,11 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { if l.IsReference() && !r.IsReference() { // check if the referenced schema matches or not // https://github.com/pb33f/libopenapi/issues/218 - lHash := l.Schema().Hash() - rHash := r.Schema().Hash() - if lHash != rHash { + // if either proxy cannot be resolved (e.g. an unresolvable circular reference), + // equality cannot be proven, so report the change instead of hashing a nil schema. + lSchema := l.Schema() + rSchema := r.Schema() + if lSchema == nil || rSchema == nil || lSchema.Hash() != rSchema.Hash() { var lContentNode *yaml.Node = l.GetValueNode() if len(l.GetValueNode().Content) > 1 { lContentNode = l.GetValueNode().Content[1] @@ -381,6 +385,18 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { lSchema := l.Schema() rSchema := r.Schema() + // if either proxy cannot be resolved (e.g. an unresolvable circular reference), + // the schemas cannot be walked. Report a modification if only one side is nil. + if lSchema == nil || rSchema == nil { + if lSchema != rSchema { + CreateChange(&changes, Modified, v3.SchemaLabel, + l.GetValueNode(), r.GetValueNode(), true, l, r) + sc.PropertyChanges = NewPropertyChanges(changes) + return sc + } + return nil + } + if low.AreEqual(lSchema, rSchema) { // there is no point going on, we know nothing changed! return nil diff --git a/what-changed/model/schema_test.go b/what-changed/model/schema_test.go index 2095c544e..dc2fe9907 100644 --- a/what-changed/model/schema_test.go +++ b/what-changed/model/schema_test.go @@ -3232,3 +3232,67 @@ components: assert.Equal(t, 1, changes.TotalChanges()) } + +func TestCompareSchemas_UnresolvableCircularAliasRef(t *testing.T) { + // a property that changes between an inline schema and a $ref pointing at a + // ref-to-ref alias chain inside a reference cycle. The alias chain cannot be + // resolved by the proxy (circular lookup), so SchemaProxy.Schema() returns nil; + // comparing must report a change, not panic hashing a nil schema. + inline := `openapi: 3.1.0 +components: + schemas: + VendorCredentialSummary: + $ref: "#/components/schemas/VendorCredentialFile" + VendorCredentialFile: + $ref: "#/components/schemas/VendorCredentialFileImpl" + VendorCredentialFileImpl: + type: object + properties: + summary: + $ref: "#/components/schemas/VendorCredentialSummary" + VendorCredentialDataRequest: + type: object + properties: + summary: + allOf: + - type: object + additionalProperties: true +` + + ref := `openapi: 3.1.0 +components: + schemas: + VendorCredentialSummary: + $ref: "#/components/schemas/VendorCredentialFile" + VendorCredentialFile: + $ref: "#/components/schemas/VendorCredentialFileImpl" + VendorCredentialFileImpl: + type: object + properties: + summary: + $ref: "#/components/schemas/VendorCredentialSummary" + VendorCredentialDataRequest: + type: object + properties: + summary: + $ref: "#/components/schemas/VendorCredentialSummary" +` + + leftDoc, rightDoc := test_BuildDoc(inline, ref) + + // inline -> ref + lSchemaProxy := leftDoc.Components.Value.FindSchema("VendorCredentialDataRequest").Value + rSchemaProxy := rightDoc.Components.Value.FindSchema("VendorCredentialDataRequest").Value + changes := CompareSchemas(lSchemaProxy, rSchemaProxy) + assert.NotNil(t, changes) + assert.Equal(t, 1, changes.TotalChanges()) + assert.Equal(t, Modified, changes.GetAllChanges()[0].ChangeType) + assert.Equal(t, v3.RefLabel, changes.GetAllChanges()[0].Property) + + // ref -> inline + changes = CompareSchemas(rSchemaProxy, lSchemaProxy) + assert.NotNil(t, changes) + assert.Equal(t, 1, changes.TotalChanges()) + assert.Equal(t, Modified, changes.GetAllChanges()[0].ChangeType) + assert.Equal(t, v3.RefLabel, changes.GetAllChanges()[0].Property) +} From 53a3bc35a8804f9b6510334da4986fefb9c1cedf Mon Sep 17 00:00:00 2001 From: Ash Godfrey Date: Thu, 2 Jul 2026 15:53:23 +0100 Subject: [PATCH 4/4] refactor: extract schemasProvablyEqual helper for the ref-flip guards Both inline<->ref branches carried a verbatim copy of the resolve-and- compare guard; extracting it keeps the two directions symmetric by construction. Co-Authored-By: Claude Fable 5 --- what-changed/model/schema.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/what-changed/model/schema.go b/what-changed/model/schema.go index 416929597..6bf65c581 100644 --- a/what-changed/model/schema.go +++ b/what-changed/model/schema.go @@ -304,6 +304,15 @@ func (s *SchemaChanges) TotalBreakingChanges() int { return t } +// schemasProvablyEqual checks that both proxies resolve and hash identically. If either proxy +// cannot be resolved (e.g. an unresolvable circular reference), equality cannot be proven, so a +// change should be reported instead of hashing a nil schema. +func schemasProvablyEqual(l, r *base.SchemaProxy) bool { + lSchema := l.Schema() + rSchema := r.Schema() + return lSchema != nil && rSchema != nil && lSchema.Hash() == rSchema.Hash() +} + // CompareSchemas accepts a left and right SchemaProxy and checks for changes. If anything is found, returns // a pointer to SchemaChanges, otherwise returns nil func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { @@ -346,11 +355,7 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { if !l.IsReference() && r.IsReference() { // check if the referenced schema matches or not // https://github.com/pb33f/libopenapi/issues/218 - // if either proxy cannot be resolved (e.g. an unresolvable circular reference), - // equality cannot be proven, so report the change instead of hashing a nil schema. - lSchema := l.Schema() - rSchema := r.Schema() - if lSchema == nil || rSchema == nil || lSchema.Hash() != rSchema.Hash() { + if !schemasProvablyEqual(l, r) { var rContentNode *yaml.Node = r.GetValueNode() if len(r.GetValueNode().Content) > 1 { rContentNode = r.GetValueNode().Content[1] @@ -366,11 +371,7 @@ func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges { if l.IsReference() && !r.IsReference() { // check if the referenced schema matches or not // https://github.com/pb33f/libopenapi/issues/218 - // if either proxy cannot be resolved (e.g. an unresolvable circular reference), - // equality cannot be proven, so report the change instead of hashing a nil schema. - lSchema := l.Schema() - rSchema := r.Schema() - if lSchema == nil || rSchema == nil || lSchema.Hash() != rSchema.Hash() { + if !schemasProvablyEqual(l, r) { var lContentNode *yaml.Node = l.GetValueNode() if len(l.GetValueNode().Content) > 1 { lContentNode = l.GetValueNode().Content[1]