Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Created May 22, 2019 12:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save yasirkula/9627ab7ec237b48267621b053d8bb7f3 to your computer and use it in GitHub Desktop.
Save yasirkula/9627ab7ec237b48267621b053d8bb7f3 to your computer and use it in GitHub Desktop.
Tar-like archive in pure C# (.NET 2.0 compatible)
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace SimplePatchToolCore
{
public class SimpleArchive
{
// Archive Structure
// 4 bytes: <NumberOfDirectoriesInt>
// for i = 0 to <NumberOfDirectoriesInt>
// 4 bytes: <LengthOfDirectoryNameInt>
// <LengthOfDirectoryNameInt> bytes: Directory name
//
// for each file
// 4 bytes: Index of the file's directory
// 4 bytes: <LengthOfFilenameInt>
// <LengthOfFilenameInt> bytes: Filename
// 8 bytes: <LengthOfFileContentsLong>
// <LengthOfFileContentsLong> bytes: File contents
public const int END_OF_STREAM = -13;
private FileStream fs;
private readonly List<string> directories;
private readonly bool littleEndian;
private readonly UTF8Encoding textEncoding;
private readonly byte[] intBytes;
private readonly byte[] longBytes;
private readonly byte[] fileBytes;
private byte[] stringBytes;
public SimpleArchive()
{
directories = new List<string>( 128 );
littleEndian = BitConverter.IsLittleEndian;
textEncoding = new UTF8Encoding();
intBytes = new byte[4];
longBytes = new byte[8];
fileBytes = new byte[8 * 1024];
stringBytes = new byte[512];
}
public void Pack( string directory, string archivePath )
{
directories.Clear();
if( directory[directory.Length - 1] != '/' && directory[directory.Length - 1] != '\\' )
directory += Path.DirectorySeparatorChar;
using( fs = new FileStream( archivePath, FileMode.Create ) )
{
IndexDirectoriesRecursively( new DirectoryInfo( directory ), "" );
WriteInt( directories.Count );
for( int i = 0; i < directories.Count; i++ )
WriteString( directories[i] );
for( int i = 0; i < directories.Count; i++ )
{
DirectoryInfo directoryInfo = new DirectoryInfo( directory + directories[i].Replace( '/', Path.DirectorySeparatorChar ) );
if( !directoryInfo.Exists ) // Shouldn't happen
continue;
PackFiles( directoryInfo, i );
}
}
}
public void Unpack( string archivePath, string directory )
{
directories.Clear();
if( directory[directory.Length - 1] != '/' && directory[directory.Length - 1] != '\\' )
directory += Path.DirectorySeparatorChar;
using( fs = new FileStream( archivePath, FileMode.Open, FileAccess.Read ) )
{
int numberOfDirectories = ReadInt();
for( int i = 0; i < numberOfDirectories; i++ )
{
string directoryName = ReadString().Replace( '/', Path.DirectorySeparatorChar );
directories.Add( directoryName );
Directory.CreateDirectory( directory + directoryName );
}
UnpackFiles( directory );
}
}
private void IndexDirectoriesRecursively( DirectoryInfo directory, string relativePath )
{
directories.Add( relativePath );
DirectoryInfo[] subDirectories = directory.GetDirectories();
for( int i = 0; i < subDirectories.Length; i++ )
IndexDirectoriesRecursively( subDirectories[i], string.Concat( relativePath, "/", subDirectories[i].Name ) );
}
private void PackFiles( DirectoryInfo directory, int directoryIndex )
{
FileInfo[] files = directory.GetFiles();
for( int i = 0; i < files.Length; i++ )
{
FileInfo file = files[i];
WriteInt( directoryIndex );
WriteString( file.Name );
WriteFileBytes( file );
}
}
private void UnpackFiles( string directory )
{
while( true )
{
int directoryIndex = ReadInt();
if( directoryIndex == END_OF_STREAM )
break;
string filename = ReadString();
ReadFileBytes( new FileInfo( string.Concat( directory, directories[directoryIndex], Path.DirectorySeparatorChar, filename ) ) );
}
}
private void WriteInt( int value )
{
byte[] bytes = BitConverter.GetBytes( value );
if( littleEndian )
Array.Reverse( bytes );
fs.Write( bytes, 0, 4 );
}
private void WriteLong( long value )
{
byte[] bytes = BitConverter.GetBytes( value );
if( littleEndian )
Array.Reverse( bytes );
fs.Write( bytes, 0, 8 );
}
private void WriteString( string value )
{
byte[] bytes = textEncoding.GetBytes( value );
WriteInt( bytes.Length );
fs.Write( bytes, 0, bytes.Length );
}
private void WriteFileBytes( FileInfo value )
{
WriteLong( value.Length );
using( FileStream input = value.OpenRead() )
{
int bytesRead;
while( ( bytesRead = input.Read( fileBytes, 0, fileBytes.Length ) ) > 0 )
{
fs.Write( fileBytes, 0, bytesRead );
}
}
}
private int ReadInt()
{
if( fs.Read( intBytes, 0, 4 ) < 4 )
return END_OF_STREAM;
if( littleEndian )
Array.Reverse( intBytes );
return BitConverter.ToInt32( intBytes, 0 );
}
private long ReadLong()
{
if( fs.Read( longBytes, 0, 8 ) < 8 )
return END_OF_STREAM;
if( littleEndian )
Array.Reverse( longBytes );
return BitConverter.ToInt64( longBytes, 0 );
}
private string ReadString()
{
int length = ReadInt();
if( stringBytes.Length < length )
stringBytes = new byte[length];
if( fs.Read( stringBytes, 0, length ) < length )
return null;
return textEncoding.GetString( stringBytes, 0, length );
}
private void ReadFileBytes( FileInfo file )
{
long length = ReadLong();
using( FileStream output = file.Create() )
{
int bytesRead;
while( length > 0L && ( bytesRead = fs.Read( fileBytes, 0, length < fileBytes.Length ? (int) length : fileBytes.Length ) ) > 0 )
{
output.Write( fileBytes, 0, bytesRead );
length -= bytesRead;
}
}
}
}
}
@yasirkula
Copy link
Author

Compatible with

  • .NET 2.0
  • .NET Standard 1.3
  • .NET Core 1.0
  • Mono 2.0

Usage

SimpleArchive archive = new SimpleArchive(); // Reuse this instance when possible for better GC

archive.Pack( directoryToArchive, archiveFile ); // Pack a directory into a single archive file
archive.Unpack( archiveFile, outputDirectory ); // Extract files from the archive file into a directory

Notes

  • Files are not compressed in the archive
  • Files' metadata (e.g. creation date, last modified date) are not saved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment