/* * Ref: http://www.info-zip.org/pub/infozip/doc/appnote-981119-iz.zip */ #include "global.h" #include "RageFileDriverZip.h" #include "RageFileDriverSlice.h" #include "RageFileDriverDeflate.h" #include "RageFile.h" #include "RageLog.h" #include "RageUtil.h" #include "RageUtil_FileDB.h" #include static struct FileDriverEntry_ZIP: public FileDriverEntry { FileDriverEntry_ZIP(): FileDriverEntry( "ZIP" ) { } RageFileDriver *Create( const RString &sRoot ) const { return new RageFileDriverZip( sRoot ); } } const g_RegisterDriver; RageFileDriverZip::RageFileDriverZip(): RageFileDriver( new NullFilenameDB ), m_Mutex( "RageFileDriverZip" ) { m_bFileOwned = false; m_pZip = NULL; } RageFileDriverZip::RageFileDriverZip( const RString &sPath ): RageFileDriver( new NullFilenameDB ), m_Mutex( "RageFileDriverZip" ) { m_bFileOwned = false; m_pZip = NULL; Load( sPath ); } bool RageFileDriverZip::Load( const RString &sPath ) { ASSERT( m_pZip == NULL ); /* don't load twice */ m_bFileOwned = true; m_sPath = sPath; m_Mutex.SetName( ssprintf("RageFileDriverZip(%s)", sPath.c_str()) ); RageFile *pFile = new RageFile; if( !pFile->Open(sPath) ) { WARN( ssprintf("Couldn't open %s: %s", sPath.c_str(), pFile->GetError().c_str()) ); delete pFile; return false; } m_pZip = pFile; return ParseZipfile(); } bool RageFileDriverZip::Load( RageFileBasic *pFile ) { ASSERT( m_pZip == NULL ); /* don't load twice */ m_sPath = ssprintf("%p", pFile); m_Mutex.SetName( ssprintf("RageFileDriverZip(%p)", pFile) ); m_pZip = pFile; return ParseZipfile(); } bool RageFileDriverZip::ReadEndCentralRecord( int &iTotalEntries, int &iCentralDirectoryOffset ) { RString sError; RString sSig = FileReading::ReadString( *m_pZip, 4, sError ); FileReading::read_16_le( *m_pZip, sError ); /* skip number of this disk */ FileReading::read_16_le( *m_pZip, sError ); /* skip disk with central directory */ FileReading::read_16_le( *m_pZip, sError ); /* skip number of entries on this disk */ iTotalEntries = FileReading::read_16_le( *m_pZip, sError ); FileReading::read_32_le( *m_pZip, sError ); /* skip size of the central directory */ iCentralDirectoryOffset = FileReading::read_32_le( *m_pZip, sError ); int iCommentLength = FileReading::read_16_le( *m_pZip, sError ); m_sComment = FileReading::ReadString( *m_pZip, iCommentLength, sError ); if( sError != "" ) { WARN( ssprintf("%s: %s", m_sPath.c_str(), sError.c_str()) ); return false; } return true; } /* Find the end of central directory record, and seek to it. */ bool RageFileDriverZip::SeekToEndCentralRecord() { const int iSearchTo = max( m_pZip->GetFileSize() - 1024*32, 0 ); int iRealPos = m_pZip->GetFileSize(); while( iRealPos > 0 && iRealPos >= iSearchTo ) { /* Move back in the file; leave some overlap between checks, to handle * the case where the signature crosses the block boundary. */ char buf[1024*4]; iRealPos -= sizeof(buf) - 4; iRealPos = max( 0, iRealPos ); m_pZip->Seek( iRealPos ); int iGot = m_pZip->Read( buf, sizeof(buf) ); if( iGot == -1 ) { WARN( ssprintf("%s: %s", m_sPath.c_str(), m_pZip->GetError().c_str()) ); return false; } for( int iPos = iGot - 4; iPos >= 0; --iPos ) { if( memcmp(buf + iPos, "\x50\x4B\x05\x06", 4) ) continue; m_pZip->Seek( iRealPos + iPos ); return true; } } return false; } bool RageFileDriverZip::ParseZipfile() { if( !SeekToEndCentralRecord() ) { WARN( ssprintf("Couldn't open %s: couldn't find end of central directory record", m_sPath.c_str()) ); return false; } /* Read the end of central directory record. */ int iTotalEntries, iCentralDirectoryOffset; if( !ReadEndCentralRecord(iTotalEntries, iCentralDirectoryOffset) ) return false; /* warned already */ /* Seek to the start of the central file directory. */ m_pZip->Seek( iCentralDirectoryOffset ); /* Loop through files in central directory. */ for( int i = 0; i < iTotalEntries; ++i ) { FileInfo info; info.m_iDataOffset = -1; int got = ProcessCdirFileHdr( info ); if( got == -1 ) /* error */ break; if( got == 0 ) /* skip */ continue; FileInfo *pInfo = new FileInfo( info ); m_pFiles.push_back( pInfo ); FDB->AddFile( "/" + pInfo->m_sName, pInfo->m_iUncompressedSize, pInfo->m_iCRC32, pInfo ); } if( m_pFiles.size() == 0 ) WARN( ssprintf("%s: no files found in central file header", m_sPath.c_str()) ); return true; } int RageFileDriverZip::ProcessCdirFileHdr( FileInfo &info ) { RString sError; RString sSig = FileReading::ReadString( *m_pZip, 4, sError ); if( sSig != "\x50\x4B\x01\x02" ) { WARN( ssprintf("%s: central directory record signature not found", m_sPath.c_str()) ); return -1; } FileReading::read_8( *m_pZip, sError ); /* skip version made by */ int iOSMadeBy = FileReading::read_8( *m_pZip, sError ); FileReading::read_16_le( *m_pZip, sError ); /* skip version needed to extract */ int iGeneralPurpose = FileReading::read_16_le( *m_pZip, sError ); info.m_iCompressionMethod = (ZipCompressionMethod) FileReading::read_16_le( *m_pZip, sError ); FileReading::read_16_le( *m_pZip, sError ); /* skip last mod file time */ FileReading::read_16_le( *m_pZip, sError ); /* skip last mod file date */ info.m_iCRC32 = FileReading::read_32_le( *m_pZip, sError ); info.m_iCompressedSize = FileReading::read_32_le( *m_pZip, sError ); info.m_iUncompressedSize = FileReading::read_32_le( *m_pZip, sError ); int iFilenameLength = FileReading::read_16_le( *m_pZip, sError ); int iExtraFieldLength = FileReading::read_16_le( *m_pZip, sError ); int iFileCommentLength = FileReading::read_16_le( *m_pZip, sError ); FileReading::read_16_le( *m_pZip, sError ); /* relative offset of local header */ FileReading::read_16_le( *m_pZip, sError ); /* skip internal file attributes */ unsigned iExternalFileAttributes = FileReading::read_32_le( *m_pZip, sError ); info.m_iOffset = FileReading::read_32_le( *m_pZip, sError ); /* Check for errors before reading variable-length fields. */ if( sError != "" ) { WARN( ssprintf("%s: %s", m_sPath.c_str(), sError.c_str()) ); return -1; } info.m_sName = FileReading::ReadString( *m_pZip, iFilenameLength, sError ); FileReading::SkipBytes( *m_pZip, iExtraFieldLength, sError ); /* skip extra field */ FileReading::SkipBytes( *m_pZip, iFileCommentLength, sError ); /* skip file comment */ if( sError != "" ) { WARN( ssprintf("%s: %s", m_sPath.c_str(), sError.c_str()) ); return -1; } /* Check usability last, so we always read past the whole entry and don't leave the * file pointer in the middle of a record. */ if( iGeneralPurpose & 1 ) { WARN( ssprintf("Skipped encrypted \"%s\" in \"%s\"", info.m_sName.c_str(), m_sPath.c_str()) ); return 0; } /* Skip directories. */ if( iExternalFileAttributes & (1<<4) ) return 0; info.m_iFilePermissions = 0; enum { MADE_BY_UNIX = 3 }; switch( iOSMadeBy ) { case MADE_BY_UNIX: info.m_iFilePermissions = (iExternalFileAttributes >> 16) & 0x1FF; break; } if( info.m_iCompressionMethod != STORED && info.m_iCompressionMethod != DEFLATED ) { WARN( ssprintf("File \"%s\" in \"%s\" uses unsupported compression method %i", info.m_sName.c_str(), m_sPath.c_str(), info.m_iCompressionMethod) ); return 0; } return 1; } bool RageFileDriverZip::ReadLocalFileHeader( FileInfo &info ) { /* Seek to and read the local file header. */ m_pZip->Seek( info.m_iOffset ); RString sError; RString sSig = FileReading::ReadString( *m_pZip, 4, sError ); if( sError != "" ) { WARN( ssprintf("%s: error opening \"%s\": %s", m_sPath.c_str(), info.m_sName.c_str(), sError.c_str()) ); return false; } if( sSig != "\x50\x4B\x03\x04" ) { WARN( ssprintf("%s: local file header not found for \"%s\"", m_sPath.c_str(), info.m_sName.c_str()) ); return false; } FileReading::SkipBytes( *m_pZip, 22, sError ); /* skip most of the local file header */ const int iFilenameLength = FileReading::read_16_le( *m_pZip, sError ); const int iExtraFieldLength = FileReading::read_16_le( *m_pZip, sError ); info.m_iDataOffset = m_pZip->Tell() + iFilenameLength + iExtraFieldLength; if( sError != "" ) { WARN( ssprintf("%s: %s", m_sPath.c_str(), sError.c_str()) ); return false; } return true; } RageFileDriverZip::~RageFileDriverZip() { for( unsigned i = 0; i < m_pFiles.size(); ++i ) delete m_pFiles[i]; if( m_bFileOwned ) delete m_pZip; } const RageFileDriverZip::FileInfo *RageFileDriverZip::GetFileInfo( const RString &sPath ) const { return (const FileInfo *) FDB->GetFilePriv( sPath ); } RageFileBasic *RageFileDriverZip::Open( const RString &sPath, int iMode, int &iErr ) { if( iMode & RageFile::WRITE ) { iErr = ERROR_WRITING_NOT_SUPPORTED; return NULL; } FileInfo *info = (FileInfo *) FDB->GetFilePriv( sPath ); if( info == NULL ) { iErr = ENOENT; return NULL; } m_Mutex.Lock(); /* If we haven't figured out the offset to the real data yet, do so now. */ if( info->m_iDataOffset == -1 ) { if( !ReadLocalFileHeader(*info) ) { m_Mutex.Unlock(); return NULL; } } /* We won't do any further access to zip, except to copy it (which is * threadsafe), so we can unlock now. */ m_Mutex.Unlock(); RageFileDriverSlice *pSlice = new RageFileDriverSlice( m_pZip->Copy(), info->m_iDataOffset, info->m_iCompressedSize ); pSlice->DeleteFileWhenFinished(); switch( info->m_iCompressionMethod ) { case STORED: return pSlice; case DEFLATED: { RageFileObjInflate *pInflate = new RageFileObjInflate( pSlice, info->m_iUncompressedSize ); pInflate->DeleteFileWhenFinished(); return pInflate; } default: /* unknown compression method */ iErr = ENOSYS; return NULL; } } /* NOP for now. This could check to see if the ZIP's mtime has changed, and reload. */ void RageFileDriverZip::FlushDirCache( const RString &sPath ) { } /* * Copyright (c) 2003-2005 Glenn Maynard * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */