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

validation.gno

6.14 Kb · 250 lines
  1package craft
  2
  3import (
  4	"net/url"
  5	"strconv"
  6)
  7
  8const (
  9	recipeBatchLimit             = 100
 10	maxRecipeIngredients         = 16
 11	defaultRecipeListLimit       = 100
 12	defaultCraftFee        int64 = 1_000_000
 13)
 14
 15type craftIngredient struct {
 16	Slot    uint
 17	TokenID string
 18	Amount  int64
 19}
 20
 21func mustParseRecipeCSV(recipesCSV, itemIDsCSV string) ([]string, []uint32) {
 22	if recipesCSV == "" {
 23		panic("recipes is required")
 24	}
 25	if itemIDsCSV == "" {
 26		panic("item ids is required")
 27	}
 28
 29	canonicalRecipes := []string{}
 30	itemIDs := []uint32{}
 31	recipeStart, recipeEnd := 0, 0
 32	itemIDStart, itemIDEnd := 0, 0
 33	for {
 34		for recipeEnd < len(recipesCSV) && recipesCSV[recipeEnd] != ',' {
 35			recipeEnd++
 36		}
 37		for itemIDEnd < len(itemIDsCSV) && itemIDsCSV[itemIDEnd] != ',' {
 38			itemIDEnd++
 39		}
 40
 41		recipeToken := recipesCSV[recipeStart:recipeEnd]
 42		itemIDText := itemIDsCSV[itemIDStart:itemIDEnd]
 43		if recipeToken == "" {
 44			panic("empty recipe not allowed")
 45		}
 46		if itemIDText == "" {
 47			panic("empty item id not allowed")
 48		}
 49
 50		canonicalRecipe, _ := mustParseRecipe(recipeToken)
 51		itemIDValue, err := strconv.ParseUint(itemIDText, 10, 32)
 52		if err != nil {
 53			panic("invalid item id: " + itemIDText)
 54		}
 55
 56		canonicalRecipes = append(canonicalRecipes, canonicalRecipe)
 57		itemIDs = append(itemIDs, uint32(itemIDValue))
 58		if len(canonicalRecipes) > recipeBatchLimit {
 59			panic("recipes exceeds 100")
 60		}
 61
 62		recipeDone := recipeEnd >= len(recipesCSV)
 63		itemIDDone := itemIDEnd >= len(itemIDsCSV)
 64		if recipeDone != itemIDDone {
 65			panic("recipes and item ids count mismatch")
 66		}
 67		if recipeDone {
 68			return canonicalRecipes, itemIDs
 69		}
 70
 71		recipeEnd++
 72		itemIDEnd++
 73		recipeStart = recipeEnd
 74		itemIDStart = itemIDEnd
 75	}
 76}
 77
 78func mustParseRecipeKeyCSV(recipesCSV string) []string {
 79	if recipesCSV == "" {
 80		panic("recipes is required")
 81	}
 82
 83	canonicalRecipes := []string{}
 84	recipeStart, recipeEnd := 0, 0
 85	for {
 86		for recipeEnd < len(recipesCSV) && recipesCSV[recipeEnd] != ',' {
 87			recipeEnd++
 88		}
 89
 90		recipeToken := recipesCSV[recipeStart:recipeEnd]
 91		if recipeToken == "" {
 92			panic("empty recipe not allowed")
 93		}
 94
 95		canonicalRecipe, _ := mustParseRecipe(recipeToken)
 96		canonicalRecipes = append(canonicalRecipes, canonicalRecipe)
 97		if len(canonicalRecipes) > recipeBatchLimit {
 98			panic("recipes exceeds 100")
 99		}
100
101		if recipeEnd >= len(recipesCSV) {
102			return canonicalRecipes
103		}
104
105		recipeEnd++
106		recipeStart = recipeEnd
107	}
108}
109
110func mustParseRecipe(recipe string) (string, []craftIngredient) {
111	canonical, ingredients, errMsg := parseRecipe(recipe)
112	if errMsg != "" {
113		panic(errMsg)
114	}
115	return canonical, ingredients
116}
117
118// A valid recipe is "<4-hex occupancy>|<ingredient>|...". Occupancy bits
119// choose slots 0-15 in order; each ingredient is "tokenId:amount" or
120// "slot:tokenId:amount". Example: "0013|iron:1|iron:2|stick:2" (bits 0,1,4)
121// canonicalizes to "0013|0:iron:1|1:iron:2|4:stick:2".
122func parseRecipe(recipe string) (string, []craftIngredient, string) {
123	if len(recipe) < 7 || recipe[4] != '|' {
124		return "", nil, "invalid recipe"
125	}
126
127	occupancyValue, err := strconv.ParseUint(recipe[:4], 16, 16)
128	if err != nil || occupancyValue == 0 {
129		return "", nil, "invalid recipe occupancy"
130	}
131	occupancy := uint16(occupancyValue)
132	slots := occupiedSlots(occupancy)
133
134	ingredients := []craftIngredient{}
135	entryStart, entryEnd := 5, 5
136	for {
137		for entryEnd < len(recipe) && recipe[entryEnd] != '|' {
138			entryEnd++
139		}
140
141		if len(ingredients) == len(slots) {
142			return "", nil, "occupied slots and ingredients count mismatch"
143		}
144
145		ingredient, errMsg := parseRecipeIngredient(
146			recipe[entryStart:entryEnd],
147			slots[len(ingredients)],
148		)
149		if errMsg != "" {
150			return "", nil, errMsg
151		}
152		ingredients = append(ingredients, ingredient)
153		if len(ingredients) > maxRecipeIngredients {
154			return "", nil, "recipe ingredients exceeds 16"
155		}
156
157		if entryEnd >= len(recipe) {
158			break
159		}
160
161		entryEnd++
162		entryStart = entryEnd
163	}
164
165	if len(ingredients) != len(slots) {
166		return "", nil, "occupied slots and ingredients count mismatch"
167	}
168
169	canonicalRecipe := formatOccupancy(occupancy)
170	for _, ingredient := range ingredients {
171		canonicalRecipe += "|" +
172			strconv.FormatUint(uint64(ingredient.Slot), 10) + ":" +
173			url.PathEscape(ingredient.TokenID) + ":" +
174			strconv.FormatInt(ingredient.Amount, 10)
175	}
176	return canonicalRecipe, ingredients, ""
177}
178
179func parseRecipeIngredient(entry string, expectedSlot uint) (craftIngredient, string) {
180	amountSeparator := len(entry) - 1
181	for amountSeparator > 0 && entry[amountSeparator] != ':' {
182		amountSeparator--
183	}
184	if amountSeparator == 0 || entry[amountSeparator] != ':' {
185		return craftIngredient{}, "invalid recipe ingredient"
186	}
187
188	tokenField := entry[:amountSeparator]
189	amountText := entry[amountSeparator+1:]
190	encodedTokenID := tokenField
191	slotPrefixEnd := 0
192	for slotPrefixEnd < len(tokenField) && tokenField[slotPrefixEnd] != ':' {
193		slotPrefixEnd++
194	}
195	if slotPrefixEnd < len(tokenField) && tokenField[slotPrefixEnd] == ':' {
196		slotValue, err := strconv.ParseUint(tokenField[:slotPrefixEnd], 10, 8)
197		if err == nil {
198			if uint(slotValue) != expectedSlot {
199				return craftIngredient{}, "ingredient slot mismatch"
200			}
201			encodedTokenID = tokenField[slotPrefixEnd+1:]
202		}
203	}
204
205	tokenID, err := url.PathUnescape(encodedTokenID)
206	if err != nil || tokenID == "" {
207		return craftIngredient{}, "invalid item token id"
208	}
209
210	amount, err := strconv.ParseInt(amountText, 10, 64)
211	if err != nil {
212		return craftIngredient{}, "invalid ingredient amount: " + amountText
213	}
214	if amount <= 0 {
215		return craftIngredient{}, "ingredient amount must be positive"
216	}
217
218	return craftIngredient{
219		Slot:    expectedSlot,
220		TokenID: tokenID,
221		Amount:  amount,
222	}, ""
223}
224
225func occupiedSlots(occupancy uint16) []uint {
226	slots := []uint{}
227	for slot := uint(0); slot < 16; slot++ {
228		if occupancy&(uint16(1)<<slot) != 0 {
229			slots = append(slots, slot)
230		}
231	}
232	return slots
233}
234
235func formatOccupancy(occupancy uint16) string {
236	value := strconv.FormatUint(uint64(occupancy), 16)
237	return "0000"[:4-len(value)] + value
238}
239
240func assertRecipeListPageCount(page, count int) {
241	if page < 1 {
242		panic("page must be at least 1")
243	}
244	if count < 1 {
245		panic("count must be at least 1")
246	}
247	if count > listLimit {
248		panic("count exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")")
249	}
250}