Fix corrupted JSON caused by goccy library (#37214)

Fix #37211
This commit is contained in:
wxiaoguang
2026-04-14 22:00:20 +08:00
committed by GitHub
parent 699eb41e7c
commit b9961e193d
9 changed files with 137 additions and 62 deletions

4
go.mod
View File

@@ -12,7 +12,7 @@ require (
code.gitea.io/sdk/gitea v0.24.1 code.gitea.io/sdk/gitea v0.24.1
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.19.1 connectrpc.com/connect v1.19.1
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
gitea.com/go-chi/cache v0.2.1 gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
@@ -57,7 +57,6 @@ require (
github.com/go-redsync/redsync/v4 v4.16.0 github.com/go-redsync/redsync/v4 v4.16.0
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3
github.com/go-webauthn/webauthn v0.16.4 github.com/go-webauthn/webauthn v0.16.4
github.com/goccy/go-json v0.10.6
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
@@ -196,6 +195,7 @@ require (
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.3 // indirect github.com/go-webauthn/x v0.2.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect

4
go.sum
View File

@@ -18,8 +18,8 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs= gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs=
gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c= gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ=
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g= gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q= gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=

View File

@@ -1,35 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package json
import (
"bytes"
"io"
"github.com/goccy/go-json"
)
var _ Interface = jsonGoccy{}
type jsonGoccy struct{}
func (jsonGoccy) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (jsonGoccy) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (jsonGoccy) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (jsonGoccy) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}
func (jsonGoccy) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
return json.Indent(dst, src, prefix, indent)
}

View File

@@ -11,7 +11,7 @@ import (
) )
func getDefaultJSONHandler() Interface { func getDefaultJSONHandler() Interface {
return jsonGoccy{} return jsonV1{}
} }
func MarshalKeepOptionalEmpty(v any) ([]byte, error) { func MarshalKeepOptionalEmpty(v any) ([]byte, error) {

View File

@@ -5,12 +5,14 @@ package validation
import ( import (
"fmt" "fmt"
"io"
"regexp" "regexp"
"strings" "strings"
"code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"gitea.com/go-chi/binding" "gitea.com/go-chi/binding"
@@ -31,8 +33,23 @@ const (
ErrInvalidBadgeSlug = "InvalidBadgeSlug" ErrInvalidBadgeSlug = "InvalidBadgeSlug"
) )
type jsonProvider struct{}
func (j jsonProvider) Marshal(v any) ([]byte, error) { return json.Marshal(v) }
func (j jsonProvider) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) }
func (j jsonProvider) NewDecoder(reader io.Reader) binding.JSONDecoder {
return json.NewDecoder(reader)
}
func (j jsonProvider) NewEncoder(writer io.Writer) binding.JSONEncoder {
return json.NewEncoder(writer)
}
// AddBindingRules adds additional binding rules // AddBindingRules adds additional binding rules
func AddBindingRules() { func AddBindingRules() {
binding.JSONProvider = jsonProvider{}
addGitRefNameBindingRule() addGitRefNameBindingRule()
addValidURLListBindingRule() addValidURLListBindingRule()
addValidURLBindingRule() addValidURLBindingRule()

View File

@@ -10,7 +10,9 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project" project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/webhook" "code.gitea.io/gitea/services/webhook"
@@ -523,16 +525,49 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
type MergePullRequestForm struct { type MergePullRequestForm struct {
// required: true // required: true
// enum: ["merge","rebase","rebase-merge","squash","fast-forward-only","manually-merged"] // enum: ["merge","rebase","rebase-merge","squash","fast-forward-only","manually-merged"]
Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"` Do string `json:"do" binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"`
MergeTitleField string MergeTitleField string `json:"merge_title_field,omitempty"`
MergeMessageField string MergeMessageField string `json:"merge_message_field,omitempty"`
MergeCommitID string // only used for manually-merged MergeCommitID string `json:"merge_commit_id,omitempty"` // only used for manually-merged
HeadCommitID string `json:"head_commit_id,omitempty"` HeadCommitID string `json:"head_commit_id,omitempty"`
ForceMerge bool `json:"force_merge,omitempty"` ForceMerge bool `json:"force_merge,omitempty"`
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"` MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"` DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"`
} }
func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error {
// This is for backward compatibility, to support both field names like "do" and "Do",
// because old code doesn't have "json" tag for these fields
type aux struct {
Do1 string `json:"do"`
Do2 string `json:"Do"`
MergeTitleField1 string `json:"merge_title_field"`
MergeTitleField2 string `json:"MergeTitleField"`
MergeMessageField1 string `json:"merge_message_field"`
MergeMessageField2 string `json:"MergeMessageField"`
MergeCommitID1 string `json:"merge_commit_id"`
MergeCommitID2 string `json:"MergeCommitID"`
HeadCommitID string `json:"head_commit_id"`
ForceMerge bool `json:"force_merge"`
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed"`
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge"`
}
var a aux
if err := json.Unmarshal(b, &a); err != nil {
return err
}
f.Do = util.IfZero(a.Do1, a.Do2)
f.MergeTitleField = util.IfZero(a.MergeTitleField1, a.MergeTitleField2)
f.MergeMessageField = util.IfZero(a.MergeMessageField1, a.MergeMessageField2)
f.MergeCommitID = util.IfZero(a.MergeCommitID1, a.MergeCommitID2)
f.HeadCommitID = a.HeadCommitID
f.ForceMerge = a.ForceMerge
f.MergeWhenChecksSucceed = a.MergeWhenChecksSucceed
f.DeleteBranchAfterMerge = a.DeleteBranchAfterMerge
return nil
}
// Validate validates the fields // Validate validates the fields
func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req) ctx := context.GetValidateContext(req)

View File

@@ -6,7 +6,10 @@ package forms
import ( import (
"testing" "testing"
"code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestSubmitReviewForm_IsEmpty(t *testing.T) { func TestSubmitReviewForm_IsEmpty(t *testing.T) {
@@ -37,3 +40,48 @@ func TestSubmitReviewForm_IsEmpty(t *testing.T) {
assert.Equal(t, v.expected, v.form.HasEmptyContent()) assert.Equal(t, v.expected, v.form.HasEmptyContent())
} }
} }
func TestMergePullRequestForm(t *testing.T) {
expected := &MergePullRequestForm{
Do: "merge",
MergeTitleField: "title",
MergeMessageField: "message",
MergeCommitID: "merge-id",
HeadCommitID: "head-id",
ForceMerge: true,
MergeWhenChecksSucceed: true,
DeleteBranchAfterMerge: new(true),
}
t.Run("NewFields", func(t *testing.T) {
input := `{
"do": "merge",
"merge_title_field": "title",
"merge_message_field": "message",
"merge_commit_id": "merge-id",
"head_commit_id": "head-id",
"force_merge": true,
"merge_when_checks_succeed": true,
"delete_branch_after_merge": true
}`
var m *MergePullRequestForm
require.NoError(t, json.Unmarshal([]byte(input), &m))
assert.Equal(t, expected, m)
})
t.Run("OldFields", func(t *testing.T) {
input := `{
"Do": "merge",
"MergeTitleField": "title",
"MergeMessageField": "message",
"MergeCommitID": "merge-id",
"head_commit_id": "head-id",
"force_merge": true,
"merge_when_checks_succeed": true,
"delete_branch_after_merge": true
}`
var m *MergePullRequestForm
require.NoError(t, json.Unmarshal([]byte(input), &m))
assert.Equal(t, expected, m)
})
}

View File

@@ -26804,10 +26804,14 @@
"description": "MergePullRequestForm form for merging Pull Request", "description": "MergePullRequestForm form for merging Pull Request",
"type": "object", "type": "object",
"required": [ "required": [
"Do" "do"
], ],
"properties": { "properties": {
"Do": { "delete_branch_after_merge": {
"type": "boolean",
"x-go-name": "DeleteBranchAfterMerge"
},
"do": {
"type": "string", "type": "string",
"enum": [ "enum": [
"merge", "merge",
@@ -26816,20 +26820,8 @@
"squash", "squash",
"fast-forward-only", "fast-forward-only",
"manually-merged" "manually-merged"
] ],
}, "x-go-name": "Do"
"MergeCommitID": {
"type": "string"
},
"MergeMessageField": {
"type": "string"
},
"MergeTitleField": {
"type": "string"
},
"delete_branch_after_merge": {
"type": "boolean",
"x-go-name": "DeleteBranchAfterMerge"
}, },
"force_merge": { "force_merge": {
"type": "boolean", "type": "boolean",
@@ -26839,6 +26831,18 @@
"type": "string", "type": "string",
"x-go-name": "HeadCommitID" "x-go-name": "HeadCommitID"
}, },
"merge_commit_id": {
"type": "string",
"x-go-name": "MergeCommitID"
},
"merge_message_field": {
"type": "string",
"x-go-name": "MergeMessageField"
},
"merge_title_field": {
"type": "string",
"x-go-name": "MergeTitleField"
},
"merge_when_checks_succeed": { "merge_when_checks_succeed": {
"type": "boolean", "type": "boolean",
"x-go-name": "MergeWhenChecksSucceed" "x-go-name": "MergeWhenChecksSucceed"

View File

@@ -55,7 +55,10 @@ func TestAPIIssuesReactions(t *testing.T) {
DecodeJSON(t, resp, &apiNewReaction) DecodeJSON(t, resp, &apiNewReaction)
// Add existing reaction // Add existing reaction
MakeRequest(t, req, http.StatusForbidden) req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "rocket",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Blocked user can't react to comment // Blocked user can't react to comment
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
@@ -142,7 +145,10 @@ func TestAPICommentReactions(t *testing.T) {
DecodeJSON(t, resp, &apiNewReaction) DecodeJSON(t, resp, &apiNewReaction)
// Add existing reaction // Add existing reaction
MakeRequest(t, req, http.StatusForbidden) req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "+1",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Get end result of reaction list of issue #1 // Get end result of reaction list of issue #1
req = NewRequest(t, "GET", urlStr). req = NewRequest(t, "GET", urlStr).