Repository: segmentio/encoding (go)

Issue:
"checkptr: pointer arithmetic result points to invalid allocation" caused by #81
Using segmentio/encoding v0.2.16 on both MacOS and Linux with Go 1.16.2, I get `fatal error: checkptr: pointer arithmetic result points to invalid allocation` from Go's race detector pointing a finger at `ascii/valid_print.go:31` with certain input to `json.Marshal()`, suggesting it is related to #81.

Here's simplified test code to trigger it:
```
package main

import (
	"testing"

	"github.com/segmentio/encoding/json"
)

type Foo struct {
	Source struct {
		Table string
	}
}

func TestUnmarshal(t *testing.T) {
	input := []byte(`{"source": {"table": "1234567"}}`)
	r := &Foo{}
	json.Unmarshal(input, r)
}
```

Run:
```
go mod init segtest
go mod tidy
go test -v -race -trimpath ./...
```

And you should see:
```
fatal error: checkptr: pointer arithmetic result points to invalid allocation

goroutine 19 [running]:
runtime.throw(0x12669e8, 0x40)
	runtime/panic.go:1117 +0x72 fp=0xc000042b28 sp=0xc000042af8 pc=0x1078352
runtime.checkptrArithmetic(0xc00012a060, 0xc000042bc8, 0x1, 0x1)
	runtime/checkptr.go:43 +0xbe fp=0xc000042b58 sp=0xc000042b28 pc=0x1047b7e
github.com/segmentio/encoding/ascii.ValidPrintString(0xc00012a040, 0x20, 0x1)
	github.com/segmentio/encoding@v0.2.16/ascii/valid_print.go:31 +0x4e5 fp=0xc000042be0 sp=0xc000042b58 pc=0x11d2725
github.com/segmentio/encoding/ascii.ValidPrint(...)
	github.com/segmentio/encoding@v0.2.16/ascii/valid_print.go:8
github.com/segmentio/encoding/json.internalParseFlags(0xc00012a040, 0x20, 0x20, 0xc000121380)
	github.com/segmentio/encoding@v0.2.16/json/parse.go:33 +0xcb fp=0xc000042c68 sp=0xc000042be0 pc=0x120420b
github.com/segmentio/encoding/json.Parse(0xc00012a040, 0x20, 0x20, 0x1223620, 0xc00011a520, 0x0, 0x10, 0xc000030640, 0xc00011a520, 0x138f720, ...)
	github.com/segmentio/encoding@v0.2.16/json/json.go:303 +0x114 fp=0xc000042dd0 sp=0xc000042c68 pc=0x1203a94
github.com/segmentio/encoding/json.Unmarshal(0xc00012a040, 0x20, 0x20, 0x1223620, 0xc00011a520, 0x0, 0x0)
	github.com/segmentio/encoding@v0.2.16/json/json.go:285 +0x93 fp=0xc000042e78 sp=0xc000042dd0 pc=0x1203833
segtest.TestUnmarshal(0xc000102900)
	segtest/main_test.go:18 +0x108 fp=0xc000042ed0 sp=0xc000042e78 pc=0x1213b68
```

If you can't reproduce, I'd be happy to provide more detail.

Pull request description:
fix issue 84
Fixes #84 

This PR modifies the functions in the `ascii` package to prevent pointers being advanced beyond the end of the memory areas they started in.

Technically, we were never dereferencing pointers with addresses beyond the allocated areas, but Go makes it invalid to simply have pointers that address unallocated memory. To my understanding, this is done to prevent exposing out-of-bounds pointers to the GC, which could mistakenly create references to objects that shouldn't have any, or potentially cause runtime errors if the GC finds pointers that aren't mapping to any allocated areas.

I believe that an alternative solution could have been to use `//go:nosplit` on the functions, which should prevent these "out-of-bounds" pointers from being seen by the GC, tho I haven't found enough literature on the subject to feel confident enough in that assumption.

Thanks @aaron42net for reporting the issue!

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 80dfc1d..5256e4f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -6,7 +6,7 @@ jobs:
     steps:
       - checkout
       - run: go mod download
-      - run: make test
+      - run: make test -j8
 
   benchmark:
     docker:
