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);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs
Normal file
26
AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace AMWD.Common.Packing.Tar.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface of a archive writer.
|
||||
/// </summary>
|
||||
public interface IArchiveDataWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Write <paramref name="count"/> bytes of data from <paramref name="buffer"/> to corresponding archive.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The data storage.</param>
|
||||
/// <param name="count">How many bytes to be written to the corresponding archive.</param>
|
||||
int Write(byte[] buffer, int count);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the writer can write.
|
||||
/// </summary>
|
||||
bool CanWrite { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The writer delegate.
|
||||
/// </summary>
|
||||
/// <param name="writer">The writer.</param>
|
||||
public delegate void WriteDataDelegate(IArchiveDataWriter writer);
|
||||
}
|
||||
125
AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs
Normal file
125
AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using AMWD.Common.Packing.Tar.Utils;
|
||||
|
||||
namespace AMWD.Common.Packing.Tar.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// See "struct star_header" in <a href="https://www.gnu.org/software/tar/manual/html_node/Standard.html" />
|
||||
/// </summary>
|
||||
public interface ITarHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// The name field is the file name of the file, with directory names (if any) preceding the file name,
|
||||
/// separated by slashes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>name</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>0</c>
|
||||
/// </remarks>
|
||||
string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The mode field provides nine bits specifying file permissions and three bits to specify
|
||||
/// the Set UID, Set GID, and Save Text (sticky) modes.
|
||||
/// When special permissions are required to create a file with a given mode,
|
||||
/// and the user restoring files from the archive does not hold such permissions,
|
||||
/// the mode bit(s) specifying those special permissions are ignored.
|
||||
/// Modes which are not supported by the operating system restoring files from the archive will be ignored.
|
||||
/// Unsupported modes should be faked up when creating or updating an archive; e.g.,
|
||||
/// the group permission could be copied from the other permission.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>mode</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>100</c>
|
||||
/// </remarks>
|
||||
int Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The uid field is the numeric user ID of the file owners.
|
||||
/// If the operating system does not support numeric user ID, this field should be ignored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>uid</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>108</c>
|
||||
/// </remarks>
|
||||
int UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The gid fields is the numeric group ID of the file owners.
|
||||
/// If the operating system does not support numeric group ID, this field should be ignored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>gid</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>116</c>
|
||||
/// </remarks>
|
||||
int GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size field is the size of the file in bytes;
|
||||
/// linked files are archived with this field specified as zero.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>size</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>124</c>
|
||||
/// </remarks>
|
||||
long SizeInBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>mtime</para>
|
||||
/// <para>byte offset: 136</para>
|
||||
/// The mtime field represents the data modification time of the file at the time it was archived.
|
||||
/// It represents the integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>mtime</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>136</c>
|
||||
/// </remarks>
|
||||
DateTime LastModification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The typeflag field specifies the type of file archived.
|
||||
/// If a particular implementation does not recognize or permit the specified type,
|
||||
/// the file will be extracted as if it were a regular file.
|
||||
/// As this action occurs, tar issues a warning to the standard error.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>typeflag</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>156</c>
|
||||
/// </remarks>
|
||||
EntryType EntryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The uname field will contain the ASCII representation of the owner of the file.
|
||||
/// If found, the user ID is used rather than the value in the uid field.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>uname</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>265</c>
|
||||
/// </remarks>
|
||||
string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The gname field will contain the ASCII representation of the group of the file.
|
||||
/// If found, the group ID is used rather than the values in the gid field.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>gname</c>
|
||||
/// <br/>
|
||||
/// Byte offset: <c>297</c>
|
||||
/// </remarks>
|
||||
string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of this header.
|
||||
/// </summary>
|
||||
int HeaderSize { get; }
|
||||
}
|
||||
}
|
||||
210
AMWD.Common/Packing/Tar/TarReader.cs
Normal file
210
AMWD.Common/Packing/Tar/TarReader.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
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>
|
||||
/// https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarReader.cs
|
||||
/// </remarks>
|
||||
public class TarReader
|
||||
{
|
||||
private readonly byte[] dataBuffer = new byte[512];
|
||||
private readonly UsTarHeader header;
|
||||
private readonly Stream inStream;
|
||||
private long remainingBytesInFile;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs TarReader object to read data from `tarredData` stream
|
||||
/// </summary>
|
||||
/// <param name="tarredData">A stream to read tar archive from</param>
|
||||
public TarReader(Stream tarredData)
|
||||
{
|
||||
inStream = tarredData;
|
||||
header = new UsTarHeader();
|
||||
}
|
||||
|
||||
public ITarHeader FileInfo
|
||||
{
|
||||
get { return 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(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 a current file 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;
|
||||
byte[] read;
|
||||
while ((readBytes = Read(out 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);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
byte[] buffer;
|
||||
while (Read(out buffer) != -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
AMWD.Common/Packing/Tar/TarWriter.cs
Normal file
63
AMWD.Common/Packing/Tar/TarWriter.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using AMWD.Common.Packing.Tar.Interfaces;
|
||||
using AMWD.Common.Packing.Tar.Utils;
|
||||
|
||||
namespace AMWD.Common.Packing.Tar
|
||||
{
|
||||
// https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarWriter.cs
|
||||
public class TarWriter : LegacyTarWriter
|
||||
{
|
||||
public TarWriter(Stream outStream) : base(outStream)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType)
|
||||
{
|
||||
var tarHeader = new UsTarHeader()
|
||||
{
|
||||
FileName = name,
|
||||
Mode = mode,
|
||||
UserId = userId,
|
||||
GroupId = groupId,
|
||||
SizeInBytes = count,
|
||||
LastModification = lastModificationTime,
|
||||
EntryType = entryType,
|
||||
UserName = Convert.ToString(userId, 8),
|
||||
GroupName = Convert.ToString(groupId, 8)
|
||||
};
|
||||
OutStream.Write(tarHeader.GetHeaderValue(), 0, tarHeader.HeaderSize);
|
||||
}
|
||||
|
||||
protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, string userName, string groupName, int mode, EntryType entryType)
|
||||
{
|
||||
WriteHeader(
|
||||
name: name,
|
||||
lastModificationTime: lastModificationTime,
|
||||
count: count,
|
||||
userId: userName.GetHashCode(),
|
||||
groupId: groupName.GetHashCode(),
|
||||
mode: mode,
|
||||
entryType: entryType);
|
||||
}
|
||||
|
||||
public virtual void Write(string name, long dataSizeInBytes, string userName, string groupName, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate)
|
||||
{
|
||||
var writer = new DataWriter(OutStream, dataSizeInBytes);
|
||||
WriteHeader(name, lastModificationTime, dataSizeInBytes, userName, groupName, mode, EntryType.File);
|
||||
while (writer.CanWrite)
|
||||
{
|
||||
writeDelegate(writer);
|
||||
}
|
||||
AlignTo512(dataSizeInBytes, false);
|
||||
}
|
||||
|
||||
public void Write(Stream data, long dataSizeInBytes, string fileName, string userId, string groupId, int mode,
|
||||
DateTime lastModificationTime)
|
||||
{
|
||||
WriteHeader(fileName, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
|
||||
WriteContent(dataSizeInBytes, data);
|
||||
AlignTo512(dataSizeInBytes, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
AMWD.Common/Packing/Tar/Utils/DataWriter.cs
Normal file
45
AMWD.Common/Packing/Tar/Utils/DataWriter.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.IO;
|
||||
using AMWD.Common.Packing.Tar.Interfaces;
|
||||
|
||||
namespace AMWD.Common.Packing.Tar.Utils
|
||||
{
|
||||
internal class DataWriter : IArchiveDataWriter
|
||||
{
|
||||
private readonly long size;
|
||||
private long remainingBytes;
|
||||
private readonly Stream stream;
|
||||
|
||||
public DataWriter(Stream data, long dataSizeInBytes)
|
||||
{
|
||||
size = dataSizeInBytes;
|
||||
remainingBytes = size;
|
||||
stream = data;
|
||||
}
|
||||
|
||||
public bool CanWrite { get; private set; } = true;
|
||||
|
||||
public int Write(byte[] buffer, int count)
|
||||
{
|
||||
if (remainingBytes == 0)
|
||||
{
|
||||
CanWrite = false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int bytesToWrite;
|
||||
if (remainingBytes - count < 0)
|
||||
{
|
||||
bytesToWrite = (int)remainingBytes;
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesToWrite = count;
|
||||
}
|
||||
|
||||
stream.Write(buffer, 0, bytesToWrite);
|
||||
remainingBytes -= bytesToWrite;
|
||||
|
||||
return bytesToWrite;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
AMWD.Common/Packing/Tar/Utils/EntryType.cs
Normal file
73
AMWD.Common/Packing/Tar/Utils/EntryType.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
namespace AMWD.Common.Packing.Tar.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// See "Values used in typeflag field." in <a href="https://www.gnu.org/software/tar/manual/html_node/Standard.html" />
|
||||
/// </summary>
|
||||
public enum EntryType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// AREGTYPE, regular file
|
||||
/// </summary>
|
||||
File = 0,
|
||||
|
||||
/// <summary>
|
||||
/// REGTYPE, regular file
|
||||
/// </summary>
|
||||
FileObsolete = 0x30,
|
||||
|
||||
/// <summary>
|
||||
/// LNKTYPE, link
|
||||
/// </summary>
|
||||
HardLink = 0x31,
|
||||
|
||||
/// <summary>
|
||||
/// SYMTYPE, reserved
|
||||
/// </summary>
|
||||
SymLink = 0x32,
|
||||
|
||||
/// <summary>
|
||||
/// CHRTYPE, character special
|
||||
/// </summary>
|
||||
CharDevice = 0x33,
|
||||
|
||||
/// <summary>
|
||||
/// BLKTYPE, block special
|
||||
/// </summary>
|
||||
BlockDevice = 0x34,
|
||||
|
||||
/// <summary>
|
||||
/// DIRTYPE, directory
|
||||
/// </summary>
|
||||
Directory = 0x35,
|
||||
|
||||
/// <summary>
|
||||
/// FIFOTYPE, FIFO special
|
||||
/// </summary>
|
||||
Fifo = 0x36,
|
||||
|
||||
/// <summary>
|
||||
/// CONTTYPE, reserved
|
||||
/// </summary>
|
||||
Content = 0x37,
|
||||
|
||||
/// <summary>
|
||||
/// XHDTYPE, Extended header referring to the next file in the archive
|
||||
/// </summary>
|
||||
ExtendedHeader = 0x78,
|
||||
|
||||
/// <summary>
|
||||
/// XGLTYPE, Global extended header
|
||||
/// </summary>
|
||||
GlobalExtendedHeader = 0x67,
|
||||
|
||||
/// <summary>
|
||||
/// GNUTYPE_LONGLINK, Identifies the *next* file on the tape as having a long linkname.
|
||||
/// </summary>
|
||||
LongLink = 0x4b,
|
||||
|
||||
/// <summary>
|
||||
/// GNUTYPE_LONGNAME, Identifies the *next* file on the tape as having a long name.
|
||||
/// </summary>
|
||||
LongName = 0x4c
|
||||
}
|
||||
}
|
||||
258
AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs
Normal file
258
AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using AMWD.Common.Packing.Tar.Interfaces;
|
||||
|
||||
namespace AMWD.Common.Packing.Tar.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a legacy TAR writer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Copied from <a href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/LegacyTarWriter.cs" />
|
||||
/// </remarks>
|
||||
public class LegacyTarWriter : IDisposable
|
||||
{
|
||||
private readonly Stream outStream;
|
||||
protected byte[] buffer = new byte[1024];
|
||||
private bool isClosed;
|
||||
public bool ReadOnZero = true;
|
||||
|
||||
/// <summary>
|
||||
/// Writes tar (see GNU tar) archive to a stream
|
||||
/// </summary>
|
||||
/// <param name="outStream">stream to write archive to</param>
|
||||
public LegacyTarWriter(Stream outStream)
|
||||
{
|
||||
this.outStream = outStream;
|
||||
}
|
||||
|
||||
protected virtual Stream OutStream
|
||||
{
|
||||
get { return outStream; }
|
||||
}
|
||||
|
||||
#region IDisposable Members
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
#endregion IDisposable Members
|
||||
|
||||
public void WriteDirectoryEntry(string path, int userId, int groupId, int mode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentNullException("path");
|
||||
if (path[path.Length - 1] != '/')
|
||||
{
|
||||
path += '/';
|
||||
}
|
||||
DateTime lastWriteTime;
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
lastWriteTime = Directory.GetLastWriteTime(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
lastWriteTime = DateTime.Now;
|
||||
}
|
||||
|
||||
// handle long path names (> 99 characters)
|
||||
if (path.Length > 99)
|
||||
{
|
||||
WriteLongName(
|
||||
name: path,
|
||||
userId: userId,
|
||||
groupId: groupId,
|
||||
mode: mode,
|
||||
lastModificationTime: lastWriteTime);
|
||||
|
||||
// shorten the path name so it can be written properly
|
||||
path = path.Substring(0, 99);
|
||||
}
|
||||
|
||||
WriteHeader(path, lastWriteTime, 0, userId, groupId, mode, EntryType.Directory);
|
||||
}
|
||||
|
||||
public void WriteDirectory(string directory, bool doRecursive)
|
||||
{
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
throw new ArgumentNullException("directory");
|
||||
|
||||
WriteDirectoryEntry(directory, 0, 0, 0755);
|
||||
|
||||
string[] files = Directory.GetFiles(directory);
|
||||
foreach (var fileName in files)
|
||||
{
|
||||
Write(fileName);
|
||||
}
|
||||
|
||||
string[] directories = Directory.GetDirectories(directory);
|
||||
foreach (var dirName in directories)
|
||||
{
|
||||
WriteDirectoryEntry(dirName, 0, 0, 0755);
|
||||
if (doRecursive)
|
||||
{
|
||||
WriteDirectory(dirName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
throw new ArgumentNullException("fileName");
|
||||
using (FileStream file = File.OpenRead(fileName))
|
||||
{
|
||||
Write(file, file.Length, fileName, 61, 61, 511, File.GetLastWriteTime(file.Name));
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(FileStream file)
|
||||
{
|
||||
string path = Path.GetFullPath(file.Name).Replace(Path.GetPathRoot(file.Name), string.Empty);
|
||||
path = path.Replace(Path.DirectorySeparatorChar, '/');
|
||||
Write(file, file.Length, path, 61, 61, 511, File.GetLastWriteTime(file.Name));
|
||||
}
|
||||
|
||||
public void Write(Stream data, long dataSizeInBytes, string name)
|
||||
{
|
||||
Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now);
|
||||
}
|
||||
|
||||
public virtual void Write(string name, long dataSizeInBytes, int userId, int groupId, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate)
|
||||
{
|
||||
IArchiveDataWriter writer = new DataWriter(OutStream, dataSizeInBytes);
|
||||
WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
|
||||
while (writer.CanWrite)
|
||||
{
|
||||
writeDelegate(writer);
|
||||
}
|
||||
AlignTo512(dataSizeInBytes, false);
|
||||
}
|
||||
|
||||
public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode,
|
||||
DateTime lastModificationTime)
|
||||
{
|
||||
if (isClosed)
|
||||
throw new TarException("Can not write to the closed writer");
|
||||
|
||||
// handle long file names (> 99 characters)
|
||||
if (name.Length > 99)
|
||||
{
|
||||
WriteLongName(
|
||||
name: name,
|
||||
userId: userId,
|
||||
groupId: groupId,
|
||||
mode: mode,
|
||||
lastModificationTime: lastModificationTime);
|
||||
|
||||
// shorten the file name so it can be written properly
|
||||
name = name.Substring(0, 99);
|
||||
}
|
||||
|
||||
WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File);
|
||||
WriteContent(dataSizeInBytes, data);
|
||||
AlignTo512(dataSizeInBytes, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle long file or path names (> 99 characters).
|
||||
/// Write header and content, which its content contain the long (complete) file/path name.
|
||||
/// <para>This handling method is adapted from https://github.com/qmfrederik/dotnet-packaging/pull/50/files#diff-f64c58cc18e8e445cee6ffed7a0d765cdb442c0ef21a3ed80bd20514057967b1 </para>
|
||||
/// </summary>
|
||||
/// <param name="name">File name or path name.</param>
|
||||
/// <param name="userId">User ID.</param>
|
||||
/// <param name="groupId">Group ID.</param>
|
||||
/// <param name="mode">Mode.</param>
|
||||
/// <param name="lastModificationTime">Last modification time.</param>
|
||||
private void WriteLongName(string name, int userId, int groupId, int mode, DateTime lastModificationTime)
|
||||
{
|
||||
// must include a trailing \0
|
||||
var nameLength = Encoding.UTF8.GetByteCount(name);
|
||||
byte[] entryName = new byte[nameLength + 1];
|
||||
|
||||
Encoding.UTF8.GetBytes(name, 0, name.Length, entryName, 0);
|
||||
|
||||
// add a "././@LongLink" pseudo-entry which contains the full name
|
||||
using (var nameStream = new MemoryStream(entryName))
|
||||
{
|
||||
WriteHeader("././@LongLink", lastModificationTime, entryName.Length, userId, groupId, mode, EntryType.LongName);
|
||||
WriteContent(entryName.Length, nameStream);
|
||||
AlignTo512(entryName.Length, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected void WriteContent(long count, Stream data)
|
||||
{
|
||||
while (count > 0 && count > buffer.Length)
|
||||
{
|
||||
int bytesRead = data.Read(buffer, 0, buffer.Length);
|
||||
if (bytesRead < 0)
|
||||
throw new IOException("LegacyTarWriter unable to read from provided stream");
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
if (ReadOnZero)
|
||||
Thread.Sleep(100);
|
||||
else
|
||||
break;
|
||||
}
|
||||
OutStream.Write(buffer, 0, bytesRead);
|
||||
count -= bytesRead;
|
||||
}
|
||||
if (count > 0)
|
||||
{
|
||||
int bytesRead = data.Read(buffer, 0, (int)count);
|
||||
if (bytesRead < 0)
|
||||
throw new IOException("LegacyTarWriter unable to read from provided stream");
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
while (count > 0)
|
||||
{
|
||||
OutStream.WriteByte(0);
|
||||
--count;
|
||||
}
|
||||
}
|
||||
else
|
||||
OutStream.Write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType)
|
||||
{
|
||||
var header = new TarHeader
|
||||
{
|
||||
FileName = name,
|
||||
LastModification = lastModificationTime,
|
||||
SizeInBytes = count,
|
||||
UserId = userId,
|
||||
GroupId = groupId,
|
||||
Mode = mode,
|
||||
EntryType = entryType
|
||||
};
|
||||
OutStream.Write(header.GetHeaderValue(), 0, header.HeaderSize);
|
||||
}
|
||||
|
||||
public void AlignTo512(long size, bool acceptZero)
|
||||
{
|
||||
size = size % 512;
|
||||
if (size == 0 && !acceptZero) return;
|
||||
while (size < 512)
|
||||
{
|
||||
OutStream.WriteByte(0);
|
||||
size++;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Close()
|
||||
{
|
||||
if (isClosed) return;
|
||||
AlignTo512(0, true);
|
||||
AlignTo512(0, true);
|
||||
isClosed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
AMWD.Common/Packing/Tar/Utils/TarException.cs
Normal file
13
AMWD.Common/Packing/Tar/Utils/TarException.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Common.Packing.Tar.Utils
|
||||
{
|
||||
public class TarException : Exception
|
||||
{
|
||||
public TarException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
194
AMWD.Common/Packing/Tar/Utils/TarHeader.cs
Normal file
194
AMWD.Common/Packing/Tar/Utils/TarHeader.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using AMWD.Common.Packing.Tar.Interfaces;
|
||||
|
||||
namespace AMWD.Common.Packing.Tar.Utils
|
||||
{
|
||||
internal class TarHeader : ITarHeader
|
||||
{
|
||||
private readonly byte[] buffer = new byte[512];
|
||||
private long headerChecksum;
|
||||
|
||||
public TarHeader()
|
||||
{
|
||||
// Default values
|
||||
Mode = 511; // 0777 dec
|
||||
UserId = 61; // 101 dec
|
||||
GroupId = 61; // 101 dec
|
||||
}
|
||||
|
||||
private string fileName;
|
||||
protected readonly DateTime TheEpoch = new DateTime(1970, 1, 1, 0, 0, 0);
|
||||
public EntryType EntryType { get; set; }
|
||||
private static byte[] spaces = Encoding.ASCII.GetBytes(" ");
|
||||
|
||||
public virtual string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
return fileName.Replace("\0", string.Empty);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.Length > 100)
|
||||
{
|
||||
throw new TarException("A file name can not be more than 100 chars long");
|
||||
}
|
||||
fileName = value;
|
||||
}
|
||||
}
|
||||
public int Mode { get; set; }
|
||||
|
||||
public string ModeString
|
||||
{
|
||||
get { return Convert.ToString(Mode, 8).PadLeft(7, '0'); }
|
||||
}
|
||||
|
||||
public int UserId { get; set; }
|
||||
public virtual string UserName
|
||||
{
|
||||
get { return UserId.ToString(); }
|
||||
set { UserId = Int32.Parse(value); }
|
||||
}
|
||||
|
||||
public string UserIdString
|
||||
{
|
||||
get { return Convert.ToString(UserId, 8).PadLeft(7, '0'); }
|
||||
}
|
||||
|
||||
public int GroupId { get; set; }
|
||||
public virtual string GroupName
|
||||
{
|
||||
get { return GroupId.ToString(); }
|
||||
set { GroupId = Int32.Parse(value); }
|
||||
}
|
||||
|
||||
public string GroupIdString
|
||||
{
|
||||
get { return Convert.ToString(GroupId, 8).PadLeft(7, '0'); }
|
||||
}
|
||||
|
||||
public long SizeInBytes { get; set; }
|
||||
|
||||
public string SizeString
|
||||
{
|
||||
get { return Convert.ToString(SizeInBytes, 8).PadLeft(11, '0'); }
|
||||
}
|
||||
|
||||
public DateTime LastModification { get; set; }
|
||||
|
||||
public string LastModificationString
|
||||
{
|
||||
get
|
||||
{
|
||||
return Convert.ToString((long)(LastModification - TheEpoch).TotalSeconds, 8).PadLeft(11, '0');
|
||||
}
|
||||
}
|
||||
|
||||
public string HeaderChecksumString
|
||||
{
|
||||
get { return Convert.ToString(headerChecksum, 8).PadLeft(6, '0'); }
|
||||
}
|
||||
|
||||
public virtual int HeaderSize
|
||||
{
|
||||
get { return 512; }
|
||||
}
|
||||
|
||||
public byte[] GetBytes()
|
||||
{
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public virtual bool UpdateHeaderFromBytes()
|
||||
{
|
||||
FileName = Encoding.ASCII.GetString(buffer, 0, 100);
|
||||
// thanks to Shasha Alperocivh. Trimming nulls.
|
||||
Mode = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 100, 7).Trim(), 8);
|
||||
UserId = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 108, 7).Trim(), 8);
|
||||
GroupId = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 116, 7).Trim(), 8);
|
||||
|
||||
EntryType = (EntryType)buffer[156];
|
||||
|
||||
if ((buffer[124] & 0x80) == 0x80) // if size in binary
|
||||
{
|
||||
long sizeBigEndian = BitConverter.ToInt64(buffer, 0x80);
|
||||
SizeInBytes = IPAddress.NetworkToHostOrder(sizeBigEndian);
|
||||
}
|
||||
else
|
||||
{
|
||||
SizeInBytes = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 124, 11), 8);
|
||||
}
|
||||
long unixTimeStamp = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 136, 11), 8);
|
||||
LastModification = TheEpoch.AddSeconds(unixTimeStamp);
|
||||
|
||||
var storedChecksum = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 148, 6));
|
||||
RecalculateChecksum(buffer);
|
||||
if (storedChecksum == headerChecksum)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
RecalculateAltChecksum(buffer);
|
||||
return storedChecksum == headerChecksum;
|
||||
}
|
||||
|
||||
private void RecalculateAltChecksum(byte[] buf)
|
||||
{
|
||||
spaces.CopyTo(buf, 148);
|
||||
headerChecksum = 0;
|
||||
foreach (byte b in buf)
|
||||
{
|
||||
if ((b & 0x80) == 0x80)
|
||||
{
|
||||
headerChecksum -= b ^ 0x80;
|
||||
}
|
||||
else
|
||||
{
|
||||
headerChecksum += b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual byte[] GetHeaderValue()
|
||||
{
|
||||
// Clean old values
|
||||
Array.Clear(buffer, 0, buffer.Length);
|
||||
|
||||
if (string.IsNullOrEmpty(FileName)) throw new TarException("FileName can not be empty.");
|
||||
if (FileName.Length >= 100) throw new TarException("FileName is too long. It must be less than 100 bytes.");
|
||||
|
||||
// Fill header
|
||||
Encoding.ASCII.GetBytes(FileName.PadRight(100, '\0')).CopyTo(buffer, 0);
|
||||
Encoding.ASCII.GetBytes(ModeString).CopyTo(buffer, 100);
|
||||
Encoding.ASCII.GetBytes(UserIdString).CopyTo(buffer, 108);
|
||||
Encoding.ASCII.GetBytes(GroupIdString).CopyTo(buffer, 116);
|
||||
Encoding.ASCII.GetBytes(SizeString).CopyTo(buffer, 124);
|
||||
Encoding.ASCII.GetBytes(LastModificationString).CopyTo(buffer, 136);
|
||||
|
||||
// buffer[156] = 20;
|
||||
buffer[156] = ((byte)EntryType);
|
||||
|
||||
RecalculateChecksum(buffer);
|
||||
|
||||
// Write checksum
|
||||
Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(buffer, 148);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
protected virtual void RecalculateChecksum(byte[] buf)
|
||||
{
|
||||
// Set default value for checksum. That is 8 spaces.
|
||||
spaces.CopyTo(buf, 148);
|
||||
|
||||
// Calculate checksum
|
||||
headerChecksum = 0;
|
||||
foreach (byte b in buf)
|
||||
{
|
||||
headerChecksum += b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs
Normal file
136
AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Common.Packing.Tar.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// UsTar header implementation.
|
||||
/// </summary>
|
||||
internal class UsTarHeader : TarHeader
|
||||
{
|
||||
private const string magic = "ustar";
|
||||
private const string version = " ";
|
||||
private string groupName;
|
||||
|
||||
private string namePrefix = string.Empty;
|
||||
private string userName;
|
||||
|
||||
public override string UserName
|
||||
{
|
||||
get
|
||||
{
|
||||
return userName.Replace("\0", string.Empty);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.Length > 32)
|
||||
{
|
||||
throw new TarException("user name can not be longer than 32 chars");
|
||||
}
|
||||
userName = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string GroupName
|
||||
{
|
||||
get
|
||||
{
|
||||
return groupName.Replace("\0", string.Empty);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.Length > 32)
|
||||
{
|
||||
throw new TarException("group name can not be longer than 32 chars");
|
||||
}
|
||||
groupName = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
return namePrefix.Replace("\0", string.Empty) + base.FileName.Replace("\0", string.Empty);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.Length > 100)
|
||||
{
|
||||
if (value.Length > 255)
|
||||
{
|
||||
throw new TarException("UsTar fileName can not be longer thatn 255 chars");
|
||||
}
|
||||
int position = value.Length - 100;
|
||||
|
||||
// Find first path separator in the remaining 100 chars of the file name
|
||||
while (!IsPathSeparator(value[position]))
|
||||
{
|
||||
++position;
|
||||
if (position == value.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (position == value.Length)
|
||||
position = value.Length - 100;
|
||||
namePrefix = value.Substring(0, position);
|
||||
base.FileName = value.Substring(position, value.Length - position);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.FileName = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool UpdateHeaderFromBytes()
|
||||
{
|
||||
byte[] bytes = GetBytes();
|
||||
UserName = Encoding.ASCII.GetString(bytes, 0x109, 32);
|
||||
GroupName = Encoding.ASCII.GetString(bytes, 0x129, 32);
|
||||
namePrefix = Encoding.ASCII.GetString(bytes, 347, 157);
|
||||
return base.UpdateHeaderFromBytes();
|
||||
}
|
||||
|
||||
internal static bool IsPathSeparator(char ch)
|
||||
{
|
||||
return (ch == '\\' || ch == '/' || ch == '|'); // All the path separators I ever met.
|
||||
}
|
||||
|
||||
public override byte[] GetHeaderValue()
|
||||
{
|
||||
byte[] header = base.GetHeaderValue();
|
||||
|
||||
Encoding.ASCII.GetBytes(magic).CopyTo(header, 0x101); // Mark header as ustar
|
||||
Encoding.ASCII.GetBytes(version).CopyTo(header, 0x106);
|
||||
Encoding.ASCII.GetBytes(UserName).CopyTo(header, 0x109);
|
||||
Encoding.ASCII.GetBytes(GroupName).CopyTo(header, 0x129);
|
||||
Encoding.ASCII.GetBytes(namePrefix).CopyTo(header, 347);
|
||||
|
||||
if (SizeInBytes >= 0x1FFFFFFFF)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(SizeInBytes));
|
||||
SetMarker(AlignTo12(bytes)).CopyTo(header, 124);
|
||||
}
|
||||
|
||||
RecalculateChecksum(header);
|
||||
Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(header, 148);
|
||||
return header;
|
||||
}
|
||||
|
||||
private static byte[] SetMarker(byte[] bytes)
|
||||
{
|
||||
bytes[0] |= 0x80;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] AlignTo12(byte[] bytes)
|
||||
{
|
||||
var retVal = new byte[12];
|
||||
bytes.CopyTo(retVal, 12 - bytes.Length);
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Added `directory.build.props` and `directory.build.targets`
|
||||
- Added `ArReader` and `ArWriter` for Unix archives
|
||||
- Added `TarReader` and `TarWriter` for TAR archives
|
||||
|
||||
|
||||
## [v1.10.0](https://git.am-wd.de/AM.WD/common/compare/v1.9.0...v1.10.0) - 2022-09-18
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<NrtRevisionFormat>{semvertag:main}</NrtRevisionFormat>
|
||||
<NrtRevisionFormat>{semvertag:main}{!:-mod}</NrtRevisionFormat>
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<Project>
|
||||
<Target Condition="'$(Configuration)' == 'DebugLocal'" Name="MovePackageToLocal" AfterTargets="Pack">
|
||||
<Target Condition="'$(Configuration)' == 'DebugLocal'" Name="CopyPackageToLocal" AfterTargets="Pack">
|
||||
<Delete Files="D:\NuGetLocal\$(PackageId).$(PackageVersion).nupkg" />
|
||||
<Delete Files="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).snupkg" />
|
||||
<Delete Files="D:\NuGetLocal\$(PackageId).$(PackageVersion).snupkg" />
|
||||
|
||||
<Move SourceFiles="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).nupkg" DestinationFolder="D:\NuGetLocal" />
|
||||
<Copy SourceFiles="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).nupkg" DestinationFolder="D:\NuGetLocal" />
|
||||
<Copy SourceFiles="$(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).snupkg" DestinationFolder="D:\NuGetLocal" />
|
||||
</Target>
|
||||
|
||||
<Target Condition="'$(Configuration)' == 'DebugLocal'" Name="PushToLocalFeed" AfterTargets="Pack">
|
||||
<Exec Command="dotnet nuget push -s https://nuget.syshorst.de/v3/index.json $(MSBuildProjectDirectory)\bin\$(Configuration)\$(PackageId).$(PackageVersion).nupkg" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
231
UnitTests/Common/Packing/Ar/ArReaderTests.cs
Normal file
231
UnitTests/Common/Packing/Ar/ArReaderTests.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using AMWD.Common.Packing.Ar;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace UnitTests.Common.Packing.Ar
|
||||
{
|
||||
[TestClass]
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class ArReaderTests
|
||||
{
|
||||
private readonly DateTime fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
private Dictionary<string, ArFileInfo> files;
|
||||
|
||||
private Stream inStream;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
files = new Dictionary<string, ArFileInfo>
|
||||
{
|
||||
{
|
||||
"abcd.tmp",
|
||||
new ArFileInfo
|
||||
{
|
||||
FileName = "abcd.tmp",
|
||||
FileSize = 14,
|
||||
GroupId = 456,
|
||||
Mode = 33188,
|
||||
ModifyTime = fixedDateTime,
|
||||
UserId = 123
|
||||
}
|
||||
},
|
||||
{
|
||||
"efgh.tmp",
|
||||
new ArFileInfo
|
||||
{
|
||||
FileName = "efgh.tmp",
|
||||
FileSize = 14,
|
||||
GroupId = 456,
|
||||
Mode = 33188,
|
||||
ModifyTime = fixedDateTime,
|
||||
UserId = 123
|
||||
}
|
||||
},
|
||||
{
|
||||
"ijkl.tmp",
|
||||
new ArFileInfo
|
||||
{
|
||||
FileName = "ijkl.tmp",
|
||||
FileSize = 13,
|
||||
GroupId = 456,
|
||||
Mode = 33188,
|
||||
ModifyTime = fixedDateTime,
|
||||
UserId = 123
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
inStream = new MemoryStream();
|
||||
inStream.Write(Encoding.ASCII.GetBytes("!<arch>\n"));
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
int unixSeconds = (int)file.Value.ModifyTime.Subtract(DateTime.UnixEpoch).TotalSeconds;
|
||||
|
||||
inStream.Write(Encoding.ASCII.GetBytes($"{file.Key,-16}{unixSeconds,-12}123 456 100644 {file.Value.FileSize,-10}`\n"));
|
||||
inStream.Write(Encoding.UTF8.GetBytes(new string('a', (int)file.Value.FileSize)));
|
||||
if (file.Value.FileSize % 2 != 0)
|
||||
inStream.Write(Encoding.ASCII.GetBytes("\n"));
|
||||
}
|
||||
|
||||
inStream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
inStream.Dispose();
|
||||
inStream = null;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldInitializeArchive()
|
||||
{
|
||||
// Arrange
|
||||
inStream.Dispose();
|
||||
inStream = new MemoryStream();
|
||||
inStream.Write(Encoding.ASCII.GetBytes("!<arch>\n"));
|
||||
inStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// Act
|
||||
var reader = new ArReader(inStream);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(reader);
|
||||
Assert.IsFalse(reader.GetFileList().Any());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldInitializeWithFiles()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var reader = new ArReader(inStream);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(reader);
|
||||
Assert.IsTrue(reader.GetFileList().Any());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldListFileNames()
|
||||
{
|
||||
// Arrange
|
||||
var reader = new ArReader(inStream);
|
||||
|
||||
// Act
|
||||
var fileList = reader.GetFileList().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(reader);
|
||||
Assert.AreEqual(files.Count, fileList.Count);
|
||||
|
||||
foreach (string name in files.Keys)
|
||||
Assert.IsTrue(fileList.Contains(name));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnValidFileInfo()
|
||||
{
|
||||
// Arrange
|
||||
var infos = new List<ArFileInfo>();
|
||||
var reader = new ArReader(inStream);
|
||||
|
||||
// Act
|
||||
foreach (string name in files.Keys)
|
||||
infos.Add(reader.GetFileInfo(name));
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(reader);
|
||||
Assert.AreEqual(files.Count, infos.Count);
|
||||
|
||||
foreach (var expected in files.Values)
|
||||
{
|
||||
var actual = infos.Single(fi => fi.FileName == expected.FileName);
|
||||
|
||||
Assert.AreEqual(expected.FileName, actual.FileName);
|
||||
Assert.AreEqual(expected.FileSize, actual.FileSize);
|
||||
Assert.AreEqual(expected.GroupId, actual.GroupId);
|
||||
Assert.AreEqual(expected.Mode, actual.Mode);
|
||||
Assert.AreEqual(expected.ModifyTime, actual.ModifyTime);
|
||||
Assert.AreEqual(expected.UserId, actual.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldReturnValidFileContent()
|
||||
{
|
||||
// Arrange
|
||||
var contents = new Dictionary<string, string>();
|
||||
var reader = new ArReader(inStream);
|
||||
|
||||
// Act
|
||||
foreach (string name in files.Keys)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
reader.ReadFile(name, ms);
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
contents.Add(name, Encoding.UTF8.GetString(ms.ToArray()));
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(reader);
|
||||
Assert.AreEqual(files.Count, contents.Count);
|
||||
|
||||
foreach (var expected in files.Values)
|
||||
{
|
||||
string content = contents[expected.FileName];
|
||||
|
||||
if (expected.FileSize % 2 != 0)
|
||||
Assert.AreEqual(13, content.Length);
|
||||
else
|
||||
Assert.AreEqual(14, content.Length);
|
||||
|
||||
Assert.AreEqual(new string('a', (int)expected.FileSize), content);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldWriteFileToDisk()
|
||||
{
|
||||
// Arrange
|
||||
string tmpFile = Path.GetTempFileName();
|
||||
var reader = new ArReader(inStream);
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
reader.ReadFile("abcd.tmp", ms);
|
||||
reader.ReadFile("abcd.tmp", tmpFile);
|
||||
|
||||
// Assert
|
||||
CollectionAssert.AreEqual(ms.ToArray(), File.ReadAllBytes(tmpFile));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
private class OverrideStream : MemoryStream
|
||||
{
|
||||
public override bool CanWrite => CanWriteOR;
|
||||
|
||||
public bool CanWriteOR { get; set; }
|
||||
|
||||
public override bool CanSeek => CanSeekOR;
|
||||
|
||||
public bool CanSeekOR { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
306
UnitTests/Common/Packing/Ar/ArWriterTests.cs
Normal file
306
UnitTests/Common/Packing/Ar/ArWriterTests.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using AMWD.Common.Packing.Ar;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace UnitTests.Common.Packing.Ar
|
||||
{
|
||||
[TestClass]
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class ArWriterTests
|
||||
{
|
||||
private readonly DateTime fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
private readonly Dictionary<string, string> files = new();
|
||||
|
||||
private Stream outStream;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
files.Clear();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var (filePath, content) = GenerateTestFile();
|
||||
files.Add(filePath, content);
|
||||
}
|
||||
|
||||
outStream = new MemoryStream();
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
foreach (var kvp in files)
|
||||
File.Delete(kvp.Key);
|
||||
|
||||
files.Clear();
|
||||
|
||||
outStream.Dispose();
|
||||
outStream = null;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldInitializeArchive()
|
||||
{
|
||||
// Arrange
|
||||
byte[] initBytes = new byte[8];
|
||||
|
||||
// Act
|
||||
_ = new ArWriter(outStream);
|
||||
|
||||
outStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(initBytes.Length, outStream.Length);
|
||||
|
||||
outStream.Read(initBytes, 0, initBytes.Length);
|
||||
CollectionAssert.AreEqual(Encoding.ASCII.GetBytes("!<arch>\n"), initBytes);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldWriteOneFile()
|
||||
{
|
||||
// Arrange
|
||||
var firstFileKvp = files.First();
|
||||
byte[] headerBytes = new byte[60];
|
||||
byte[] contentBytes = new byte[14];
|
||||
|
||||
var writer = new ArWriter(outStream);
|
||||
|
||||
// Act
|
||||
writer.WriteFile(firstFileKvp.Key, 123, 456);
|
||||
|
||||
outStream.Seek(8, SeekOrigin.Begin); // set behind init bytes
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length);
|
||||
|
||||
outStream.Read(headerBytes, 0, headerBytes.Length);
|
||||
outStream.Read(contentBytes, 0, contentBytes.Length);
|
||||
|
||||
string header = Encoding.ASCII.GetString(headerBytes);
|
||||
string content = Encoding.UTF8.GetString(contentBytes);
|
||||
|
||||
Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 123 456 100644 14 `\n", header);
|
||||
Assert.AreEqual(firstFileKvp.Value, content);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldWriteMultipleFiles()
|
||||
{
|
||||
// Arrange
|
||||
var writer = new ArWriter(outStream);
|
||||
|
||||
// Act
|
||||
foreach (var kvp in files)
|
||||
writer.WriteFile(kvp.Key, 123, 456);
|
||||
|
||||
outStream.Seek(8, SeekOrigin.Begin);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual((8 + 3 * 60 + 3 * 14), outStream.Length);
|
||||
|
||||
foreach (var kvp in files)
|
||||
{
|
||||
byte[] headerBytes = new byte[60];
|
||||
byte[] contentBytes = new byte[14];
|
||||
|
||||
outStream.Read(headerBytes, 0, headerBytes.Length);
|
||||
outStream.Read(contentBytes, 0, contentBytes.Length);
|
||||
|
||||
string header = Encoding.ASCII.GetString(headerBytes);
|
||||
string content = Encoding.UTF8.GetString(contentBytes);
|
||||
|
||||
Assert.AreEqual($"{Path.GetFileName(kvp.Key),-16}1677666030 123 456 100644 14 `\n", header);
|
||||
Assert.AreEqual(kvp.Value, content);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldPadToEven()
|
||||
{
|
||||
// Arrange
|
||||
var (filePath, fileContent) = GenerateTestFile(13);
|
||||
|
||||
try
|
||||
{
|
||||
byte[] headerBytes = new byte[60];
|
||||
byte[] contentBytes = new byte[14];
|
||||
|
||||
var writer = new ArWriter(outStream);
|
||||
|
||||
// Act
|
||||
writer.WriteFile(filePath, 123, 456);
|
||||
|
||||
outStream.Seek(8, SeekOrigin.Begin);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length);
|
||||
|
||||
outStream.Read(headerBytes, 0, headerBytes.Length);
|
||||
outStream.Read(contentBytes, 0, contentBytes.Length);
|
||||
|
||||
string header = Encoding.ASCII.GetString(headerBytes);
|
||||
string content = Encoding.UTF8.GetString(contentBytes);
|
||||
|
||||
Assert.AreEqual($"{Path.GetFileName(filePath),-16}1677666030 123 456 100644 13 `\n", header);
|
||||
Assert.AreEqual(fileContent + "\n", content);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldFailOnFileNameTooLong()
|
||||
{
|
||||
// Arrange
|
||||
var (filePath, _) = GenerateTestFile();
|
||||
try
|
||||
{
|
||||
string path = Path.GetDirectoryName(filePath);
|
||||
string fileName = Path.GetFileName(filePath);
|
||||
fileName = fileName.PadLeft(20, 'a');
|
||||
|
||||
File.Move(filePath, Path.Combine(path, fileName));
|
||||
filePath = Path.Combine(path, fileName);
|
||||
|
||||
var writer = new ArWriter(outStream);
|
||||
|
||||
// Act
|
||||
writer.WriteFile(filePath, 123, 456);
|
||||
|
||||
// Assert - Exception
|
||||
Assert.Fail();
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldWriteEmptyOnNegativeUserId()
|
||||
{
|
||||
// Arrange
|
||||
var firstFileKvp = files.First();
|
||||
byte[] headerBytes = new byte[60];
|
||||
byte[] contentBytes = new byte[14];
|
||||
|
||||
var writer = new ArWriter(outStream);
|
||||
|
||||
// Act
|
||||
writer.WriteFile(firstFileKvp.Key, -123, 456);
|
||||
|
||||
outStream.Seek(8, SeekOrigin.Begin);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length);
|
||||
|
||||
outStream.Read(headerBytes, 0, headerBytes.Length);
|
||||
outStream.Read(contentBytes, 0, contentBytes.Length);
|
||||
|
||||
string header = Encoding.ASCII.GetString(headerBytes);
|
||||
string content = Encoding.UTF8.GetString(contentBytes);
|
||||
|
||||
Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 456 100644 14 `\n", header);
|
||||
Assert.AreEqual(firstFileKvp.Value, content);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldWriteEmptyOnNegativeGroupId()
|
||||
{
|
||||
// Arrange
|
||||
var firstFileKvp = files.First();
|
||||
byte[] headerBytes = new byte[60];
|
||||
byte[] contentBytes = new byte[14];
|
||||
|
||||
var writer = new ArWriter(outStream);
|
||||
|
||||
// Act
|
||||
writer.WriteFile(firstFileKvp.Key, 123, -456);
|
||||
|
||||
outStream.Seek(8, SeekOrigin.Begin);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length);
|
||||
|
||||
outStream.Read(headerBytes, 0, headerBytes.Length);
|
||||
outStream.Read(contentBytes, 0, contentBytes.Length);
|
||||
|
||||
string header = Encoding.ASCII.GetString(headerBytes);
|
||||
string content = Encoding.UTF8.GetString(contentBytes);
|
||||
|
||||
Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 123 100644 14 `\n", header);
|
||||
Assert.AreEqual(firstFileKvp.Value, content);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ShouldWriteEmptyOnNegativeMode()
|
||||
{
|
||||
// Arrange
|
||||
var firstFileKvp = files.First();
|
||||
byte[] headerBytes = new byte[60];
|
||||
byte[] contentBytes = new byte[14];
|
||||
|
||||
var writer = new ArWriter(outStream);
|
||||
|
||||
// Act
|
||||
writer.WriteFile(firstFileKvp.Key, 123, 456, -1);
|
||||
|
||||
outStream.Seek(8, SeekOrigin.Begin);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length);
|
||||
|
||||
outStream.Read(headerBytes, 0, headerBytes.Length);
|
||||
outStream.Read(contentBytes, 0, contentBytes.Length);
|
||||
|
||||
string header = Encoding.ASCII.GetString(headerBytes);
|
||||
string content = Encoding.UTF8.GetString(contentBytes);
|
||||
|
||||
Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 123 456 14 `\n", header);
|
||||
Assert.AreEqual(firstFileKvp.Value, content);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ShouldFailOnNonWritableStream()
|
||||
{
|
||||
// Arrange
|
||||
using var testStream = new NonWriteStream();
|
||||
|
||||
// Act
|
||||
_ = new ArWriter(testStream);
|
||||
|
||||
// Assert - ArgumentException
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
private (string filePath, string content) GenerateTestFile(int length = 14)
|
||||
{
|
||||
string filePath = Path.GetTempFileName();
|
||||
string text = CryptographyHelper.GetRandomString(length);
|
||||
|
||||
File.WriteAllText(filePath, text);
|
||||
File.SetLastWriteTimeUtc(filePath, fixedDateTime);
|
||||
return (filePath, text);
|
||||
}
|
||||
|
||||
private class NonWriteStream : MemoryStream
|
||||
{
|
||||
public NonWriteStream()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
public override bool CanWrite => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user