// SPDX-License-Identifier: MIT package template import ( "fmt" "sort" "strings" "bnema.dev/ermite-actions/expr" ) // ReadYAML parses a YAML document and reads it as a template of the schema // definition rootType (for workflows, "workflow-root-strict"). Problems // found in the document, such as syntax errors, unexpected keys or // misplaced expressions, are recorded in ctx; the returned token holds what // could be read and is nil when the document is not valid YAML. // // An empty document is an error, as in the reference. A document larger than // [MaxFileBytes] is refused before it is parsed, with one error about the // file. func ReadYAML(ctx *Context, rootType, file string, content []byte) (root Token) { if len(content) > MaxFileBytes { e := &Error{File: file, fileOnly: true, Raw: fmt.Sprintf("The file is too large: %d bytes, the limit is %d bytes", len(content), MaxFileBytes)} ctx.add(e) return nil } res := readYAMLDocument(file, content, ctx.RunnerYAML) for _, e := range res.errs { ctx.add(e) } if len(res.errs) > 0 { return nil } defer func() { if r := recover(); r != nil { f, ok := r.(fatal) if !ok { panic(r) } ctx.ErrorInFile(file, f.msg) } }() rd := &reader{ctx: ctx, schema: ctx.schema, file: file} if res.root == nil { if ctx.RunnerYAML { // YamlDotNet produces no DocumentStart event for an empty stream. fatalf("Expected document start parse event") } fatalf("Expected a scalar value, a sequence, or a mapping") } def := newDefinitionInfo(ctx.schema, nil, ctx.schema.get(rootType)) root = rd.readValue(res.root, def) for _, e := range res.after { ctx.add(e) } return root } type reader struct { ctx *Context schema *Schema file string // noExpressions is set when reading the result of an evaluation, whose // strings are text even if they contain ${{. noExpressions bool // depth is the nesting of the collections being read, see Context.MaxDepth. depth int } // enter counts one more level of nesting. Like TemplateMemory.IncrementDepth // it fails when the levels already entered reach the limit, so a limit of 100 // allows 100 nested collections. func (r *reader) enter() { if r.ctx.MaxDepth <= 0 { return } if r.depth >= r.ctx.MaxDepth { fatalf("Maximum object depth exceeded") } r.depth++ } // text is the string a literal becomes when the schema wants a string, and // the text of a key that is not a string: Token.String, except that a number // is shown as the runner's NumberToken shows it in runner mode. func (r *reader) text(t Token) string { if n, ok := t.(*Number); ok && r.ctx.RunnerYAML { return DotNetNumberString(n.Value) } return t.String() } // foldKey is the form of a key under which duplicates are detected: the // reference upper-cases like JavaScript, the runner compares keys with // .NET's OrdinalIgnoreCase. func (r *reader) foldKey(s string) string { if r.ctx.RunnerYAML { return expr.OrdinalFold(s) } return strings.ToUpper(s) } func (r *reader) leave() { if r.ctx.MaxDepth > 0 { r.depth-- } } // skip walks a value that is not kept (the value of an unexpected key, the // items of a collection the schema does not allow) and counts its nesting, // as the reference's SkipValue does. func (r *reader) skip(n *rnode) { if n == nil { return } if n.fault != "" { fatalf("%s", n.fault) } if r.ctx.MaxDepth <= 0 { return } switch n.kind { case rSeq: r.enter() for _, it := range n.items { r.skip(it) } r.leave() case rMap: r.enter() for i := range n.keys { r.skip(n.keys[i]) r.skip(n.vals[i]) } r.leave() } } func (r *reader) origin(n *rnode) Origin { return Origin{File: r.file, Pos: n.pos} } func (r *reader) readValue(n *rnode, def *DefinitionInfo) Token { if n.fault != "" { fatalf("%s", n.fault) } switch n.kind { case rLiteral: scalar := r.parseScalar(n, n.lit, def) return r.validate(scalar, def) case rSeq: r.enter() seq := NewSequence(r.origin(n)) defs := def.schema.definitionsOfType(def.Definition, DefSequence) if len(defs) > 0 { itemDef := def.child(defs[0].ItemType) for _, it := range n.items { seq.Items = append(seq.Items, r.readValue(it, itemDef)) } } else { r.ctx.ErrorAt(seq, "A sequence was not expected") for _, it := range n.items { r.skip(it) } } seq.def = def r.leave() return seq } r.enter() m := NewMapping(r.origin(n)) defs := def.schema.definitionsOfType(def.Definition, DefMapping) if len(defs) > 0 { if len(defs) > 1 || len(defs[0].Properties) > 0 || defs[0].LooseKeyType == "" { r.mappingWithProperties(n, m, def, defs) } else { keyDef := def.child(defs[0].LooseKeyType) valDef := def.child(defs[0].LooseValueType) r.mappingAllLoose(n, m, def, keyDef, valDef, defs[0]) } } else { r.ctx.ErrorAt(m, "A mapping was not expected") for i := range n.keys { r.skip(n.keys[i]) r.skip(n.vals[i]) } } if m.def == nil { m.def = def } r.leave() return m } // key reads the key of a mapping entry; a key that is not a scalar is an // error and reads as an empty string. func (r *reader) key(n *rnode, i int, def *DefinitionInfo) Token { k := n.keys[i] if k.fault != "" { fatalf("%s", k.fault) } if k.badKey { if r.ctx.RunnerYAML { // The runner's loop over the keys ends at the first one that is not // a literal, and then expects the end of the mapping. fatalf("Expected mapping end") } r.ctx.ErrorAt(NewString(r.origin(k), ""), "Unexpected value ''") } return r.parseScalar(k, k.lit, def) } func (r *reader) mappingWithProperties(n *rnode, m *Mapping, def *DefinitionInfo, defs []*Definition) { var looseKey, looseVal string if defs[0].LooseKeyType != "" { looseKey, looseVal = defs[0].LooseKeyType, defs[0].LooseValueType } var looseKeyDef, looseValDef *DefinitionInfo upperKeys := map[string]bool{} hasExpressionKey := false for i := range n.keys { keyScalar := r.key(n, i, def) val := n.vals[i] if IsExpression(keyScalar) { hasExpressionKey = true if len(def.allowed) > 0 { m.Add(keyScalar, r.readValue(val, def.child("any"))) } else { r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context") r.skip(val) } continue } nextKey, ok := keyScalar.(*String) if !ok { nextKey = &String{tokenBase: *keyScalar.base(), Value: r.text(keyScalar)} } if nextKey.Value != "" { up := r.foldKey(nextKey.Value) if upperKeys[up] { r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value) r.skip(val) continue } upperKeys[up] = true } var prop *Property prop, defs = matchPropertyAndFilter(defs, nextKey.Value) if prop != nil { if backgroundStepProperties[nextKey.Value] { r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value) r.skip(val) continue } nd := def.child(prop.Type) nextKey.def = nd m.Add(nextKey, r.readValue(val, nd)) continue } if looseKey != "" { if looseKeyDef == nil { looseKeyDef = def.child(looseKey) looseValDef = def.child(looseVal) } r.validate(nextKey, looseKeyDef) nextKey.def = def.child(defs[0].LooseValueType) m.Add(nextKey, r.readValue(val, looseValDef)) continue } r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value) r.skip(val) } if len(defs) == 1 { m.def = newDefinitionInfo(def.schema, def, defs[0]) } switch { case len(defs) > 1: hits := map[string]int{} for _, d := range defs { for _, k := range d.PropertyNames { hits[k]++ } } var props []string for k, c := range hits { if c == 1 && !backgroundStepOnlyProperties[k] { props = append(props, k) } } sort.Strings(props) r.ctx.Errorf(m, "There's not enough info to determine what you meant. Add one of these properties: %s", strings.Join(props, ", ")) case len(defs) == 1 && !hasExpressionKey: for _, name := range defs[0].PropertyNames { if defs[0].Properties[name].Required && !upperKeys[r.foldKey(name)] { r.ctx.Errorf(m, "Required property is missing: %s", name) } } } } var ( backgroundStepProperties = map[string]bool{"background": true, "wait": true, "wait-all": true, "cancel": true} backgroundStepOnlyProperties = map[string]bool{"wait": true, "wait-all": true, "cancel": true} ) func (r *reader) mappingAllLoose(n *rnode, m *Mapping, def, keyDef, valDef *DefinitionInfo, md *Definition) { upperKeys := map[string]bool{} for i := range n.keys { keyScalar := r.key(n, i, def) keyScalar.base().def = keyDef val := n.vals[i] if IsExpression(keyScalar) { if len(def.allowed) > 0 { m.Add(keyScalar, r.readValue(val, valDef)) } else { r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context") r.skip(val) } continue } nextKey, ok := keyScalar.(*String) if !ok { nextKey = &String{tokenBase: *keyScalar.base(), Value: r.text(keyScalar)} } if nextKey.Value != "" { up := r.foldKey(nextKey.Value) if upperKeys[up] { r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value) r.skip(val) continue } upperKeys[up] = true } r.validate(nextKey, keyDef) nextKey.def = def.child(md.LooseValueType) m.Add(nextKey, r.readValue(val, valDef)) } } // validate checks a scalar against the scalar definitions of def. A literal // that is not a string may be converted to one. func (r *reader) validate(scalar Token, def *DefinitionInfo) Token { switch scalar.Kind() { case KindNull, KindBoolean, KindNumber, KindString: for _, d := range def.schema.scalarDefinitions(def.Definition) { if d.isMatch(scalar) { scalar.base().def = newDefinitionInfo(def.schema, def, d) return scalar } } if scalar.Kind() != KindString { sl := &String{tokenBase: *scalar.base(), Value: r.text(scalar)} for _, d := range def.schema.scalarDefinitions(def.Definition) { if d.isMatch(sl) { sl.def = newDefinitionInfo(def.schema, def, d) return sl } } } r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", r.text(scalar))) case KindBasicExpression: if len(def.allowed) == 0 { r.ctx.ErrorAt(scalar, "A template expression is not allowed in this context") } default: r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", scalar.String())) } return scalar }