391 lines13 KBC
1/*
2catalog.c: the index and pages of catalog.json (see catalog.h).
3
4Both are parsed with cJSON (the index is ~30 KB, a page under 100 KB), copied
5into compact structs, and the cJSON tree freed. Every item is checked: an id,
6a title, a size, a sha256 of 64 hex digits and a relative URL that stays under
7dl.ps3.world. One that fails is skipped, not trusted.
8*/
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12#include "plat.h"
13#include "catalog.h"
14#include "thirdparty/cJSON.h"
15
16static const char *type_names[NUM_TYPES] = {"homebrew", "theme", "avatar"};
17
18const char *catalog_type_name(int type)
19{
20 return type >= 0 && type < NUM_TYPES ? type_names[type] : "?";
21}
22
23int catalog_letter_slot(char l)
24{
25 return l >= 'A' && l <= 'Z' ? l - 'A' + 1 : 0;
26}
27
28static const char *Str(const cJSON *o, const char *key)
29{
30 const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, key);
31 return cJSON_IsString(v) && v->valuestring ? v->valuestring : "";
32}
33
34// a path under the base: no scheme, no leading slash, no ".." segment (plain or
35// percent-encoded), no backslashes or control characters ("Wow...p3t" is fine)
36static int Safe_Path(const char *p)
37{
38 if (!*p || *p == '/' || strstr(p, "://"))
39 return 0;
40 for (const char *seg = p; *seg; ) {
41 const char *end = strchr(seg, '/');
42 size_t l = end ? (size_t)(end - seg) : strlen(seg);
43 char low[8] = {0};
44 for (size_t i = 0; i < l && i < sizeof(low) - 1; i++)
45 low[i] = (seg[i] >= 'A' && seg[i] <= 'Z') ? seg[i] + 32 : seg[i];
46 if ((l == 2 && !strcmp(low, "..")) || (l == 4 && (!strcmp(low, ".%2e") || !strcmp(low, "%2e."))) ||
47 (l == 6 && !strcmp(low, "%2e%2e")))
48 return 0;
49 seg += l + (end ? 1 : 0);
50 }
51 for (; *p; p++)
52 if ((unsigned char)*p < 0x20 || *p == '\\' || *p == 0x7f)
53 return 0;
54 return 1;
55}
56
57// a size is a whole number of bytes, 8 GiB at most (it goes into the free-space sums)
58static int Size_Ok(const cJSON *size)
59{
60 return cJSON_IsNumber(size) && size->valuedouble >= 0 && size->valuedouble <= 8589934592.0 &&
61 (double)(long long)size->valuedouble == size->valuedouble;
62}
63
64static int Hex_Digest(const char *s, unsigned char out[32])
65{
66 if (strlen(s) != 64)
67 return 0;
68 for (int i = 0; i < 32; i++) {
69 unsigned v = 0;
70 for (int k = 0; k < 2; k++) {
71 char c = s[i * 2 + k];
72 v <<= 4;
73 if (c >= '0' && c <= '9') v |= c - '0';
74 else if (c >= 'a' && c <= 'f') v |= c - 'a' + 10;
75 else if (c >= 'A' && c <= 'F') v |= c - 'A' + 10;
76 else return 0;
77 }
78 out[i] = (unsigned char)v;
79 }
80 return 1;
81}
82
83static int Schema_Ok(const cJSON *root, char *err, int errsize)
84{
85 const cJSON *schema = cJSON_GetObjectItemCaseSensitive(root, "schema");
86 if (!cJSON_IsNumber(schema)) {
87 snprintf(err, errsize, "The catalogue couldn't be read (no schema)");
88 return 0;
89 }
90 if (schema->valueint != CATALOG_SCHEMA) {
91 snprintf(err, errsize, "This catalogue needs a %s version of the app (schema %d)",
92 schema->valueint > CATALOG_SCHEMA ? "newer" : "different", schema->valueint);
93 return 0;
94 }
95 return 1;
96}
97
98/*
99==================
100Index
101==================
102*/
103static int File_Of(const cJSON *o, cat_file_t *f)
104{
105 const char *url = Str(o, "url");
106 if (!Safe_Path(url) || strlen(url) >= sizeof(f->url) || !Hex_Digest(Str(o, "sha256"), f->sha256))
107 return 0;
108 snprintf(f->url, sizeof(f->url), "%s", url);
109 return 1;
110}
111
112static void List_Free(cat_list_t *l);
113
114static int List_Of(const cJSON *o, cat_list_t *l)
115{
116 const cJSON *count = cJSON_GetObjectItemCaseSensitive(o, "count");
117 const cJSON *pages = cJSON_GetObjectItemCaseSensitive(o, "pages");
118 const cJSON *letters = cJSON_GetObjectItemCaseSensitive(o, "letters");
119 const cJSON *p;
120 if (!cJSON_IsNumber(count) || count->valueint < 0 || !cJSON_IsArray(pages))
121 return 0;
122 snprintf(l->name, sizeof(l->name), "%s", Str(o, "name"));
123 l->count = count->valueint;
124 l->num_pages = cJSON_GetArraySize(pages);
125 l->pages = calloc(l->num_pages ? l->num_pages : 1, sizeof(cat_file_t));
126 if (!l->pages) return 0;
127 int i = 0;
128 cJSON_ArrayForEach(p, pages)
129 if (!File_Of(p, &l->pages[i++])) return 0;
130 if (!File_Of(cJSON_GetObjectItemCaseSensitive(o, "search"), &l->search))
131 return 0;
132 for (i = 0; i < NUM_LETTERS; i++) l->letter_first[i] = -1;
133 cJSON_ArrayForEach(p, letters)
134 if (p->string && cJSON_IsNumber(p) && p->valueint >= 0 && p->valueint < l->count)
135 l->letter_first[catalog_letter_slot(p->string[0])] = p->valueint;
136 // the other orders (each a list of its own, with no orders of its own); any that's off is left out
137 const cJSON *sorted = cJSON_GetObjectItemCaseSensitive(o, "sorted");
138 int ns = cJSON_IsArray(sorted) ? cJSON_GetArraySize(sorted) : 0;
139 if (ns > NUM_SORTS - 1) ns = NUM_SORTS - 1;
140 if (ns && (l->sorted = calloc(ns, sizeof(cat_list_t)))) {
141 cJSON_ArrayForEach(p, sorted) {
142 if (l->num_sorted == ns) break;
143 cat_list_t *s = &l->sorted[l->num_sorted];
144 if (cJSON_GetObjectItemCaseSensitive(p, "sorted") || !List_Of(p, s) || s->count != l->count) {
145 List_Free(s);
146 break; // (in order: newest, largest; one off and the ones after it go too)
147 }
148 l->num_sorted++;
149 }
150 }
151 return 1;
152}
153
154static void List_Free(cat_list_t *l)
155{
156 for (int i = 0; i < l->num_sorted; i++) List_Free(&l->sorted[i]);
157 free(l->sorted);
158 free(l->pages);
159 memset(l, 0, sizeof(*l));
160}
161
162cat_index_t *catalog_parse_index(const char *json, size_t len, char *err, int errsize)
163{
164 cJSON *root = cJSON_ParseWithLength(json, len);
165 cat_index_t *ix = NULL;
166 const cJSON *sections, *s, *l;
167
168 err[0] = 0;
169 if (!root) {
170 snprintf(err, errsize, "The catalogue couldn't be read (not valid JSON)");
171 return NULL;
172 }
173 if (!Schema_Ok(root, err, errsize))
174 goto out;
175 if (strcmp(Str(root, "base_url"), CATALOG_BASE)) {
176 snprintf(err, errsize, "The catalogue points somewhere other than %s", CATALOG_BASE);
177 goto out;
178 }
179 const cJSON *ps = cJSON_GetObjectItemCaseSensitive(root, "page_size");
180 sections = cJSON_GetObjectItemCaseSensitive(root, "sections");
181 if (!cJSON_IsNumber(ps) || ps->valueint <= 0 || !cJSON_IsArray(sections)) {
182 snprintf(err, errsize, "The catalogue couldn't be read (no page size or sections)");
183 goto out;
184 }
185 ix = calloc(1, sizeof(*ix));
186 if (!ix) { snprintf(err, errsize, "Out of memory"); goto out; }
187 snprintf(ix->generated, sizeof(ix->generated), "%s", Str(root, "generated"));
188 // homebrew's descriptions in other languages: "descriptions": {"es": {url, sha256}, ...}
189 const cJSON *d;
190 cJSON_ArrayForEach(d, cJSON_GetObjectItemCaseSensitive(root, "descriptions")) {
191 if (ix->num_desc == MAX_DESC_LANGS || !d->string || strlen(d->string) >= sizeof(ix->desc[0].code) ||
192 strspn(d->string, "abcdefghijklmnopqrstuvwxyz") != strlen(d->string)) continue;
193 cat_desc_t *c = &ix->desc[ix->num_desc];
194 if (!File_Of(d, &c->file)) continue;
195 snprintf(c->code, sizeof(c->code), "%s", d->string);
196 ix->num_desc++;
197 }
198 // the app's update: "1.2.3", a PKG under the base, its size and sha256 (left out if any of it's off)
199 const cJSON *app = cJSON_GetObjectItemCaseSensitive(root, "app");
200 const char *ver = Str(app, "version");
201 if (cJSON_IsObject(app) && *ver && strlen(ver) < sizeof(ix->app.version) && strspn(ver, "0123456789.") == strlen(ver) &&
202 Size_Ok(cJSON_GetObjectItemCaseSensitive(app, "size")) && File_Of(app, &ix->app.file)) {
203 snprintf(ix->app.version, sizeof(ix->app.version), "%s", ver);
204 ix->app.size = (long long)cJSON_GetObjectItemCaseSensitive(app, "size")->valuedouble;
205 }
206 ix->page_size = ps->valueint;
207 cJSON_ArrayForEach(s, sections) {
208 int t;
209 for (t = 0; t < NUM_TYPES; t++)
210 if (!strcmp(Str(s, "type"), type_names[t])) break;
211 const cJSON *lists = cJSON_GetObjectItemCaseSensitive(s, "lists");
212 if (t == NUM_TYPES || ix->lists[t] || !cJSON_IsArray(lists)) continue;
213 int n = cJSON_GetArraySize(lists), k = 0;
214 ix->lists[t] = calloc(n ? n : 1, sizeof(cat_list_t));
215 if (!ix->lists[t]) continue;
216 int sum = 0;
217 cJSON_ArrayForEach(l, lists) {
218 if (List_Of(l, &ix->lists[t][k])) {
219 sum += ix->lists[t][k].count;
220 k++;
221 } else {
222 List_Free(&ix->lists[t][k]);
223 plat_log("catalog: a %s list was skipped (bad fields)", type_names[t]);
224 }
225 }
226 ix->num_lists[t] = k;
227 // the section's own count: an "All" list and its categories hold the same items twice
228 const cJSON *count = cJSON_GetObjectItemCaseSensitive(s, "count");
229 ix->count[t] = cJSON_IsNumber(count) ? count->valueint : sum;
230 }
231out:
232 cJSON_Delete(root);
233 return ix;
234}
235
236void catalog_free_index(cat_index_t *ix)
237{
238 if (!ix) return;
239 for (int t = 0; t < NUM_TYPES; t++) {
240 for (int i = 0; i < ix->num_lists[t]; i++)
241 List_Free(&ix->lists[t][i]);
242 free(ix->lists[t]);
243 }
244 free(ix);
245}
246
247/*
248==================
249Pages
250==================
251*/
252static const char *const fields[] = {"id", "title", "author", "version", "description", "group", "url", "icon", "preview", "list", "titleId"};
253#define NUM_FIELDS (int)(sizeof(fields) / sizeof(fields[0]))
254
255// an id names cache files (icons/<id>.jpg ...) and downloaded.txt lines: lower-case
256// letters, digits and '-', 63 at most (what the generator makes)
257static int Id_Ok(const char *id)
258{
259 size_t n = strlen(id);
260 if (n < 1 || n > 63 || *id == '-')
261 return 0;
262 return strspn(id, "abcdefghijklmnopqrstuvwxyz0123456789-") == n;
263}
264
265// checks one item; fills what doesn't need the string block
266static int Check(const cJSON *o, cat_item_t *it)
267{
268 const char *type = Str(o, "type"), *req = Str(o, "requires");
269 const cJSON *size = cJSON_GetObjectItemCaseSensitive(o, "size");
270 int t;
271
272 for (t = 0; t < NUM_TYPES; t++)
273 if (!strcmp(type, type_names[t])) break;
274 if (t == NUM_TYPES || !Id_Ok(Str(o, "id")) || !*Str(o, "title") || !Size_Ok(size))
275 return 0;
276 if (!Safe_Path(Str(o, "url")) || (*Str(o, "icon") && !Safe_Path(Str(o, "icon"))) ||
277 (*Str(o, "preview") && !Safe_Path(Str(o, "preview"))))
278 return 0;
279 if (!Hex_Digest(Str(o, "sha256"), it->sha256))
280 return 0;
281 it->type = (unsigned char)t;
282 it->size = (long long)size->valuedouble;
283 const cJSON *upd = cJSON_GetObjectItemCaseSensitive(o, "updated");
284 it->updated = cJSON_IsNumber(upd) && upd->valuedouble > 0 ? (long long)upd->valuedouble : 0;
285 it->requires = !strcmp(req, "cfw") ? REQ_CFW : !strcmp(req, "hen") ? REQ_HEN : REQ_ANY;
286 return 1;
287}
288
289catalog_t *catalog_parse_page(const char *json, size_t len, char *err, int errsize)
290{
291 cJSON *root = cJSON_ParseWithLength(json, len);
292 catalog_t *c = NULL;
293 const cJSON *items, *o;
294 size_t total = 0;
295 int n = 0, skipped = 0;
296
297 err[0] = 0;
298 if (!root) {
299 snprintf(err, errsize, "A catalogue page couldn't be read (not valid JSON)");
300 return NULL;
301 }
302 if (!Schema_Ok(root, err, errsize))
303 goto out;
304 items = cJSON_GetObjectItemCaseSensitive(root, "items");
305 if (!cJSON_IsArray(items)) {
306 snprintf(err, errsize, "A catalogue page couldn't be read (no items)");
307 goto out;
308 }
309
310 // first pass: what passes, and how much string space it needs
311 cJSON_ArrayForEach(o, items) {
312 cat_item_t tmp;
313 if (!Check(o, &tmp)) { skipped++; continue; }
314 for (int f = 0; f < NUM_FIELDS; f++)
315 total += strlen(Str(o, fields[f])) + 1;
316 n++;
317 }
318 c = calloc(1, sizeof(*c));
319 if (!c || !(c->items = calloc(n ? n : 1, sizeof(cat_item_t))) || !(c->strings = malloc(total ? total : 1))) {
320 snprintf(err, errsize, "Out of memory for a catalogue page (%d items)", n);
321 catalog_free(c);
322 c = NULL;
323 goto out;
324 }
325
326 // second pass: copy
327 char *s = c->strings;
328 cJSON_ArrayForEach(o, items) {
329 cat_item_t *it = &c->items[c->count];
330 if (c->count >= n || !Check(o, it)) continue;
331 const char **dst[NUM_FIELDS] = {&it->id, &it->title, &it->author, &it->version,
332 &it->description, &it->group, &it->url, &it->icon, &it->preview, &it->list, &it->title_id};
333 for (int f = 0; f < NUM_FIELDS; f++) {
334 size_t l = strlen(Str(o, fields[f])) + 1;
335 memcpy(s, Str(o, fields[f]), l);
336 *dst[f] = s;
337 s += l;
338 }
339 c->count++;
340 }
341 if (skipped)
342 plat_log("catalog: %d item(s) skipped (missing or bad fields)", skipped);
343
344out:
345 cJSON_Delete(root);
346 return c;
347}
348
349int catalog_parse_list(const char *json, cat_list_t *l)
350{
351 cJSON *root = cJSON_Parse(json);
352 memset(l, 0, sizeof(*l));
353 int ok = root && List_Of(root, l);
354 cJSON_Delete(root);
355 if (!ok) catalog_free_list(l);
356 return ok;
357}
358
359void catalog_free_list(cat_list_t *l)
360{
361 List_Free(l);
362}
363
364void catalog_free(catalog_t *c)
365{
366 if (!c) return;
367 free(c->items);
368 free(c->strings);
369 free(c);
370}
371
372cat_item_t *catalog_item_copy(const cat_item_t *it)
373{
374 const char *src[NUM_FIELDS] = {it->id, it->title, it->author, it->version, it->description, it->group, it->url, it->icon, it->preview, it->list, it->title_id};
375 for (int f = 0; f < NUM_FIELDS; f++)
376 if (!src[f]) src[f] = ""; // (one made by hand without a newer field: as none, not a read of address 0)
377 size_t total = sizeof(cat_item_t);
378 for (int f = 0; f < NUM_FIELDS; f++) total += strlen(src[f]) + 1;
379 cat_item_t *c = malloc(total); // the struct, then its strings, in one block (one free)
380 if (!c) return NULL;
381 *c = *it;
382 char *s = (char *)(c + 1);
383 const char **dst[NUM_FIELDS] = {&c->id, &c->title, &c->author, &c->version, &c->description, &c->group, &c->url, &c->icon, &c->preview, &c->list, &c->title_id};
384 for (int f = 0; f < NUM_FIELDS; f++) {
385 size_t l = strlen(src[f]) + 1;
386 memcpy(s, src[f], l);
387 *dst[f] = s;
388 s += l;
389 }
390 return c;
391}