Skip to content

Commit bcf9659

Browse files
committed
fix
1 parent 07afc37 commit bcf9659

File tree

9 files changed

+121
-48
lines changed

9 files changed

+121
-48
lines changed

modules/assetfs/embed.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error {
365365
if err = embedFiles(meta.Root, fsRootPath, ""); err != nil {
366366
return err
367367
}
368-
jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL
368+
jsonBuf, err := json.Marshal(meta)
369369
if err != nil {
370370
return err
371371
}
372372
_, _ = output.Write([]byte{'\n'})
373-
_, err = output.Write(jsonBuf)
373+
_, err = output.Write(bytes.TrimSpace(jsonBuf))
374374
return err
375375
}

modules/json/jsonlegacy.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
14
//go:build !goexperiment.jsonv2
25

36
package json
@@ -7,3 +10,7 @@ import jsoniter "github.com/json-iterator/go"
710
func getDefaultJSONHandler() Interface {
811
return JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
912
}
13+
14+
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
15+
return DefaultJSONMarshaler.Marshal(v)
16+
}

modules/json/jsonv2.go

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,59 +7,63 @@ package json
77

88
import (
99
"bytes"
10+
jsonv1 "encoding/json" //nolint:depguard // this package wraps it
1011
jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it
1112
"io"
1213
)
1314

1415
// JSONv2 implements Interface via encoding/json/v2
1516
// Requires GOEXPERIMENT=jsonv2 to be set at build time
16-
type JSONv2 struct{}
17-
18-
var _ Interface = JSONv2{}
19-
20-
func getDefaultJSONHandler() Interface {
21-
return JSONv2{}
17+
type JSONv2 struct {
18+
marshalOptions jsonv2.Options
19+
marshalKeepOptionalEmptyOptions jsonv2.Options
20+
unmarshalOptions jsonv2.Options
2221
}
2322

24-
func jsonv2DefaultMarshalOptions() jsonv2.Options {
25-
return jsonv2.JoinOptions(
23+
var jsonV2 JSONv2
24+
25+
func init() {
26+
commonMarshalOptions := []jsonv2.Options{
2627
jsonv2.MatchCaseInsensitiveNames(true),
2728
jsonv2.FormatNilSliceAsNull(true),
2829
jsonv2.FormatNilMapAsNull(true),
2930
jsonv2.Deterministic(true),
30-
)
31+
}
32+
jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...)
33+
jsonV2.unmarshalOptions = jsonv2.JoinOptions(jsonv2.MatchCaseInsensitiveNames(true))
34+
35+
// by default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from.
36+
// v1 has a different behavior: if the `""` is from a null pointer, or a Marshal function, it is kept.
37+
jsonV2.marshalKeepOptionalEmptyOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsonv1.OmitEmptyWithLegacySemantics(true))...)
3138
}
3239

33-
func jsonv2DefaultUnmarshalOptions() jsonv2.Options {
34-
return jsonv2.JoinOptions(
35-
jsonv2.MatchCaseInsensitiveNames(true),
36-
)
40+
func getDefaultJSONHandler() Interface {
41+
return jsonV2
3742
}
3843

39-
func (JSONv2) Marshal(v any) ([]byte, error) {
40-
return jsonv2.Marshal(v, jsonv2DefaultMarshalOptions())
44+
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
45+
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
4146
}
4247

43-
func (JSONv2) Unmarshal(data []byte, v any) error {
44-
// legacy behavior: treat empty or whitespace-only input as no input, it should be safe
45-
data = bytes.TrimSpace(data)
46-
if len(data) == 0 {
47-
return nil
48-
}
49-
return jsonv2.Unmarshal(data, v, jsonv2DefaultUnmarshalOptions())
48+
func (j JSONv2) Marshal(v any) ([]byte, error) {
49+
return jsonv2.Marshal(v, j.marshalOptions)
50+
}
51+
52+
func (j JSONv2) Unmarshal(data []byte, v any) error {
53+
return jsonv2.Unmarshal(data, v, j.unmarshalOptions)
5054
}
5155

52-
func (JSONv2) NewEncoder(writer io.Writer) Encoder {
53-
return &encoderV2{writer: writer, opts: jsonv2DefaultMarshalOptions()}
56+
func (j JSONv2) NewEncoder(writer io.Writer) Encoder {
57+
return &encoderV2{writer: writer, opts: j.marshalOptions}
5458
}
5559

56-
func (JSONv2) NewDecoder(reader io.Reader) Decoder {
57-
return &decoderV2{reader: reader, opts: jsonv2DefaultMarshalOptions()}
60+
func (j JSONv2) NewDecoder(reader io.Reader) Decoder {
61+
return &decoderV2{reader: reader, opts: j.unmarshalOptions}
5862
}
5963

6064
// Indent implements Interface using standard library (JSON v2 doesn't have Indent yet)
6165
func (JSONv2) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
62-
return json.Indent(dst, src, prefix, indent)
66+
return jsonv1.Indent(dst, src, prefix, indent)
6367
}
6468

6569
type encoderV2 struct {

modules/lfs/http_client_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) {
193193
},
194194
{
195195
endpoint: "https://invalid-json-response.io",
196-
expectedError: "invalid json",
196+
expectedError: "/(invalid json|jsontext: invalid character)/",
197197
},
198198
{
199199
endpoint: "https://valid-batch-request-download.io",
@@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) {
258258
return nil
259259
})
260260
if c.expectedError != "" {
261-
assert.ErrorContains(t, err, c.expectedError)
261+
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
262+
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
263+
} else {
264+
assert.ErrorContains(t, err, c.expectedError)
265+
}
262266
} else {
263267
assert.NoError(t, err)
264268
}
@@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) {
297301
},
298302
{
299303
endpoint: "https://invalid-json-response.io",
300-
expectedError: "invalid json",
304+
expectedError: "/(invalid json|jsontext: invalid character)/",
301305
},
302306
{
303307
endpoint: "https://valid-batch-request-upload.io",
@@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) {
352356
return io.NopCloser(new(bytes.Buffer)), objectError
353357
})
354358
if c.expectedError != "" {
355-
assert.ErrorContains(t, err, c.expectedError)
359+
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
360+
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
361+
} else {
362+
assert.ErrorContains(t, err, c.expectedError)
363+
}
356364
} else {
357365
assert.NoError(t, err)
358366
}

modules/optional/serialization_test.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ import (
1515
)
1616

1717
type testSerializationStruct struct {
18-
NormalString string `json:"normal_string" yaml:"normal_string"`
19-
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
20-
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
21-
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
18+
NormalString string `json:"normal_string" yaml:"normal_string"`
19+
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
20+
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
21+
22+
// It causes an undefined behavior: should the "omitempty" tag only omit "null", or also the empty string?
23+
// The behavior is inconsistent between json and v2 packages, and there is no such use case in Gitea.
24+
// If anyone really needs it, they can use json.MarshalKeepOptionalEmpty to revert the v1 behavior
25+
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
26+
2227
OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"`
23-
OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"`
28+
OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"`
2429
}
2530

2631
func TestOptionalToJson(t *testing.T) {
@@ -32,7 +37,7 @@ func TestOptionalToJson(t *testing.T) {
3237
{
3338
name: "empty",
3439
obj: new(testSerializationStruct),
35-
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`,
40+
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_two_string":null}`,
3641
},
3742
{
3843
name: "some",
@@ -44,12 +49,12 @@ func TestOptionalToJson(t *testing.T) {
4449
OptTwoBool: optional.None[bool](),
4550
OptTwoString: optional.None[string](),
4651
},
47-
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
52+
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
4853
},
4954
}
5055
for _, tc := range tests {
5156
t.Run(tc.name, func(t *testing.T) {
52-
b, err := json.Marshal(tc.obj)
57+
b, err := json.MarshalKeepOptionalEmpty(tc.obj)
5358
assert.NoError(t, err)
5459
assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected")
5560

@@ -75,7 +80,7 @@ func TestOptionalFromJson(t *testing.T) {
7580
},
7681
{
7782
name: "some",
78-
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
83+
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
7984
want: testSerializationStruct{
8085
NormalString: "a string",
8186
NormalBool: true,
@@ -169,7 +174,7 @@ normal_bool: true
169174
optional_bool: false
170175
optional_string: ""
171176
optional_two_bool: null
172-
optional_twostring: null
177+
optional_two_string: null
173178
`,
174179
want: testSerializationStruct{
175180
NormalString: "a string",

services/webhook/deliver_test.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,54 @@ func TestWebhookDeliverHookTask(t *testing.T) {
131131
assert.NoError(t, unittest.PrepareTestDatabase())
132132

133133
done := make(chan struct{}, 1)
134+
version2Body := `{
135+
"body": "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1",
136+
"msgtype": "",
137+
"format": "org.matrix.custom.html",
138+
"formatted_body": "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] user1 pushed 2 commits to <a href=\"http://localhost:3000/test/repo/src/branch/test\">test</a>:<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1",
139+
"io.gitea.commits": [
140+
{
141+
"id": "2020558fe2e34debb818a514715839cabd25e778",
142+
"message": "commit message",
143+
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
144+
"author": {
145+
"name": "user1",
146+
"email": "user1@localhost",
147+
"username": "user1"
148+
},
149+
"committer": {
150+
"name": "user1",
151+
"email": "user1@localhost",
152+
"username": "user1"
153+
},
154+
"verification": null,
155+
"timestamp": "0001-01-01T00:00:00Z",
156+
"added": null,
157+
"removed": null,
158+
"modified": null
159+
},
160+
{
161+
"id": "2020558fe2e34debb818a514715839cabd25e778",
162+
"message": "commit message",
163+
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
164+
"author": {
165+
"name": "user1",
166+
"email": "user1@localhost",
167+
"username": "user1"
168+
},
169+
"committer": {
170+
"name": "user1",
171+
"email": "user1@localhost",
172+
"username": "user1"
173+
},
174+
"verification": null,
175+
"timestamp": "0001-01-01T00:00:00Z",
176+
"added": null,
177+
"removed": null,
178+
"modified": null
179+
}
180+
]
181+
}`
134182
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
135183
assert.Equal(t, "PUT", r.Method)
136184
switch r.URL.Path {
@@ -142,13 +190,13 @@ func TestWebhookDeliverHookTask(t *testing.T) {
142190
assert.NoError(t, err)
143191
assert.Equal(t, `{"data": 42}`, string(body))
144192

145-
case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
193+
case "/webhook/4ddf3b1533e54f082ae6eadfc1b5530be36c8893":
146194
// Version 2
147195
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
148196
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
149197
body, err := io.ReadAll(r.Body)
150198
assert.NoError(t, err)
151-
assert.Len(t, body, 2147)
199+
assert.JSONEq(t, version2Body, string(body))
152200

153201
default:
154202
w.WriteHeader(http.StatusNotFound)

services/webhook/matrix.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ func getMessageBody(htmlText string) string {
274274

275275
// getMatrixTxnID computes the transaction ID to ensure idempotency
276276
func getMatrixTxnID(payload []byte) (string, error) {
277+
payload = bytes.TrimSpace(payload)
277278
if len(payload) >= matrixPayloadSizeLimit {
278279
return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
279280
}

services/webhook/matrix_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func TestMatrixJSONPayload(t *testing.T) {
216216
require.NoError(t, err)
217217

218218
assert.Equal(t, "PUT", req.Method)
219-
assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path)
219+
assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/4ddf3b1533e54f082ae6eadfc1b5530be36c8893", req.URL.Path)
220220
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
221221
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
222222
var body MatrixPayload

tests/integration/api_repo_branch_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) {
121121
resp = MakeRequest(t, req, http.StatusForbidden)
122122
bs, err = io.ReadAll(resp.Body)
123123
assert.NoError(t, err)
124-
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
124+
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
125125

126126
resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden)
127127
bs, err = io.ReadAll(resp.Body)
128128
assert.NoError(t, err)
129-
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
129+
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
130130
}

0 commit comments

Comments
 (0)