mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-22 03:42:55 -05:00
See CHANGELOG.md (6.0.0). Full history of the work: tag ftx2-last and branch history archived in ps5upload-docs.
144 lines
5.5 KiB
C
144 lines
5.5 KiB
C
/* Host-side tests for UTF-16 → UTF-8 cheat-XML normalisation (#317).
|
|
*
|
|
* The bug this guards: SHN/MC4 trainer files are sometimes saved UTF-16
|
|
* (a real `ff fe`/`fe ff` BOM). The cheat parser scans bytes for ASCII tags
|
|
* like "<Cheat", which never match `<\0C\0h\0e\0a\0t\0`, so those files
|
|
* parsed to zero cheats and "didn't appear for some games". These cases
|
|
* cover the byte shapes actually seen in the wild (a real UTF-16LE SHN from
|
|
* etaHEN's repo starts exactly `ff fe 3c 00 3f 00 78 00 6d 00 6c 00`). */
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "../src/xml_encoding.c"
|
|
|
|
static int failures = 0;
|
|
|
|
#define CHECK(expr) \
|
|
do { \
|
|
if (!(expr)) { \
|
|
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \
|
|
failures++; \
|
|
} \
|
|
} while (0)
|
|
|
|
/* Build the UTF-16 (LE or BE) encoding of an ASCII string into `out`,
|
|
* optionally with a BOM. Returns the byte length. */
|
|
static size_t make_utf16(const char *ascii, int little, int bom,
|
|
unsigned char *out) {
|
|
size_t o = 0;
|
|
if (bom) {
|
|
if (little) { out[o++] = 0xFF; out[o++] = 0xFE; }
|
|
else { out[o++] = 0xFE; out[o++] = 0xFF; }
|
|
}
|
|
for (const char *p = ascii; *p; p++) {
|
|
if (little) { out[o++] = (unsigned char)*p; out[o++] = 0x00; }
|
|
else { out[o++] = 0x00; out[o++] = (unsigned char)*p; }
|
|
}
|
|
return o;
|
|
}
|
|
|
|
int main(void) {
|
|
unsigned char buf[512];
|
|
size_t out_len = 0;
|
|
|
|
/* Plain 8-bit input is left alone (NULL → caller parses the original). */
|
|
{
|
|
const char *ascii = "<Trainer><Cheat Text=\"x\"></Cheat></Trainer>";
|
|
char *r = xml_to_utf8(ascii, strlen(ascii), &out_len);
|
|
CHECK(r == NULL);
|
|
CHECK(out_len == 0);
|
|
}
|
|
|
|
/* UTF-16LE with BOM → the ASCII tags reappear so the parser can see them. */
|
|
{
|
|
size_t n = make_utf16("<Cheat Text=\"Infinite Money\">", 1, 1, buf);
|
|
char *r = xml_to_utf8((char *)buf, n, &out_len);
|
|
CHECK(r != NULL);
|
|
CHECK(r && strcmp(r, "<Cheat Text=\"Infinite Money\">") == 0);
|
|
CHECK(out_len == strlen("<Cheat Text=\"Infinite Money\">"));
|
|
free(r);
|
|
}
|
|
|
|
/* UTF-16BE with BOM. */
|
|
{
|
|
size_t n = make_utf16("<Cheat>", 0, 1, buf);
|
|
char *r = xml_to_utf8((char *)buf, n, &out_len);
|
|
CHECK(r != NULL);
|
|
CHECK(r && strcmp(r, "<Cheat>") == 0);
|
|
free(r);
|
|
}
|
|
|
|
/* UTF-16LE with NO BOM — recognised by the odd-byte-NUL heuristic. */
|
|
{
|
|
size_t n = make_utf16("<Cheatline><Offset>27693CA</Offset>", 1, 0, buf);
|
|
char *r = xml_to_utf8((char *)buf, n, &out_len);
|
|
CHECK(r != NULL);
|
|
CHECK(r && strcmp(r, "<Cheatline><Offset>27693CA</Offset>") == 0);
|
|
free(r);
|
|
}
|
|
|
|
/* UTF-16BE with NO BOM — even-byte-NUL heuristic. */
|
|
{
|
|
size_t n = make_utf16("<Cheat Name=\"y\">", 0, 0, buf);
|
|
char *r = xml_to_utf8((char *)buf, n, &out_len);
|
|
CHECK(r != NULL);
|
|
CHECK(r && strcmp(r, "<Cheat Name=\"y\">") == 0);
|
|
free(r);
|
|
}
|
|
|
|
/* A non-ASCII BMP char (é = U+00E9) round-trips to correct 2-byte UTF-8. */
|
|
{
|
|
unsigned char le[8];
|
|
size_t o = 0;
|
|
le[o++] = 0xFF; le[o++] = 0xFE; /* BOM */
|
|
le[o++] = 0xE9; le[o++] = 0x00; /* é */
|
|
char *r = xml_to_utf8((char *)le, o, &out_len);
|
|
CHECK(r != NULL);
|
|
CHECK(out_len == 2);
|
|
CHECK(r && (unsigned char)r[0] == 0xC3 && (unsigned char)r[1] == 0xA9);
|
|
free(r);
|
|
}
|
|
|
|
/* Too short to classify → NULL, no crash. */
|
|
{
|
|
char *r = xml_to_utf8("<", 1, &out_len);
|
|
CHECK(r == NULL);
|
|
}
|
|
|
|
/* MC4 plaintext is entity- and backslash-escaped XML (R16, #373). This is
|
|
* the head of the real decrypted etaHEN CUSA00002_01.00.mc4. */
|
|
{
|
|
char t[] = "<?xml version=\\"1.0\\" encoding=\\"utf-16\\"?>\r\n"
|
|
"<Trainer Game=\\"Killzone Shadow Fall\\" Cusa=\\"CUSA00002\\">\r\n"
|
|
" <Cheat Control=\\"Toggel\\" Text=\\"A & B"><>";
|
|
size_t n = xml_unescape_entities(t, strlen(t));
|
|
CHECK(n == strlen(t));
|
|
CHECK(strstr(t, "<?xml version=\"1.0\" encoding=\"utf-16\"?>") == t);
|
|
CHECK(strstr(t, "<Trainer Game=\"Killzone Shadow Fall\" Cusa=\"CUSA00002\">") != NULL);
|
|
CHECK(strstr(t, "<Cheat Control=\"Toggel\" Text=\"A & B\"><>") != NULL);
|
|
}
|
|
/* Plain XML and other text are untouched. */
|
|
{
|
|
char t[] = "<Trainer Game=\"a & b\"></Trainer>";
|
|
size_t n = xml_unescape_entities(t, strlen(t));
|
|
CHECK(n == strlen(t));
|
|
CHECK(strcmp(t, "<Trainer Game=\"a & b\"></Trainer>") == 0);
|
|
char u[] = "{\\\"x\\\":1}";
|
|
CHECK(xml_unescape_entities(u, strlen(u)) == strlen(u));
|
|
}
|
|
/* Unknown entities and a lone trailing backslash survive. */
|
|
{
|
|
char t[] = "<a b= >\\";
|
|
size_t n = xml_unescape_entities(t, strlen(t));
|
|
CHECK(strcmp(t, "<a b= >\\") == 0);
|
|
CHECK(n == strlen(t));
|
|
}
|
|
|
|
if (failures == 0) {
|
|
printf("xml_encoding_selftest: all checks passed\n");
|
|
return 0;
|
|
}
|
|
fprintf(stderr, "xml_encoding_selftest: %d failure(s)\n", failures);
|
|
return 1;
|
|
}
|