dmitri.shuralyov.com/service/change/...

Start on a fs service implementation; use mock data.
dmitshur committed 2 years ago commit bc830c6484e477da14b5ac3e373319eb4468785b
fs/fs.go
@@ -0,0 +1,841 @@
+package fs
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	"dmitri.shuralyov.com/changes"
+	"github.com/shurcooL/users"
+)
+
+type Service struct{}
+
+var s = struct {
+	changes []struct {
+		changes.Change
+		Timeline []interface{}
+		Commits  []changes.Commit
+		Diff     []byte
+	}
+}{
+	changes: []struct {
+		changes.Change
+		Timeline []interface{}
+		Commits  []changes.Commit
+		Diff     []byte
+	}{
+		{
+			Change: changes.Change{
+				ID:        1,
+				State:     changes.OpenState,
+				Title:     "Initial implementation of woff2.",
+				Labels:    nil,
+				Author:    shurcool,
+				CreatedAt: time.Now().Add(-5 * time.Minute),
+				Replies:   0,
+
+				Commits: 1,
+			},
+			Timeline: nil,
+			Commits: []changes.Commit{{
+				SHA:        "4a911c4a1eabcc20a66ccc5c983dede401da2796",
+				Message:    "Initial implementation of woff2.\n\nMaybe some additional details here.",
+				Author:     shurcool,
+				AuthorTime: time.Now().Add(-10 * time.Minute),
+			}},
+			Diff: []byte(diff),
+		},
+	},
+}
+
+// List changes.
+func (*Service) List(ctx context.Context, repo string, opt changes.ListOptions) ([]changes.Change, error) {
+	if repo != "dmitri.shuralyov.com/font/woff2" {
+		return nil, os.ErrNotExist
+	}
+	var cs []changes.Change
+	for _, c := range s.changes {
+		cs = append(cs, c.Change)
+	}
+	return cs, nil
+}
+
+// Count changes.
+func (*Service) Count(ctx context.Context, repo string, opt changes.ListOptions) (uint64, error) {
+	if repo != "dmitri.shuralyov.com/font/woff2" {
+		return 0, os.ErrNotExist
+	}
+	var counts func(s changes.State) bool
+	switch opt.Filter {
+	case changes.FilterOpen:
+		counts = func(s changes.State) bool { return s == changes.OpenState }
+	case changes.FilterClosedMerged:
+		counts = func(s changes.State) bool { return s == changes.ClosedState || s == changes.MergedState }
+	case changes.FilterAll:
+		counts = func(s changes.State) bool { return true }
+	default:
+		// TODO: Map to 400 Bad Request HTTP error.
+		return 0, fmt.Errorf("opt.State has unsupported value %q", opt.Filter)
+	}
+	var count uint64
+	for _, c := range s.changes {
+		if counts(c.State) {
+			count++
+		}
+	}
+	return count, nil
+}
+
+// Get a change.
+func (*Service) Get(ctx context.Context, repo string, id uint64) (changes.Change, error) {
+	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+		return changes.Change{}, os.ErrNotExist
+	}
+	return s.changes[0].Change, nil
+}
+
+// ListTimeline lists timeline items (changes.Comment, changes.TimelineItem) for specified change id.
+func (*Service) ListTimeline(ctx context.Context, repo string, id uint64, opt *changes.ListTimelineOptions) ([]interface{}, error) {
+	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+		return nil, os.ErrNotExist
+	}
+	return s.changes[0].Timeline, nil
+}
+
+// ListCommits lists change commits.
+func (*Service) ListCommits(ctx context.Context, repo string, id uint64) ([]changes.Commit, error) {
+	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+		return nil, os.ErrNotExist
+	}
+	return s.changes[0].Commits, nil
+}
+
+// Get a change diff.
+func (*Service) GetDiff(ctx context.Context, repo string, id uint64, opt *changes.GetDiffOptions) ([]byte, error) {
+	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+		return nil, os.ErrNotExist
+	}
+	return s.changes[0].Diff, nil
+}
+
+var shurcool = users.User{
+	UserSpec: users.UserSpec{
+		ID:     1924134,
+		Domain: "github.com",
+	},
+	Login:     "shurcooL",
+	Name:      "Dmitri Shuralyov",
+	Email:     "dmitri@shuralyov.com",
+	AvatarURL: "https://dmitri.shuralyov.com/avatar.jpg",
+	HTMLURL:   "https://dmitri.shuralyov.com",
+	SiteAdmin: true,
+}
+
+const diff = `diff --git a/LICENSE b/LICENSE
+new file mode 100644
+index 0000000..49ea0f9
+--- /dev/null
++++ b/LICENSE
+@@ -0,0 +1,27 @@
++Copyright (c) 2018 The Go Authors. All rights reserved.
++
++Redistribution and use in source and binary forms, with or without
++modification, are permitted provided that the following conditions are
++met:
++
++   * Redistributions of source code must retain the above copyright
++notice, this list of conditions and the following disclaimer.
++   * 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.
++   * Neither the name of Google Inc. nor the names of its
++contributors may be used to endorse or promote products derived from
++this software without specific prior written permission.
++
++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
++OWNER 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.
+diff --git a/doc.go b/doc.go
+new file mode 100644
+index 0000000..a751214
+--- /dev/null
++++ b/doc.go
+@@ -0,0 +1,4 @@
++// Package woff2 implements a WOFF2 font decoder.
++//
++// The WOFF2 font packaging format is specified at https://www.w3.org/TR/WOFF2/.
++package woff2
+diff --git a/parse.go b/parse.go
+new file mode 100644
+index 0000000..498a4a8
+--- /dev/null
++++ b/parse.go
+@@ -0,0 +1,438 @@
++package woff2
++
++import (
++	"bytes"
++	"encoding/binary"
++	"fmt"
++	"io"
++
++	"github.com/dsnet/compress/brotli"
++)
++
++// File represents a parsed WOFF2 file.
++type File struct {
++	Header         Header
++	TableDirectory TableDirectory
++	// CollectionDirectory is present only if the font is a collection,
++	// as reported by Header.IsCollection.
++	CollectionDirectory *CollectionDirectory
++
++	// FontData is the concatenation of data for each table in the font.
++	// During storage, it's compressed using Brotli.
++	FontData []byte
++
++	ExtendedMetadata *ExtendedMetadata
++
++	// PrivateData is an optional block of private data for the font designer,
++	// foundry, or vendor to use.
++	PrivateData []byte
++}
++
++// Parse parses the WOFF2 data from r.
++func Parse(r io.Reader) (File, error) {
++	hdr, err := parseHeader(r)
++	if err != nil {
++		return File{}, err
++	}
++	td, err := parseTableDirectory(r, hdr)
++	if err != nil {
++		return File{}, err
++	}
++	cd, err := parseCollectionDirectory(r, hdr)
++	if err != nil {
++		return File{}, err
++	}
++	fd, err := parseCompressedFontData(r, hdr, td)
++	if err != nil {
++		return File{}, err
++	}
++	em, err := parseExtendedMetadata(r, hdr)
++	if err != nil {
++		return File{}, err
++	}
++	pd, err := parsePrivateData(r, hdr)
++	if err != nil {
++		return File{}, err
++	}
++
++	n, err := io.Copy(discardZeroes{}, r)
++	if err != nil {
++		return File{}, fmt.Errorf("Parse: %v", err)
++	}
++	if n > 3 {
++		return File{}, fmt.Errorf("Parse: %d bytes left remaining, want no more than 3", n)
++	}
++
++	return File{
++		Header:              hdr,
++		TableDirectory:      td,
++		CollectionDirectory: cd,
++		FontData:            fd,
++		ExtendedMetadata:    em,
++		PrivateData:         pd,
++	}, nil
++}
++
++// discardZeroes is an io.Writer that returns an error if any non-zero bytes are written to it.
++type discardZeroes struct{}
++
++func (discardZeroes) Write(p []byte) (int, error) {
++	for _, b := range p {
++		if b != 0 {
++			return 0, fmt.Errorf("encountered non-zero byte %d", b)
++		}
++	}
++	return len(p), nil
++}
++
++// Header is the file header with basic font type and version,
++// along with offsets to metadata and private data blocks.
++type Header struct {
++	Signature           uint32 // The identifying signature; must be 0x774F4632 ('wOF2').
++	Flavor              uint32 // The "sfnt version" of the input font.
++	Length              uint32 // Total size of the WOFF file.
++	NumTables           uint16 // Number of entries in directory of font tables.
++	Reserved            uint16 // Reserved; set to 0.
++	TotalSfntSize       uint32 // Total size needed for the uncompressed font data, including the sfnt header, directory, and font tables (including padding).
++	TotalCompressedSize uint32 // Total length of the compressed data block.
++	MajorVersion        uint16 // Major version of the WOFF file.
++	MinorVersion        uint16 // Minor version of the WOFF file.
++	MetaOffset          uint32 // Offset to metadata block, from beginning of WOFF file.
++	MetaLength          uint32 // Length of compressed metadata block.
++	MetaOrigLength      uint32 // Uncompressed size of metadata block.
++	PrivOffset          uint32 // Offset to private data block, from beginning of WOFF file.
++	PrivLength          uint32 // Length of private data block.
++}
++
++func parseHeader(r io.Reader) (Header, error) {
++	var hdr Header
++	err := binary.Read(r, order, &hdr)
++	if err != nil {
++		return Header{}, err
++	}
++	if hdr.Signature != signature {
++		return Header{}, fmt.Errorf("parseHeader: invalid signature: got %#08x, want %#08x", hdr.Signature, signature)
++	}
++	return hdr, nil
++}
++
++// IsCollection reports whether this is a font collection, i.e.,
++// if the value of Flavor field is set to the TrueType Collection flavor 'ttcf'.
++func (hdr Header) IsCollection() bool {
++	return hdr.Flavor == ttcfFlavor
++}
++
++// TableDirectory is the directory of font tables, containing size and other info.
++type TableDirectory []TableDirectoryEntry
++
++func parseTableDirectory(r io.Reader, hdr Header) (TableDirectory, error) {
++	var td TableDirectory
++	for i := 0; i < int(hdr.NumTables); i++ {
++		var e TableDirectoryEntry
++
++		err := readU8(r, &e.Flags)
++		if err != nil {
++			return nil, err
++		}
++		if e.Flags&0x3f == 0x3f {
++			e.Tag = new(uint32)
++			err := readU32(r, e.Tag)
++			if err != nil {
++				return nil, err
++			}
++		}
++		err = readBase128(r, &e.OrigLength)
++		if err != nil {
++			return nil, err
++		}
++
++		switch tag, transformVersion := e.tag(), e.transformVersion(); tag {
++		case glyfTable, locaTable:
++			// 0 means transform for glyf/loca tables.
++			if transformVersion == 0 {
++				e.TransformLength = new(uint32)
++				err := readBase128(r, e.TransformLength)
++				if err != nil {
++					return nil, err
++				}
++
++				// The transform length of the transformed loca table MUST always be zero.
++				if tag == locaTable && *e.TransformLength != 0 {
++					return nil, fmt.Errorf("parseTableDirectory: 'loca' table has non-zero transform length %d", *e.TransformLength)
++				}
++			}
++		default:
++			// Non-0 means transform for other tables.
++			if transformVersion != 0 {
++				e.TransformLength = new(uint32)
++				err := readBase128(r, e.TransformLength)
++				if err != nil {
++					return nil, err
++				}
++			}
++		}
++
++		td = append(td, e)
++	}
++	return td, nil
++}
++
++// Table is a high-level representation of a table.
++type Table struct {
++	Tag    uint32
++	Offset int
++	Length int
++}
++
++// Tables returns the derived high-level information
++// about the tables in the table directory.
++func (td TableDirectory) Tables() []Table {
++	var ts []Table
++	var offset int
++	for _, t := range td {
++		length := int(t.length())
++		ts = append(ts, Table{
++			Tag:    t.tag(),
++			Offset: offset,
++			Length: length,
++		})
++		offset += length
++	}
++	return ts
++}
++
++// uncompressedSize computes the total uncompressed size
++// of the tables in the table directory.
++func (td TableDirectory) uncompressedSize() int64 {
++	var n int64
++	for _, t := range td {
++		n += int64(t.length())
++	}
++	return n
++}
++
++// TableDirectoryEntry is a table directory entry.
++type TableDirectoryEntry struct {
++	Flags           uint8   // Table type and flags.
++	Tag             *uint32 // 4-byte tag (optional).
++	OrigLength      uint32  // Length of original table.
++	TransformLength *uint32 // Transformed length (optional).
++}
++
++func (e TableDirectoryEntry) tag() uint32 {
++	switch e.Tag {
++	case nil:
++		return knownTableTags[e.Flags&0x3f] // Bits [0..5].
++	default:
++		return *e.Tag
++	}
++}
++
++func (e TableDirectoryEntry) transformVersion() uint8 {
++	return e.Flags >> 6 // Bits [6..7].
++}
++
++func (e TableDirectoryEntry) length() uint32 {
++	switch e.TransformLength {
++	case nil:
++		return e.OrigLength
++	default:
++		return *e.TransformLength
++	}
++}
++
++// CollectionDirectory is an optional table containing the font fragment descriptions
++// of font collection entries.
++type CollectionDirectory struct {
++	Header  CollectionHeader
++	Entries []CollectionFontEntry
++}
++
++// CollectionHeader is a part of CollectionDirectory.
++type CollectionHeader struct {
++	Version  uint32
++	NumFonts uint16
++}
++
++// CollectionFontEntry represents a CollectionFontEntry record.
++type CollectionFontEntry struct {
++	NumTables    uint16   // The number of tables in this font.
++	Flavor       uint32   // The "sfnt version" of the font.
++	TableIndices []uint16 // The indicies identifying an entry in the Table Directory for each table in this font.
++}
++
++func parseCollectionDirectory(r io.Reader, hdr Header) (*CollectionDirectory, error) {
++	// CollectionDirectory is present only if the input font is a collection.
++	if !hdr.IsCollection() {
++		return nil, nil
++	}
++
++	var cd CollectionDirectory
++	err := readU32(r, &cd.Header.Version)
++	if err != nil {
++		return nil, err
++	}
++	err = read255UShort(r, &cd.Header.NumFonts)
++	if err != nil {
++		return nil, err
++	}
++	for i := 0; i < int(cd.Header.NumFonts); i++ {
++		var e CollectionFontEntry
++
++		err := read255UShort(r, &e.NumTables)
++		if err != nil {
++			return nil, err
++		}
++		err = readU32(r, &e.Flavor)
++		if err != nil {
++			return nil, err
++		}
++		for j := 0; j < int(e.NumTables); j++ {
++			var tableIndex uint16
++			err := read255UShort(r, &tableIndex)
++			if err != nil {
++				return nil, err
++			}
++			if tableIndex >= hdr.NumTables {
++				return nil, fmt.Errorf("parseCollectionDirectory: tableIndex >= hdr.NumTables")
++			}
++			e.TableIndices = append(e.TableIndices, tableIndex)
++		}
++
++		cd.Entries = append(cd.Entries, e)
++	}
++	return &cd, nil
++}
++
++func parseCompressedFontData(r io.Reader, hdr Header, td TableDirectory) ([]byte, error) {
++	// Compressed font data.
++	br, err := brotli.NewReader(io.LimitReader(r, int64(hdr.TotalCompressedSize)), nil)
++	//br, err := brotli.NewReader(&exactReader{R: r, N: int64(hdr.TotalCompressedSize)}, nil)
++	if err != nil {
++		return nil, err
++	}
++	var buf bytes.Buffer
++	n, err := io.Copy(&buf, br)
++	if err != nil {
++		return nil, fmt.Errorf("parseCompressedFontData: io.Copy: %v", err)
++	}
++	err = br.Close()
++	if err != nil {
++		return nil, fmt.Errorf("parseCompressedFontData: br.Close: %v", err)
++	}
++	if uncompressedSize := td.uncompressedSize(); n != uncompressedSize {
++		return nil, fmt.Errorf("parseCompressedFontData: unexpected size of uncompressed data: got %d, want %d", n, uncompressedSize)
++	}
++	return buf.Bytes(), nil
++}
++
++// ExtendedMetadata is an optional block of extended metadata,
++// represented in XML format and compressed for storage in the WOFF2 file.
++type ExtendedMetadata struct{}
++
++func parseExtendedMetadata(r io.Reader, hdr Header) (*ExtendedMetadata, error) {
++	if hdr.MetaLength == 0 {
++		return nil, nil
++	}
++	return nil, fmt.Errorf("parseExtendedMetadata: not implemented")
++}
++
++func parsePrivateData(r io.Reader, hdr Header) ([]byte, error) {
++	if hdr.PrivLength == 0 {
++		return nil, nil
++	}
++	return nil, fmt.Errorf("parsePrivateData: not implemented")
++}
++
++// readU8 reads a UInt8 value.
++func readU8(r io.Reader, v *uint8) error {
++	return binary.Read(r, order, v)
++}
++
++// readU16 reads a UInt16 value.
++func readU16(r io.Reader, v *uint16) error {
++	return binary.Read(r, order, v)
++}
++
++// readU32 reads a UInt32 value.
++func readU32(r io.Reader, v *uint32) error {
++	return binary.Read(r, order, v)
++}
++
++// readBase128 reads a UIntBase128 value.
++func readBase128(r io.Reader, v *uint32) error {
++	var accum uint32
++	for i := 0; i < 5; i++ {
++		var data uint8
++		err := binary.Read(r, order, &data)
++		if err != nil {
++			return err
++		}
++
++		// Leading zeros are invalid.
++		if i == 0 && data == 0x80 {
++			return fmt.Errorf("leading zero is invalid")
++		}
++
++		// If any of top 7 bits are set then accum << 7 would overflow.
++		if accum&0xfe000000 != 0 {
++			return fmt.Errorf("top seven bits are set, about to overflow")
++		}
++
++		accum = (accum << 7) | uint32(data)&0x7f
++
++		// Spin until most significant bit of data byte is false.
++		if (data & 0x80) == 0 {
++			*v = accum
++			return nil
++		}
++	}
++	return fmt.Errorf("UIntBase128 sequence exceeds 5 bytes")
++}
++
++// read255UShort reads a 255UInt16 value.
++func read255UShort(r io.Reader, v *uint16) error {
++	const (
++		oneMoreByteCode1 = 255
++		oneMoreByteCode2 = 254
++		wordCode         = 253
++		lowestUCode      = 253
++	)
++	var code uint8
++	err := binary.Read(r, order, &code)
++	if err != nil {
++		return err
++	}
++	switch code {
++	case wordCode:
++		var value uint16
++		err := binary.Read(r, order, &value)
++		if err != nil {
++			return err
++		}
++		*v = value
++		return nil
++	case oneMoreByteCode1:
++		var value uint8
++		err := binary.Read(r, order, &value)
++		if err != nil {
++			return err
++		}
++		*v = uint16(value) + lowestUCode
++		return nil
++	case oneMoreByteCode2:
++		var value uint8
++		err := binary.Read(r, order, &value)
++		if err != nil {
++			return err
++		}
++		*v = uint16(value) + lowestUCode*2
++		return nil
++	default:
++		*v = uint16(code)
++		return nil
++	}
++}
++
++// WOFF2 uses big endian encoding.
++var order binary.ByteOrder = binary.BigEndian
+diff --git a/parse_test.go b/parse_test.go
+new file mode 100644
+index 0000000..87545cf
+--- /dev/null
++++ b/parse_test.go
+@@ -0,0 +1,109 @@
++package woff2_test
++
++import (
++	"fmt"
++	"log"
++
++	"dmitri.shuralyov.com/font/woff2"
++	"github.com/shurcooL/gofontwoff"
++)
++
++func ExampleParse() {
++	f, err := gofontwoff.Assets.Open("/Go-Regular.woff2")
++	if err != nil {
++		log.Fatalln(err)
++	}
++	defer f.Close()
++
++	font, err := woff2.Parse(f)
++	if err != nil {
++		log.Fatalln(err)
++	}
++	Dump(font)
++
++	// Output:
++	//
++	// Signature:           0x774f4632
++	// Flavor:              0x00010000
++	// Length:              46132
++	// NumTables:           14
++	// Reserved:            0
++	// TotalSfntSize:       140308
++	// TotalCompressedSize: 46040
++	// MajorVersion:        1
++	// MinorVersion:        0
++	// MetaOffset:          0
++	// MetaLength:          0
++	// MetaOrigLength:      0
++	// PrivOffset:          0
++	// PrivLength:          0
++	//
++	// TableDirectory: 14 entries
++	// 	{Flags: 0x06, Tag: <nil>, OrigLength: 96, TransformLength: <nil>}
++	// 	{Flags: 0x00, Tag: <nil>, OrigLength: 1318, TransformLength: <nil>}
++	// 	{Flags: 0x08, Tag: <nil>, OrigLength: 176, TransformLength: <nil>}
++	// 	{Flags: 0x09, Tag: <nil>, OrigLength: 3437, TransformLength: <nil>}
++	// 	{Flags: 0x11, Tag: <nil>, OrigLength: 8, TransformLength: <nil>}
++	// 	{Flags: 0x0a, Tag: <nil>, OrigLength: 118912, TransformLength: 105020}
++	// 	{Flags: 0x0b, Tag: <nil>, OrigLength: 1334, TransformLength: 0}
++	// 	{Flags: 0x01, Tag: <nil>, OrigLength: 54, TransformLength: <nil>}
++	// 	{Flags: 0x02, Tag: <nil>, OrigLength: 36, TransformLength: <nil>}
++	// 	{Flags: 0x03, Tag: <nil>, OrigLength: 2662, TransformLength: <nil>}
++	// 	{Flags: 0x04, Tag: <nil>, OrigLength: 32, TransformLength: <nil>}
++	// 	{Flags: 0x05, Tag: <nil>, OrigLength: 6967, TransformLength: <nil>}
++	// 	{Flags: 0x07, Tag: <nil>, OrigLength: 4838, TransformLength: <nil>}
++	// 	{Flags: 0x0c, Tag: <nil>, OrigLength: 188, TransformLength: <nil>}
++	//
++	// CollectionDirectory: <nil>
++	// CompressedFontData: 124832 bytes (uncompressed size)
++	// ExtendedMetadata: <nil>
++	// PrivateData: []
++}
++
++func Dump(f woff2.File) {
++	dumpHeader(f.Header)
++	fmt.Println()
++	dumpTableDirectory(f.TableDirectory)
++	fmt.Println()
++	fmt.Println("CollectionDirectory:", f.CollectionDirectory)
++	fmt.Println("CompressedFontData:", len(f.CompressedFontData.Data), "bytes (uncompressed size)")
++	fmt.Println("ExtendedMetadata:", f.ExtendedMetadata)
++	fmt.Println("PrivateData:", f.PrivateData)
++}
++
++func dumpHeader(hdr woff2.Header) {
++	fmt.Printf("Signature:           %#08x\n", hdr.Signature)
++	fmt.Printf("Flavor:              %#08x\n", hdr.Flavor)
++	fmt.Printf("Length:              %d\n", hdr.Length)
++	fmt.Printf("NumTables:           %d\n", hdr.NumTables)
++	fmt.Printf("Reserved:            %d\n", hdr.Reserved)
++	fmt.Printf("TotalSfntSize:       %d\n", hdr.TotalSfntSize)
++	fmt.Printf("TotalCompressedSize: %d\n", hdr.TotalCompressedSize)
++	fmt.Printf("MajorVersion:        %d\n", hdr.MajorVersion)
++	fmt.Printf("MinorVersion:        %d\n", hdr.MinorVersion)
++	fmt.Printf("MetaOffset:          %d\n", hdr.MetaOffset)
++	fmt.Printf("MetaLength:          %d\n", hdr.MetaLength)
++	fmt.Printf("MetaOrigLength:      %d\n", hdr.MetaOrigLength)
++	fmt.Printf("PrivOffset:          %d\n", hdr.PrivOffset)
++	fmt.Printf("PrivLength:          %d\n", hdr.PrivLength)
++}
++
++func dumpTableDirectory(td woff2.TableDirectory) {
++	fmt.Println("TableDirectory:", len(td), "entries")
++	for _, t := range td {
++		fmt.Printf("\t{")
++		fmt.Printf("Flags: %#02x, ", t.Flags)
++		if t.Tag != nil {
++			fmt.Printf("Tag: %v, ", *t.Tag)
++		} else {
++			fmt.Printf("Tag: <nil>, ")
++		}
++		fmt.Printf("OrigLength: %v, ", t.OrigLength)
++		if t.TransformLength != nil {
++			fmt.Printf("TransformLength: %v", *t.TransformLength)
++		} else {
++			fmt.Printf("TransformLength: <nil>")
++		}
++		fmt.Printf("}\n")
++	}
++}
+diff --git a/tags.go b/tags.go
+new file mode 100644
+index 0000000..3c13a55
+--- /dev/null
++++ b/tags.go
+@@ -0,0 +1,79 @@
++package woff2
++
++const (
++	// signature is the WOFF 2.0 file identifying signature 'wOF2'.
++	signature = uint32('w'<<24 | 'O'<<16 | 'F'<<8 | '2')
++
++	// ttcfFlavor is the TrueType Collection flavor 'ttcf'.
++	ttcfFlavor = uint32('t'<<24 | 't'<<16 | 'c'<<8 | 'f')
++
++	glyfTable = uint32('g'<<24 | 'l'<<16 | 'y'<<8 | 'f')
++	locaTable = uint32('l'<<24 | 'o'<<16 | 'c'<<8 | 'a')
++)
++
++// knownTableTags is the "Known Table Tags" table.
++var knownTableTags = [...]uint32{
++	0:  uint32('c'<<24 | 'm'<<16 | 'a'<<8 | 'p'),
++	1:  uint32('h'<<24 | 'e'<<16 | 'a'<<8 | 'd'),
++	2:  uint32('h'<<24 | 'h'<<16 | 'e'<<8 | 'a'),
++	3:  uint32('h'<<24 | 'm'<<16 | 't'<<8 | 'x'),
++	4:  uint32('m'<<24 | 'a'<<16 | 'x'<<8 | 'p'),
++	5:  uint32('n'<<24 | 'a'<<16 | 'm'<<8 | 'e'),
++	6:  uint32('O'<<24 | 'S'<<16 | '/'<<8 | '2'),
++	7:  uint32('p'<<24 | 'o'<<16 | 's'<<8 | 't'),
++	8:  uint32('c'<<24 | 'v'<<16 | 't'<<8 | ' '),
++	9:  uint32('f'<<24 | 'p'<<16 | 'g'<<8 | 'm'),
++	10: uint32('g'<<24 | 'l'<<16 | 'y'<<8 | 'f'),
++	11: uint32('l'<<24 | 'o'<<16 | 'c'<<8 | 'a'),
++	12: uint32('p'<<24 | 'r'<<16 | 'e'<<8 | 'p'),
++	13: uint32('C'<<24 | 'F'<<16 | 'F'<<8 | ' '),
++	14: uint32('V'<<24 | 'O'<<16 | 'R'<<8 | 'G'),
++	15: uint32('E'<<24 | 'B'<<16 | 'D'<<8 | 'T'),
++	16: uint32('E'<<24 | 'B'<<16 | 'L'<<8 | 'C'),
++	17: uint32('g'<<24 | 'a'<<16 | 's'<<8 | 'p'),
++	18: uint32('h'<<24 | 'd'<<16 | 'm'<<8 | 'x'),
++	19: uint32('k'<<24 | 'e'<<16 | 'r'<<8 | 'n'),
++	20: uint32('L'<<24 | 'T'<<16 | 'S'<<8 | 'H'),
++	21: uint32('P'<<24 | 'C'<<16 | 'L'<<8 | 'T'),
++	22: uint32('V'<<24 | 'D'<<16 | 'M'<<8 | 'X'),
++	23: uint32('v'<<24 | 'h'<<16 | 'e'<<8 | 'a'),
++	24: uint32('v'<<24 | 'm'<<16 | 't'<<8 | 'x'),
++	25: uint32('B'<<24 | 'A'<<16 | 'S'<<8 | 'E'),
++	26: uint32('G'<<24 | 'D'<<16 | 'E'<<8 | 'F'),
++	27: uint32('G'<<24 | 'P'<<16 | 'O'<<8 | 'S'),
++	28: uint32('G'<<24 | 'S'<<16 | 'U'<<8 | 'B'),
++	29: uint32('E'<<24 | 'B'<<16 | 'S'<<8 | 'C'),
++	30: uint32('J'<<24 | 'S'<<16 | 'T'<<8 | 'F'),
++	31: uint32('M'<<24 | 'A'<<16 | 'T'<<8 | 'H'),
++	32: uint32('C'<<24 | 'B'<<16 | 'D'<<8 | 'T'),
++	33: uint32('C'<<24 | 'B'<<16 | 'L'<<8 | 'C'),
++	34: uint32('C'<<24 | 'O'<<16 | 'L'<<8 | 'R'),
++	35: uint32('C'<<24 | 'P'<<16 | 'A'<<8 | 'L'),
++	36: uint32('S'<<24 | 'V'<<16 | 'G'<<8 | ' '),
++	37: uint32('s'<<24 | 'b'<<16 | 'i'<<8 | 'x'),
++	38: uint32('a'<<24 | 'c'<<16 | 'n'<<8 | 't'),
++	39: uint32('a'<<24 | 'v'<<16 | 'a'<<8 | 'r'),
++	40: uint32('b'<<24 | 'd'<<16 | 'a'<<8 | 't'),
++	41: uint32('b'<<24 | 'l'<<16 | 'o'<<8 | 'c'),
++	42: uint32('b'<<24 | 's'<<16 | 'l'<<8 | 'n'),
++	43: uint32('c'<<24 | 'v'<<16 | 'a'<<8 | 'r'),
++	44: uint32('f'<<24 | 'd'<<16 | 's'<<8 | 'c'),
++	45: uint32('f'<<24 | 'e'<<16 | 'a'<<8 | 't'),
++	46: uint32('f'<<24 | 'm'<<16 | 't'<<8 | 'x'),
++	47: uint32('f'<<24 | 'v'<<16 | 'a'<<8 | 'r'),
++	48: uint32('g'<<24 | 'v'<<16 | 'a'<<8 | 'r'),
++	49: uint32('h'<<24 | 's'<<16 | 't'<<8 | 'y'),
++	50: uint32('j'<<24 | 'u'<<16 | 's'<<8 | 't'),
++	51: uint32('l'<<24 | 'c'<<16 | 'a'<<8 | 'r'),
++	52: uint32('m'<<24 | 'o'<<16 | 'r'<<8 | 't'),
++	53: uint32('m'<<24 | 'o'<<16 | 'r'<<8 | 'x'),
++	54: uint32('o'<<24 | 'p'<<16 | 'b'<<8 | 'd'),
++	55: uint32('p'<<24 | 'r'<<16 | 'o'<<8 | 'p'),
++	56: uint32('t'<<24 | 'r'<<16 | 'a'<<8 | 'k'),
++	57: uint32('Z'<<24 | 'a'<<16 | 'p'<<8 | 'f'),
++	58: uint32('S'<<24 | 'i'<<16 | 'l'<<8 | 'f'),
++	59: uint32('G'<<24 | 'l'<<16 | 'a'<<8 | 't'),
++	60: uint32('G'<<24 | 'l'<<16 | 'o'<<8 | 'c'),
++	61: uint32('F'<<24 | 'e'<<16 | 'a'<<8 | 't'),
++	62: uint32('S'<<24 | 'i'<<16 | 'l'<<8 | 'l'),
++}
+diff --git a/tags_test.go b/tags_test.go
+new file mode 100644
+index 0000000..19eab67
+--- /dev/null
++++ b/tags_test.go
+@@ -0,0 +1,12 @@
++package woff2
++
++import (
++	"testing"
++)
++
++func TestKnownTableTagsLength(t *testing.T) {
++	const want = 63
++	if got := len(knownTableTags); got != want {
++		t.Errorf("got len(knownTableTags): %v, want: %v", got, want)
++	}
++}
+`