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}