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

rot13.gno

5.71 Kb · 170 lines
  1// Package rot13 ports Go's classic ROT13 example — the one used to teach
  2// strings.Map and io.Reader in the standard library docs — to an on-chain
  3// Gno realm. The core is a pure letter-rotation cipher over ASCII: ROT13
  4// shifts each letter 13 places, which (since the alphabet has 26 letters)
  5// makes ROT13 its own inverse. Caesar generalizes it to any shift.
  6//
  7// Everything here is pure strings/unicode logic: no state, no randomness,
  8// no clock — Render reads its input straight from the gnoweb path.
  9package rot13
 10
 11import (
 12	"strconv"
 13	"strings"
 14)
 15
 16// Rot13 applies the ROT13 substitution cipher, rotating ASCII letters by 13
 17// and leaving every other byte untouched. Because 13 is half of 26,
 18// Rot13(Rot13(s)) == s — the cipher is its own inverse. This mirrors the
 19// canonical strings.Map example from the Go docs.
 20func Rot13(s string) string {
 21	return strings.Map(rot13Rune, s)
 22}
 23
 24// rot13Rune is the mapping function handed to strings.Map — the heart of the
 25// classic example.
 26func rot13Rune(r rune) rune {
 27	switch {
 28	case r >= 'a' && r <= 'z':
 29		return 'a' + (r-'a'+13)%26
 30	case r >= 'A' && r <= 'Z':
 31		return 'A' + (r-'A'+13)%26
 32	}
 33	return r
 34}
 35
 36// Caesar generalizes ROT13 to an arbitrary shift. Negative and large shifts
 37// are normalized into [0,26). Only ASCII letters move; anything else passes
 38// through unchanged. Caesar(s, 13) is exactly Rot13(s).
 39func Caesar(s string, shift int) string {
 40	sh := rune(((shift % 26) + 26) % 26)
 41	return strings.Map(func(r rune) rune {
 42		switch {
 43		case r >= 'a' && r <= 'z':
 44			return 'a' + (r-'a'+sh)%26
 45		case r >= 'A' && r <= 'Z':
 46			return 'A' + (r-'A'+sh)%26
 47		}
 48		return r
 49	}, s)
 50}
 51
 52// hexNibble decodes a single hex digit, returning -1 if invalid.
 53func hexNibble(b byte) int {
 54	switch {
 55	case b >= '0' && b <= '9':
 56		return int(b - '0')
 57	case b >= 'a' && b <= 'f':
 58		return int(b-'a') + 10
 59	case b >= 'A' && b <= 'F':
 60		return int(b-'A') + 10
 61	}
 62	return -1
 63}
 64
 65// percentDecode turns "%20"/"+" style path escapes back into readable text so
 66// gnoweb links with spaces or punctuation round-trip nicely. Invalid escapes
 67// are left verbatim.
 68func percentDecode(s string) string {
 69	var sb strings.Builder
 70	for i := 0; i < len(s); i++ {
 71		c := s[i]
 72		switch {
 73		case c == '+':
 74			sb.WriteByte(' ')
 75		case c == '%' && i+2 < len(s):
 76			hi, lo := hexNibble(s[i+1]), hexNibble(s[i+2])
 77			if hi >= 0 && lo >= 0 {
 78				sb.WriteByte(byte(hi<<4 | lo))
 79				i += 2
 80			} else {
 81				sb.WriteByte(c)
 82			}
 83		default:
 84			sb.WriteByte(c)
 85		}
 86	}
 87	return sb.String()
 88}
 89
 90// Render is the gnoweb entry point (not a crossing call).
 91//
 92//	Render("")                 -> explanation + a worked example
 93//	Render("/Hello, Gno!")     -> the input and its ROT13
 94//	Render("/caesar/3/attack") -> a Caesar shift of 3 applied to "attack"
 95func Render(path string) string {
 96	p := strings.TrimPrefix(path, "/")
 97
 98	// Caesar sub-path: /caesar/<shift>/<text>
 99	if rest, ok := strings.CutPrefix(p, "caesar/"); ok {
100		shiftStr, text, found := strings.Cut(rest, "/")
101		if !found {
102			return renderRoot() + "\n> Usage: `/caesar/<shift>/<text>` — e.g. `/caesar/3/attack%20at%20dawn`\n"
103		}
104		shift, err := strconv.Atoi(shiftStr)
105		if err != nil {
106			return renderRoot() + "\n> `" + shiftStr + "` is not a valid integer shift.\n"
107		}
108		text = percentDecode(text)
109		return renderCaesar(text, shift)
110	}
111
112	if p == "" {
113		return renderRoot()
114	}
115
116	return renderRot13(percentDecode(p))
117}
118
119func renderRoot() string {
120	var sb strings.Builder
121	sb.WriteString("# ROT13\n\n")
122	sb.WriteString("On-chain port of Go's classic **ROT13** cipher — the standard-library ")
123	sb.WriteString("`strings.Map` / `io.Reader` teaching example. Each ASCII letter is rotated ")
124	sb.WriteString("13 places through the alphabet; everything else is left alone.\n\n")
125	sb.WriteString("Because the alphabet has 26 letters and 13 is exactly half, **ROT13 is its ")
126	sb.WriteString("own inverse** — encoding twice returns the original text.\n\n")
127
128	const demo = "Hello, Gno!"
129	enc := Rot13(demo)
130	sb.WriteString("## Example\n\n")
131	sb.WriteString("| stage | text |\n|---|---|\n")
132	sb.WriteString("| input | `" + demo + "` |\n")
133	sb.WriteString("| ROT13 | `" + enc + "` |\n")
134	sb.WriteString("| ROT13 again | `" + Rot13(enc) + "` |\n\n")
135
136	sb.WriteString("## Try it\n\n")
137	sb.WriteString("- `Render(\"/Uryyb, Tab!\")` — decode any text (append it to the path).\n")
138	sb.WriteString("- `Render(\"/caesar/3/attack at dawn\")` — a general Caesar shift.\n\n")
139	sb.WriteString("Or call the exported functions directly:\n\n")
140	sb.WriteString("```go\n")
141	sb.WriteString("Rot13(\"Hello, Gno!\")     // " + strconv.Quote(enc) + "\n")
142	sb.WriteString("Caesar(\"attack\", 3)      // " + strconv.Quote(Caesar("attack", 3)) + "\n")
143	sb.WriteString("Caesar(\"Hello\", 13)      // same as Rot13: " + strconv.Quote(Caesar("Hello", 13)) + "\n")
144	sb.WriteString("```\n")
145	return sb.String()
146}
147
148func renderRot13(text string) string {
149	enc := Rot13(text)
150	var sb strings.Builder
151	sb.WriteString("# ROT13\n\n")
152	sb.WriteString("| stage | text |\n|---|---|\n")
153	sb.WriteString("| input | `" + text + "` |\n")
154	sb.WriteString("| ROT13 | `" + enc + "` |\n\n")
155	sb.WriteString("_ROT13 is its own inverse — running it on the output above gives back the input._\n\n")
156	sb.WriteString("[Back to overview](/r/moul/x/daily/rot13/v1:)\n")
157	return sb.String()
158}
159
160func renderCaesar(text string, shift int) string {
161	enc := Caesar(text, shift)
162	var sb strings.Builder
163	sb.WriteString("# Caesar shift " + strconv.Itoa(shift) + "\n\n")
164	sb.WriteString("| stage | text |\n|---|---|\n")
165	sb.WriteString("| input | `" + text + "` |\n")
166	sb.WriteString("| shifted | `" + enc + "` |\n\n")
167	sb.WriteString("_Decode with the opposite shift: `/caesar/" + strconv.Itoa(-shift) + "/...`._\n\n")
168	sb.WriteString("[Back to overview](/r/moul/x/daily/rot13/v1:)\n")
169	return sb.String()
170}