| 1 | /* |
| 2 | store.c: see store.h. |
| 3 |
|
| 4 | Install targets (v1): |
| 5 | homebrew /dev_hdd0/packages/<file>.pkg, for Package Manager > Install Package Files |
| 6 | theme /dev_hdd0/theme/<file>.p3t, where the XMB lists themes (Settings > Theme |
| 7 | Settings > Theme), as themes.ps3.world's guide |
| 8 | avatar /dev_hdd0/home/<profile>/psn_avatar/<file>.edat, as avatars.ps3.world's guide |
| 9 | */ |
| 10 | #include <stdio.h> |
| 11 | #include <stdlib.h> |
| 12 | #include <string.h> |
| 13 | #include <time.h> |
| 14 | #include "mbedtls/sha256.h" |
| 15 | #include "plat.h" |
| 16 | #include "download.h" |
| 17 | #include "store.h" |
| 18 | #include "lang.h" |
| 19 |
|
| 20 | #define MAX_RECORDS 4096 |
| 21 | #define HOME_DIR "/dev_hdd0/home" |
| 22 |
|
| 23 | typedef struct { |
| 24 | char id[64]; |
| 25 | char sha[65]; |
| 26 | char path[300]; |
| 27 | char title[128]; // (lines from before 0.6.0 have none) |
| 28 | long long when; // time() it finished, 0 if not known |
| 29 | long long size; // on disk, read at start |
| 30 | } record_t; |
| 31 |
|
| 32 | static record_t *records; |
| 33 | static int num_records; |
| 34 | static char avatar_profile[64]; |
| 35 | static char cache_dir[512], icons_dir[512], previews_dir[512], files_dir[512]; |
| 36 |
|
| 37 | static void Hex(const unsigned char *in, char *out) |
| 38 | { |
| 39 | for (int i = 0; i < 32; i++) snprintf(out + i * 2, 3, "%02x", in[i]); |
| 40 | } |
| 41 |
|
| 42 | /* |
| 43 | ================== |
| 44 | Settings and records |
| 45 | ================== |
| 46 | */ |
| 47 | static void Settings_Load(void) |
| 48 | { |
| 49 | char path[512]; |
| 50 | snprintf(path, sizeof(path), "%s/settings.txt", plat_save_dir()); |
| 51 | char *s = (char *)plat_read_file(path, NULL); |
| 52 | if (!s) return; |
| 53 | const char *v = strstr(s, "avatar_profile="); |
| 54 | if (v) { |
| 55 | v += 15; |
| 56 | int n = 0; |
| 57 | while (v[n] && v[n] != '\n' && v[n] != '\r' && n < (int)sizeof(avatar_profile) - 1) n++; |
| 58 | snprintf(avatar_profile, sizeof(avatar_profile), "%.*s", n, v); |
| 59 | } |
| 60 | free(s); |
| 61 | } |
| 62 |
|
| 63 | static void Settings_Save(void) |
| 64 | { |
| 65 | char path[512], buf[128]; |
| 66 | snprintf(path, sizeof(path), "%s/settings.txt", plat_save_dir()); |
| 67 | int n = snprintf(buf, sizeof(buf), "avatar_profile=%s\n", avatar_profile); |
| 68 | plat_write_file(path, buf, n); |
| 69 | } |
| 70 |
|
| 71 | static void Records_Load(void) |
| 72 | { |
| 73 | char path[512]; |
| 74 | snprintf(path, sizeof(path), "%s/downloaded.txt", plat_save_dir()); |
| 75 | char *s = (char *)plat_read_file(path, NULL); |
| 76 | if (!s) return; |
| 77 | for (char *line = s, *next; line && *line && num_records < MAX_RECORDS; line = next) { |
| 78 | next = strchr(line, '\n'); |
| 79 | if (next) *next++ = 0; |
| 80 | char *cr = strchr(line, '\r'); |
| 81 | if (cr) *cr = 0; |
| 82 | // id, sha256, path, then (0.6.0 on) when and the title |
| 83 | char *f[5] = {line}; |
| 84 | int nf = 1; |
| 85 | for (char *t = line; nf < 5 && (t = strchr(t, '\t')); nf++) { *t++ = 0; f[nf] = t; } |
| 86 | if (nf < 3) continue; |
| 87 | record_t *r = &records[num_records]; |
| 88 | memset(r, 0, sizeof(*r)); |
| 89 | snprintf(r->id, sizeof(r->id), "%s", f[0]); |
| 90 | snprintf(r->sha, sizeof(r->sha), "%s", f[1]); |
| 91 | snprintf(r->path, sizeof(r->path), "%s", f[2]); |
| 92 | if (nf > 3) r->when = atoll(f[3]); |
| 93 | if (nf > 4) snprintf(r->title, sizeof(r->title), "%s", f[4]); |
| 94 | if ((r->size = plat_file_size(r->path)) >= 0) // only what's still there |
| 95 | num_records++; |
| 96 | } |
| 97 | free(s); |
| 98 | plat_log("store: %d downloads recorded", num_records); |
| 99 | } |
| 100 |
|
| 101 | static void Records_Save(void) |
| 102 | { |
| 103 | char path[512]; |
| 104 | size_t cap = (size_t)num_records * (sizeof(record_t) + 32) + 1, n = 0; // a line: the fields, a number and tabs |
| 105 | char *buf = malloc(cap); |
| 106 | if (!buf) return; |
| 107 | for (int i = 0; i < num_records; i++) { |
| 108 | int r = snprintf(buf + n, cap - n, "%s\t%s\t%s\t%lld\t%s\n", records[i].id, records[i].sha, records[i].path, |
| 109 | records[i].when, records[i].title); |
| 110 | if (r < 0 || (size_t)r >= cap - n) break; |
| 111 | n += r; |
| 112 | } |
| 113 | snprintf(path, sizeof(path), "%s/downloaded.txt", plat_save_dir()); |
| 114 | if (!plat_write_file(path, buf, n)) |
| 115 | plat_log("store: couldn't write %s", path); |
| 116 | free(buf); |
| 117 | } |
| 118 |
|
| 119 | void store_init(void) |
| 120 | { |
| 121 | records = calloc(MAX_RECORDS, sizeof(record_t)); |
| 122 | snprintf(cache_dir, sizeof(cache_dir), "%s/cache", plat_save_dir()); |
| 123 | snprintf(icons_dir, sizeof(icons_dir), "%s/icons", cache_dir); |
| 124 | snprintf(previews_dir, sizeof(previews_dir), "%s/previews", cache_dir); |
| 125 | snprintf(files_dir, sizeof(files_dir), "%s/files", cache_dir); |
| 126 | plat_mkdirs(icons_dir); |
| 127 | plat_mkdirs(previews_dir); |
| 128 | plat_mkdirs(files_dir); |
| 129 | Settings_Load(); |
| 130 | if (records) Records_Load(); |
| 131 | } |
| 132 |
|
| 133 | const char *store_downloaded(const cat_item_t *it) |
| 134 | { |
| 135 | char sha[65]; |
| 136 | Hex(it->sha256, sha); |
| 137 | for (int i = num_records - 1; i >= 0; i--) |
| 138 | if (!strcmp(records[i].id, it->id)) |
| 139 | return strcmp(records[i].sha, sha) ? NULL : records[i].path; // an older version isn't this one |
| 140 | return NULL; |
| 141 | } |
| 142 |
|
| 143 | void store_mark_downloaded(const cat_item_t *it, const char *path) |
| 144 | { |
| 145 | int i; |
| 146 | if (!records) return; |
| 147 | for (i = 0; i < num_records; i++) |
| 148 | if (!strcmp(records[i].id, it->id)) break; |
| 149 | if (i == num_records) { |
| 150 | if (num_records == MAX_RECORDS) return; |
| 151 | num_records++; |
| 152 | } |
| 153 | record_t *r = &records[i]; |
| 154 | snprintf(r->id, sizeof(r->id), "%s", it->id); |
| 155 | Hex(it->sha256, r->sha); |
| 156 | snprintf(r->path, sizeof(r->path), "%s", path); |
| 157 | snprintf(r->title, sizeof(r->title), "%s", it->title); |
| 158 | for (char *c = r->title; *c; c++) // (a field of a tab-separated line) |
| 159 | if (*c == '\t' || *c == '\n' || *c == '\r') *c = ' '; |
| 160 | r->when = (long long)time(NULL); |
| 161 | r->size = it->size; |
| 162 | Records_Save(); |
| 163 | } |
| 164 |
|
| 165 | int store_num_downloads(void) { return num_records; } |
| 166 |
|
| 167 | // newest first |
| 168 | int store_download_at(int i, store_rec_t *out) |
| 169 | { |
| 170 | if (i < 0 || i >= num_records) return 0; |
| 171 | const record_t *r = &records[num_records - 1 - i]; |
| 172 | out->id = r->id; |
| 173 | out->sha = r->sha; |
| 174 | out->path = r->path; |
| 175 | out->title = r->title; |
| 176 | out->when = r->when; |
| 177 | out->size = r->size; |
| 178 | return 1; |
| 179 | } |
| 180 |
|
| 181 | int store_delete_download(int i) |
| 182 | { |
| 183 | if (i < 0 || i >= num_records) return 0; |
| 184 | int k = num_records - 1 - i; |
| 185 | if (remove(records[k].path) != 0 && plat_file_size(records[k].path) >= 0) { |
| 186 | plat_log("store: couldn't delete %s", records[k].path); |
| 187 | return 0; |
| 188 | } |
| 189 | plat_log("store: deleted %s", records[k].path); |
| 190 | memmove(&records[k], &records[k + 1], (num_records - 1 - k) * sizeof(record_t)); |
| 191 | num_records--; |
| 192 | Records_Save(); |
| 193 | return 1; |
| 194 | } |
| 195 |
|
| 196 | /* |
| 197 | ================== |
| 198 | Cache |
| 199 | ================== |
| 200 | */ |
| 201 | cat_index_t *store_load_index(void) |
| 202 | { |
| 203 | char path[600], err[160]; |
| 204 | size_t len; |
| 205 | snprintf(path, sizeof(path), "%s/catalog.json", cache_dir); |
| 206 | char *s = (char *)plat_read_file(path, &len); |
| 207 | if (!s) return NULL; |
| 208 | cat_index_t *ix = catalog_parse_index(s, len, err, sizeof(err)); |
| 209 | free(s); |
| 210 | if (!ix) plat_log("store: cached catalogue unusable: %s", err); |
| 211 | return ix; |
| 212 | } |
| 213 |
|
| 214 | void store_save_index(const char *json, size_t len) |
| 215 | { |
| 216 | char path[600]; |
| 217 | snprintf(path, sizeof(path), "%s/catalog.json", cache_dir); |
| 218 | if (!plat_write_file(path, json, len)) |
| 219 | plat_log("store: couldn't cache the catalogue in %s", path); |
| 220 | } |
| 221 |
|
| 222 | char *store_get_file(const cat_file_t *f, size_t *len, volatile int *cancel, int *err_kind, char *err, int errsize) |
| 223 | { |
| 224 | char path[600], hex[65], url[300]; |
| 225 | unsigned char got[32]; |
| 226 | Hex(f->sha256, hex); |
| 227 | snprintf(path, sizeof(path), "%s/%s", files_dir, hex); |
| 228 | *err_kind = ERR_NONE; |
| 229 | err[0] = 0; |
| 230 |
|
| 231 | char *s = (char *)plat_read_file(path, len); |
| 232 | if (s) { |
| 233 | mbedtls_sha256((const unsigned char *)s, *len, got, 0); |
| 234 | if (!memcmp(got, f->sha256, 32)) |
| 235 | return s; |
| 236 | free(s); // damaged: fetch it again |
| 237 | } |
| 238 | http_res_t res; |
| 239 | snprintf(url, sizeof(url), "%s%s", CATALOG_BASE, f->url); |
| 240 | s = http_fetch(NULL, url, NULL, NULL, 30, cancel, len, &res); |
| 241 | if (!s || res.status != 200) { |
| 242 | *err_kind = download_error_kind(res.err, s ? res.status : 0); |
| 243 | snprintf(err, errsize, "%s", s ? "HTTP error" : res.err); |
| 244 | if (s) snprintf(err, errsize, "HTTP %d", res.status); |
| 245 | free(s); |
| 246 | return NULL; |
| 247 | } |
| 248 | mbedtls_sha256((const unsigned char *)s, *len, got, 0); |
| 249 | if (memcmp(got, f->sha256, 32)) { |
| 250 | // the index and the page are from different uploads (or the upload is half done) |
| 251 | *err_kind = ERR_CHECKSUM; |
| 252 | snprintf(err, errsize, tr("%s doesn't match the catalogue (it may be being updated): try again later"), f->url); |
| 253 | free(s); |
| 254 | return NULL; |
| 255 | } |
| 256 | if (!plat_write_file(path, s, *len)) |
| 257 | plat_log("store: couldn't cache %s", path); |
| 258 | return s; |
| 259 | } |
| 260 |
|
| 261 | // a cached picture is named by its item and where it came from: when the catalogue |
| 262 | // points it somewhere new (a sharper one), it's fetched again |
| 263 | static unsigned Url_Hash(const char *s) |
| 264 | { |
| 265 | unsigned h = 2166136261u; |
| 266 | for (; *s; s++) h = (h ^ (unsigned char)*s) * 16777619u; |
| 267 | return h; |
| 268 | } |
| 269 |
|
| 270 | void store_icon_path(const cat_item_t *it, char *out, int size) |
| 271 | { |
| 272 | snprintf(out, size, "%s/%s-%08x.jpg", icons_dir, it->id, Url_Hash(it->icon)); |
| 273 | } |
| 274 |
|
| 275 | void store_preview_path(const cat_item_t *it, char *out, int size) |
| 276 | { |
| 277 | snprintf(out, size, "%s/%s-%08x", previews_dir, it->id, Url_Hash(it->preview)); |
| 278 | } |
| 279 |
|
| 280 | /* |
| 281 | ================== |
| 282 | Install targets |
| 283 | ================== |
| 284 | */ |
| 285 |
|
| 286 | // the file name from the URL's last part: %XX decoded, and nothing FAT32 refuses |
| 287 | static void File_Name(const char *url, char *out, int size) |
| 288 | { |
| 289 | const char *b = strrchr(url, '/'); |
| 290 | b = b ? b + 1 : url; |
| 291 | int o = 0; |
| 292 | for (; *b && o < size - 1; b++) { |
| 293 | char ch = *b; |
| 294 | if (ch == '%' && b[1] && b[2]) { |
| 295 | char hex[3] = {b[1], b[2], 0}; |
| 296 | ch = (char)strtol(hex, NULL, 16); |
| 297 | b += 2; |
| 298 | } |
| 299 | if ((unsigned char)ch < 0x20 || strchr("\\/:*?\"<>|", ch)) ch = '_'; |
| 300 | out[o++] = ch; |
| 301 | } |
| 302 | out[o] = 0; |
| 303 | if (!o) snprintf(out, size, "download"); |
| 304 | } |
| 305 |
|
| 306 | int store_profiles(char (*names)[64], int max) |
| 307 | { |
| 308 | char all[16][64]; |
| 309 | int n = plat_list_dirs(HOME_DIR, all, 16), k = 0; |
| 310 | for (int i = 0; i < n && k < max; i++) |
| 311 | if (strlen(all[i]) == 8 && strspn(all[i], "0123456789") == 8) // 00000001 ... |
| 312 | snprintf(names[k++], 64, "%s", all[i]); |
| 313 | return k; |
| 314 | } |
| 315 |
|
| 316 | const char *store_avatar_profile(void) |
| 317 | { |
| 318 | if (avatar_profile[0]) { |
| 319 | char p[128]; |
| 320 | snprintf(p, sizeof(p), HOME_DIR "/%s", avatar_profile); |
| 321 | if (plat_dir_exists(p)) return avatar_profile; |
| 322 | } |
| 323 | char names[16][64]; |
| 324 | if (store_profiles(names, 16) != 1) |
| 325 | return ""; // none, or more than one: the user picks |
| 326 | snprintf(avatar_profile, sizeof(avatar_profile), "%s", names[0]); |
| 327 | return avatar_profile; |
| 328 | } |
| 329 |
|
| 330 | void store_set_avatar_profile(const char *name) |
| 331 | { |
| 332 | snprintf(avatar_profile, sizeof(avatar_profile), "%s", name); |
| 333 | Settings_Save(); |
| 334 | } |
| 335 |
|
| 336 | int store_dest(const cat_item_t *it, char *out, int size) |
| 337 | { |
| 338 | char name[256]; |
| 339 | File_Name(it->url, name, sizeof(name)); |
| 340 | switch (it->type) { |
| 341 | case TYPE_HOMEBREW: |
| 342 | snprintf(out, size, "/dev_hdd0/packages/%s", name); |
| 343 | return DEST_OK; |
| 344 | case TYPE_THEME: |
| 345 | snprintf(out, size, "/dev_hdd0/theme/%s", name); |
| 346 | return DEST_OK; |
| 347 | case TYPE_AVATAR: { |
| 348 | const char *p = store_avatar_profile(); |
| 349 | if (!p[0]) { |
| 350 | char names[16][64]; |
| 351 | return store_profiles(names, 16) ? DEST_PICK_PROFILE : DEST_NO_PROFILE; |
| 352 | } |
| 353 | snprintf(out, size, HOME_DIR "/%s/psn_avatar/%s", p, name); |
| 354 | return DEST_OK; |
| 355 | } |
| 356 | } |
| 357 | return DEST_NO_PROFILE; |
| 358 | } |
| 359 |
|
| 360 | const char *store_dest_message(int code) |
| 361 | { |
| 362 | switch (code) { |
| 363 | case DEST_NO_PROFILE: |
| 364 | return "No user profiles were found in /dev_hdd0/home."; |
| 365 | case DEST_PICK_PROFILE: |
| 366 | return "Choose which user the avatar is for."; |
| 367 | } |
| 368 | return ""; |
| 369 | } |