go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/model/utils.go (about)

     1  // Copyright 2023 The LUCI Authors.
     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 model
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"io"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/klauspost/compress/zlib"
    27  
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  
    31  	apipb "go.chromium.org/luci/swarming/proto/api_v2"
    32  )
    33  
    34  // checkIsHex returns an error if the string doesn't look like a lowercase hex
    35  // string.
    36  func checkIsHex(s string, minLen int) error {
    37  	if len(s) < minLen {
    38  		return errors.New("too small")
    39  	}
    40  	for _, c := range s {
    41  		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
    42  			return errors.Reason("bad lowercase hex string %q, wrong char %c", s, c).Err()
    43  		}
    44  	}
    45  	return nil
    46  }
    47  
    48  // ToJSONProperty serializes a value into a JSON blob property.
    49  //
    50  // Empty maps and lists are stored as nulls.
    51  func ToJSONProperty(val any) (datastore.Property, error) {
    52  	if val == nil {
    53  		return datastore.MkPropertyNI(nil), nil
    54  	}
    55  	blob, err := json.Marshal(val)
    56  	if bytes.Equal(blob, []byte("{}")) || bytes.Equal(blob, []byte("[]")) {
    57  		return datastore.MkPropertyNI(nil), nil
    58  	}
    59  	return datastore.MkPropertyNI(string(blob)), err
    60  }
    61  
    62  // FromJSONProperty deserializes a JSON blob property into `val`.
    63  //
    64  // If the property is missing, `val` will be unchanged. Assumes `val` is a list
    65  // or a dict.
    66  //
    67  // Recognizes zlib-compressed properties for compatibility with older entities.
    68  func FromJSONProperty(prop datastore.Property, val any) error {
    69  	propVal, err := prop.Project(datastore.PTBytes)
    70  	if err != nil {
    71  		return err
    72  	}
    73  	blob, _ := propVal.([]byte)
    74  	if len(blob) == 0 {
    75  		return nil
    76  	}
    77  
    78  	// This seems to be an uncompressed JSON. Load it as is. Note that zlib
    79  	// compressed data always starts with 0x78 byte (part of the zlib header).
    80  	if blob[0] == '{' || blob[0] == '[' {
    81  		return json.Unmarshal(blob, val)
    82  	}
    83  
    84  	// If this doesn't look like JSON, this is likely an older zlib-compressed
    85  	// property. Try to uncompress and load it.
    86  	r, err := zlib.NewReader(bytes.NewBuffer(blob))
    87  	if err != nil {
    88  		return err
    89  	}
    90  	w := bytes.NewBuffer(nil)
    91  	if _, err := io.Copy(w, r); err != nil {
    92  		_ = r.Close()
    93  		return err
    94  	}
    95  	if err := r.Close(); err != nil {
    96  		return err
    97  	}
    98  	return json.Unmarshal(w.Bytes(), val)
    99  }
   100  
   101  // LegacyProperty is a placeholder for "recognizing" known legacy properties.
   102  //
   103  // Properties of this type are silently discarded when read (and consequently
   104  // not stored back when written). This is useful for dropping properties that
   105  // were known to exist at some point, but which are no longer used by anything
   106  // at all. If we just ignore them completely, they'll end up in `Extra` maps,
   107  // which we want to avoid (`Extra` is only for truly unexpected properties).
   108  type LegacyProperty struct{}
   109  
   110  var _ datastore.PropertyConverter = &LegacyProperty{}
   111  
   112  // FromProperty implements datastore.PropertyConverter.
   113  func (*LegacyProperty) FromProperty(p datastore.Property) error {
   114  	return nil
   115  }
   116  
   117  // ToProperty implements datastore.PropertyConverter.
   118  func (*LegacyProperty) ToProperty() (datastore.Property, error) {
   119  	return datastore.Property{}, datastore.ErrSkipProperty
   120  }
   121  
   122  // SortStringPairs sorts string pairs.
   123  // This was stolen from go.chromium.org/luci/buildbucket/protoutil/tag.go
   124  // and should probably be moved to go.chromium.org/luci/common, but that would
   125  // require a larger refactor, hence the following:
   126  // TODO (crbug.com/1508908): remove this once refactored.
   127  func SortStringPairs(pairs []*apipb.StringPair) {
   128  	sort.Slice(pairs, func(i, j int) bool {
   129  		switch {
   130  		case pairs[i].Key < pairs[j].Key:
   131  			return true
   132  		case pairs[i].Key > pairs[j].Key:
   133  			return false
   134  		default:
   135  			return pairs[i].Value < pairs[j].Value
   136  		}
   137  	})
   138  }
   139  
   140  // dimensionsFlatToPb converts a list of k:v pairs into []*apipb.StringListPair.
   141  func dimensionsFlatToPb(flat []string) []*apipb.StringListPair {
   142  	// In the vast majority of cases `flat` is already sorted and we can skip
   143  	// unnecessary maps and resorting. Start with the assumption it is sorted and
   144  	// fallback to a generic implementation if we notice a violation.
   145  	var out []*apipb.StringListPair
   146  	for _, kv := range flat {
   147  		k, v, _ := strings.Cut(kv, ":")
   148  		if len(out) == 0 {
   149  			out = append(out, &apipb.StringListPair{
   150  				Key:   k,
   151  				Value: []string{v},
   152  			})
   153  			continue
   154  		}
   155  		switch prev := out[len(out)-1]; {
   156  		case k == prev.Key:
   157  			switch prevV := prev.Value[len(prev.Value)-1]; {
   158  			case v == prevV:
   159  				// Skip the duplicate.
   160  			case v > prevV:
   161  				prev.Value = append(prev.Value, v)
   162  			default: // v < prevV => the `flat` is not sorted in ascending order
   163  				return dimensionsFlatToPbSlow(flat)
   164  			}
   165  		case k > prev.Key:
   166  			out = append(out, &apipb.StringListPair{
   167  				Key:   k,
   168  				Value: []string{v},
   169  			})
   170  		default: // i.e. k < prev.Key => the `flat` is not sorted in ascending order
   171  			return dimensionsFlatToPbSlow(flat)
   172  		}
   173  	}
   174  	return out
   175  }
   176  
   177  // dimensionsFlatToPbSlow is the same as dimensionsFlatToPb, but it doesn't rely
   178  // on `flat` being presorted.
   179  func dimensionsFlatToPbSlow(flat []string) []*apipb.StringListPair {
   180  	sortedCopy := append(make([]string, 0, len(flat)), flat...)
   181  	sort.Strings(sortedCopy)
   182  	return dimensionsFlatToPb(sortedCopy)
   183  }
   184  
   185  // MapToStringListPair converts a map[string][]string to []*apipb.StringListPair.
   186  // If keySorting, sorting is applied to the keys.
   187  func MapToStringListPair(p map[string][]string, keySorting bool) []*apipb.StringListPair {
   188  	if len(p) == 0 {
   189  		return nil
   190  	}
   191  	keys := make([]string, 0, len(p))
   192  	for k := range p {
   193  		keys = append(keys, k)
   194  	}
   195  	if keySorting {
   196  		sort.Strings(keys)
   197  	}
   198  	slp := make([]*apipb.StringListPair, len(keys))
   199  	for i, key := range keys {
   200  		slp[i] = &apipb.StringListPair{
   201  			Key:   key,
   202  			Value: p[key],
   203  		}
   204  	}
   205  	return slp
   206  }
   207  
   208  // PutMockTaskOutput is a testing util that will create mock TaskOutputChunk datastore entities.
   209  func PutMockTaskOutput(ctx context.Context, reqKey *datastore.Key, numChunks int) {
   210  	toPut := make([]*TaskOutputChunk, numChunks)
   211  	for i := 0; i < numChunks; i++ {
   212  		expectedStr := strings.Repeat(strconv.Itoa(i), ChunkSize)
   213  		var b bytes.Buffer
   214  		w := zlib.NewWriter(&b)
   215  		_, err := w.Write([]byte(expectedStr))
   216  		if err != nil {
   217  			panic(err)
   218  		}
   219  		err = w.Close()
   220  		if err != nil {
   221  			panic(err)
   222  		}
   223  		compressedChunk := b.Bytes()
   224  		toPut[i] = &TaskOutputChunk{
   225  			Key:   TaskOutputChunkKey(ctx, reqKey, int64(i)),
   226  			Chunk: compressedChunk,
   227  		}
   228  	}
   229  	err := datastore.Put(ctx, toPut)
   230  	if err != nil {
   231  		panic(err)
   232  	}
   233  }