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

fs: Add second commit to mock data.
shurcooL committed 4 days ago commit ede004665f39daffb01fdd6c41652a2ff7ac6901
change.go
@@ -19,11 +19,11 @@ type Service interface {
 	// Get a change.
 	Get(ctx context.Context, repo string, id uint64) (Change, error)
 
 	// ListTimeline lists timeline items (change.Comment, change.Review, change.TimelineItem) for specified change id.
 	ListTimeline(ctx context.Context, repo string, id uint64, opt *ListTimelineOptions) ([]interface{}, error)
-	// ListCommits lists change commits.
+	// ListCommits lists change commits, from first to last.
 	ListCommits(ctx context.Context, repo string, id uint64) ([]Commit, error)
 	// Get a change diff.
 	GetDiff(ctx context.Context, repo string, id uint64, opt *GetDiffOptions) ([]byte, error)
 }
 
fs/fs.go
@@ -15,18 +15,18 @@ type Service struct{}
 var s = struct {
 	changes []struct {
 		change.Change
 		Timeline []interface{}
 		Commits  []change.Commit
-		Diff     []byte
+		Diffs    map[string][]byte // Key is commit SHA. "all" is diff of all commits combined.
 	}
 }{
 	changes: []struct {
 		change.Change
 		Timeline []interface{}
 		Commits  []change.Commit
-		Diff     []byte
+		Diffs    map[string][]byte
 	}{
 		{
 			Change: change.Change{
 				ID:        1,
 				State:     change.OpenState,
@@ -34,11 +34,11 @@ var s = struct {
 				Labels:    nil,
 				Author:    shurcool,
 				CreatedAt: time.Date(2018, 2, 12, 0, 9, 19, 621031866, time.UTC),
 				Replies:   0,
 
-				Commits: 1,
+				Commits: 2,
 			},
 			Timeline: []interface{}{
 				change.Comment{
 					User:      shurcool,
 					CreatedAt: time.Date(2018, 2, 12, 0, 9, 19, 621031866, time.UTC),
@@ -90,12 +90,24 @@ https://go.googlesource.com/image/+/master/font/gofont/ttfs/.
 Add basic test coverage.
 
 Helps https://github.com/ConradIrwin/font/issues/1.`,
 				Author:     shurcool,
 				AuthorTime: time.Date(2018, 2, 11, 20, 10, 28, 0, time.UTC),
+			}, {
+				SHA: "61339d441b319cd6ca35d952522f86cc42ad4b6e",
+				Message: `Update test for new API.
+
+The API had changed earlier, the test wasn't updated for it. This
+change fixes that, allowing tests to pass.`,
+				Author:     shurcool,
+				AuthorTime: time.Date(2018, 2, 12, 20, 22, 29, 674711268, time.UTC),
 			}},
-			Diff: []byte(diff),
+			Diffs: map[string][]byte{
+				"all": []byte(diffAll),
+				"d2568fb6f10921b2d0c84d58bad14b2fadb88aa7": []byte(diffCommit1),
+				"61339d441b319cd6ca35d952522f86cc42ad4b6e": []byte(diffCommit2),
+			},
 		},
 	},
 }
 
 // List changes.
@@ -179,11 +191,16 @@ func (*Service) ListCommits(ctx context.Context, repo string, id uint64) ([]chan
 // Get a change diff.
 func (*Service) GetDiff(ctx context.Context, repo string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
 	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
 		return nil, os.ErrNotExist
 	}
-	return s.changes[0].Diff, nil
+	switch opt {
+	case nil:
+		return s.changes[0].Diffs["all"], nil
+	default:
+		return s.changes[0].Diffs[opt.Commit], nil
+	}
 }
 
 // threadType is the notifications thread type for this service.
 const threadType = "Change"
 
@@ -201,11 +218,11 @@ var shurcool = users.User{
 	AvatarURL: "https://dmitri.shuralyov.com/avatar.jpg",
 	HTMLURL:   "https://dmitri.shuralyov.com",
 	SiteAdmin: true,
 }
 
-const diff = `diff --git a/Commit Message b/Commit Message
+const diffAll = `diff --git a/Commit Message b/Commit Message
 new file mode 100644
 index 0000000..dfb31fe
 --- /dev/null
 +++ b/Commit Message
 @@ -0,0 +1,27 @@
@@ -762,11 +779,11 @@ index 0000000..87545cf
 +	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("CompressedFontData:", len(f.FontData), "bytes (uncompressed size)")
 +	fmt.Println("ExtendedMetadata:", f.ExtendedMetadata)
 +	fmt.Println("PrivateData:", f.PrivateData)
 +}
 +
 +func dumpHeader(hdr woff2.Header) {
@@ -905,5 +922,692 @@ index 0000000..3980f39
 +	if got := len(knownTableTags); got != want {
 +		t.Errorf("got len(knownTableTags): %v, want: %v", got, want)
 +	}
 +}
 `
+
+const diffCommit1 = `diff --git a/doc.go b/doc.go
+index fd35888..a751214 100644
+--- a/doc.go
++++ b/doc.go
+@@ -2,5 +2,3 @@
+ //
+ // The WOFF2 font packaging format is specified at https://www.w3.org/TR/WOFF2/.
+ package woff2
+-
+-// TODO: Implement.
+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..3980f39
+--- /dev/null
++++ b/tags_test.go
+@@ -0,0 +1,10 @@
++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)
++	}
++}
+`
+
+const diffCommit2 = `diff --git a/parse_test.go b/parse_test.go
+index 87545cf..14e2830 100644
+--- a/parse_test.go
++++ b/parse_test.go
+@@ -66,7 +66,7 @@ func Dump(f woff2.File) {
+ 	dumpTableDirectory(f.TableDirectory)
+ 	fmt.Println()
+ 	fmt.Println("CollectionDirectory:", f.CollectionDirectory)
+-	fmt.Println("CompressedFontData:", len(f.CompressedFontData.Data), "bytes (uncompressed size)")
++	fmt.Println("CompressedFontData:", len(f.FontData), "bytes (uncompressed size)")
+ 	fmt.Println("ExtendedMetadata:", f.ExtendedMetadata)
+ 	fmt.Println("PrivateData:", f.PrivateData)
+ }
+`