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 }