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

pkg.gno

5.23 Kb · 175 lines
  1package forge
  2
  3import "strings"
  4
  5// MaxPkgPathLen bounds a published package path. gno.land's own paths are far
  6// shorter; the cap exists because every stored byte is a storage deposit
  7// somebody pays.
  8const MaxPkgPathLen = 256
  9
 10// Package is the claim that a repo publishes a deployed package path.
 11//
 12// This is the join nothing else in the forge could express. A repo id is
 13// "<namespace>/<name>", exactly two parts, so `r/moul/home` can never be one:
 14// there was no way to ask "which repo publishes this package path", which is
 15// the first question a reader arriving from an explorer has.
 16//
 17// The claim is deliberately one-directional and unverified. A realm cannot ask
 18// the chain who deployed a path, so the forge cannot check that the repo really
 19// is the source of the package. What it can guarantee is that only someone with
 20// write access to the repo said so, and that the namespace of the repo is
 21// governed by r/sys/users. A reader weighs the claim the way they weigh a
 22// go.mod: it says where the author says the source lives.
 23type Package struct {
 24	Path        string // "gno.land/r/moul/home", the deployed package path
 25	RepoID      string
 26	Ref         string // the ref this was published from, "" if unstated
 27	OID         string // the object id, "" if unstated
 28	Tag         string // the release tag, "" if not published as part of one
 29	PublishedAt int64  // block height
 30	PublishedBy address
 31}
 32
 33// ValidPkgPath reports whether s is a plausible gno package path.
 34//
 35// Deliberately shallow: it checks shape, not existence, because a realm cannot
 36// ask the chain whether a path is deployed and a check that only sometimes
 37// works is worse than one that never claims to.
 38func ValidPkgPath(s string) bool {
 39	if s == "" || len(s) > MaxPkgPathLen {
 40		return false
 41	}
 42	if strings.Contains(s, "..") || strings.HasSuffix(s, "/") {
 43		return false
 44	}
 45	parts := strings.Split(s, "/")
 46	if len(parts) < 3 {
 47		return false
 48	}
 49	for _, p := range parts {
 50		if p == "" {
 51			return false
 52		}
 53		for i := 0; i < len(p); i++ {
 54			c := p[i]
 55			ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
 56				(c >= '0' && c <= '9') || c == '.' || c == '-' || c == '_'
 57			if !ok {
 58				return false
 59			}
 60		}
 61	}
 62	return true
 63}
 64
 65// PublishPackage records that this repo publishes path, and indexes it
 66// forge-wide so PackageRepo can answer without scanning every repo.
 67//
 68// Re-publishing the same path from the same repo overwrites the entry, which is
 69// what a redeploy is. Publishing a path another repo already claims is refused:
 70// silently moving it would let any repo owner capture another project's
 71// package path in an explorer.
 72func (f *Forge) PublishPackage(actor address, height int64, repoID, path, ref, oid, tag string) (*Package, error) {
 73	r := f.Repo(repoID)
 74	if r == nil {
 75		return nil, ErrRepoNotFound
 76	}
 77	if r.Archived {
 78		return nil, ErrRepoArchived
 79	}
 80	if !r.Can(actor, RoleWriter) {
 81		return nil, ErrUnauthorized
 82	}
 83	if !ValidPkgPath(path) {
 84		return nil, ErrInvalidPkgPath
 85	}
 86	if ref != "" && !ValidRefName(ref) {
 87		return nil, ErrInvalidRefName
 88	}
 89	if oid != "" && !ValidOID(oid) {
 90		return nil, ErrInvalidOID
 91	}
 92	if tag != "" && !ValidText(tag, MaxTagLen) {
 93		return nil, ErrInvalidText
 94	}
 95	if owner := f.packages.Get(path); owner != nil {
 96		if existing := owner.(*Package); existing.RepoID != repoID {
 97			return nil, ErrPackageClaimed
 98		}
 99	}
100
101	p := &Package{
102		Path:        path,
103		RepoID:      repoID,
104		Ref:         ref,
105		OID:         oid,
106		Tag:         tag,
107		PublishedAt: height,
108		PublishedBy: actor,
109	}
110	f.packages.Set(path, p)
111	r.packages.Set(path, p)
112	return p, nil
113}
114
115// UnpublishPackage withdraws the claim. The package stays on chain; only the
116// repo's assertion about it goes away.
117func (f *Forge) UnpublishPackage(actor address, repoID, path string) error {
118	r := f.Repo(repoID)
119	if r == nil {
120		return ErrRepoNotFound
121	}
122	if !r.Can(actor, RoleWriter) {
123		return ErrUnauthorized
124	}
125	if _, removed := r.packages.Remove(path); !removed {
126		return ErrPackageNotFound
127	}
128	f.packages.Remove(path)
129	return nil
130}
131
132// PackageRepo is the lookup an explorer makes: given a deployed path, which
133// repo claims to publish it. Returns "" when nobody does.
134func (f *Forge) PackageRepo(path string) string {
135	v := f.packages.Get(path)
136	if v == nil {
137		return ""
138	}
139	return v.(*Package).RepoID
140}
141
142// PackageAt returns the full claim for a path, or nil.
143func (f *Forge) PackageAt(path string) *Package {
144	v := f.packages.Get(path)
145	if v == nil {
146		return nil
147	}
148	return v.(*Package)
149}
150
151// PackageCount is how many paths the whole forge claims.
152func (f *Forge) PackageCount() int { return f.packages.Size() }
153
154// IteratePackages walks every claimed path in the forge, in path order.
155func (f *Forge) IteratePackages(offset, count int, cb func(*Package) bool) {
156	if count <= 0 {
157		count = f.packages.Size()
158	}
159	f.packages.IterateByOffset(offset, count, func(_ string, v any) bool {
160		return cb(v.(*Package))
161	})
162}
163
164// PackageCount is how many paths this repo claims.
165func (r *Repo) PackageCount() int { return r.packages.Size() }
166
167// IteratePackages walks the paths this repo claims, in path order.
168func (r *Repo) IteratePackages(offset, count int, cb func(*Package) bool) {
169	if count <= 0 {
170		count = r.packages.Size()
171	}
172	r.packages.IterateByOffset(offset, count, func(_ string, v any) bool {
173		return cb(v.(*Package))
174	})
175}