FileSystem method – _CreatePulseFile()
- BOOL FileSystem::_CreatePulseFile( const CHAR *pOutputPath )
- {
- FileIO fFileRead; // File Stream for reading files
- FileIO fPAKWrite; // File Stream for writing to the Pulse PAK File
- FileReader fileReader; // Used for the filters
- FileWriter fileWriter; // Used for the filters
- FileEntryPointer pFileEntry;
- DirPathPointer pFileDirPath;
- String filePath; // Will contain the path of a file to be read in the disk
- POS_T64 lastWritePos;
- PAKGenFilePairList::Iterator fileIter;
- PAKGenFilePairList::Iterator fileIterEnd;
- PAKGenDirList::Iterator dirIter;
- PAKGenDirList::Iterator dirIterEnd;
- FileIO fFileRead : FileIO is a simple fstream wrapper (see msdn for more information about fstream). fFileRead will be used to open up and read each file in the file entry table.
- FileIO fPAKWrite : fPAKWrite is an fstream used to create and open the final PAK file, specified in pOutputPath, and write the final data into it.
- FileReader fileReader : The file reader object used to attach fFileRead and pass into the Encode() method.
- FileWriter fileWriter : File writer object used to attach fPAKWrite and passed to the Encode() method.
- FileEntryPointer pFileEntry : m_pGenFiles is a list containing Pair<> with FileEntryPointer and DirPathPointer in it. We just use this to get the pointer to FileEntryPointer inside the Pair<>.
- DirPathPointer pFileDirPath : pDirFilePath is a pointer used to get the DirPathPointer inside the Pair<> of m_pGenFile list. This contains the absolute path not the relative path ( i.e “c:\music\bla\” ).
- String filePath : Temporary string buffer to format the path including the file name to be processed(open then read).
- POS_T64 lastWritePos : A pointer pointing on where the position of the last file processed in fPAKWrite stopped. This is used to calculate the size of the processes file.
- PAKGenFilePairList::Iterator fileIter, fileIterEnd : Iterators for iterating through the File Entry list(or table) for processing.
- PAKGenDirList:: Iterator diriter, dirIterEnd : Iterators for iterating through the folder entry list(or table) for processing.
Before we create the file to save out our PAK file, we need to check the the *pOutputPath’s diretories exists. We can easily create a file by opening fPAKWrite with the write mode settings. But the problem is that if the directory on where you want the file to create doesn’t exist, then the file creation will fail. In order to solve this problem we need to create the directories if it doesn’t exist.
- // Check to make sure the directories exists
- CHAR *pFolderCheck = new CHAR[PSX_MAX_PATH];
- CHAR *pPtr = const_cast< CHAR*>( pOutputPath );
- while ( pPtr = PSX_StrChr( pPtr, PSX_String( ‘\\’ ) ) )
- {
- ++pPtr;
- PSX_StrCpy( pFolderCheck , pOutputPath, pPtr – pOutputPath );
- pFolderCheck [ pPtr – pOutputPath ] = NULL;
- // Create if it doesn’t exist
- if ( !System::IsDirectoryExist( pFolderCheck ) )
- CreateDirectory( pFolderCheck, NULL );
- }
- PSX_SafeDelete( pFolderCheck );
Create directories if it doesn’t exist.
After making sure the save directories exists, we’re now ready to create the file then attach our fPAKWriter to our fileWriter object
- // Prepare to create the Pulse PAK File
- fPAKWrite.Open( pOutputPath, FileIO::FILEOP_WRITE | FileIO::FILEOP_BINARY | FileIO::FILEOP_TRUNCATE );
- if ( !fPAKWrite.IsOpen() )
- return FALSE;
- fileWriter.SetFileStream( &fPAKWrite );
Create file then attach to fileWriter.
Processing of the actual files comes next. Make sure we set lastWritePos to 0 since the file is currently empty. Then initialize our file iterators to prepare for iterating through each file entries. As we keep on iterating through each file entry, we open the file for reading using fFileRead. Then if the user has specified a function in pOnProcessFileCallback earlier in Create() then we’ll be calling that function in order to determine what kind of filter we want to use. Otherwise we’ll be using the default filter that pretty much does nothing but copies each file bit by bit. After encoding the file, we simply close the fstream since we’re done processing that current file, calculate the total size of the processed file ( currentWritePos – lastWritePos ), then go to the next file to process.
- lastWritePos = 0;
- fileIter = m_pGenFiles->IteratorBegin();
- fileIterEnd = m_pGenFiles->IteratorEnd();
- // Write file data
- while ( fileIter != fileIterEnd )
- {
- pFileEntry = (*fileIter).first;
- pFileDirPath = (*fileIter).second;
- filePath = *pFileDirPath + PSX_String("\\") + pFileEntry->m_name.GetCString();
- // Open file for reading
- fFileRead.Open( filePath.GetCString(), FileIO::FILEOP_READ | FileIO::FILEOP_BINARY );
- PSX_Assert( fFileRead.IsOpen(), "Failed to open a file." );
- fileReader.SetFileStream( &fFileRead );
- if ( m_pOnProcessFile )
- pFileEntry->m_PAKData.m_filterBit = m_pOnProcessFile( pFileEntry->m_name.GetCString(), pFileDirPath->GetCString() );
- Encode( &(*pFileEntry), &fileReader, &fileWriter );
- fFileRead.Close();
- // Update file entry info and other bookeeping vars
- pFileEntry->m_PAKData.m_diskStart = lastWritePos;
- pFileEntry->m_PAKData.m_compressedSize = fPAKWrite.Tell64() – lastWritePos;
- lastWritePos = fPAKWrite.Tell64();
- ++fileIter;
- }
Iterate through and process each file.
Once done processing all the files, all that is left to do is simply write the directory entry table, file entry table, then finally the header for our PAK file.
- // Write folder entry table
- m_pHeader->m_dirDiskStart = fPAKWrite.Tell64();
- dirIter = m_pGenDirs->IteratorBegin();
- dirIterEnd = m_pGenDirs->IteratorEnd();
- while ( dirIter != dirIterEnd )
- {
- (*dirIter)->WriteData( &fileWriter );
- ++dirIter;
- }
- // Write file entry table
- m_pHeader->m_fileDiskStart = fPAKWrite.Tell64();
- fileIter = m_pGenFiles->IteratorBegin();
- fileIterEnd = m_pGenFiles->IteratorEnd();
- while ( fileIter != fileIterEnd )
- {
- (*fileIter).first->WriteData( &fileWriter );
- ++fileIter;
- }
- // Then finally the Pulse File header
- m_pHeader->WriteData( &fileWriter );
- fPAKWrite.Close();
- return TRUE;
- }
Write out directory table, file table and header then wrap it up!
FileSystem method – Decode(), Encode()
Here is the code for the Encode and Decode methods. It simply accesses the filter map and calls their respective functions.
- PSX_INLINE void FileSystem::Encode( DirFileEntry *pFileEntry, IReader *pReader, IWriter *pWriter )
- {
- m_filters[ static_cast<FILTER_TYPE>(pFileEntry->m_PAKData.m_filterBit) ]->Encode( pReader, pWriter );
- }
- PSX_INLINE void FileSystem::Decode( DirFileEntry *pFileEntry, IReader *pReader, IWriter *pWriter )
- {
- m_filters[ static_cast<FILTER_TYPE>(pFileEntry->m_PAKData.m_filterBit) ]->Decode( pReader, pWriter );
- }
FileSystem method – Unpack()
The next method that we will tackle is the Unpack method. This method can extract the entire contents of PAK file then save the contents in a specified path. Now that we have an idea on how our PAK file is internally structured by looking at the Create() method, this method should be trivial. Besides from declaring our familiar temporary objects, we open our PAK file using FileIO plus with some necessary error checks and initializations. We’re also using a utility function called ReadPAKInfo() which reads the entire directory and file table and the header.
- BOOL FileSystem::Unpack( const CHAR *pPAKPath, const CHAR *pOutputPath )
- {
- FileIO pulseFile;
- FileIO createFile;
- FileReader fileReader;
- FileWriter fileWriter;
- BOOL bReturn = TRUE;
- BOOL bAddSeparator = TRUE;
- PAKGenDirList::Iterator dirIter;
- PAKGenDirList::Iterator dirIterEnd;
- PAKGenFileList::Iterator fileIter;
- PAKGenFileList::Iterator fileIterEnd;
- CHAR catPath[PSX_MAX_PATH];
- DWORD currPathIndex;
- FilterPointer pFilter;
- ReleaseResources();
- if ( *(pOutputPath + PSX_StrLen( pOutputPath ) – 1) == PSX_String( ‘\\’ ) )
- bAddSeparator = FALSE;
- // Allocate memory to store PAK information
- m_pHeader = new FileHeader;
- m_pGenDirs = new PAKGenDirList;
- m_pGenFileList = new PAKGenFileList;
- PSX_Assert( m_pHeader && m_pGenDirs && m_pGenFileList, "Failed to allocate memory." );
- // Read header and entries and automatically verify header.
- if ( !ReadPAKInfo( pPAKPath, m_pHeader, m_pGenDirs, m_pGenFileList ) )
- {
- bReturn = FALSE;
- goto UnpackPAKEnd;
- }
- pulseFile.Open( pPAKPath, FileIO::FILEOP_READ | FileIO::FILEOP_BINARY );
- if ( pulseFile.IsOpen() == FALSE )
- {
- bReturn = FALSE;
- goto UnpackPAKEnd;
- }
- if ( !InitializeFilters( m_pHeader->m_filterBitField ) )
- {
- bReturn = FALSE;
- goto UnpackPAKEnd;
- }
- // Make sure folder save path exists
- if ( !System::CreateDirectory( pOutputPath ) )
- {
- bReturn = FALSE;
- goto UnpackPAKEnd;
- }
- // We’re ready to create folders and files
- fileReader.SetFileStream( &pulseFile );
Initializing our data for extracting files from PAK file.
After initializing all our data, we need to make sure that all the folders exists specified in *pOutputPath. We do this by iterating through the directory table and creating each folder at a time.
- // Create folders
- dirIter = m_pGenDirs->IteratorBegin();
- dirIterEnd = m_pGenDirs->IteratorEnd();
- while ( dirIter != dirIterEnd )
- {
- PSX_StrCpy( catPath, pOutputPath, PSX_MAX_PATH );
- if ( bAddSeparator )
- PSX_StrCat( catPath, "\\" );
- PSX_StrCat( catPath, (*dirIter)->m_name.GetCString() );
- System::CreateDirectory( catPath );
- ++dirIter;
- }
Create a directory for each directory entry in *pOutPath.
Now we’re ready to extract each file and save it out! But there’s one thing that we need to take note first. The DirFileEntry’s path is not stored as a string containing its path. Instead, it stores an index to the directory table. We do this in order to save some extra bytes. Assuming we’re currently working on directory path “c:\documents and settings\user\downloads\movies\” and the movies directories contains a thousand of video files. Assuming that our PAK’s root path starts in the user directory then all one thousand video file will contain a path string of “user\downloads\movies\” which takes 22 bytes! That’s 22 thousand bytes! Instead of just storing an index, which is 4 bytes, we’ll only take 4 thousand bytes. That’s a saving of 18 thousand bytes! Since the directory table is on the list, and it would be slow to use a subscript array(if we can even use it), we use a simple technique of simply keeping track of the current path index. This will work because of how we saved our files in the PAK file. All the files in the PAK file are set up so that they contain in one contiguous block for one directory. If we find out that the DirFileEntry’s path index changed, that means we need to move our directory entry forward by one. The rest of the file creation code is almost exactly same when we processed the files for creating our PAK file. The only difference is instead of encoding the files, we decode it instead. And we also have to make sure that we set the read limit so it’ll only stop at the end of that current file entry’s content. This is the reason why the we have to add an extra method for our derived IReader class called SetReadLimit().
- // Start creating files. This is a little tricky to implement…
- currPathIndex = 0;
- fileReader.SetFileStream( &pulseFile );
- dirIter = m_pGenDirs->IteratorBegin();
- fileIter = m_pGenFileList->IteratorBegin();
- fileIterEnd = m_pGenFileList->IteratorEnd();
- while ( fileIter != fileIterEnd )
- {
- // currPathIndex tells us on what directory we’re creating files in
- while ( currPathIndex != (*fileIter)->m_PAKData.m_pathIndex )
- {
- ++currPathIndex;
- ++dirIter;
- // Error check
- PSX_Assert( !(dirIter == dirIterEnd), "Error Pulse File." );
- }
- PSX_StrCpy( catPath, pOutputPath, PSX_MAX_PATH );
- if ( bAddSeparator )
- PSX_StrCat( catPath, PSX_String("\\") );
- PSX_StrCat( catPath, (*dirIter)->m_name.GetCString() );
- // Be aware that a dirEntry could contain an empty string…
- if ( (*dirIter)->m_name.GetLength() )
- PSX_StrCat( catPath, PSX_String("\\") );
- // Then filename
- PSX_StrCat( catPath, (*fileIter)->m_name.GetCString() );
- createFile.Open( catPath, FileIO::FILEOP_WRITE | FileIO::FILEOP_BINARY );
- PSX_Assert( createFile.IsOpen(), "Failed to open file." );
- // Setup reader, writer and set seek to read file data
- pulseFile.Seek64( (*fileIter)->m_PAKData.m_diskStart );
- fileReader.SetReadLimit( (*fileIter)->m_PAKData.m_size );
- fileWriter.SetFileStream( &createFile );
- // Finally write data with the choosen filter.
- Decode( &(**fileIter), &fileReader, &fileWriter );
- createFile.Close();
- ++fileIter;
- }
Processing each file and saving it out.
Finally, we just simply wrap things up by releasing our allocated resources then return.
- UnpackPAKEnd:
- pulseFile.Close();
- ReleaseResources();
- return bReturn;
- }
See Virtual File System – Part 6 for the continuation of this article.