go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/text/templateproto/normalize.go (about) 1 // Copyright 2016 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 templateproto 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "io" 22 "reflect" 23 "regexp" 24 "strings" 25 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/common/errors" 28 ) 29 30 // Normalize will normalize all of the Templates in this message, returning an 31 // error if any are invalid. 32 func (f *File) Normalize() error { 33 me := errors.MultiError(nil) 34 for tname, t := range f.Template { 35 if err := t.Normalize(); err != nil { 36 me = append(me, fmt.Errorf("for template %q: %s", tname, err)) 37 } 38 } 39 if len(me) > 0 { 40 return me 41 } 42 return nil 43 } 44 45 // ParamRegex is the regular expression that all parameter names must match. 46 var ParamRegex = regexp.MustCompile(`^\${[^}]+}$`) 47 48 // Normalize will normalize the Template message, returning an error if it is 49 // invalid. 50 func (t *File_Template) Normalize() error { 51 if t.Body == "" { 52 return errors.New("body is empty") 53 } 54 55 defaultParams := make(map[string]*Value, len(t.Param)) 56 for k, param := range t.Param { 57 if k == "" { 58 return fmt.Errorf("param %q: invalid name", k) 59 } 60 if !ParamRegex.MatchString(k) { 61 return fmt.Errorf("param %q: malformed name", k) 62 } 63 if !strings.Contains(t.Body, k) { 64 return fmt.Errorf("param %q: not present in body", k) 65 } 66 if err := param.Normalize(); err != nil { 67 return fmt.Errorf("param %q: %s", k, err) 68 } 69 if param.Default != nil { 70 defaultParams[k] = param.Default 71 } else { 72 defaultParams[k] = param.Schema.Zero() 73 } 74 } 75 76 maybeJSON, err := t.Render(defaultParams) 77 if err != nil { 78 return fmt.Errorf("rendering: %s", err) 79 } 80 81 err = json.Unmarshal([]byte(maybeJSON), &map[string]any{}) 82 if err != nil { 83 return fmt.Errorf("parsing rendered body: %s", err) 84 } 85 return nil 86 } 87 88 // Normalize will normalize the Parameter, returning an error if it is invalid. 89 func (p *File_Template_Parameter) Normalize() error { 90 if p == nil { 91 return errors.New("is nil") 92 } 93 if err := p.Schema.Normalize(); err != nil { 94 return fmt.Errorf("schema: %s", err) 95 } 96 if p.Default != nil { 97 if err := p.Default.Normalize(); err != nil { 98 return fmt.Errorf("default value: %s", err) 99 } 100 if err := p.Accepts(p.Default); err != nil { 101 return fmt.Errorf("default value: %s", err) 102 } 103 } 104 return nil 105 } 106 107 // Accepts returns nil if this Parameter can accept the Value. 108 func (p *File_Template_Parameter) Accepts(v *Value) error { 109 if v.IsNull() { 110 if !p.Nullable { 111 return errors.New("not nullable") 112 } 113 } else if err := p.Schema.Accepts(v); err != nil { 114 return err 115 } else if err := v.Check(p.Schema); err != nil { 116 return err 117 } 118 return nil 119 } 120 121 // Normalize will normalize the Schema, returning an error if it is invalid. 122 func (s *Schema) Normalize() error { 123 if s == nil { 124 return errors.New("is nil") 125 } 126 if s.Schema == nil { 127 return errors.New("has no type") 128 } 129 if enum := s.GetEnum(); enum != nil { 130 return enum.Normalize() 131 } 132 return nil 133 } 134 135 // Normalize will normalize the Schema_Set, returning an error if it is 136 // invalid. 137 func (s *Schema_Set) Normalize() error { 138 if len(s.Entry) == 0 { 139 return errors.New("set requires entries") 140 } 141 set := stringset.New(len(s.Entry)) 142 for _, entry := range s.Entry { 143 if entry.Token == "" { 144 return errors.New("blank token") 145 } 146 if !set.Add(entry.Token) { 147 return fmt.Errorf("duplicate token %q", entry.Token) 148 } 149 } 150 return nil 151 } 152 153 // Has returns true iff the given token is a valid value for this enumeration. 154 func (s *Schema_Set) Has(token string) bool { 155 for _, tok := range s.Entry { 156 if tok.Token == token { 157 return true 158 } 159 } 160 return false 161 } 162 163 // IsNull returns true if this Value is the null value. 164 func (v *Value) IsNull() bool { 165 _, ret := v.Value.(*Value_Null) 166 return ret 167 } 168 169 // Check ensures that this value conforms to the given schema. 170 func (v *Value) Check(s *Schema) error { 171 check, needsCheck := v.Value.(interface { 172 Check(*Schema) error 173 }) 174 if !needsCheck { 175 return nil 176 } 177 return check.Check(s) 178 } 179 180 // Normalize returns a non-nil error if the Value is invalid for its nominal type. 181 func (v *Value) Normalize() error { 182 norm, needsNormalization := v.Value.(interface { 183 Normalize() error 184 }) 185 if !needsNormalization { 186 return nil 187 } 188 return norm.Normalize() 189 } 190 191 // Check returns nil iff this Value meets the max length criteria. 192 func (v *Value_Bytes) Check(schema *Schema) error { 193 s := schema.GetBytes() 194 if s.MaxLength > 0 && uint32(len(v.Bytes)) > s.MaxLength { 195 return fmt.Errorf("value is too large: %d > %d", len(v.Bytes), s.MaxLength) 196 } 197 return nil 198 } 199 200 // Check returns nil iff this Value meets the max length criteria, and/or 201 // can be used to fill an enumeration value from the provided schema. 202 func (v *Value_Str) Check(schema *Schema) error { 203 switch s := schema.Schema.(type) { 204 case *Schema_Str: 205 maxLen := s.Str.MaxLength 206 if maxLen > 0 && uint32(len(v.Str)) > maxLen { 207 return fmt.Errorf("value is too large: %d > %d", len(v.Str), maxLen) 208 } 209 210 case *Schema_Enum: 211 if !s.Enum.Has(v.Str) { 212 return fmt.Errorf("value does not match enum: %q", v.Str) 213 } 214 215 default: 216 panic(fmt.Errorf("for Value_Str: unknown schema %T", s)) 217 } 218 219 return nil 220 } 221 222 // Check returns nil iff this Value correctly parses as a JSON object. 223 func (v *Value_Object) Check(schema *Schema) error { 224 s := schema.GetObject() 225 if s.MaxLength > 0 && uint32(len(v.Object)) > s.MaxLength { 226 return fmt.Errorf("value is too large: %d > %d", len(v.Object), s.MaxLength) 227 } 228 return nil 229 } 230 231 // Normalize returns nil iff this Value correctly parses as a JSON object. 232 func (v *Value_Object) Normalize() error { 233 newObj, err := NormalizeJSON(v.Object, true) 234 if err != nil { 235 return err 236 } 237 v.Object = newObj 238 return nil 239 } 240 241 // Check returns nil iff this Value correctly parses as a JSON array. 242 func (v *Value_Array) Check(schema *Schema) error { 243 s := schema.GetArray() 244 if s.MaxLength > 0 && uint32(len(v.Array)) > s.MaxLength { 245 return fmt.Errorf("value is too large: %d > %d", len(v.Array), s.MaxLength) 246 } 247 return nil 248 } 249 250 // Normalize returns nil iff this Value correctly parses as a JSON array. 251 func (v *Value_Array) Normalize() error { 252 newAry, err := NormalizeJSON(v.Array, false) 253 if err != nil { 254 return err 255 } 256 v.Array = newAry 257 return nil 258 } 259 260 var ( 261 typeOfSchemaStr = reflect.TypeOf((*Schema_Str)(nil)) 262 typeOfSchemaEnum = reflect.TypeOf((*Schema_Enum)(nil)) 263 ) 264 265 // Accepts returns nil if this Schema can accept the Value. 266 func (s *Schema) Accepts(v *Value) error { 267 typ := reflect.TypeOf(s.Schema) 268 if typ == typeOfSchemaEnum { 269 typ = typeOfSchemaStr 270 } 271 if typ != reflect.TypeOf(v.schemaType()) { 272 return fmt.Errorf("type is %q, expected %q", schemaTypeStr(v.schemaType()), schemaTypeStr(s.Schema)) 273 } 274 return nil 275 } 276 277 // Zero produces a Value from this schema which is a valid 'zero' value (in the 278 // go sense) 279 func (s *Schema) Zero() *Value { 280 switch sub := s.Schema.(type) { 281 case *Schema_Int: 282 return MustNewValue(0) 283 case *Schema_Uint: 284 return MustNewValue(uint(0)) 285 case *Schema_Float: 286 return MustNewValue(0.0) 287 case *Schema_Bool: 288 return MustNewValue(false) 289 case *Schema_Str: 290 return MustNewValue("") 291 case *Schema_Bytes: 292 return MustNewValue([]byte{}) 293 case *Schema_Enum: 294 return MustNewValue(sub.Enum.Entry[0].Token) 295 case *Schema_Object: 296 return &Value{Value: &Value_Object{"{}"}} 297 case *Schema_Array: 298 return &Value{Value: &Value_Array{"[]"}} 299 } 300 panic(fmt.Errorf("unknown schema type: %v", s)) 301 } 302 303 // NormalizeJSON is used to take some free-form JSON and validates that: 304 // - it only contains a valid JSON object (e.g. `{...stuff...}`); OR 305 // - it only contains a valid JSON array (e.g. `[...stuff...]`) 306 // 307 // If obj is true, this looks for an object, if it's false, it looks for an 308 // array. 309 // 310 // This will also remove all extra whitespace and sort all objects by key. 311 func NormalizeJSON(data string, obj bool) (string, error) { 312 buf := bytes.NewBufferString(data) 313 dec := json.NewDecoder(buf) 314 dec.UseNumber() 315 var decoded any 316 if obj { 317 decoded = &map[string]any{} 318 } else { 319 decoded = &[]any{} 320 } 321 err := dec.Decode(decoded) 322 if err != nil { 323 return "", err 324 } 325 bufdat, err := io.ReadAll(dec.Buffered()) 326 if err != nil { 327 panic(err) 328 } 329 rest := strings.TrimSpace(string(bufdat) + buf.String()) 330 if rest != "" { 331 return "", fmt.Errorf("got extra junk: %q", rest) 332 } 333 334 buf.Reset() 335 err = json.NewEncoder(buf).Encode(decoded) 336 337 // the TrimSpace chops off an extraneous newline that the json lib adds on. 338 return string(bytes.TrimSpace(buf.Bytes())), err 339 } 340 341 // Normalize will normalize this Specifier 342 func (s *Specifier) Normalize() error { 343 if s.TemplateName == "" { 344 return errors.New("empty template_name") 345 } 346 for k, v := range s.Params { 347 if k == "" { 348 return errors.New("empty param key") 349 } 350 if err := v.Normalize(); err != nil { 351 return fmt.Errorf("param %q: %s", k, err) 352 } 353 } 354 return nil 355 }