diff --git a/ascii.go b/ascii.go new file mode 100644 index 0000000..6c66eb5 --- /dev/null +++ b/ascii.go @@ -0,0 +1,124 @@ +package jsoncolor + +import "unsafe" + +// asciiValid returns true if b contains only ASCII characters. +// +// From https://github.com/segmentio/encoding/blob/v0.1.14/ascii/valid.go#L28 +// +//go:nosplit +func asciiValid(b []byte) bool { + s, n := unsafe.Pointer(&b), uintptr(len(b)) + + i := uintptr(0) + p := *(*unsafe.Pointer)(s) + + for n >= 8 { + if ((*(*uint64)(unsafe.Pointer(uintptr(p) + i))) & 0x8080808080808080) != 0 { + return false + } + i += 8 + n -= 8 + } + + if n >= 4 { + if ((*(*uint32)(unsafe.Pointer(uintptr(p) + i))) & 0x80808080) != 0 { + return false + } + i += 4 + n -= 4 + } + + var x uint32 + switch n { + case 3: + x = uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i + 1)))<<8 + case 2: + x = uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i))) + case 1: + x = uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) + default: + return true + } + return (x & 0x80808080) == 0 +} + +// asciiValidPrint returns true if b contains only printable ASCII characters. +// +// From https://github.com/segmentio/encoding/blob/v0.1.14/ascii/valid.go#L83 +// +//go:nosplit +func asciiValidPrint(b []byte) bool { + s, n := unsafe.Pointer(&b), uintptr(len(b)) + + if n == 0 { + return true + } + + i := uintptr(0) + p := *(*unsafe.Pointer)(s) + + for (n - i) >= 8 { + x := *(*uint64)(unsafe.Pointer(uintptr(p) + i)) + if hasLess64(x, 0x20) || hasMore64(x, 0x7e) { + return false + } + i += 8 + } + + if (n - i) >= 4 { + x := *(*uint32)(unsafe.Pointer(uintptr(p) + i)) + if hasLess32(x, 0x20) || hasMore32(x, 0x7e) { + return false + } + i += 4 + } + + var x uint32 + switch n - i { + case 3: + x = 0x20000000 | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i + 1)))<<8 + case 2: + x = 0x20200000 | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i))) + case 1: + x = 0x20202000 | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) + default: + return true + } + return !(hasLess32(x, 0x20) || hasMore32(x, 0x7e)) +} + +// https://graphics.stanford.edu/~seander/bithacks.html#HasLessInWord +const ( + hasLessConstL64 = (^uint64(0)) / 255 + hasLessConstR64 = hasLessConstL64 * 128 + + hasLessConstL32 = (^uint32(0)) / 255 + hasLessConstR32 = hasLessConstL32 * 128 + + hasMoreConstL64 = (^uint64(0)) / 255 + hasMoreConstR64 = hasMoreConstL64 * 128 + + hasMoreConstL32 = (^uint32(0)) / 255 + 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_test.go b/ascii_test.go new file mode 100644 index 0000000..9f9d306 --- /dev/null +++ b/ascii_test.go @@ -0,0 +1,65 @@ +package jsoncolor + +import ( + "strings" + "testing" +) + +// Based on https://github.com/segmentio/encoding/blob/v0.1.14/ascii/valid_test.go +var testCases = [...]struct { + valid bool + validPrint bool + str string +}{ + {valid: true, validPrint: true, str: ""}, + {valid: true, validPrint: true, str: "hello"}, + {valid: true, validPrint: true, str: "Hello World!"}, + {valid: true, validPrint: true, str: "Hello\"World!"}, + {valid: true, validPrint: true, str: "Hello\\World!"}, + {valid: true, validPrint: false, str: "Hello\nWorld!"}, + {valid: true, validPrint: false, str: "Hello\rWorld!"}, + {valid: true, validPrint: false, str: "Hello\tWorld!"}, + {valid: true, validPrint: false, str: "Hello\bWorld!"}, + {valid: true, validPrint: false, str: "Hello\fWorld!"}, + {valid: true, validPrint: true, str: "H~llo World!"}, + {valid: true, validPrint: true, str: "H~llo"}, + {valid: false, validPrint: false, str: "你好"}, + {valid: true, validPrint: true, str: "~"}, + {valid: false, validPrint: false, str: "\x80"}, + {valid: true, validPrint: false, str: "\x7F"}, + {valid: false, validPrint: false, str: "\xFF"}, + {valid: true, validPrint: true, str: "some kind of long string with only ascii characters."}, + {valid: false, validPrint: false, str: "some kind of long string with a non-ascii character at the end.\xff"}, + {valid: true, validPrint: true, str: strings.Repeat("1234567890", 1000)}, +} + +func TestAsciiValid(t *testing.T) { + for _, tc := range testCases { + t.Run(limit(tc.str), func(t *testing.T) { + expect := tc.validPrint + + if valid := asciiValidPrint([]byte(tc.str)); expect != valid { + t.Errorf("expected %t but got %t", expect, valid) + } + }) + } +} + +func TestAsciiValidPrint(t *testing.T) { + for _, tc := range testCases { + t.Run(limit(tc.str), func(t *testing.T) { + expect := tc.validPrint + + if valid := asciiValidPrint([]byte(tc.str)); expect != valid { + t.Errorf("expected %t but got %t", expect, valid) + } + }) + } +} + +func limit(s string) string { + if len(s) > 17 { + return s[:17] + "..." + } + return s +} diff --git a/go.mod b/go.mod index 985ced6..5099b58 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,10 @@ require ( github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.8 - github.com/mattn/go-isatty v0.0.14 + github.com/mattn/go-isatty v0.0.20 github.com/nwidger/jsoncolor v0.3.0 github.com/segmentio/encoding v0.1.14 github.com/stretchr/testify v1.7.0 - golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 // indirect - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b + golang.org/x/sys v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 095613d..dfb2105 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/nwidger/jsoncolor v0.3.0 h1:VdTH8Dc0SJoq4pJ8pRxxFZW0/5Ng5akbN4YToCBJDSU= github.com/nwidger/jsoncolor v0.3.0/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -32,12 +34,12 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 h1:7ZDGnxgHAMw7thfC5bEos0RDAccZKxioiWBhfIe+tvw= golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/jsoncolor.go b/jsoncolor.go index 3d2b189..2edc941 100644 --- a/jsoncolor.go +++ b/jsoncolor.go @@ -6,8 +6,6 @@ import ( "strconv" "github.com/mattn/go-isatty" - - "golang.org/x/term" ) // Colors specifies colorization of JSON output. Each field @@ -153,32 +151,23 @@ func IsColorTerminal(w io.Writer) bool { return false } - if !isTerminal(w) { + f, ok := w.(*os.File) + if !ok { return false } + fd := f.Fd() - if os.Getenv("TERM") == "dumb" { + if !isatty.IsTerminal(fd) { return false } - f, ok := w.(*os.File) - if !ok { + if os.Getenv("TERM") == "dumb" { return false } - if isatty.IsCygwinTerminal(f.Fd()) { + if isatty.IsCygwinTerminal(fd) { return false } return true } - -// isTerminal returns true if w is a terminal. -func isTerminal(w io.Writer) bool { - switch v := w.(type) { - case *os.File: - return term.IsTerminal(int(v.Fd())) - default: - return false - } -} diff --git a/parse.go b/parse.go index 38102fa..0f43b4c 100644 --- a/parse.go +++ b/parse.go @@ -7,8 +7,6 @@ import ( "unicode" "unicode/utf16" "unicode/utf8" - - "github.com/segmentio/encoding/ascii" ) // All spaces characters defined in the json specification. @@ -398,7 +396,7 @@ func parseStringFast(b []byte) ([]byte, []byte, bool, error) { if n <= 1 { return nil, b[len(b):], false, syntaxError(b, "missing '\"' at the end of a string value") } - if bytes.IndexByte(b[1:n], '\\') < 0 && ascii.ValidPrint(b[1:n]) { + if bytes.IndexByte(b[1:n], '\\') < 0 && asciiValidPrint(b[1:n]) { return b[:n], b[n:], false, nil } @@ -706,7 +704,7 @@ func hasLeadingZeroes(b []byte) bool { } func appendToLower(b, s []byte) []byte { - if ascii.Valid(s) { // fast path for ascii strings + if asciiValid(s) { // fast path for ascii strings i := 0 for j := range s {