Skip to content

Commit 9706401

Browse files
authored
Update o1 Policy digest to use registry PolicyConfig (#3889)
1 parent f280099 commit 9706401

File tree

5 files changed

+371
-40
lines changed

5 files changed

+371
-40
lines changed

private/bufpkg/bufpolicy/digest.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@ package bufpolicy
1717
import (
1818
"bytes"
1919
"encoding/hex"
20+
"encoding/json"
2021
"errors"
2122
"fmt"
23+
"slices"
2224
"strconv"
2325
"strings"
2426

27+
pluginoptionv1 "buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go/buf/plugin/option/v1"
28+
"buf.build/go/bufplugin/option"
29+
"buf.build/go/standard/xslices"
2530
"github.com/bufbuild/buf/private/bufpkg/bufcas"
31+
"github.com/bufbuild/buf/private/bufpkg/bufconfig"
2632
"github.com/bufbuild/buf/private/bufpkg/bufparse"
2733
"github.com/bufbuild/buf/private/pkg/syserror"
2834
)
@@ -212,3 +218,207 @@ func (d *digest) String() string {
212218
}
213219

214220
func (*digest) isDigest() {}
221+
222+
// getO1Digest returns the O1 digest for the given PolicyConfig.
223+
func getO1Digest(policyConfig PolicyConfig) (Digest, error) {
224+
policyDataJSON, err := marshalStablePolicyConfig(policyConfig)
225+
if err != nil {
226+
return nil, err
227+
}
228+
bufcasDigest, err := bufcas.NewDigestForContent(bytes.NewReader(policyDataJSON))
229+
if err != nil {
230+
return nil, err
231+
}
232+
return NewDigest(DigestTypeO1, bufcasDigest)
233+
}
234+
235+
// marshalStablePolicyConfig marshals the given PolicyConfig to a stable JSON representation.
236+
//
237+
// This is used for the O1 digest and should not be used for other purposes.
238+
// It is a valid JSON encoding of the type buf.registry.policy.v1beta1.PolicyConfig.
239+
func marshalStablePolicyConfig(policyConfig PolicyConfig) ([]byte, error) {
240+
lintConfig := policyConfig.LintConfig()
241+
if lintConfig == nil {
242+
return nil, syserror.Newf("policyConfig.LintConfig() must not be nil")
243+
}
244+
breakingConfig := policyConfig.BreakingConfig()
245+
if breakingConfig == nil {
246+
return nil, syserror.Newf("policyConfig.BreakingConfig() must not be nil")
247+
}
248+
pluginConfigs, err := xslices.MapError(policyConfig.PluginConfigs(), func(pluginConfig bufconfig.PluginConfig) (*policyV1Beta1PolicyConfig_PluginConfig, error) {
249+
ref := pluginConfig.Ref()
250+
if ref == nil {
251+
return nil, fmt.Errorf("PluginConfig must have a non-nil Ref")
252+
}
253+
optionsConfig, err := optionsToOptionConfig(pluginConfig.Options())
254+
if err != nil {
255+
return nil, err
256+
}
257+
return &policyV1Beta1PolicyConfig_PluginConfig{
258+
Name: policyV1Beta1PolicyConfig_PluginConfig_Name{
259+
Owner: ref.FullName().Owner(),
260+
Plugin: ref.FullName().Name(),
261+
Ref: ref.Ref(),
262+
},
263+
Options: optionsConfig,
264+
Args: pluginConfig.Args(),
265+
}, nil
266+
})
267+
if err != nil {
268+
return nil, fmt.Errorf("failed converting PluginConfigs to PolicyConfig_CheckPluginConfig: %w", err)
269+
}
270+
slices.SortFunc(pluginConfigs, func(a, b *policyV1Beta1PolicyConfig_PluginConfig) int {
271+
// Sort by owner, plugin, and ref.
272+
return strings.Compare(
273+
fmt.Sprintf("%s/%s:%s", a.Name.Owner, a.Name.Plugin, a.Name.Ref),
274+
fmt.Sprintf("%s/%s:%s", b.Name.Owner, b.Name.Plugin, b.Name.Ref),
275+
)
276+
})
277+
config := policyV1Beta1PolicyConfig{
278+
Lint: &policyV1Beta1PolicyConfig_LintConfig{
279+
Use: lintConfig.UseIDsAndCategories(),
280+
Except: lintConfig.ExceptIDsAndCategories(),
281+
EnumZeroValueSuffix: lintConfig.EnumZeroValueSuffix(),
282+
RpcAllowSameRequestResponse: lintConfig.RPCAllowSameRequestResponse(),
283+
RpcAllowGoogleProtobufEmptyRequests: lintConfig.RPCAllowGoogleProtobufEmptyRequests(),
284+
RpcAllowGoogleProtobufEmptyResponses: lintConfig.RPCAllowGoogleProtobufEmptyResponses(),
285+
ServiceSuffix: lintConfig.ServiceSuffix(),
286+
},
287+
Breaking: &policyV1Beta1PolicyConfig_BreakingConfig{
288+
Use: breakingConfig.UseIDsAndCategories(),
289+
Except: breakingConfig.ExceptIDsAndCategories(),
290+
IgnoreUnstablePackages: breakingConfig.IgnoreUnstablePackages(),
291+
},
292+
Plugins: pluginConfigs,
293+
}
294+
return json.Marshal(config)
295+
}
296+
297+
// policyV1Beta1PolicyConfig is a stable JSON representation of the buf.registry.policy.v1beta1.PolicyConfig.
298+
type policyV1Beta1PolicyConfig struct {
299+
Lint *policyV1Beta1PolicyConfig_LintConfig `json:"lint,omitempty"`
300+
Breaking *policyV1Beta1PolicyConfig_BreakingConfig `json:"breaking,omitempty"`
301+
Plugins []*policyV1Beta1PolicyConfig_PluginConfig `json:"plugins,omitempty"`
302+
}
303+
304+
// policyV1Beta1PolicyConfig_LintConfig is a stable JSON representation of the buf.registry.policy.v1beta1.PolicyConfig.LintConfig.
305+
type policyV1Beta1PolicyConfig_LintConfig struct {
306+
Use []string `json:"use,omitempty"`
307+
Except []string `json:"except,omitempty"`
308+
EnumZeroValueSuffix string `json:"enumZeroValue_suffix,omitempty"`
309+
RpcAllowSameRequestResponse bool `json:"rpcAllowSame_request_response,omitempty"`
310+
RpcAllowGoogleProtobufEmptyRequests bool `json:"rpcAllowGoogleProtobufEmptyRequests,omitempty"`
311+
RpcAllowGoogleProtobufEmptyResponses bool `json:"rpcAllowGoogleProtobufEmptyResponses,omitempty"`
312+
ServiceSuffix string `json:"serviceSuffix,omitempty"`
313+
}
314+
315+
// policyV1Beta1PolicyConfig_BreakingConfig is a stable JSON representation of the buf.registry.policy.v1beta1.PolicyConfig.BreakingConfig.
316+
type policyV1Beta1PolicyConfig_BreakingConfig struct {
317+
Use []string `json:"use,omitempty"`
318+
Except []string `json:"except,omitempty"`
319+
IgnoreUnstablePackages bool `json:"ignoreUnstablePackages,omitempty"`
320+
}
321+
322+
// policyV1Beta1PolicyConfig_PluginConfig is a stable JSON representation of the buf.registry.policy.v1beta1.PolicyConfig.PluginConfig.
323+
type policyV1Beta1PolicyConfig_PluginConfig struct {
324+
Name policyV1Beta1PolicyConfig_PluginConfig_Name `json:"name,omitempty"`
325+
Options []*optionV1Option `json:"options,omitempty"`
326+
Args []string `json:"args,omitempty"`
327+
}
328+
329+
// policyV1Beta1PolicyConfig_PluginConfig_Name is a stable JSON representation of the buf.registry.policy.v1beta1.PolicyConfig.PluginConfig.Name.
330+
type policyV1Beta1PolicyConfig_PluginConfig_Name struct {
331+
Owner string `json:"owner,omitempty"`
332+
Plugin string `json:"plugin,omitempty"`
333+
Ref string `json:"ref,omitempty"`
334+
}
335+
336+
// optionV1Option is a stable JSON representation of the buf.plugin.option.v1.Option.
337+
type optionV1Option struct {
338+
Key string `json:"key,omitempty"`
339+
Value *optionV1Value `json:"value,omitempty"`
340+
}
341+
342+
// optionV1Value is a stable JSON representation of the buf.plugin.option.v1.Value.
343+
type optionV1Value struct {
344+
BoolValue bool `json:"boolValue,omitempty"`
345+
Int64Value int64 `json:"intValue,omitempty"`
346+
DoubleValue float64 `json:"floatValue,omitempty"`
347+
StringValue string `json:"stringValue,omitempty"`
348+
BytesValue []byte `json:"bytesValue,omitempty"`
349+
ListValue *optionV1ListValue `json:"listValue,omitempty"`
350+
}
351+
352+
// optionV1ListValue is a stable JSON representation of the buf.plugin.option.v1.ListValue.
353+
type optionV1ListValue struct {
354+
Values []*optionV1Value `json:"values,omitempty"`
355+
}
356+
357+
// optionsToOptionsV1Options converts a map of options to a slice of optionV1Option.
358+
func optionsToOptionConfig(keyToValue map[string]any) ([]*optionV1Option, error) {
359+
options, err := option.NewOptions(keyToValue) // This will validate the options.
360+
if err != nil {
361+
return nil, fmt.Errorf("failed to convert options: %w", err)
362+
}
363+
optionsProto, err := options.ToProto()
364+
if err != nil {
365+
return nil, fmt.Errorf("failed to convert options to proto: %w", err)
366+
}
367+
// Sort the options by key to ensure a stable order.
368+
slices.SortFunc(optionsProto, func(a, b *pluginoptionv1.Option) int {
369+
return strings.Compare(a.Key, b.Key)
370+
})
371+
optionsV1Options := make([]*optionV1Option, len(optionsProto))
372+
for i, optionProto := range optionsProto {
373+
optionValue, err := optionV1ValueProtoToOptionValue(optionProto.Value)
374+
if err != nil {
375+
return nil, fmt.Errorf("failed to convert option value: %w", err)
376+
}
377+
optionsV1Options[i] = &optionV1Option{
378+
Key: optionProto.Key,
379+
Value: optionValue,
380+
}
381+
}
382+
return optionsV1Options, nil
383+
}
384+
385+
func optionV1ValueProtoToOptionValue(optionValue *pluginoptionv1.Value) (*optionV1Value, error) {
386+
if optionValue == nil {
387+
return nil, nil
388+
}
389+
switch optionValue.Type.(type) {
390+
case *pluginoptionv1.Value_BoolValue:
391+
return &optionV1Value{BoolValue: optionValue.GetBoolValue()}, nil
392+
case *pluginoptionv1.Value_Int64Value:
393+
return &optionV1Value{Int64Value: optionValue.GetInt64Value()}, nil
394+
case *pluginoptionv1.Value_DoubleValue:
395+
return &optionV1Value{DoubleValue: optionValue.GetDoubleValue()}, nil
396+
case *pluginoptionv1.Value_StringValue:
397+
return &optionV1Value{StringValue: optionValue.GetStringValue()}, nil
398+
case *pluginoptionv1.Value_BytesValue:
399+
return &optionV1Value{BytesValue: optionValue.GetBytesValue()}, nil
400+
case *pluginoptionv1.Value_ListValue:
401+
listValue, err := optionV1ListValueProtoToOptionListValue(optionValue.GetListValue())
402+
if err != nil {
403+
return nil, err
404+
}
405+
return &optionV1Value{ListValue: listValue}, nil
406+
default:
407+
return nil, fmt.Errorf("unknown option value type: %T", optionValue.Type)
408+
}
409+
}
410+
411+
func optionV1ListValueProtoToOptionListValue(listValue *pluginoptionv1.ListValue) (*optionV1ListValue, error) {
412+
if listValue == nil {
413+
return nil, nil
414+
}
415+
values := make([]*optionV1Value, len(listValue.Values))
416+
for i, value := range listValue.Values {
417+
optionValue, err := optionV1ValueProtoToOptionValue(value)
418+
if err != nil {
419+
return nil, fmt.Errorf("failed to convert option value: %w", err)
420+
}
421+
values[i] = optionValue
422+
}
423+
return &optionV1ListValue{Values: values}, nil
424+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package bufpolicy
16+
17+
import (
18+
"testing"
19+
20+
"github.com/bufbuild/buf/private/bufpkg/bufconfig"
21+
"github.com/bufbuild/buf/private/bufpkg/bufparse"
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestO1Digest(t *testing.T) {
27+
t.Parallel()
28+
lintConfig := bufconfig.NewLintConfig(
29+
bufconfig.NewEnabledCheckConfigForUseIDsAndCategories(
30+
bufconfig.FileVersionV2,
31+
[]string{"LINT_ID_1", "LINT_ID_2"},
32+
true, // Disable builtin is true by default.
33+
),
34+
"enumZeroValueSuffix",
35+
true,
36+
true,
37+
true,
38+
"serviceSuffix",
39+
false, // Policy configs do not allow comment ignores.
40+
)
41+
breakingConfig := bufconfig.NewBreakingConfig(
42+
bufconfig.NewEnabledCheckConfigForUseIDsAndCategories(
43+
bufconfig.FileVersionV2,
44+
[]string{"BREAKING_ID_1", "BREAKING_ID_2"},
45+
true, // Disable builtin is true by default.
46+
),
47+
true,
48+
)
49+
policyConfig := &testPolicyConfig{
50+
lintConfig: lintConfig,
51+
breakingConfig: breakingConfig,
52+
}
53+
testPolicyConfigO1Digest(t, policyConfig, "o1:db2906a09cca66da39f800207c75a8a2134d1c7918eca793e19ab07d76bea7a8f1282827205a6b7013ee0c1196af4ae7ef3abc6c41dab039bc24153d2e2dc4af")
54+
// Add a remote plugin config.
55+
options := map[string]any{
56+
"a": "b",
57+
"c": 3,
58+
"d": 1.2,
59+
"e": []string{"a", "b", "c"},
60+
}
61+
args := []string{"arg1", "arg2"}
62+
remotePluginRef, err := bufparse.NewRef("buf.build", "acme", "my-plugin", "v1.0.0")
63+
require.NoError(t, err)
64+
remotePluginConfig, err := bufconfig.NewRemoteWasmPluginConfig(remotePluginRef, options, args)
65+
require.NoError(t, err)
66+
policyConfig.pluginConfigs = append(policyConfig.pluginConfigs, remotePluginConfig)
67+
testPolicyConfigO1Digest(t, policyConfig, "o1:6862edf26139073f77846d2afa6d3c23016f4f0ae9abce74ec5485bb8c65ee2c32a9da80263bdf1ea1736ca46fd8fa31c5e14610c2c3dbee4fab96985122fa14")
68+
remotePluginRef2, err := bufparse.NewRef("buf.build", "acme", "a-plugin", "")
69+
require.NoError(t, err)
70+
remotePluginConfig2, err := bufconfig.NewRemoteWasmPluginConfig(remotePluginRef2, options, args)
71+
require.NoError(t, err)
72+
// We should get the same digest regardless of the order of the remote plugins.
73+
policyConfig.pluginConfigs = append(policyConfig.pluginConfigs, remotePluginConfig2)
74+
const multiPluginDigest = "o1:8612d6270b3ea1e222554eb40aadd9194dcfedf772ffc00ac053abed3ce8e201487088ede5f889b1bfc6236f280e0cab47cf434f91de2a9ccc1ad562334582f7"
75+
testPolicyConfigO1Digest(t, policyConfig, multiPluginDigest)
76+
// Swap the order and assert that the digest is the same.
77+
policyConfig.pluginConfigs[0], policyConfig.pluginConfigs[1] = policyConfig.pluginConfigs[1], policyConfig.pluginConfigs[0]
78+
testPolicyConfigO1Digest(t, policyConfig, multiPluginDigest)
79+
}
80+
81+
func testPolicyConfigO1Digest(t *testing.T, policyConfig PolicyConfig, expectDigest string) {
82+
digestFromPolicyConfig, err := getO1Digest(policyConfig)
83+
require.NoError(t, err)
84+
expectedDigest, err := ParseDigest(expectDigest)
85+
require.NoError(t, err)
86+
assert.True(t, DigestEqual(expectedDigest, digestFromPolicyConfig), "Digest mismatch, expected %q got %q", expectedDigest.String(), digestFromPolicyConfig.String())
87+
}
88+
89+
type testPolicyConfig struct {
90+
lintConfig bufconfig.LintConfig
91+
breakingConfig bufconfig.BreakingConfig
92+
pluginConfigs []bufconfig.PluginConfig
93+
}
94+
95+
func (p *testPolicyConfig) LintConfig() bufconfig.LintConfig {
96+
return p.lintConfig
97+
}
98+
func (p *testPolicyConfig) BreakingConfig() bufconfig.BreakingConfig {
99+
return p.breakingConfig
100+
}
101+
func (p *testPolicyConfig) PluginConfigs() []bufconfig.PluginConfig {
102+
return p.pluginConfigs
103+
}

0 commit comments

Comments
 (0)