├── testdata └── block.blend ├── examples └── blendview │ ├── block.png │ └── blendview.go ├── .travis.yml ├── blend_test.go ├── cmd └── blendef │ ├── blendef.go │ ├── gen_parse.go │ └── gen_struct.go ├── block ├── example_test.go ├── dna.go └── block.go ├── README.md └── blend.go /testdata/block.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mewspring/blend/HEAD/testdata/block.blend -------------------------------------------------------------------------------- /examples/blendview/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mewspring/blend/HEAD/examples/blendview/block.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | notifications: 4 | email: false 5 | 6 | env: 7 | global: 8 | - secure: "GlPLJfSq5DsDp2zFTxi3151bF5wJbLWkSIAYsel9DwL9O5YRCASsV4XgRN9/DBC3hS/rbiB6tCCIZXuumufMg96YDboxINSBX8qlH6bqu20ybUXe9tg1BjazHgo7Lp/KOCKYt4VU1/qrMGQKOcxyl6moBmUp+GCHoYe86XVoi5A=" 9 | - PATH=$HOME/gopath/bin:$PATH 10 | 11 | before_install: 12 | - go get golang.org/x/tools/cmd/cover 13 | - go get golang.org/x/tools/cmd/goimports 14 | - go get golang.org/x/tools/cmd/vet 15 | - go get golang.org/x/lint/golint 16 | - go get github.com/mattn/goveralls 17 | 18 | install: 19 | - go get ./... 20 | 21 | before_script: 22 | - wget https://gist.github.com/mewmew/379014c9a2e6885e238d/raw/goclean.sh 23 | - chmod +x goclean.sh 24 | 25 | script: 26 | - ./goclean.sh 27 | -------------------------------------------------------------------------------- /blend_test.go: -------------------------------------------------------------------------------- 1 | package blend_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/mewspring/blend" 8 | ) 9 | 10 | func ExampleBlend() { 11 | // Parse blend file header and block headers. 12 | b, err := blend.Parse("testdata/block.blend") 13 | if err != nil { 14 | log.Fatalln(err) 15 | } 16 | defer b.Close() 17 | 18 | // Parse DNA block. 19 | dna, err := b.GetDNA() 20 | if err != nil { 21 | log.Fatalln(err) 22 | } 23 | 24 | // Parse and output the body of block 11. 25 | blk := b.Blocks[11] 26 | err = blk.ParseBody(b.Hdr.Order, dna) 27 | if err != nil { 28 | log.Fatalln(err) 29 | } 30 | fmt.Printf("%#v\n", blk.Body) 31 | 32 | // Output: 33 | // &block.ScrVert{Next:0x0, Prev:0x4953858, Newv:0x0, Vec:block.Vec2s{X:1920, Y:1053}, Flag:1, Editflag:1} 34 | } 35 | -------------------------------------------------------------------------------- /cmd/blendef/blendef.go: -------------------------------------------------------------------------------- 1 | // blendef is a tool which regenerates the block structure definitions and the 2 | // block parsing logic of the block package. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/mewspring/blend" 12 | ) 13 | 14 | func init() { 15 | flag.Usage = usage 16 | } 17 | 18 | func usage() { 19 | fmt.Fprintln(os.Stderr, "Usage: blendef FILE.blend") 20 | } 21 | 22 | func main() { 23 | flag.Parse() 24 | if flag.NArg() != 1 { 25 | flag.Usage() 26 | os.Exit(1) 27 | } 28 | err := blendef(flag.Arg(0)) 29 | if err != nil { 30 | log.Fatalln(err) 31 | } 32 | } 33 | 34 | // blendef parses the provided blend file and generates the following Go files: 35 | // 36 | // struct.go // structure definitions 37 | // parse.go // block parser logic 38 | func blendef(filePath string) (err error) { 39 | blend.WarnVersion = false 40 | b, err := blend.Parse(filePath) 41 | if err != nil { 42 | return err 43 | } 44 | defer b.Close() 45 | 46 | dna, err := b.GetDNA() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // Generate struct.go 52 | err = genStruct(b, dna) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // Generate parse.go 58 | err = genParse(b, dna) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /block/example_test.go: -------------------------------------------------------------------------------- 1 | package block_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/mewspring/blend" 8 | "github.com/mewspring/blend/block" 9 | ) 10 | 11 | func ExamplePointer() { 12 | // Parse blend file header and file blocks. 13 | b, err := blend.ParseAll("../testdata/block.blend") 14 | if err != nil { 15 | log.Fatalln(err) 16 | } 17 | for _, blk := range b.Blocks { 18 | lamp, ok := blk.Body.(*block.Lamp) 19 | if !ok { 20 | continue 21 | } 22 | // Get access to the actual data instead of the stored memory address. 23 | data, err := lamp.Curfalloff.Data() 24 | if err != nil { 25 | log.Fatalln(err) 26 | } 27 | fmt.Printf("%#v\n", data) 28 | break 29 | } 30 | 31 | // Output: 32 | // &block.CurveMapping{Flag:1, Cur:0, Preset:0, Changed_timestamp:0, Curr:block.Rctf{Xmin:0, Xmax:1, Ymin:0, Ymax:1}, Clipr:block.Rctf{Xmin:0, Xmax:1, Ymin:0, Ymax:1}, Cm:[4]block.CurveMap{block.CurveMap{Totpoint:2, Flag:1, Range:256, Mintable:0, Maxtable:1, Ext_in:[2]float32{-0.70710677, 0.7071067}, Ext_out:[2]float32{-0.7071067, 0.70710677}, Curve:0x4dd66a8, Table:0x0, Premultable:0x0, Premul_ext_in:[2]float32{0, 0}, Premul_ext_out:[2]float32{0, 0}}, block.CurveMap{Totpoint:0, Flag:0, Range:0, Mintable:0, Maxtable:0, Ext_in:[2]float32{0, 0}, Ext_out:[2]float32{0, 0}, Curve:0x0, Table:0x0, Premultable:0x0, Premul_ext_in:[2]float32{0, 0}, Premul_ext_out:[2]float32{0, 0}}, block.CurveMap{Totpoint:0, Flag:0, Range:0, Mintable:0, Maxtable:0, Ext_in:[2]float32{0, 0}, Ext_out:[2]float32{0, 0}, Curve:0x0, Table:0x0, Premultable:0x0, Premul_ext_in:[2]float32{0, 0}, Premul_ext_out:[2]float32{0, 0}}, block.CurveMap{Totpoint:0, Flag:0, Range:0, Mintable:0, Maxtable:0, Ext_in:[2]float32{0, 0}, Ext_out:[2]float32{0, 0}, Curve:0x0, Table:0x0, Premultable:0x0, Premul_ext_in:[2]float32{0, 0}, Premul_ext_out:[2]float32{0, 0}}}, Black:[3]float32{0, 0, 0}, White:[3]float32{1, 1, 1}, Bwmul:[3]float32{1, 1, 1}, Sample:[3]float32{0, 0, 0}} 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blend 2 | 3 | [![Build Status](https://travis-ci.org/mewmew/blend.svg?branch=master)](https://travis-ci.org/mewmew/blend) 4 | [![Coverage Status](https://img.shields.io/coveralls/mewmew/blend.svg)](https://coveralls.io/r/mewmew/blend?branch=master) 5 | [![GoDoc](https://godoc.org/github.com/mewmew/blend?status.svg)](https://godoc.org/github.com/mewmew/blend) 6 | 7 | The blend project provides support for reading [blend][1] files, which are used by [Blender](http://www.blender.org/). 8 | 9 | [1]: http://www.blender.org/development/architecture/blender-file-format/ 10 | 11 | # Documentation 12 | 13 | Documentation provided by GoDoc. 14 | 15 | - [blend]: implements parsing of Blender files. 16 | - [block][blend/block]: implements parsing of blend file blocks. 17 | 18 | [blend]: http://godoc.org/github.com/mewmew/blend 19 | [blend/block]: http://godoc.org/github.com/mewmew/blend/block 20 | 21 | ## Installation 22 | 23 | go get github.com/mewmew/blend 24 | 25 | To parse more complex blend files "block/struct.go" and "block/parse.go" may need to be regenereted. To regenerate these two files for any given blend file, use the `blendef` tool. 26 | 27 | go get github.com/mewmew/blend/cmd/blendef 28 | cd $GOPATH/src/github.com/mewmew/blend/block 29 | blendef /path/to/complex.blend 30 | 31 | A more detailed description is given in the "self-describing format" section. 32 | 33 | ## Examples 34 | 35 | * Extract an embedded thumbnail from a blend file. 36 | 37 | go get github.com/mewmew/blend/examples/blendview 38 | cd $GOPATH/src/github.com/mewmew/blend/testdata 39 | blendview block.blend 40 | 41 | ![Extracted thumbnail](https://raw.githubusercontent.com/mewmew/blend/master/examples/blendview/block.png) 42 | 43 | * Parse a single block in a blend file. 44 | 45 | http://godoc.org/github.com/mewmew/blend#example-Blend 46 | 47 | * Parse all blocks in a blend file and access the data of a pointer. 48 | 49 | http://godoc.org/github.com/mewmew/blend/block#example-Pointer 50 | 51 | ## Self-describing format 52 | 53 | One unique feature of blend files is that they contain a full definition of every structure used in its file blocks. The structure definitions are stored in the DNA block. 54 | 55 | All block structure definitions ("block/struct.go") and the block parsing logic ("block/parse.go") have been generating by parsing the DNA block of "testdata/block.blend". 56 | 57 | The tool which was used to generate these two files is located at: 58 | 59 | github.com/mewmew/blend/cmd/blendef 60 | 61 | More complex blend files may contain structures which are not yet defined in the block package. If so, use `blendef` to regenerate "struct.go" and "parse.go" for the given blend file. 62 | 63 | ## Public domain 64 | 65 | The source code and any original content of this repository is hereby released into the [public domain]. 66 | 67 | [public domain]: https://creativecommons.org/publicdomain/zero/1.0/ 68 | -------------------------------------------------------------------------------- /cmd/blendef/gen_parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/mewspring/blend" 10 | "github.com/mewspring/blend/block" 11 | ) 12 | 13 | // genParse generates the block parser logic required to parse the provided 14 | // blend file. 15 | // 16 | // The output is stored in "parse.go". 17 | func genParse(b *blend.Blend, dna *block.DNA) (err error) { 18 | f, err := os.Create("parse.go") 19 | if err != nil { 20 | return err 21 | } 22 | defer f.Close() 23 | 24 | // Create sorted list of type names. 25 | structs := make(map[string]bool) 26 | for _, st := range dna.Structs { 27 | structs[st.Type] = true 28 | } 29 | typeNames := make([]string, len(structs)) 30 | var i int 31 | for typeName := range structs { 32 | typeNames[i] = typeName 33 | i++ 34 | } 35 | sort.Strings(typeNames) 36 | 37 | // Generate start of struct.go file. 38 | fmt.Fprintf(f, preStruct, b.Hdr.Ver) 39 | 40 | // Generate block body parsing cases. 41 | for _, typeName := range typeNames { 42 | typ := strings.Title(typeName) 43 | fmt.Fprintf(f, midStruct, typeName, typ, typ, typ) 44 | } 45 | 46 | // Generate end of struct.go file. 47 | fmt.Fprint(f, postStruct) 48 | 49 | return nil 50 | } 51 | 52 | const preStruct = `// NOTE: this file has been automatically generated by blendef for Blender v%d. 53 | 54 | package block 55 | 56 | import ( 57 | "encoding/binary" 58 | "encoding/hex" 59 | "fmt" 60 | "io" 61 | "io/ioutil" 62 | "log" 63 | "os" 64 | ) 65 | 66 | // ParseBody parses the block body and stores it in blk.Body. It is safe to call 67 | // ParseBody multiple times on the same block. 68 | func (blk *Block) ParseBody(order binary.ByteOrder, dna *DNA) (err error) { 69 | // Get block body reader. 70 | r, ok := blk.Body.(io.Reader) 71 | if !ok { 72 | // Body has already been parsed. 73 | return nil 74 | } 75 | 76 | index := blk.Hdr.SDNAIndex 77 | if index == 0 { 78 | // Parse based on block code. 79 | switch blk.Hdr.Code { 80 | case CodeDATA: 81 | blk.Body, err = ioutil.ReadAll(r) 82 | if err != nil { 83 | return err 84 | } 85 | case CodeDNA1: 86 | blk.Body, err = ParseDNA(r, order) 87 | if err != nil { 88 | return err 89 | } 90 | case CodeREND, CodeTEST: 91 | /// TODO: implement specific block body parsing for REND and TEST. 92 | blk.Body, err = ioutil.ReadAll(r) 93 | if err != nil { 94 | return err 95 | } 96 | default: 97 | return fmt.Errorf("Block.ParseBody: parsing of %%q not yet implemented", blk.Hdr.Code) 98 | } 99 | } else { 100 | // Parse based on SDNA index. 101 | typ := dna.Structs[index].Type 102 | switch typ { 103 | ` 104 | 105 | const midStruct = ` case "%s": 106 | if blk.Hdr.Count > 1 { 107 | // Parse block body structures. 108 | bodies := make([]*%s, blk.Hdr.Count) 109 | for i := range bodies { 110 | body := new(%s) 111 | err = binary.Read(r, order, body) 112 | if err != nil { 113 | return err 114 | } 115 | bodies[i] = body 116 | } 117 | blk.Body = bodies 118 | } else { 119 | // Parse block body structure. 120 | body := new(%s) 121 | err = binary.Read(r, order, body) 122 | if err != nil { 123 | return err 124 | } 125 | blk.Body = body 126 | } 127 | /// ### [ tmp ] ### 128 | // Verify that all bytes in the block body have been read. 129 | buf, err := ioutil.ReadAll(r) 130 | if err != nil { 131 | return err 132 | } 133 | if len(buf) > 0 { 134 | log.Printf("%%d unread bytes in %%q.", len(buf), typ) 135 | log.Printf("blk.Hdr: %%#v\n", blk.Hdr) 136 | log.Println(hex.Dump(buf)) 137 | os.Exit(1) 138 | } 139 | /// ### [/ tmp ] ### 140 | ` 141 | 142 | const postStruct = ` } 143 | } 144 | 145 | return nil 146 | } 147 | ` 148 | -------------------------------------------------------------------------------- /examples/blendview/blendview.go: -------------------------------------------------------------------------------- 1 | // blendview is a tool which extracts embedded thumbnails from blend files. 2 | package main 3 | 4 | import ( 5 | "encoding/binary" 6 | "flag" 7 | "fmt" 8 | "image" 9 | "image/color" 10 | "log" 11 | 12 | "github.com/mewkiz/pkg/imgutil" 13 | "github.com/mewkiz/pkg/pathutil" 14 | "github.com/mewspring/blend" 15 | "github.com/mewspring/blend/block" 16 | ) 17 | 18 | func main() { 19 | flag.Parse() 20 | for _, filePath := range flag.Args() { 21 | err := blendview(filePath) 22 | if err != nil { 23 | log.Fatalln(err) 24 | } 25 | } 26 | } 27 | 28 | // blendview parses the provided blend file and extract the embedded thumbnail. 29 | func blendview(filePath string) (err error) { 30 | // Parse blend file header and block headers. 31 | b, err := blend.Parse(filePath) 32 | if err != nil { 33 | return err 34 | } 35 | defer b.Close() 36 | 37 | // Parse DNA block. 38 | dna, err := b.GetDNA() 39 | if err != nil { 40 | log.Fatalln(err) 41 | } 42 | 43 | for _, blk := range b.Blocks { 44 | // Locate the TEST block. 45 | if blk.Hdr.Code != block.CodeTEST { 46 | continue 47 | } 48 | err = blk.ParseBody(b.Hdr.Order, dna) 49 | if err != nil { 50 | return err 51 | } 52 | buf, ok := blk.Body.([]byte) 53 | if !ok { 54 | return fmt.Errorf("blendview: invalid body type of %q block; expected []byte, got %T", blk.Hdr.Code, blk.Body) 55 | } 56 | t, err := NewThumb(buf) 57 | if err != nil { 58 | return err 59 | } 60 | thumbPath := pathutil.TrimExt(filePath) + ".png" 61 | err = imgutil.WriteFile(thumbPath, t) 62 | if err != nil { 63 | return err 64 | } 65 | fmt.Println("Created:", thumbPath) 66 | return nil 67 | } 68 | return fmt.Errorf("unable to locate %q block in %q", block.CodeTEST, filePath) 69 | } 70 | 71 | // Thumb is a thumbnail image based on the body of a TEST block. 72 | type Thumb struct { 73 | w, h int 74 | // Pixels are stored in backwards order with respect to normal image raster 75 | // scan order, starting in the lower right corner, going right to left, and 76 | // then row by row from the bottom to the top of the image. 77 | // 78 | // Each pixel is stored in RGBA order. 79 | pix []byte 80 | } 81 | 82 | // NewThumb returns an image.Image based on the body of a TEST block. 83 | func NewThumb(buf []byte) (t *Thumb, err error) { 84 | if len(buf) < 8 { 85 | return nil, fmt.Errorf("NewThumb: %q block body length (%d) below 8", block.CodeTEST, len(buf)) 86 | } 87 | t = &Thumb{ 88 | w: int(binary.LittleEndian.Uint32(buf)), 89 | h: int(binary.LittleEndian.Uint32(buf[4:])), 90 | pix: buf[8:], 91 | } 92 | // Verify length of pix buf. 93 | want := 4 * t.w * t.h 94 | if len(t.pix) != want { 95 | return nil, fmt.Errorf("NewThumb: invalid pix buf length; expected %d got %d", want, len(t.pix)) 96 | } 97 | return t, nil 98 | } 99 | 100 | // At returns the color of the pixel at (x, y). 101 | // At(Bounds().Min.X, Bounds().Min.Y) returns the upper-left pixel of the grid. 102 | // At(Bounds().Max.X-1, Bounds().Max.Y-1) returns the lower-right one. 103 | func (t *Thumb) At(x, y int) color.Color { 104 | // Pixels are stored in backwards order with respect to normal image raster 105 | // scan order, starting in the lower right corner, going right to left, and 106 | // then row by row from the bottom to the top of the image. 107 | off := (len(t.pix) - 4) 108 | off -= 4 * (x + y*t.w) 109 | // Each pixel is stored in RGBA order. 110 | r := t.pix[off] 111 | g := t.pix[off+1] 112 | b := t.pix[off+2] 113 | a := t.pix[off+3] 114 | return color.RGBA{r, g, b, a} 115 | } 116 | 117 | // Bounds returns the domain for which At can return non-zero color. 118 | // The bounds do not necessarily contain the point (0, 0). 119 | func (t *Thumb) Bounds() image.Rectangle { 120 | return image.Rect(0, 0, t.w, t.h) 121 | } 122 | 123 | // ColorModel returns the Image's color model. 124 | func (t *Thumb) ColorModel() color.Model { 125 | return color.RGBAModel 126 | } 127 | -------------------------------------------------------------------------------- /blend.go: -------------------------------------------------------------------------------- 1 | // Package blend implements parsing of Blender files. 2 | package blend 3 | 4 | import ( 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "strconv" 12 | 13 | "github.com/mewspring/blend/block" 14 | ) 15 | 16 | // Blend represents the information contained within a blend file. It contains a 17 | // file header and a slice of file blocks. 18 | type Blend struct { 19 | Hdr *Header 20 | Blocks []*block.Block 21 | // io.Closer of the underlying file. 22 | io.Closer 23 | } 24 | 25 | // Parse parsers the provided blend file. An *io.SectionReader is stored in each 26 | // individual block body. By default, blocks must be parsed manually using the 27 | // ParseBody method. Use ParseAll for a complete parsing of the blend file, 28 | // including all block bodies. 29 | // 30 | // Caller should close b when done reading from it. 31 | func Parse(filePath string) (b *Blend, err error) { 32 | b = new(Blend) 33 | f, err := os.Open(filePath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | b.Closer = f 38 | 39 | // Parse file header. 40 | b.Hdr, err = ParseHeader(f) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // Parse file blocks. 46 | for { 47 | blk, err := block.Parse(f, b.Hdr.Order, b.Hdr.PtrSize) 48 | if err != nil { 49 | return nil, err 50 | } 51 | if blk.Hdr.Code == block.CodeENDB { 52 | break 53 | } 54 | _, ok := block.Addr[blk.Hdr.OldAddr] 55 | if ok { 56 | return nil, fmt.Errorf("blend.Parse: multiple occurances of struct address %#x", blk.Hdr.OldAddr) 57 | } 58 | block.Addr[blk.Hdr.OldAddr] = blk 59 | 60 | b.Blocks = append(b.Blocks, blk) 61 | } 62 | 63 | return b, nil 64 | } 65 | 66 | // ParseAll parses the blend file and all block bodies. 67 | // 68 | // ParseAll closes b when done reading from it. 69 | func ParseAll(filePath string) (b *Blend, err error) { 70 | // Parse file header and block headers. 71 | b, err = Parse(filePath) 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer b.Close() 76 | 77 | // Parse DNA block. 78 | dna, err := b.GetDNA() 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | // Parse block bodies. 84 | for _, blk := range b.Blocks { 85 | blk.ParseBody(b.Hdr.Order, dna) 86 | } 87 | 88 | return b, nil 89 | } 90 | 91 | // GetDNA locates, parses and returns the DNA block. 92 | func (b *Blend) GetDNA() (dna *block.DNA, err error) { 93 | for _, blk := range b.Blocks { 94 | dna, ok := blk.Body.(*block.DNA) 95 | if ok { 96 | // DNA block already parsed. 97 | return dna, nil 98 | } 99 | if blk.Hdr.Code == block.CodeDNA1 { 100 | // Parse the DNA block body and store it in blk.Body. 101 | r, ok := blk.Body.(io.Reader) 102 | if !ok { 103 | return nil, errors.New("Blend.GetDNA: unable to locate DNA block body reader") 104 | } 105 | dna, err = block.ParseDNA(r, b.Hdr.Order) 106 | if err != nil { 107 | return nil, err 108 | } 109 | blk.Body = dna 110 | return dna, nil 111 | } 112 | } 113 | return nil, errors.New("Blend.GetDNA: unable to locate DNA block") 114 | } 115 | 116 | // A Header is present at the beginning of each blend file. 117 | type Header struct { 118 | // Pointer size. 119 | PtrSize int 120 | // Byte order. 121 | Order binary.ByteOrder 122 | // Blender version. 123 | Ver int 124 | } 125 | 126 | // ParseHeader parses and returns the header of a blend file. 127 | // 128 | // Example file header: 129 | // "BLENDER_V100" 130 | // // 0-6 magic ("BLENDER") 131 | // // 7 pointer size ("_" or "-") 132 | // // 8 endianness ("V" or "v") 133 | // // 9-11 version ("100") 134 | func ParseHeader(r io.Reader) (hdr *Header, err error) { 135 | buf := make([]byte, 12) 136 | _, err = io.ReadFull(r, buf) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | // File identifier. 142 | magic := string(buf[0:7]) 143 | if magic != "BLENDER" { 144 | return nil, fmt.Errorf("blend.ParseHeader: invalid file identifier %q", magic) 145 | } 146 | 147 | // Pointer size. 148 | hdr = new(Header) 149 | size := buf[7] 150 | switch size { 151 | case '_': 152 | // _ = 4 byte pointer 153 | hdr.PtrSize = 4 154 | case '-': 155 | // - = 8 byte pointer 156 | hdr.PtrSize = 8 157 | default: 158 | return nil, fmt.Errorf("blend.ParseHeader: invalid pointer size character '%c'", size) 159 | } 160 | 161 | // Byte order. 162 | order := buf[8] 163 | switch order { 164 | case 'v': 165 | // v = little endian 166 | hdr.Order = binary.LittleEndian 167 | case 'V': 168 | // V = big endian 169 | hdr.Order = binary.BigEndian 170 | default: 171 | return nil, fmt.Errorf("blend.ParseHeader: invalid byte order character '%c'", order) 172 | } 173 | 174 | // Version. 175 | hdr.Ver, err = strconv.Atoi(string(buf[9:12])) 176 | if err != nil { 177 | return nil, fmt.Errorf("blend.ParseHeader: invalid version; %s", err) 178 | } 179 | if WarnVersion { 180 | if hdr.Ver != block.BlenderVer { 181 | log.Printf("Warning: Version mismatch between file (v%d) and block package (v%d).\n", hdr.Ver, block.BlenderVer) 182 | if hdr.Ver < block.BlenderVer { 183 | log.Println("The file's Blender version is too old. Use Blender [1] to open and resave the file.") 184 | log.Println("[1]: http://www.blender.org/") 185 | } else { 186 | log.Println("The block package Blender version is too old. Use blendef [1] to regenerate the block package.") 187 | log.Println("[1]: go get github.com/mewspring/blend/cmd/blendef") 188 | } 189 | } 190 | } 191 | 192 | return hdr, nil 193 | } 194 | 195 | // When WarnVersion is set to true, ParseHeader will report warnings if the 196 | // Blender version of the blend file and the block package differs. 197 | var WarnVersion = true 198 | -------------------------------------------------------------------------------- /block/dna.go: -------------------------------------------------------------------------------- 1 | package block 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | ) 10 | 11 | // DNA stores information about the various structures contained within a blend 12 | // file. 13 | type DNA struct { 14 | // Names is a slice which contains the name of each structure field. It can 15 | // be accessed with the name index. 16 | Names []string 17 | // Types is a slice which contains the name of each type. It can be accessed 18 | // with the type index. 19 | Types []string 20 | // TypeSizes is a slice which contains the size of each type. It can be 21 | // accessed with the type index. 22 | TypeSizes []int 23 | // Structs is a slice which contains structure definitions. The SDNA index 24 | // can be used to access individual structures. 25 | Structs []DNAStruct 26 | } 27 | 28 | // DNAStruct stores information about a structure. 29 | type DNAStruct struct { 30 | // Type is the type name of the structure. 31 | Type string 32 | // Fields is a slice which contains the structure's field definitions. 33 | Fields []DNAField 34 | } 35 | 36 | // DNAField stores information about a structure field. 37 | type DNAField struct { 38 | // Type is the type name of the field. 39 | Type string 40 | // Name contains information about the field's name, pointer count and array 41 | // and function information. 42 | // 43 | // Below are a couple of examples: 44 | // "id_type" 45 | // "**links" 46 | // "*point_cache[2]" 47 | // "clip[6][4]" 48 | // "(*free_edit)()" 49 | Name string 50 | } 51 | 52 | // ParseDNA parses and returns the body of the "DNA1" block. 53 | func ParseDNA(r io.Reader, order binary.ByteOrder) (body *DNA, err error) { 54 | br := bufio.NewReader(r) 55 | rawID := make([]byte, 4) 56 | 57 | // Identifier. 58 | _, err = io.ReadFull(br, rawID) 59 | if err != nil { 60 | return nil, err 61 | } 62 | id := string(rawID) 63 | if id != "SDNA" { 64 | return nil, fmt.Errorf("block.ParseDNA: invalid identifier %q", id) 65 | } 66 | 67 | // Name identifier. 68 | _, err = io.ReadFull(br, rawID) 69 | if err != nil { 70 | return nil, err 71 | } 72 | nameID := string(rawID) 73 | if nameID != "NAME" { 74 | return nil, fmt.Errorf("block.ParseDNA: invalid name identifier %q", nameID) 75 | } 76 | 77 | // Name count. 78 | var x32 int32 79 | err = binary.Read(br, order, &x32) 80 | if err != nil { 81 | return nil, err 82 | } 83 | nameCount := int(x32) 84 | 85 | // Names. 86 | body = new(DNA) 87 | body.Names = make([]string, nameCount) 88 | var total int 89 | for i := range body.Names { 90 | buf, err := br.ReadSlice(0x00) 91 | if err != nil { 92 | return nil, err 93 | } 94 | total += len(buf) 95 | body.Names[i] = string(buf[:len(buf)-1]) 96 | } 97 | err = align(br, total, 4) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | // Type identifier. 103 | _, err = io.ReadFull(br, rawID) 104 | if err != nil { 105 | return nil, err 106 | } 107 | typeID := string(rawID) 108 | if typeID != "TYPE" { 109 | return nil, fmt.Errorf("block.ParseDNA: invalid type identifier %q", typeID) 110 | } 111 | 112 | // Type count. 113 | err = binary.Read(br, order, &x32) 114 | if err != nil { 115 | return nil, err 116 | } 117 | typeCount := int(x32) 118 | 119 | // Types. 120 | body.Types = make([]string, typeCount) 121 | total = 0 122 | for i := range body.Types { 123 | buf, err := br.ReadSlice(0x00) 124 | if err != nil { 125 | return nil, err 126 | } 127 | total += len(buf) 128 | body.Types[i] = string(buf[:len(buf)-1]) 129 | } 130 | err = align(br, total, 4) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | // Type length identifier. 136 | _, err = io.ReadFull(br, rawID) 137 | if err != nil { 138 | return nil, err 139 | } 140 | lenID := string(rawID) 141 | if lenID != "TLEN" { 142 | return nil, fmt.Errorf("block.ParseDNA: invalid type length identifier %q", lenID) 143 | } 144 | 145 | // Type sizes. 146 | var x16 int16 147 | body.TypeSizes = make([]int, typeCount) 148 | total = 0 149 | for i := range body.TypeSizes { 150 | err = binary.Read(br, order, &x16) 151 | if err != nil { 152 | return nil, err 153 | } 154 | total += 2 155 | body.TypeSizes[i] = int(x16) 156 | } 157 | err = align(br, total, 4) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | // Structure identifier. 163 | _, err = io.ReadFull(br, rawID) 164 | if err != nil { 165 | return nil, err 166 | } 167 | structID := string(rawID) 168 | if structID != "STRC" { 169 | return nil, fmt.Errorf("block.ParseDNA: invalid structure identifier %q.", structID) 170 | } 171 | 172 | // Structure count. 173 | err = binary.Read(br, order, &x32) 174 | if err != nil { 175 | return nil, err 176 | } 177 | structCount := int(x32) 178 | 179 | // Structures. 180 | body.Structs = make([]DNAStruct, structCount) 181 | for i := range body.Structs { 182 | // Structure type. 183 | err = binary.Read(br, order, &x16) 184 | if err != nil { 185 | return nil, err 186 | } 187 | body.Structs[i].Type = body.Types[x16] 188 | 189 | // Field count. 190 | err = binary.Read(br, order, &x16) 191 | if err != nil { 192 | return nil, err 193 | } 194 | fieldCount := int(x16) 195 | body.Structs[i].Fields = make([]DNAField, fieldCount) 196 | 197 | // Fields. 198 | for j := range body.Structs[i].Fields { 199 | // Field type. 200 | err = binary.Read(br, order, &x16) 201 | if err != nil { 202 | return nil, err 203 | } 204 | body.Structs[i].Fields[j].Type = body.Types[x16] 205 | 206 | // Field name. 207 | err = binary.Read(br, order, &x16) 208 | if err != nil { 209 | return nil, err 210 | } 211 | body.Structs[i].Fields[j].Name = body.Names[x16] 212 | } 213 | } 214 | 215 | return body, nil 216 | } 217 | 218 | // align advances the reader so that it is aligned with n, were total 219 | // corresponds to the number of bytes read so far. 220 | func align(r io.Reader, total int, n int) (err error) { 221 | i := total % n 222 | if i > 0 { 223 | _, err = io.CopyN(ioutil.Discard, r, int64(n-i)) 224 | if err != nil { 225 | return err 226 | } 227 | } 228 | return nil 229 | } 230 | -------------------------------------------------------------------------------- /block/block.go: -------------------------------------------------------------------------------- 1 | // Package block implements parsing of blend file blocks. 2 | // 3 | // One unique feature of blend files is that they contain a full definition of 4 | // every structure used in its file blocks. The structure definitions are stored 5 | // in the DNA block. 6 | // 7 | // All block structure definitions ("struct.go") and the block parsing logic 8 | // ("parse.go") have been generating by parsing the DNA block of 9 | // "testdata/block.blend". 10 | // 11 | // The tool which was used to generate these two files is available through: 12 | // go get github.com/mewspring/blend/cmd/blendef 13 | // 14 | // More complex blend files may contain structures which are not yet defined in 15 | // this package. If so, use blendef to regenerate "struct.go" and "parse.go" for 16 | // the given blend file. 17 | package block 18 | 19 | import ( 20 | "encoding/binary" 21 | "io" 22 | "log" 23 | "os" 24 | ) 25 | 26 | // A Block contains a header and a type dependent body. 27 | type Block struct { 28 | Hdr *Header 29 | // Body contains an *io.SectionReader which is later replaced with a concrete 30 | // block type after it has been parsed. 31 | Body interface{} 32 | } 33 | 34 | // Parse parses and returns a file block. 35 | func Parse(f *os.File, order binary.ByteOrder, ptrSize int) (blk *Block, err error) { 36 | // Parse block header. 37 | blk = new(Block) 38 | blk.Hdr, err = ParseHeader(f, order, ptrSize) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | // Store section reader for block body. 44 | off, err := f.Seek(blk.Hdr.Size, os.SEEK_CUR) 45 | if err != nil { 46 | return nil, err 47 | } 48 | blk.Body = io.NewSectionReader(f, off-blk.Hdr.Size, blk.Hdr.Size) 49 | 50 | return blk, nil 51 | } 52 | 53 | // Header contains information about the block's type and size. 54 | type Header struct { 55 | // Code provides a rough type description of the block. 56 | Code BlockCode 57 | // Total length of the data after the block header. 58 | Size int64 59 | // Memory address of the structure when it was written to disk. 60 | OldAddr uint64 61 | // Index in the Structure DNA. 62 | SDNAIndex int 63 | // Number of structures located in this block. 64 | Count int 65 | } 66 | 67 | // ParseHeader parses and returns a file block header. 68 | // 69 | // Example file block header: 70 | // 44 41 54 41 E0 00 00 00 88 5E 9D 04 00 00 00 00 DATA.....^...... 71 | // F8 00 00 00 0E 00 00 00 ........ 72 | // 73 | // // 0-3 block code ("DATA") 74 | // // 4-7 size (0x000000E0 = 224) 75 | // // 8-15 old addr (0x00000000049D5E88) // size depends on PtrSize. 76 | // // 16-19 sdna index (0x000000F8 = 248) 77 | // // 20-23 count (0x0000000E = 14) 78 | func ParseHeader(r io.Reader, order binary.ByteOrder, ptrSize int) (hdr *Header, err error) { 79 | // Block code. 80 | buf := make([]byte, 4) 81 | _, err = io.ReadFull(r, buf) 82 | if err != nil { 83 | return nil, err 84 | } 85 | hdr = new(Header) 86 | code := string(buf) 87 | switch code { 88 | case "AR\x00\x00": 89 | hdr.Code = CodeAR 90 | case "BR\x00\x00": 91 | hdr.Code = CodeBR 92 | case "CA\x00\x00": 93 | hdr.Code = CodeCA 94 | case "DATA": 95 | hdr.Code = CodeDATA 96 | case "DNA1": 97 | hdr.Code = CodeDNA1 98 | case "ENDB": 99 | hdr.Code = CodeENDB 100 | case "GLOB": 101 | hdr.Code = CodeGLOB 102 | case "IM\x00\x00": 103 | hdr.Code = CodeIM 104 | case "LA\x00\x00": 105 | hdr.Code = CodeLA 106 | case "LS\x00\x00": 107 | hdr.Code = CodeLS 108 | case "MA\x00\x00": 109 | hdr.Code = CodeMA 110 | case "ME\x00\x00": 111 | hdr.Code = CodeME 112 | case "OB\x00\x00": 113 | hdr.Code = CodeOB 114 | case "REND": 115 | hdr.Code = CodeREND 116 | case "SC\x00\x00": 117 | hdr.Code = CodeSC 118 | case "SN\x00\x00": 119 | hdr.Code = CodeSN 120 | case "SR\x00\x00": 121 | hdr.Code = CodeSR 122 | case "TE\x00\x00": 123 | hdr.Code = CodeTE 124 | case "TEST": 125 | hdr.Code = CodeTEST 126 | case "TX\x00\x00": 127 | hdr.Code = CodeTX 128 | case "WM\x00\x00": 129 | hdr.Code = CodeWM 130 | case "WO\x00\x00": 131 | hdr.Code = CodeWO 132 | default: 133 | log.Printf("Header.ParseHeader: block code %q not yet implemented.\n", code) 134 | hdr.Code = CodeUnknown 135 | } 136 | 137 | // Block size. 138 | var x int32 139 | err = binary.Read(r, order, &x) 140 | if err != nil { 141 | return nil, err 142 | } 143 | hdr.Size = int64(x) 144 | 145 | // Old memory address. 146 | switch ptrSize { 147 | case 4: 148 | var x uint32 149 | err = binary.Read(r, order, &x) 150 | if err != nil { 151 | return nil, err 152 | } 153 | hdr.OldAddr = uint64(x) 154 | case 8: 155 | var x uint64 156 | err = binary.Read(r, order, &x) 157 | if err != nil { 158 | return nil, err 159 | } 160 | hdr.OldAddr = x 161 | } 162 | 163 | // SDNA index. 164 | err = binary.Read(r, order, &x) 165 | if err != nil { 166 | return nil, err 167 | } 168 | hdr.SDNAIndex = int(x) 169 | 170 | // Structure count. 171 | err = binary.Read(r, order, &x) 172 | if err != nil { 173 | return nil, err 174 | } 175 | hdr.Count = int(x) 176 | 177 | return hdr, nil 178 | } 179 | 180 | // BlockCode represents a rough type description of a block. 181 | type BlockCode int 182 | 183 | func (typ BlockCode) String() string { 184 | var m = map[BlockCode]string{ 185 | CodeAR: "AR", 186 | CodeBR: "BR", 187 | CodeCA: "CA", 188 | CodeDATA: "DATA", 189 | CodeDNA1: "DNA1", 190 | CodeENDB: "ENDB", 191 | CodeGLOB: "GLOB", 192 | CodeIM: "IM", 193 | CodeLA: "LA", 194 | CodeLS: "LS", 195 | CodeMA: "MA", 196 | CodeME: "ME", 197 | CodeOB: "OB", 198 | CodeREND: "REND", 199 | CodeSC: "SC", 200 | CodeSN: "SN", 201 | CodeSR: "SR", 202 | CodeTE: "TE", 203 | CodeTEST: "TEST", 204 | CodeTX: "TX", 205 | CodeWM: "WM", 206 | CodeWO: "WO", 207 | } 208 | s, ok := m[typ] 209 | if !ok { 210 | return "" 211 | } 212 | return s 213 | } 214 | 215 | // Block codes. 216 | const ( 217 | CodeAR BlockCode = iota 218 | CodeBR 219 | CodeCA 220 | CodeDATA 221 | CodeDNA1 222 | CodeENDB 223 | CodeGLOB 224 | CodeIM 225 | CodeLA 226 | CodeLS 227 | CodeMA 228 | CodeME 229 | CodeOB 230 | CodeREND 231 | CodeSC 232 | CodeSN 233 | CodeSR 234 | CodeTE 235 | CodeTEST 236 | CodeTX 237 | CodeWM 238 | CodeWO 239 | 240 | CodeUnknown BlockCode = -1 241 | ) 242 | -------------------------------------------------------------------------------- /cmd/blendef/gen_struct.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/mewspring/blend" 11 | "github.com/mewspring/blend/block" 12 | ) 13 | 14 | // predef maps C types to untyped Go types. 15 | var predef = map[string]string{ 16 | // int types. 17 | "char": "int", 18 | "short": "int", 19 | "int": "int", 20 | "long": "int", 21 | "int64_t": "int", 22 | 23 | // uint types. 24 | "uchar": "uint", 25 | "ushort": "uint", 26 | "ulong": "uint", 27 | "uint64_t": "uint", 28 | 29 | // float types. 30 | "float": "float", 31 | "double": "float", 32 | 33 | // empty struct types. 34 | "void": "struct{}", 35 | } 36 | 37 | /// TODO: use text/template. 38 | 39 | // genStruct generates Go structure definitions by parsing the DNA data. 40 | // 41 | // The output is stored in "struct.go". 42 | func genStruct(b *blend.Blend, dna *block.DNA) (err error) { 43 | f, err := os.Create("struct.go") 44 | if err != nil { 45 | return err 46 | } 47 | defer f.Close() 48 | 49 | // Map type sizes. 50 | size := make(map[string]int) 51 | for i, typ := range dna.Types { 52 | size[typ] = dna.TypeSizes[i] 53 | } 54 | 55 | // Map basic type definitions. 56 | basic := make(map[string]string) 57 | for _, typ := range dna.Types { 58 | def, ok := predef[typ] 59 | if ok { 60 | n := size[typ] 61 | switch n { 62 | case 0: 63 | basic[typ] = def 64 | case 1: 65 | basic[typ] = def + "8" 66 | case 2: 67 | basic[typ] = def + "16" 68 | case 4: 69 | basic[typ] = def + "32" 70 | case 8: 71 | basic[typ] = def + "64" 72 | default: 73 | return fmt.Errorf("genStruct: size %d of basic type %q not supported", n, typ) 74 | } 75 | } 76 | } 77 | 78 | // Generate Go pointer definition. 79 | fmt.Fprintf(f, "// NOTE: this file has been automatically generated by blendef for Blender v%d.\n", b.Hdr.Ver) 80 | fmt.Fprintln(f) 81 | fmt.Fprintln(f, "package block") 82 | fmt.Fprintln(f) 83 | fmt.Fprintln(f, "import (") 84 | fmt.Fprintln(f, ` "fmt"`) 85 | fmt.Fprintln(f, ")") 86 | fmt.Fprintln(f) 87 | fmt.Fprintln(f, `// BlenderVer is the version of Blender used when generating the files`) 88 | fmt.Fprintln(f, `// "parse.go" and "struct.go" of this package. Use blendef to regenerate these`) 89 | fmt.Fprintln(f, `// files if this version differs from the blend file's version.`) 90 | fmt.Fprintf(f, "const BlenderVer = %d\n", b.Hdr.Ver) 91 | fmt.Fprintln(f) 92 | fmt.Fprintln(f, "// Pointer is the memory address of a structure when it was written to disk.") 93 | switch b.Hdr.PtrSize { 94 | case 4: 95 | fmt.Fprintln(f, "type Pointer uint32") 96 | case 8: 97 | fmt.Fprintln(f, "type Pointer uint64") 98 | } 99 | fmt.Fprintln(f) 100 | fmt.Fprintln(f, pointerCode) 101 | 102 | // Generate Go structure definitions. 103 | for index, st := range dna.Structs { 104 | fmt.Fprintln(f, "// SDNA index:", index) 105 | fmt.Fprintf(f, "type %s struct {\n", strings.Title(st.Type)) 106 | for _, field := range st.Fields { 107 | // Parse and capitalize field name. 108 | name, isFunc, ptrCount, arraySizes, err := parseName(field.Name) 109 | if err != nil { 110 | return err 111 | } 112 | name = strings.Title(name) 113 | if strings.HasPrefix(name, "_") { 114 | // Somewhat ugly fix for the following binary.Read error: 115 | // "reflect: reflect.Value.SetInt using value obtained using unexported field" 116 | name = "X" + name 117 | } 118 | 119 | typ := field.Type 120 | def, ok := basic[typ] 121 | if ok { 122 | // Use Go basic type names. 123 | typ = def 124 | } else { 125 | // Capitalize non-basic type names. 126 | typ = strings.Title(typ) 127 | } 128 | 129 | if isFunc { 130 | if field.Type == "void" { 131 | fmt.Fprintf(f, "\t%s func()\n", name) 132 | } else { 133 | fmt.Fprintf(f, "\t%s func() %s\n", name, typ) 134 | } 135 | } else { 136 | array := new(bytes.Buffer) 137 | for _, arraySize := range arraySizes { 138 | fmt.Fprintf(array, "[%d]", arraySize) 139 | } 140 | if ptrCount > 0 { 141 | ptr := strings.Repeat("*", ptrCount) 142 | fmt.Fprintf(f, "\t%s %sPointer // %s%s%s\n", name, array, array, ptr, typ) 143 | } else { 144 | fmt.Fprintf(f, "\t%s %s%s\n", name, array, typ) 145 | } 146 | } 147 | } 148 | fmt.Fprintln(f, "}") 149 | fmt.Fprintln(f) 150 | } 151 | 152 | // Map of structure type names. 153 | structs := make(map[string]bool) 154 | for _, st := range dna.Structs { 155 | structs[st.Type] = true 156 | } 157 | 158 | // Generate empty Go structure definitions. 159 | for _, typ := range dna.Types { 160 | _, ok := basic[typ] 161 | if ok { 162 | continue 163 | } 164 | _, ok = structs[typ] 165 | if ok { 166 | continue 167 | } 168 | fmt.Fprintf(f, "type %s struct{}\n", strings.Title(typ)) 169 | } 170 | 171 | return nil 172 | } 173 | 174 | // parseName parses the provided string and extracts name, pointer count and 175 | // array and function information. 176 | // 177 | // Example input strings: 178 | // "id_type" 179 | // "**links" 180 | // "*point_cache[2]" 181 | // "clip[6][4]" 182 | // "(*free_edit)()" 183 | func parseName(s string) (name string, isFunc bool, ptrCount int, arraySizes []int, err error) { 184 | // Parse function pointer. 185 | if len(s) > 1 && s[0] == '(' && s[1] == '*' { 186 | p := s[2:] 187 | end := strings.Index(p, ")") 188 | if end == -1 { 189 | return "", false, 0, nil, fmt.Errorf("parseName: unmatched opening parenthesis in %q", s) 190 | } 191 | name = p[:end] 192 | return name, true, 0, nil, nil 193 | } 194 | 195 | // Parse pointer count. 196 | p := s 197 | for i := 0; i < len(p); i++ { 198 | if p[i] != '*' { 199 | p = p[i:] 200 | break 201 | } 202 | ptrCount++ 203 | } 204 | 205 | // Parse name. 206 | pos := strings.Index(p, "[") 207 | if pos == -1 { 208 | return p, false, ptrCount, nil, nil 209 | } 210 | name = p[:pos] 211 | p = p[pos:] 212 | 213 | // Parse array sizes. 214 | for { 215 | // Get start position. 216 | pos := strings.Index(p, "[") 217 | if pos == -1 { 218 | return name, false, ptrCount, arraySizes, nil 219 | } 220 | p = p[pos+1:] 221 | 222 | // Get end position. 223 | end := strings.Index(p, "]") 224 | if end == -1 { 225 | return "", false, 0, nil, fmt.Errorf("parseName: unmatched opening bracket in %q", s) 226 | } 227 | num := p[:end] 228 | p = p[end+1:] 229 | 230 | arraySize, err := strconv.Atoi(num) 231 | if err != nil { 232 | return "", false, 0, nil, err 233 | } 234 | arraySizes = append(arraySizes, arraySize) 235 | } 236 | } 237 | 238 | const pointerCode = `// Addr is a map from the memory address of a structure (when it was written to 239 | // disk) to its file block. 240 | var Addr = make(map[uint64]*Block) 241 | 242 | // Data translates the memory address into a usable pointer and returns it. 243 | func (addr Pointer) Data() (data interface{}, err error) { 244 | blk, ok := Addr[uint64(addr)] 245 | if !ok { 246 | return nil, fmt.Errorf("Pointer.Data: unable to locate data for pointer %#x", addr) 247 | } 248 | return blk.Body, nil 249 | } 250 | ` 251 | --------------------------------------------------------------------------------