using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace AMWD.Common.Packing.Ar
{
///
/// Reads UNIX ar (archive) files in the GNU format.
///
public class ArReader
{
// Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29
private readonly Stream inStream;
private readonly List files = new();
private readonly long streamStartPosition;
private static readonly DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
///
/// Initializes a new instance of the class.
///
/// The stream to read the archive from.
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();
}
///
/// Returns a list with all filenames of the archive.
///
public IEnumerable GetFileList()
{
return files.Select(fi => fi.FileName).ToList();
}
///
/// Returns the file info of a specific file in the archive.
///
/// The name of the specific file.
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();
}
///
/// Reads a file from the archive into a stream.
///
/// The file name in the archive.
/// The output stream.
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);
}
///
/// Reads a fie from the archive and saves it to disk.
///
/// The file name in the archive.
/// The destination path on disk.
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 != "!\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) // `\n
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
};
}
}
}