1// SPDX-License-Identifier: MIT
2
3package action
4
5import (
6 "fmt"
7 "strings"
8 "unicode"
9
10 "bnema.dev/ermite-actions/expr"
11 "bnema.dev/ermite-actions/template"
12)
13
14// asciiEqualFold is String.Equals(a, b, OrdinalIgnoreCase) when b is an ASCII
15// literal ("docker", "default"): .NET's simple case mapping folds no non-ASCII
16// character into an ASCII one, so only A-Z and a-z can match. It is wrong for
17// two arbitrary strings ("é" and "É" are equal under OrdinalIgnoreCase); use
18// ordinalEqual for those.
19func asciiEqualFold(a, b string) bool {
20 if len(a) != len(b) {
21 return false
22 }
23 for i := 0; i < len(a); i++ {
24 x, y := a[i], b[i]
25 if x == y {
26 continue
27 }
28 if x >= 'a' && x <= 'z' {
29 x -= 'a' - 'A'
30 }
31 if y >= 'a' && y <= 'z' {
32 y -= 'a' - 'A'
33 }
34 if x != y {
35 return false
36 }
37 }
38 return true
39}
40
41// utf16Len is String.Length.
42func utf16Len(s string) int {
43 n := 0
44 for _, r := range s {
45 if r >= 0x10000 {
46 n += 2
47 } else {
48 n++
49 }
50 }
51 return n
52}
53
54// isNullOrWhiteSpace is String.IsNullOrWhiteSpace.
55func isNullOrWhiteSpace(s string) bool { return strings.TrimFunc(s, unicode.IsSpace) == "" }
56
57// The .NET exception classes the runner's loader raises.
58const (
59 exArgument = "System.ArgumentException"
60 exArgumentNull = "System.ArgumentNullException"
61 exArgumentRng = "System.ArgumentOutOfRangeException"
62 exNotSupported = "System.NotSupportedException"
63 exNullRef = "System.NullReferenceException"
64 exInvalidOp = "System.InvalidOperationException"
65 exTemplate = "GitHub.DistributedTask.ObjectTemplating.TemplateValidationException"
66)
67
68// thrown stands for a .NET exception: the loader panics with it where the
69// runner throws, and Parse records it where the runner catches it.
70type thrown struct{ class, msg string }
71
72func throwf(class, format string, a ...any) { panic(thrown{class, fmt.Sprintf(format, a...)}) }
73
74// typeName is the CLR type name of a token, "" for null, as in the messages
75// of the runner's Assert methods.
76func typeName(t template.Token) string {
77 if t == nil {
78 return ""
79 }
80 return t.Kind().String()
81}
82
83func assertFail(t template.Token, desc, want string) {
84 throwf(exArgument, "Unexpected type '%s' encountered while reading '%s'. The type '%s' was expected.", typeName(t), desc, want)
85}
86
87func assertString(t template.Token, desc string) *template.String {
88 if s, ok := t.(*template.String); ok {
89 return s
90 }
91 assertFail(t, desc, "StringToken")
92 return nil
93}
94
95func assertMapping(t template.Token, desc string) *template.Mapping {
96 if m, ok := t.(*template.Mapping); ok {
97 return m
98 }
99 assertFail(t, desc, "MappingToken")
100 return nil
101}
102
103func assertSequence(t template.Token, desc string) *template.Sequence {
104 if s, ok := t.(*template.Sequence); ok {
105 return s
106 }
107 assertFail(t, desc, "SequenceToken")
108 return nil
109}
110
111func assertBoolean(t template.Token, desc string) *template.Boolean {
112 if b, ok := t.(*template.Boolean); ok {
113 return b
114 }
115 assertFail(t, desc, "BooleanToken")
116 return nil
117}
118
119func assertScalar(t template.Token, desc string) template.Token {
120 if t != nil && template.IsScalar(t) {
121 return t
122 }
123 assertFail(t, desc, "ScalarToken")
124 return nil
125}
126
127// trimSpaceJS is String.prototype.trim.
128func trimSpaceJS(s string) string {
129 return strings.TrimFunc(s, func(r rune) bool { return unicode.IsSpace(r) || r == '\ufeff' })
130}
131
132// ordinalEqual is String.Equals(a, b, StringComparison.OrdinalIgnoreCase).
133func ordinalEqual(a, b string) bool { return expr.OrdinalFold(a) == expr.OrdinalFold(b) }