1
0

WIP: Added packing of AR and TAR

This commit is contained in:
2023-03-13 10:08:50 +01:00
parent e8a1378f60
commit 7cd5358ac8
18 changed files with 2071 additions and 4 deletions

View File

@@ -0,0 +1,53 @@
using System;
//[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
namespace AMWD.Common.Packing.Ar
{
/// <summary>
/// Represents the file information saved in the archive.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class ArFileInfo
{
/// <summary>
/// Gets or sets the file name.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the file size in bytes.
/// </summary>
public long FileSize { get; set; }
/// <summary>
/// Gets or sets the timestamp of the last modification.
/// </summary>
public DateTime ModifyTime { get; set; }
/// <summary>
/// Gets or sets the user id.
/// </summary>
public int UserId { get; set; }
/// <summary>
/// Gets or sets the group id.
/// </summary>
public int GroupId { get; set; }
/// <summary>
/// Gets or sets the access mode in decimal (not octal!).
/// </summary>
/// <remarks>
/// To see the octal representation use <c>Convert.ToString(Mode, 8)</c>.
/// </remarks>
public int Mode { get; set; }
}
internal class ArFileInfoExtended : ArFileInfo
{
public long HeaderPosition { get; set; }
public long DataPosition { get; set; }
}
}

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace AMWD.Common.Packing.Ar
{
/// <summary>
/// Reads UNIX ar (archive) files in the GNU format.
/// </summary>
public class ArReader
{
// Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29
private readonly Stream inStream;
private readonly List<ArFileInfoExtended> files = new();
private readonly long streamStartPosition;
private static readonly DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Initializes a new instance of the <see cref="ArReader"/> class.
/// </summary>
/// <param name="inStream">The stream to read the archive from.</param>
public ArReader(Stream inStream)
{
if (!inStream.CanRead || !inStream.CanSeek)
throw new ArgumentException("Stream not readable or seekable", nameof(inStream));
streamStartPosition = inStream.Position;
this.inStream = inStream;
Initialize();
}
/// <summary>
/// Returns a list with all filenames of the archive.
/// </summary>
public IEnumerable<string> GetFileList()
{
return files.Select(fi => fi.FileName).ToList();
}
/// <summary>
/// Returns the file info of a specific file in the archive.
/// </summary>
/// <param name="fileName">The name of the specific file.</param>
public ArFileInfo GetFileInfo(string fileName)
{
return files
.Where(fi => fi.FileName == fileName)
.Select(fi => new ArFileInfo
{
FileName = fi.FileName,
FileSize = fi.FileSize,
GroupId = fi.GroupId,
Mode = fi.Mode,
ModifyTime = fi.ModifyTime,
UserId = fi.UserId
})
.FirstOrDefault();
}
/// <summary>
/// Reads a file from the archive into a stream.
/// </summary>
/// <param name="fileName">The file name in the archive.</param>
/// <param name="outStream">The output stream.</param>
public void ReadFile(string fileName, Stream outStream)
{
if (!outStream.CanWrite)
throw new ArgumentException("Stream not writable", nameof(outStream));
var info = files.Where(fi => fi.FileName == fileName).FirstOrDefault();
if (info == null)
return;
long bytesToRead = info.FileSize;
byte[] buffer = new byte[1024 * 1024];
inStream.Seek(info.DataPosition, SeekOrigin.Begin);
while (bytesToRead > 0)
{
int readCount = (int)Math.Min(bytesToRead, buffer.Length);
inStream.Read(buffer, 0, readCount);
outStream.Write(buffer, 0, readCount);
bytesToRead -= readCount;
}
inStream.Seek(streamStartPosition, SeekOrigin.Begin);
}
/// <summary>
/// Reads a fie from the archive and saves it to disk.
/// </summary>
/// <param name="fileName">The file name in the archive.</param>
/// <param name="destinationPath">The destination path on disk.</param>
public void ReadFile(string fileName, string destinationPath)
{
var info = files.Where(fi => fi.FileName == fileName).FirstOrDefault();
if (info == null)
return;
using (var fs = File.OpenWrite(destinationPath))
{
ReadFile(fileName, fs);
}
File.SetLastWriteTimeUtc(destinationPath, info.ModifyTime);
}
private void Initialize()
{
// Reset stream
inStream.Seek(streamStartPosition, SeekOrigin.Begin);
// Read header
string header = ReadAsciiString(8);
if (header != "!<arch>\n")
throw new FormatException("The file stream is no archive");
// Create file list
while (inStream.Position < inStream.Length)
{
var info = ReadFileHeader();
files.Add(info);
// Move stream behind file content
inStream.Seek(info.FileSize, SeekOrigin.Current);
// Align to even offsets (padded with LF bytes)
if (inStream.Position % 2 != 0)
inStream.Seek(1, SeekOrigin.Current);
}
// Reset stream
inStream.Seek(streamStartPosition, SeekOrigin.Begin);
}
private string ReadAsciiString(int byteCount)
{
byte[] buffer = new byte[byteCount];
inStream.Read(buffer, 0, byteCount);
return Encoding.ASCII.GetString(buffer);
}
private ArFileInfoExtended ReadFileHeader()
{
long startPosition = inStream.Position;
string fileName = ReadAsciiString(16).Trim();
int.TryParse(ReadAsciiString(12).Trim(), out int unixTimestamp);
int.TryParse(ReadAsciiString(6).Trim(), out int userId);
int.TryParse(ReadAsciiString(6).Trim(), out int groupId);
int mode = Convert.ToInt32(ReadAsciiString(8).Trim(), 8);
long.TryParse(ReadAsciiString(10).Trim(), out long fileSize);
// file magic
byte[] magic = new byte[2];
inStream.Read(magic, 0, magic.Length);
if (magic[0] != 0x60 || magic[1] != 0x0A)
throw new FormatException("Invalid file magic");
return new ArFileInfoExtended
{
HeaderPosition = startPosition,
DataPosition = inStream.Position,
FileName = fileName,
ModifyTime = unixEpoch.AddSeconds(unixTimestamp),
UserId = userId,
GroupId = groupId,
Mode = mode,
FileSize = fileSize
};
}
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.IO;
using System.Text;
namespace AMWD.Common.Packing.Ar
{
/// <summary>
/// Writes UNIX ar (archive) files in the GNU format.
/// </summary>
/// <remarks>
/// Copied from <a href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Ar/ArWriter.cs"/>
/// </remarks>
public class ArWriter
{
// Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29
private readonly Stream outStream;
private static readonly DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Initialises a new instance of the <see cref="ArWriter"/> class.
/// </summary>
/// <param name="outStream">The stream to write the archive to.</param>
public ArWriter(Stream outStream)
{
if (!outStream.CanWrite)
throw new ArgumentException("Stream not writable", nameof(outStream));
this.outStream = outStream;
Initialize();
}
/// <summary>
/// Writes a file from disk to the archive.
/// </summary>
/// <param name="fileName">The name of the file to copy.</param>
/// <param name="userId">The user ID of the file in the archive.</param>
/// <param name="groupId">The group ID of the file in the archive.</param>
/// <param name="mode">The mode of the file in the archive (decimal).</param>
public void WriteFile(string fileName, int userId = 0, int groupId = 0, int mode = 33188 /* 0100644 */)
{
var fi = new FileInfo(fileName);
using var fs = File.OpenRead(fileName);
WriteFile(fs, fi.Name, fi.LastWriteTimeUtc, userId, groupId, mode);
}
/// <summary>
/// Writes a file from a Stream to the archive.
/// </summary>
/// <param name="stream">The stream to read the file contents from.</param>
/// <param name="fileName">The name of the file in the archive.</param>
/// <param name="modifyTime">The last modification time of the file in the archive.</param>
/// <param name="userId">The user ID of the file in the archive.</param>
/// <param name="groupId">The group ID of the file in the archive.</param>
/// <param name="mode">The mode of the file in the archive (decimal).</param>
public void WriteFile(Stream stream, string fileName, DateTime modifyTime, int userId = 0, int groupId = 0, int mode = 33188 /* 0100644 */)
{
// Write file header
WriteFileHeader(fileName, modifyTime, userId, groupId, mode, stream.Length);
// Write file contents
stream.CopyTo(outStream);
// Align to even offsets, pad with LF bytes
if ((outStream.Position % 2) != 0)
{
byte[] bytes = new byte[] { 0x0A };
outStream.Write(bytes, 0, 1);
}
}
/// <summary>
/// Writes the archive header.
/// </summary>
private void Initialize()
{
WriteAsciiString("!<arch>\n");
}
/// <summary>
/// Writes a file header.
/// </summary>
private void WriteFileHeader(string fileName, DateTime modifyTime, int userId, int groupId, int mode, long fileSize)
{
// File name
if (fileName.Length > 16)
throw new ArgumentException("Long file names are not supported.");
WriteAsciiString(fileName.PadRight(16, ' '));
// File modification timestamp
int unixTime = (int)(modifyTime - unixEpoch).TotalSeconds;
WriteAsciiString(unixTime.ToString().PadRight(12, ' '));
// User ID
if (userId >= 0)
{
WriteAsciiString(userId.ToString().PadRight(6, ' '));
}
else
{
WriteAsciiString(" ");
}
// Group ID
if (groupId >= 0)
{
WriteAsciiString(groupId.ToString().PadRight(6, ' '));
}
else
{
WriteAsciiString(" ");
}
// File mode
if (mode >= 0)
{
WriteAsciiString(Convert.ToString(mode, 8).PadRight(8, ' '));
}
else
{
WriteAsciiString(" ");
}
// File size in bytes
if (fileSize < 0 || 10000000000 <= fileSize)
throw new ArgumentOutOfRangeException("Invalid file size."); // above 9.32 GB
WriteAsciiString(fileSize.ToString().PadRight(10, ' '));
// File magic
byte[] bytes = new byte[] { 0x60, 0x0A };
outStream.Write(bytes, 0, 2);
}
/// <summary>
/// Writes a string using ASCII encoding.
/// </summary>
/// <param name="str">The string to write to the output stream.</param>
private void WriteAsciiString(string str)
{
byte[] bytes = Encoding.ASCII.GetBytes(str);
outStream.Write(bytes, 0, bytes.Length);
}
}
}

View File

@@ -0,0 +1,26 @@
namespace AMWD.Common.Packing.Tar.Interfaces
{
/// <summary>
/// Interface of a archive writer.
/// </summary>
public interface IArchiveDataWriter
{
/// <summary>
/// Write <paramref name="count"/> bytes of data from <paramref name="buffer"/> to corresponding archive.
/// </summary>
/// <param name="buffer">The data storage.</param>
/// <param name="count">How many bytes to be written to the corresponding archive.</param>
int Write(byte[] buffer, int count);
/// <summary>
/// Gets a value indicating whether the writer can write.
/// </summary>
bool CanWrite { get; }
}
/// <summary>
/// The writer delegate.
/// </summary>
/// <param name="writer">The writer.</param>
public delegate void WriteDataDelegate(IArchiveDataWriter writer);
}

View File

@@ -0,0 +1,125 @@
using System;
using AMWD.Common.Packing.Tar.Utils;
namespace AMWD.Common.Packing.Tar.Interfaces
{
/// <summary>
/// See "struct star_header" in <a href="https://www.gnu.org/software/tar/manual/html_node/Standard.html" />
/// </summary>
public interface ITarHeader
{
/// <summary>
/// The name field is the file name of the file, with directory names (if any) preceding the file name,
/// separated by slashes.
/// </summary>
/// <remarks>
/// <c>name</c>
/// <br/>
/// Byte offset: <c>0</c>
/// </remarks>
string FileName { get; set; }
/// <summary>
/// The mode field provides nine bits specifying file permissions and three bits to specify
/// the Set UID, Set GID, and Save Text (sticky) modes.
/// When special permissions are required to create a file with a given mode,
/// and the user restoring files from the archive does not hold such permissions,
/// the mode bit(s) specifying those special permissions are ignored.
/// Modes which are not supported by the operating system restoring files from the archive will be ignored.
/// Unsupported modes should be faked up when creating or updating an archive; e.g.,
/// the group permission could be copied from the other permission.
/// </summary>
/// <remarks>
/// <c>mode</c>
/// <br/>
/// Byte offset: <c>100</c>
/// </remarks>
int Mode { get; set; }
/// <summary>
/// The uid field is the numeric user ID of the file owners.
/// If the operating system does not support numeric user ID, this field should be ignored.
/// </summary>
/// <remarks>
/// <c>uid</c>
/// <br/>
/// Byte offset: <c>108</c>
/// </remarks>
int UserId { get; set; }
/// <summary>
/// The gid fields is the numeric group ID of the file owners.
/// If the operating system does not support numeric group ID, this field should be ignored.
/// </summary>
/// <remarks>
/// <c>gid</c>
/// <br/>
/// Byte offset: <c>116</c>
/// </remarks>
int GroupId { get; set; }
/// <summary>
/// The size field is the size of the file in bytes;
/// linked files are archived with this field specified as zero.
/// </summary>
/// <remarks>
/// <c>size</c>
/// <br/>
/// Byte offset: <c>124</c>
/// </remarks>
long SizeInBytes { get; set; }
/// <summary>
/// <para>mtime</para>
/// <para>byte offset: 136</para>
/// The mtime field represents the data modification time of the file at the time it was archived.
/// It represents the integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
/// </summary>
/// <remarks>
/// <c>mtime</c>
/// <br/>
/// Byte offset: <c>136</c>
/// </remarks>
DateTime LastModification { get; set; }
/// <summary>
/// The typeflag field specifies the type of file archived.
/// If a particular implementation does not recognize or permit the specified type,
/// the file will be extracted as if it were a regular file.
/// As this action occurs, tar issues a warning to the standard error.
/// </summary>
/// <remarks>
/// <c>typeflag</c>
/// <br/>
/// Byte offset: <c>156</c>
/// </remarks>
EntryType EntryType { get; set; }
/// <summary>
/// The uname field will contain the ASCII representation of the owner of the file.
/// If found, the user ID is used rather than the value in the uid field.
/// </summary>
/// <remarks>
/// <c>uname</c>
/// <br/>
/// Byte offset: <c>265</c>
/// </remarks>
string UserName { get; set; }
/// <summary>
/// The gname field will contain the ASCII representation of the group of the file.
/// If found, the group ID is used rather than the values in the gid field.
/// </summary>
/// <remarks>
/// <c>gname</c>
/// <br/>
/// Byte offset: <c>297</c>
/// </remarks>
string GroupName { get; set; }
/// <summary>
/// The size of this header.
/// </summary>
int HeaderSize { get; }
}
}

View File

@@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using AMWD.Common.Packing.Tar.Interfaces;
using AMWD.Common.Packing.Tar.Utils;
namespace AMWD.Common.Packing.Tar
{
/// <summary>
/// Extract contents of a tar file represented by a stream for the TarReader constructor
/// </summary>
/// <remarks>
/// https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarReader.cs
/// </remarks>
public class TarReader
{
private readonly byte[] dataBuffer = new byte[512];
private readonly UsTarHeader header;
private readonly Stream inStream;
private long remainingBytesInFile;
/// <summary>
/// Constructs TarReader object to read data from `tarredData` stream
/// </summary>
/// <param name="tarredData">A stream to read tar archive from</param>
public TarReader(Stream tarredData)
{
inStream = tarredData;
header = new UsTarHeader();
}
public ITarHeader FileInfo
{
get { return header; }
}
/// <summary>
/// Read all files from an archive to a directory. It creates some child directories to
/// reproduce a file structure from the archive.
/// </summary>
/// <param name="destDirectory">The out directory.</param>
///
/// CAUTION! This method is not safe. It's not tar-bomb proof.
/// {see http://en.wikipedia.org/wiki/Tar_(file_format) }
/// If you are not sure about the source of an archive you extracting,
/// then use MoveNext and Read and handle paths like ".." and "../.." according
/// to your business logic.
public void ReadToEnd(string destDirectory)
{
while (MoveNext(false))
{
string fileNameFromArchive = FileInfo.FileName;
string totalPath = destDirectory + Path.DirectorySeparatorChar + fileNameFromArchive;
if (UsTarHeader.IsPathSeparator(fileNameFromArchive[fileNameFromArchive.Length - 1]) || FileInfo.EntryType == EntryType.Directory)
{
// Record is a directory
Directory.CreateDirectory(totalPath);
continue;
}
// If record is a file
string fileName = Path.GetFileName(totalPath);
string directory = totalPath.Remove(totalPath.Length - fileName.Length);
Directory.CreateDirectory(directory);
using (FileStream file = File.Create(totalPath))
{
Read(file);
}
}
}
/// <summary>
/// Read data from a current file to a Stream.
/// </summary>
/// <param name="dataDestanation">A stream to read data to</param>
///
/// <seealso cref="MoveNext"/>
public void Read(Stream dataDestanation)
{
Debug.WriteLine("tar stream position Read in: " + inStream.Position);
int readBytes;
byte[] read;
while ((readBytes = Read(out read)) != -1)
{
Debug.WriteLine("tar stream position Read while(...) : " + inStream.Position);
dataDestanation.Write(read, 0, readBytes);
}
Debug.WriteLine("tar stream position Read out: " + inStream.Position);
}
protected int Read(out byte[] buffer)
{
if (remainingBytesInFile == 0)
{
buffer = null;
return -1;
}
int align512 = -1;
long toRead = remainingBytesInFile - 512;
if (toRead > 0)
toRead = 512;
else
{
align512 = 512 - (int)remainingBytesInFile;
toRead = remainingBytesInFile;
}
int bytesRead = inStream.Read(dataBuffer, 0, (int)toRead);
remainingBytesInFile -= bytesRead;
if (inStream.CanSeek && align512 > 0)
{
inStream.Seek(align512, SeekOrigin.Current);
}
else
while (align512 > 0)
{
inStream.ReadByte();
--align512;
}
buffer = dataBuffer;
return bytesRead;
}
/// <summary>
/// Check if all bytes in buffer are zeroes
/// </summary>
/// <param name="buffer">buffer to check</param>
/// <returns>true if all bytes are zeroes, otherwise false</returns>
private static bool IsEmpty(IEnumerable<byte> buffer)
{
foreach (byte b in buffer)
{
if (b != 0) return false;
}
return true;
}
/// <summary>
/// Move internal pointer to a next file in archive.
/// </summary>
/// <param name="skipData">Should be true if you want to read a header only, otherwise false</param>
/// <returns>false on End Of File otherwise true</returns>
///
/// Example:
/// while(MoveNext())
/// {
/// Read(dataDestStream);
/// }
/// <seealso cref="Read(Stream)"/>
public bool MoveNext(bool skipData)
{
Debug.WriteLine("tar stream position MoveNext in: " + inStream.Position);
if (remainingBytesInFile > 0)
{
if (!skipData)
{
throw new TarException(
"You are trying to change file while not all the data from the previous one was read. If you do want to skip files use skipData parameter set to true.");
}
// Skip to the end of file.
if (inStream.CanSeek)
{
long remainer = (remainingBytesInFile % 512);
inStream.Seek(remainingBytesInFile + (512 - (remainer == 0 ? 512 : remainer)), SeekOrigin.Current);
}
else
{
byte[] buffer;
while (Read(out buffer) != -1)
{
}
}
}
byte[] bytes = header.GetBytes();
int headerRead = inStream.Read(bytes, 0, header.HeaderSize);
if (headerRead < 512)
{
throw new TarException("Can not read header");
}
if (IsEmpty(bytes))
{
headerRead = inStream.Read(bytes, 0, header.HeaderSize);
if (headerRead == 512 && IsEmpty(bytes))
{
Debug.WriteLine("tar stream position MoveNext out(false): " + inStream.Position);
return false;
}
throw new TarException("Broken archive");
}
if (header.UpdateHeaderFromBytes())
{
throw new TarException("Checksum check failed");
}
remainingBytesInFile = header.SizeInBytes;
Debug.WriteLine("tar stream position MoveNext out(true): " + inStream.Position);
return true;
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.IO;
using AMWD.Common.Packing.Tar.Interfaces;
using AMWD.Common.Packing.Tar.Utils;
namespace AMWD.Common.Packing.Tar
{
// https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarWriter.cs
public class TarWriter : LegacyTarWriter
{
public TarWriter(Stream outStream) : base(outStream)
{
}
protected override void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType)
{
var tarHeader = new UsTarHeader()
{
FileName = name,
Mode = mode,
UserId = userId,
GroupId = groupId,
SizeInBytes = count,
LastModification = lastModificationTime,
EntryType = entryType,
UserName = Convert.ToString(userId, 8),
GroupName = Convert.ToString(groupId, 8)
};
OutStream.Write(tarHeader.GetHeaderValue(), 0, tarHeader.HeaderSize);
}
protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, string userName, string groupName, int mode, EntryType entryType)
{
WriteHeader(
name: name,
lastModificationTime: lastModificationTime,
count: count,
userId: userName.GetHashCode(),
groupId: groupName.GetHashCode(),
mode: mode,
entryType: entryType);
}
public virtual void Write(string name, long dataSizeInBytes, string userName, string groupName, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate)
{
var writer = new DataWriter(OutStream, dataSizeInBytes);
WriteHeader(name, lastModificationTime, dataSizeInBytes, userName, groupName, mode, EntryType.File);
while (writer.CanWrite)
{
writeDelegate(writer);
}
AlignTo512(dataSizeInBytes, false);
}
public void Write(Stream data, long dataSizeInBytes, string fileName, string userId, string groupId, int mode,
DateTime lastModificationTime)
{
WriteHeader(fileName, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
WriteContent(dataSizeInBytes, data);
AlignTo512(dataSizeInBytes, false);
}
}
}

View File

@@ -0,0 +1,45 @@
using System.IO;
using AMWD.Common.Packing.Tar.Interfaces;
namespace AMWD.Common.Packing.Tar.Utils
{
internal class DataWriter : IArchiveDataWriter
{
private readonly long size;
private long remainingBytes;
private readonly Stream stream;
public DataWriter(Stream data, long dataSizeInBytes)
{
size = dataSizeInBytes;
remainingBytes = size;
stream = data;
}
public bool CanWrite { get; private set; } = true;
public int Write(byte[] buffer, int count)
{
if (remainingBytes == 0)
{
CanWrite = false;
return -1;
}
int bytesToWrite;
if (remainingBytes - count < 0)
{
bytesToWrite = (int)remainingBytes;
}
else
{
bytesToWrite = count;
}
stream.Write(buffer, 0, bytesToWrite);
remainingBytes -= bytesToWrite;
return bytesToWrite;
}
}
}

View File

@@ -0,0 +1,73 @@
namespace AMWD.Common.Packing.Tar.Utils
{
/// <summary>
/// See "Values used in typeflag field." in <a href="https://www.gnu.org/software/tar/manual/html_node/Standard.html" />
/// </summary>
public enum EntryType : byte
{
/// <summary>
/// AREGTYPE, regular file
/// </summary>
File = 0,
/// <summary>
/// REGTYPE, regular file
/// </summary>
FileObsolete = 0x30,
/// <summary>
/// LNKTYPE, link
/// </summary>
HardLink = 0x31,
/// <summary>
/// SYMTYPE, reserved
/// </summary>
SymLink = 0x32,
/// <summary>
/// CHRTYPE, character special
/// </summary>
CharDevice = 0x33,
/// <summary>
/// BLKTYPE, block special
/// </summary>
BlockDevice = 0x34,
/// <summary>
/// DIRTYPE, directory
/// </summary>
Directory = 0x35,
/// <summary>
/// FIFOTYPE, FIFO special
/// </summary>
Fifo = 0x36,
/// <summary>
/// CONTTYPE, reserved
/// </summary>
Content = 0x37,
/// <summary>
/// XHDTYPE, Extended header referring to the next file in the archive
/// </summary>
ExtendedHeader = 0x78,
/// <summary>
/// XGLTYPE, Global extended header
/// </summary>
GlobalExtendedHeader = 0x67,
/// <summary>
/// GNUTYPE_LONGLINK, Identifies the *next* file on the tape as having a long linkname.
/// </summary>
LongLink = 0x4b,
/// <summary>
/// GNUTYPE_LONGNAME, Identifies the *next* file on the tape as having a long name.
/// </summary>
LongName = 0x4c
}
}

View File

@@ -0,0 +1,258 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using AMWD.Common.Packing.Tar.Interfaces;
namespace AMWD.Common.Packing.Tar.Utils
{
/// <summary>
/// Implements a legacy TAR writer.
/// </summary>
/// <remarks>
/// Copied from <a href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/LegacyTarWriter.cs" />
/// </remarks>
public class LegacyTarWriter : IDisposable
{
private readonly Stream outStream;
protected byte[] buffer = new byte[1024];
private bool isClosed;
public bool ReadOnZero = true;
/// <summary>
/// Writes tar (see GNU tar) archive to a stream
/// </summary>
/// <param name="outStream">stream to write archive to</param>
public LegacyTarWriter(Stream outStream)
{
this.outStream = outStream;
}
protected virtual Stream OutStream
{
get { return outStream; }
}
#region IDisposable Members
public void Dispose()
{
Close();
}
#endregion IDisposable Members
public void WriteDirectoryEntry(string path, int userId, int groupId, int mode)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException("path");
if (path[path.Length - 1] != '/')
{
path += '/';
}
DateTime lastWriteTime;
if (Directory.Exists(path))
{
lastWriteTime = Directory.GetLastWriteTime(path);
}
else
{
lastWriteTime = DateTime.Now;
}
// handle long path names (> 99 characters)
if (path.Length > 99)
{
WriteLongName(
name: path,
userId: userId,
groupId: groupId,
mode: mode,
lastModificationTime: lastWriteTime);
// shorten the path name so it can be written properly
path = path.Substring(0, 99);
}
WriteHeader(path, lastWriteTime, 0, userId, groupId, mode, EntryType.Directory);
}
public void WriteDirectory(string directory, bool doRecursive)
{
if (string.IsNullOrEmpty(directory))
throw new ArgumentNullException("directory");
WriteDirectoryEntry(directory, 0, 0, 0755);
string[] files = Directory.GetFiles(directory);
foreach (var fileName in files)
{
Write(fileName);
}
string[] directories = Directory.GetDirectories(directory);
foreach (var dirName in directories)
{
WriteDirectoryEntry(dirName, 0, 0, 0755);
if (doRecursive)
{
WriteDirectory(dirName, true);
}
}
}
public void Write(string fileName)
{
if (string.IsNullOrEmpty(fileName))
throw new ArgumentNullException("fileName");
using (FileStream file = File.OpenRead(fileName))
{
Write(file, file.Length, fileName, 61, 61, 511, File.GetLastWriteTime(file.Name));
}
}
public void Write(FileStream file)
{
string path = Path.GetFullPath(file.Name).Replace(Path.GetPathRoot(file.Name), string.Empty);
path = path.Replace(Path.DirectorySeparatorChar, '/');
Write(file, file.Length, path, 61, 61, 511, File.GetLastWriteTime(file.Name));
}
public void Write(Stream data, long dataSizeInBytes, string name)
{
Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now);
}
public virtual void Write(string name, long dataSizeInBytes, int userId, int groupId, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate)
{
IArchiveDataWriter writer = new DataWriter(OutStream, dataSizeInBytes);
WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
while (writer.CanWrite)
{
writeDelegate(writer);
}
AlignTo512(dataSizeInBytes, false);
}
public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode,
DateTime lastModificationTime)
{
if (isClosed)
throw new TarException("Can not write to the closed writer");
// handle long file names (> 99 characters)
if (name.Length > 99)
{
WriteLongName(
name: name,
userId: userId,
groupId: groupId,
mode: mode,
lastModificationTime: lastModificationTime);
// shorten the file name so it can be written properly
name = name.Substring(0, 99);
}
WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
WriteContent(dataSizeInBytes, data);
AlignTo512(dataSizeInBytes, false);
}
/// <summary>
/// Handle long file or path names (> 99 characters).
/// Write header and content, which its content contain the long (complete) file/path name.
/// <para>This handling method is adapted from https://github.com/qmfrederik/dotnet-packaging/pull/50/files#diff-f64c58cc18e8e445cee6ffed7a0d765cdb442c0ef21a3ed80bd20514057967b1 </para>
/// </summary>
/// <param name="name">File name or path name.</param>
/// <param name="userId">User ID.</param>
/// <param name="groupId">Group ID.</param>
/// <param name="mode">Mode.</param>
/// <param name="lastModificationTime">Last modification time.</param>
private void WriteLongName(string name, int userId, int groupId, int mode, DateTime lastModificationTime)
{
// must include a trailing \0
var nameLength = Encoding.UTF8.GetByteCount(name);
byte[] entryName = new byte[nameLength + 1];
Encoding.UTF8.GetBytes(name, 0, name.Length, entryName, 0);
// add a "././@LongLink" pseudo-entry which contains the full name
using (var nameStream = new MemoryStream(entryName))
{
WriteHeader("././@LongLink", lastModificationTime, entryName.Length, userId, groupId, mode, EntryType.LongName);
WriteContent(entryName.Length, nameStream);
AlignTo512(entryName.Length, false);
}
}
protected void WriteContent(long count, Stream data)
{
while (count > 0 && count > buffer.Length)
{
int bytesRead = data.Read(buffer, 0, buffer.Length);
if (bytesRead < 0)
throw new IOException("LegacyTarWriter unable to read from provided stream");
if (bytesRead == 0)
{
if (ReadOnZero)
Thread.Sleep(100);
else
break;
}
OutStream.Write(buffer, 0, bytesRead);
count -= bytesRead;
}
if (count > 0)
{
int bytesRead = data.Read(buffer, 0, (int)count);
if (bytesRead < 0)
throw new IOException("LegacyTarWriter unable to read from provided stream");
if (bytesRead == 0)
{
while (count > 0)
{
OutStream.WriteByte(0);
--count;
}
}
else
OutStream.Write(buffer, 0, bytesRead);
}
}
protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType)
{
var header = new TarHeader
{
FileName = name,
LastModification = lastModificationTime,
SizeInBytes = count,
UserId = userId,
GroupId = groupId,
Mode = mode,
EntryType = entryType
};
OutStream.Write(header.GetHeaderValue(), 0, header.HeaderSize);
}
public void AlignTo512(long size, bool acceptZero)
{
size = size % 512;
if (size == 0 && !acceptZero) return;
while (size < 512)
{
OutStream.WriteByte(0);
size++;
}
}
public virtual void Close()
{
if (isClosed) return;
AlignTo512(0, true);
AlignTo512(0, true);
isClosed = true;
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace AMWD.Common.Packing.Tar.Utils
{
public class TarException : Exception
{
public TarException(string message) : base(message)
{
}
}
}

View File

@@ -0,0 +1,194 @@
using System;
using System.Net;
using System.Text;
using AMWD.Common.Packing.Tar.Interfaces;
namespace AMWD.Common.Packing.Tar.Utils
{
internal class TarHeader : ITarHeader
{
private readonly byte[] buffer = new byte[512];
private long headerChecksum;
public TarHeader()
{
// Default values
Mode = 511; // 0777 dec
UserId = 61; // 101 dec
GroupId = 61; // 101 dec
}
private string fileName;
protected readonly DateTime TheEpoch = new DateTime(1970, 1, 1, 0, 0, 0);
public EntryType EntryType { get; set; }
private static byte[] spaces = Encoding.ASCII.GetBytes(" ");
public virtual string FileName
{
get
{
return fileName.Replace("\0", string.Empty);
}
set
{
if (value.Length > 100)
{
throw new TarException("A file name can not be more than 100 chars long");
}
fileName = value;
}
}
public int Mode { get; set; }
public string ModeString
{
get { return Convert.ToString(Mode, 8).PadLeft(7, '0'); }
}
public int UserId { get; set; }
public virtual string UserName
{
get { return UserId.ToString(); }
set { UserId = Int32.Parse(value); }
}
public string UserIdString
{
get { return Convert.ToString(UserId, 8).PadLeft(7, '0'); }
}
public int GroupId { get; set; }
public virtual string GroupName
{
get { return GroupId.ToString(); }
set { GroupId = Int32.Parse(value); }
}
public string GroupIdString
{
get { return Convert.ToString(GroupId, 8).PadLeft(7, '0'); }
}
public long SizeInBytes { get; set; }
public string SizeString
{
get { return Convert.ToString(SizeInBytes, 8).PadLeft(11, '0'); }
}
public DateTime LastModification { get; set; }
public string LastModificationString
{
get
{
return Convert.ToString((long)(LastModification - TheEpoch).TotalSeconds, 8).PadLeft(11, '0');
}
}
public string HeaderChecksumString
{
get { return Convert.ToString(headerChecksum, 8).PadLeft(6, '0'); }
}
public virtual int HeaderSize
{
get { return 512; }
}
public byte[] GetBytes()
{
return buffer;
}
public virtual bool UpdateHeaderFromBytes()
{
FileName = Encoding.ASCII.GetString(buffer, 0, 100);
// thanks to Shasha Alperocivh. Trimming nulls.
Mode = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 100, 7).Trim(), 8);
UserId = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 108, 7).Trim(), 8);
GroupId = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 116, 7).Trim(), 8);
EntryType = (EntryType)buffer[156];
if ((buffer[124] & 0x80) == 0x80) // if size in binary
{
long sizeBigEndian = BitConverter.ToInt64(buffer, 0x80);
SizeInBytes = IPAddress.NetworkToHostOrder(sizeBigEndian);
}
else
{
SizeInBytes = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 124, 11), 8);
}
long unixTimeStamp = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 136, 11), 8);
LastModification = TheEpoch.AddSeconds(unixTimeStamp);
var storedChecksum = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 148, 6));
RecalculateChecksum(buffer);
if (storedChecksum == headerChecksum)
{
return true;
}
RecalculateAltChecksum(buffer);
return storedChecksum == headerChecksum;
}
private void RecalculateAltChecksum(byte[] buf)
{
spaces.CopyTo(buf, 148);
headerChecksum = 0;
foreach (byte b in buf)
{
if ((b & 0x80) == 0x80)
{
headerChecksum -= b ^ 0x80;
}
else
{
headerChecksum += b;
}
}
}
public virtual byte[] GetHeaderValue()
{
// Clean old values
Array.Clear(buffer, 0, buffer.Length);
if (string.IsNullOrEmpty(FileName)) throw new TarException("FileName can not be empty.");
if (FileName.Length >= 100) throw new TarException("FileName is too long. It must be less than 100 bytes.");
// Fill header
Encoding.ASCII.GetBytes(FileName.PadRight(100, '\0')).CopyTo(buffer, 0);
Encoding.ASCII.GetBytes(ModeString).CopyTo(buffer, 100);
Encoding.ASCII.GetBytes(UserIdString).CopyTo(buffer, 108);
Encoding.ASCII.GetBytes(GroupIdString).CopyTo(buffer, 116);
Encoding.ASCII.GetBytes(SizeString).CopyTo(buffer, 124);
Encoding.ASCII.GetBytes(LastModificationString).CopyTo(buffer, 136);
// buffer[156] = 20;
buffer[156] = ((byte)EntryType);
RecalculateChecksum(buffer);
// Write checksum
Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(buffer, 148);
return buffer;
}
protected virtual void RecalculateChecksum(byte[] buf)
{
// Set default value for checksum. That is 8 spaces.
spaces.CopyTo(buf, 148);
// Calculate checksum
headerChecksum = 0;
foreach (byte b in buf)
{
headerChecksum += b;
}
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Net;
using System.Text;
namespace AMWD.Common.Packing.Tar.Utils
{
/// <summary>
/// UsTar header implementation.
/// </summary>
internal class UsTarHeader : TarHeader
{
private const string magic = "ustar";
private const string version = " ";
private string groupName;
private string namePrefix = string.Empty;
private string userName;
public override string UserName
{
get
{
return userName.Replace("\0", string.Empty);
}
set
{
if (value.Length > 32)
{
throw new TarException("user name can not be longer than 32 chars");
}
userName = value;
}
}
public override string GroupName
{
get
{
return groupName.Replace("\0", string.Empty);
}
set
{
if (value.Length > 32)
{
throw new TarException("group name can not be longer than 32 chars");
}
groupName = value;
}
}
public override string FileName
{
get
{
return namePrefix.Replace("\0", string.Empty) + base.FileName.Replace("\0", string.Empty);
}
set
{
if (value.Length > 100)
{
if (value.Length > 255)
{
throw new TarException("UsTar fileName can not be longer thatn 255 chars");
}
int position = value.Length - 100;
// Find first path separator in the remaining 100 chars of the file name
while (!IsPathSeparator(value[position]))
{
++position;
if (position == value.Length)
{
break;
}
}
if (position == value.Length)
position = value.Length - 100;
namePrefix = value.Substring(0, position);
base.FileName = value.Substring(position, value.Length - position);
}
else
{
base.FileName = value;
}
}
}
public override bool UpdateHeaderFromBytes()
{
byte[] bytes = GetBytes();
UserName = Encoding.ASCII.GetString(bytes, 0x109, 32);
GroupName = Encoding.ASCII.GetString(bytes, 0x129, 32);
namePrefix = Encoding.ASCII.GetString(bytes, 347, 157);
return base.UpdateHeaderFromBytes();
}
internal static bool IsPathSeparator(char ch)
{
return (ch == '\\' || ch == '/' || ch == '|'); // All the path separators I ever met.
}
public override byte[] GetHeaderValue()
{
byte[] header = base.GetHeaderValue();
Encoding.ASCII.GetBytes(magic).CopyTo(header, 0x101); // Mark header as ustar
Encoding.ASCII.GetBytes(version).CopyTo(header, 0x106);
Encoding.ASCII.GetBytes(UserName).CopyTo(header, 0x109);
Encoding.ASCII.GetBytes(GroupName).CopyTo(header, 0x129);
Encoding.ASCII.GetBytes(namePrefix).CopyTo(header, 347);
if (SizeInBytes >= 0x1FFFFFFFF)
{
byte[] bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(SizeInBytes));
SetMarker(AlignTo12(bytes)).CopyTo(header, 124);
}
RecalculateChecksum(header);
Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(header, 148);
return header;
}
private static byte[] SetMarker(byte[] bytes)
{
bytes[0] |= 0x80;
return bytes;
}
private static byte[] AlignTo12(byte[] bytes)
{
var retVal = new byte[12];
bytes.CopyTo(retVal, 12 - bytes.Length);
return retVal;
}
}
}