@@ -17,12 +17,18 @@ package bufpolicy
1717import (
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
214220func (* 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+ }
0 commit comments