273 lines
8.1 KiB
C
273 lines
8.1 KiB
C
/**
|
|
* Copyright (c) 2026 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "assetdsk.h"
|
|
#include "util/memory.h"
|
|
#include "util/endian.h"
|
|
#include "assert/assert.h"
|
|
#include <zlib.h>
|
|
|
|
errorret_t assetDskParseHeader(
|
|
const uint8_t *bytes,
|
|
const size_t bytesSize,
|
|
assetdskheader_t *outHeader
|
|
) {
|
|
assertNotNull(bytes, "Bytes cannot be NULL.");
|
|
assertNotNull(outHeader, "Out header cannot be NULL.");
|
|
|
|
if(bytesSize < ASSET_DSK_HEADER_SIZE) {
|
|
errorThrow("dusk.dsk header is truncated.");
|
|
}
|
|
|
|
if(memoryCompare(bytes, ASSET_DSK_MAGIC, ASSET_DSK_MAGIC_SIZE) != 0) {
|
|
errorThrow("dusk.dsk has an invalid magic header.");
|
|
}
|
|
|
|
// Every field is a little-endian uint32_t regardless of host - convert
|
|
// to host order (a no-op on little-endian hosts, a real byteswap on
|
|
// Dolphin's big-endian PowerPC).
|
|
uint32_t fields[7];
|
|
memoryCopy(fields, bytes + ASSET_DSK_MAGIC_SIZE, sizeof(fields));
|
|
for(uint8_t i = 0; i < 7; i++) {
|
|
fields[i] = endianLittleToHost32(fields[i]);
|
|
}
|
|
|
|
const uint32_t version = fields[0];
|
|
if(version != ASSET_DSK_VERSION) {
|
|
errorThrow("dusk.dsk has an unsupported version: %u", version);
|
|
}
|
|
|
|
outHeader->compressedOffset = fields[1];
|
|
outHeader->compressedSize = fields[2];
|
|
outHeader->compressedChecksum = fields[3];
|
|
outHeader->storedOffset = fields[4];
|
|
outHeader->storedSize = fields[5];
|
|
outHeader->storedChecksum = fields[6];
|
|
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t assetDskOpenFromPath(
|
|
const char_t *path,
|
|
zip_t **outCompressed,
|
|
zip_t **outStored
|
|
) {
|
|
errorChain(assetDskOpenFromPathRange(
|
|
path, 0, SIZE_MAX, outCompressed, outStored
|
|
));
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t assetDskOpenFromPathRange(
|
|
const char_t *path,
|
|
const size_t baseOffset,
|
|
const size_t baseSize,
|
|
zip_t **outCompressed,
|
|
zip_t **outStored
|
|
) {
|
|
assertNotNull(path, "Path cannot be NULL.");
|
|
assertNotNull(outCompressed, "Out compressed cannot be NULL.");
|
|
assertNotNull(outStored, "Out stored cannot be NULL.");
|
|
|
|
*outCompressed = NULL;
|
|
*outStored = NULL;
|
|
|
|
FILE *headerFile = fopen(path, "rb");
|
|
if(headerFile == NULL) {
|
|
errorThrow("Failed to open dusk.dsk: %s", path);
|
|
}
|
|
|
|
if(fseek(headerFile, (long) baseOffset, SEEK_SET) != 0) {
|
|
fclose(headerFile);
|
|
errorThrow("Failed to seek to dusk.dsk range in file: %s", path);
|
|
}
|
|
|
|
uint8_t headerBytes[ASSET_DSK_HEADER_SIZE];
|
|
size_t headerRead = fread(headerBytes, 1, sizeof(headerBytes), headerFile);
|
|
fclose(headerFile);
|
|
if(headerRead != sizeof(headerBytes)) {
|
|
errorThrow("Failed to read dusk.dsk header: %s", path);
|
|
}
|
|
|
|
assetdskheader_t header;
|
|
errorChain(assetDskParseHeader(headerBytes, sizeof(headerBytes), &header));
|
|
|
|
if(
|
|
(size_t) header.compressedOffset + header.compressedSize > baseSize ||
|
|
(size_t) header.storedOffset + header.storedSize > baseSize
|
|
) {
|
|
errorThrow("dusk.dsk header describes ranges beyond its containing file.");
|
|
}
|
|
|
|
zip_error_t zipError;
|
|
zip_error_init(&zipError);
|
|
|
|
zip_source_t *compressedSource = zip_source_file_create(
|
|
path,
|
|
(zip_uint64_t) (baseOffset + header.compressedOffset),
|
|
(zip_int64_t) header.compressedSize,
|
|
&zipError
|
|
);
|
|
if(compressedSource == NULL) {
|
|
errorThrow(
|
|
"Failed to create compressed dusk.dsk source: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
*outCompressed = zip_open_from_source(compressedSource, ZIP_RDONLY, &zipError);
|
|
if(*outCompressed == NULL) {
|
|
zip_source_free(compressedSource);
|
|
errorThrow(
|
|
"Failed to open compressed dusk.dsk archive: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
zip_source_t *storedSource = zip_source_file_create(
|
|
path,
|
|
(zip_uint64_t) (baseOffset + header.storedOffset),
|
|
(zip_int64_t) header.storedSize,
|
|
&zipError
|
|
);
|
|
if(storedSource == NULL) {
|
|
zip_close(*outCompressed);
|
|
*outCompressed = NULL;
|
|
errorThrow(
|
|
"Failed to create stored dusk.dsk source: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
*outStored = zip_open_from_source(storedSource, ZIP_RDONLY, &zipError);
|
|
if(*outStored == NULL) {
|
|
zip_source_free(storedSource);
|
|
zip_close(*outCompressed);
|
|
*outCompressed = NULL;
|
|
errorThrow(
|
|
"Failed to open stored dusk.dsk archive: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
// The stored archive is small by convention, so verifying its checksum
|
|
// here (one extra small read) is cheap; the compressed archive isn't
|
|
// checked since it's meant to be read lazily/on-demand from here on.
|
|
uint8_t *storedBytes = (uint8_t *) memoryAllocate(header.storedSize);
|
|
FILE *storedFile = fopen(path, "rb");
|
|
if(storedFile == NULL) {
|
|
memoryFree(storedBytes);
|
|
zip_close(*outStored);
|
|
zip_close(*outCompressed);
|
|
*outStored = NULL;
|
|
*outCompressed = NULL;
|
|
errorThrow("Failed to re-open dusk.dsk to verify stored checksum: %s", path);
|
|
}
|
|
fseek(storedFile, (long) (baseOffset + header.storedOffset), SEEK_SET);
|
|
size_t storedRead = fread(storedBytes, 1, header.storedSize, storedFile);
|
|
fclose(storedFile);
|
|
if(storedRead != header.storedSize) {
|
|
memoryFree(storedBytes);
|
|
zip_close(*outStored);
|
|
zip_close(*outCompressed);
|
|
*outStored = NULL;
|
|
*outCompressed = NULL;
|
|
errorThrow("Failed to read dusk.dsk stored archive to verify checksum: %s", path);
|
|
}
|
|
|
|
uint32_t checksum = (uint32_t) crc32(0L, storedBytes, (uInt) header.storedSize);
|
|
memoryFree(storedBytes);
|
|
if(checksum != header.storedChecksum) {
|
|
zip_close(*outStored);
|
|
zip_close(*outCompressed);
|
|
*outStored = NULL;
|
|
*outCompressed = NULL;
|
|
errorThrow("dusk.dsk stored archive failed checksum verification: %s", path);
|
|
}
|
|
|
|
errorOk();
|
|
}
|
|
|
|
errorret_t assetDskOpenFromBuffer(
|
|
uint8_t *buffer,
|
|
const size_t bufferSize,
|
|
zip_t **outCompressed,
|
|
zip_t **outStored
|
|
) {
|
|
assertNotNull(buffer, "Buffer cannot be NULL.");
|
|
assertNotNull(outCompressed, "Out compressed cannot be NULL.");
|
|
assertNotNull(outStored, "Out stored cannot be NULL.");
|
|
|
|
*outCompressed = NULL;
|
|
*outStored = NULL;
|
|
|
|
assetdskheader_t header;
|
|
errorChain(assetDskParseHeader(buffer, bufferSize, &header));
|
|
|
|
if(
|
|
(size_t) header.compressedOffset + header.compressedSize > bufferSize ||
|
|
(size_t) header.storedOffset + header.storedSize > bufferSize
|
|
) {
|
|
errorThrow("dusk.dsk header describes ranges beyond the buffer.");
|
|
}
|
|
|
|
uint32_t compressedChecksum = (uint32_t) crc32(
|
|
0L, buffer + header.compressedOffset, (uInt) header.compressedSize
|
|
);
|
|
if(compressedChecksum != header.compressedChecksum) {
|
|
errorThrow("dusk.dsk compressed archive failed checksum verification.");
|
|
}
|
|
|
|
uint32_t storedChecksum = (uint32_t) crc32(
|
|
0L, buffer + header.storedOffset, (uInt) header.storedSize
|
|
);
|
|
if(storedChecksum != header.storedChecksum) {
|
|
errorThrow("dusk.dsk stored archive failed checksum verification.");
|
|
}
|
|
|
|
zip_error_t zipError;
|
|
zip_error_init(&zipError);
|
|
|
|
// freep=0 for both - they're non-owning windows into the same caller-owned
|
|
// buffer, not independent allocations libzip should free.
|
|
zip_source_t *compressedSource = zip_source_buffer_create(
|
|
buffer + header.compressedOffset, header.compressedSize, 0, &zipError
|
|
);
|
|
if(compressedSource == NULL) {
|
|
errorThrow(
|
|
"Failed to create compressed dusk.dsk source: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
*outCompressed = zip_open_from_source(compressedSource, ZIP_RDONLY, &zipError);
|
|
if(*outCompressed == NULL) {
|
|
zip_source_free(compressedSource);
|
|
errorThrow(
|
|
"Failed to open compressed dusk.dsk archive: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
zip_source_t *storedSource = zip_source_buffer_create(
|
|
buffer + header.storedOffset, header.storedSize, 0, &zipError
|
|
);
|
|
if(storedSource == NULL) {
|
|
zip_close(*outCompressed);
|
|
*outCompressed = NULL;
|
|
errorThrow(
|
|
"Failed to create stored dusk.dsk source: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
*outStored = zip_open_from_source(storedSource, ZIP_RDONLY, &zipError);
|
|
if(*outStored == NULL) {
|
|
zip_source_free(storedSource);
|
|
zip_close(*outCompressed);
|
|
*outCompressed = NULL;
|
|
errorThrow(
|
|
"Failed to open stored dusk.dsk archive: %s", zip_error_strerror(&zipError)
|
|
);
|
|
}
|
|
|
|
errorOk();
|
|
}
|