cuelang.org/go@v0.13.0/cue/marshal.go (about) 1 // Copyright 2019 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cue 16 17 import ( 18 "bytes" 19 "compress/gzip" 20 "encoding/gob" 21 "path/filepath" 22 "strings" 23 24 "cuelang.org/go/cue/ast" 25 "cuelang.org/go/cue/ast/astutil" 26 "cuelang.org/go/cue/build" 27 "cuelang.org/go/cue/errors" 28 "cuelang.org/go/cue/format" 29 "cuelang.org/go/cue/token" 30 "cuelang.org/go/internal" 31 "cuelang.org/go/internal/core/export" 32 ) 33 34 // root. 35 type instanceData struct { 36 Root bool 37 Path string 38 Files []fileData 39 } 40 41 type fileData struct { 42 Name string 43 Data []byte 44 } 45 46 const version = 1 47 48 type unmarshaller struct { 49 ctxt *build.Context 50 imports map[string]*instanceData 51 } 52 53 func (b *unmarshaller) load(pos token.Pos, path string) *build.Instance { 54 bi := b.imports[path] 55 if bi == nil { 56 return nil 57 } 58 return b.build(bi) 59 } 60 61 func (b *unmarshaller) build(bi *instanceData) *build.Instance { 62 p := b.ctxt.NewInstance(bi.Path, nil) 63 p.ImportPath = bi.Path 64 for _, f := range bi.Files { 65 _ = p.AddFile(f.Name, f.Data) 66 } 67 p.Complete() 68 return p 69 } 70 71 func compileInstances(r *Runtime, data []*instanceData) (instances []*Instance, err error) { 72 b := unmarshaller{ 73 imports: map[string]*instanceData{}, 74 } 75 b.ctxt = build.NewContext(build.Loader(b.load)) 76 for _, i := range data { 77 if i.Path == "" { 78 if !i.Root { 79 return nil, errors.Newf(token.NoPos, 80 "data contains non-root package without import path") 81 } 82 continue 83 } 84 b.imports[i.Path] = i 85 } 86 87 builds := []*build.Instance{} 88 for _, i := range data { 89 if !i.Root { 90 continue 91 } 92 builds = append(builds, b.build(i)) 93 } 94 95 return r.BuildInstances(builds) 96 } 97 98 // Unmarshal returns a slice of instances from bytes generated by 99 // [Runtime.Marshal]. 100 func (r *Runtime) Unmarshal(b []byte) ([]*Instance, error) { 101 if len(b) == 0 { 102 return nil, errors.Newf(token.NoPos, "unmarshal failed: empty buffer") 103 } 104 105 switch b[0] { 106 case version: 107 default: 108 return nil, errors.Newf(token.NoPos, 109 "unmarshal failed: unsupported version %d, regenerate data", b[0]) 110 } 111 112 reader, err := gzip.NewReader(bytes.NewReader(b[1:])) 113 if err != nil { 114 return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err) 115 } 116 117 data := []*instanceData{} 118 err = gob.NewDecoder(reader).Decode(&data) 119 if err != nil { 120 return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err) 121 } 122 123 return compileInstances(r, data) 124 } 125 126 // Marshal creates bytes from a group of instances. Imported instances will 127 // be included in the emission. 128 // 129 // The stored instances are functionally the same, but preserving of file 130 // information is only done on a best-effort basis. 131 func (r *Runtime) Marshal(values ...InstanceOrValue) (b []byte, err error) { 132 staged := []instanceData{} 133 done := map[string]int{} 134 135 var errs errors.Error 136 137 var stageInstance func(i Value) (pos int) 138 stageInstance = func(i Value) (pos int) { 139 inst := i.BuildInstance() 140 if p, ok := done[inst.ImportPath]; ok { 141 return p 142 } 143 // TODO: support exporting instance 144 file, _ := export.Def(r.runtime(), inst.ID(), i.instance().root) 145 imports := []string{} 146 file.VisitImports(func(i *ast.ImportDecl) { 147 for _, spec := range i.Specs { 148 info, _ := astutil.ParseImportSpec(spec) 149 imports = append(imports, info.ID) 150 } 151 }) 152 153 if inst.PkgName != "" { 154 if pkg := internal.Package(file); pkg == nil { 155 pkg := &ast.Package{Name: ast.NewIdent(inst.PkgName)} 156 file.Decls = append([]ast.Decl{pkg}, file.Decls...) 157 } else if pkg.Name.Name != inst.PkgName { 158 // pi is guaranteed to be generated by Def, so it is "safe" to modify. 159 pkg.Name = ast.NewIdent(inst.PkgName) 160 } 161 } 162 163 b, err := format.Node(file) 164 errs = errors.Append(errs, errors.Promote(err, "marshal")) 165 166 filename := "unmarshal" 167 if len(inst.Files) == 1 { 168 filename = inst.Files[0].Filename 169 170 dir := inst.Dir 171 if inst.Root != "" { 172 dir = inst.Root 173 } 174 if dir != "" { 175 filename = filepath.FromSlash(filename) 176 filename, _ = filepath.Rel(dir, filename) 177 filename = filepath.ToSlash(filename) 178 } 179 } 180 // TODO: this should probably be changed upstream, but as the path 181 // is for reference purposes only, this is safe. 182 importPath := filepath.ToSlash(i.instance().ImportPath) 183 184 staged = append(staged, instanceData{ 185 Path: importPath, 186 Files: []fileData{{filename, b}}, 187 }) 188 189 p := len(staged) - 1 190 191 for _, imp := range imports { 192 i := getImportFromPath(r.runtime(), imp) 193 if i == nil || !strings.Contains(imp, ".") { 194 continue // a builtin package. 195 } 196 stageInstance(i.Value()) 197 } 198 199 return p 200 } 201 202 for _, val := range values { 203 staged[stageInstance(val.Value())].Root = true 204 } 205 206 buf := &bytes.Buffer{} 207 buf.WriteByte(version) 208 209 zw := gzip.NewWriter(buf) 210 if err := gob.NewEncoder(zw).Encode(staged); err != nil { 211 return nil, err 212 } 213 214 if err := zw.Close(); err != nil { 215 return nil, err 216 } 217 218 return buf.Bytes(), nil 219 220 }