Remove packing
This commit is contained in:
@@ -17,8 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `ArReader` and `ArWriter` for Unix archives
|
|
||||||
- `TarReader` and `TarWriter` for TAR archives
|
|
||||||
- `.AddRange()` for collections
|
- `.AddRange()` for collections
|
||||||
- `.AddIfNotNull()` for collections
|
- `.AddIfNotNull()` for collections
|
||||||
- `DomainComparer` ordering alphabetically from TLD to sub-domain
|
- `DomainComparer` ordering alphabetically from TLD to sub-domain
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
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 = [];
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
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: <see href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Ar/ArWriter.cs">DotnetMakeDeb</see>
|
|
||||||
/// </remarks>
|
|
||||||
public class ArWriter
|
|
||||||
{
|
|
||||||
// Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29
|
|
||||||
|
|
||||||
private readonly Stream _outStream;
|
|
||||||
|
|
||||||
/// <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));
|
|
||||||
|
|
||||||
_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 = [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
|
|
||||||
long unixTime = ((DateTimeOffset)DateTime.SpecifyKind(modifyTime, DateTimeKind.Utc)).ToUnixTimeSeconds();
|
|
||||||
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(nameof(fileSize), "Invalid file size."); // above 9.32 GB
|
|
||||||
|
|
||||||
WriteAsciiString(fileSize.ToString().PadRight(10, ' '));
|
|
||||||
|
|
||||||
// File magic
|
|
||||||
byte[] bytes = [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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using AMWD.Common.Packing.Ar;
|
|
||||||
|
|
||||||
namespace AMWD.Common.Tests.Packing.Ar
|
|
||||||
{
|
|
||||||
[TestClass]
|
|
||||||
public class ArReaderTest
|
|
||||||
{
|
|
||||||
private readonly DateTime _fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc);
|
|
||||||
|
|
||||||
private Dictionary<string, ArFileInfo> _files;
|
|
||||||
|
|
||||||
private MemoryStream _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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowExceptionOnMissingRead()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
using var stream = new OverrideStream();
|
|
||||||
stream.CanReadOR = false;
|
|
||||||
stream.CanSeekOR = true;
|
|
||||||
stream.CanWriteOR = true;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var reader = new ArReader(stream);
|
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowExceptionOnMissingSeek()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
using var stream = new OverrideStream();
|
|
||||||
stream.CanReadOR = true;
|
|
||||||
stream.CanSeekOR = false;
|
|
||||||
stream.CanWriteOR = true;
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var reader = new ArReader(stream);
|
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[ExpectedException(typeof(ArgumentException))]
|
|
||||||
public void ShouldThrowExceptionOnMissingWrite()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
using var stream = new OverrideStream();
|
|
||||||
stream.CanReadOR = true;
|
|
||||||
stream.CanSeekOR = true;
|
|
||||||
stream.CanWriteOR = false;
|
|
||||||
|
|
||||||
var reader = new ArReader(_inStream);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
reader.ReadFile("abcd.tmp", stream);
|
|
||||||
|
|
||||||
// Assert - ArgumentException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[ExpectedException(typeof(FormatException))]
|
|
||||||
public void ShouldThrowExceptionOnInvalidArchive()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
_inStream.Seek(8, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_ = new ArReader(_inStream);
|
|
||||||
|
|
||||||
// Assert - FormatException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[ExpectedException(typeof(FormatException))]
|
|
||||||
public void ShouldThrowExceptionOnInvalidMagic()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
_inStream.Seek(0, SeekOrigin.End);
|
|
||||||
_inStream.Write(Encoding.ASCII.GetBytes($"{"foo.bar",-16}{"123456789",-12}123 456 100644 {"0",-10}´\n"));
|
|
||||||
_inStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
_ = new ArReader(_inStream);
|
|
||||||
|
|
||||||
// Assert - FormatException
|
|
||||||
Assert.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void ShouldWriteNothingToStreamForMissingFile()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var reader = new ArReader(_inStream);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
|
|
||||||
reader.ReadFile("foo.bar", ms);
|
|
||||||
ms.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.AreEqual(0, ms.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void ShouldWriteNothingToDiskForMissingFile()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
string tmpFile = Path.GetTempFileName();
|
|
||||||
var reader = new ArReader(_inStream);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Act
|
|
||||||
reader.ReadFile("foo.bar", tmpFile);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.AreEqual(0, new FileInfo(tmpFile).Length);
|
|
||||||
}
|
|
||||||
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; }
|
|
||||||
|
|
||||||
public override bool CanRead => CanReadOR;
|
|
||||||
|
|
||||||
public bool CanReadOR { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using AMWD.Common.Packing.Ar;
|
|
||||||
|
|
||||||
namespace AMWD.Common.Tests.Packing.Ar
|
|
||||||
{
|
|
||||||
[TestClass]
|
|
||||||
public class ArWriterTest
|
|
||||||
{
|
|
||||||
private readonly DateTime _fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc);
|
|
||||||
|
|
||||||
private readonly Dictionary<string, string> _files = [];
|
|
||||||
|
|
||||||
private MemoryStream _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