Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

meta.gno

5.38 Kb · 201 lines
  1// Package meta validates launch metadata and addresses, and encodes text
  2// safely for JSON and Markdown.
  3package meta
  4
  5import (
  6	"errors"
  7	"strings"
  8	"unicode"
  9	"unicode/utf8"
 10)
 11
 12var (
 13	ErrName        = errors.New("launchpad/meta: name must be 1-32 bytes of printable text without markup characters")
 14	ErrTicker      = errors.New("launchpad/meta: ticker must be 1-10 characters A-Z or 0-9")
 15	ErrDescription = errors.New("launchpad/meta: description must be at most 280 bytes of printable text without markup characters")
 16	ErrImage       = errors.New("launchpad/meta: image must be an ipfs:// or https:// URI of at most 200 bytes")
 17	ErrWebsite     = errors.New("launchpad/meta: website must be an https:// URL of at most 200 bytes")
 18	ErrHandle      = errors.New("launchpad/meta: handle must be 1-32 characters A-Z, a-z, 0-9 or _")
 19)
 20
 21const hexDigits = "0123456789abcdef"
 22
 23// The chain decodes four texts to the same address: the bech32 form it gives
 24// a caller, that form in capitals, and the bech32m checksum in either case.
 25// State keyed by the text, such as a token balance or a role, recorded under
 26// any of the other three is something no caller can ever reach.
 27
 28// bech32Value holds, for each character of the bech32 alphabet
 29// "qpzry9x8gf2tvdw0s3jn54khce6mua7l", its 5-bit value plus one. Zero marks
 30// every other byte, capitals included.
 31var bech32Value = [256]byte{
 32	'q': 1, 'p': 2, 'z': 3, 'r': 4, 'y': 5, '9': 6, 'x': 7, '8': 8,
 33	'g': 9, 'f': 10, '2': 11, 't': 12, 'v': 13, 'd': 14, 'w': 15, '0': 16,
 34	's': 17, '3': 18, 'j': 19, 'n': 20, '5': 21, '4': 22, 'k': 23, 'h': 24,
 35	'c': 25, 'e': 26, '6': 27, 'm': 28, 'u': 29, 'a': 30, '7': 31, 'l': 32,
 36}
 37
 38// bech32Gen[t] is the XOR of the BIP-173 generators selected by the five
 39// bits of t: one table read per character instead of five tests.
 40var bech32Gen = [32]uint32{
 41	0x00000000, 0x3b6a57b2, 0x26508e6d, 0x1d3ad9df,
 42	0x1ea119fa, 0x25cb4e48, 0x38f19797, 0x039bc025,
 43	0x3d4233dd, 0x0628646f, 0x1b12bdb0, 0x2078ea02,
 44	0x23e32a27, 0x18897d95, 0x05b3a44a, 0x3ed9f3f8,
 45	0x2a1462b3, 0x117e3501, 0x0c44ecde, 0x372ebb6c,
 46	0x34b57b49, 0x0fdf2cfb, 0x12e5f524, 0x298fa296,
 47	0x1756516e, 0x2c3c06dc, 0x3106df03, 0x0a6c88b1,
 48	0x09f74894, 0x329d1f26, 0x2fa7c6f9, 0x14cd914b,
 49}
 50
 51// The checksum state once the address prefix "g" has been absorbed, which
 52// BIP-173 expands to the values 3, 0, 7.
 53const bech32PrefixState uint32 = 0x8c07
 54
 55// ValidAddress reports whether a is an account or realm address written the
 56// one way the chain gives it to a caller: "g1", then 38 lowercase bech32
 57// characters whose checksum is the original bech32 one. It reads the text
 58// itself: decoding and re-encoding it costs far more gas.
 59func ValidAddress(a address) bool {
 60	s := string(a)
 61	if len(s) != 40 || s[0] != 'g' || s[1] != '1' {
 62		return false
 63	}
 64	chk := bech32PrefixState
 65	for i := 2; i < 40; i++ {
 66		v := bech32Value[s[i]]
 67		if v == 0 {
 68			return false
 69		}
 70		chk = (chk&0x1ffffff)<<5 ^ uint32(v-1) ^ bech32Gen[chk>>25]
 71	}
 72	return chk == 1 // 1 is bech32; bech32m ends on 0x2bc830a3
 73}
 74
 75func printable(s string) bool {
 76	if !utf8.ValidString(s) {
 77		return false
 78	}
 79	for _, r := range s {
 80		if !unicode.IsPrint(r) {
 81			return false
 82		}
 83	}
 84	return true
 85}
 86
 87func Name(s string) error {
 88	if len(s) < 1 || len(s) > 32 || !printable(s) || strings.ContainsAny(s, "[]()*#<>`|\\") || strings.TrimSpace(s) != s {
 89		return ErrName
 90	}
 91	return nil
 92}
 93
 94func Ticker(s string) error {
 95	if len(s) < 1 || len(s) > 10 {
 96		return ErrTicker
 97	}
 98	for i := 0; i < len(s); i++ {
 99		c := s[i]
100		if !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') {
101			return ErrTicker
102		}
103	}
104	return nil
105}
106
107func Description(s string) error {
108	if len(s) > 280 || !printable(s) || strings.ContainsAny(s, "[]()<>") {
109		return ErrDescription
110	}
111	return nil
112}
113
114func safeURI(s string, schemes []string) bool {
115	if len(s) > 200 {
116		return false
117	}
118	matched := false
119	for _, p := range schemes {
120		if strings.HasPrefix(s, p) && len(s) > len(p) {
121			matched = true
122		}
123	}
124	if !matched {
125		return false
126	}
127	for i := 0; i < len(s); i++ {
128		c := s[i]
129		if c <= 0x20 || c >= 0x7f || strings.IndexByte("\"'<>\\`()[]{}|^", c) >= 0 {
130			return false
131		}
132	}
133	return true
134}
135
136func Image(s string) error {
137	if s == "" || safeURI(s, []string{"ipfs://", "https://"}) {
138		return nil
139	}
140	return ErrImage
141}
142
143func Website(s string) error {
144	if s == "" || safeURI(s, []string{"https://"}) {
145		return nil
146	}
147	return ErrWebsite
148}
149
150func Handle(s string) error {
151	if s == "" {
152		return nil
153	}
154	if len(s) > 32 {
155		return ErrHandle
156	}
157	for i := 0; i < len(s); i++ {
158		c := s[i]
159		if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') && c != '_' {
160			return ErrHandle
161		}
162	}
163	return nil
164}
165
166// JSONString quotes s as a JSON string. Control bytes become \u00XX so the
167// value survives the frontend's qeval double decode.
168func JSONString(s string) string {
169	var b strings.Builder
170	b.WriteByte('"')
171	for i := 0; i < len(s); i++ {
172		c := s[i]
173		switch {
174		case c == '"':
175			b.WriteString(`\"`)
176		case c == '\\':
177			b.WriteString(`\\`)
178		case c < 0x20:
179			b.WriteString(`\u00`)
180			b.WriteByte(hexDigits[c>>4])
181			b.WriteByte(hexDigits[c&0xf])
182		default:
183			b.WriteByte(c)
184		}
185	}
186	b.WriteByte('"')
187	return b.String()
188}
189
190// EscapeMarkdown backslash-escapes every Markdown control character.
191func EscapeMarkdown(s string) string {
192	var b strings.Builder
193	for i := 0; i < len(s); i++ {
194		c := s[i]
195		if strings.IndexByte("\\`*_{}[]()#+-.!|<>~", c) >= 0 {
196			b.WriteByte('\\')
197		}
198		b.WriteByte(c)
199	}
200	return b.String()
201}