blob: 8d8652526ddcdb24657ebf2fa35f4d4471d0fc31 [file] [log] [blame]
// Copyright 2019 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cue
import (
"bytes"
"compress/gzip"
"encoding/gob"
"path/filepath"
"strings"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/ast/astutil"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
"cuelang.org/go/internal/core/export"
)
// root.
type instanceData struct {
Root bool
Path string
Files []fileData
}
type fileData struct {
Name string
Data []byte
}
const version = 1
type unmarshaller struct {
ctxt *build.Context
imports map[string]*instanceData
}
func (b *unmarshaller) load(pos token.Pos, path string) *build.Instance {
bi := b.imports[path]
if bi == nil {
return nil
}
return b.build(bi)
}
func (b *unmarshaller) build(bi *instanceData) *build.Instance {
p := b.ctxt.NewInstance(bi.Path, b.load)
p.ImportPath = bi.Path
for _, f := range bi.Files {
_ = p.AddFile(f.Name, f.Data)
}
p.Complete()
return p
}
func compileInstances(r *Runtime, data []*instanceData) (instances []*Instance, err error) {
b := unmarshaller{
ctxt: build.NewContext(),
imports: map[string]*instanceData{},
}
for _, i := range data {
if i.Path == "" {
if !i.Root {
return nil, errors.Newf(token.NoPos,
"data contains non-root package without import path")
}
continue
}
b.imports[i.Path] = i
}
builds := []*build.Instance{}
for _, i := range data {
if !i.Root {
continue
}
builds = append(builds, b.build(i))
}
return r.build(builds)
}
// Unmarshal creates an Instance from bytes generated by the MarshalBinary
// method of an instance.
func (r *Runtime) Unmarshal(b []byte) ([]*Instance, error) {
if len(b) == 0 {
return nil, errors.Newf(token.NoPos, "unmarshal failed: empty buffer")
}
switch b[0] {
case version:
default:
return nil, errors.Newf(token.NoPos,
"unmarshal failed: unsupported version %d, regenerate data", b[0])
}
reader, err := gzip.NewReader(bytes.NewReader(b[1:]))
if err != nil {
return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err)
}
data := []*instanceData{}
err = gob.NewDecoder(reader).Decode(&data)
if err != nil {
return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err)
}
return compileInstances(r, data)
}
// Marshal creates bytes from a group of instances. Imported instances will
// be included in the emission.
//
// The stored instances are functionally the same, but preserving of file
// information is only done on a best-effort basis.
func (r *Runtime) Marshal(instances ...*Instance) (b []byte, err error) {
ctx := r.index().newContext()
staged := []instanceData{}
done := map[string]int{}
var errs errors.Error
var stageInstance func(i *Instance) (pos int)
stageInstance = func(i *Instance) (pos int) {
if p, ok := done[i.ImportPath]; ok {
return p
}
// TODO: support exporting instance
file, _ := export.Def(r.idx.Runtime, i.inst.ID(), i.root)
imports := []string{}
file.VisitImports(func(i *ast.ImportDecl) {
for _, spec := range i.Specs {
info, _ := astutil.ParseImportSpec(spec)
imports = append(imports, info.ID)
}
})
if i.PkgName != "" {
p, name, _ := internal.PackageInfo(file)
if p == nil {
pkg := &ast.Package{Name: ast.NewIdent(i.PkgName)}
file.Decls = append([]ast.Decl{pkg}, file.Decls...)
} else if name != i.PkgName {
// p is guaranteed to be generated by Def, so it is "safe" to
// modify.
p.Name = ast.NewIdent(i.PkgName)
}
}
b, err := format.Node(file)
errs = errors.Append(errs, errors.Promote(err, "marshal"))
filename := "unmarshal"
if i.inst != nil && len(i.inst.Files) == 1 {
filename = i.inst.Files[0].Filename
dir := i.Dir
if i.inst != nil && i.inst.Root != "" {
dir = i.inst.Root
}
if dir != "" {
filename = filepath.FromSlash(filename)
filename, _ = filepath.Rel(dir, filename)
filename = filepath.ToSlash(filename)
}
}
// TODO: this should probably be changed upstream, but as the path
// is for reference purposes only, this is safe.
importPath := filepath.ToSlash(i.ImportPath)
staged = append(staged, instanceData{
Path: importPath,
Files: []fileData{{filename, b}},
})
p := len(staged) - 1
for _, imp := range imports {
i := ctx.getImportFromPath(imp)
if i == nil || !strings.Contains(imp, ".") {
continue // a builtin package.
}
stageInstance(i)
}
return p
}
for _, i := range instances {
staged[stageInstance(i)].Root = true
}
buf := &bytes.Buffer{}
buf.WriteByte(version)
zw := gzip.NewWriter(buf)
if err := gob.NewEncoder(zw).Encode(staged); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}