Play-/Source/CsoImageStream.cpp
2018-04-30 21:01:23 +01:00

241 lines
5.8 KiB
C++

#include <algorithm>
#include <stdexcept>
#include <string.h>
#include <assert.h>
#include "CsoImageStream.h"
#include "zlib.h"
typedef uint32 uint32_le;
typedef uint64 uint64_le;
static const uint32 CSO_READ_BUFFER_SIZE = 256 * 1024;
struct CsoHeader
{
uint8 magic[4];
uint32_le header_size;
uint64_le total_bytes;
uint32_le frame_size;
uint8 ver;
uint8 align;
uint8 reserved[2];
};
CCsoImageStream::CCsoImageStream(CStream* baseStream)
: m_baseStream(baseStream)
, m_readBuffer(nullptr)
, m_zlibBuffer(nullptr)
, m_index(nullptr)
, m_position(0)
{
if(baseStream == nullptr)
{
throw std::runtime_error("Null base stream supplied.");
}
ReadFileHeader();
InitializeBuffers();
}
CCsoImageStream::~CCsoImageStream()
{
delete[] m_readBuffer;
delete[] m_zlibBuffer;
delete[] m_index;
}
void CCsoImageStream::ReadFileHeader()
{
CsoHeader hdr = {};
m_baseStream->Seek(0, Framework::STREAM_SEEK_SET);
if(m_baseStream->Read(&hdr, sizeof(CsoHeader)) != sizeof(CsoHeader))
{
throw std::runtime_error("Could not read full CSO header.");
}
if(hdr.magic[0] != 'C' || hdr.magic[1] != 'I' || hdr.magic[2] != 'S' || hdr.magic[3] != 'O')
{
throw std::runtime_error("Not a valid CSO file.");
}
if(hdr.ver > 1)
{
throw std::runtime_error("Only CSOv1 supported right now.");
}
m_frameSize = hdr.frame_size;
if((m_frameSize & (m_frameSize - 1)) != 0)
{
throw std::runtime_error("CSO frame size must be a power of two.");
}
if(m_frameSize < 0x800)
{
throw std::runtime_error("CSO frame size must be at least one sector.");
}
// Determine the translation from bytes to frame.
m_frameShift = 0;
for(uint32 i = m_frameSize; i > 1; i >>= 1)
{
++m_frameShift;
}
m_indexShift = hdr.align;
m_totalSize = hdr.total_bytes;
}
void CCsoImageStream::InitializeBuffers()
{
uint32 numFrames = static_cast<uint32>((m_totalSize + m_frameSize - 1) / m_frameSize);
// We might read a bit of alignment too, so be prepared.
if(m_frameSize + (1 << m_indexShift) < CSO_READ_BUFFER_SIZE)
{
m_readBuffer = new uint8[CSO_READ_BUFFER_SIZE];
}
else
{
m_readBuffer = new uint8[m_frameSize + (1 << m_indexShift)];
}
m_zlibBuffer = new uint8[m_frameSize + (1 << m_indexShift)];
m_zlibBufferFrame = numFrames;
const uint32 indexSize = numFrames + 1;
m_index = new uint32[indexSize];
if(m_baseStream->Read(m_index, sizeof(uint32) * indexSize) != sizeof(uint32) * indexSize)
{
throw std::runtime_error("Unable to read CSO index.");
}
}
void CCsoImageStream::Seek(int64 position, Framework::STREAM_SEEK_DIRECTION origin)
{
switch(origin)
{
case Framework::STREAM_SEEK_CUR:
m_position += position;
break;
case Framework::STREAM_SEEK_SET:
m_position = position;
break;
case Framework::STREAM_SEEK_END:
m_position = GetTotalSize() + position;
break;
}
}
uint64 CCsoImageStream::Tell()
{
return m_position;
}
bool CCsoImageStream::IsEOF()
{
return m_position >= GetTotalSize();
}
uint64 CCsoImageStream::Read(void* buffer, uint64 size)
{
uint64 remaining = size;
uint8* dest = reinterpret_cast<uint8*>(buffer);
// We read only one frame at a time for now.
while(remaining > 0 && !IsEOF())
{
uint32 bytes = ReadFromNextFrame(dest, remaining);
remaining -= bytes;
m_position += bytes;
dest += bytes;
}
return size - remaining;
}
uint64 CCsoImageStream::Write(const void* buffer, uint64 size)
{
throw std::runtime_error("Unable to write to CSO, read only.");
}
uint64 CCsoImageStream::GetTotalSize() const
{
return m_totalSize;
}
uint32 CCsoImageStream::ReadFromNextFrame(uint8* dest, uint64 maxBytes)
{
assert(!IsEOF());
const uint32 frame = static_cast<uint32>(m_position >> m_frameShift);
const uint32 offset = static_cast<uint32>(m_position - (frame << m_frameShift));
// This is how many bytes we will actually be reading from this frame.
const uint32 bytes = static_cast<uint32>(std::min(maxBytes, static_cast<uint64>(m_frameSize - offset)));
// Grab the index data for the frame we're about to read.
const bool compressed = (m_index[frame + 0] & 0x80000000) == 0;
const uint32 index0 = m_index[frame + 0] & 0x7FFFFFFF;
const uint32 index1 = m_index[frame + 1] & 0x7FFFFFFF;
// Calculate where the compressed payload is (if compressed.)
const uint64 frameRawPos = static_cast<uint64>(index0) << m_indexShift;
const uint64 frameRawSize = (index1 - index0) << m_indexShift;
if(!compressed)
{
// Just read directly, easy.
if(ReadBaseAt(frameRawPos + offset, dest, bytes) != bytes)
{
throw std::runtime_error("Unable to read uncompressed bytes from CSO.");
}
}
else
{
// We don't need to decompress if we already did this same frame last time.
if(m_zlibBufferFrame != frame)
{
// This might be less bytes than frameRawSize in case of padding on the last frame.
// This is because the index positions must be aligned.
const uint64 readRawBytes = ReadBaseAt(frameRawPos, m_readBuffer, frameRawSize);
DecompressFrame(frame, readRawBytes);
}
// Now we just copy the offset data from the cache.
memcpy(dest, m_zlibBuffer + offset, bytes);
}
return bytes;
}
void CCsoImageStream::DecompressFrame(uint32 frame, uint64 readBufferSize)
{
z_stream z;
z.zalloc = Z_NULL;
z.zfree = Z_NULL;
z.opaque = Z_NULL;
if(inflateInit2(&z, -15) != Z_OK)
{
throw std::runtime_error("Unable to initialize zlib for CSO decompression.");
}
z.next_in = m_readBuffer;
z.avail_in = static_cast<uint32>(readBufferSize);
z.next_out = m_zlibBuffer;
z.avail_out = m_frameSize;
int status = inflate(&z, Z_FINISH);
if(status != Z_STREAM_END || z.total_out != m_frameSize)
{
inflateEnd(&z);
throw std::runtime_error("Unable to decompress CSO frame using zlib.");
}
inflateEnd(&z);
// Our buffer now contains this frame.
m_zlibBufferFrame = frame;
}
uint64 CCsoImageStream::ReadBaseAt(uint64 pos, uint8* dest, uint64 bytes)
{
m_baseStream->Seek(pos, Framework::STREAM_SEEK_SET);
return m_baseStream->Read(dest, bytes);
}