new 'hclpack' package

This commit is contained in:
Martin Atkins 2018-11-11 09:23:11 -08:00
commit 12378af8b3
17 changed files with 1665 additions and 5 deletions

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/agext/levenshtein v1.2.1
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3
github.com/apparentlymart/go-textseg v1.0.0
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e
github.com/davecgh/go-spew v1.1.1
github.com/go-test/deep v1.0.1
github.com/google/go-cmp v0.2.0

2
go.sum
View File

@ -6,6 +6,8 @@ github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhi
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e h1:D64GF/Xr5zSUnM3q1Jylzo4sK7szhP/ON+nb2DB5XJA=
github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=

View File

@ -86,8 +86,8 @@ func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostic
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported attribute",
Detail: fmt.Sprintf("An attribute named %q is not expected here.%s", name, suggestion),
Summary: "Unsupported argument",
Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
Subject: &attr.NameRange,
})
}
@ -107,7 +107,7 @@ func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostic
// Is there an attribute of the same name?
for _, attrS := range schema.Attributes {
if attrS.Name == blockTy {
suggestion = fmt.Sprintf(" Did you mean to define attribute %q?", blockTy)
suggestion = fmt.Sprintf(" Did you mean to define argument %q? If so, use the equals sign to assign it a value.", blockTy)
break
}
}
@ -151,8 +151,8 @@ func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Bod
if attrS.Required {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required attribute",
Detail: fmt.Sprintf("The attribute %q is required, but no definition was found.", attrS.Name),
Summary: "Missing required argument",
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
Subject: b.MissingItemRange().Ptr(),
})
}

24
hclpack/didyoumean.go Normal file
View File

@ -0,0 +1,24 @@
package hclpack
import (
"github.com/agext/levenshtein"
)
// nameSuggestion tries to find a name from the given slice of suggested names
// that is close to the given name and returns it if found. If no suggestion
// is close enough, returns the empty string.
//
// The suggestions are tried in order, so earlier suggestions take precedence
// if the given string is similar to two or more suggestions.
//
// This function is intended to be used with a relatively-small number of
// suggestions. It's not optimized for hundreds or thousands of them.
func nameSuggestion(given string, suggestions []string) string {
for _, suggestion := range suggestions {
dist := levenshtein.Distance(given, suggestion, nil)
if dist < 3 { // threshold determined experimentally
return suggestion
}
}
return ""
}

14
hclpack/doc.go Normal file
View File

@ -0,0 +1,14 @@
// Package hclpack provides a straightforward representation of HCL block/body
// structure that can be easily serialized and deserialized for compact
// transmission (e.g. over a network) without transmitting the full source code.
//
// Expressions are retained in native syntax source form so that their
// evaluation can be delayed until a package structure is decoded by some
// other system that has enough information to populate the evaluation context.
//
// Packed structures retain source location information but do not retain
// actual source code. To make sense of source locations returned in diagnostics
// and via other APIs the caller must somehow gain access to the original source
// code that the packed representation was built from, which is a problem that
// must be solved somehow by the calling application.
package hclpack

129
hclpack/example_test.go Normal file
View File

@ -0,0 +1,129 @@
package hclpack_test
import (
"bytes"
"encoding/json"
"fmt"
"os"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclpack"
)
func Example_marshalJSON() {
src := `
service "example" {
priority = 2
platform {
os = "linux"
arch = "amd64"
}
process "web" {
exec = ["./webapp"]
}
process "worker" {
exec = ["./worker"]
}
}
`
body, diags := hclpack.PackNativeFile([]byte(src), "example.svc", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
fmt.Fprintf(os.Stderr, "Failed to parse: %s", diags.Error())
return
}
jb, err := body.MarshalJSON()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to marshal: %s", err)
return
}
// Normally the compact form is best, but we'll indent just for the sake
// of this example so the result is readable.
var buf bytes.Buffer
json.Indent(&buf, jb, "", " ")
os.Stdout.Write(buf.Bytes())
// Output:
// {
// "r": {
// "b": [
// {
// "h": [
// "service",
// "example"
// ],
// "b": {
// "a": {
// "priority": {
// "s": "2",
// "r": "ChAKDA4QDhA"
// }
// },
// "b": [
// {
// "h": [
// "platform"
// ],
// "b": {
// "a": {
// "arch": {
// "s": "\"amd64\"",
// "r": "IiwiJCYsKCo"
// },
// "os": {
// "s": "\"linux\"",
// "r": "FiAWGBogHB4"
// }
// },
// "r": "Li4"
// },
// "r": "EhQSFA"
// },
// {
// "h": [
// "process",
// "web"
// ],
// "b": {
// "a": {
// "exec": {
// "s": "[\"./webapp\"]",
// "r": "OEA4OjxAPD4"
// }
// },
// "r": "QkI"
// },
// "r": "MDYwMjQ2"
// },
// {
// "h": [
// "process",
// "worker"
// ],
// "b": {
// "a": {
// "exec": {
// "s": "[\"./worker\"]",
// "r": "TFRMTlBUUFI"
// }
// },
// "r": "VlY"
// },
// "r": "REpERkhK"
// }
// ],
// "r": "WFg"
// },
// "r": "AggCBAYI"
// }
// ],
// "r": "Wlo"
// },
// "s": [
// "example.svc"
// ],
// "p": "BAQEAA4OAAICABISAggMABAQAAYGAAICAggIABAQAgYKAAQEAAoKAAICAAoKAAICAgYGAAgIAAYGAAICAAoKAAICAgoKAggIAA4OAAICAAoKAgwQAAgIAAYGAAICABYWAgoKAggIAA4OAAICABAQAgwQAAgIAAYGAAICABYWAgoKAgYGAgQE"
// }
}

160
hclpack/expression.go Normal file
View File

