mirror of https://github.com/goharbor/harbor.git
481 lines
13 KiB
Go
481 lines
13 KiB
Go
/*
|
|
Copyright 2021 The CloudEvents Authors
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package event
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
|
|
"github.com/cloudevents/sdk-go/v2/types"
|
|
)
|
|
|
|
const specVersionV03Flag uint8 = 1 << 4
|
|
const specVersionV1Flag uint8 = 1 << 5
|
|
const dataBase64Flag uint8 = 1 << 6
|
|
const dataContentTypeFlag uint8 = 1 << 7
|
|
|
|
func checkFlag(state uint8, flag uint8) bool {
|
|
return state&flag != 0
|
|
}
|
|
|
|
func appendFlag(state *uint8, flag uint8) {
|
|
*state = (*state) | flag
|
|
}
|
|
|
|
var iterPool = sync.Pool{
|
|
New: func() interface{} {
|
|
return jsoniter.Parse(jsoniter.ConfigFastest, nil, 1024)
|
|
},
|
|
}
|
|
|
|
func borrowIterator(reader io.Reader) *jsoniter.Iterator {
|
|
iter := iterPool.Get().(*jsoniter.Iterator)
|
|
iter.Reset(reader)
|
|
return iter
|
|
}
|
|
|
|
func returnIterator(iter *jsoniter.Iterator) {
|
|
iter.Error = nil
|
|
iter.Attachment = nil
|
|
iterPool.Put(iter)
|
|
}
|
|
|
|
func ReadJson(out *Event, reader io.Reader) error {
|
|
iterator := borrowIterator(reader)
|
|
defer returnIterator(iterator)
|
|
|
|
return readJsonFromIterator(out, iterator)
|
|
}
|
|
|
|
// ReadJson allows you to read the bytes reader as an event
|
|
func readJsonFromIterator(out *Event, iterator *jsoniter.Iterator) error {
|
|
// Parsing dependency graph:
|
|
// SpecVersion
|
|
// ^ ^
|
|
// | +--------------+
|
|
// + +
|
|
// All Attributes datacontenttype (and datacontentencoding for v0.3)
|
|
// (except datacontenttype) ^
|
|
// |
|
|
// |
|
|
// +
|
|
// Data
|
|
|
|
var state uint8 = 0
|
|
var cachedData []byte
|
|
|
|
var (
|
|
// Universally parseable fields.
|
|
id string
|
|
typ string
|
|
source types.URIRef
|
|
subject *string
|
|
time *types.Timestamp
|
|
datacontenttype *string
|
|
extensions = make(map[string]interface{})
|
|
|
|
// These fields require knowledge about the specversion to be parsed.
|
|
schemaurl jsoniter.Any
|
|
datacontentencoding jsoniter.Any
|
|
dataschema jsoniter.Any
|
|
dataBase64 jsoniter.Any
|
|
)
|
|
|
|
for key := iterator.ReadObject(); key != ""; key = iterator.ReadObject() {
|
|
// Check if we have some error in our error cache
|
|
if iterator.Error != nil {
|
|
return iterator.Error
|
|
}
|
|
|
|
// We have a key, now we need to figure out what to do
|
|
// depending on the parsing state
|
|
|
|
// If it's a specversion, trigger state change
|
|
if key == "specversion" {
|
|
if checkFlag(state, specVersionV1Flag|specVersionV03Flag) {
|
|
return fmt.Errorf("specversion was already provided")
|
|
}
|
|
sv := iterator.ReadString()
|
|
|
|
// Check proper specversion
|
|
switch sv {
|
|
case CloudEventsVersionV1:
|
|
con := &EventContextV1{
|
|
ID: id,
|
|
Type: typ,
|
|
Source: source,
|
|
Subject: subject,
|
|
Time: time,
|
|
DataContentType: datacontenttype,
|
|
}
|
|
|
|
// Add the fields relevant for the version ...
|
|
if dataschema != nil {
|
|
var err error
|
|
con.DataSchema, err = toUriPtr(dataschema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if dataBase64 != nil {
|
|
stream := jsoniter.ConfigFastest.BorrowStream(nil)
|
|
defer jsoniter.ConfigFastest.ReturnStream(stream)
|
|
dataBase64.WriteTo(stream)
|
|
cachedData = stream.Buffer()
|
|
if stream.Error != nil {
|
|
return stream.Error
|
|
}
|
|
appendFlag(&state, dataBase64Flag)
|
|
}
|
|
|
|
// ... add all remaining fields as extensions.
|
|
if schemaurl != nil {
|
|
extensions["schemaurl"] = schemaurl.GetInterface()
|
|
}
|
|
if datacontentencoding != nil {
|
|
extensions["datacontentencoding"] = datacontentencoding.GetInterface()
|
|
}
|
|
|
|
out.Context = con
|
|
appendFlag(&state, specVersionV1Flag)
|
|
case CloudEventsVersionV03:
|
|
con := &EventContextV03{
|
|
ID: id,
|
|
Type: typ,
|
|
Source: source,
|
|
Subject: subject,
|
|
Time: time,
|
|
DataContentType: datacontenttype,
|
|
}
|
|
var err error
|
|
// Add the fields relevant for the version ...
|
|
if schemaurl != nil {
|
|
con.SchemaURL, err = toUriRefPtr(schemaurl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if datacontentencoding != nil {
|
|
con.DataContentEncoding, err = toStrPtr(datacontentencoding)
|
|
if *con.DataContentEncoding != Base64 {
|
|
err = ValidationError{"datacontentencoding": errors.New("invalid datacontentencoding value, the only allowed value is 'base64'")}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
appendFlag(&state, dataBase64Flag)
|
|
}
|
|
|
|
// ... add all remaining fields as extensions.
|
|
if dataschema != nil {
|
|
extensions["dataschema"] = dataschema.GetInterface()
|
|
}
|
|
if dataBase64 != nil {
|
|
extensions["data_base64"] = dataBase64.GetInterface()
|
|
}
|
|
|
|
out.Context = con
|
|
appendFlag(&state, specVersionV03Flag)
|
|
default:
|
|
return ValidationError{"specversion": errors.New("unknown value: " + sv)}
|
|
}
|
|
|
|
// Apply all extensions to the context object.
|
|
for key, val := range extensions {
|
|
if err := out.Context.SetExtension(key, val); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// If no specversion ...
|
|
if !checkFlag(state, specVersionV03Flag|specVersionV1Flag) {
|
|
switch key {
|
|
case "id":
|
|
id = iterator.ReadString()
|
|
case "type":
|
|
typ = iterator.ReadString()
|
|
case "source":
|
|
source = readUriRef(iterator)
|
|
case "subject":
|
|
subject = readStrPtr(iterator)
|
|
case "time":
|
|
time = readTimestamp(iterator)
|
|
case "datacontenttype":
|
|
datacontenttype = readStrPtr(iterator)
|
|
appendFlag(&state, dataContentTypeFlag)
|
|
case "data":
|
|
cachedData = iterator.SkipAndReturnBytes()
|
|
case "data_base64":
|
|
dataBase64 = iterator.ReadAny()
|
|
case "dataschema":
|
|
dataschema = iterator.ReadAny()
|
|
case "schemaurl":
|
|
schemaurl = iterator.ReadAny()
|
|
case "datacontentencoding":
|
|
datacontentencoding = iterator.ReadAny()
|
|
default:
|
|
extensions[key] = iterator.Read()
|
|
}
|
|
continue
|
|
}
|
|
|
|
// From this point downward -> we can assume the event has a context pointer non nil
|
|
|
|
// If it's a datacontenttype, trigger state change
|
|
if key == "datacontenttype" {
|
|
if checkFlag(state, dataContentTypeFlag) {
|
|
return fmt.Errorf("datacontenttype was already provided")
|
|
}
|
|
|
|
dct := iterator.ReadString()
|
|
|
|
switch ctx := out.Context.(type) {
|
|
case *EventContextV03:
|
|
ctx.DataContentType = &dct
|
|
case *EventContextV1:
|
|
ctx.DataContentType = &dct
|
|
}
|
|
appendFlag(&state, dataContentTypeFlag)
|
|
continue
|
|
}
|
|
|
|
// If it's a datacontentencoding and it's v0.3, trigger state change
|
|
if checkFlag(state, specVersionV03Flag) && key == "datacontentencoding" {
|
|
if checkFlag(state, dataBase64Flag) {
|
|
return ValidationError{"datacontentencoding": errors.New("datacontentencoding was specified twice")}
|
|
}
|
|
|
|
dce := iterator.ReadString()
|
|
|
|
if dce != Base64 {
|
|
return ValidationError{"datacontentencoding": errors.New("invalid datacontentencoding value, the only allowed value is 'base64'")}
|
|
}
|
|
|
|
out.Context.(*EventContextV03).DataContentEncoding = &dce
|
|
appendFlag(&state, dataBase64Flag)
|
|
continue
|
|
}
|
|
|
|
// We can parse all attributes, except data.
|
|
// If it's data or data_base64 and we don't have the attributes to process it, then we cache it
|
|
// The expanded form of this condition is:
|
|
// (checkFlag(state, specVersionV1Flag) && !checkFlag(state, dataContentTypeFlag) && (key == "data" || key == "data_base64")) ||
|
|
// (checkFlag(state, specVersionV03Flag) && !(checkFlag(state, dataContentTypeFlag) && checkFlag(state, dataBase64Flag)) && key == "data")
|
|
if (state&(specVersionV1Flag|dataContentTypeFlag) == specVersionV1Flag && (key == "data" || key == "data_base64")) ||
|
|
((state&specVersionV03Flag == specVersionV03Flag) && (state&(dataContentTypeFlag|dataBase64Flag) != (dataContentTypeFlag | dataBase64Flag)) && key == "data") {
|
|
if key == "data_base64" {
|
|
appendFlag(&state, dataBase64Flag)
|
|
}
|
|
cachedData = iterator.SkipAndReturnBytes()
|
|
continue
|
|
}
|
|
|
|
// At this point or this value is an attribute (excluding datacontenttype and datacontentencoding), or this value is data and this condition is valid:
|
|
// (specVersionV1Flag & dataContentTypeFlag) || (specVersionV03Flag & dataContentTypeFlag & dataBase64Flag)
|
|
switch eventContext := out.Context.(type) {
|
|
case *EventContextV03:
|
|
switch key {
|
|
case "id":
|
|
eventContext.ID = iterator.ReadString()
|
|
case "type":
|
|
eventContext.Type = iterator.ReadString()
|
|
case "source":
|
|
eventContext.Source = readUriRef(iterator)
|
|
case "subject":
|
|
eventContext.Subject = readStrPtr(iterator)
|
|
case "time":
|
|
eventContext.Time = readTimestamp(iterator)
|
|
case "schemaurl":
|
|
eventContext.SchemaURL = readUriRefPtr(iterator)
|
|
case "data":
|
|
iterator.Error = consumeData(out, checkFlag(state, dataBase64Flag), iterator)
|
|
default:
|
|
if eventContext.Extensions == nil {
|
|
eventContext.Extensions = make(map[string]interface{}, 1)
|
|
}
|
|
iterator.Error = eventContext.SetExtension(key, iterator.Read())
|
|
}
|
|
case *EventContextV1:
|
|
switch key {
|
|
case "id":
|
|
eventContext.ID = iterator.ReadString()
|
|
case "type":
|
|
eventContext.Type = iterator.ReadString()
|
|
case "source":
|
|
eventContext.Source = readUriRef(iterator)
|
|
case "subject":
|
|
eventContext.Subject = readStrPtr(iterator)
|
|
case "time":
|
|
eventContext.Time = readTimestamp(iterator)
|
|
case "dataschema":
|
|
eventContext.DataSchema = readUriPtr(iterator)
|
|
case "data":
|
|
iterator.Error = consumeData(out, false, iterator)
|
|
case "data_base64":
|
|
iterator.Error = consumeData(out, true, iterator)
|
|
default:
|
|
if eventContext.Extensions == nil {
|
|
eventContext.Extensions = make(map[string]interface{}, 1)
|
|
}
|
|
iterator.Error = eventContext.SetExtension(key, iterator.Read())
|
|
}
|
|
}
|
|
}
|
|
|
|
if state&(specVersionV03Flag|specVersionV1Flag) == 0 {
|
|
return ValidationError{"specversion": errors.New("no specversion")}
|
|
}
|
|
|
|
if iterator.Error != nil {
|
|
return iterator.Error
|
|
}
|
|
|
|
// If there is a dataToken cached, we always defer at the end the processing
|
|
// because nor datacontenttype or datacontentencoding are mandatory.
|
|
if cachedData != nil {
|
|
return consumeDataAsBytes(out, checkFlag(state, dataBase64Flag), cachedData)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func consumeDataAsBytes(e *Event, isBase64 bool, b []byte) error {
|
|
if isBase64 {
|
|
e.DataBase64 = true
|
|
|
|
// Allocate payload byte buffer
|
|
base64Encoded := b[1 : len(b)-1] // remove quotes
|
|
e.DataEncoded = make([]byte, base64.StdEncoding.DecodedLen(len(base64Encoded)))
|
|
length, err := base64.StdEncoding.Decode(e.DataEncoded, base64Encoded)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.DataEncoded = e.DataEncoded[0:length]
|
|
return nil
|
|
}
|
|
|
|
mt, _ := e.Context.GetDataMediaType()
|
|
// Empty content type assumes json
|
|
if mt != "" && mt != ApplicationJSON && mt != TextJSON {
|
|
// If not json, then data is encoded as string
|
|
iter := jsoniter.ParseBytes(jsoniter.ConfigFastest, b)
|
|
src := iter.ReadString() // handles escaping
|
|
e.DataEncoded = []byte(src)
|
|
if iter.Error != nil {
|
|
return fmt.Errorf("unexpected data payload for media type %q, expected a string: %w", mt, iter.Error)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
e.DataEncoded = b
|
|
return nil
|
|
}
|
|
|
|
func consumeData(e *Event, isBase64 bool, iter *jsoniter.Iterator) error {
|
|
if isBase64 {
|
|
e.DataBase64 = true
|
|
|
|
// Allocate payload byte buffer
|
|
base64Encoded := iter.ReadStringAsSlice()
|
|
e.DataEncoded = make([]byte, base64.StdEncoding.DecodedLen(len(base64Encoded)))
|
|
length, err := base64.StdEncoding.Decode(e.DataEncoded, base64Encoded)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.DataEncoded = e.DataEncoded[0:length]
|
|
return nil
|
|
}
|
|
|
|
mt, _ := e.Context.GetDataMediaType()
|
|
if mt != ApplicationJSON && mt != TextJSON {
|
|
// If not json, then data is encoded as string
|
|
src := iter.ReadString() // handles escaping
|
|
e.DataEncoded = []byte(src)
|
|
if iter.Error != nil {
|
|
return fmt.Errorf("unexpected data payload for media type %q, expected a string: %w", mt, iter.Error)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
e.DataEncoded = iter.SkipAndReturnBytes()
|
|
return nil
|
|
}
|
|
|
|
func readUriRef(iter *jsoniter.Iterator) types.URIRef {
|
|
str := iter.ReadString()
|
|
uriRef := types.ParseURIRef(str)
|
|
if uriRef == nil {
|
|
iter.Error = fmt.Errorf("cannot parse uri ref: %v", str)
|
|
return types.URIRef{}
|
|
}
|
|
return *uriRef
|
|
}
|
|
|
|
func readStrPtr(iter *jsoniter.Iterator) *string {
|
|
str := iter.ReadString()
|
|
if str == "" {
|
|
return nil
|
|
}
|
|
return &str
|
|
}
|
|
|
|
func readUriRefPtr(iter *jsoniter.Iterator) *types.URIRef {
|
|
return types.ParseURIRef(iter.ReadString())
|
|
}
|
|
|
|
func readUriPtr(iter *jsoniter.Iterator) *types.URI {
|
|
return types.ParseURI(iter.ReadString())
|
|
}
|
|
|
|
func readTimestamp(iter *jsoniter.Iterator) *types.Timestamp {
|
|
t, err := types.ParseTimestamp(iter.ReadString())
|
|
if err != nil {
|
|
iter.Error = err
|
|
}
|
|
return t
|
|
}
|
|
|
|
func toStrPtr(val jsoniter.Any) (*string, error) {
|
|
str := val.ToString()
|
|
if val.LastError() != nil {
|
|
return nil, val.LastError()
|
|
}
|
|
if str == "" {
|
|
return nil, nil
|
|
}
|
|
return &str, nil
|
|
}
|
|
|
|
func toUriRefPtr(val jsoniter.Any) (*types.URIRef, error) {
|
|
str := val.ToString()
|
|
if val.LastError() != nil {
|
|
return nil, val.LastError()
|
|
}
|
|
return types.ParseURIRef(str), nil
|
|
}
|
|
|
|
func toUriPtr(val jsoniter.Any) (*types.URI, error) {
|
|
str := val.ToString()
|
|
if val.LastError() != nil {
|
|
return nil, val.LastError()
|
|
}
|
|
return types.ParseURI(str), nil
|
|
}
|
|
|
|
// UnmarshalJSON implements the json unmarshal method used when this type is
|
|
// unmarshaled using json.Unmarshal.
|
|
func (e *Event) UnmarshalJSON(b []byte) error {
|
|
iterator := jsoniter.ConfigFastest.BorrowIterator(b)
|
|
defer jsoniter.ConfigFastest.ReturnIterator(iterator)
|
|
return readJsonFromIterator(e, iterator)
|
|
}
|