reads.gno
2.81 Kb · 123 lines
1package memba_appstore_v2
2
3import (
4 "strings"
5
6 "gno.land/p/nt/ufmt/v0"
7)
8
9const (
10 DefaultPageSize = 20 // default JSON window
11 MaxPageLimit = 100 // hard cap on any JSON read window (read-DoS guard)
12)
13
14func clampLimit(limit int) int {
15 if limit <= 0 {
16 return DefaultPageSize
17 }
18 if limit > MaxPageLimit {
19 return MaxPageLimit
20 }
21 return limit
22}
23
24// jsonEscape escapes a string for embedding in a JSON string literal — every
25// listing text field is attacker-supplied, so this is the only thing between a
26// malicious name/tagline and a broken/injected JSON payload on the read path.
27func jsonEscape(s string) string {
28 var sb strings.Builder
29 for _, r := range s {
30 switch r {
31 case '"':
32 sb.WriteString("\\\"")
33 case '\\':
34 sb.WriteString("\\\\")
35 case '\n':
36 sb.WriteString("\\n")
37 case '\r':
38 sb.WriteString("\\r")
39 case '\t':
40 sb.WriteString("\\t")
41 default:
42 if r < 0x20 {
43 sb.WriteString(ufmt.Sprintf("\\u%04x", r))
44 } else {
45 sb.WriteString(string(r))
46 }
47 }
48 }
49 return sb.String()
50}
51
52// listingJSON renders one listing. `full` adds the (large) description — omitted
53// from list windows so a bounded page stays small.
54func listingJSON(l *Listing, full bool) string {
55 descr := ""
56 if full {
57 descr = ufmt.Sprintf(`,"descr":"%s"`, jsonEscape(l.Descr))
58 }
59 return ufmt.Sprintf(
60 `{"id":%d,"pkgPath":"%s","name":"%s","tagline":"%s","category":"%s","iconCID":"%s","appURL":"%s","publisher":"%s","status":"%s","flagCount":%d,"createdAt":%d%s}`,
61 l.Id,
62 jsonEscape(l.PkgPath),
63 jsonEscape(l.Name),
64 jsonEscape(l.Tagline),
65 jsonEscape(l.Category),
66 jsonEscape(l.IconCID),
67 jsonEscape(l.AppURL),
68 l.Publisher.String(),
69 l.Status,
70 l.FlagCount,
71 l.CreatedAt,
72 descr,
73 )
74}
75
76// ListLiveJSON returns a bounded JSON array of the visible (live, un-hidden)
77// listings: skip `offset`, take up to clamped `limit`. Iteration stops as soon as
78// the window is filled, so the scan is bounded by offset+limit regardless of
79// catalog size.
80func ListLiveJSON(offset, limit int) string {
81 if offset < 0 {
82 offset = 0
83 }
84 limit = clampLimit(limit)
85
86 var sb strings.Builder
87 sb.WriteString("[")
88 skipped := 0
89 taken := 0
90 first := true
91 listings.Iterate("", "", func(_ string, v any) bool {
92 l := v.(*Listing)
93 if !isVisible(l) {
94 return false
95 }
96 if skipped < offset {
97 skipped++
98 return false
99 }
100 if taken >= limit {
101 return true // window full — stop
102 }
103 if !first {
104 sb.WriteString(",")
105 }
106 first = false
107 sb.WriteString(listingJSON(l, false))
108 taken++
109 return false
110 })
111 sb.WriteString("]")
112 return sb.String()
113}
114
115// GetListingJSON returns a single listing (any status) with its description, or
116// the JSON literal `null` if the package path is not registered.
117func GetListingJSON(pkgPath string) string {
118 v, ok := listings.Get(pkgPath)
119 if !ok {
120 return "null"
121 }
122 return listingJSON(v.(*Listing), true)
123}