forked from TrueCloudLab/neoneo-go
9c3d8cd398
It should be able to parse versions like `1.1.1-rc.1`. Close #1540.
231 lines
6.7 KiB
Go
231 lines
6.7 KiB
Go
package nef
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
)
|
|
|
|
// NEO Executable Format 3 (NEF3)
|
|
// Standard: https://github.com/neo-project/proposals/pull/121/files
|
|
// Implementation: https://github.com/neo-project/neo/blob/v3.0.0-preview2/src/neo/SmartContract/NefFile.cs#L8
|
|
// +------------+-----------+------------------------------------------------------------+
|
|
// | Field | Length | Comment |
|
|
// +------------+-----------+------------------------------------------------------------+
|
|
// | Magic | 4 bytes | Magic header |
|
|
// | Compiler | 32 bytes | Compiler used |
|
|
// | Version | 16 bytes | Compiler version (Major, Minor, Build, Version) |
|
|
// | ScriptHash | 20 bytes | ScriptHash for the script (BE) |
|
|
// +------------+-----------+------------------------------------------------------------+
|
|
// | Checksum | 4 bytes | First four bytes of double SHA256 hash of the header |
|
|
// +------------+-----------+------------------------------------------------------------+
|
|
// | Script | Var bytes | Var bytes for the payload |
|
|
// +------------+-----------+------------------------------------------------------------+
|
|
|
|
const (
|
|
// Magic is a magic File header constant.
|
|
Magic uint32 = 0x3346454E
|
|
// MaxScriptLength is the maximum allowed contract script length.
|
|
MaxScriptLength = 1024 * 1024
|
|
// compilerFieldSize is the length of `Compiler` File header field in bytes.
|
|
compilerFieldSize = 32
|
|
)
|
|
|
|
// File represents compiled contract file structure according to the NEF3 standard.
|
|
type File struct {
|
|
Header Header
|
|
Checksum uint32
|
|
Script []byte
|
|
}
|
|
|
|
// Header represents File header.
|
|
type Header struct {
|
|
Magic uint32
|
|
Compiler string
|
|
Version Version
|
|
ScriptHash util.Uint160
|
|
}
|
|
|
|
// Version represents compiler version.
|
|
type Version struct {
|
|
Major int32
|
|
Minor int32
|
|
Build int32
|
|
Revision int32
|
|
}
|
|
|
|
// NewFile returns new NEF3 file with script specified.
|
|
func NewFile(script []byte) (File, error) {
|
|
file := File{
|
|
Header: Header{
|
|
Magic: Magic,
|
|
Compiler: "neo-go",
|
|
ScriptHash: hash.Hash160(script),
|
|
},
|
|
Script: script,
|
|
}
|
|
v, err := GetVersion(config.Version)
|
|
if err != nil {
|
|
return file, err
|
|
}
|
|
file.Header.Version = v
|
|
file.Checksum = file.Header.CalculateChecksum()
|
|
return file, nil
|
|
}
|
|
|
|
// GetVersion returns Version from the given string. It accepts the following formats:
|
|
// `major[-...].minor[-...].build[-...]` and `major[-...].minor[-...].build[-...].revision[-...]`
|
|
// where `major`, `minor`, `build` and `revision` are 32-bit integers with base=10
|
|
func GetVersion(version string) (Version, error) {
|
|
var (
|
|
result Version
|
|
err error
|
|
)
|
|
versions := strings.SplitN(version, ".", 4)
|
|
if len(versions) < 3 {
|
|
return result, errors.New("invalid version format")
|
|
}
|
|
result.Major, err = parseDashedVersion(versions[0])
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to parse major version: %w", err)
|
|
}
|
|
result.Minor, err = parseDashedVersion(versions[1])
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to parse minor version: %w", err)
|
|
|
|
}
|
|
result.Build, err = parseDashedVersion(versions[2])
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to parse build version: %w", err)
|
|
}
|
|
if len(versions) == 4 {
|
|
result.Revision, err = parseDashedVersion(versions[3])
|
|
if err != nil {
|
|
return result, fmt.Errorf("failed to parse revision version: %w", err)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// parseDashedVersion extracts int from string of the format `int[-...]` where `int` is
|
|
// a 32-bit integer with base=10.
|
|
func parseDashedVersion(version string) (int32, error) {
|
|
version = strings.SplitN(version, "-", 2)[0]
|
|
result, err := strconv.ParseInt(version, 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return int32(result), nil
|
|
}
|
|
|
|
// EncodeBinary implements io.Serializable interface.
|
|
func (v *Version) EncodeBinary(w *io.BinWriter) {
|
|
w.WriteU32LE(uint32(v.Major))
|
|
w.WriteU32LE(uint32(v.Minor))
|
|
w.WriteU32LE(uint32(v.Build))
|
|
w.WriteU32LE(uint32(v.Revision))
|
|
}
|
|
|
|
// DecodeBinary implements io.Serializable interface.
|
|
func (v *Version) DecodeBinary(r *io.BinReader) {
|
|
v.Major = int32(r.ReadU32LE())
|
|
v.Minor = int32(r.ReadU32LE())
|
|
v.Build = int32(r.ReadU32LE())
|
|
v.Revision = int32(r.ReadU32LE())
|
|
}
|
|
|
|
// EncodeBinary implements io.Serializable interface.
|
|
func (h *Header) EncodeBinary(w *io.BinWriter) {
|
|
w.WriteU32LE(h.Magic)
|
|
if len(h.Compiler) > compilerFieldSize {
|
|
w.Err = errors.New("invalid compiler name length")
|
|
return
|
|
}
|
|
bytes := []byte(h.Compiler)
|
|
w.WriteBytes(bytes)
|
|
if len(bytes) < compilerFieldSize {
|
|
w.WriteBytes(make([]byte, compilerFieldSize-len(bytes)))
|
|
}
|
|
h.Version.EncodeBinary(w)
|
|
h.ScriptHash.EncodeBinary(w)
|
|
}
|
|
|
|
// DecodeBinary implements io.Serializable interface.
|
|
func (h *Header) DecodeBinary(r *io.BinReader) {
|
|
h.Magic = r.ReadU32LE()
|
|
if h.Magic != Magic {
|
|
r.Err = errors.New("invalid Magic")
|
|
return
|
|
}
|
|
buf := make([]byte, compilerFieldSize)
|
|
r.ReadBytes(buf)
|
|
buf = bytes.TrimRightFunc(buf, func(r rune) bool {
|
|
return r == 0
|
|
})
|
|
h.Compiler = string(buf)
|
|
h.Version.DecodeBinary(r)
|
|
h.ScriptHash.DecodeBinary(r)
|
|
}
|
|
|
|
// CalculateChecksum returns first 4 bytes of double-SHA256(Header) converted to uint32.
|
|
func (h *Header) CalculateChecksum() uint32 {
|
|
buf := io.NewBufBinWriter()
|
|
h.EncodeBinary(buf.BinWriter)
|
|
if buf.Err != nil {
|
|
panic(buf.Err)
|
|
}
|
|
return binary.LittleEndian.Uint32(hash.Checksum(buf.Bytes()))
|
|
}
|
|
|
|
// EncodeBinary implements io.Serializable interface.
|
|
func (n *File) EncodeBinary(w *io.BinWriter) {
|
|
n.Header.EncodeBinary(w)
|
|
w.WriteU32LE(n.Checksum)
|
|
w.WriteVarBytes(n.Script)
|
|
}
|
|
|
|
// DecodeBinary implements io.Serializable interface.
|
|
func (n *File) DecodeBinary(r *io.BinReader) {
|
|
n.Header.DecodeBinary(r)
|
|
n.Checksum = r.ReadU32LE()
|
|
checksum := n.Header.CalculateChecksum()
|
|
if checksum != n.Checksum {
|
|
r.Err = errors.New("CRC verification fail")
|
|
return
|
|
}
|
|
n.Script = r.ReadVarBytes(MaxScriptLength)
|
|
if !hash.Hash160(n.Script).Equals(n.Header.ScriptHash) {
|
|
r.Err = errors.New("script hashes mismatch")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Bytes returns byte array with serialized NEF File.
|
|
func (n File) Bytes() ([]byte, error) {
|
|
buf := io.NewBufBinWriter()
|
|
n.EncodeBinary(buf.BinWriter)
|
|
if buf.Err != nil {
|
|
return nil, buf.Err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// FileFromBytes returns NEF File deserialized from given bytes.
|
|
func FileFromBytes(source []byte) (File, error) {
|
|
result := File{}
|
|
r := io.NewBinReaderFromBuf(source)
|
|
result.DecodeBinary(r)
|
|
if r.Err != nil {
|
|
return result, r.Err
|
|
}
|
|
return result, nil
|
|
}
|