mirror of
https://github.com/martravi/wiiqt.git
synced 2024-11-25 10:36:56 +01:00
c71ac45ab2
use case-insensitivity. seems all the nintendo tools are running on windows and in the system menu html, paths are case-insensitive. it doesnt seem like there is anywhere where they would need to be the other way * lz77: support for unpacking archives larger than 0xffffff bytes * nusdownloader: dont try to write files to cache if there is not one set
1428 lines
45 KiB
C++
1428 lines
45 KiB
C++
#include "u8.h"
|
|
#include "lz77.h"
|
|
#include "tools.h"
|
|
//#include "md5.h"
|
|
#include "ash.h"
|
|
|
|
#define U8_HEADER_ALIGNMENT 0x20 //wiibrew says 0x40, but wii.cs is using 0x20 and i havent heard of any bricks because of it
|
|
static quint32 swap24( quint32 i )
|
|
{
|
|
#if Q_BYTE_ORDER == Q_BIG_ENDIAN
|
|
return i & 0xffffff;
|
|
#else
|
|
int r = qFromBigEndian( i );
|
|
return r >> 8;
|
|
#endif
|
|
}
|
|
|
|
U8::U8( bool initialize, int type, const QStringList &names )
|
|
{
|
|
ok = false;
|
|
isLz77 = false;
|
|
wii_cs_error = false;
|
|
paths.clear();
|
|
nestedU8s.clear();
|
|
fst = NULL;
|
|
fstSize = 0;
|
|
headerType = type;
|
|
imetNames = names;
|
|
if( !initialize )
|
|
return;
|
|
|
|
//headerType = type;
|
|
//imetNames = names;
|
|
|
|
ok = CreateEmptyData();
|
|
}
|
|
|
|
bool U8::CreateEmptyData()
|
|
{
|
|
isLz77 = false;
|
|
nestedU8s.clear();
|
|
data = QByteArray( 0x20, '\0' );
|
|
|
|
//create a generic u8 with no real info in it. only a fst with a root node
|
|
QBuffer buf( &data );
|
|
if( !buf.open( QBuffer::WriteOnly ) )
|
|
{
|
|
qWarning() << "Can't create buffer";
|
|
data.clear();
|
|
return false;
|
|
}
|
|
buf.putChar( 'U' );
|
|
buf.putChar( '\xAA' );
|
|
buf.putChar( '8' );
|
|
buf.putChar( '\x2D' );
|
|
|
|
rootnode_offset = 0x00000020;
|
|
quint32 tmp = qFromBigEndian( rootnode_offset );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
|
|
fstSize = 0x0000000D;
|
|
tmp = qFromBigEndian( fstSize );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
|
|
data_offset = qFromBigEndian( 0x00000040 );
|
|
tmp = qFromBigEndian( data_offset );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
buf.close();
|
|
|
|
//create the new fst
|
|
QByteArray fstArray = QByteArray( 0x20, '\0' );
|
|
fst = (FEntry*)( fstArray.data() );
|
|
fst[ 0 ].Type = 1;
|
|
fst[ 0 ].FileLength = qFromBigEndian( 0x00000001 );
|
|
NameOff = 0x0C;
|
|
|
|
data.append( fstArray );
|
|
|
|
//point the fst pointer to the new data
|
|
fst = (FEntry*)( data.data() + rootnode_offset );
|
|
|
|
|
|
ok = true;
|
|
return true;
|
|
}
|
|
|
|
bool U8::RenameEntry( const QString &path, const QString &newName )
|
|
{
|
|
qDebug() << "U8::RenameEntry(" << path << "," << newName << ")";
|
|
if( !ok )
|
|
{
|
|
qWarning() << "U8::RenameEntry -> archive has no data";
|
|
return false;
|
|
}
|
|
if( newName.contains( "/" ) )
|
|
{
|
|
qWarning() << "U8::RenameEntry -> newName cannot contain \'/\'";
|
|
return false;
|
|
}
|
|
//check if this is a path to a file in a nested archive
|
|
QMap<QString, U8 >::iterator i = nestedU8s.begin();
|
|
while( i != nestedU8s.constEnd() )
|
|
{
|
|
if( path.startsWith( i.key() ) && path != i.key() )
|
|
{
|
|
QString subPath = path;
|
|
subPath.remove( 0, i.key().size() + 1 );//remove the path of the archive itself + the slash
|
|
|
|
bool ret = i.value().RenameEntry( subPath, newName );
|
|
if( !ret )
|
|
{
|
|
qWarning() << "U8::RenameEntry -> error replacing data in child U8 archive";
|
|
return false;
|
|
}
|
|
//qDebug() << "child entry updated, now replacing child archive in this one";
|
|
return ReplaceEntry( i.key(),i.value().GetData() );
|
|
//NOTE - after replacing the entry, "i" is no longer valid. keep that in mind when changing this code. its a bitch to track down this bug
|
|
}
|
|
++i;
|
|
}
|
|
//make sure the new filename doesnt already exist
|
|
QString parentPath = path;
|
|
while( parentPath.startsWith( "/") )//remove leading "/"s
|
|
parentPath.remove( 0, 1 );
|
|
while( parentPath.endsWith( "/") )//remove trailing "/"s
|
|
parentPath.resize( parentPath.size() - 1 );
|
|
|
|
int slash = parentPath.lastIndexOf( "/" );
|
|
if( slash != -1 )
|
|
{
|
|
parentPath.chop( ( parentPath.size() - slash ) );
|
|
parentPath += "/" + newName;
|
|
}
|
|
else
|
|
parentPath = newName;
|
|
|
|
if( FindEntry( parentPath ) != -1 )
|
|
{
|
|
qWarning() << "U8::RenameEntry ->" << parentPath << "already exists in the archive";
|
|
return false;
|
|
}
|
|
|
|
//find the entry to rename
|
|
int entryToRename = FindEntry( path );
|
|
if( entryToRename == -1 )
|
|
{
|
|
qWarning() << "U8::RenameEntry" << path << "doesn\'t exists in the archive";
|
|
qWarning() << "choices are" << paths;
|
|
return false;
|
|
}
|
|
|
|
quint32 newNameLen = newName.size();
|
|
quint32 oldNameLen = FstName( entryToRename ).size();
|
|
int difference = newNameLen - oldNameLen;
|
|
|
|
|
|
quint32 nFstSize = fstSize + difference;
|
|
int dataAdjustment = 0;
|
|
if( RU( nFstSize, U8_HEADER_ALIGNMENT ) < RU( fstSize, U8_HEADER_ALIGNMENT ) )
|
|
dataAdjustment = - RU( ( fstSize - nFstSize ), U8_HEADER_ALIGNMENT );
|
|
|
|
else if( RU( nFstSize, U8_HEADER_ALIGNMENT ) > RU( fstSize, U8_HEADER_ALIGNMENT ) )
|
|
dataAdjustment = RU( ( nFstSize - fstSize ), U8_HEADER_ALIGNMENT );
|
|
|
|
qDebug() << "old size:" << hex << oldNameLen\
|
|
<< "new size:" << hex << newNameLen\
|
|
<< "difference:" << hex << difference
|
|
<< "dataAdjustment:" << hex << dataAdjustment;
|
|
QByteArray nFstData( ( qFromBigEndian( fst[ 0 ].FileLength ) ) * 0xc, '\0' );
|
|
FEntry *nfst = (FEntry*)( nFstData.data() );
|
|
//make the new root entry
|
|
nfst[ 0 ] = fst[ 0 ];
|
|
|
|
|
|
|
|
//the new data payload ( just copy the existing one )
|
|
QByteArray nPayload = data.right( data.size() - data_offset );
|
|
QByteArray nNameTable;
|
|
nNameTable.append( '\0' );//add a null byte for the root name
|
|
|
|
quint32 cnt = qFromBigEndian( fst[ 0 ].FileLength );//the number of entries
|
|
|
|
|
|
for( quint32 i = 1; i < cnt; i++ )
|
|
{
|
|
FEntry *e = &fst[ i ];//old entry
|
|
FEntry *ne = &nfst[ i ];//new entry
|
|
|
|
ne->NameOffset = swap24( nNameTable.size() );//offset to the new entry's name in the string table
|
|
|
|
//add the name to the new name table
|
|
if( i == (quint32)entryToRename )
|
|
{
|
|
nNameTable.append( newName.toLatin1() );
|
|
}
|
|
else
|
|
nNameTable.append( FstName( e ).toLatin1() );
|
|
|
|
nNameTable.append( '\0' );
|
|
if( e->Type )//nothing special to change for directories except the name offset
|
|
{
|
|
ne->Type = 1;
|
|
ne->ParentOffset = e->ParentOffset;
|
|
ne->NextOffset = e->NextOffset;
|
|
}
|
|
else//only need to change the name offset & the data offset for files
|
|
{
|
|
ne->Type = 0;
|
|
ne->FileOffset = qFromBigEndian( qFromBigEndian( e->FileOffset ) + dataAdjustment );// + qFromBigEndian( dataAdjustment );
|
|
//qFromBigEndian( (quint32)( 0x20 + RU( U8_HEADER_ALIGNMENT, nFstSize ) + nPayload.size() ) );
|
|
ne->FileLength = e->FileLength;
|
|
qDebug() << "old offset" << hex << qFromBigEndian( e->FileOffset ) << "new offset" << hex << qFromBigEndian( e->FileOffset ) + dataAdjustment;
|
|
}
|
|
|
|
}
|
|
//now put all the parts together to make a new U8 archive
|
|
nFstData.append( nNameTable );
|
|
|
|
data = QByteArray( 0x20, '\0' );
|
|
QBuffer buf( &data );
|
|
if( !buf.open( QBuffer::WriteOnly ) )
|
|
{
|
|
qWarning() << "U8::AddEntry -> Can't create buffer";
|
|
return -1;
|
|
}
|
|
buf.putChar( 'U' );
|
|
buf.putChar( '\xAA' );
|
|
buf.putChar( '8' );
|
|
buf.putChar( '\x2D' );
|
|
|
|
rootnode_offset = 0x00000020;
|
|
quint32 t = qFromBigEndian( rootnode_offset );
|
|
buf.write( (const char*)&t, 4 );
|
|
|
|
fstSize = nFstData.size();
|
|
t = qFromBigEndian( fstSize );
|
|
buf.write( (const char*)&t, 4 );
|
|
|
|
data_offset = RU( ( 0x20 + fstSize ), U8_HEADER_ALIGNMENT );
|
|
t = qFromBigEndian( data_offset );
|
|
buf.write( (const char*)&t, 4 );
|
|
buf.close();
|
|
|
|
data.append( nFstData );//add the fst
|
|
int padding = data_offset - data.size();//pad to 'data_offset' after the fst
|
|
if( padding )
|
|
{
|
|
data.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
data.append( nPayload );//add the actual file data
|
|
|
|
padding = RU( data.size(), 0x20 ) - data.size();//pad the entire thing to 0x20 bytes TOTO: should probably already be done, and this step is not really necessary
|
|
if( padding )
|
|
{
|
|
data.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
|
|
|
|
|
|
CreateEntryList();
|
|
|
|
//hexdump( data, 0, 0x100 );
|
|
//hexdump( data );
|
|
return true;
|
|
}
|
|
|
|
bool U8::ReplaceEntry( const QString &path, const QByteArray &nba, bool autoCompress )
|
|
{
|
|
//qDebug() << "U8::ReplaceEntry(" << path << ")";
|
|
if( !ok )
|
|
{
|
|
qWarning() << "U8::ReplaceEntry -> archive has no data";
|
|
return false;
|
|
}
|
|
//check if this is a path to a file in a nested archive
|
|
QMap<QString, U8 >::iterator i = nestedU8s.begin();
|
|
while( i != nestedU8s.constEnd() )
|
|
{
|
|
if( path.startsWith( i.key() ) && path != i.key() )
|
|
{
|
|
QString subPath = path;
|
|
subPath.remove( 0, i.key().size() + 1 );//remove the path of the archive itself + the slash
|
|
//qDebug() << "replacing file" << subPath << "in child U8" << i.key();
|
|
|
|
U8 ch = i.value();
|
|
QString chPath = i.key();
|
|
//qDebug() << "replacing" << subPath << "in child archive";
|
|
bool ret = ch.ReplaceEntry( subPath, nba, autoCompress );
|
|
if( !ret )
|
|
{
|
|
qWarning() << "U8::ReplaceEntry -> error replacing data in child U8 archive";
|
|
return false;
|
|
}
|
|
//qDebug() << "child entry updated, now replacing child archive in this one";
|
|
return ReplaceEntry( chPath, ch.GetData() );
|
|
//NOTE - after replacing the entry, "i" is no longer valid. keep that in mind when changing this code. its a bitch to track down this bug
|
|
}
|
|
++i;
|
|
}
|
|
//find the entry to replace
|
|
int entryToReplace = FindEntry( path );
|
|
if( entryToReplace == -1 )
|
|
{
|
|
qWarning() << "U8::ReplaceEntry" << path << "doesn\'t exists in the archive";
|
|
qWarning() << "choices are" << paths;
|
|
return false;
|
|
}
|
|
if( qFromBigEndian( fst[ entryToReplace ].Type ) )
|
|
{
|
|
qWarning() << "U8::ReplaceEntry -> replacing a directory is not supported" << path;
|
|
return false;
|
|
}
|
|
|
|
|
|
/*qDebug() << "old size:" << hex << oldSizePadded\
|
|
<< "new size:" << hex << newSizePadded\
|
|
<< "difference:" << hex << difference;*/
|
|
QByteArray newData = nba;
|
|
if( autoCompress )
|
|
{
|
|
QByteArray oldData = data.mid( qFromBigEndian( fst[ entryToReplace ].FileOffset ), qFromBigEndian( fst[ entryToReplace ].FileLength ) );
|
|
bool oldCompressed = ( LZ77::GetLz77Offset( oldData ) > -1 || IsAshCompressed( oldData ) );
|
|
if( oldCompressed && LZ77::GetLz77Offset( newData ) == -1 && !IsAshCompressed( newData ) )
|
|
{
|
|
newData = LZ77::Compress( newData );
|
|
}
|
|
}
|
|
|
|
quint32 newSizePadded = RU( newData.size(), 0x20 );
|
|
quint32 oldSizePadded = RU( qFromBigEndian( fst[ entryToReplace ].FileLength ), 0x20 );
|
|
int difference = newSizePadded - oldSizePadded;
|
|
|
|
data.remove( qFromBigEndian( fst[ entryToReplace ].FileOffset ), oldSizePadded );
|
|
QByteArray newPaddedData = newData;
|
|
if( newSizePadded > (quint32)newPaddedData.size() )
|
|
{
|
|
newPaddedData.append( QByteArray( newSizePadded - newPaddedData.size(), '\0' ) );
|
|
}
|
|
data.insert( qFromBigEndian( fst[ entryToReplace ].FileOffset ), newPaddedData );
|
|
|
|
if( newSizePadded == oldSizePadded )//the sized match, so nothing else to change
|
|
return true;
|
|
|
|
//point the fst to the new data
|
|
fst = (FEntry*)( data.data() + rootnode_offset );
|
|
|
|
//edit the changed size
|
|
FEntry *e = &fst[ entryToReplace ];
|
|
e->FileLength = qFromBigEndian( newData.size() );
|
|
//hexdump( (const void*)e, sizeof( FEntry ) );
|
|
|
|
//edit all the file offsets after the changed item
|
|
quint32 cnt = qFromBigEndian( fst[ 0 ].FileLength );
|
|
for( quint32 i = entryToReplace + 1; i < cnt; i++ )
|
|
{
|
|
FEntry *e = &fst[ i ];//old entry
|
|
if( e->Type )//nothing changes for directories
|
|
continue;
|
|
|
|
//qDebug() << "changed" << FstName( e ) << "offset from" << hex << qFromBigEndian( fst[ i ].FileOffset ) << "to" << qFromBigEndian( fst[ i ].FileOffset ) + difference;
|
|
e->FileOffset = qFromBigEndian( qFromBigEndian( fst[ i ].FileOffset ) + difference );
|
|
}
|
|
CreateEntryList();
|
|
|
|
//hexdump( data, 0, 0x100 );
|
|
//hexdump( data );
|
|
return true;
|
|
|
|
}
|
|
|
|
bool U8::RemoveEntry( const QString &path )
|
|
{
|
|
//qDebug() << "U8::RemoveEntry(" << path << ")";
|
|
if( !ok )
|
|
{
|
|
qWarning() << "U8::RemoveEntry -> archive has no data";
|
|
return false;
|
|
}
|
|
//check if this is a path to a file in a nested archive
|
|
QMap<QString, U8 >::iterator i = nestedU8s.begin();
|
|
while( i != nestedU8s.constEnd() )
|
|
{
|
|
if( path.startsWith( i.key() ) && path != i.key() )
|
|
{
|
|
QString subPath = path;
|
|
subPath.remove( 0, i.key().size() + 1 );//remove the path of the archive itself + the slash
|
|
|
|
U8 ch = i.value();
|
|
QString chPath = i.key();
|
|
//qDebug() << "deleting" << subPath << "in child archive";
|
|
|
|
bool ret = ch.RemoveEntry( subPath );
|
|
if( !ret )
|
|
{
|
|
qWarning() << "U8::RemoveEntry -> error replacing data in child U8 archive";
|
|
return false;
|
|
}
|
|
return ReplaceEntry( chPath, ch.GetData() );//insert the new nested archive in this one
|
|
//NOTE - after replacing the entry, "i" is no longer valid. keep that in mind when changing this code. its a bitch to track down this bug
|
|
}
|
|
++i;
|
|
}
|
|
//find the entry to delete
|
|
int entryToDelete = FindEntry( path );
|
|
if( entryToDelete == -1 )
|
|
{
|
|
qWarning() << "U8::RemoveEntry" << path << "doesn\'t exists in the archive";
|
|
return false;
|
|
}
|
|
|
|
//create a list of all the directory indexes that my have their size changed
|
|
QList<int> parents;
|
|
int parent;
|
|
quint32 numDeletedEntries;
|
|
quint32 fstLenToDelete;
|
|
if( fst[ entryToDelete ].Type )//deleting a directory
|
|
{
|
|
parent = qFromBigEndian( fst[ entryToDelete ].ParentOffset );
|
|
while( parent )
|
|
{
|
|
parents << parent;
|
|
parent = qFromBigEndian( fst[ parent ].ParentOffset );
|
|
}
|
|
numDeletedEntries = qFromBigEndian( fst[ entryToDelete ].NextOffset ) - entryToDelete;
|
|
fstLenToDelete = 0;
|
|
for( quint32 i = entryToDelete; i < qFromBigEndian( fst[ entryToDelete ].NextOffset ); i ++ )
|
|
{
|
|
fstLenToDelete += FstName( i ).size() + 1 + 0xc;
|
|
}
|
|
|
|
}
|
|
else//deleting a file
|
|
{
|
|
parent = entryToDelete - 1;
|
|
while( parent )
|
|
{
|
|
if( fst[ parent ].Type && qFromBigEndian( fst[ parent ].NextOffset ) > (quint32)entryToDelete )
|
|
{
|
|
parents << parent;
|
|
parent = qFromBigEndian( fst[ parent ].ParentOffset );
|
|
}
|
|
else
|
|
parent--;
|
|
}
|
|
numDeletedEntries = 1;
|
|
fstLenToDelete = FstName( entryToDelete ).size() + 1 + 0xc;
|
|
|
|
}
|
|
|
|
|
|
//qDebug() << "will delete" << numDeletedEntries << "entries";
|
|
|
|
quint32 nFstSize = fstSize - fstLenToDelete;//old fst size - 0xc bytes for each deleted entry and the length of its name + 1 null byte
|
|
//qDebug() << "nFstSize" << nFstSize;
|
|
QByteArray nFstData( ( qFromBigEndian( fst[ 0 ].FileLength ) - numDeletedEntries ) * 0xc, '\0' );
|
|
FEntry *nfst = (FEntry*)( nFstData.data() );
|
|
|
|
//the new data payload ( all the files )
|
|
QByteArray nPayload;
|
|
QByteArray nNameTable;
|
|
nNameTable.append( '\0' );//add a null byte for the root name
|
|
|
|
quint32 cnt = qFromBigEndian( fst[ 0 ].FileLength );//the number of entries + 1 for the new one
|
|
|
|
//make the new root entry
|
|
nfst[ 0 ] = fst[ 0 ];
|
|
nfst[ 0 ].FileLength = qFromBigEndian( cnt - numDeletedEntries );
|
|
|
|
QList<int> movedDirs;//keep a list of the directories that are shifted back so their children can be shifted too
|
|
for( quint32 i = 1; i < cnt; i++ )
|
|
{
|
|
if( i >= (quint32)entryToDelete && i < (quint32)( entryToDelete + numDeletedEntries ) )//skip deleted entries
|
|
continue;
|
|
|
|
//copy old entries and adjust as needed
|
|
quint8 adj = i < (quint32)entryToDelete ? 0 : numDeletedEntries;
|
|
quint32 ni = i - adj;
|
|
|
|
//qDebug() << "keeping" << FstName( i ) << "in the new archive ( moved from" << hex << i << "to" << hex << ni << ")";
|
|
//if( parents.contains( i ) )
|
|
//qDebug() << "\tthis is a parent of the deleted item";
|
|
|
|
FEntry *e = &fst[ i ];//old entry
|
|
FEntry *ne = &nfst[ ni ];//new entry
|
|
|
|
nfst[ ni ].NameOffset = swap24( nNameTable.size() );//offset to the new entry's name in the string table
|
|
|
|
//add the name to the new name table
|
|
nNameTable.append( FstName( e ).toLatin1() );
|
|
nNameTable.append( '\0' );
|
|
|
|
|
|
if( e->Type )//directory
|
|
{
|
|
ne->Type = 1;
|
|
if( !adj )//directories before the new entry
|
|
{
|
|
ne->ParentOffset = e->ParentOffset;
|
|
ne->NextOffset = qFromBigEndian( qFromBigEndian( e->NextOffset ) \
|
|
- ( parents.contains( i ) ? numDeletedEntries : 0 ) );
|
|
}
|
|
else//directories after the new entry
|
|
{
|
|
ne->ParentOffset = qFromBigEndian( qFromBigEndian( e->ParentOffset ) \
|
|
- ( movedDirs.contains( qFromBigEndian( e->ParentOffset ) ) ? numDeletedEntries : 0 ) );
|
|
ne->NextOffset = qFromBigEndian( qFromBigEndian( e->NextOffset ) - numDeletedEntries );
|
|
|
|
movedDirs << i;
|
|
|
|
//qDebug() << "e.parent:" << hex << qFromBigEndian( e->ParentOffset ) << "movedDirs:" << movedDirs;
|
|
//hexdump( (const void*)ne, sizeof( FEntry) );
|
|
}
|
|
}
|
|
else//file
|
|
{
|
|
ne->Type = 0;
|
|
ne->FileOffset = \
|
|
qFromBigEndian( (quint32)( 0x20 + RU( nFstSize, U8_HEADER_ALIGNMENT ) + nPayload.size() ) );
|
|
ne->FileLength = e->FileLength;
|
|
nPayload.append( data.mid( qFromBigEndian( e->FileOffset ), qFromBigEndian( e->FileLength ) ) );
|
|
int padding = RU( nPayload.size(), 0x20 ) - nPayload.size();//pad to 0x20 bytes between files
|
|
if( padding )
|
|
{
|
|
nPayload.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
//qDebug() << "writing fileOffset of" << hex << ni << hex << (quint32)( 0x20 + RU( U8_HEADER_ALIGNMENT, nFstSize ) + nPayload.size() );
|
|
}
|
|
//hexdump( (const void*)ne, sizeof( FEntry) );
|
|
|
|
}
|
|
//hexdump( nFstData );
|
|
|
|
//now put all the parts together to make a new U8 archive
|
|
nFstData.append( nNameTable );
|
|
|
|
data = QByteArray( 0x20, '\0' );
|
|
QBuffer buf( &data );
|
|
if( !buf.open( QBuffer::WriteOnly ) )
|
|
{
|
|
qWarning() << "U8::AddEntry -> Can't create buffer";
|
|
return -1;
|
|
}
|
|
buf.putChar( 'U' );
|
|
buf.putChar( '\xAA' );
|
|
buf.putChar( '8' );
|
|
buf.putChar( '\x2D' );
|
|
|
|
rootnode_offset = 0x00000020;
|
|
quint32 t = qFromBigEndian( rootnode_offset );
|
|
buf.write( (const char*)&t, 4 );
|
|
|
|
fstSize = nFstData.size();
|
|
t = qFromBigEndian( fstSize );
|
|
buf.write( (const char*)&t, 4 );
|
|
|
|
data_offset = RU( ( 0x20 + fstSize ), U8_HEADER_ALIGNMENT );
|
|
t = qFromBigEndian( data_offset );
|
|
buf.write( (const char*)&t, 4 );
|
|
buf.close();
|
|
|
|
data.append( nFstData );//add the fst
|
|
int padding = data_offset - data.size();//pad to 'data_offset' after the fst
|
|
if( padding )
|
|
{
|
|
data.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
data.append( nPayload );//add the actual file data
|
|
|
|
padding = RU( data.size(), 0x20 ) - data.size();//pad the entire thing to 0x20 bytes TOTO: should probably already be done, and this step is not really necessary
|
|
if( padding )
|
|
{
|
|
data.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
|
|
//make sure the pirvate variables are correct
|
|
fst = (FEntry*)( data.data() + rootnode_offset );
|
|
NameOff = 0x0C * qFromBigEndian( fst[ 0 ].FileLength );
|
|
|
|
CreateEntryList();
|
|
|
|
//hexdump( data );
|
|
//qDebug() << "dataSize after removal:" << hex << data.size();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
int U8::AddEntry( const QString &path, int type, const QByteArray &newData )
|
|
{
|
|
//qDebug() << "U8::AddEntry(" << path << "," << type << "," << hex << newData.size() << ")";
|
|
//make sure there is actually data to manipulate
|
|
if( !ok && !CreateEmptyData() )
|
|
{
|
|
return -1;
|
|
}
|
|
//check if this is a path to a file in a nested archive
|
|
QMap<QString, U8 >::iterator i = nestedU8s.begin();
|
|
while( i != nestedU8s.constEnd() )
|
|
{
|
|
if( path.startsWith( i.key() ) && path != i.key() )
|
|
{
|
|
QString subPath = path;
|
|
//make a copy of the string, as "i" will become invalid on replace entry, and create a crash that is a bitch to track down
|
|
QString thisPath = i.key();
|
|
U8 chVal = i.value();
|
|
//qDebug() << "adding entry" << subPath << "in child U8" << thisPath;
|
|
subPath.remove( 0, thisPath.size() + 1 );//remove the path of the archive itself + the slash
|
|
bool ret = chVal.AddEntry( subPath, type, newData );
|
|
if( !ret )
|
|
{
|
|
qWarning() << "U8::AddEntry -> error replacing data in child U8 archive";
|
|
return false;
|
|
}
|
|
|
|
if( !ReplaceEntry( thisPath, chVal.GetData() ) )//insert the new nested archive in this one
|
|
return -1;
|
|
|
|
//qDebug() << "done replacing the child entry in this archive, finding the index to return (" << thisPath << ")";
|
|
return FindEntry( thisPath );//just return the index of the nested archive
|
|
}
|
|
++i;
|
|
}
|
|
|
|
//make sure this path doesnt already exist
|
|
if( FindEntry( path ) != -1 )
|
|
{
|
|
qWarning() << "U8::AddEntry" << path << "already exists in the archive";
|
|
return -1;
|
|
}
|
|
//find the parent for this new entry
|
|
QString parentPath = path;
|
|
while( parentPath.startsWith( "/") )//remove leading "/"s
|
|
parentPath.remove( 0, 1 );
|
|
while( parentPath.endsWith( "/") )//remove trailing "/"s
|
|
parentPath.resize( parentPath.size() - 1 );
|
|
|
|
int parent = 0;
|
|
int slash = parentPath.lastIndexOf( "/" );
|
|
if( slash != -1 )
|
|
{
|
|
//parentPath.resize( ( parentPath.size() - slash ) - 2 );
|
|
parentPath.chop( ( parentPath.size() - slash ) );
|
|
parent = FindEntry( parentPath );
|
|
if( parent == -1 )//need to create subfolders for this fucker
|
|
{
|
|
parent = AddEntry( parentPath, 1 );
|
|
if( parent == -1 )
|
|
{
|
|
qWarning() << "U8::AddEntry -> could not create subfolders for" << path;
|
|
return -1;
|
|
}
|
|
}
|
|
if( !fst[ parent ].Type )//parent maps to a file, not folder
|
|
{
|
|
qWarning() << "U8::AddEntry -> can't add child entry to a file";
|
|
return -1;
|
|
}
|
|
}
|
|
//qDebug() << "adding entry to" << FstName( parent );
|
|
|
|
//find the index to insert the new path
|
|
int newEntry = qFromBigEndian( fst[ parent ].NextOffset );
|
|
|
|
//create a list of all the directory indexes that my have their size changed to hold the new entry
|
|
QList<int> parents;
|
|
while( parentPath.contains( "/" ) )
|
|
{
|
|
parents << FindEntry( parentPath );
|
|
int slash = parentPath.lastIndexOf( "/" );
|
|
parentPath.chop( ( parentPath.size() - slash ) );
|
|
}
|
|
parents << FindEntry( parentPath );
|
|
//qDebug() << "parents" << parents;
|
|
|
|
//create the new data
|
|
|
|
//the new fst
|
|
QString name = path;//get the actual name of this entry
|
|
int sl = name.lastIndexOf( "/" );
|
|
if( sl != -1 )
|
|
{
|
|
name.remove( 0, sl + 1 );
|
|
}
|
|
|
|
quint32 nFstSize = fstSize + 0x0C + name.size() + 1;//old fst size + 0xc bytes for the new entry + room for the new name in the string table
|
|
QByteArray nFstData( ( qFromBigEndian( fst[ 0 ].FileLength ) + 1 ) * 0xc, '\0' );
|
|
FEntry *nfst = (FEntry*)( nFstData.data() );
|
|
|
|
//the new data payload ( all the files )
|
|
QByteArray nPayload;
|
|
QByteArray nNameTable;
|
|
nNameTable.append( '\0' );//add a null byte for the root name
|
|
|
|
quint32 cnt = qFromBigEndian( fst[ 0 ].FileLength ) + 1;//the number of entries + 1 for the new one
|
|
|
|
//make the new root entry
|
|
nfst[ 0 ] = fst[ 0 ];
|
|
nfst[ 0 ].FileLength = qFromBigEndian( cnt );
|
|
|
|
QList<int> movedDirs;//keep a list of the directories that are shifted back so their children can be shifted too
|
|
for( quint32 i = 1; i < cnt; i++ )
|
|
{
|
|
nfst[ i ].NameOffset = swap24( nNameTable.size() );//offset to the new entry's name in the string table
|
|
if( i == (quint32)newEntry )//add the new entry
|
|
{
|
|
FEntry *ne = &nfst[ i ];
|
|
if( type )
|
|
{
|
|
ne->Type = 1;
|
|
ne->ParentOffset = qFromBigEndian( parent );
|
|
ne->NextOffset = qFromBigEndian( i + 1 );
|
|
}
|
|
else
|
|
{
|
|
ne->Type = 0;
|
|
ne->FileLength = qFromBigEndian( (quint32)newData.size() );
|
|
ne->FileOffset =\
|
|
qFromBigEndian( (quint32)( 0x20 + RU( nFstSize, U8_HEADER_ALIGNMENT ) + nPayload.size() ) );
|
|
nPayload.append( newData );
|
|
int padding = RU( nPayload.size(), 0x20 ) - nPayload.size();//pad to 0x20 bytes between files
|
|
if( padding )
|
|
{
|
|
nPayload.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
}
|
|
nNameTable.append( name.toLatin1() );//add this entry's name to the table
|
|
nNameTable.append( '\0' );
|
|
continue;
|
|
}
|
|
|
|
//copy old entries and adjust as needed
|
|
quint8 adj = i < (quint32)newEntry ? 0 : 1;
|
|
quint32 ni = i - adj;
|
|
|
|
FEntry *e = &fst[ ni ];//old entry
|
|
FEntry *ne = &nfst[ i ];//new entry
|
|
|
|
//add the name to the new name table
|
|
nNameTable.append( FstName( e ).toLatin1() );
|
|
nNameTable.append( '\0' );
|
|
if( e->Type )
|
|
{
|
|
ne->Type = 1;
|
|
if( !adj )//directories before the new entry
|
|
{
|
|
ne->ParentOffset = e->ParentOffset;
|
|
ne->NextOffset = qFromBigEndian( qFromBigEndian( e->NextOffset ) \
|
|
+ ( parents.contains( i ) ? 1 : 0 ) );
|
|
}
|
|
else//directories after the new entry
|
|
{
|
|
movedDirs << ni;
|
|
ne->ParentOffset = qFromBigEndian( qFromBigEndian( e->ParentOffset ) \
|
|
+ ( movedDirs.contains( qFromBigEndian( e->ParentOffset ) ) ? 1 : 0 ) );
|
|
ne->NextOffset = qFromBigEndian( qFromBigEndian( e->NextOffset ) + 1 );
|
|
}
|
|
}
|
|
else//all file entries
|
|
{
|
|
ne->Type = 0;
|
|
ne->FileOffset = \
|
|
qFromBigEndian( (quint32)( 0x20 + RU( nFstSize, U8_HEADER_ALIGNMENT ) + nPayload.size() ) );
|
|
ne->FileLength = e->FileLength;
|
|
nPayload.append( data.mid( qFromBigEndian( e->FileOffset ), qFromBigEndian( e->FileLength ) ) );
|
|
int padding = RU( nPayload.size(), 0x20 ) - nPayload.size();//pad to 0x20 bytes between files
|
|
if( padding )
|
|
{
|
|
nPayload.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
}
|
|
|
|
}
|
|
//now put all the parts together to make a new U8 archive
|
|
nFstData.append( nNameTable );
|
|
|
|
data = QByteArray( 0x20, '\0' );
|
|
QBuffer buf( &data );
|
|
if( !buf.open( QBuffer::WriteOnly ) )
|
|
{
|
|
qWarning() << "U8::AddEntry -> Can't create buffer";
|
|
return -1;
|
|
}
|
|
buf.putChar( 'U' );
|
|
buf.putChar( '\xAA' );
|
|
buf.putChar( '8' );
|
|
buf.putChar( '\x2D' );
|
|
|
|
rootnode_offset = 0x00000020;
|
|
quint32 t = qFromBigEndian( rootnode_offset );
|
|
buf.write( (const char*)&t, 4 );
|
|
|
|
fstSize = nFstData.size();
|
|
t = qFromBigEndian( fstSize );
|
|
buf.write( (const char*)&t, 4 );
|
|
|
|
data_offset = RU( ( 0x20 + fstSize ), U8_HEADER_ALIGNMENT );
|
|
t = qFromBigEndian( data_offset );
|
|
buf.write( (const char*)&t, 4 );
|
|
buf.close();
|
|
|
|
data.append( nFstData );//add the fst
|
|
int padding = data_offset - data.size();//pad to 'data_offset' after the fst
|
|
if( padding )
|
|
{
|
|
data.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
data.append( nPayload );//add the actual file data
|
|
|
|
padding = RU( data.size(), 0x20 ) - data.size();//pad the entire thing to 0x20 bytes TOTO: should probably already be done, and this step is not really necessary
|
|
if( padding )
|
|
{
|
|
data.append( QByteArray( padding, '\0' ) );
|
|
}
|
|
|
|
//make sure the pirvate variables are correct
|
|
fst = (FEntry*)( data.data() + rootnode_offset );
|
|
NameOff = 0x0C * qFromBigEndian( fst[ 0 ].FileLength );
|
|
//qDebug() << "done adding the entry";
|
|
|
|
CreateEntryList();
|
|
|
|
//hexdump( data );
|
|
return newEntry;
|
|
}
|
|
|
|
void U8::CreateEntryList()
|
|
{
|
|
//qDebug() << "U8::CreateEntryList";
|
|
paths.clear();
|
|
nestedU8s.clear();
|
|
fst = (FEntry*)( data.data() + rootnode_offset );
|
|
quint32 cnt = qFromBigEndian( fst[ 0 ].FileLength );
|
|
if( ( cnt * 0x0d ) + rootnode_offset > (quint32)data.size() )
|
|
{
|
|
qWarning() << "U8::CreateEntryList -> invalid archive";
|
|
ok = false;
|
|
data.clear();
|
|
paths.clear();
|
|
fst = NULL;
|
|
return;
|
|
}
|
|
NameOff = cnt * 0x0C;
|
|
bool fixWarn = false;//ony print the warning 1 time
|
|
|
|
//qDebug() << "cnt" << hex << cnt;
|
|
for( quint32 i = 1; i < cnt; ++i )//this is not the most effecient way to do things, but it seems to work ok and these archives are small enough that it happens fast anyways
|
|
{
|
|
//start at the beginning of the fst and enter every directory whos "nextoffset" is greater than this index,
|
|
//adding the names along the way and hopefully finding "i"
|
|
QString path;
|
|
quint32 current = 1;
|
|
quint32 folder = 0;
|
|
while( 1 )
|
|
{
|
|
if( !current )
|
|
{
|
|
qWarning() << "U8::CreateEntryList -> error parsing the archive";
|
|
break;
|
|
}
|
|
if( current == i )
|
|
{
|
|
path += FstName( i );
|
|
if( fst[ current ].Type )
|
|
{
|
|
//sanity check: make sure the parent offset is correct
|
|
if( folder != qFromBigEndian( fst[ current ].ParentOffset ) )
|
|
{
|
|
qWarning() << "U8::CreateEntryList -> error parsing the archive - recursion mismatch in"
|
|
<< path << "expected:" << hex << folder << "got:" << hex << qFromBigEndian( fst[ current ].ParentOffset )\
|
|
<< "(" << FstName( qFromBigEndian( fst[ current ].ParentOffset ) ) << ")";
|
|
|
|
//some tools use "recursion" instead of "parent offset".
|
|
//it just coincidentally works for archives such as the opening.bnr and .app since they dont have many folders to mess up
|
|
if( qFromBigEndian( fst[ current ].ParentOffset ) == (quint32)path.count( "/" ) )
|
|
{
|
|
if( !fixWarn )
|
|
{
|
|
qWarning() << "This archive was made by a broken tool such as U8Mii. I'm trying to fix it, but I can't make any promises";
|
|
fixWarn = true;
|
|
wii_cs_error = true;
|
|
}
|
|
#ifndef U8_DONT_FIX_RECURSION_ERRORS
|
|
fst[ current ].ParentOffset = qFromBigEndian( folder );
|
|
#endif
|
|
}
|
|
//#ifndef U8_DONT_FIX_RECURSION_ERRORS
|
|
// fst[ current ].ParentOffset = qFromBigEndian( folder );
|
|
//#endif
|
|
}
|
|
path += '/';
|
|
paths << path;
|
|
}
|
|
else
|
|
{
|
|
//add this file to the list of entries
|
|
paths << path;
|
|
|
|
//check if this file is really a U8 archive, and add it to the list of children
|
|
QByteArray chData = data.mid( qFromBigEndian( fst[ current ].FileOffset ), qFromBigEndian( fst[ current ].FileLength ) );
|
|
if( IsU8( chData ) )
|
|
{
|
|
U8 child( chData );
|
|
if( child.IsOK() )
|
|
{
|
|
nestedU8s.insert( path, child );
|
|
foreach( const QString &chPath, child.Entries() )
|
|
{
|
|
QString newPath = path + "/" + chPath;
|
|
paths << newPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if( fst[ current ].Type && i < qFromBigEndian( fst[ current ].NextOffset ) )
|
|
{
|
|
path += FstName( current ) + "/";
|
|
folder = current;
|
|
current++;
|
|
continue;
|
|
|
|
}
|
|
current = NextEntryInFolder( current, folder );
|
|
}
|
|
//paths << path;
|
|
}
|
|
//qDebug() << "done";
|
|
//qDebug() << "paths" << paths;
|
|
}
|
|
|
|
U8::U8( const QByteArray &ba )
|
|
{
|
|
wii_cs_error = false;
|
|
//qDebug() << "U8::U8 dataSize:" << hex << ba.size();
|
|
Load( ba );
|
|
}
|
|
|
|
void U8::Load( const QByteArray &ba )
|
|
{
|
|
ok = false;
|
|
wii_cs_error = false;
|
|
isLz77 = false;
|
|
headerType = U8_Hdr_none;
|
|
paths.clear();
|
|
imetNames.clear();
|
|
/*if( ba.size() < 0x80 )
|
|
{
|
|
//qWarning() << "U8::Load:" << hex << ba.size();
|
|
//qWarning() << "U8::Load -> where is the rest of the data?";
|
|
return;
|
|
}*/
|
|
data = ba;
|
|
if( IsAshCompressed( data ) )//decrypt ASH0 files
|
|
data = DecryptAsh( data );
|
|
|
|
quint32 tmp;
|
|
int off = LZ77::GetLz77Offset( data );
|
|
int off2 = GetU8Offset( data );
|
|
if( off != -1 && ( off2 == -1 || ( off2 != -1 && off < off2 ) ) )
|
|
{
|
|
isLz77 = true;
|
|
data = LZ77::Decompress( data );
|
|
off2 = GetU8Offset( data );
|
|
}
|
|
|
|
if( off2 == -1 )
|
|
{
|
|
qDebug() << "This isnt a U8";
|
|
return;
|
|
}
|
|
|
|
data = data.mid( off2 );
|
|
|
|
QBuffer buf( &data );
|
|
if( !buf.open( QBuffer::ReadOnly ) )
|
|
{
|
|
qWarning() << "Can't create buffer";
|
|
return;
|
|
}
|
|
|
|
buf.seek( 4 );
|
|
buf.read( ( char*)&tmp, 4 );
|
|
rootnode_offset = qFromBigEndian( tmp );
|
|
if( rootnode_offset != 0x20 )
|
|
{
|
|
qWarning() << "rootnodeOffset" << hex << rootnode_offset;
|
|
qWarning() << hex << data.size();
|
|
hexdump( data );
|
|
}
|
|
|
|
buf.read( ( char*)&tmp, 4 );
|
|
fstSize = qFromBigEndian( tmp );
|
|
|
|
buf.read( ( char*)&tmp, 4 );
|
|
data_offset = qFromBigEndian( tmp );
|
|
|
|
buf.close();
|
|
|
|
CreateEntryList();
|
|
|
|
|
|
ReadHeader( ba );
|
|
ok = true;
|
|
//qDebug() << "loaded" << paths;
|
|
}
|
|
|
|
bool U8::IsOK()
|
|
{
|
|
return ok;
|
|
}
|
|
|
|
QString U8::FstName( const FEntry *entry )
|
|
{
|
|
//qDebug() << "U8::FstName";
|
|
if( entry == &fst[ 0 ] )
|
|
return QString();
|
|
|
|
int nameStart = swap24( entry->NameOffset ) + NameOff + rootnode_offset;
|
|
return data.mid( nameStart, nameStart + 0x100 );
|
|
}
|
|
|
|
QString U8::FstName( quint32 i )
|
|
{
|
|
if( i > fst[ 0 ].FileLength )
|
|
{
|
|
qWarning() << "U8::FstName -> index is out of range";
|
|
return QString();
|
|
}
|
|
return FstName( &fst[ i ] );
|
|
}
|
|
|
|
quint32 U8::NextEntryInFolder( quint32 current, quint32 directory )
|
|
{
|
|
//qDebug() << "U8::NextEntryInFolder(" << hex << current << "," << hex << directory << ")";
|
|
quint32 next = ( fst[ current ].Type ? qFromBigEndian( fst[ current ].FileLength ) : current + 1 );
|
|
//qDebug() << "next" << next << "len" << hex << qFromBigEndian( fst[ directory ].FileLength );
|
|
if( next < qFromBigEndian( fst[ directory ].FileLength ) )
|
|
return next;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int U8::FindEntry( const QString &str, int d )
|
|
{
|
|
//qDebug() << "U8::FindEntry(" << str << "," << d << ")";
|
|
if( str.isEmpty() )
|
|
return 0;
|
|
|
|
if( str.startsWith( "/" ) )
|
|
{
|
|
QString r = str;
|
|
r.remove( 0, 1 );
|
|
return FindEntry( r, d );
|
|
}
|
|
|
|
if( !fst[ d ].Type )
|
|
{
|
|
qDebug() << "ERROR!!" << FstName( &fst[ d ] ) << "is not a directory";
|
|
return -1;
|
|
}
|
|
|
|
int next = d + 1;
|
|
FEntry *entry = &fst[ next ];
|
|
|
|
while( next )
|
|
{
|
|
QString item;
|
|
int sl = str.indexOf( "/" );
|
|
if( sl > -1 )
|
|
item = str.left( sl );
|
|
else
|
|
item = str;
|
|
|
|
if( !FstName( entry ).compare( item, Qt::CaseInsensitive ) )
|
|
//if( FstName( entry ) == item )
|
|
{
|
|
if( item == str || item + "/" == str )//this is the item we are looking for
|
|
{
|
|
return next;
|
|
}
|
|
//this item is a parent folder of one we are looking for
|
|
return FindEntry( str.right( ( str.size() - sl ) - 1 ), next );
|
|
}
|
|
//find the next entry in this folder
|
|
next = NextEntryInFolder( next, d );
|
|
entry = &fst[ next ];
|
|
}
|
|
|
|
//no entry with the given path was found
|
|
return -1;
|
|
}
|
|
|
|
const QByteArray U8::GetData( const QString &str, bool onlyPayload )
|
|
{
|
|
//qDebug() << "U8::GetData(" << str << ")";
|
|
if( str.isEmpty() )//give the data for this whole archive
|
|
{
|
|
if( onlyPayload )
|
|
return data;
|
|
|
|
//qDebug() << "U8::GetData -> returning all the data for this archive. headerType:" << headerType;
|
|
|
|
QByteArray ret = data;
|
|
|
|
switch( headerType )
|
|
{
|
|
case U8_Hdr_IMET_app:
|
|
case U8_Hdr_IMET_bnr:
|
|
ret = AddIMET( headerType );//data is lz77 compressed in this function if it needs to be
|
|
break;
|
|
case U8_Hdr_IMD5:
|
|
{
|
|
if( isLz77 )
|
|
ret = LZ77::Compress( ret );
|
|
|
|
ret = AddIMD5( ret );
|
|
|
|
//hexdump( ret, 0, 0x40 );
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
//check if this is a path to a file in a nested archive
|
|
QMap<QString, U8 >::iterator i = nestedU8s.begin();
|
|
while( i != nestedU8s.constEnd() )
|
|
{
|
|
if( str.startsWith( i.key() ) && str != i.key() )
|
|
{
|
|
QString subPath = str;
|
|
subPath.remove( 0, i.key().size() + 1 );//remove the path of the archive itself + the slash
|
|
return i.value().GetData( subPath, onlyPayload );
|
|
}
|
|
++i;
|
|
}
|
|
int index = FindEntry( str );
|
|
if( index < 0 )
|
|
{
|
|
qWarning() << "U8::GetData" << str << "was not found in the archive";
|
|
return QByteArray();
|
|
}
|
|
if( fst[ index ].Type )
|
|
{
|
|
qWarning() << "U8::GetData" << str << "is a directory";
|
|
return QByteArray();
|
|
}
|
|
QByteArray ret = data.mid( qFromBigEndian( fst[ index ].FileOffset ), qFromBigEndian( fst[ index ].FileLength ) );
|
|
//hexdump( ret, 0, 0x40 );
|
|
if( onlyPayload )
|
|
{
|
|
if( LZ77::GetLz77Offset( ret ) != -1 )
|
|
ret = LZ77::Decompress( ret );
|
|
|
|
else if( IsAshCompressed( ret ) )
|
|
ret = DecryptAsh( ret );
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
quint32 U8::GetSize( const QString &str )
|
|
{
|
|
if( str.isEmpty() )
|
|
{
|
|
return data.size();
|
|
}
|
|
int index = FindEntry( str );
|
|
if( index < 0 )
|
|
{
|
|
qWarning() << "U8::GetSize" << str << "was not found in the archive";
|
|
return 0;
|
|
}
|
|
if( fst[ index ].Type )
|
|
{
|
|
qWarning() << "U8::GetSize" << str << "is a directory";
|
|
return 0;
|
|
}
|
|
return qFromBigEndian( fst[ index ].FileLength );
|
|
}
|
|
|
|
const QStringList U8::Entries()
|
|
{
|
|
return paths;
|
|
}
|
|
|
|
int U8::GetU8Offset( const QByteArray &ba )
|
|
{
|
|
QByteArray start = ba.left( 5000 );
|
|
return start.indexOf( "U\xAA\x38\x2d" );
|
|
}
|
|
|
|
bool U8::IsU8( const QByteArray &ba )
|
|
{
|
|
QByteArray data = ba;
|
|
if( IsAshCompressed( data ) )//decrypt ASH0 files
|
|
data = DecryptAsh( data );
|
|
|
|
int off = LZ77::GetLz77Offset( data );//decrypt LZ77
|
|
if( off != -1 )
|
|
data = LZ77::Decompress( data );
|
|
|
|
QByteArray start = data.left( 5000 );
|
|
return start.indexOf( "U\xAA\x38\x2d" ) != -1;
|
|
}
|
|
|
|
#define IMET_MAX_NAME_LEN 0x2a
|
|
typedef struct
|
|
{
|
|
quint32 sig; // "IMET"
|
|
quint32 unk1;
|
|
quint32 unk2;
|
|
quint32 filesizes[ 3 ];
|
|
quint32 unk3;
|
|
quint16 names[ 10 ][ IMET_MAX_NAME_LEN ];
|
|
quint8 zeroes2[ 0x24c ];
|
|
quint8 md5[ 0x10 ];
|
|
} IMET;
|
|
|
|
void U8::ReadHeader( const QByteArray &ba )
|
|
{
|
|
//qDebug() << "U8::ReadHeader(" << hex << ba.size() << ")";
|
|
//hexdump( ba );
|
|
headerType = U8_Hdr_none;
|
|
imetNames.clear();
|
|
if( ba.startsWith( "IMD5" ) )//dont bother to read any more data
|
|
{
|
|
//qDebug() << "IMD5 header";
|
|
headerType = U8_Hdr_IMD5;
|
|
return;
|
|
}
|
|
|
|
QByteArray start = ba.left( sizeof( IMET ) + 0x80 );
|
|
QBuffer buf( &start );
|
|
if( !buf.open( QBuffer::ReadOnly ) )
|
|
{
|
|
qWarning() << "U8::ReadHeader Can't create buffer";
|
|
return;
|
|
}
|
|
int off = start.indexOf( "IMET" );
|
|
//qDebug() << "imet offset" << hex << off << "u8 offset" << hex << GetU8Offset( ba );
|
|
if( off == 0x40 || off == 0x80 )//read imet header
|
|
{
|
|
if( off > GetU8Offset( ba ) )//in case somebody wants to put a IMET archive inside another U8 for whatever reason
|
|
return;
|
|
|
|
if( off == 0x40 )
|
|
headerType = U8_Hdr_IMET_bnr;
|
|
else
|
|
headerType = U8_Hdr_IMET_app;
|
|
|
|
buf.seek( off );
|
|
IMET imet;
|
|
buf.read( (char*)&imet, sizeof( IMET ) );
|
|
buf.close();
|
|
|
|
for( int h = 0; h < 10; h ++ )
|
|
{
|
|
QString name;
|
|
for( int i = 0; i < IMET_MAX_NAME_LEN && imet.names[ h ][ i ] != 0; i++ )//no need to switch endian, 0x0000 is a palendrome
|
|
name += QChar( qFromBigEndian( imet.names[ h ][ i ] ) );
|
|
|
|
imetNames << name;
|
|
}
|
|
//done
|
|
return;
|
|
}
|
|
}
|
|
QByteArray U8::AddIMD5( QByteArray ba )
|
|
{
|
|
quint32 size = ba.size();
|
|
|
|
/*MD5 hash;
|
|
hash.update( ba.data(), size );
|
|
hash.finalize();*/
|
|
|
|
size = qFromBigEndian( size );
|
|
|
|
QByteArray imd5;
|
|
QBuffer buf( &imd5 );
|
|
if( !buf.open( QBuffer::WriteOnly ) )
|
|
{
|
|
qWarning() << "U8::AddIMD5 Can't create buffer";
|
|
return QByteArray();
|
|
}
|
|
buf.putChar( 'I' );
|
|
buf.putChar( 'M' );
|
|
buf.putChar( 'D' );
|
|
buf.putChar( '5' );
|
|
|
|
buf.write( (const char*)&size, 4 );
|
|
buf.seek( 0x10 );
|
|
|
|
buf.write( GetMd5( ba ) );
|
|
//buf.write( (const char*)hash.hexdigestChar(), 16 );
|
|
buf.close();
|
|
//qDebug() << hash.hexdigest().c_str();
|
|
//hexdump( (void*)imd5.data(), 0x20 );
|
|
|
|
ba.prepend( imd5 );
|
|
|
|
return ba;
|
|
}
|
|
|
|
const QByteArray U8::AddIMD5()
|
|
{
|
|
QByteArray ba = data;
|
|
ba = AddIMD5( ba );
|
|
return ba;
|
|
}
|
|
|
|
QByteArray U8::GetIMET( const QStringList &names, int paddingType, quint32 iconSize, quint32 bannerSize, quint32 soundSize )
|
|
{
|
|
QByteArray ret = QByteArray( sizeof( IMET ) + 0x40, '\0' );
|
|
QBuffer buf( &ret );
|
|
if( !buf.open( QBuffer::WriteOnly ) )
|
|
{
|
|
qWarning() << "U8::GetIMET Can't create buffer";
|
|
return QByteArray();
|
|
}
|
|
buf.seek( 0 + 0x40 );
|
|
buf.putChar( 'I' );
|
|
buf.putChar( 'M' );
|
|
buf.putChar( 'E' );
|
|
buf.putChar( 'T' );
|
|
buf.seek( 6 + 0x40 );
|
|
buf.putChar( '\x6' );
|
|
buf.seek( 11 + 0x40 );
|
|
buf.putChar( '\x3' );
|
|
|
|
quint32 tmp = qFromBigEndian( iconSize );
|
|
buf.write( (char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( bannerSize );
|
|
buf.write( (char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( soundSize );
|
|
buf.write( (char*)&tmp, 4 );
|
|
|
|
int nameOffset = 0x1c + 0x40;
|
|
int numNames = names.size();
|
|
for( int i = 0; i < numNames; i++ )
|
|
{
|
|
QString name = names.at( i );
|
|
int nameLen = name.size();
|
|
buf.seek( nameOffset + ( IMET_MAX_NAME_LEN * i * 2 ) );
|
|
for( int j = 0; j < nameLen; j++ )
|
|
{
|
|
quint16 letter = qFromBigEndian( name.at( j ).unicode() );
|
|
buf.write( ( const char*)&letter, 2 );
|
|
}
|
|
}
|
|
|
|
/*MD5 hash;
|
|
hash.update( ret.data(), sizeof( IMET ) + 0x40 );
|
|
hash.finalize();*/
|
|
|
|
buf.seek( 0x5f0 );
|
|
buf.write( GetMd5( ret ) );
|
|
//buf.write( (const char*)hash.hexdigestChar(), 16 );
|
|
buf.close();
|
|
|
|
switch( paddingType )
|
|
{
|
|
case U8_Hdr_IMET_app:
|
|
ret.prepend( QByteArray( 0x40, '\0' ) );
|
|
break;
|
|
case U8_Hdr_IMET_bnr:
|
|
break;
|
|
default:
|
|
ret.remove( 0, 0x40 );
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
const QStringList &U8::IMETNames()
|
|
{
|
|
return imetNames;
|
|
}
|
|
|
|
void U8::SetImetNames( const QStringList &names )
|
|
{
|
|
imetNames.clear();
|
|
imetNames = names;
|
|
}
|
|
|
|
const QByteArray U8::AddIMET( int paddingType )
|
|
{
|
|
QByteArray ret = GetData( "meta/icon.bin" );
|
|
|
|
quint32 iconSize;
|
|
quint32 bannerSize;
|
|
quint32 soundSize;
|
|
|
|
//if these are lz77 compressed, write the uncompressed size to the header ?bannerbomb?
|
|
//otherwise write the size - 0x20 ( imd5 header size )
|
|
|
|
if( LZ77::GetLz77Offset( ret ) != -1 )
|
|
iconSize = LZ77::GetDecompressedSize( ret );
|
|
else
|
|
iconSize = ret.size() - 0x20;
|
|
|
|
ret = GetData( "meta/banner.bin" );
|
|
if( LZ77::GetLz77Offset( ret ) != -1 )
|
|
bannerSize = LZ77::GetDecompressedSize( ret );
|
|
else
|
|
bannerSize = ret.size() - 0x20;
|
|
|
|
ret = GetData( "meta/sound.bin" );
|
|
if( LZ77::GetLz77Offset( ret ) != -1 )
|
|
soundSize = LZ77::GetDecompressedSize( ret );
|
|
else
|
|
soundSize = ret.size() - 0x20;
|
|
|
|
ret = GetIMET( imetNames, paddingType, iconSize, bannerSize, soundSize );
|
|
if( isLz77 )//really? can the entire banner be lz77 compressed?
|
|
ret += LZ77::Compress( data );
|
|
else
|
|
ret += data;
|
|
|
|
return ret;
|
|
}
|
|
|
|
int U8::GetHeaderType()
|
|
{
|
|
return headerType;
|
|
}
|