Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}