diff --git a/Makefile b/Makefile
index 09c0db2..18fb2d8 100644
--- a/Makefile
+++ b/Makefile
@@ -10,14 +10,25 @@ go-fuzz-build := ${GOPATH}/bin/go-fuzz-build
 go-fuzz-corpus := ${GOPATH}/src/github.com/dvyukov/go-fuzz-corpus
 go-fuzz-dep := ${GOPATH}/src/github.com/dvyukov/go-fuzz/go-fuzz-dep
 
-test:
-	go test -v -cover ./ascii
-	go test -v -cover ./json
-	go test -v -cover ./proto
-	go test -v -cover -tags go1.15 ./json
-	go test -v -cover ./iso8601
-	go run ./json/bugs/issue11/main.go
-	go run ./json/bugs/issue18/main.go
+test: test-ascii test-json test-json-bugs test-json-1.17 test-proto test-iso8601
+
+test-ascii:
+	go test -cover -race ./ascii
+
+test-json:
+	go test -cover -race ./json
+
+test-json-bugs:
+	go test -cover -race ./json/bugs/...
+
+test-json-1.17:
+	go test -cover -race -tags go1.17 ./json
+
+test-proto:
+	go test -cover -race ./proto
+
+test-iso8601:
+	go test -cover -race ./iso8601
 
 $(benchstat):
 	GO111MODULE=off go get -u golang.org/x/perf/cmd/benchstat
diff --git a/ascii/ascii.go b/ascii/ascii.go
index e20b4f8..17a1a60 100644
--- a/ascii/ascii.go
+++ b/ascii/ascii.go
@@ -35,22 +35,18 @@ const (
 	hasMoreConstR32 = hasMoreConstL32 * 128
 )
 
-//go:nosplit
 func hasLess64(x, n uint64) bool {
 	return ((x - (hasLessConstL64 * n)) & ^x & hasLessConstR64) != 0
 }
 
-//go:nosplit
 func hasLess32(x, n uint32) bool {
 	return ((x - (hasLessConstL32 * n)) & ^x & hasLessConstR32) != 0
 }
 
-//go:nosplit
 func hasMore64(x, n uint64) bool {
 	return (((x + (hasMoreConstL64 * (127 - n))) | x) & hasMoreConstR64) != 0
 }
 
-//go:nosplit
 func hasMore32(x, n uint32) bool {
 	return (((x + (hasMoreConstL32 * (127 - n))) | x) & hasMoreConstR32) != 0
 }
