Files
dusk/src/dusk/asset/assetdsk.c
T

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();
}