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("&")
137 case '<':
138 b.WriteString("<")
139 case '>':
140 b.WriteString(">")
141 case '"':
142 b.WriteString(""")
143 case '\'':
144 b.WriteString("'")
145 default:
146 b.WriteRune(r)
147 }
148 }
149 return b.String()
150}