diff --git a/ascii/equal_fold.go b/ascii/equal_fold.go
index 584e4c3..c1dd49b 100644
--- a/ascii/equal_fold.go
+++ b/ascii/equal_fold.go
@@ -35,32 +35,41 @@ func EqualFoldString(a, b string) bool {
 	n := uintptr(len(a))
 	p := *(*unsafe.Pointer)(unsafe.Pointer(&a))
 	q := *(*unsafe.Pointer)(unsafe.Pointer(&b))
-	// If there is more than 32 bytes to copy, use the AVX optimized version,
-	// otherwise the overhead of the function call tends to be greater than
-	// looping 2 or 3 times over 8 bytes.
-	if n >= 32 && asm.equalFoldAVX2 != nil {
-		if asm.equalFoldAVX2((*byte)(p), (*byte)(q), n) == 0 {
-			return false
+	// Pre-check to avoid the other tests that would all evaluate to false.
+	// For very small strings, this helps reduce the processing overhead.
+	if n >= 8 {
+		// If there is more than 32 bytes to copy, use the AVX optimized version,
+		// otherwise the overhead of the function call tends to be greater than
+		// looping 2 or 3 times over 8 bytes.
+		if n > 32 && asm.equalFoldAVX2 != nil {
+			if asm.equalFoldAVX2((*byte)(p), (*byte)(q), n) == 0 {
+				return false
+			}
+			k := (n / 16) * 16
+			p = unsafe.Pointer(uintptr(p) + k)
+			q = unsafe.Pointer(uintptr(q) + k)
+			n -= k
 		}
-		k := (n / 16) * 16
-		p = unsafe.Pointer(uintptr(p) + k)
-		q = unsafe.Pointer(uintptr(q) + k)
-		n -= k
-	}
 
-	for n >= 8 {
-		const mask = 0xDFDFDFDFDFDFDFDF
+		for n > 8 {
+			const mask = 0xDFDFDFDFDFDFDFDF
 
-		if (*(*uint64)(p) & mask) != (*(*uint64)(q) & mask) {
-			return false
+			if (*(*uint64)(p) & mask) != (*(*uint64)(q) & mask) {
+				return false
+			}
+
+			p = unsafe.Pointer(uintptr(p) + 8)
+			q = unsafe.Pointer(uintptr(q) + 8)
+			n -= 8
 		}
 
-		p = unsafe.Pointer(uintptr(p) + 8)
-		q = unsafe.Pointer(uintptr(q) + 8)
-		n -= 8
+		if n == 8 {
+			const mask = 0xDFDFDFDFDFDFDFDF
+			return (*(*uint64)(p) & mask) == (*(*uint64)(q) & mask)
+		}
 	}
 
-	if n >= 4 {
+	if n > 4 {
 		const mask = 0xDFDFDFDF
 
 		if (*(*uint32)(p) & mask) != (*(*uint32)(q) & mask) {
@@ -73,6 +82,8 @@ func EqualFoldString(a, b string) bool {
 	}
 
 	switch n {
+	case 4:
+		return (*(*uint32)(p) & 0xDFDFDFDF) == (*(*uint32)(q) & 0xDFDFDFDF)
 	case 3:
 		x := uint32(*(*uint16)(p)) | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + 2)))<<16
 		y := uint32(*(*uint16)(q)) | uint32(*(*uint8)(unsafe.Pointer(uintptr(q) + 2)))<<16
diff --git a/ascii/valid.go b/ascii/valid.go
index 35310e0..36f7b7e 100644
--- a/ascii/valid.go
+++ b/ascii/valid.go
@@ -23,24 +23,30 @@ func ValidString(s string) bool {
 	p := *(*unsafe.Pointer)(unsafe.Pointer(&s))
 	n := uintptr(len(s))
 
-	if n >= 32 && asm.validAVX2 != nil {
-		if asm.validAVX2((*byte)(p), n) == 0 {
-			return false
+	if n >= 8 {
+		if n > 32 && asm.validAVX2 != nil {
+			if asm.validAVX2((*byte)(p), n) == 0 {
+				return false
+			}
+			k := (n / 16) * 16
+			p = unsafe.Pointer(uintptr(p) + k)
+			n -= k
 		}
-		k := (n / 16) * 16
-		p = unsafe.Pointer(uintptr(p) + k)
-		n -= k
-	}
 
-	for n >= 8 {
-		if (*(*uint64)(p) & 0x8080808080808080) != 0 {
-			return false
+		for n > 8 {
+			if (*(*uint64)(p) & 0x8080808080808080) != 0 {
+				return false
+			}
+			p = unsafe.Pointer(uintptr(p) + 8)
+			n -= 8
+		}
+
+		if n == 8 {
+			return (*(*uint64)(p) & 0x8080808080808080) == 0
 		}
-		p = unsafe.Pointer(uintptr(p) + 8)
-		n -= 8
 	}
 
-	if n >= 4 {
+	if n > 4 {
 		if (*(*uint32)(p) & 0x80808080) != 0 {
 			return false
 		}
@@ -50,6 +56,8 @@ func ValidString(s string) bool {
 
 	var x uint32
 	switch n {
+	case 4:
+		x = *(*uint32)(p)
 	case 3:
 		x = uint32(*(*uint16)(p)) | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + 2)))<<16
 	case 2:
diff --git a/ascii/valid_print.go b/ascii/valid_print.go
index bd2bdf9..474c69b 100644
--- a/ascii/valid_print.go
+++ b/ascii/valid_print.go
@@ -23,24 +23,33 @@ func ValidPrintString(s string) bool {
 	p := *(*unsafe.Pointer)(unsafe.Pointer(&s))
 	n := uintptr(len(s))
 
-	if n >= 32 && asm.validPrintAVX2 != nil {
-		if asm.validPrintAVX2((*byte)(p), n) == 0 {
-			return false
+	if n >= 8 {
+		if n > 32 && asm.validPrintAVX2 != nil {
+			if asm.validPrintAVX2((*byte)(p), n) == 0 {
+				return false
+			}
+			if (n % 16) == 0 {
+				return true
+			}
+			k := (n / 16) * 16
+			p = unsafe.Pointer(uintptr(p) + k)
+			n -= k
 		}
-		k := (n / 16) * 16
-		p = unsafe.Pointer(uintptr(p) + k)
-		n -= k
-	}
 
-	for n >= 8 {
-		if hasLess64(*(*uint64)(p), 0x20) || hasMore64(*(*uint64)(p), 0x7e) {
-			return false
+		for n > 8 {
+			if hasLess64(*(*uint64)(p), 0x20) || hasMore64(*(*uint64)(p), 0x7e) {
+				return false
+			}
+			p = unsafe.Pointer(uintptr(p) + 8)
+			n -= 8
+		}
+
+		if n == 8 {
+			return !(hasLess64(*(*uint64)(p), 0x20) || hasMore64(*(*uint64)(p), 0x7e))
 		}
-		p = unsafe.Pointer(uintptr(p) + 8)
-		n -= 8
 	}
 
-	if n >= 4 {
+	if n > 4 {
 		if hasLess32(*(*uint32)(p), 0x20) || hasMore32(*(*uint32)(p), 0x7e) {
 			return false
 		}
@@ -50,6 +59,8 @@ func ValidPrintString(s string) bool {
 
 	var x uint32
 	switch n {
+	case 4:
+		x = 0x20202020 | *(*uint32)(p)
 	case 3:
 		x = 0x20000000 | uint32(*(*uint16)(p)) | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + 2)))<<16
 	case 2:
diff --git a/internal/runtime_reflect/map.go b/internal/runtime_reflect/map.go
index 9893fa4..b682acc 100644
--- a/internal/runtime_reflect/map.go
+++ b/internal/runtime_reflect/map.go
@@ -16,12 +16,12 @@ func Assign(typ, dst, src unsafe.Pointer) {
 	typedmemmove(typ, dst, src)
 }
 
-func MapAssign(t, m, k unsafe.Pointer) uintptr {
-	return uintptr(mapassign(t, m, k))
+func MapAssign(t, m, k unsafe.Pointer) unsafe.Pointer {
+	return mapassign(t, m, k)
 }
 
-func MakeMap(t unsafe.Pointer, cap int) uintptr {
-	return uintptr(makemap(t, cap))
+func MakeMap(t unsafe.Pointer, cap int) unsafe.Pointer {
+	return makemap(t, cap)
 }
 
 type MapIter struct{ hiter }
@@ -45,9 +45,9 @@ func (it *MapIter) HasNext() bool {
 	return it.key != nil
 }
 
-func (it *MapIter) Key() uintptr { return uintptr(it.key) }
+func (it *MapIter) Key() unsafe.Pointer { return it.key }
 
-func (it *MapIter) Value() uintptr { return uintptr(it.value) }
+func (it *MapIter) Value() unsafe.Pointer { return it.value }
 
 // copied from src/runtime/map.go, all pointer types replaced with
 // unsafe.Pointer.
diff --git a/internal/runtime_reflect/slice.go b/internal/runtime_reflect/slice.go
index db68a7f..b89fc89 100644
--- a/internal/runtime_reflect/slice.go
+++ b/internal/runtime_reflect/slice.go
@@ -20,8 +20,8 @@ func (s *Slice) SetLen(n int) {
 	s.len = n
 }
 
-func (s *Slice) Index(i int, elemSize uintptr) uintptr {
-	return uintptr(s.data) + (uintptr(i) * elemSize)
+func (s *Slice) Index(i int, elemSize uintptr) unsafe.Pointer {
+	return unsafe.Pointer(uintptr(s.data) + (uintptr(i) * elemSize))
 }
 
 func MakeSlice(elemType unsafe.Pointer, len, cap int) Slice {
diff --git a/json/bugs/issue11/main.go b/json/bugs/issue11/main.go
index dcafe47..38dd16d 100644
--- a/json/bugs/issue11/main.go
+++ b/json/bugs/issue11/main.go
@@ -1,22 +1,3 @@
 package main
 
-import (
-	"fmt"
-	"log"
-
-	"github.com/segmentio/encoding/json"
-)
-
-func main() {
-	m := map[string]map[string]interface{}{
-		"outerkey": {
-			"innerkey": "innervalue",
-		},
-	}
-
-	b, err := json.Marshal(m)
-	if err != nil {
-		log.Fatal(err)
-	}
-	fmt.Println(string(b))
-}
+func main() {}
diff --git a/json/bugs/issue18/main.go b/json/bugs/issue18/main.go
index eda5349..da29a2c 100644
--- a/json/bugs/issue18/main.go
+++ b/json/bugs/issue18/main.go
@@ -1,22 +1,4 @@
 package main
 
-import (
-	"bytes"
-	"fmt"
-
-	"github.com/segmentio/encoding/json"
-)
-
 func main() {
-	b := []byte(`{
-	"userId": "blah",
-	}`)
-
-	d := json.NewDecoder(bytes.NewReader(b))
-
-	var a struct {
-		UserId string `json:"userId"`
-	}
-	fmt.Println(d.Decode(&a))
-	fmt.Println(a)
 }
diff --git a/json/bugs/issue84/main.go b/json/bugs/issue84/main.go
new file mode 100644
index 0000000..38dd16d
--- /dev/null
+++ b/json/bugs/issue84/main.go
@@ -0,0 +1,3 @@
+package main
+
+func main() {}
diff --git a/json/codec.go b/json/codec.go
index 5e06a99..22d35fd 100644
--- a/json/codec.go
+++ b/json/codec.go
@@ -844,7 +844,7 @@ func isValidTag(s string) bool {
 	}
 	for _, c := range s {
 		switch {
-		case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
+		case strings.ContainsRune("!#$%&()*+-./:;<=>?@[]^_{|}~ ", c):
 			// Backslash and quote chars are reserved, but
 			// otherwise any punctuation chars are allowed
 			// in a tag name.
diff --git a/proto/map.go b/proto/map.go
index c9d3dae..8912afd 100644
--- a/proto/map.go
+++ b/proto/map.go
@@ -50,8 +50,8 @@ func mapSizeFuncOf(t reflect.Type, f *mapField) sizeFunc {
 		defer m.Done()
 
 		for m.Init(pointer(t), p); m.HasNext(); m.Next() {
-			keySize := f.keyCodec.size(unsafe.Pointer(m.Key()), wantzero)
-			valSize := f.valCodec.size(unsafe.Pointer(m.Value()), wantzero)
+			keySize := f.keyCodec.size(m.Key(), wantzero)
+			valSize := f.valCodec.size(m.Value(), wantzero)
 
 			if keySize > 0 {
 				n += keyTagSize + keySize
@@ -105,8 +105,8 @@ func mapEncodeFuncOf(t reflect.Type, f *mapField) encodeFunc {
 		defer m.Done()
 
 		for m.Init(pointer(t), p); m.HasNext(); m.Next() {
-			key := unsafe.Pointer(m.Key())
-			val := unsafe.Pointer(m.Value())
+			key := m.Key()
+			val := m.Value()
 
 			keySize := f.keyCodec.size(key, wantzero)
 			valSize := f.valCodec.size(val, wantzero)
@@ -220,7 +220,7 @@ func mapDecodeFuncOf(t reflect.Type, f *mapField, seen map[reflect.Type]*codec)
 	return func(b []byte, p unsafe.Pointer, _ flags) (int, error) {
 		m := (*unsafe.Pointer)(p)
 		if *m == nil {
-			*m = unsafe.Pointer(MakeMap(mtype, 10))
+			*m = MakeMap(mtype, 10)
 		}
 		if len(b) == 0 {
 			return 0, nil
@@ -233,7 +233,7 @@ func mapDecodeFuncOf(t reflect.Type, f *mapField, seen map[reflect.Type]*codec)
 
 		n, err := structCodec.decode(b, s, noflags)
 		if err == nil {
-			v := unsafe.Pointer(MapAssign(mtype, *m, s))
+			v := MapAssign(mtype, *m, s)
 			Assign(vtype, v, unsafe.Pointer(uintptr(s)+valueOffset))
 		}
 
diff --git a/proto/slice.go b/proto/slice.go
index b8b954b..4778a7c 100644
--- a/proto/slice.go
+++ b/proto/slice.go
@@ -41,7 +41,7 @@ func sliceSizeFuncOf(t reflect.Type, r *repeatedField) sizeFunc {
 
 		if v := (*Slice)(p); v != nil {
 			for i := 0; i < v.Len(); i++ {
-				elem := unsafe.Pointer(v.Index(i, elemSize))
+				elem := v.Index(i, elemSize)
 				size := r.codec.size(elem, wantzero)
 				n += tagSize + size
 				if r.embedded {
@@ -64,7 +64,7 @@ func sliceEncodeFuncOf(t reflect.Type, r *repeatedField) encodeFunc {
 
 		if s := (*Slice)(p); s != nil {
 			for i := 0; i < s.Len(); i++ {
-				elem := unsafe.Pointer(s.Index(i, elemSize))
+				elem := s.Index(i, elemSize)
 				size := r.codec.size(elem, wantzero)
 
 				n := copy(b[offset:], tagData)
@@ -108,7 +108,7 @@ func sliceDecodeFuncOf(t reflect.Type, r *repeatedField) decodeFunc {
 			*s = growSlice(elemType, s)
 		}
 
-		n, err := r.codec.decode(b, unsafe.Pointer(s.Index(i, elemSize)), noflags)
+		n, err := r.codec.decode(b, s.Index(i, elemSize), noflags)
 		if err == nil {
 			s.SetLen(i + 1)
 		}


Test patch:
diff --git a/json/bugs/issue11/main_test.go b/json/bugs/issue11/main_test.go
new file mode 100644
index 0000000..eeae147
--- /dev/null
+++ b/json/bugs/issue11/main_test.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"testing"
+
+	"github.com/segmentio/encoding/json"
+)
+
+func TestIssue11(t *testing.T) {
+	m := map[string]map[string]interface{}{
+		"outerkey": {
+			"innerkey": "innervalue",
+		},
+	}
+
+	b, err := json.Marshal(m)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(string(b))
+}
diff --git a/json/bugs/issue18/main_test.go b/json/bugs/issue18/main_test.go
new file mode 100644
index 0000000..cca1b5d
--- /dev/null
+++ b/json/bugs/issue18/main_test.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/segmentio/encoding/json"
+)
+
+func TestIssue18(t *testing.T) {
+	b := []byte(`{
+	"userId": "blah",
+	}`)
+
+	d := json.NewDecoder(bytes.NewReader(b))
+
+	var a struct {
+		UserId string `json:"userId"`
+	}
+	fmt.Println(d.Decode(&a))
+	fmt.Println(a)
+}
diff --git a/json/bugs/issue84/main_test.go b/json/bugs/issue84/main_test.go
new file mode 100644
index 0000000..981cbba
--- /dev/null
+++ b/json/bugs/issue84/main_test.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"testing"
+
+	"github.com/segmentio/encoding/json"
+)
+
+type Foo struct {
+	Source struct {
+		Table string
+	}
+}
+
+func TestUnmarshal(t *testing.T) {
+	input := []byte(`{"source": {"table": "1234567"}}`)
+	r := &Foo{}
+	json.Unmarshal(input, r)
+}
diff --git a/json/golang_tagkey_test.go b/json/golang_tagkey_test.go
index f77c49c..17d4c03 100644
--- a/json/golang_tagkey_test.go
+++ b/json/golang_tagkey_test.go
@@ -41,7 +41,7 @@ type percentSlashTag struct {
 }
 
 type punctuationTag struct {
-	V string `json:"!#$%&()*+-./:<=>?@[]^_{|}~"` // https://golang.org/issue/3546
+	V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~"` // https://golang.org/issue/3546
 }
 
 type dashTag struct {
@@ -90,7 +90,7 @@ var structTagObjectKeyTests = []struct {
 	{badFormatTag{"Orfevre"}, "Orfevre", "Y"},
 	{badCodeTag{"Reliable Man"}, "Reliable Man", "Z"},
 	{percentSlashTag{"brut"}, "brut", "text/html%"},
-	{punctuationTag{"Union Rags"}, "Union Rags", "!#$%&()*+-./:<=>?@[]^_{|}~"},
+	{punctuationTag{"Union Rags"}, "Union Rags", "!#$%&()*+-./:;<=>?@[]^_{|}~"},
 	{spaceTag{"Perreddu"}, "Perreddu", "With space"},
 	{unicodeTag{"Loukanikos"}, "Loukanikos", "Ελλάδα"},
 }