1
0
Files
common/AMWD.Common/Packing/Tar/TarReader.cs
2024-01-14 13:10:33 +01:00

206 lines
6.0 KiB
C#

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using AMWD.Common.Packing.Tar.Interfaces;
using AMWD.Common.Packing.Tar.Utils;
namespace AMWD.Common.Packing.Tar
{
/// <summary>
/// Extract contents of a tar file represented by a stream for the TarReader constructor.
/// </summary>
/// <remarks>
/// Constructs TarReader object to read data from `tarredData` stream.
/// <br />
/// Copied from: <see href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarReader.cs">DotnetMakeDeb</see>
/// </remarks>
/// <param name="tarredData">A stream to read tar archive from</param>
public class TarReader(Stream tarredData)
{
private readonly byte[] _dataBuffer = new byte[512];
private readonly UsTarHeader _header = new();
private readonly Stream _inStream = tarredData;
private long _remainingBytesInFile;
/// <summary>
/// Gets the file info (the header).
/// </summary>
public ITarHeader FileInfo => _header;
/// <summary>
/// Read all files from an archive to a directory. It creates some child directories to
/// reproduce a file structure from the archive.
/// </summary>
/// <param name="destDirectory">The out directory.</param>
///
/// CAUTION! This method is not safe. It's not tar-bomb proof.
/// {see http://en.wikipedia.org/wiki/Tar_(file_format) }
/// If you are not sure about the source of an archive you extracting,
/// then use MoveNext and Read and handle paths like ".." and "../.." according
/// to your business logic.
public void ReadToEnd(string destDirectory)
{
while (MoveNext(skipData: false))
{
string fileNameFromArchive = FileInfo.FileName;
string totalPath = destDirectory + Path.DirectorySeparatorChar + fileNameFromArchive;
if (UsTarHeader.IsPathSeparator(fileNameFromArchive[fileNameFromArchive.Length - 1]) || FileInfo.EntryType == EntryType.Directory)
{
// Record is a directory
Directory.CreateDirectory(totalPath);
continue;
}
// If record is a file
string fileName = Path.GetFileName(totalPath);
string directory = totalPath.Remove(totalPath.Length - fileName.Length);
Directory.CreateDirectory(directory);
using FileStream file = File.Create(totalPath);
Read(file);
}
}
/// <summary>
/// Read data from the current archive to a Stream.
/// </summary>
/// <param name="dataDestanation">A stream to read data to</param>
/// <seealso cref="MoveNext"/>
public void Read(Stream dataDestanation)
{
Debug.WriteLine("tar stream position Read in: " + _inStream.Position);
int readBytes;
while ((readBytes = Read(out byte[] read)) != -1)
{
Debug.WriteLine("tar stream position Read while(...) : " + _inStream.Position);
dataDestanation.Write(read, 0, readBytes);
}
Debug.WriteLine("tar stream position Read out: " + _inStream.Position);
}
/// <summary>
/// Reads data from the current archive to a buffer array.
/// </summary>
/// <param name="buffer">The buffer array.</param>
/// <returns>The nuber of bytes read.</returns>
protected int Read(out byte[] buffer)
{
if (_remainingBytesInFile == 0)
{
buffer = null;
return -1;
}
int align512 = -1;
long toRead = _remainingBytesInFile - 512;
if (toRead > 0)
{
toRead = 512;
}
else
{
align512 = 512 - (int)_remainingBytesInFile;
toRead = _remainingBytesInFile;
}
int bytesRead = _inStream.Read(_dataBuffer, 0, (int)toRead);
_remainingBytesInFile -= bytesRead;
if (_inStream.CanSeek && align512 > 0)
{
_inStream.Seek(align512, SeekOrigin.Current);
}
else
while (align512 > 0)
{
_inStream.ReadByte();
--align512;
}
buffer = _dataBuffer;
return bytesRead;
}
/// <summary>
/// Check if all bytes in buffer are zeroes
/// </summary>
/// <param name="buffer">buffer to check</param>
/// <returns>true if all bytes are zeroes, otherwise false</returns>
private static bool IsEmpty(IEnumerable<byte> buffer)
{
foreach (byte b in buffer)
{
if (b != 0)
return false;
}
return true;
}
/// <summary>
/// Move internal pointer to a next file in archive.
/// </summary>
/// <param name="skipData">Should be true if you want to read a header only, otherwise false</param>
/// <returns>false on End Of File otherwise true</returns>
///
/// Example:
/// while(MoveNext())
/// {
/// Read(dataDestStream);
/// }
/// <seealso cref="Read(Stream)"/>
public bool MoveNext(bool skipData)
{
Debug.WriteLine("tar stream position MoveNext in: " + _inStream.Position);
if (_remainingBytesInFile > 0)
{
if (!skipData)
{
throw new TarException(
"You are trying to change file while not all the data from the previous one was read. If you do want to skip files use skipData parameter set to true.");
}
// Skip to the end of file.
if (_inStream.CanSeek)
{
long remainer = _remainingBytesInFile % 512;
_inStream.Seek(_remainingBytesInFile + (512 - (remainer == 0 ? 512 : remainer)), SeekOrigin.Current);
}
else
{
while (Read(out _) != -1) ;
}
}
byte[] bytes = _header.GetBytes();
int headerRead = _inStream.Read(bytes, 0, _header.HeaderSize);
if (headerRead < 512)
throw new TarException("Can not read header");
if (IsEmpty(bytes))
{
headerRead = _inStream.Read(bytes, 0, _header.HeaderSize);
if (headerRead == 512 && IsEmpty(bytes))
{
Debug.WriteLine("tar stream position MoveNext out(false): " + _inStream.Position);
return false;
}
throw new TarException("Broken archive");
}
if (_header.UpdateHeaderFromBytes())
{
throw new TarException("Checksum check failed");
}
_remainingBytesInFile = _header.SizeInBytes;
Debug.WriteLine("tar stream position MoveNext out(true): " + _inStream.Position);
return true;
}
}
}