From 1f2b903aef4bb79a23b84f3bce1ad4f0884c0a98 Mon Sep 17 00:00:00 2001 From: Frederik Carlier Date: Sun, 14 May 2017 22:20:18 +0200 Subject: [PATCH] Preliminary support for reading tar files --- Packaging.Targets.Tests/IO/TarFileTests.cs | 51 +++++++ Packaging.Targets/IO/ArchiveFile.cs | 4 +- Packaging.Targets/IO/GZipDecompressor.cs | 34 +++++ Packaging.Targets/IO/TarFile.cs | 61 ++++++++ Packaging.Targets/IO/TarHeader.cs | 169 +++++++++++++++++++++ Packaging.Targets/IO/TarTypeFlag.cs | 21 +++ 6 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 Packaging.Targets.Tests/IO/TarFileTests.cs create mode 100644 Packaging.Targets/IO/GZipDecompressor.cs create mode 100644 Packaging.Targets/IO/TarFile.cs create mode 100644 Packaging.Targets/IO/TarHeader.cs create mode 100644 Packaging.Targets/IO/TarTypeFlag.cs diff --git a/Packaging.Targets.Tests/IO/TarFileTests.cs b/Packaging.Targets.Tests/IO/TarFileTests.cs new file mode 100644 index 0000000..1650eda --- /dev/null +++ b/Packaging.Targets.Tests/IO/TarFileTests.cs @@ -0,0 +1,51 @@ +using Packaging.Targets.IO; +using System.Collections.ObjectModel; +using System.IO; +using Xunit; + +namespace Packaging.Targets.Tests.IO +{ + /// + /// Tests the class. + /// + public class TarFileTests + { + /// + /// Reads the contents of a .tar file. + /// + [Fact] + public void ReadTarFileTest() + { + using (Stream stream = File.OpenRead("Deb/libplist3_1.12-3.1_amd64.deb")) + using (ArFile arFile = new ArFile(stream, leaveOpen: true)) + { + // Skip the debian version + arFile.Read(); + + // This is the tar file + arFile.Read(); + + Collection filenames = new Collection(); + Collection contents = new Collection(); + Collection headers = new Collection(); + + using (Stream entryStream = arFile.Open()) + using (GZipDecompressor decompressedStream = new GZipDecompressor(entryStream, leaveOpen: true)) + using (TarFile tarFile = new TarFile(decompressedStream, leaveOpen: true)) + { + while (tarFile.Read()) + { + filenames.Add(tarFile.FileName); + headers.Add((TarHeader)tarFile.FileHeader); + + using (Stream data = tarFile.Open()) + using (StreamReader reader = new StreamReader(data)) + { + contents.Add(reader.ReadToEnd()); + } + } + } + } + } + } +} diff --git a/Packaging.Targets/IO/ArchiveFile.cs b/Packaging.Targets/IO/ArchiveFile.cs index d0dd04c..360505a 100644 --- a/Packaging.Targets/IO/ArchiveFile.cs +++ b/Packaging.Targets/IO/ArchiveFile.cs @@ -27,7 +27,7 @@ namespace Packaging.Targets.IO /// Initializes a new instance of the class. /// /// - /// A which represents the CPIO data. + /// A which represents the archive data. /// /// /// to leave the underlying open when this @@ -138,7 +138,7 @@ namespace Packaging.Targets.IO } else { - byte[] buffer = new byte[PaddingSize(alignmentBase, (int)this.EntryStream.Length)]; + byte[] buffer = new byte[PaddingSize(alignmentBase, (int)this.Stream.Position)]; this.Stream.Read(buffer, 0, buffer.Length); } } diff --git a/Packaging.Targets/IO/GZipDecompressor.cs b/Packaging.Targets/IO/GZipDecompressor.cs new file mode 100644 index 0000000..0b3be9f --- /dev/null +++ b/Packaging.Targets/IO/GZipDecompressor.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace Packaging.Targets.IO +{ + /// + /// Provides read-only access do a decompressed , and keeps track of the current position. + /// + internal class GZipDecompressor : GZipStream + { + private long position = 0; + + public GZipDecompressor(Stream stream, bool leaveOpen) + : base(stream, CompressionMode.Decompress, leaveOpen) + { + } + + /// + public override long Position + { + get { return this.position; } + set { throw new NotSupportedException(); } + } + + /// + public override int Read(byte[] array, int offset, int count) + { + var read = base.Read(array, offset, count); + this.position += read; + return read; + } + } +} diff --git a/Packaging.Targets/IO/TarFile.cs b/Packaging.Targets/IO/TarFile.cs new file mode 100644 index 0000000..eacb3d4 --- /dev/null +++ b/Packaging.Targets/IO/TarFile.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; + +namespace Packaging.Targets.IO +{ + /// + /// Represents a Tar archive. + /// + public class TarFile : ArchiveFile + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A which represents the tar file. + /// + /// + /// to leave the underlying open when this + /// is disposed of; otherwise, . + /// + public TarFile(Stream stream, bool leaveOpen) + : base(stream, leaveOpen) + { + } + + private TarHeader entryHeader; + + /// + public override bool Read() + { + if (this.EntryStream != null) + { + this.EntryStream.Dispose(); + } + + this.Align(512); + + this.entryHeader = this.Stream.ReadStruct(); + this.FileHeader = this.entryHeader; + this.FileName = this.entryHeader.FileName; + + // There are two empty blocks at the end of the file. + if (this.entryHeader.Magic == string.Empty) + { + return false; + } + + if (this.entryHeader.Magic != "ustar") + { + throw new InvalidDataException("The magic for the file entry is invalid"); + } + + this.Align(512); + + // TODO: Validate Checksum + this.EntryStream = new SubStream(this.Stream, this.Stream.Position, this.entryHeader.FileSize, leaveParentOpen: true); + + return true; + } + } +} diff --git a/Packaging.Targets/IO/TarHeader.cs b/Packaging.Targets/IO/TarHeader.cs new file mode 100644 index 0000000..8203d2a --- /dev/null +++ b/Packaging.Targets/IO/TarHeader.cs @@ -0,0 +1,169 @@ +using System; +using System.Runtime.InteropServices; + +namespace Packaging.Targets.IO +{ + /// + /// Represents the header for an individual entry in a .tar archive. + /// + internal struct TarHeader : IArchiveHeader + { + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 100)] + private char[] name; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + private char[] mode; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + private char[] uid; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + private char[] gid; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 12)] + private char[] size; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 12)] + private char[] mtime; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + private char[] chksum; + + private byte typeflag; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 100)] + private char[] linkname; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 6)] + private char[] magic; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 2)] + private char[] version; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 32)] + private char[] uname; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 32)] + private char[] gname; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + private char[] devmajor; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + private char[] devminor; + + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 155)] + private char[] prefix; + + /// + /// Gets or sets the name of the current file. + /// + public string FileName + { + get { return this.GetString(this.name); } + set { this.name = value.ToCharArray(); } + } + + public LinuxFileMode FileMode + { + get { return (LinuxFileMode)Convert.ToUInt32(this.GetString(this.mode), 8); } + set { this.mode = ((uint)value).ToString("x8").ToCharArray(); } + } + + public uint UserId + { + get { return Convert.ToUInt32(this.GetString(this.uid), 8); } + set { this.uid = value.ToString().ToCharArray(); } + } + + public uint GroupId + { + get { return Convert.ToUInt32(this.GetString(this.gid), 8); } + set { this.gid = value.ToString("x8").ToCharArray(); } + } + + public uint FileSize + { + get { return Convert.ToUInt32(this.GetString(this.size), 8); } + set { this.size = value.ToString("x8").ToCharArray(); } + } + + public DateTimeOffset LastModified + { + get { return DateTimeOffset.FromUnixTimeSeconds((long)Convert.ToUInt32(this.GetString(this.mtime))); } + set { this.mtime = value.ToUnixTimeSeconds().ToString("x8").ToCharArray(); } + } + + public uint Checksum + { + get { return Convert.ToUInt32(this.GetString(this.chksum), 8); } + set { this.chksum = value.ToString("x8").ToCharArray(); } + } + + public TarTypeFlag TypeFlag + { + get { return (TarTypeFlag)this.typeflag; } + set { this.typeflag = (byte)value; } + } + + public string LinkName + { + get { return this.GetString(this.linkname); } + set { this.linkname = value.ToCharArray(); } + } + + public string Magic + { + get { return this.GetString(this.magic).Trim(); } + set { this.magic = value.PadRight(6).ToCharArray(); } + } + + public uint Version + { + get { return Convert.ToUInt32(this.GetString(this.version), 8); } + set { this.version = value.ToString("x8").ToCharArray(); } + } + + public string UserName + { + get { return this.GetString(this.uname); } + set { this.uname = value.ToCharArray(); } + } + + public string GroupName + { + get { return this.GetString(this.gname); } + set { this.gname = value.ToCharArray(); } + } + + public uint DevMajor + { + get { return Convert.ToUInt32(this.GetString(this.devmajor), 8); } + set { this.devmajor = value.ToString("x8").ToCharArray(); } + } + + public uint DevMinor + { + get { return Convert.ToUInt32(this.GetString(this.devminor), 8); } + set { this.devminor = value.ToString("x8").ToCharArray(); } + } + + public string Prefix + { + get { return this.GetString(this.prefix); } + set { this.prefix = value.ToCharArray(); } + } + + private string GetString(char[] buffer) + { + int count = 0; + + while (count < buffer.Length && buffer[count] != '\0') + { + count++; + } + + return new string(buffer, 0, count); + } + } +} diff --git a/Packaging.Targets/IO/TarTypeFlag.cs b/Packaging.Targets/IO/TarTypeFlag.cs new file mode 100644 index 0000000..2e8bd0b --- /dev/null +++ b/Packaging.Targets/IO/TarTypeFlag.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Packaging.Targets.IO +{ + internal enum TarTypeFlag : byte + { + RegType = (byte)'0', + ARegType = (byte)'\0', + LnkType = (byte)'1', + SymType = (byte)'2', + ChrType = (byte)'3', + BlkType = (byte)'4', + DirType = (byte)'5', + FifoType = (byte)'6', + ConttType = (byte)'7', + ExtendedHeader = (byte)'x', + GlobalExtendedHeader = (byte)'g' + } +}