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,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;
}
}
}