1 // Copyright 2019 The Kubernetes Authors. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package refvar 5 6 import ( 7 "fmt" 8 "log" 9 "strings" 10 ) 11 12 const ( 13 operator = '$' 14 referenceOpener = '(' 15 referenceCloser = ')' 16 ) 17 18 // syntaxWrap returns the input string wrapped by the expansion syntax. 19 func syntaxWrap(input string) string { 20 var sb strings.Builder 21 sb.WriteByte(operator) 22 sb.WriteByte(referenceOpener) 23 sb.WriteString(input) 24 sb.WriteByte(referenceCloser) 25 return sb.String() 26 } 27 28 // MappingFunc maps a string to anything. 29 type MappingFunc func(string) interface{} 30 31 // MakePrimitiveReplacer returns a MappingFunc that uses a map to do 32 // replacements, and a histogram to count map hits. 33 // 34 // Func behavior: 35 // 36 // If the input key is NOT found in the map, the key is wrapped up as 37 // as a variable declaration string and returned, e.g. key FOO becomes $(FOO). 38 // This string is presumably put back where it was found, and might get replaced 39 // later. 40 // 41 // If the key is found in the map, the value is returned if it is a primitive 42 // type (string, bool, number), and the hit is counted. 43 // 44 // If it's not a primitive type (e.g. a map, struct, func, etc.) then this 45 // function doesn't know what to do with it and it returns the key wrapped up 46 // again as if it had not been replaced. This should probably be an error. 47 func MakePrimitiveReplacer( 48 counts map[string]int, someMap map[string]interface{}) MappingFunc { 49 return func(key string) interface{} { 50 if value, ok := someMap[key]; ok { 51 switch typedV := value.(type) { 52 case string, int, int32, int64, float32, float64, bool: 53 counts[key]++ 54 return typedV 55 default: 56 // If the value is some complicated type (e.g. a map or struct), 57 // this function doesn't know how to jam it into a string, 58 // so just pretend it was a cache miss. 59 // Likely this should be an error instead of a silent failure, 60 // since the programmer passed an impossible value. 61 log.Printf( 62 "MakePrimitiveReplacer: bad replacement type=%T val=%v", 63 typedV, typedV) 64 return syntaxWrap(key) 65 } 66 } 67 // If unable to return the mapped variable, return it 68 // as it was found, and a later mapping might be able to 69 // replace it. 70 return syntaxWrap(key) 71 } 72 } 73 74 // DoReplacements replaces variable references in the input string 75 // using the mapping function. 76 func DoReplacements(input string, mapping MappingFunc) interface{} { 77 var buf strings.Builder 78 checkpoint := 0 79 for cursor := 0; cursor < len(input); cursor++ { 80 if input[cursor] == operator && cursor+1 < len(input) { 81 // Copy the portion of the input string since the last 82 // checkpoint into the buffer 83 buf.WriteString(input[checkpoint:cursor]) 84 85 // Attempt to read the variable name as defined by the 86 // syntax from the input string 87 read, isVar, advance := tryReadVariableName(input[cursor+1:]) 88 89 if isVar { 90 // We were able to read a variable name correctly; 91 // apply the mapping to the variable name and copy the 92 // bytes into the buffer 93 mapped := mapping(read) 94 if input == syntaxWrap(read) { 95 // Preserve the type of variable 96 return mapped 97 } 98 99 // Variable is used in a middle of a string 100 buf.WriteString(fmt.Sprintf("%v", mapped)) 101 } else { 102 // Not a variable name; copy the read bytes into the buffer 103 buf.WriteString(read) 104 } 105 106 // Advance the cursor in the input string to account for 107 // bytes consumed to read the variable name expression 108 cursor += advance 109 110 // Advance the checkpoint in the input string 111 checkpoint = cursor + 1 112 } 113 } 114 115 // Return the buffer and any remaining unwritten bytes in the 116 // input string. 117 return buf.String() + input[checkpoint:] 118 } 119 120 // tryReadVariableName attempts to read a variable name from the input 121 // string and returns the content read from the input, whether that content 122 // represents a variable name to perform mapping on, and the number of bytes 123 // consumed in the input string. 124 // 125 // The input string is assumed not to contain the initial operator. 126 func tryReadVariableName(input string) (string, bool, int) { 127 switch input[0] { 128 case operator: 129 // Escaped operator; return it. 130 return input[0:1], false, 1 131 case referenceOpener: 132 // Scan to expression closer 133 for i := 1; i < len(input); i++ { 134 if input[i] == referenceCloser { 135 return input[1:i], true, i + 1 136 } 137 } 138 139 // Incomplete reference; return it. 140 return string(operator) + string(referenceOpener), false, 1 141 default: 142 // Not the beginning of an expression, ie, an operator 143 // that doesn't begin an expression. Return the operator 144 // and the first rune in the string. 145 return string(operator) + string(input[0]), false, 1 146 } 147 } 148