Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions 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}