using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using Windows.Storage; namespace DrumKit.Archiving { public class TarballReader { #region Data types etc /// /// Tarball header structure /// private struct TarballHeader { public string FileName; public uint FileMode; public uint OwnerId, GroupId; public int Size; public DateTime LastModified; public uint Checksum; public byte LinkIndicator; public string LinkedFile; } #endregion #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 = SharpDX.Text.ASCIIEncoding.ASCII.GetString(buffer, 0, 100).Trim('\0', ' '); header.FileName = temp; // File mode temp = SharpDX.Text.ASCIIEncoding.ASCII.GetString(buffer, 100, 8).Trim('\0', ' '); header.FileMode = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToUInt32(temp, 8); // Owner id temp = SharpDX.Text.ASCIIEncoding.ASCII.GetString(buffer, 108, 8).Trim('\0', ' '); header.OwnerId = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToUInt32(temp, 8); // Group id temp = SharpDX.Text.ASCIIEncoding.ASCII.GetString(buffer, 116, 8).Trim('\0', ' '); header.GroupId = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToUInt32(temp, 8); // Size temp = SharpDX.Text.ASCIIEncoding.ASCII.GetString(buffer, 124, 12).Trim('\0', ' '); header.Size = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToInt32(temp, 8); // Last modified date temp = SharpDX.Text.ASCIIEncoding.ASCII.GetString(buffer, 136, 12).Trim('\0', ' '); int seconds = (string.IsNullOrEmpty(temp)) ? 0 : Convert.ToInt32(temp, 8); header.LastModified = new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(seconds).ToLocalTime(); // Checksum temp = SharpDX.Text.ASCIIEncoding.ASCII.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 = SharpDX.Text.ASCIIEncoding.ASCII.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 this.header = this.ParseHeaderFields(buffer); // 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 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, 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 } }