Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

reads.gno

4.87 Kb · 167 lines
  1package memba_appstore_v3
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/g1a9g8nu3m0xgx58hh36fph49e2xkqm9axlxuf5z/avl"
  7	"gno.land/p/nt/ufmt/v0"
  8)
  9
 10const (
 11	DefaultPageSize = 20  // default JSON window
 12	MaxPageLimit    = 100 // hard cap on any JSON read window (read-DoS guard)
 13)
 14
 15func clampLimit(limit int) int {
 16	if limit <= 0 {
 17		return DefaultPageSize
 18	}
 19	if limit > MaxPageLimit {
 20		return MaxPageLimit
 21	}
 22	return limit
 23}
 24
 25// jsonEscape escapes a string for embedding in a JSON string literal — every listing text field
 26// is attacker-supplied, so this is the only thing between a malicious name/tagline/reason/CID and
 27// a broken/injected JSON payload on the read path.
 28func jsonEscape(s string) string {
 29	var sb strings.Builder
 30	for _, r := range s {
 31		switch r {
 32		case '"':
 33			sb.WriteString("\\\"")
 34		case '\\':
 35			sb.WriteString("\\\\")
 36		case '\n':
 37			sb.WriteString("\\n")
 38		case '\r':
 39			sb.WriteString("\\r")
 40		case '\t':
 41			sb.WriteString("\\t")
 42		default:
 43			if r < 0x20 {
 44				sb.WriteString(ufmt.Sprintf("\\u%04x", r))
 45			} else {
 46				sb.WriteString(string(r))
 47			}
 48		}
 49	}
 50	return sb.String()
 51}
 52
 53// screenshotsJSON renders a CID slice as a JSON string array body (each element escaped).
 54func screenshotsJSON(cids []string) string {
 55	var sb strings.Builder
 56	for i, c := range cids {
 57		if i > 0 {
 58			sb.WriteString(",")
 59		}
 60		sb.WriteString(`"`)
 61		sb.WriteString(jsonEscape(c))
 62		sb.WriteString(`"`)
 63	}
 64	return sb.String()
 65}
 66
 67// listingJSON renders one listing. `full` adds the (large) description + the screenshot gallery —
 68// omitted from list windows so a bounded page stays small. rejectReason is short and always
 69// included (the My-Submissions view needs it in list windows).
 70func listingJSON(l *Listing, full bool) string {
 71	extra := ""
 72	if full {
 73		extra = ufmt.Sprintf(`,"descr":"%s","screenshotCIDs":[%s]`,
 74			jsonEscape(l.Descr), screenshotsJSON(l.ScreenshotCIDs))
 75	}
 76	return ufmt.Sprintf(
 77		`{"id":%d,"pkgPath":"%s","name":"%s","tagline":"%s","category":"%s","iconCID":"%s","appURL":"%s","publisher":"%s","status":"%s","rejectReason":"%s","paidResubmitCredit":%t,"resubmitCount":%d,"flagCount":%d,"createdAt":%d%s}`,
 78		l.Id,
 79		jsonEscape(l.PkgPath),
 80		jsonEscape(l.Name),
 81		jsonEscape(l.Tagline),
 82		jsonEscape(l.Category),
 83		jsonEscape(l.IconCID),
 84		jsonEscape(l.AppURL),
 85		l.Publisher.String(),
 86		l.Status,
 87		jsonEscape(l.RejectReason),
 88		l.PaidResubmitCredit,
 89		l.ResubmitCount,
 90		l.FlagCount,
 91		l.CreatedAt,
 92		extra,
 93	)
 94}
 95
 96// windowByPrefix iterates a composite-key index (statusIndex or publisherIndex) over the prefix
 97// range [prefix+"\x00", prefix+"\x01") — a true O(offset+limit) window ordered by zero-padded id.
 98// When flagFilter is set, flag-hidden listings are excluded (the public live/pending lists).
 99func windowByPrefix(index *avl.Tree, prefix string, offset, limit int, flagFilter bool) string {
100	limit = clampLimit(limit)
101	if offset < 0 {
102		offset = 0
103	}
104	var sb strings.Builder
105	sb.WriteString("[")
106	skipped, taken := 0, 0
107	first := true
108	index.Iterate(prefix+"\x00", prefix+"\x01", func(_ string, v any) bool {
109		lv, ok := listings.Get(v.(string))
110		if !ok {
111			return false
112		}
113		l := lv.(*Listing)
114		if flagFilter && l.FlagCount >= FlagHideThreshold {
115			return false
116		}
117		if skipped < offset {
118			skipped++
119			return false
120		}
121		if taken >= limit {
122			return true // window full — stop
123		}
124		if !first {
125			sb.WriteString(",")
126		}
127		first = false
128		sb.WriteString(listingJSON(l, false))
129		taken++
130		return false
131	})
132	sb.WriteString("]")
133	return sb.String()
134}
135
136// ListLiveJSON returns a bounded JSON array of the visible (live, un-flag-hidden) listings —
137// the Verified tab. Bounded by scanning only the "live" index slice.
138func ListLiveJSON(offset, limit int) string {
139	return windowByPrefix(statusIndex, StatusLive, offset, limit, true)
140}
141
142// ListByStatusJSON returns a bounded window of listings in `status`. Public tabs (live, pending)
143// exclude flag-hidden listings; rejected/delisted are returned raw (used by curator/owner views).
144// An unknown status returns an empty array — the client passes a fixed enum, never free text.
145func ListByStatusJSON(status string, offset, limit int) string {
146	if !validStatus(status) {
147		return "[]"
148	}
149	flagFilter := status == StatusLive || status == StatusPending
150	return windowByPrefix(statusIndex, status, offset, limit, flagFilter)
151}
152
153// ListByPublisherJSON returns a bounded window of a publisher's listings across ALL statuses
154// (the My-Submissions view) — no flag filter, since a publisher sees their own flagged/rejected.
155func ListByPublisherJSON(publisher string, offset, limit int) string {
156	return windowByPrefix(publisherIndex, publisher, offset, limit, false)
157}
158
159// GetListingJSON returns a single listing (any status) with its full detail (descr + screenshots),
160// or the JSON literal `null` if the package path is not registered.
161func GetListingJSON(pkgPath string) string {
162	v, ok := listings.Get(pkgPath)
163	if !ok {
164		return "null"
165	}
166	return listingJSON(v.(*Listing), true)
167}