177 lines
5.0 KiB
C#
177 lines
5.0 KiB
C#
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;
|
|
|
|
/// <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;
|
|
_inStream = inStream;
|
|
|
|
Initialize();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list with all filenames of the archive.
|
|
/// </summary>
|
|
public IEnumerable<string> GetFileList()
|
|
=> _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) // `\n
|
|
throw new FormatException("Invalid file magic");
|
|
|
|
return new ArFileInfoExtended
|
|
{
|
|
HeaderPosition = startPosition,
|
|
DataPosition = _inStream.Position,
|
|
FileName = fileName,
|
|
ModifyTime = DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).DateTime,
|
|
UserId = userId,
|
|
GroupId = groupId,
|
|
Mode = mode,
|
|
FileSize = fileSize
|
|
};
|
|
}
|
|
}
|
|
}
|