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