@ -0,0 +1,160 @@
package hclpack
import (
"fmt"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
)
// Expression is an implementation of hcl.Expression in terms of some raw
// expression source code. The methods of this type will first parse the
// source code and then pass the call through to the real expression that
// is produced.
type Expression struct {
// Source is the raw source code of the expression, which should be parsed
// as the syntax specified by SourceType.
Source []byte
SourceType ExprSourceType
// Range_ and StartRange_ describe the physical extents of the expression
// in the original source code. SourceRange_ is its entire range while
// StartRange is just the tokens that introduce the expression type. For
// simple expression types, SourceRange and StartRange are identical.
Range_, StartRange_ hcl.Range
}
var _ hcl.Expression = (*Expression)(nil)
// Value implements the Value method of hcl.Expression but with the additional
// step of first parsing the expression source code. This implementation is
// unusual in that it can potentially return syntax errors, whereas other
// Value implementations usually work with already-parsed expressions.
func (e *Expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
expr, diags := e.Parse()
if diags.HasErrors() {
return cty.DynamicVal, diags
}
val, moreDiags := expr.Value(ctx)
diags = append(diags, moreDiags...)
return val, diags
}
// Variables implements the Variables method of hcl.Expression but with the
// additional step of first parsing the expression source code.
//
// Since this method cannot return errors, it will return a nil slice if
// parsing fails, indicating that no variables are present. This is okay in
// practice because a subsequent call to Value would fail with syntax errors
// regardless of what variables are in the context.
func (e *Expression) Variables() []hcl.Traversal {
expr, diags := e.Parse()
if diags.HasErrors() {
return nil
}
return expr.Variables()
}
// UnwrapExpression parses and returns the underlying expression, if possible.
//
// This is essentially the same as Parse but without the ability to return an
// error; it is here only to support the static analysis facilities in the
// main HCL package (ExprList, ExprMap, etc). If any error is encountered
// during parsing, the result is a static expression that always returns
// cty.DynamicVal.
//
// This function does not impose any further conversions on the underlying
// expression, so the result may still not be suitable for the static analysis
// functions, depending on the source type of the expression and thus what
// type of physical expression it becomes after decoding.
func (e *Expression) UnwrapExpression() hcl.Expression {
expr, diags := e.Parse()
if diags.HasErrors() {
return hcl.StaticExpr(cty.DynamicVal, e.Range_)
}
return expr
}
func (e *Expression) Range() hcl.Range {
return e.Range_
}
func (e *Expression) StartRange() hcl.Range {
return e.StartRange_
}
// Parse attempts to parse the source code of the receiving expression using
// its indicated source type, returning the expression if possible and any
// diagnostics produced during parsing.
func (e *Expression) Parse() (hcl.Expression, hcl.Diagnostics) {
switch e.SourceType {
case ExprNative:
return hclsyntax.ParseExpression(e.Source, e.Range_.Filename, e.Range_.Start)
case ExprTemplate:
return hclsyntax.ParseTemplate(e.Source, e.Range_.Filename, e.Range_.Start)
case ExprLiteralJSON:
ty, err := ctyjson.ImpliedType(e.Source)
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Invalid JSON value",
Detail: fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
Subject: &e.Range_,
},
}
}
val, err := ctyjson.Unmarshal(e.Source, ty)
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Invalid JSON value",
Detail: fmt.Sprintf("The JSON representation of this expression is invalid: %s.", err),
Subject: &e.Range_,
},
}
}
return hcl.StaticExpr(val, e.Range_), nil
default:
// This should never happen for a valid Expression.
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Invalid expression source type",
Detail: fmt.Sprintf("Packed version of this expression has an invalid source type %s. This is always a bug.", e.SourceType),
Subject: &e.Range_,
},
}
}
}
func (e *Expression) addRanges(rngs map[hcl.Range]struct{}) {
rngs[e.Range_] = struct{}{}
rngs[e.StartRange_] = struct{}{}
}
// ExprSourceType defines the syntax type used for an expression's source code,
// which is then used to select a suitable parser for it when evaluating.
type ExprSourceType rune
//go:generate stringer -type ExprSourceType
const (
// ExprNative indicates that an expression must be parsed as native
// expression syntax, with hclsyntax.ParseExpression.
ExprNative ExprSourceType = 'N'
// ExprTemplate indicates that an expression must be parsed as native
// template syntax, with hclsyntax.ParseTemplate.
ExprTemplate ExprSourceType = 'T'
// ExprLiteralJSON indicates that an expression must be parsed as JSON and
// treated literally, using cty/json. This can be used when populating
// literal attribute values from a non-HCL source.
ExprLiteralJSON ExprSourceType = 'L'
)

View File

