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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

text.gno

4.20 Kb · 150 lines
  1// Package safe holds the pure checks and formulas every platform realm
  2// shares: text and SVG validation, basis-point math, payee splits, merkle
  3// proofs, provenance hashes and the one JSON writer. Nothing here has state.
  4package safe
  5
  6import (
  7	"errors"
  8	"strconv"
  9	"strings"
 10	"unicode/utf8"
 11)
 12
 13// Domain separates every hash this platform commits to from hashes of the
 14// same bytes made for any other purpose. It equals the brand slug and
 15// changes with it (paths change anyway when the brand is chosen).
 16const Domain = "art"
 17
 18// CheckText reports why s may not be stored as user-visible text.
 19//
 20// Rejected: invalid UTF-8; C0 and C1 control characters, which include tab,
 21// newline and carriage return (the record-injection vector of RA H-1); DEL;
 22// U+2028/U+2029, which end a line in older JavaScript parsers (RA L-12); and
 23// every bidi control or zero-width character that lets one string render as
 24// another.
 25func CheckText(s string, maxLen int) error {
 26	if len(s) > maxLen {
 27		return errors.New("too long (max " + strconv.Itoa(maxLen) + " bytes)")
 28	}
 29	if !utf8.ValidString(s) {
 30		return errors.New("invalid UTF-8")
 31	}
 32	for _, r := range s {
 33		if forbiddenRune(r) {
 34			return errors.New("forbidden character U+" + strings.ToUpper(strconv.FormatInt(int64(r), 16)))
 35		}
 36	}
 37	return nil
 38}
 39
 40// AssertText panics with the field name when CheckText rejects s.
 41func AssertText(field, s string, maxLen int) {
 42	if err := CheckText(s, maxLen); err != nil {
 43		panic(field + ": " + err.Error())
 44	}
 45}
 46
 47// AssertNonEmptyText is AssertText that also refuses "".
 48func AssertNonEmptyText(field, s string, maxLen int) {
 49	if s == "" {
 50		panic(field + ": must not be empty")
 51	}
 52	AssertText(field, s, maxLen)
 53}
 54
 55func forbiddenRune(r rune) bool {
 56	switch {
 57	case r < 0x20, r == 0x7f, r >= 0x80 && r <= 0x9f:
 58		return true
 59	case r == 0x061c, r == 0xfeff: // Arabic letter mark, zero-width no-break space
 60		return true
 61	case r >= 0x200b && r <= 0x200f: // zero-width space/joiners, LRM, RLM
 62		return true
 63	case r == 0x2028, r == 0x2029:
 64		return true
 65	case r >= 0x202a && r <= 0x202e: // bidi embeddings and overrides
 66		return true
 67	case r >= 0x2066 && r <= 0x2069: // bidi isolates
 68		return true
 69	}
 70	return false
 71}
 72
 73// AssertSymbol enforces the grc721 symbol alphabet ([A-Za-z0-9_-], 1..11
 74// bytes) up front, so the error names the field instead of surfacing from
 75// grc721.NewToken.
 76func AssertSymbol(field, s string) {
 77	if s == "" || len(s) > 11 {
 78		panic(field + ": must be 1-11 characters")
 79	}
 80	for i := 0; i < len(s); i++ {
 81		c := s[i]
 82		if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-') {
 83			panic(field + ": only A-Z, a-z, 0-9, '_' and '-' are allowed")
 84		}
 85	}
 86}
 87
 88// AssertHex checks that s is exactly nBytes of hex and returns it lowercased.
 89func AssertHex(field, s string, nBytes int) string {
 90	if len(s) != 2*nBytes {
 91		panic(field + ": must be " + strconv.Itoa(2*nBytes) + " hex characters")
 92	}
 93	low := strings.ToLower(s)
 94	for i := 0; i < len(low); i++ {
 95		c := low[i]
 96		if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
 97			panic(field + ": must be hex")
 98		}
 99	}
100	return low
101}
102
103// ParseID parses a positive decimal id ("1", "42"). Leading zeros, signs
104// and empty strings are refused so every id has exactly one spelling.
105func ParseID(field, s string) int64 {
106	if s == "" || len(s) > 18 || s[0] == '0' {
107		panic(field + ": invalid id " + strconv.Quote(s))
108	}
109	var n int64
110	for i := 0; i < len(s); i++ {
111		c := s[i]
112		if c < '0' || c > '9' {
113			panic(field + ": invalid id " + strconv.Quote(s))
114		}
115		n = n*10 + int64(c-'0')
116	}
117	return n
118}
119
120// PadInt renders n with leading zeros to width digits, so decimal ids and
121// prices sort correctly as avl keys. n must be >= 0.
122func PadInt(n int64, width int) string {
123	s := strconv.FormatInt(n, 10)
124	if len(s) >= width {
125		return s
126	}
127	return strings.Repeat("0", width-len(s)) + s
128}
129
130// XMLEscape makes s safe as SVG/XML character data or a quoted attribute.
131func XMLEscape(s string) string {
132	var b strings.Builder
133	for _, r := range s {
134		switch r {
135		case '&':
136			b.WriteString("&amp;")
137		case '<':
138			b.WriteString("&lt;")
139		case '>':
140			b.WriteString("&gt;")
141		case '"':
142			b.WriteString("&quot;")
143		case '\'':
144			b.WriteString("&apos;")
145		default:
146			b.WriteRune(r)
147		}
148	}
149	return b.String()
150}