WIP: Added packing of AR and TAR
This commit is contained in:
53
AMWD.Common/Packing/Ar/ArFileInfo.cs
Normal file
53
AMWD.Common/Packing/Ar/ArFileInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
180
AMWD.Common/Packing/Ar/ArReader.cs
Normal file
180
AMWD.Common/Packing/Ar/ArReader.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
147
AMWD.Common/Packing/Ar/ArWriter.cs
Normal file
147
AMWD.Common/Packing/Ar/ArWriter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user