Skip to content

Commit bd848aa

Browse files
lobattfjl
andauthored
common: improve printing of Hash and Address (#21834)
Both Hash and Address have a String method, which returns the value as hex with 0x prefix. They also had a Format method which tried to print the value using printf of []byte. The way Format worked was at odds with String though, leading to a situation where fmt.Sprintf("%v", hash) returned the decimal notation and hash.String() returned a hex string. This commit makes it consistent again. Both types now support the %v, %s, %q format verbs for 0x-prefixed hex output. %x, %X creates unprefixed hex output. %d is also supported and returns the decimal notation "[1 2 3...]". For Address, the case of hex characters in %v, %s, %q output is determined using the EIP-55 checksum. Using %x, %X with Address disables checksumming. Co-authored-by: Felix Lange <[email protected]>
1 parent ed0670c commit bd848aa

File tree

2 files changed

+242
-18
lines changed

2 files changed

+242
-18
lines changed

common/types.go

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package common
1818

1919
import (
20+
"bytes"
2021
"database/sql/driver"
2122
"encoding/hex"
2223
"encoding/json"
@@ -84,10 +85,34 @@ func (h Hash) String() string {
8485
return h.Hex()
8586
}
8687

87-
// Format implements fmt.Formatter, forcing the byte slice to be formatted as is,
88-
// without going through the stringer interface used for logging.
88+
// Format implements fmt.Formatter.
89+
// Hash supports the %v, %s, %v, %x, %X and %d format verbs.
8990
func (h Hash) Format(s fmt.State, c rune) {
90-
fmt.Fprintf(s, "%"+string(c), h[:])
91+
hexb := make([]byte, 2+len(h)*2)
92+
copy(hexb, "0x")
93+
hex.Encode(hexb[2:], h[:])
94+
95+
switch c {
96+
case 'x', 'X':
97+
if !s.Flag('#') {
98+
hexb = hexb[2:]
99+
}
100+
if c == 'X' {
101+
hexb = bytes.ToUpper(hexb)
102+
}
103+
fallthrough
104+
case 'v', 's':
105+
s.Write(hexb)
106+
case 'q':
107+
q := []byte{'"'}
108+
s.Write(q)
109+
s.Write(hexb)
110+
s.Write(q)
111+
case 'd':
112+
fmt.Fprint(s, ([len(h)]byte)(h))
113+
default:
114+
fmt.Fprintf(s, "%%!%c(hash=%x)", c, h)
115+
}
91116
}
92117

93118
// UnmarshalText parses a hash in hex syntax.
@@ -208,35 +233,68 @@ func (a Address) Hash() Hash { return BytesToHash(a[:]) }
208233

209234
// Hex returns an EIP55-compliant hex string representation of the address.
210235
func (a Address) Hex() string {
211-
unchecksummed := hex.EncodeToString(a[:])
236+
return string(a.checksumHex())
237+
}
238+
239+
// String implements fmt.Stringer.
240+
func (a Address) String() string {
241+
return a.Hex()
242+
}
243+
244+
func (a *Address) checksumHex() []byte {
245+
buf := a.hex()
246+
247+
// compute checksum
212248
sha := sha3.NewLegacyKeccak256()
213-
sha.Write([]byte(unchecksummed))
249+
sha.Write(buf[2:])
214250
hash := sha.Sum(nil)
215-
216-
result := []byte(unchecksummed)
217-
for i := 0; i < len(result); i++ {
218-
hashByte := hash[i/2]
251+
for i := 2; i < len(buf); i++ {
252+
hashByte := hash[(i-2)/2]
219253
if i%2 == 0 {
220254
hashByte = hashByte >> 4
221255
} else {
222256
hashByte &= 0xf
223257
}
224-
if result[i] > '9' && hashByte > 7 {
225-
result[i] -= 32
258+
if buf[i] > '9' && hashByte > 7 {
259+
buf[i] -= 32
226260
}
227261
}
228-
return "0x" + string(result)
262+
return buf[:]
229263
}
230264

231-
// String implements fmt.Stringer.
232-
func (a Address) String() string {
233-
return a.Hex()
265+
func (a Address) hex() []byte {
266+
var buf [len(a)*2 + 2]byte
267+
copy(buf[:2], "0x")
268+
hex.Encode(buf[2:], a[:])
269+
return buf[:]
234270
}
235271

236-
// Format implements fmt.Formatter, forcing the byte slice to be formatted as is,
237-
// without going through the stringer interface used for logging.
272+
// Format implements fmt.Formatter.
273+
// Address supports the %v, %s, %v, %x, %X and %d format verbs.
238274
func (a Address) Format(s fmt.State, c rune) {
239-
fmt.Fprintf(s, "%"+string(c), a[:])
275+
switch c {
276+
case 'v', 's':
277+
s.Write(a.checksumHex())
278+
case 'q':
279+
q := []byte{'"'}
280+
s.Write(q)
281+
s.Write(a.checksumHex())
282+
s.Write(q)
283+
case 'x', 'X':
284+
// %x disables the checksum.
285+
hex := a.hex()
286+
if !s.Flag('#') {
287+
hex = hex[2:]
288+
}
289+
if c == 'X' {
290+
hex = bytes.ToUpper(hex)
291+
}
292+
s.Write(hex)
293+
case 'd':
294+
fmt.Fprint(s, ([len(a)]byte)(a))
295+
default:
296+
fmt.Fprintf(s, "%%!%c(address=%x)", c, a)
297+
}
240298
}
241299

242300
// SetBytes sets the address to the value of b.

common/types_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package common
1818

1919
import (
20+
"bytes"
2021
"database/sql/driver"
2122
"encoding/json"
23+
"fmt"
2224
"math/big"
2325
"reflect"
2426
"strings"
@@ -371,3 +373,167 @@ func TestAddress_Value(t *testing.T) {
371373
})
372374
}
373375
}
376+
377+
func TestAddress_Format(t *testing.T) {
378+
b := []byte{
379+
0xb2, 0x6f, 0x2b, 0x34, 0x2a, 0xab, 0x24, 0xbc, 0xf6, 0x3e,
380+
0xa2, 0x18, 0xc6, 0xa9, 0x27, 0x4d, 0x30, 0xab, 0x9a, 0x15,
381+
}
382+
var addr Address
383+
addr.SetBytes(b)
384+
385+
tests := []struct {
386+
name string
387+
out string
388+
want string
389+
}{
390+
{
391+
name: "println",
392+
out: fmt.Sprintln(addr),
393+
want: "0xB26f2b342AAb24BCF63ea218c6A9274D30Ab9A15\n",
394+
},
395+
{
396+
name: "print",
397+
out: fmt.Sprint(addr),
398+
want: "0xB26f2b342AAb24BCF63ea218c6A9274D30Ab9A15",
399+
},
400+
{
401+
name: "printf-s",
402+
out: func() string {
403+
buf := new(bytes.Buffer)
404+
fmt.Fprintf(buf, "%s", addr)
405+
return buf.String()
406+
}(),
407+
want: "0xB26f2b342AAb24BCF63ea218c6A9274D30Ab9A15",
408+
},
409+
{
410+
name: "printf-q",
411+
out: fmt.Sprintf("%q", addr),
412+
want: `"0xB26f2b342AAb24BCF63ea218c6A9274D30Ab9A15"`,
413+
},
414+
{
415+
name: "printf-x",
416+
out: fmt.Sprintf("%x", addr),
417+
want: "b26f2b342aab24bcf63ea218c6a9274d30ab9a15",
418+
},
419+
{
420+
name: "printf-X",
421+
out: fmt.Sprintf("%X", addr),
422+
want: "B26F2B342AAB24BCF63EA218C6A9274D30AB9A15",
423+
},
424+
{
425+
name: "printf-#x",
426+
out: fmt.Sprintf("%#x", addr),
427+
want: "0xb26f2b342aab24bcf63ea218c6a9274d30ab9a15",
428+
},
429+
{
430+
name: "printf-v",
431+
out: fmt.Sprintf("%v", addr),
432+
want: "0xB26f2b342AAb24BCF63ea218c6A9274D30Ab9A15",
433+
},
434+
// The original default formatter for byte slice
435+
{
436+
name: "printf-d",
437+
out: fmt.Sprintf("%d", addr),
438+
want: "[178 111 43 52 42 171 36 188 246 62 162 24 198 169 39 77 48 171 154 21]",
439+
},
440+
// Invalid format char.
441+
{
442+
name: "printf-t",
443+
out: fmt.Sprintf("%t", addr),
444+
want: "%!t(address=b26f2b342aab24bcf63ea218c6a9274d30ab9a15)",
445+
},
446+
}
447+
for _, tt := range tests {
448+
t.Run(tt.name, func(t *testing.T) {
449+
if tt.out != tt.want {
450+
t.Errorf("%s does not render as expected:\n got %s\nwant %s", tt.name, tt.out, tt.want)
451+
}
452+
})
453+
}
454+
}
455+
456+
func TestHash_Format(t *testing.T) {
457+
var hash Hash
458+
hash.SetBytes([]byte{
459+
0xb2, 0x6f, 0x2b, 0x34, 0x2a, 0xab, 0x24, 0xbc, 0xf6, 0x3e,
460+
0xa2, 0x18, 0xc6, 0xa9, 0x27, 0x4d, 0x30, 0xab, 0x9a, 0x15,
461+
0xa2, 0x18, 0xc6, 0xa9, 0x27, 0x4d, 0x30, 0xab, 0x9a, 0x15,
462+
0x10, 0x00,
463+
})
464+
465+
tests := []struct {
466+
name string
467+
out string
468+
want string
469+
}{
470+
{
471+
name: "println",
472+
out: fmt.Sprintln(hash),
473+
want: "0xb26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000\n",
474+
},
475+
{
476+
name: "print",
477+
out: fmt.Sprint(hash),
478+
want: "0xb26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000",
479+
},
480+
{
481+
name: "printf-s",
482+
out: func() string {
483+
buf := new(bytes.Buffer)
484+
fmt.Fprintf(buf, "%s", hash)
485+
return buf.String()
486+
}(),
487+
want: "0xb26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000",
488+
},
489+
{
490+
name: "printf-q",
491+
out: fmt.Sprintf("%q", hash),
492+
want: `"0xb26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000"`,
493+
},
494+
{
495+
name: "printf-x",
496+
out: fmt.Sprintf("%x", hash),
497+
want: "b26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000",
498+
},
499+
{
500+
name: "printf-X",
501+
out: fmt.Sprintf("%X", hash),
502+
want: "B26F2B342AAB24BCF63EA218C6A9274D30AB9A15A218C6A9274D30AB9A151000",
503+
},
504+
{
505+
name: "printf-#x",
506+
out: fmt.Sprintf("%#x", hash),
507+
want: "0xb26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000",
508+
},
509+
{
510+
name: "printf-#X",
511+
out: fmt.Sprintf("%#X", hash),
512+
want: "0XB26F2B342AAB24BCF63EA218C6A9274D30AB9A15A218C6A9274D30AB9A151000",
513+
},
514+
{
515+
name: "printf-v",
516+
out: fmt.Sprintf("%v", hash),
517+
want: "0xb26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000",
518+
},
519+
// The original default formatter for byte slice
520+
{
521+
name: "printf-d",
522+
out: fmt.Sprintf("%d", hash),
523+
want: "[178 111 43 52 42 171 36 188 246 62 162 24 198 169 39 77 48 171 154 21 162 24 198 169 39 77 48 171 154 21 16 0]",
524+
},
525+
// Invalid format char.
526+
{
527+
name: "printf-t",
528+
out: fmt.Sprintf("%t", hash),
529+
want: "%!t(hash=b26f2b342aab24bcf63ea218c6a9274d30ab9a15a218c6a9274d30ab9a151000)",
530+
},
531+
}
532+
for _, tt := range tests {
533+
t.Run(tt.name, func(t *testing.T) {
534+
if tt.out != tt.want {
535+
t.Errorf("%s does not render as expected:\n got %s\nwant %s", tt.name, tt.out, tt.want)
536+
}
537+
})
538+
}
539+
}

0 commit comments

Comments
 (0)