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  }