render.gno
9.42 Kb · 233 lines
1package registry
2
3import (
4 "chain/runtime"
5 "chain/runtime/unsafe"
6 "strconv"
7 "strings"
8
9 pm "gno.land/p/moul/gnopm/v0"
10 "gno.land/p/moul/md/v0"
11 "gno.land/p/moul/realmpath/v0"
12 "gno.land/p/moul/txlink/v0"
13)
14
15// untrusted-render: every STORED field is charset-validated at write time by
16// gno.land/p/moul/gnopm/v0 (ValidPkgPath, ValidRepo, ValidCommit, ValidDir,
17// ValidRef). Each is an allowlist, so a stored field cannot hold a backtick, a
18// pipe, a bracket, a control character or a bidi override, and therefore needs
19// no escaping at any of the several places one claim is shown. Validating once
20// at the write beats escaping at every read, and it is why ValidRef is narrower
21// than git's own rule: one denylist field would have undone it for all of them.
22//
23// The one string here that is NOT validated is the render path itself, which
24// arrives from the URL and is echoed back by notFound. That one is escaped.
25
26// pageSize bounds every listing: a Render that walks unbounded state is a
27// Render that eventually stops rendering.
28const pageSize = 20
29
30// Render routes gnoweb paths:
31//
32// / every claimed package path
33// /<package path> the claims about one package
34// /help what this realm is, and how to write to it
35func Render(path string) string {
36 req := realmpath.Parse(path)
37 parts := req.PathParts()
38
39 if len(parts) == 0 || parts[0] == "" {
40 return renderHome(pageOf(req))
41 }
42 if len(parts) == 1 && parts[0] == "help" {
43 return renderHelp()
44 }
45 // A package path contains slashes, so it arrives split. Rejoining is the
46 // whole of the routing: anything that reassembles into a valid path is a
47 // package page, anything else is a miss.
48 pkgPath := strings.Join(parts, "/")
49 if !pm.ValidPkgPath(pkgPath) {
50 return notFound(pkgPath + " is not a package path")
51 }
52 return renderPackage(pkgPath)
53}
54
55func base() string {
56 return strings.TrimPrefix(unsafe.CurrentRealm().PkgPath(), runtime.ChainDomain())
57}
58
59func pkgURL(pkgPath string) string { return base() + ":" + pkgPath }
60
61// link does not escape its text, because every call site passes a field that is
62// charset-validated at write time (see the note above). Escaping here would
63// turn "gno.land/p/moul/md/v1" into "gno\.land/p/moul/md/v1" on every row of
64// every listing, for no safety anyone gains.
65func link(text, url string) string { return "[" + text + "](" + url + ")" }
66
67// join glues blocks with exactly one blank line between them.
68//
69// gno collapses two consecutive blank lines inside an example's // Output:
70// block, exactly as Go does, so a Render that ever emits them cannot be pinned
71// by an example at all. Appending "\n" by hand as sections are built is how a
72// Render acquires them; assembling a list of blocks and joining once is how it
73// stops. Every block arrives without its own leading or trailing blank.
74func join(blocks []string) string {
75 for i, b := range blocks {
76 blocks[i] = strings.Trim(b, "\n")
77 }
78 return strings.Join(blocks, "\n\n") + "\n"
79}
80
81func pageOf(req *realmpath.Request) int {
82 n, err := strconv.Atoi(req.Query.Get("page"))
83 if err != nil || n < 1 {
84 return 1
85 }
86 return n
87}
88
89// notFound echoes the requested path, which is the only untrusted string this
90// file handles: it comes from the URL and has already failed validation by the
91// time we are here. It is escaped rather than dropped, because "that is not a
92// package path" without saying which one is an unhelpful error.
93func notFound(why string) string {
94 return md.H1("Not found") + "\n" + md.EscapeText(why) + ".\n\n" + link("Back to the registry", base()) + "\n"
95}
96
97func renderHome(page int) string {
98 var b strings.Builder
99 b.WriteString(md.H1("gnopm source registry"))
100 b.WriteString("\nWhere a deployed package says it came from. Every entry is a claim made by whoever signed it, never a verified fact: this realm cannot clone a repository, so it records testimony in the shape a verifier needs and labels who said it.\n")
101 b.WriteString("\n**Packages:** " + strconv.Itoa(reg.Size()) + " · **Claims:** " + strconv.Itoa(reg.Claims()) + "\n")
102
103 if reg.Size() == 0 {
104 b.WriteString("\nNothing claimed yet. " + link("Register the first package", txlink.Call("Register")) + "\n")
105 b.WriteString("\n" + link("How this works", base()+":help") + "\n")
106 return b.String()
107 }
108
109 b.WriteString("\n| package | claims | owner has claimed |\n")
110 b.WriteString("| --- | ---: | --- |\n")
111 rows := 0
112 reg.IteratePackages((page-1)*pageSize, pageSize, func(p *pm.Package) bool {
113 rows++
114 owner := "no"
115 if _, _, _, ok := OwnerClaim(p.PkgPath); ok {
116 owner = "yes"
117 }
118 b.WriteString("| " + link(p.PkgPath, pkgURL(p.PkgPath)) +
119 " | " + strconv.Itoa(p.Count()) +
120 " | " + owner + " |\n")
121 return false
122 })
123
124 if p := pager(page, rows); p != "" {
125 b.WriteString("\n" + p + "\n")
126 }
127 b.WriteString("\n" + link("How this works", base()+":help") + "\n")
128 return b.String()
129}
130
131func pager(page, rows int) string {
132 var parts []string
133 if page > 1 {
134 parts = append(parts, link("previous", base()+"?page="+strconv.Itoa(page-1)))
135 }
136 if rows == pageSize {
137 parts = append(parts, link("next", base()+"?page="+strconv.Itoa(page+1)))
138 }
139 return strings.Join(parts, " · ")
140}
141
142func renderPackage(pkgPath string) string {
143 p := reg.Package(pkgPath)
144 if p == nil {
145 return join([]string{
146 md.H1(pkgPath),
147 "Nobody has claimed a source for this package.",
148 link("Claim it", txlink.Call("Register", "pkgPath", pkgPath)),
149 link("Back to the registry", base()),
150 })
151 }
152
153 // The namespace owner first, then everyone else. The ordering IS the
154 // tagging: no ranking is stored, it is recomputed here from the one thing
155 // about a claim that the chain can actually prove.
156 var owner, others []*pm.Claim
157 p.IterateClaims(func(c *pm.Claim) bool {
158 if c.OwnedBy(resolveName) {
159 owner = append(owner, c)
160 } else {
161 others = append(others, c)
162 }
163 return false
164 })
165
166 ns := md.InlineCode(pm.Namespace(pkgPath))
167 blocks := []string{
168 md.H1(pkgPath),
169 strconv.Itoa(p.Count()) + " claim(s). A claim is what an address said, not what anyone checked.",
170 }
171 if len(owner) > 0 {
172 blocks = append(blocks,
173 md.H2("Claimed by the namespace owner"),
174 "The signer owns "+ns+", so this claim comes from the party that controls the path. That is not a proof the source matches. It is a proof of who is speaking.")
175 for _, c := range owner {
176 blocks = append(blocks, claimBlock(c))
177 }
178 }
179 if len(others) > 0 {
180 blocks = append(blocks,
181 md.H2("Claimed by others"),
182 "These addresses do not own "+ns+". A third party may be filling in the map honestly, or pointing at a repository that has nothing to do with this package. Read the source before trusting either.")
183 for _, c := range others {
184 blocks = append(blocks, claimBlock(c))
185 }
186 }
187 return join(append(blocks, link("Back to the registry", base())))
188}
189
190func claimBlock(c *pm.Claim) string {
191 var b strings.Builder
192 b.WriteString("| field | value |\n| --- | --- |\n")
193 b.WriteString("| claimant | " + md.InlineCode(c.Claimant.String()) + " |\n")
194 b.WriteString("| commit | " + link(c.Commit[:12], c.SourceURL()) + " |\n")
195 b.WriteString("| repository | " + link(c.Repo, c.Repo) + " |\n")
196 dir := c.Dir
197 if dir == "" {
198 dir = "(repository root)"
199 }
200 b.WriteString("| directory | " + md.InlineCode(dir) + " |\n")
201 if c.Ref != "" {
202 b.WriteString("| ref | " + md.InlineCode(c.Ref) + " |\n")
203 }
204 b.WriteString("| claimed at | block " + strconv.FormatInt(c.Height, 10) + " |\n")
205 updated := "never"
206 if c.UpdatedAt != c.Height {
207 updated = "block " + strconv.FormatInt(c.UpdatedAt, 10)
208 }
209 b.WriteString("| last updated | " + updated + " |")
210 return b.String()
211}
212
213func renderHelp() string {
214 var b strings.Builder
215 b.WriteString(md.H1("How this registry works"))
216 b.WriteString("\nA gno import path is a chain address, not a repository URL, so nothing on chain says where a deployed package came from. This realm is where an address can say so.\n")
217
218 b.WriteString("\n" + md.H2("What it proves"))
219 b.WriteString("\nNothing on its own, and that is the honest answer. A realm cannot clone a repository, so it cannot check that the code at a commit is the code at a path. Three claims get confused with each other and only two of them can ever be settled:\n\n")
220 b.WriteString("| claim | provable |\n| --- | --- |\n")
221 b.WriteString("| this package came from that repository | no. Not here and not anywhere: anyone may deploy any bytes and claim any repository |\n")
222 b.WriteString("| the deployed bytes equal the addpkg payload of that directory at that commit | yes, by hashing both sides. Off chain, by anything that can clone |\n")
223 b.WriteString("| the claimant owns this path's namespace | yes, from chain data, and it is recomputed on every read |\n")
224
225 b.WriteString("\n" + md.H2("Writing to it"))
226 b.WriteString("\nRegister the source of a package you deployed. Calling it again replaces your own claim and nobody else's, which is how you move a claim to a new commit after a redeploy.\n\n")
227 b.WriteString(link("Register", txlink.Call("Register")) + " · " + link("Withdraw", txlink.Call("Withdraw")) + "\n")
228 b.WriteString("\nFields: the full package path, an https repository URL, a 40 or 64 character lowercase hex commit, the subdirectory holding the package (empty for the repository root), and optionally a fully-qualified ref such as " + md.InlineCode("refs/tags/v1.2.0") + ".\n")
229 b.WriteString("\nAnyone may claim any path. A claim from an address that does not own the namespace is not hidden: it is shown under its own heading and ranked below the owner's.\n")
230
231 b.WriteString("\n" + link("Back to the registry", base()) + "\n")
232 return b.String()
233}