github.com/btwiuse/jiri@v0.0.0-20191125065820-53353bcfef54/metadata/metadata.go (about) 1 // Copyright 2015 The Vanadium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package metadata implements a mechanism for setting and retrieving metadata 6 // stored in program binaries. 7 // 8 // Metadata is a flat mapping of unique string identifiers to their associated 9 // string values. Both the ids and the values should be human-readable strings, 10 // since many uses of metadata involve textual output for humans; e.g. dumping 11 // metadata from the program on the command-line, or prepending metadata to log 12 // files. 13 // 14 // The ids must be unique, and avoiding collisions is up to the users of this 15 // package; it is recommended to prefix your identifiers with your project name. 16 // 17 // There are typically two sources for metadata, both supported by this package: 18 // 19 // 1) External metadata gathered by a build tool, e.g. the build timestamp. 20 // 21 // 2) Internal metadata compiled into the program, e.g. version numbers. 22 // 23 // External metadata is injected into the program binary by the linker. The Go 24 // ld linker provides a -X option that may be used for this purpose; see LDFlag 25 // for more details. 26 // 27 // Internal metadata is already compiled into the program binary, but the 28 // metadata package must still be made aware of it. Call the Insert function in 29 // an init function to accomplish this: 30 // 31 // package mypkg 32 // import "github.com/btwiuse/jiri/metadata" 33 // 34 // func init() { 35 // metadata.Insert("myproject.myid", "value") 36 // } 37 // 38 // The built-in metadata comes pre-populated with the Go architecture, operating 39 // system and version. 40 // 41 // This package registers a flag -metadata via an init function. Setting 42 // -metadata on the command-line causes the program to dump metadata in the 43 // XML format and exit. 44 package metadata 45 46 import ( 47 "bytes" 48 "compress/zlib" 49 "encoding/base64" 50 "encoding/xml" 51 "flag" 52 "fmt" 53 "io" 54 "os" 55 "reflect" 56 "runtime" 57 "sort" 58 "strings" 59 ) 60 61 // T represents the metadata for a program binary. 62 type T struct { 63 entries map[string]string 64 } 65 66 // String returns a human-readable representation; the same as ToXML. 67 func (x *T) String() string { 68 return x.ToXML() 69 } 70 71 // Insert sets the metadata entry for id to value, and returns the previous 72 // value. Whitespace is trimmed from either end of the value. 73 func (x *T) Insert(id, value string) string { 74 if x.entries == nil { 75 x.entries = make(map[string]string) 76 } 77 old := x.entries[id] 78 x.entries[id] = strings.TrimSpace(value) 79 return old 80 } 81 82 // Lookup retrieves the value for the given id from x. 83 func (x *T) Lookup(id string) string { 84 return x.entries[id] 85 } 86 87 // FromMap returns new metadata initialized with the given entries. Calls 88 // Insert on each element of entries. 89 func FromMap(entries map[string]string) *T { 90 x := new(T) 91 for id, value := range entries { 92 x.Insert(id, value) 93 } 94 return x 95 } 96 97 // ToMap returns a copy of the entries in x. Mutating the returned map has no 98 // effect on x. 99 func (x *T) ToMap() map[string]string { 100 if len(x.entries) == 0 { 101 return nil 102 } 103 ret := make(map[string]string, len(x.entries)) 104 for id, value := range x.entries { 105 ret[id] = value 106 } 107 return ret 108 } 109 110 type xmlMetaData struct { 111 XMLName struct{} `xml:"metadata"` 112 Entries []xmlEntry `xml:"md"` 113 } 114 115 type xmlEntry struct { 116 ID string `xml:"id,attr"` 117 // When marshalling only one of Value or ValueCDATA is set; Value is normally 118 // used, and ValueCDATA is used to add explicit "<![CDATA[...]]>" wrapping. 119 // 120 // When unmarshalling Value is set to the unescaped data, while ValueCDATA is 121 // set to the raw XML. 122 Value string `xml:",chardata"` 123 ValueCDATA string `xml:",innerxml"` 124 } 125 126 // FromXML returns new metadata initialized with the given XML encoded data. 127 // The expected schema is described in ToXML. 128 func FromXML(data []byte) (*T, error) { 129 x := new(T) 130 if len(data) == 0 { 131 return x, nil 132 } 133 var xmlData xmlMetaData 134 if err := xml.Unmarshal(data, &xmlData); err != nil { 135 return nil, err 136 } 137 for _, entry := range xmlData.Entries { 138 x.Insert(entry.ID, entry.Value) 139 } 140 return x, nil 141 } 142 143 // ToXML returns the XML encoding of x, using the schema described below. 144 // 145 // <metadata> 146 // <md id="A">a value</md> 147 // <md id="B"><![CDATA[ 148 // foo 149 // bar 150 // ]]></md> 151 // <md id="C">c value</md> 152 // </metadata> 153 func (x *T) ToXML() string { 154 return x.toXML(true) 155 } 156 157 func (x *T) toXML(indent bool) string { 158 // Write each XML <md> entry ordered by id. 159 var ids []string 160 for id, _ := range x.entries { 161 ids = append(ids, id) 162 } 163 sort.Strings(ids) 164 var data xmlMetaData 165 for _, id := range ids { 166 entry := xmlEntry{ID: id} 167 value := x.entries[id] 168 if xmlUseCDATASection(value) { 169 entry.ValueCDATA = cdataStart + "\n" + value + "\n " + cdataEnd 170 } else { 171 entry.Value = value 172 } 173 data.Entries = append(data.Entries, entry) 174 } 175 var dataXML []byte 176 if indent { 177 dataXML, _ = xml.MarshalIndent(data, "", " ") 178 } else { 179 dataXML, _ = xml.Marshal(data) 180 } 181 return string(dataXML) 182 } 183 184 const ( 185 cdataStart = "<![CDATA[" 186 cdataEnd = "]]>" 187 ) 188 189 func xmlUseCDATASection(value string) bool { 190 // Cannot use CDATA if "]]>" appears since that's the CDATA terminator. 191 if strings.Contains(value, cdataEnd) { 192 return false 193 } 194 // The choice at this point is a heuristic; it only determines how "pretty" 195 // the output looks. 196 b := []byte(value) 197 var buf bytes.Buffer 198 xml.EscapeText(&buf, b) 199 return !bytes.Equal(buf.Bytes(), b) 200 } 201 202 // FromBase64 returns new metadata initialized with the given base64 encoded 203 // data. The data is expected to have started as a valid XML representation of 204 // metadata, then zlib compressed, and finally base64 encoded. 205 func FromBase64(data []byte) (*T, error) { 206 if len(data) == 0 { 207 return new(T), nil 208 } 209 dataXML := make([]byte, base64.StdEncoding.DecodedLen(len(data))) 210 n, err := base64.StdEncoding.Decode(dataXML, data) 211 if err != nil { 212 return nil, err 213 } 214 var b bytes.Buffer 215 r, err := zlib.NewReader(bytes.NewReader(dataXML[:n])) 216 if err != nil { 217 return nil, err 218 } 219 _, errCopy := io.Copy(&b, r) 220 errClose := r.Close() 221 switch { 222 case errCopy != nil: 223 return nil, err 224 case errClose != nil: 225 return nil, err 226 } 227 return FromXML(b.Bytes()) 228 } 229 230 // ToBase64 returns the base64 encoding of x. First x is XML encoded, then zlib 231 // compressed, and finally base64 encoded. 232 func (x *T) ToBase64() string { 233 var b bytes.Buffer 234 w := zlib.NewWriter(&b) 235 w.Write([]byte(x.toXML(false))) 236 w.Close() 237 return base64.StdEncoding.EncodeToString(b.Bytes()) 238 } 239 240 var thisPkgPath = reflect.TypeOf(T{}).PkgPath() 241 242 // LDFlag returns the flag to pass to the Go ld linker to initialize the 243 // built-in metadata with x. Calls LDFlagExternal with the appropriate package 244 // path and unexported variable name to initialize BuiltIn. 245 func LDFlag(x *T) string { 246 return LDFlagExternal(thisPkgPath, "initBuiltIn", x) 247 } 248 249 // LDFlagExternal returns the flag to pass to the Go ld linker to initialize the 250 // string variable defined in pkgpath to the base64 encoding of x. See the 251 // documentation of the -X option at https://golang.org/cmd/ld 252 // 253 // The base64 encoding is used to avoid quoting and escaping issues when passing 254 // the flag through the go toolchain. An example of using the result to install 255 // a Go binary with metadata x: 256 // 257 // LDFlagExternal("main", "myvar", x) == "-X main.myvar=eJwBAAD//wAAAAE=" 258 // 259 // $ go install -ldflags="-X main.myvar=eJwBAAD//wAAAAE=" mypackage 260 func LDFlagExternal(pkgpath, variable string, x *T) string { 261 return fmt.Sprintf("-X %s.%s=%s", pkgpath, variable, x.ToBase64()) 262 } 263 264 // Insert sets the built-in metadata entry for id to value, and returns the 265 // previous value. Whitespace is trimmed from either end of the value. 266 // 267 // The built-in metadata is initialized by the Go ld linker. See the LDFlag 268 // function for more details. 269 func Insert(id, value string) string { return BuiltIn.Insert(id, value) } 270 271 // Lookup retrieves the value for the given id from the built-in metadata. 272 func Lookup(id string) string { return BuiltIn.Lookup(id) } 273 274 // ToBase64 returns the base64 encoding of the built-in metadata. First the 275 // metadata is XML encoded, then zlib compressed, and finally base64 encoded. 276 func ToBase64() string { return BuiltIn.ToBase64() } 277 278 // ToXML returns the XML encoding of the built-in metadata. The schema is 279 // defined in T.ToXML. 280 func ToXML() string { return BuiltIn.ToXML() } 281 282 // ToMap returns a copy of the entries in the built-in metadata. Mutating the 283 // returned map has no effect on the built-in metadata. 284 func ToMap() map[string]string { return BuiltIn.ToMap() } 285 286 // BuiltIn represents the metadata built-in to the Go program. The top-level 287 // functions such as Insert, ToBase64, and so on are wrappers for the methods of 288 // BuiltIn. 289 var BuiltIn T 290 291 // initBuiltIn is expected to be initialized by the Go ld linker. 292 var initBuiltIn string 293 294 func init() { 295 // First initialize the BuiltIn metadata based on linker-injected metadata. 296 if x, err := FromBase64([]byte(initBuiltIn)); err != nil { 297 // Don't panic, since a binary without metadata is more useful than a binary 298 // that always panics with invalid metadata. 299 fmt.Fprintf(os.Stderr, ` 300 metadata: built-in initialization failed (%v) from base64 data: %v 301 `, err, initBuiltIn) 302 } else { 303 BuiltIn = *x 304 } 305 // Now set values from the runtime. These may not be overridden by the 306 // linker-injected metadata, and should not be overridden by user packages. 307 BuiltIn.Insert("go.Arch", runtime.GOARCH) 308 BuiltIn.Insert("go.OS", runtime.GOOS) 309 BuiltIn.Insert("go.Version", runtime.Version()) 310 311 flag.Var(metadataFlag{}, "metadata", "Displays metadata for the program and exits.") 312 } 313 314 // metadataFlag implements a flag that dumps the default metadata and exits the 315 // program when it is set. 316 type metadataFlag struct{} 317 318 func (metadataFlag) IsBoolFlag() bool { return true } 319 func (metadataFlag) String() string { return "<just specify -metadata to activate>" } 320 func (metadataFlag) Set(string) error { 321 fmt.Println(BuiltIn.String()) 322 os.Exit(0) 323 return nil 324 }