blob: 324a1aa996deccc17fe7007710a32f6ab02e172d [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package policy
import (
"bytes"
"encoding/gob"
"fmt"
"sort"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
// ConfigBundle is a bunch of related parsed text proto files.
//
// For example, it may be a main top-level config and a bunch of include files
// it references.
//
// Keys are paths, values are corresponding proto messages. Users are supposed
// to know themselves what concrete proto types correspond to what paths.
type ConfigBundle map[string]proto.Message
// blobWithType is underlying type of a slice we gob-serialize in serialize().
type blobWithType struct {
Path string // the path the config was fetched from
Kind string // proto message kind, to know how to deserialize
Blob []byte // proto message, binary serialization
}
// serializeBundle deterministically converts ConfigBundle into a byte blob.
//
// The byte blob references proto message names to know how to deserialize them
// later.
func serializeBundle(b ConfigBundle) ([]byte, error) {
keys := make([]string, 0, len(b))
for k := range b {
keys = append(keys, k)
}
sort.Strings(keys)
items := make([]blobWithType, 0, len(b))
for _, k := range keys {
v := b[k]
if v == nil {
return nil, fmt.Errorf("no config body in %q", k)
}
blob, err := proto.Marshal(v)
if err != nil {
return nil, err
}
items = append(items, blobWithType{
Path: k,
Kind: string(proto.MessageName(v)),
Blob: blob,
})
}
out := bytes.Buffer{}
if err := gob.NewEncoder(&out).Encode(items); err != nil {
return nil, err
}
return out.Bytes(), nil
}
// deserialize parses the serialized ConfigBundle.
//
// It skips configs with proto types no longer registered in the proto lib
// registry. It returns them unparsed in 'unknown' slice.
//
// Returns an error if some known proto message can't be deserialized.
func deserializeBundle(blob []byte) (b ConfigBundle, unknown []blobWithType, err error) {
items := []blobWithType{}
if err := gob.NewDecoder(bytes.NewReader(blob)).Decode(&items); err != nil {
return nil, nil, err
}
b = make(ConfigBundle, len(items))
for _, item := range items {
typ, err := protoregistry.GlobalTypes.FindMessageByName(protoreflect.FullName(item.Kind))
if err != nil {
unknown = append(unknown, item)
continue
}
msg := typ.New().Interface()
if err := proto.Unmarshal(item.Blob, msg); err != nil {
return nil, nil, err
}
b[item.Path] = msg
}
return b, unknown, nil
}