@ -0,0 +1,70 @@
package hclpack
import (
"testing"
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
)
func TestExpressionValue(t *testing.T) {
tests := map[string]struct {
Expr *Expression
Ctx *hcl.EvalContext
Want cty.Value
}{
"simple literal expr": {
&Expression{
Source: []byte(`"hello"`),
SourceType: ExprNative,
},
nil,
cty.StringVal("hello"),
},
"simple literal template": {
&Expression{
Source: []byte(`hello ${5}`),
SourceType: ExprTemplate,
},
nil,
cty.StringVal("hello 5"),
},
"expr with variable": {
&Expression{
Source: []byte(`foo`),
SourceType: ExprNative,
},
&hcl.EvalContext{
Variables: map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
},
cty.StringVal("bar"),
},
"template with variable": {
&Expression{
Source: []byte(`foo ${foo}`),
SourceType: ExprTemplate,
},
&hcl.EvalContext{
Variables: map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
},
cty.StringVal("foo bar"),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, diags := test.Expr.Value(test.Ctx)
for _, diag := range diags {
t.Errorf("unexpected diagnostic: %s", diag.Error())
}
if !test.Want.RawEquals(got) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

View File

@ -0,0 +1,21 @@
// Code generated by "stringer -type ExprSourceType"; DO NOT EDIT.
package hclpack
import "strconv"
const (
_ExprSourceType_name_0 = "ExprNative"
_ExprSourceType_name_1 = "ExprTemplate"
)
func (i ExprSourceType) String() string {
switch {
case i == 78:
return _ExprSourceType_name_0
case i == 84:
return _ExprSourceType_name_1
default:
return "ExprSourceType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

211
hclpack/json_marshal.go Normal file
View File

@ -0,0 +1,211 @@
package hclpack
import (
"encoding/json"
"github.com/hashicorp/hcl2/hcl"
)
// MarshalJSON is an implementation of Marshaler from encoding/json, allowing
// bodies to be included in other types that are JSON-marshalable.
//
// The result of MarshalJSON is optimized for compactness rather than easy
// human consumption/editing. Use UnmarshalJSON to decode it.
func (b *Body) MarshalJSON() ([]byte, error) {
rngs := make(map[hcl.Range]struct{})
b.addRanges(rngs)
fns, posList, posMap := packPositions(rngs)
head := jsonHeader{
Body: b.forJSON(posMap),
Sources: fns,
Pos: posList,
}
return json.Marshal(&head)
}
func (b *Body) forJSON(pos map[string]map[hcl.Pos]posOfs) bodyJSON {
var ret bodyJSON
if len(b.Attributes) > 0 {
ret.Attrs = make(map[string]attrJSON, len(b.Attributes))
for name, attr := range b.Attributes {
ret.Attrs[name] = attr.forJSON(pos)
}
}
if len(b.ChildBlocks) > 0 {
ret.Blocks = make([]blockJSON, len(b.ChildBlocks))
for i, block := range b.ChildBlocks {
ret.Blocks[i] = block.forJSON(pos)
}
}
ret.Ranges = make(rangesPacked, 1)
ret.Ranges[0] = packRange(b.MissingItemRange_, pos)
return ret
}
func (a *Attribute) forJSON(pos map[string]map[hcl.Pos]posOfs) attrJSON {
var ret attrJSON
ret.Source = string(a.Expr.Source)
switch a.Expr.SourceType {
case ExprNative:
ret.Syntax = 0
case ExprTemplate:
ret.Syntax = 1
case ExprLiteralJSON:
ret.Syntax = 2
}
ret.Ranges = make(rangesPacked, 4)
ret.Ranges[0] = packRange(a.Range, pos)
ret.Ranges[1] = packRange(a.NameRange, pos)
ret.Ranges[2] = packRange(a.Expr.Range_, pos)
ret.Ranges[3] = packRange(a.Expr.StartRange_, pos)
return ret
}
func (b *Block) forJSON(pos map[string]map[hcl.Pos]posOfs) blockJSON {
var ret blockJSON
ret.Header = make([]string, len(b.Labels)+1)
ret.Header[0] = b.Type
copy(ret.Header[1:], b.Labels)
ret.Body = b.Body.forJSON(pos)
ret.Ranges = make(rangesPacked, 2+len(b.LabelRanges))
ret.Ranges[0] = packRange(b.DefRange, pos)
ret.Ranges[1] = packRange(b.TypeRange, pos)
for i, rng := range b.LabelRanges {
ret.Ranges[i+2] = packRange(rng, pos)
}
return ret
}
// UnmarshalJSON is an implementation of Unmarshaler from encoding/json,
// allowing bodies to be included in other types that are JSON-unmarshalable.
func (b *Body) UnmarshalJSON(data []byte) error {
var head jsonHeader
err := json.Unmarshal(data, &head)
if err != nil {
return err
}
fns := head.Sources
positions := head.Pos.Unpack()
*b = head.Body.decode(fns, positions)
return nil
}
type jsonHeader struct {
Body bodyJSON `json:"r"`
Sources []string `json:"s,omitempty"`
Pos positionsPacked `json:"p,omitempty"`
}
type bodyJSON struct {
// Files are the source filenames that were involved in
Attrs map[string]attrJSON `json:"a,omitempty"`
Blocks []blockJSON `json:"b,omitempty"`
// Ranges contains the MissingItemRange
Ranges rangesPacked `json:"r,omitempty"`
}
func (bj *bodyJSON) decode(fns []string, positions []position) Body {
var ret Body
if len(bj.Attrs) > 0 {
ret.Attributes = make(map[string]Attribute, len(bj.Attrs))
for name, aj := range bj.Attrs {
ret.Attributes[name] = aj.decode(fns, positions)
}
}
if len(bj.Blocks) > 0 {
ret.ChildBlocks = make([]Block, len(bj.Blocks))
for i, blj := range bj.Blocks {
ret.ChildBlocks[i] = blj.decode(fns, positions)
}
}
ret.MissingItemRange_ = bj.Ranges.UnpackIdx(fns, positions, 0)
return ret
}
type attrJSON struct {
// To keep things compact, in the JSON encoding we flatten the
// expression down into the attribute object, since overhead
// for attributes adds up in a complex config.
Source string `json:"s"`
Syntax int `json:"t,omitempty"` // omitted for 0=native
// Ranges contains the Range, NameRange, Expr.Range, Expr.StartRange
Ranges rangesPacked `json:"r,omitempty"`
}
func (aj *attrJSON) decode(fns []string, positions []position) Attribute {
var ret Attribute
ret.Expr.Source = []byte(aj.Source)
switch aj.Syntax {
case 0:
ret.Expr.SourceType = ExprNative
case 1:
ret.Expr.SourceType = ExprTemplate
case 2:
ret.Expr.SourceType = ExprLiteralJSON
}
ret.Range = aj.Ranges.UnpackIdx(fns, positions, 0)
ret.NameRange = aj.Ranges.UnpackIdx(fns, positions, 1)
ret.Expr.Range_ = aj.Ranges.UnpackIdx(fns, positions, 2)
ret.Expr.StartRange_ = aj.Ranges.UnpackIdx(fns, positions, 3)
if ret.Expr.StartRange_ == (hcl.Range{}) {
// If the start range wasn't present then we'll just use the Range
ret.Expr.StartRange_ = ret.Expr.Range_
}
return ret
}
type blockJSON struct {
// Header is the type followed by any labels. We flatten this here
// to keep the JSON encoding compact.
Header []string `json:"h"`
Body bodyJSON `json:"b,omitempty"`
// Ranges contains the DefRange followed by the TypeRange and then
// each of the label ranges in turn.
Ranges rangesPacked `json:"r,omitempty"`
}
func (blj *blockJSON) decode(fns []string, positions []position) Block {
var ret Block
if len(blj.Header) > 0 { // If the header is invalid then we'll end up with an empty type
ret.Type = blj.Header[0]
}
if len(blj.Header) > 1 {
ret.Labels = blj.Header[1:]
}
ret.Body = blj.Body.decode(fns, positions)
ret.DefRange = blj.Ranges.UnpackIdx(fns, positions, 0)
ret.TypeRange = blj.Ranges.UnpackIdx(fns, positions, 1)
if len(ret.Labels) > 0 {
ret.LabelRanges = make([]hcl.Range, len(ret.Labels))
for i := range ret.Labels {
ret.LabelRanges[i] = blj.Ranges.UnpackIdx(fns, positions, i+2)
}
}
return ret
}

View File

@ -0,0 +1,47 @@
package hclpack
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl2/hcl"
)
func TestJSONRoundTrip(t *testing.T) {
src := `
service "example" {
priority = 2
platform {
os = "linux"
arch = "amd64"
}
process "web" {
exec = ["./webapp"]
}
process "worker" {
exec = ["./worker"]
}
}
`
startBody, diags := PackNativeFile([]byte(src), "example.svc", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
t.Fatalf("Failed to parse: %s", diags.Error())
}
jb, err := startBody.MarshalJSON()
if err != nil {
t.Fatalf("Failed to marshal: %s", err)
}
endBody := &Body{}
err = endBody.UnmarshalJSON(jb)
if err != nil {
t.Fatalf("Failed to unmarshal: %s", err)
}
if !cmp.Equal(startBody, endBody) {
t.Errorf("incorrect result\n%s", cmp.Diff(startBody, endBody))
}
}

58
hclpack/pack_native.go Normal file
View File

@ -0,0 +1,58 @@
package hclpack
import (
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
)
// PackNativeFile parses the given source code as HCL native syntax and packs
// it into a hclpack Body ready to be marshalled.
//
// If the given source code contains syntax errors then error diagnostics will
// be returned. A non-nil body might still be returned in this case, which
// allows a cautious caller to still do certain analyses on the result.
func PackNativeFile(src []byte, filename string, start hcl.Pos) (*Body, hcl.Diagnostics) {
f, diags := hclsyntax.ParseConfig(src, filename, start)
rootBody := f.Body.(*hclsyntax.Body)
return packNativeBody(rootBody, src), diags
}
func packNativeBody(body *hclsyntax.Body, src []byte) *Body {
ret := &Body{}
for name, attr := range body.Attributes {
exprRng := attr.Expr.Range()
exprStartRng := attr.Expr.StartRange()
exprSrc := exprRng.SliceBytes(src)
ret.setAttribute(name, Attribute{
Expr: Expression{
Source: exprSrc,
SourceType: ExprNative,
Range_: exprRng,
StartRange_: exprStartRng,
},
Range: attr.Range(),
NameRange: attr.NameRange,
})
}
for _, block := range body.Blocks {
childBody := packNativeBody(block.Body, src)
defRange := block.TypeRange
if len(block.LabelRanges) > 0 {
defRange = hcl.RangeBetween(defRange, block.LabelRanges[len(block.LabelRanges)-1])
}
ret.appendBlock(Block{
Type: block.Type,
Labels: block.Labels,
Body: *childBody,
TypeRange: block.TypeRange,
DefRange: defRange,
LabelRanges: block.LabelRanges,
})
}
ret.MissingItemRange_ = body.EndRange
return ret
}

166
hclpack/pack_native_test.go Normal file
View File

@ -0,0 +1,166 @@
package hclpack
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl2/hcl"
)
func TestPackNativeFile(t *testing.T) {
src := `
foo = "bar"
baz = "boz"
child {
a = b + c
}
another_child "foo" "bar" {}
`
got, diags := PackNativeFile([]byte(src), "", hcl.Pos{Line: 1, Column: 1})
for _, diag := range diags {
t.Errorf("unexpected diagnostic: %s", diag.Error())
}
want := &Body{
Attributes: map[string]Attribute{
"baz": {
Expr: Expression{
Source: []byte(`"boz"`),
SourceType: ExprNative,
Range_: hcl.Range{
Start: hcl.Pos{Line: 3, Column: 7, Byte: 19},
End: hcl.Pos{Line: 3, Column: 12, Byte: 24},
},
StartRange_: hcl.Range{
Start: hcl.Pos{Line: 3, Column: 8, Byte: 20},
End: hcl.Pos{Line: 3, Column: 11, Byte: 23},
},
},
Range: hcl.Range{
Start: hcl.Pos{Line: 3, Column: 1, Byte: 13},
End: hcl.Pos{Line: 3, Column: 12, Byte: 24},
},
NameRange: hcl.Range{
Start: hcl.Pos{Line: 3, Column: 1, Byte: 13},
End: hcl.Pos{Line: 3, Column: 4, Byte: 16},
},
},
"foo": {
Expr: Expression{
Source: []byte(`"bar"`),
SourceType: ExprNative,
Range_: hcl.Range{
Start: hcl.Pos{Line: 2, Column: 7, Byte: 7},
End: hcl.Pos{Line: 2, Column: 12, Byte: 12},
},
StartRange_: hcl.Range{
Start: hcl.Pos{Line: 2, Column: 8, Byte: 8},
End: hcl.Pos{Line: 2, Column: 11, Byte: 11},
},
},
Range: hcl.Range{
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
End: hcl.Pos{Line: 2, Column: 12, Byte: 12},
},
NameRange: hcl.Range{
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
End: hcl.Pos{Line: 2, Column: 4, Byte: 4},
},
},
},
ChildBlocks: []Block{
{
Type: "child",
Body: Body{
Attributes: map[string]Attribute{
"a": {
Expr: Expression{
Source: []byte(`b + c`),
SourceType: ExprNative,
Range_: hcl.Range{
Start: hcl.Pos{Line: 6, Column: 7, Byte: 40},
End: hcl.Pos{Line: 6, Column: 12, Byte: 45},
},
StartRange_: hcl.Range{
Start: hcl.Pos{Line: 6, Column: 7, Byte: 40},
End: hcl.Pos{Line: 6, Column: 8, Byte: 41},
},
},
Range: hcl.Range{
Start: hcl.Pos{Line: 6, Column: 3, Byte: 36},
End: hcl.Pos{Line: 6, Column: 12, Byte: 45},
},
NameRange: hcl.Range{
Start: hcl.Pos{Line: 6, Column: 3, Byte: 36},
End: hcl.Pos{Line: 6, Column: 4, Byte: 37},
},
},
},
MissingItemRange_: hcl.Range{
Start: hcl.Pos{Line: 7, Column: 2, Byte: 47},
End: hcl.Pos{Line: 7, Column: 2, Byte: 47},
},
},
DefRange: hcl.Range{
Start: hcl.Pos{Line: 5, Column: 1, Byte: 26},
End: hcl.Pos{Line: 5, Column: 6, Byte: 31},
},
TypeRange: hcl.Range{
Start: hcl.Pos{Line: 5, Column: 1, Byte: 26},
End: hcl.Pos{Line: 5, Column: 6, Byte: 31},
},
},
{
Type: "another_child",
Labels: []string{"foo", "bar"},
Body: Body{
MissingItemRange_: hcl.Range{
Start: hcl.Pos{Line: 9, Column: 29, Byte: 77},
End: hcl.Pos{Line: 9, Column: 29, Byte: 77},
},
},
DefRange: hcl.Range{
Start: hcl.Pos{Line: 9, Column: 1, Byte: 49},
End: hcl.Pos{Line: 9, Column: 26, Byte: 74},
},
TypeRange: hcl.Range{
Start: hcl.Pos{Line: 9, Column: 1, Byte: 49},
End: hcl.Pos{Line: 9, Column: 14, Byte: 62},
},
LabelRanges: []hcl.Range{
hcl.Range{
Start: hcl.Pos{Line: 9, Column: 15, Byte: 63},
End: hcl.Pos{Line: 9, Column: 20, Byte: 68},
},
hcl.Range{
Start: hcl.Pos{Line: 9, Column: 21, Byte: 69},
End: hcl.Pos{Line: 9, Column: 26, Byte: 74},
},
},
},
},
MissingItemRange_: hcl.Range{
Start: hcl.Pos{Line: 10, Column: 1, Byte: 78},
End: hcl.Pos{Line: 10, Column: 1, Byte: 78},
},
}
if !cmp.Equal(want, got) {
bytesAsString := func(s []byte) string {
return string(s)
}
posAsString := func(pos hcl.Pos) string {
return fmt.Sprintf("%#v", pos)
}
t.Errorf("wrong result\n%s", cmp.Diff(
want, got,
cmp.Transformer("bytesAsString", bytesAsString),
cmp.Transformer("posAsString", posAsString),
))
}
}

324
hclpack/positions_packed.go Normal file
View File

@ -0,0 +1,324 @@
package hclpack
import (
"encoding/base64"
"sort"
"github.com/hashicorp/hcl2/hcl"
)
// positionsPacked is a delta-based representation of source positions
// that implements encoding.TextMarshaler and encoding.TextUnmarshaler using
// a compact variable-length quantity encoding to mimimize the overhead of
// storing source positions.
//
// Serializations of the other types in this package can refer to positions
// in a positionsPacked by index.
type positionsPacked []positionPacked
func (pp positionsPacked) MarshalBinary() ([]byte, error) {
lenInt := len(pp) * 4 // each positionPacked contains four ints, but we don't include the fileidx
// guess avg of ~1.25 bytes per int, in which case we'll avoid further allocation
buf := newVLQBuf(lenInt + (lenInt / 4))
var lastFileIdx int
for _, ppr := range pp {
// Rather than writing out the same file index over and over, we instead
// insert a ; delimiter each time it increases. Since it's common for
// for a body to be entirely in one file, this can lead to considerable
// savings in that case.
delims := ppr.FileIdx - lastFileIdx
for i := 0; i < delims; i++ {
buf = buf.AppendRawByte(';')
}
buf = buf.AppendInt(ppr.LineDelta)
buf = buf.AppendInt(ppr.ColumnDelta)
buf = buf.AppendInt(ppr.ByteDelta)
}
return buf.Bytes(), nil
}
func (pp positionsPacked) MarshalText() ([]byte, error) {
raw, err := pp.MarshalBinary()
if err != nil {
return nil, err
}
l := base64.RawStdEncoding.EncodedLen(len(raw))
ret := make([]byte, l)
base64.RawStdEncoding.Encode(ret, raw)
return ret, nil
}
func (pp *positionsPacked) UnmarshalBinary(data []byte) error {
buf := vlqBuf(data)
var ret positionsPacked
fileIdx := 0
for len(buf) > 0 {
if buf[0] == ';' {
// Starting a new file, then.
fileIdx++
buf = buf[1:]
continue
}
var ppr positionPacked
var err error
ppr.LineDelta, buf, err = buf.ReadInt()
if err != nil {
return err
}
ppr.ColumnDelta, buf, err = buf.ReadInt()
if err != nil {
return err
}
ppr.ByteDelta, buf, err = buf.ReadInt()
if err != nil {
return err
}
ret = append(ret, ppr)
}
*pp = ret
return nil
}
func (pp *positionsPacked) UnmarshalText(data []byte) error {
maxL := base64.RawStdEncoding.DecodedLen(len(data))
into := make([]byte, maxL)
realL, err := base64.RawStdEncoding.Decode(into, data)
if err != nil {
return err
}
return pp.UnmarshalBinary(into[:realL])
}
type position struct {
FileIdx int
Pos hcl.Pos
}
func (pp positionsPacked) Unpack() []position {
ret := make([]position, len(pp))
var accPos hcl.Pos
var accFileIdx int
for i, relPos := range pp {
if relPos.FileIdx != accFileIdx {
accPos = hcl.Pos{} // reset base position for each new file
accFileIdx = pp[i].FileIdx
}
if relPos.LineDelta > 0 {
accPos.Column = 0 // reset column position for each new line
}
accPos.Line += relPos.LineDelta
accPos.Column += relPos.ColumnDelta
accPos.Byte += relPos.ByteDelta
ret[i] = position{
FileIdx: relPos.FileIdx,
Pos: accPos,
}
}
return ret
}
type positionPacked struct {
FileIdx int
LineDelta, ColumnDelta, ByteDelta int
}
func (pp positionsPacked) Len() int {
return len(pp)
}
func (pp positionsPacked) Less(i, j int) bool {
return pp[i].FileIdx < pp[j].FileIdx
}
func (pp positionsPacked) Swap(i, j int) {
pp[i], pp[j] = pp[j], pp[i]
}
// posOfs is an index into a positionsPacked. The zero value of this type
// represents the absense of a position.
type posOfs int
func newPosOffs(idx int) posOfs {
return posOfs(idx + 1)
}
func (o posOfs) Index() int {
return int(o - 1)
}
// rangePacked is a range represented as two indexes into a positionsPacked.
// This implements encoding.TextMarshaler and encoding.TextUnmarshaler using
// a compact variable-length quantity encoding.
type rangePacked struct {
Start posOfs
End posOfs
}
func packRange(rng hcl.Range, pos map[string]map[hcl.Pos]posOfs) rangePacked {
return rangePacked{
Start: pos[rng.Filename][rng.Start],
End: pos[rng.Filename][rng.End],
}
}
func (rp rangePacked) Unpack(fns []string, poss []position) hcl.Range {
startIdx := rp.Start.Index()
endIdx := rp.End.Index()
if startIdx < 0 && startIdx >= len(poss) {
return hcl.Range{} // out of bounds, so invalid
}
if endIdx < 0 && endIdx >= len(poss) {
return hcl.Range{} // out of bounds, so invalid
}
startPos := poss[startIdx]
endPos := poss[endIdx]
fnIdx := startPos.FileIdx
var fn string
if fnIdx >= 0 && fnIdx < len(fns) {
fn = fns[fnIdx]
}
return hcl.Range{
Filename: fn,
Start: startPos.Pos,
End: endPos.Pos,
}
}
// rangesPacked represents a sequence of ranges, packed compactly into a single
// string during marshaling.
type rangesPacked []rangePacked
func (rp rangesPacked) MarshalBinary() ([]byte, error) {
lenInt := len(rp) * 2 // each positionPacked contains two ints
// guess avg of ~1.25 bytes per int, in which case we'll avoid further allocation
buf := newVLQBuf(lenInt + (lenInt / 4))
for _, rpr := range rp {
buf = buf.AppendInt(int(rpr.Start)) // intentionally storing these as 1-based offsets
buf = buf.AppendInt(int(rpr.End))
}
return buf.Bytes(), nil
}
func (rp rangesPacked) MarshalText() ([]byte, error) {
raw, err := rp.MarshalBinary()
if err != nil {
return nil, err
}
l := base64.RawStdEncoding.EncodedLen(len(raw))
ret := make([]byte, l)
base64.RawStdEncoding.Encode(ret, raw)
return ret, nil
}
func (rp *rangesPacked) UnmarshalBinary(data []byte) error {
buf := vlqBuf(data)
var ret rangesPacked
for len(buf) > 0 {
var startInt, endInt int
var err error
startInt, buf, err = buf.ReadInt()
if err != nil {
return err
}
endInt, buf, err = buf.ReadInt()
if err != nil {
return err
}
ret = append(ret, rangePacked{
Start: posOfs(startInt), // these are stored as 1-based offsets, so safe to convert directly
End: posOfs(endInt),
})
}
*rp = ret
return nil
}
func (rp *rangesPacked) UnmarshalText(data []byte) error {
maxL := base64.RawStdEncoding.DecodedLen(len(data))
into := make([]byte, maxL)
realL, err := base64.RawStdEncoding.Decode(into, data)
if err != nil {
return err
}
return rp.UnmarshalBinary(into[:realL])
}
func (rps rangesPacked) UnpackIdx(fns []string, poss []position, idx int) hcl.Range {
if idx < 0 || idx >= len(rps) {
return hcl.Range{} // out of bounds, so invalid
}
return rps[idx].Unpack(fns, poss)
}
// packPositions will find the distinct positions from the given ranges
// and then pack them into a positionsPacked, along with a lookup table to find
// the encoded offset of each distinct position.
func packPositions(rngs map[hcl.Range]struct{}) (fns []string, poss positionsPacked, posMap map[string]map[hcl.Pos]posOfs) {
const noOfs = posOfs(0)
posByFile := make(map[string][]hcl.Pos)
for rng := range rngs {
fn := rng.Filename
posByFile[fn] = append(posByFile[fn], rng.Start)
posByFile[fn] = append(posByFile[fn], rng.End)
}
fns = make([]string, 0, len(posByFile))
for fn := range posByFile {
fns = append(fns, fn)
}
sort.Strings(fns)
var retPos positionsPacked
posMap = make(map[string]map[hcl.Pos]posOfs)
for fileIdx, fn := range fns {
poss := posByFile[fn]
sort.Sort(sortPositions(poss))
var prev hcl.Pos
for _, pos := range poss {
if _, exists := posMap[fn][pos]; exists {
continue
}
ofs := newPosOffs(len(retPos))
if pos.Line != prev.Line {
// Column indices start from zero for each new line.
prev.Column = 0
}
retPos = append(retPos, positionPacked{
FileIdx: fileIdx,
LineDelta: pos.Line - prev.Line,
ColumnDelta: pos.Column - prev.Column,
ByteDelta: pos.Byte - prev.Byte,
})
if posMap[fn] == nil {
posMap[fn] = make(map[hcl.Pos]posOfs)
}
posMap[fn][pos] = ofs
prev = pos
}
}
return fns, retPos, posMap
}
type sortPositions []hcl.Pos
func (sp sortPositions) Len() int {
return len(sp)
}
func (sp sortPositions) Less(i, j int) bool {
return sp[i].Byte < sp[j].Byte
}
func (sp sortPositions) Swap(i, j int) {
sp[i], sp[j] = sp[j], sp[i]
}

290
hclpack/structure.go Normal file
View File

@ -0,0 +1,290 @@
package hclpack
import (
"fmt"
"github.com/hashicorp/hcl2/hcl"
)
// Body is an implementation of hcl.Body.
type Body struct {
Attributes map[string]Attribute
ChildBlocks []Block
MissingItemRange_ hcl.Range
}
var _ hcl.Body = (*Body)(nil)
// Content is an implementation of the method of the same name on hcl.Body.
//
// When Content is called directly on a hclpack.Body, all child block bodies
// are guaranteed to be of type *hclpack.Body, so callers can type-assert
// to obtain a child Body in order to serialize it separately if needed.
func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) {
return b.content(schema, nil)
}
// PartialContent is an implementation of the method of the same name on hcl.Body.
//
// The returned "remain" body may share some backing objects with the receiver,
// so neither the receiver nor the returned remain body, or any descendent
// objects within them, may be mutated after this method is used.
//
// When Content is called directly on a hclpack.Body, all child block bodies
// and the returned "remain" body are guaranteed to be of type *hclpack.Body,
// so callers can type-assert to obtain a child Body in order to serialize it
// separately if needed.
func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) {
remain := &Body{}
content, diags := b.content(schema, remain)
return content, remain, diags
}
func (b *Body) content(schema *hcl.BodySchema, remain *Body) (*hcl.BodyContent, hcl.Diagnostics) {
if b == nil {
b = &Body{} // We'll treat a nil body like an empty one, for convenience
}
var diags hcl.Diagnostics
var attrs map[string]*hcl.Attribute
var attrUsed map[string]struct{}
if len(b.Attributes) > 0 {
attrs = make(map[string]*hcl.Attribute, len(b.Attributes))
attrUsed = make(map[string]struct{}, len(b.Attributes))
}
for _, attrS := range schema.Attributes {
name := attrS.Name
attr, exists := b.Attributes[name]
if !exists {
if attrS.Required {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required argument",
Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name),
Subject: &b.MissingItemRange_,
})
}
continue
}
attrs[name] = attr.asHCLAttribute(name)
attrUsed[name] = struct{}{}
}
for name, attr := range b.Attributes {
if _, used := attrUsed[name]; used {
continue
}
if remain != nil {
remain.setAttribute(name, attr)
continue
}
var suggestions []string
for _, attrS := range schema.Attributes {
if _, defined := attrs[name]; defined {
continue
}
suggestions = append(suggestions, attrS.Name)
}
suggestion := nameSuggestion(name, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
} else {
// Is there a block of the same name?
for _, blockS := range schema.Blocks {
if blockS.Type == name {
suggestion = fmt.Sprintf(" Did you mean to define a block of type %q?", name)
break
}
}
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported argument",
Detail: fmt.Sprintf("An argument named %q is not expected here.%s", name, suggestion),
Subject: &attr.NameRange,
})
}
blocksWanted := make(map[string]hcl.BlockHeaderSchema)
for _, blockS := range schema.Blocks {
blocksWanted[blockS.Type] = blockS
}
var blocks []*hcl.Block
for _, block := range b.ChildBlocks {
blockTy := block.Type
blockS, wanted := blocksWanted[blockTy]
if !wanted {
if remain != nil {
remain.appendBlock(block)
continue
}
var suggestions []string
for _, blockS := range schema.Blocks {
suggestions = append(suggestions, blockS.Type)
}
suggestion := nameSuggestion(blockTy, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
} else {
// Is there an attribute of the same name?
for _, attrS := range schema.Attributes {
if attrS.Name == blockTy {
suggestion = fmt.Sprintf(" Did you mean to define argument %q?", blockTy)
break
}
}
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported block type",
Detail: fmt.Sprintf("Blocks of type %q are not expected here.%s", blockTy, suggestion),
Subject: &block.TypeRange,
})
continue
}
if len(block.Labels) != len(blockS.LabelNames) {
if len(blockS.LabelNames) == 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Extraneous label for %s", blockTy),
Detail: fmt.Sprintf(
"No labels are expected for %s blocks.", blockTy,
),
Subject: &block.DefRange,
Context: &block.DefRange,
})
} else {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Wrong label count for %s", blockTy),
Detail: fmt.Sprintf(
"%s blocks expect %d label(s), but got %d.",
blockTy, len(blockS.LabelNames), len(block.Labels),
),
Subject: &block.DefRange,
Context: &block.DefRange,
})
}
continue
}
blocks = append(blocks, block.asHCLBlock())
}
return &hcl.BodyContent{
Attributes: attrs,
Blocks: blocks,
MissingItemRange: b.MissingItemRange_,
}, diags
}
// JustAttributes is an implementation of the method of the same name on hcl.Body.
func (b *Body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) {
var diags hcl.Diagnostics
if len(b.ChildBlocks) > 0 {
for _, block := range b.ChildBlocks {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Unexpected %s block", block.Type),
Detail: "Blocks are not allowed here.",
Context: &block.TypeRange,
})
}
// We'll continue processing anyway, and return any attributes we find
// so that the caller can do careful partial analysis.
}
if len(b.Attributes) == 0 {
return nil, diags
}
ret := make(hcl.Attributes, len(b.Attributes))
for n, a := range b.Attributes {
ret[n] = a.asHCLAttribute(n)
}
return ret, diags
}
// MissingItemRange is an implementation of the method of the same name on hcl.Body.
func (b *Body) MissingItemRange() hcl.Range {
return b.MissingItemRange_
}
func (b *Body) setAttribute(name string, attr Attribute) {
if b.Attributes == nil {
b.Attributes = make(map[string]Attribute)
}
b.Attributes[name] = attr
}
func (b *Body) appendBlock(block Block) {
b.ChildBlocks = append(b.ChildBlocks, block)
}
func (b *Body) addRanges(rngs map[hcl.Range]struct{}) {
rngs[b.MissingItemRange_] = struct{}{}
for _, attr := range b.Attributes {
attr.addRanges(rngs)
}
for _, block := range b.ChildBlocks {
block.addRanges(rngs)
}
}
// Block represents a nested block within a body.
type Block struct {
Type string
Labels []string
Body Body
DefRange, TypeRange hcl.Range
LabelRanges []hcl.Range
}
func (b *Block) asHCLBlock() *hcl.Block {
return &hcl.Block{
Type: b.Type,
Labels: b.Labels,
Body: &b.Body,
TypeRange: b.TypeRange,
DefRange: b.DefRange,
LabelRanges: b.LabelRanges,
}
}
func (b *Block) addRanges(rngs map[hcl.Range]struct{}) {
rngs[b.DefRange] = struct{}{}
rngs[b.TypeRange] = struct{}{}
for _, rng := range b.LabelRanges {
rngs[rng] = struct{}{}
}
b.Body.addRanges(rngs)
}
// Attribute represents an attribute definition within a body.
type Attribute struct {
Expr Expression
Range, NameRange hcl.Range
}
func (a *Attribute) asHCLAttribute(name string) *hcl.Attribute {
return &hcl.Attribute{
Name: name,
Expr: &a.Expr,
Range: a.Range,
NameRange: a.NameRange,
}
}
func (a *Attribute) addRanges(rngs map[hcl.Range]struct{}) {
rngs[a.Range] = struct{}{}
rngs[a.NameRange] = struct{}{}
a.Expr.addRanges(rngs)
}

