package buildflags import ( "encoding/json" "fmt" "maps" "strconv" "strings" controllerapi "github.com/docker/buildx/controller/pb" "github.com/pkg/errors" "github.com/tonistiigi/go-csvvalue" ) type Attests []*Attest func (a Attests) Merge(other Attests) Attests { if other == nil { a.Normalize() return a } else if a == nil { other.Normalize() return other } return append(a, other...).Normalize() } func (a Attests) Normalize() Attests { if len(a) == 0 { return nil } return removeAttestDupes(a) } func (a Attests) ToPB() []*controllerapi.Attest { if len(a) == 0 { return nil } entries := make([]*controllerapi.Attest, len(a)) for i, entry := range a { entries[i] = entry.ToPB() } return entries } type Attest struct { Type string `json:"type"` Disabled bool `json:"disabled,omitempty"` Attrs map[string]string `json:"attrs,omitempty"` } func (a *Attest) Equal(other *Attest) bool { if a.Type != other.Type || a.Disabled != other.Disabled { return false } return maps.Equal(a.Attrs, other.Attrs) } func (a *Attest) String() string { var b csvBuilder if a.Type != "" { b.Write("type", a.Type) } if a.Disabled { b.Write("disabled", "true") } if len(a.Attrs) > 0 { b.WriteAttributes(a.Attrs) } return b.String() } func (a *Attest) ToPB() *controllerapi.Attest { var b csvBuilder if a.Type != "" { b.Write("type", a.Type) } if a.Disabled { b.Write("disabled", "true") } b.WriteAttributes(a.Attrs) return &controllerapi.Attest{ Type: a.Type, Disabled: a.Disabled, Attrs: b.String(), } } func (a *Attest) MarshalJSON() ([]byte, error) { m := make(map[string]any, len(a.Attrs)+2) for k, v := range a.Attrs { m[k] = v } m["type"] = a.Type if a.Disabled { m["disabled"] = true } return json.Marshal(m) } func (a *Attest) UnmarshalJSON(data []byte) error { var m map[string]any if err := json.Unmarshal(data, &m); err != nil { return err } if typ, ok := m["type"]; ok { a.Type, ok = typ.(string) if !ok { return errors.Errorf("attest type must be a string") } delete(m, "type") } if disabled, ok := m["disabled"]; ok { a.Disabled, ok = disabled.(bool) if !ok { return errors.Errorf("attest disabled attribute must be a boolean") } delete(m, "disabled") } attrs := make(map[string]string, len(m)) for k, v := range m { s, ok := v.(string) if !ok { return errors.Errorf("attest attribute %q must be a string", k) } attrs[k] = s } a.Attrs = attrs return nil } func (a *Attest) UnmarshalText(text []byte) error { in := string(text) fields, err := csvvalue.Fields(in, nil) if err != nil { return err } a.Attrs = map[string]string{} for _, field := range fields { key, value, ok := strings.Cut(field, "=") if !ok { return errors.Errorf("invalid value %s", field) } switch strings.TrimSpace(strings.ToLower(key)) { case "type": a.Type = value case "disabled": disabled, err := strconv.ParseBool(value) if err != nil { return errors.Wrapf(err, "invalid value %s", field) } a.Disabled = disabled default: a.Attrs[key] = value } } return a.validate() } func (a *Attest) validate() error { if a.Type == "" { return errors.Errorf("attestation type not specified") } return nil } func CanonicalizeAttest(attestType string, in string) string { if in == "" { return "" } if b, err := strconv.ParseBool(in); err == nil { return fmt.Sprintf("type=%s,disabled=%t", attestType, !b) } return fmt.Sprintf("type=%s,%s", attestType, in) } func ParseAttests(in []string) ([]*controllerapi.Attest, error) { var outs []*Attest for _, s := range in { var out Attest if err := out.UnmarshalText([]byte(s)); err != nil { return nil, err } outs = append(outs, &out) } return ConvertAttests(outs) } // ConvertAttests converts Attestations for the controller API from // the ones in this package. // // Attestations of the same type will cause an error. Some tools, // like bake, remove the duplicates before calling this function. func ConvertAttests(in []*Attest) ([]*controllerapi.Attest, error) { out := make([]*controllerapi.Attest, 0, len(in)) // Check for duplicate attestations while we convert them // to the controller API. found := map[string]struct{}{} for _, attest := range in { if _, ok := found[attest.Type]; ok { return nil, errors.Errorf("duplicate attestation field %s", attest.Type) } found[attest.Type] = struct{}{} out = append(out, attest.ToPB()) } return out, nil } func ParseAttest(in string) (*controllerapi.Attest, error) { if in == "" { return nil, nil } fields, err := csvvalue.Fields(in, nil) if err != nil { return nil, err } attest := controllerapi.Attest{ Attrs: in, } for _, field := range fields { key, value, ok := strings.Cut(field, "=") if !ok { return nil, errors.Errorf("invalid value %s", field) } key = strings.TrimSpace(strings.ToLower(key)) switch key { case "type": attest.Type = value case "disabled": disabled, err := strconv.ParseBool(value) if err != nil { return nil, errors.Wrapf(err, "invalid value %s", field) } attest.Disabled = disabled } } if attest.Type == "" { return nil, errors.Errorf("attestation type not specified") } return &attest, nil } func removeAttestDupes(s []*Attest) []*Attest { res := []*Attest{} m := map[string]int{} for _, att := range s { if i, ok := m[att.Type]; ok { res[i] = att } else { m[att.Type] = len(res) res = append(res, att) } } return res }