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}