93
hclpack/structure_test.go Normal file
View File

@ -0,0 +1,93 @@
package hclpack
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl2/hcl"
)
func TestBodyContent(t *testing.T) {
tests := map[string]struct {
Body *Body
Schema *hcl.BodySchema
Want *hcl.BodyContent
}{
"empty": {
&Body{},
&hcl.BodySchema{},
&hcl.BodyContent{},
},
"nil": {
nil,
&hcl.BodySchema{},
&hcl.BodyContent{},
},
"attribute": {
&Body{
Attributes: map[string]Attribute{
"foo": {
Expr: Expression{
Source: []byte(`"hello"`),
SourceType: ExprNative,
},
},
},
},
&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "foo", Required: true},
{Name: "bar", Required: false},
},
},
&hcl.BodyContent{
Attributes: hcl.Attributes{
"foo": {
Name: "foo",
Expr: &Expression{
Source: []byte(`"hello"`),
SourceType: ExprNative,
},
},
},
},
},
"block": {
&Body{
ChildBlocks: []Block{
{
Type: "foo",
},
},
},
&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: "foo"},
},
},
&hcl.BodyContent{
Blocks: hcl.Blocks{
{
Type: "foo",
Body: &Body{},
},
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, diags := test.Body.Content(test.Schema)
for _, diag := range diags {
t.Errorf("unexpected diagnostic: %s", diag.Error())
}
if !cmp.Equal(test.Want, got) {
t.Errorf("wrong result\n%s", cmp.Diff(test.Want, got))
}
})
}
}

