| 1 | /* |
| 2 | catalog.c: the index and pages of catalog.json (see catalog.h). |
| 3 |
|
| 4 | Both are parsed with cJSON (the index is ~30 KB, a page under 100 KB), copied |
| 5 | into compact structs, and the cJSON tree freed. Every item is checked: an id, |
| 6 | a title, a size, a sha256 of 64 hex digits and a relative URL that stays under |
| 7 | dl.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 |
|
| 16 | static const char *type_names[NUM_TYPES] = {"homebrew", "theme", "avatar"}; |
| 17 |
|
| 18 | const char *catalog_type_name(int type) |
| 19 | { |
| 20 | return type >= 0 && type < NUM_TYPES ? type_names[type] : "?"; |
| 21 | } |
| 22 |
|
| 23 | int catalog_letter_slot(char l) |
| 24 | { |
| 25 | return l >= 'A' && l <= 'Z' ? l - 'A' + 1 : 0; |
| 26 | } |
| 27 |
|
| 28 | static 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) |
| 36 | static 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) |
| 58 | static 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 |
|
| 64 | static 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 |
|
| 83 | static 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 | ================== |
| 100 | Index |
| 101 | ================== |
| 102 | */ |
| 103 | static 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 |
|
| 112 | static void List_Free(cat_list_t *l); |
| 113 |
|
| 114 | static 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 |
|
| 154 | static 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 |
|
| 162 | cat_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 | } |
| 231 | out: |
| 232 | cJSON_Delete(root); |
| 233 | return ix; |
| 234 | } |
| 235 |
|
| 236 | void 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 | ================== |
| 249 | Pages |
| 250 | ================== |
| 251 | */ |
| 252 | static 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) |
| 257 | static 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 |
| 266 | static 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 |
|
| 289 | catalog_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 |
|
| 344 | out: |
| 345 | cJSON_Delete(root); |
| 346 | return c; |
| 347 | } |
| 348 |
|
| 349 | int 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 |
|
| 359 | void catalog_free_list(cat_list_t *l) |
| 360 | { |
| 361 | List_Free(l); |
| 362 | } |
| 363 |
|
| 364 | void catalog_free(catalog_t *c) |
| 365 | { |
| 366 | if (!c) return; |
| 367 | free(c->items); |
| 368 | free(c->strings); |
| 369 | free(c); |
| 370 | } |
| 371 |
|
| 372 | cat_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 | } |