using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Windows.Storage;
namespace Tarball
{
public class TarballReader
{
#region Private attributes
private Stream stream;
private TarballHeader header;
#endregion
#region Constructor
///
/// Creates a new instance of a tarball archive reader.
///
public TarballReader()
{
stream = null;
header = new TarballHeader();
}
#endregion
#region Public functions (unpack)
///
/// Unpacks a tarball in a temporary folder.
///
/// An URI to the tarball file.
/// Storage folder pointing to where the files were unpacked.
public async Task Unpack(Uri file)
{
var stfile = await StorageFile.GetFileFromApplicationUriAsync(file);
return await this.Unpack(stfile);
}
///
/// Unpacks a tarball in a specified folder.
///
/// An URI to the tarball file.
/// A folder where files will be unpacked.
/// Storage folder pointing to where the files were unpacked.
public async Task Unpack(Uri file, StorageFolder destination)
{
var stfile = await StorageFile.GetFileFromApplicationUriAsync(file);
return await this.Unpack(stfile, destination);
}
///
/// Unpacks a tarball in a temporary folder.
///
/// A path to the tarball file.
/// Storage folder pointing to where the files were unpacked.
public async Task Unpack(string file)
{
var stfile = await StorageFile.GetFileFromPathAsync(file);
return await this.Unpack(stfile);
}
///
/// Unpacks a tarball in a specified folder.
///
/// A path to the tarball file.
/// A folder where files will be unpacked.
/// Storage folder pointing to where the files were unpacked.
public async Task Unpack(string file, StorageFolder destination)
{
var stfile = await StorageFile.GetFileFromPathAsync(file);
return await this.Unpack(stfile, destination);
}
///
/// Unpacks a tarball in a temporary folder.
///
/// The tarball file.
/// Storage folder pointing to where the files were unpacked.
public async Task Unpack(StorageFile file)
{
// Prepare temp folder
var dest = await this.CreateTempFolder();
// Unpack
await this.Initialize(file);
await this.UnpackFiles(dest);
this.Dispose();
// Results
return dest;
}
///
/// Unpacks a tarball in a specified folder.
///
/// The tarball file.
/// A folder where files will be unpacked.
/// Storage folder pointing to where the files were unpacked.
public async Task Unpack(StorageFile file, StorageFolder destination)
{
// Unpack
await this.Initialize(file);
await this.UnpackFiles(destination);
this.Dispose();
// Results
return destination;
}
#endregion
#region Initialize, dispose
///
/// Performs initialization actions before unpacking (such as opening the stream).
///
private async Task Initialize(StorageFile file)
{
var str = await file.OpenReadAsync();
this.stream = str.AsStream();
}
///
/// Performs cleanups after unpacking finished.
///
private void Dispose()
{
// Clean up
this.stream.Dispose();
this.stream = null;
this.header = new TarballHeader();
}
#endregion
#region Headers
///
/// Calculates the checksum from a header.
///
/// The header bytes
private uint CalculateChecksum(byte[] buffer)
{
uint result = 0;
// Calculate sum of all bytes, with the exception of bytes 148-155
// (checksum field). These are all assumed to be 0x20.
for (int i = 0; i < buffer.Length; i++)
if (i >= 148 && i < 156)
result += 0x20;
else result += Convert.ToUInt32(buffer[i]);
// Done
return result;
}
///
/// Converts binary data to a TarballHeader.
///
private TarballHeader ParseHeaderFields(byte[] buffer)
{
TarballHeader header = new TarballHeader();
string temp;
// File name
temp = UTF8Encoding.UTF8.GetString(buffer, 0, 100).Trim('\0', ' ');
header.FileName = temp;
// File mode
temp = UTF8Encoding.UTF8.GetString(buffer, 100, 8).Trim('\0', ' ');
header.FileMode = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToUInt32(temp, 8);
// Owner id
temp = UTF8Encoding.UTF8.GetString(buffer, 108, 8).Trim('\0', ' ');
header.OwnerId = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToUInt32(temp, 8);
// Group id
temp = UTF8Encoding.UTF8.GetString(buffer, 116, 8).Trim('\0', ' ');
header.GroupId = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToUInt32(temp, 8);
// Size
temp = UTF8Encoding.UTF8.GetString(buffer, 124, 12).Trim('\0', ' ');
header.Size = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToInt64(temp, 8);
// Last modified date
temp = UTF8Encoding.UTF8.GetString(buffer, 136, 12).Trim('\0', ' ');
long seconds = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToInt64(temp, 8);
header.LastModified = new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(seconds).ToLocalTime();
// Checksum
temp = UTF8Encoding.UTF8.GetString(buffer, 148, 8).Trim('\0', ' ');
header.Checksum = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToUInt32(temp, 8);
// Link indicator
header.LinkIndicator = buffer[156];
// Linked file
temp = UTF8Encoding.UTF8.GetString(buffer, 157, 100).Trim('\0', ' ');
header.LinkedFile = temp;
// Done
return header;
}
///
/// Reads a file header.
///
/// True if another header was read, false otherwise.
private async Task ReadNextFileHeader()
{
byte[] buffer = new byte[512];
// Check current position
if (stream.Position >= stream.Length)
return false;
// Read header
await stream.ReadAsync(buffer, 0, 512);
// Parse header fields
try
{
this.header = this.ParseHeaderFields(buffer);
}
catch (Exception)
{
throw new IOException("Invalid tarball header!");
}
// Verify checksum
uint checksum = this.CalculateChecksum(buffer);
if (checksum == 256) // If 256 (only the checksum bytes different than 0), then
return false; // we most likely hit an invalid entry, probably marking the
// end of the file
if (checksum != header.Checksum)
throw new IOException("Invalid tarball checksum!");
// Done
return true;
}
#endregion
#region File system helpers
///
/// Creates a temporary folder.
///
private async Task CreateTempFolder()
{
// Generate file name
string name = "tar" + DateTime.Now.Ticks.ToString();
// Create file
var temp = ApplicationData.Current.TemporaryFolder;
return await temp.CreateFolderAsync(name, CreationCollisionOption.GenerateUniqueName);
}
#endregion
#region Unpack
///
/// Unpacks a file using the information from the header.
/// The function assumes the header was previously read.
///
/// The destination file.
private async Task UnpackNextFile(StorageFile destination)
{
// Open destination file
var str = await destination.OpenAsync(FileAccessMode.ReadWrite);
var iostr = str.AsStream();
// Write data
var buffer = new byte[512];
int read = 0, total = 0;
while (total < this.header.Size)
{
read = await this.stream.ReadAsync(buffer, 0, 512);
await iostr.WriteAsync(buffer, 0, Math.Min(read, Convert.ToInt32(this.header.Size) - total));
total += read;
}
// Cleanup
await iostr.FlushAsync();
iostr.Dispose();
}
///
/// Unpacks the files from the loaded tarball.
///
/// Destination folder.
private async Task UnpackFiles(StorageFolder destination)
{
if (this.stream == null)
throw new ArgumentNullException("No file opened!");
while (await this.ReadNextFileHeader())
{
// Directory?
if (this.header.FileName.EndsWith("/"))
await IOHelper.CreateFolderRelativeAsync(destination, this.header.FileName);
// Create file
else
{
var file = await IOHelper.CreateFileRelativeAsync(destination, this.header.FileName);
await this.UnpackNextFile(file);
}
}
}
#endregion
}
}