Code archives/Miscellaneous/Pak file system

This code has been declared by its author to be Public Domain code.

Download source code

Pak file system by Helios2008
Allows the user to pak all resources together into a single encrypted/compressed file.
Rem

	This type allows us to implement the 'eachin' support for pak files.
	files.

End Rem
Type TPakFileEnum

	Field _index:Int
	Field _pak:TPakFile

	Method HasNext:Int()
		Return _index < _pak.FileCount() 
	End Method

	Method NextObject:Object()
		Local value:Object = TPakFileChunk(_pak._fileList.ValueAtIndex(_index)).URL
		_index :+ 1
		Return value
	End Method

End Type

Rem

	This type allows the user to request files from the pak framework by using the pak:: protocol
	when requesting streams.

End Rem
Type TPAKStreamFactory Extends TStreamFactory

	Method CreateStream:TStream(url:Object,proto:String,path:String,readable:Int,writeable:Int)
	
		If (proto.ToLower() = "pak")

			' If a physical file exists, override the pak file.
			If (FileType(path) = 1)
			
				Return OpenStream(path, readable, writeable)
			
			EndIf

			' Look for file in pak files.
			path = Replace(path, "/", "\")
			For Local pak:TPakFile = EachIn TPakFile.PakFilePool
			
				If (pak.ContainsFile(path))

					Return CreateBankStream(pak.GetFileBank(path))
					
				EndIf
			
			Next
			
		EndIf
	
	End Method

End Type

New TPAKStreamFactory

Rem

	This type stores details on an individual file chunk contained within a
	pak file.

End Rem
Type TPakFileChunk

	Field URL:String

	Field DataOffset:Int
	Field DataSize:Int

	Rem
		
		Saves this chunks data from a physical file to a bank, encrypts and compresses it, then
		returns it in the form of a bank.
		
	End Rem
	Method CompileDataBank:TBank(encrypted:Int, compressed:Int, encryptionKey:String)
	
		Local bank:TBank = LoadBank(URL)
		If (bank = Null) Return Null
		
		If (compressed = True)
			bank = CompressBank(bank)
		EndIf

		If (encrypted = True)
			bank = EncryptBank(bank, encryptionKey)		
		EndIf
		
		Return bank
	
	End Method
	
	Rem
	
		Loads this chunks data out of a given stream, encrypts and compresses it, then
		returns it in the form of a bank.
	
	End Rem
	Method GetDataBank:TBank(mainStream:TStream, encrypted:Int, compressed:Int, encryptionKey:String)

		' If this chunk is still waiting to be saved to a file, then
		' you can get its data just by loading it out of the physical file.
		If (DataSize = 0 And FileType(URL) = 1)
			Return LoadBank(URL)
		EndIf
		
		Local bank:TBank = CreateBank(DataSize)
		ReadBank(bank, mainStream, 0, DataSize)
		
		If (encrypted = True)
			bank = DecryptBank(bank, encryptionKey)
		EndIf
		
		If (compressed = True)
			bank = DecompressBank(bank)
		EndIf
		
		Return bank

	End Method

End Type

Rem

	This type stores all the code used to access, save and load multiple files into
	a single 'pak' file.
	
	Files can also be encrypted, and compressed for security purposes.

End Rem
Type TPakFile

	Global PakFilePool:TList = New TList

	Field Encrypted:Int
	Field EncryptionKey:String
	Field Compressed:Int
	
	Field _fileList:TList = New TList
	
	Field _mainDataStream:TStream = Null
	Field _mainDataOffset:Int = 0
	
	Rem
		
		Gets a bank that contains the data for the given file.
		
	End Rem
	Method GetFileBank:TBank(url:String)
	
		For Local file:TPakFileChunk = EachIn _fileList
			If (file.URL.ToLower() = url.ToLower())
				SeekStream(_mainDataStream, _mainDataOffset + file.DataOffset)
				Return file.GetDataBank(_mainDataStream, Encrypted, Compressed, EncryptionKey)
			EndIf 
		Next
	
	End Method
	
	Rem
		
		Returns the number of file chunks in this pak file.
		
	End Rem	
	Method FileCount:Int()
	
		Return _fileList.Count()
	
	End Method
	
	Rem
		
		Clears all files out of this pak file.
		
	End Rem		
	Method Clear()
	
		_fileList.Clear()
	
	End Method
	
	Rem
	
		Adds support to this type for enumeration. So that the user can use the eachin keyword
		to iterate through all files in this pak file.
	
	End Rem	
	Method ObjectEnumerator:TPakFileEnum()
		Local enum:TPakFileEnum = New TPakFileEnum
		enum._index = 0
		enum._pak = Self
		Return enum
	End Method
	
	Rem
	
		Adds a reference to a physical file into this pak file. The file must
		exist when and if Save is called so its data can be read. 
	
	End Rem	
	Method AddFile(url:String)
	
		Local file:TPakFileChunk = New TPakFileChunk
		file.URL = Replace(url, "/", "\")
		
		ListAddLast(_fileList, file)
	
	End Method
	
	Rem
	
		Removes a reference to a physical file from this pak file.
	
	End Rem	
	Method RemoveFile(url:String)
	
		For Local file:TPakFileChunk = EachIn _fileList
			If (file.URL.ToLower() = url.ToLower())
				ListRemove(_fileList, file)
			EndIf 
		Next

	End Method
	
	Rem
	
		Adds all the files within a directory into this pak file.
	
	End Rem
	Method AddDirectory(url:String)
	
		url = StripSlash(url) + "/"
	
		Local files:String[] = LoadDir(url, True)
		For Local file:String = EachIn files
		
			If (FileType(url + file) = 1)
		
				AddFile(url + file)
			
			Else If (FileType(url + file) = 2)
	
				AddDirectory(url + file + "/")
						
			EndIf
		
		Next
	
	End Method
	
	Rem
		
		Remove all the files within this pak file that are in the given directory.
		
	End Rem
	Method RemoveDirectory(url:String)

		For Local file:TPakFileChunk = EachIn _fileList
			If (StripSlash(ExtractDir(file.URL)).ToLower() = StripSlash(ExtractDir(url)).ToLower()) 
				ListRemove(_fileList, file)
			EndIf 
		Next	
	
	End Method
	
	Rem
	
		Returns true if the given file exists in this pak file. 
	
	End Rem	
	Method ContainsFile:Int(url:String)
	
		url = Replace(url, "/", "\")
	
		For Local file:TPakFileChunk = EachIn _fileList
			If (file.URL.ToLower() = url.ToLower())
				Return True
			EndIf 
		Next	
		
	
	End Method
	
	Rem
	
		Saves this pak file to a physical file. All file that are referenced in
		this pak file must exist for this function to operate correctly.
	
	End Rem	
	Method Save(url:Object)
	
		Local stream:TStream = WriteStream(url)
		
		' Write in the header contains details on how
		' this pak file is tored.
		WriteString(stream, "PAK")
		WriteByte(stream, Encrypted)
		WriteByte(stream, Compressed)
		WriteInt(stream, _fileList.Count())
		
		Local offset:Int = 0
		Local dataChunks:TList = New TList

		' Write out the file table as well as working out
		' file offsets and compiling the files data.
		For Local file:TPakFileChunk = EachIn _fileList
			
			' Compile this files data and add it to the chunk list.
			Local dataBank:TBank = file.CompileDataBank(Encrypted, Compressed, EncryptionKey)
			If (dataBank = Null)
				Continue
			EndIf
			ListAddLast(dataChunks, dataBank)

			' Write out the file table information for this file.
			WriteInt(stream, file.URL.Length)
			WriteString(stream, file.URL)
			WriteInt(stream, offset)
			WriteInt(stream, BankSize(dataBank))
			
			offset :+ BankSize(dataBank)

		Next

		' Write out the data block which contains all the files data chunks
		' appended together.
		For Local data:TBank = EachIn dataChunks
			WriteBank(data, stream, 0, BankSize(data))
		Next
		
		CloseStream(stream)
	
	End Method
	
	Rem
	
		Loads this pak file from a physical file. No file data will actually be loaded
		until the user requests it.
	
	End Rem	
	Method Load(url:Object)
	
		Local stream:TStream = ReadStream(url)
		If (stream = Null) Return

		' Make sure the header is there, if its not the file
		' has been corrupted and we can ignore loading it.
		If (ReadString(stream, 3) <> "PAK") Then Return
		Encrypted = ReadByte(stream)
		Compressed = ReadByte(stream)
		Local fileCount:Int = ReadInt(Stream)
		
		' Read in the file table.
		For Local fileIndex:Int = 0 To fileCount - 1

			Local url:String = ReadString(stream, ReadInt(stream))
			Local offset:Int = ReadInt(stream)
			Local size:Int = ReadInt(stream)
			
			Local chunk:TPakFileChunk = New TPakFileChunk
			chunk.URL = url
			chunk.DataOffset = offset
			chunk.DataSize = size
			ListAddLast(_fileList, chunk)

		Next
		
		_mainDataOffset = StreamPos(stream)
		_mainDataStream = stream
			
	End Method
	
	Rem
	
		Initializes a new instance of this pak file and adds it to the 
		main pak file pool.
		
	End Rem
	Method New()
	
		ListAddLast(PakFilePool, Self)
	
	End Method
	
	Rem
	
		Disposes of all resources held by this pak file.
		
	End Rem
	Method Dispose()
	
		If (_mainDataStream) 
			CloseStream(_mainDataStream)
			_mainDataStream = Null
		EndIf
		
		ClearList(_fileList)
		
		ListRemove(PakFilePool, Self)
	
	End Method

End Type

Rem

	Creates a new pak file instance.

End Rem
Function CreatePakFile:TPakFile()
	Return New TPakFile
End Function

Rem

	Loads a given pak file from the given url.
	
	url           : URL to load pak file from.
	encryptionKey : If the pak file is encrypted this is the 
					key that will be used to decrypt it.

End Rem
Function LoadPakFile:TPakFile(url:Object, encryptionKey:String = "")
	Local pak:TPakFile = New TPakFile
	pak.EncryptionKey = encryptionKey
	pak.Load(url)
	Return pak
End Function

Rem

	Saves a given pak file to the given url.
	
	pak 			: Pak file to save.
	url 			: URL to save pak file to.
	compress 		: If set to true the pak file will be compressed.
	encrypt 		: If set to true the pak file will be encrypted.
	encryptionKey 	: If the pak file is being encrypted this is the key it will be encrypted with.

End Rem
Function SavePakFile(pak:TPakFile, url:Object, compress:Int = False, encrypt:Int = False, encryptionKey:String = "") 
	pak.Compressed = compress
	pak.Encrypted = encrypt
	pak.EncryptionKey = encryptionKey
	pak.Save(url)
End Function

Rem

	Clears all files out of the given pak file.
	
	pak : Pak file to clear.

End Rem
Function ClearPakFile(pak:TPakFile)
	pak.Clear()
End Function

Rem

	Disposes of all resources in a given pak file.
	
	pak : Pak file to dispose.

End Rem
Function DisposePakFile(pak:TPakFile)
	pak.Dispose()
End Function

Rem

	Returns true if the pak file contains the given file.
	
	pak : Pak file to search.
	url : URL of file to search for.

End Rem
Function PakFileContainsFile:Int(pak:TPakFile, url:String)
	Return pak.ContainsFile(url)
End Function

Rem

	Adds the given physical file to the pak file. The file must exist
	when and if Save is called for it to be saved correctly.
	
	pak : Pak file to clear.
	url : URL of physical file to add to pak file.

End Rem
Function AddFileToPakFile(pak:TPakFile, url:String)
	pak.AddFile(url)
End Function

Rem
	
	Removes the given physical file from the pak file. 
	
	pak : Pak file to remove physical file from.
	url : URL of physical file to remove.
	
End Rem
Function RemoveFileFromPakFile(pak:TPakFile, url:String)
	pak.RemoveFile(url)
End Function

Rem

	Adds the given directory and all the files contained in it to the pak file. The directory
	and its files must exist when and if Save is called for it to be saved correctly.
	
	pak : Pak file to clear.
	url : URL of physical directory to add to pak file.

End Rem
Function AddDirToPakFile(pak:TPakFile, url:String)
	pak.AddDirectory(url)
End Function

Rem
	
	Removes the given physical directory from the pak file. 
	
	pak : Pak file to remove physical file from.
	url : URL of physical directory to remove.
	
End Rem
Function RemoveDirFromPakFile(pak:TPakFile, url:String)
	pak.RemoveDirectory(url)
End Function

Rem

	Returns the amount of physical files contained in the given pak file.

	pak : Pak file to count files.

End Rem
Function PakFileFileCount:Int(pak:TPakFile)
	Return pak.FileCount()
End Function

Rem

	Return a bank containing the data of the given physical file stored
	in the given pak file.
	
	pak : Pak file to extract data from.
	url : URL of physical file to retrieve data for.

End Rem
Function PakFileGetFileBank:TBank(pak:TPakFile, url:String)
	Return pak.GetFileBank(url)
End Function

Rem
	
	Looks through all pak files, and all physical files for the given resource. If it exists
	this function will return true, else false.
	
	url : URL of resource file to look for.
	
End Rem
Function FileExists:Int(url:String)

	url = Replace(url, "\", "/")

	If (FileType(url) = 1) Return True
	
	For Local pak:TPakFile = EachIn TPakFile.PakFilePool
		If (pak.ContainsFile(url))
			Return True
		EndIf
	Next
	
	Return False
	
End Function

Rem

	This function will compress a bank's data and return it in another bank.
	
	Credit to Mark Sibly for this code.

End Rem
Function CompressBank:TBank(bank:TBank)

	Local size:Int = bank.Size()
	Local out_size:Int = size + size / 10 + 32
	Local out:TBank = TBank.Create(out_size)
	
	compress(out.Buf() + 4, out_size, bank.Buf(), size)
	
	out.PokeByte(0, size)
	out.PokeByte(1, size Shr 8)
	out.PokeByte(2, size Shr 16)
	out.PokeByte(3, size Shr 24)
	out.Resize(out_size + 4)
	
	Return out
	
End Function

Rem

	This function will decompress a bank's data and returns it in another bank.
	
	Credit to Mark Sibly for this code.

End Rem
Function DecompressBank:TBank(bank:TBank)

	Local out_size:Int
	out_size :| (bank.PeekByte(0))
	out_size :| (bank.PeekByte(1) Shl 8)
	out_size :| (bank.PeekByte(2) Shl 16)
	out_size :| (bank.PeekByte(3) Shl 24)
	
	Local out:TBank = TBank.Create(out_size)
	uncompress(out.Buf(), out_size, bank.Buf() + 4, bank.Size() - 4)
	
	Return out

End Function

Rem

	This function will encrypt a bank using a simple XOr encryption algorithem and return
	the encrypted data in another bank.
	
	TODO: Use a less repetative, more powerfull encryption algorithem, XOr is near useless.

End Rem
Function EncryptBank:TBank(bank:TBank, key:String)

	Local cryptBank:TBank = CreateBank(BankSize(bank))

	For Local index:Int = 0 To BankSize(bank) - 1
	
		Local plain:Int = PeekByte(bank, index)
		Local keyIndex:Int = key[index Mod key.Length]
		
		PokeByte(cryptBank, index, plain ~ keyIndex) 
	
	Next

	Return cryptBank

End Function

Rem

	This function will decrypt a bank using a simple XOr encryption algorithem and return
	the decrypted data in another bank.
	
	TODO: Use a less repetative, more powerfull encryption algorithem, XOr is near useless.

End Rem
Function DecryptBank:TBank(bank:TBank, key:String)

	Return EncryptBank(bank, key) ' Encryption and decryption uses same algorithem.

End Function




Rem

     Compiling pak file example

End Rem

' Create a new pak file and set it up as compressed.
Local pak:TPakFile = New TPakFile 
pak.Compressed = True

' Add every file in our bin directory to the pak file.
pak.AddDirectory("Bin\")

' Save this pak file to a physical file.
pak.Save("game.dat")

' Dispose of resources held by pak file.
pak.Dispose()
	


Rem

     Using pak file example

End Rem

' Create a new pak file and set it up as compressed.
Local pak:TPakFile = New TPakFile 
pak.Compressed = True

' Load it from our file.
pak.Load("game.dat")

' Lets extract some resources from it.
local image:TImage = LoadImage("pak::Bin\Graphics\MyImage.png")
local sound:TSound = LoadSound("pak::Bin\Audio\MySound.wav")

' Dispose of resources held by pak file.
pak.Dispose()

Comments

Koriolis2008
Why not just use a simple zip, and the zipstream module?
I don't mean to dismiss your pak system, I'm just wondering what are the precise advantages over a plain zip.


Helios2008
There are none really, the only advantage really is the ability to change it to your liking, add custom information and such. I guess it would also probably make it more difficult for users to get access to your assets if the file format isn't so well known.


plash2008
You should adapt RC4 ( http://www.blitzbasic.com/codearcs/codearcs.php?code=1711#comments ) for the encryption.


Code Archives Forum