kpmpgsmkii/client/src/system/cache.c

485 lines
12 KiB
C

/*
* Kangaroo Punch MultiPlayer Game Server Mark II
* Copyright (C) 2020-2021 Scott Duensing
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
// ***TODO*** Maybe add a cacheFOpen() and cacheFClose() for reading and writing directly to virtual pathnames?
#include "thirdparty/SHA256/sha256.h"
#include "cache.h"
#define CACHE_FIELD_SHA256 0
#define CACHE_FIELD_ENTRYNAME 1
#define CACHE_FIELD_FILENAME 2
static char *cacheEntryNameDirGet(char *entryName, uint8_t includeCache);
static char *cacheEntryNameGenerate(void);
static char *cacheFieldGet(char *virtualPath, uint8_t field);
uint8_t cacheDelete(char *virtualPath) {
FILE *in = NULL;
FILE *out = NULL;
char index[16] = { 0 };
char indexNew[16] = { 0 };
char buffer[1024] = { 0 };
char *name = NULL;
char *path = NULL;
char *dir2 = NULL;
char *dir1 = NULL;
char *entryDir = NULL;
// Deletes entry from cache index AND the data from the cache.
sprintf(index, "CACHE%cINDEX.DAT", OS_PATH_SLASH);
sprintf(indexNew, "CACHE%cINDEX.NEW", OS_PATH_SLASH);
// Do we have an index yet?
if (osFileExists(index)) {
in = fopen(index, "rt");
if (in) {
out = fopen(indexNew, "wt");
if (out) {
while (fgets(buffer, 1024, in) != 0) {
name = strstr(buffer, " ");
*name = 0;
name++;
path = strstr(name, " ");
*path = 0;
path++;
path[strlen(path) - 1] = 0;
if (strcmp(virtualPath, path) == 0) {
// Found! Delete data from disk.
entryDir = cacheEntryNameDirGet(name, 1);
unlink(entryDir);
// Attempt to remove the second-level directory.
dir2 = osLastPathComponentGetUpTo(entryDir);
if (rmdir(dir2) == 0) {
// Attempt to move the top level directory.
dir1 = osLastPathComponentGetUpTo(dir2);
rmdir(dir1);
DEL(dir1);
}
DEL(dir2);
} else {
// Not what we're after, copy to new index.
fprintf(out, "%s %s %s\n", buffer, name, path);
}
}
} else {
fclose(in);
return FAIL;
}
fclose(in);
unlink(index);
rename(indexNew, index);
return SUCCESS;
}
}
return FAIL;
}
uint8_t cacheEntryAdd(char *sha256, char *entryName, char *virtualPath) {
FILE *in = NULL;
FILE *out = NULL;
char index[16] = { 0 };
char indexNew[16] = { 0 };
char *name = NULL;
char *path = NULL;
char buffer[1024] = { 0 };
uint8_t found = 0;
// This adds or updates an entry to the index.
// It does not add data to the actual cache.
// Index format is: SHA256 ENTRYNAME VIRTUALPATH
sprintf(index, "CACHE%cINDEX.DAT", OS_PATH_SLASH);
sprintf(indexNew, "CACHE%cINDEX.NEW", OS_PATH_SLASH);
// Do we have an index yet?
if (!osFileExists(index)) {
// Nope. Just add this entry and be done.
out = fopen(index, "wt");
if (out) {
fprintf(out, "%s %s %s\n", sha256, entryName, virtualPath);
fclose(out);
return SUCCESS;
}
return FAIL;
}
// Read existing index, update the entry if it exists.
// If not, we'll add it at the end.
in = fopen(index, "rt");
if (in) {
out = fopen(indexNew, "wt");
if (out) {
while (fgets(buffer, 1024, in) != 0) {
name = strstr(buffer, " ");
*name = 0;
name++;
path = strstr(name, " ");
*path = 0;
path++;
path[strlen(path) - 1] = 0;
if (strcmp(virtualPath, path) == 0) {
// Update this entry.
fprintf(out, "%s %s %s\n", sha256, entryName, virtualPath);
found = 1;
} else {
// Not what we're after, copy to new index.
fprintf(out, "%s %s %s\n", buffer, name, path);
}
}
// Did we replace an entry?
if (!found) {
// Add new entry to end.
fprintf(out, "%s %s %s\n", sha256, entryName, virtualPath);
}
fclose(out);
} else {
fclose(in);
return FAIL;
}
fclose(in);
unlink(index);
rename(indexNew, index);
return SUCCESS;
}
return FAIL;
}
static char *cacheEntryNameDirGet(char *entryName, uint8_t includeCache) {
uint8_t i = 0;
uint8_t j = 0;
static char dir[21] = "CACHE";
dir[5] = OS_PATH_SLASH;
j = 6;
for (i=0; i<13; i++) {
dir[j++] = entryName[i];
if ((i == 1) || (i == 3)) {
dir[j++] = OS_PATH_SLASH;
}
}
dir[20] = 0;
return includeCache ? dir : &dir[6];
}
static char *cacheEntryNameGenerate(void) {
uint8_t i = 0;
static char name[13] = { 0 };
// Cached files are stored in a two-layer directory tree to
// reduce the number of files in any single folder.
// The folder names are two digits.
// The filename is eight random letters.
// First directory level.
i = random() % 100;
sprintf(&name[0], "%02d", i);
// Second directory level.
i = random() % 100;
sprintf(&name[2], "%02d", i);
// Random name.
for (i=4; i<12; i++) name[i] = 'A' + (random() % 26);
name[12] = 0;
return name;
}
char *cacheEntryNameGet(char *virtualPath) {
return cacheFieldGet(virtualPath, CACHE_FIELD_ENTRYNAME);
}
uint8_t cacheExists(void) {
char index[16] = { 0 };
sprintf(index, "CACHE%cINDEX.DAT", OS_PATH_SLASH);
return osFileExists(index);
}
void cacheFClose(FILE *handle) {
// Just wrap fclose() for now.
fclose(handle);
}
static char *cacheFieldGet(char *virtualPath, uint8_t field) {
FILE *in = NULL;
char index[16] = { 0 };
static char buffer[1024] = { 0 };
static char *name = NULL;
static char *path = NULL;
static char *result = NULL;
// Return SHA256 given virtual path or NULL if not found.
sprintf(index, "CACHE%cINDEX.DAT", OS_PATH_SLASH);
result = NULL;
// Do we have an index yet?
if (osFileExists(index)) {
in = fopen(index, "rt");
if (in) {
while ((fgets(buffer, 1024, in) != 0) && result == NULL) {
name = strstr(buffer, " ");
*name = 0;
name++;
path = strstr(name, " ");
*path = 0;
path++;
path[strlen(path) - 1] = 0;
if (strcmp(virtualPath, path) == 0) {
// Found! Return requested data.
switch (field) {
case CACHE_FIELD_SHA256:
result = buffer;
break;
case CACHE_FIELD_ENTRYNAME:
result = name;
break;
case CACHE_FIELD_FILENAME:
result = cacheEntryNameDirGet(name, 1);
break;
}
}
}
fclose(in);
}
}
return result;
}
char *cacheFilenameGet(char *virtualPath) {
return cacheFieldGet(virtualPath, CACHE_FIELD_FILENAME);
}
FILE *cacheFOpen(char *vpath, char *mode) {
FILE *handle = NULL;
char *entry = NULL;
char *dir = NULL;
char *temp = NULL;
// Does this file already exist?
entry = cacheEntryNameGet(vpath);
// If we're writing or appending, do we need to create it?
if ((entry == NULL) && (mode[0] == 'w' || mode[0] == 'a')) {
do {
entry = cacheEntryNameGenerate();
dir = cacheEntryNameDirGet(entry, 1);
temp = osLastPathComponentGetUpTo(dir);
osMkDirP(temp);
DEL(temp);
} while (osFileExists(dir));
// Dummy SHA for files we create ourselves. They're never checked against the server.
cacheEntryAdd("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", entry, vpath);
}
// Did we get a file?
if (entry) {
dir = cacheEntryNameDirGet(entry, 1);
handle = fopen(dir, mode);
}
return handle;
}
uint8_t cachePrePack(char *name, CachePreMakeListT *list) {
FILE *in = NULL;
FILE *out = NULL;
uint16_t i = 0;
uint8_t result = SUCCESS;
uint32_t length = 0;
uint32_t total = 0;
uint32_t temp = 0;
int8_t buffer[8192] = { 0 };
sha256_context ctx = { 0 };
uint8_t hv[32] = { 0 };
// Prebuilt cache format:
// - Virtual Path (zero terminated)
// - x bytes hash
// - 4 bytes unsigned length of data
// - length bytes of data
// ... repeat for each file ...
// - 4 bytes length of entire cache (used for embedding cache data)
out = fopen(name, "wb");
if (out) {
while (list[i].filename != NULL) {
in = fopen(list[i].filename, "rb");
if (in) {
// Get length of file.
fseek(in, 0, SEEK_END);
length = ftell(in);
fseek(in, 0, SEEK_SET);
// Generate SHA256.
sha256_init(&ctx);
do {
temp = fread(buffer, 1, sizeof(buffer), in);
if (temp) sha256_hash(&ctx, (uint8_t *)buffer, temp);
} while (temp > 0);
sha256_done(&ctx, hv);
fseek(in, 0, SEEK_SET);
// Write header.
temp = strlen(list[i].virtualPath) + 1;
fwrite(list[i].virtualPath, temp, 1, out);
fwrite(hv, sizeof(hv), 1, out);
fwrite(&length, sizeof(uint32_t), 1, out);
total += temp + sizeof(hv) + sizeof(uint32_t) + length;
// Copy data.
do {
temp = fread(buffer, 1, sizeof(buffer), in);
if (temp) fwrite(buffer, 1, temp, out);
} while (temp > 0);
fclose(in);
} else {
result = FAIL;
}
i++;
}
// Write length of entire block.
total += sizeof(uint32_t);
fwrite(&total, sizeof(uint32_t), 1, out);
fclose(out);
} else {
result = FAIL;
}
return result;
}
uint8_t cachePreUnpack(char *name) {
FILE *in = NULL;
FILE *out = NULL;
char *entryName = NULL;
char *entryDir = NULL;
char *temp = NULL;
uint8_t result = SUCCESS;
uint32_t i = 0;
uint32_t length = 0;
uint32_t total = 0;
int8_t buffer[8192] = { 0 };
uint8_t hv[32] = { 0 };
char virtualPath[CACHE_VIRTUAL_PATH_MAX] = { 0 };
in = fopen(name, "rb");
if (in) {
// Get entire pack size.
fseek(in, -sizeof(uint32_t), SEEK_END);
fread(&total, 1, sizeof(uint32_t), in);
fseek(in, 0, SEEK_SET);
do {
// Read virtual path.
i = 0;
do {
virtualPath[i++] = fgetc(in);
} while (virtualPath[i - 1] != 0);
// Read SHA256.
fread(hv, 1, sizeof(hv), in);
for (i=0; i<32; i++) sprintf((char *)&buffer[i * 2], "%02x", hv[i]);
buffer[64] = 0;
// Read length.
fread(&length, 1, sizeof(uint32_t), in);
// Get filename from cache manager & create missing directories.
do {
entryName = cacheEntryNameGenerate();
entryDir = cacheEntryNameDirGet(entryName, 1);
temp = osLastPathComponentGetUpTo(entryDir);
osMkDirP(temp);
DEL(temp);
} while (osFileExists(entryDir));
cacheEntryAdd((char *)buffer, entryName, virtualPath);
out = fopen(entryDir, "wb");
if (out) {
// Copy out data.
do {
if (length > sizeof(buffer)) {
i = sizeof(buffer);
} else {
i = length;
}
length -= i;
fread(buffer, 1, i, in);
fwrite(buffer, 1, i, out);
} while (length > 0);
fclose(out);
} else {
result = FAIL;
}
} while ((unsigned)ftell(in) < (total - sizeof(uint32_t)));
fclose(in);
} else {
result = FAIL;
}
return result;
}
char *cacheSha256Get(char *virtualPath) {
return cacheFieldGet(virtualPath, CACHE_FIELD_SHA256);
}
void cacheShutdown(void) {
// Nada.
}
void cacheStartup(char *appName) {
char *temp = NULL;
// Do we need to unpack the initial cache?
if (!cacheExists()) {
temp = utilAppNameWithNewExtensionGet(appName, "pre");
cachePreUnpack(temp);
DEL(temp);
}
}