50
hclpack/vlq.go Normal file
View File

@ -0,0 +1,50 @@
package hclpack
import (
"errors"
"github.com/bsm/go-vlq"
)
type vlqBuf []byte
var vlqSpace [vlq.MaxLen64]byte
func newVLQBuf(byteCap int) vlqBuf {
return make(vlqBuf, 0, byteCap)
}
func (b vlqBuf) AppendInt(i int) vlqBuf {
spc := cap(b) - len(b)
if spc < len(vlqSpace) {
b = append(b, vlqSpace[:]...)
b = b[:len(b)-len(vlqSpace)]
}
into := b[len(b):cap(b)]
l := vlq.PutInt(into, int64(i))
b = b[:len(b)+l]
return b
}
func (b vlqBuf) ReadInt() (int, vlqBuf, error) {
v, adv := vlq.Int([]byte(b))
if adv <= 0 {
if adv == 0 {
return 0, b, errors.New("missing expected VLQ value")
} else {
return 0, b, errors.New("invalid VLQ value")
}
}
if int64(int(v)) != v {
return 0, b, errors.New("VLQ value too big for integer on this platform")
}
return int(v), b[adv:], nil
}
func (b vlqBuf) AppendRawByte(by byte) vlqBuf {
return append(b, by)
}
func (b vlqBuf) Bytes() []byte {
return []byte(b)
}