├── .gitignore ├── LICENSE ├── README.md ├── cdb.go ├── cdb_test.go ├── cdbdump └── cdbdump.go ├── cdbmake └── cdbmake.go ├── dump.go ├── go.mod ├── hash.go └── make.go /.gitignore: -------------------------------------------------------------------------------- 1 | _* 2 | *.6 3 | 6.out 4 | *.8 5 | 8.out 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, John E. Barham 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cdb 2 | 3 | This is a [Go](http://golang.org/) package to read and write cdb ("constant database") files. 4 | 5 | The cdb file format is a machine-independent format with the following features: 6 | 7 | - *Fast lookups:* A successful lookup in a large database normally takes just two disk accesses. An unsuccessful lookup takes only one. 8 | - *Low overhead:* A database uses 2048 bytes, plus 24 bytes per record, plus the space for keys and data. 9 | - *No random limits:* cdb can handle any database up to 4 gigabytes. There are no other restrictions; records don't even have to fit into memory. 10 | 11 | See the original cdb specification and C implementation by D. J. Bernstein 12 | at http://cr.yp.to/cdb.html. 13 | 14 | ## Installation 15 | 16 | Assuming you have a working Go environment, installation is simply: 17 | 18 | go get github.com/jbarham/cdb 19 | 20 | The package documentation can be viewed online at 21 | https://pkg.go.dev/github.com/jbarham/cdb 22 | or on the command line by running `go doc github.com/jbarham/cdb`. 23 | 24 | The included self-test program `cdb_test.go` illustrates usage of the package. 25 | 26 | ## Utilities 27 | 28 | The cdb package includes ports of the programs `cdbdump` and `cdbmake` from 29 | the [original implementation](http://cr.yp.to/cdb/cdbmake.html). -------------------------------------------------------------------------------- /cdb.go: -------------------------------------------------------------------------------- 1 | // Package cdb reads and writes cdb ("constant database") files. 2 | // 3 | // See the original cdb specification and C implementation by D. J. Bernstein 4 | // at http://cr.yp.to/cdb.html. 5 | package cdb 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "io" 11 | "os" 12 | "runtime" 13 | ) 14 | 15 | const ( 16 | headerSize = uint32(256 * 8) 17 | ) 18 | 19 | type Cdb struct { 20 | r io.ReaderAt 21 | closer io.Closer 22 | buf []byte 23 | loop uint32 // number of hash slots searched under this key 24 | khash uint32 // initialized if loop is nonzero 25 | kpos uint32 // initialized if loop is nonzero 26 | hpos uint32 // initialized if loop is nonzero 27 | hslots uint32 // initialized if loop is nonzero 28 | dpos uint32 // initialized if FindNext() returns true 29 | dlen uint32 // initialized if FindNext() returns true 30 | } 31 | 32 | // Open opens the named file read-only and returns a new Cdb object. The file 33 | // should exist and be a cdb-format database file. 34 | func Open(name string) (*Cdb, error) { 35 | f, err := os.Open(name) 36 | if err != nil { 37 | return nil, err 38 | } 39 | c := New(f) 40 | c.closer = f 41 | runtime.SetFinalizer(c, (*Cdb).Close) 42 | return c, nil 43 | } 44 | 45 | // Close closes the cdb for any further reads. 46 | func (c *Cdb) Close() (err error) { 47 | if c.closer != nil { 48 | err = c.closer.Close() 49 | c.closer = nil 50 | runtime.SetFinalizer(c, nil) 51 | } 52 | return err 53 | } 54 | 55 | // New creates a new Cdb from the given ReaderAt, which should be a cdb format database. 56 | func New(r io.ReaderAt) *Cdb { 57 | c := new(Cdb) 58 | c.r = r 59 | c.buf = make([]byte, 64) 60 | return c 61 | } 62 | 63 | // Data returns the first data value for the given key. 64 | // If no such record exists, it returns EOF. 65 | func (c *Cdb) Data(key []byte) (data []byte, err error) { 66 | c.FindStart() 67 | if err = c.find(key); err != nil { 68 | return nil, err 69 | } 70 | 71 | data = make([]byte, c.dlen) 72 | err = c.read(data, c.dpos) 73 | 74 | return 75 | } 76 | 77 | // FindStart resets the cdb to search for the first record under a new key. 78 | func (c *Cdb) FindStart() { c.loop = 0 } 79 | 80 | // FindNext returns the next data value for the given key as a SectionReader. 81 | // If there are no more records for the given key, it returns EOF. 82 | // FindNext acts as an iterator: The iteration should be initialized by calling 83 | // FindStart and all subsequent calls to FindNext should use the same key value. 84 | func (c *Cdb) FindNext(key []byte) (rdata *io.SectionReader, err error) { 85 | if err := c.find(key); err != nil { 86 | return nil, err 87 | } 88 | return io.NewSectionReader(c.r, int64(c.dpos), int64(c.dlen)), nil 89 | } 90 | 91 | // Find returns the first data value for the given key as a SectionReader. 92 | // Find is the same as FindStart followed by FindNext. 93 | func (c *Cdb) Find(key []byte) (rdata *io.SectionReader, err error) { 94 | c.FindStart() 95 | return c.FindNext(key) 96 | } 97 | 98 | func (c *Cdb) find(key []byte) (err error) { 99 | defer func() { 100 | if e := recover(); e != nil { 101 | err = e.(error) 102 | } 103 | }() 104 | 105 | var pos, h uint32 106 | 107 | klen := uint32(len(key)) 108 | if c.loop == 0 { 109 | h = checksum(key) 110 | c.hpos, c.hslots = c.readNums((h << 3) & 2047) 111 | if c.hslots == 0 { 112 | return io.EOF 113 | } 114 | c.khash = h 115 | h >>= 8 116 | h %= c.hslots 117 | h <<= 3 118 | c.kpos = c.hpos + h 119 | } 120 | 121 | for c.loop < c.hslots { 122 | h, pos = c.readNums(c.kpos) 123 | if pos == 0 { 124 | return io.EOF 125 | } 126 | c.loop++ 127 | c.kpos += 8 128 | if c.kpos == c.hpos+(c.hslots<<3) { 129 | c.kpos = c.hpos 130 | } 131 | if h == c.khash { 132 | rklen, rdlen := c.readNums(pos) 133 | if rklen == klen { 134 | if c.match(key, pos+8) { 135 | c.dlen = rdlen 136 | c.dpos = pos + 8 + klen 137 | return nil 138 | } 139 | } 140 | } 141 | } 142 | 143 | return io.EOF 144 | } 145 | 146 | func (c *Cdb) read(buf []byte, pos uint32) error { 147 | _, err := c.r.ReadAt(buf, int64(pos)) 148 | return err 149 | } 150 | 151 | func (c *Cdb) match(key []byte, pos uint32) bool { 152 | buf := c.buf 153 | klen := len(key) 154 | for n := 0; n < klen; n += len(buf) { 155 | nleft := klen - n 156 | if len(buf) > nleft { 157 | buf = buf[:nleft] 158 | } 159 | if err := c.read(buf, pos); err != nil { 160 | panic(err) 161 | } 162 | if !bytes.Equal(buf, key[n:n+len(buf)]) { 163 | return false 164 | } 165 | pos += uint32(len(buf)) 166 | } 167 | return true 168 | } 169 | 170 | func (c *Cdb) readNums(pos uint32) (uint32, uint32) { 171 | if _, err := c.r.ReadAt(c.buf[:8], int64(pos)); err != nil { 172 | panic(err) 173 | } 174 | return binary.LittleEndian.Uint32(c.buf), binary.LittleEndian.Uint32(c.buf[4:]) 175 | } 176 | -------------------------------------------------------------------------------- /cdb_test.go: -------------------------------------------------------------------------------- 1 | package cdb 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | type rec struct { 14 | key string 15 | values []string 16 | } 17 | 18 | var records = []rec{ 19 | {"one", []string{"1"}}, 20 | {"two", []string{"2", "22"}}, 21 | {"three", []string{"3", "33", "333"}}, 22 | } 23 | 24 | var data []byte // set by init() 25 | 26 | func TestCdb(t *testing.T) { 27 | tmp, err := ioutil.TempFile("", "") 28 | if err != nil { 29 | t.Fatalf("Failed to create temp file: %s", err) 30 | } 31 | 32 | defer os.Remove(tmp.Name()) 33 | 34 | // Test Make 35 | err = Make(tmp, bytes.NewBuffer(data)) 36 | if err != nil { 37 | t.Fatalf("Make failed: %s", err) 38 | } 39 | 40 | // Test reading records 41 | c, err := Open(tmp.Name()) 42 | if err != nil { 43 | t.Fatalf("Error opening %s: %s", tmp.Name(), err) 44 | } 45 | 46 | _, err = c.Data([]byte("does not exist")) 47 | if err != io.EOF { 48 | t.Fatalf("non-existent key should return io.EOF") 49 | } 50 | 51 | for _, rec := range records { 52 | key := []byte(rec.key) 53 | values := rec.values 54 | 55 | v, err := c.Data(key) 56 | if err != nil { 57 | t.Fatalf("Record read failed: %s", err) 58 | } 59 | 60 | if !bytes.Equal(v, []byte(values[0])) { 61 | t.Fatal("Incorrect value returned") 62 | } 63 | 64 | c.FindStart() 65 | for _, value := range values { 66 | sr, err := c.FindNext(key) 67 | if err != nil { 68 | t.Fatalf("Record read failed: %s", err) 69 | } 70 | 71 | data := make([]byte, sr.Size()) 72 | _, err = sr.Read(data) 73 | if err != nil { 74 | t.Fatalf("Record read failed: %s", err) 75 | } 76 | 77 | if !bytes.Equal(data, []byte(value)) { 78 | t.Fatal("value mismatch") 79 | } 80 | } 81 | // Read all values, so should get EOF 82 | _, err = c.FindNext(key) 83 | if err != io.EOF { 84 | t.Fatalf("Expected EOF, got %s", err) 85 | } 86 | } 87 | 88 | // Test Dump 89 | if _, err = tmp.Seek(0, 0); err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | buf := bytes.NewBuffer(nil) 94 | err = Dump(buf, tmp) 95 | if err != nil { 96 | t.Fatalf("Dump failed: %s", err) 97 | } 98 | 99 | if !bytes.Equal(buf.Bytes(), data) { 100 | t.Fatalf("Dump round-trip failed") 101 | } 102 | } 103 | 104 | func TestEmptyFile(t *testing.T) { 105 | tmp, err := ioutil.TempFile("", "") 106 | if err != nil { 107 | t.Fatalf("Failed to create temp file: %s", err) 108 | } 109 | 110 | defer os.Remove(tmp.Name()) 111 | 112 | // Test Make 113 | err = Make(tmp, bytes.NewBuffer([]byte("\n\n"))) 114 | if err != nil { 115 | t.Fatalf("Make failed: %s", err) 116 | } 117 | 118 | // Check that all tables are length 0 119 | if _, err = tmp.Seek(0, 0); err != nil { 120 | t.Fatal(err) 121 | } 122 | rb := bufio.NewReader(tmp) 123 | readNum := makeNumReader(rb) 124 | for i := 0; i < 256; i++ { 125 | _ = readNum() // table pointer 126 | tableLen := readNum() 127 | if tableLen != 0 { 128 | t.Fatalf("table %d has non-zero length: %d", i, tableLen) 129 | } 130 | } 131 | 132 | // Test reading records 133 | c, err := Open(tmp.Name()) 134 | if err != nil { 135 | t.Fatalf("Error opening %s: %s", tmp.Name(), err) 136 | } 137 | 138 | _, err = c.Data([]byte("does not exist")) 139 | if err != io.EOF { 140 | t.Fatalf("non-existent key should return io.EOF") 141 | } 142 | } 143 | 144 | func init() { 145 | b := bytes.NewBuffer(nil) 146 | for _, rec := range records { 147 | key := rec.key 148 | for _, value := range rec.values { 149 | b.WriteString(fmt.Sprintf("+%d,%d:%s->%s\n", len(key), len(value), key, value)) 150 | } 151 | } 152 | b.WriteByte('\n') 153 | data = b.Bytes() 154 | } 155 | -------------------------------------------------------------------------------- /cdbdump/cdbdump.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | 7 | "github.com/jbarham/cdb" 8 | ) 9 | 10 | func main() { 11 | bin, bout := bufio.NewReader(os.Stdin), bufio.NewWriter(os.Stdout) 12 | err := cdb.Dump(bout, bin) 13 | bout.Flush() 14 | if err != nil { 15 | os.Exit(111) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cdbmake/cdbmake.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path" 11 | 12 | "github.com/jbarham/cdb" 13 | ) 14 | 15 | func exitOnErr(err error) { 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | 21 | func usage() { 22 | fmt.Fprint(os.Stderr, "usage: cdbmake f [ftmp]\n") 23 | os.Exit(2) 24 | } 25 | 26 | func main() { 27 | var tmp *os.File 28 | var err error 29 | 30 | flag.Parse() 31 | args := flag.Args() 32 | if len(args) == 1 { 33 | dir, _ := path.Split(args[0]) 34 | tmp, err = ioutil.TempFile(dir, "") 35 | exitOnErr(err) 36 | } else if len(args) == 2 { 37 | tmp, err = os.OpenFile(args[1], os.O_RDWR|os.O_CREATE, 0644) 38 | exitOnErr(err) 39 | } else { 40 | usage() 41 | } 42 | 43 | fname := args[0] 44 | tmpname := tmp.Name() 45 | 46 | exitOnErr(cdb.Make(tmp, bufio.NewReader(os.Stdin))) 47 | exitOnErr(tmp.Sync()) 48 | exitOnErr(tmp.Close()) 49 | exitOnErr(os.Rename(tmpname, fname)) 50 | } 51 | -------------------------------------------------------------------------------- /dump.go: -------------------------------------------------------------------------------- 1 | package cdb 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | // Dump reads the cdb-formatted data in r and dumps it as a series of formatted 11 | // records (+klen,dlen:key->data\n) and a final newline to w. 12 | // The output of Dump is suitable as input to Make. 13 | // See http://cr.yp.to/cdb/cdbmake.html for details on the record format. 14 | func Dump(w io.Writer, r io.Reader) (err error) { 15 | defer func() { // Centralize exception handling. 16 | if e := recover(); e != nil { 17 | err = e.(error) 18 | } 19 | }() 20 | 21 | rb := bufio.NewReader(r) 22 | readNum := makeNumReader(rb) 23 | rw := &recWriter{bufio.NewWriter(w)} 24 | 25 | eod := readNum() 26 | // Read rest of header. 27 | for i := 0; i < 511; i++ { 28 | readNum() 29 | } 30 | 31 | pos := headerSize 32 | for pos < eod { 33 | klen, dlen := readNum(), readNum() 34 | rw.writeString(fmt.Sprintf("+%d,%d:", klen, dlen)) 35 | rw.copyn(rb, klen) 36 | rw.writeString("->") 37 | rw.copyn(rb, dlen) 38 | rw.writeString("\n") 39 | pos += 8 + klen + dlen 40 | } 41 | rw.writeString("\n") 42 | 43 | return rw.Flush() 44 | } 45 | 46 | func makeNumReader(r io.Reader) func() uint32 { 47 | buf := make([]byte, 4) 48 | return func() uint32 { 49 | if _, err := r.Read(buf); err != nil { 50 | panic(err) 51 | } 52 | return binary.LittleEndian.Uint32(buf) 53 | } 54 | } 55 | 56 | type recWriter struct { 57 | *bufio.Writer 58 | } 59 | 60 | func (rw *recWriter) writeString(s string) { 61 | if _, err := rw.WriteString(s); err != nil { 62 | panic(err) 63 | } 64 | } 65 | 66 | func (rw *recWriter) copyn(r io.Reader, n uint32) { 67 | if _, err := io.CopyN(rw, r, int64(n)); err != nil { 68 | panic(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jbarham/cdb 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /hash.go: -------------------------------------------------------------------------------- 1 | package cdb 2 | 3 | import "hash" 4 | 5 | const ( 6 | start = 5381 // Initial cdb checksum value. 7 | ) 8 | 9 | // digest represents the partial evaluation of a checksum. 10 | type digest struct { 11 | h uint32 12 | } 13 | 14 | func (d *digest) Reset() { d.h = start } 15 | 16 | // New returns a new hash computing the cdb checksum. 17 | func cdbHash() hash.Hash32 { 18 | d := new(digest) 19 | d.Reset() 20 | return d 21 | } 22 | 23 | func (d *digest) Size() int { return 4 } 24 | 25 | func update(h uint32, p []byte) uint32 { 26 | for i := 0; i < len(p); i++ { 27 | h = ((h << 5) + h) ^ uint32(p[i]) 28 | } 29 | return h 30 | } 31 | 32 | func (d *digest) Write(p []byte) (int, error) { 33 | d.h = update(d.h, p) 34 | return len(p), nil 35 | } 36 | 37 | func (d *digest) Sum32() uint32 { return d.h } 38 | 39 | func (d *digest) Sum(in []byte) []byte { 40 | s := d.Sum32() 41 | in = append(in, byte(s>>24)) 42 | in = append(in, byte(s>>16)) 43 | in = append(in, byte(s>>8)) 44 | in = append(in, byte(s)) 45 | return in 46 | } 47 | 48 | func (d *digest) BlockSize() int { return 1 } 49 | 50 | func checksum(data []byte) uint32 { return update(start, data) } 51 | -------------------------------------------------------------------------------- /make.go: -------------------------------------------------------------------------------- 1 | package cdb 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | var BadFormatError = errors.New("bad format") 12 | 13 | // Make reads cdb-formatted records from r and writes a cdb-format database 14 | // to w. See the documentation for Dump for details on the input record format. 15 | func Make(w io.WriteSeeker, r io.Reader) (err error) { 16 | defer func() { // Centralize error handling. 17 | if e := recover(); e != nil { 18 | err = e.(error) 19 | } 20 | }() 21 | 22 | if _, err = w.Seek(int64(headerSize), 0); err != nil { 23 | return 24 | } 25 | 26 | buf := make([]byte, 8) 27 | rb := bufio.NewReader(r) 28 | wb := bufio.NewWriter(w) 29 | hash := cdbHash() 30 | hw := io.MultiWriter(hash, wb) // Computes hash when writing record key. 31 | rr := &recReader{rb} 32 | htables := make(map[uint32][]slot) 33 | pos := headerSize 34 | // Read all records and write to output. 35 | for { 36 | // Record format is "+klen,dlen:key->data\n" 37 | c := rr.readByte() 38 | if c == '\n' { // end of records 39 | break 40 | } 41 | if c != '+' { 42 | return BadFormatError 43 | } 44 | klen, dlen := rr.readNum(','), rr.readNum(':') 45 | writeNums(wb, klen, dlen, buf) 46 | hash.Reset() 47 | rr.copyn(hw, klen) 48 | rr.eatByte('-') 49 | rr.eatByte('>') 50 | rr.copyn(wb, dlen) 51 | rr.eatByte('\n') 52 | h := hash.Sum32() 53 | tableNum := h % 256 54 | htables[tableNum] = append(htables[tableNum], slot{h, pos}) 55 | pos += 8 + klen + dlen 56 | } 57 | 58 | // Write hash tables and header. 59 | 60 | // Create and reuse a single hash table. 61 | maxSlots := 0 62 | for _, slots := range htables { 63 | if len(slots) > maxSlots { 64 | maxSlots = len(slots) 65 | } 66 | } 67 | slotTable := make([]slot, maxSlots*2) 68 | 69 | header := make([]byte, headerSize) 70 | // Write hash tables. 71 | for i := uint32(0); i < 256; i++ { 72 | slots := htables[i] 73 | if slots == nil { 74 | putNum(header[i*8:], pos) 75 | continue 76 | } 77 | 78 | nslots := uint32(len(slots) * 2) 79 | hashSlotTable := slotTable[:nslots] 80 | // Reset table slots. 81 | for j := 0; j < len(hashSlotTable); j++ { 82 | hashSlotTable[j].h = 0 83 | hashSlotTable[j].pos = 0 84 | } 85 | 86 | for _, slot := range slots { 87 | slotPos := (slot.h / 256) % nslots 88 | for hashSlotTable[slotPos].pos != 0 { 89 | slotPos++ 90 | if slotPos == uint32(len(hashSlotTable)) { 91 | slotPos = 0 92 | } 93 | } 94 | hashSlotTable[slotPos] = slot 95 | } 96 | 97 | if err = writeSlots(wb, hashSlotTable, buf); err != nil { 98 | return 99 | } 100 | 101 | putNum(header[i*8:], pos) 102 | putNum(header[i*8+4:], nslots) 103 | pos += 8 * nslots 104 | } 105 | 106 | if err = wb.Flush(); err != nil { 107 | return 108 | } 109 | 110 | if _, err = w.Seek(0, 0); err != nil { 111 | return 112 | } 113 | 114 | _, err = w.Write(header) 115 | 116 | return 117 | } 118 | 119 | type recReader struct { 120 | *bufio.Reader 121 | } 122 | 123 | func (rr *recReader) readByte() byte { 124 | c, err := rr.ReadByte() 125 | if err != nil { 126 | panic(err) 127 | } 128 | 129 | return c 130 | } 131 | 132 | func (rr *recReader) eatByte(c byte) { 133 | if rr.readByte() != c { 134 | panic(errors.New("unexpected character")) 135 | } 136 | } 137 | 138 | func (rr *recReader) readNum(delim byte) uint32 { 139 | s, err := rr.ReadString(delim) 140 | if err != nil { 141 | panic(err) 142 | } 143 | 144 | s = s[:len(s)-1] // Strip delim 145 | n, err := strconv.ParseUint(s, 10, 32) 146 | if err != nil { 147 | panic(err) 148 | } 149 | 150 | return uint32(n) 151 | } 152 | 153 | func (rr *recReader) copyn(w io.Writer, n uint32) { 154 | if _, err := io.CopyN(w, rr, int64(n)); err != nil { 155 | panic(err) 156 | } 157 | } 158 | 159 | func putNum(buf []byte, x uint32) { 160 | binary.LittleEndian.PutUint32(buf, x) 161 | } 162 | 163 | func writeNums(w io.Writer, x, y uint32, buf []byte) { 164 | putNum(buf, x) 165 | putNum(buf[4:], y) 166 | if _, err := w.Write(buf[:8]); err != nil { 167 | panic(err) 168 | } 169 | } 170 | 171 | type slot struct { 172 | h, pos uint32 173 | } 174 | 175 | func writeSlots(w io.Writer, slots []slot, buf []byte) (err error) { 176 | for _, np := range slots { 177 | putNum(buf, np.h) 178 | putNum(buf[4:], np.pos) 179 | if _, err = w.Write(buf[:8]); err != nil { 180 | return 181 | } 182 | } 183 | 184 | return nil 185 | } 186 | --------------------------------------------------------------------------------