github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bundle/manifest.go (about)

     1  /*
     2   * Copyright (c) 2018-2020 vChain, Inc. All Rights Reserved.
     3   * This software is released under GPL3.
     4   * The full license information can be found under:
     5   * https://www.gnu.org/licenses/gpl-3.0.en.html
     6   *
     7   */
     8  
     9  package bundle
    10  
    11  import (
    12  	"encoding/json"
    13  	"fmt"
    14  	"io/ioutil"
    15  	"sort"
    16  
    17  	// See https://github.com/opencontainers/go-digest#usage
    18  	_ "crypto/sha256"
    19  	_ "crypto/sha512"
    20  
    21  	digest "github.com/opencontainers/go-digest"
    22  )
    23  
    24  const (
    25  	// ManifestSchemaVersion is the current manifest schema version.
    26  	ManifestSchemaVersion = 1
    27  
    28  	// ManifestFilename is the default filename for manifest when stored.
    29  	ManifestFilename = ".vcn.manifest.json"
    30  
    31  	// ManifestDigestAlgo is the only supported digest's algorithm by current manifest schema version.
    32  	ManifestDigestAlgo = digest.SHA256
    33  )
    34  
    35  // Manifest provides bundle structure when marshalled to JSON.
    36  //
    37  // Specifications (version 1):
    38  //  - `schemaVersion` is the version number of the current specification (MUST be always 1 in this case)
    39  //  - fields order is defined as per code structs definitions, orderning MUST NOT be changed
    40  //  - `items` MUST be sorted by its digest's value (lexically byte-wise)
    41  //  - multiple `items` MUST NOT have the same digest value
    42  //  - `items.paths` MUST be sorted by value (lexically byte-wise)
    43  //  - across the same manifest multiple `items.paths`'s elements MUST NOT have the value
    44  //  - json representation of the manifest MUST NOT be indented
    45  //  - sha256 is the only digest's algorithm that MUST be used
    46  //
    47  // The Normalize() method provides sorting funcionality and specification enforcement. It's implictly called
    48  // when the manifest is marshalled.
    49  type Manifest struct {
    50  	// SchemaVersion is the manifest schema that this bundle follows
    51  	SchemaVersion uint `json:"schemaVersion"`
    52  
    53  	// Items is an ordered list of items referenced by the manifest.
    54  	Items []Descriptor `json:"items"`
    55  }
    56  
    57  // MarshalJSON implements the json.Marshaler interface.
    58  func (m *Manifest) MarshalJSON() ([]byte, error) {
    59  	if err := m.Normalize(); err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	type alias Manifest
    64  	mm := alias(*m)
    65  	return json.Marshal(mm)
    66  }
    67  
    68  // Normalize deduplicates and sorts items and items's paths in accordance with manifest's schema specs.
    69  // An error is returned when duplicate paths across different items are found, or if digest's algo
    70  // does not match sha256.
    71  func (m *Manifest) Normalize() error {
    72  	if m == nil {
    73  		return fmt.Errorf("nil manifest")
    74  	}
    75  
    76  	if m.SchemaVersion != ManifestSchemaVersion {
    77  		return fmt.Errorf("unsupported bundle.Manifest schema version: %d", m.SchemaVersion)
    78  	}
    79  
    80  	// make unique index
    81  	idx := make(map[string]Descriptor, len(m.Items))
    82  	for _, d := range m.Items {
    83  		k := d.Digest.String()
    84  		if dd, ok := idx[k]; ok {
    85  			if d.Size != dd.Size {
    86  				return fmt.Errorf(
    87  					"distinct sizes found for same digest (%s): %d, %d",
    88  					d.Digest.String(),
    89  					d.Size,
    90  					dd.Size,
    91  				)
    92  			}
    93  			dd.Paths = append(dd.Paths, d.Paths...)
    94  			idx[k] = dd
    95  		} else {
    96  			idx[k] = d
    97  		}
    98  	}
    99  
   100  	// recreate unique digest list and sort paths
   101  	m.Items = make([]Descriptor, len(idx))
   102  	paths := make(map[string]bool)
   103  	i := 0
   104  	for _, d := range idx {
   105  		d.sortUnique()
   106  		m.Items[i] = d
   107  		i++
   108  
   109  		// specs enforcement:
   110  		// - the only allowed digest's algo is SHA256
   111  		// - within the same manifest multiple paths elements with same value are NOT allowed
   112  		if algo := d.Digest.Algorithm(); algo != ManifestDigestAlgo {
   113  			return fmt.Errorf("unsupported digest algorithm: %s", string(algo))
   114  		}
   115  		for _, p := range d.Paths {
   116  			if paths[p] {
   117  				return fmt.Errorf("duplicate path in manifest: %s", p)
   118  			}
   119  			paths[p] = true
   120  		}
   121  	}
   122  
   123  	// finally, sort items by digest
   124  	sort.SliceStable(m.Items, func(k, j int) bool {
   125  		return m.Items[k].Digest.String() < m.Items[j].Digest.String()
   126  	})
   127  	return nil
   128  }
   129  
   130  // Digest digests the JSON encoded m and returns a digest.Digest.
   131  func (m *Manifest) Digest() (digest.Digest, error) {
   132  	b, err := json.Marshal(m) // sorting is implicitly called by Marshal
   133  	if err != nil {
   134  		return "", err
   135  	}
   136  
   137  	return digest.SHA256.FromBytes(b), nil
   138  }
   139  
   140  // NewManifest returns a new empty Manifest.
   141  func NewManifest(items ...Descriptor) *Manifest {
   142  	if items == nil {
   143  		items = make([]Descriptor, 0)
   144  	}
   145  	return &Manifest{
   146  		SchemaVersion: ManifestSchemaVersion,
   147  		Items:         items,
   148  	}
   149  }
   150  
   151  // WriteManifest writes manifest's data to a file named by filename.
   152  func WriteManifest(manifest Manifest, filename string) error {
   153  	data, err := json.Marshal(&manifest)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	return ioutil.WriteFile(filename, data, 0644)
   158  }
   159  
   160  // ReadManifest reads the file named by filename and returns the decoded manifest.
   161  func ReadManifest(filename string) (*Manifest, error) {
   162  	data, err := ioutil.ReadFile(filename)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	d := digest.SHA256.FromBytes(data)
   168  
   169  	m := Manifest{}
   170  	json.Unmarshal(data, &m)
   171  
   172  	dd, err := m.Digest()
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	if dd != d {
   177  		return nil, fmt.Errorf("manifest integrity check failed")
   178  	}
   179  
   180  	return &m, nil
   181  }