From 7cd5358ac82a50e8c16b8c4cd59cf2c5690b79e2 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Mon, 13 Mar 2023 10:08:50 +0100 Subject: [PATCH 1/4] WIP: Added packing of AR and TAR --- AMWD.Common/Packing/Ar/ArFileInfo.cs | 53 +++ AMWD.Common/Packing/Ar/ArReader.cs | 180 +++++++++++ AMWD.Common/Packing/Ar/ArWriter.cs | 147 +++++++++ .../Tar/Interfaces/IArchiveDataWriter.cs | 26 ++ .../Packing/Tar/Interfaces/ITarHeader.cs | 125 +++++++ AMWD.Common/Packing/Tar/TarReader.cs | 210 ++++++++++++ AMWD.Common/Packing/Tar/TarWriter.cs | 63 ++++ AMWD.Common/Packing/Tar/Utils/DataWriter.cs | 45 +++ AMWD.Common/Packing/Tar/Utils/EntryType.cs | 73 +++++ .../Packing/Tar/Utils/LegacyTarWriter.cs | 258 +++++++++++++++ AMWD.Common/Packing/Tar/Utils/TarException.cs | 13 + AMWD.Common/Packing/Tar/Utils/TarHeader.cs | 194 +++++++++++ AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs | 136 ++++++++ CHANGELOG.md | 2 + Directory.Build.props | 2 +- Directory.Build.targets | 11 +- UnitTests/Common/Packing/Ar/ArReaderTests.cs | 231 +++++++++++++ UnitTests/Common/Packing/Ar/ArWriterTests.cs | 306 ++++++++++++++++++ 18 files changed, 2071 insertions(+), 4 deletions(-) create mode 100644 AMWD.Common/Packing/Ar/ArFileInfo.cs create mode 100644 AMWD.Common/Packing/Ar/ArReader.cs create mode 100644 AMWD.Common/Packing/Ar/ArWriter.cs create mode 100644 AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs create mode 100644 AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs create mode 100644 AMWD.Common/Packing/Tar/TarReader.cs create mode 100644 AMWD.Common/Packing/Tar/TarWriter.cs create mode 100644 AMWD.Common/Packing/Tar/Utils/DataWriter.cs create mode 100644 AMWD.Common/Packing/Tar/Utils/EntryType.cs create mode 100644 AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs create mode 100644 AMWD.Common/Packing/Tar/Utils/TarException.cs create mode 100644 AMWD.Common/Packing/Tar/Utils/TarHeader.cs create mode 100644 AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs create mode 100644 UnitTests/Common/Packing/Ar/ArReaderTests.cs create mode 100644 UnitTests/Common/Packing/Ar/ArWriterTests.cs diff --git a/AMWD.Common/Packing/Ar/ArFileInfo.cs b/AMWD.Common/Packing/Ar/ArFileInfo.cs new file mode 100644 index 0000000..fdd05be --- /dev/null +++ b/AMWD.Common/Packing/Ar/ArFileInfo.cs @@ -0,0 +1,53 @@ +using System; + +//[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")] + +namespace AMWD.Common.Packing.Ar +{ + /// + /// Represents the file information saved in the archive. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class ArFileInfo + { + /// + /// Gets or sets the file name. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the file size in bytes. + /// + public long FileSize { get; set; } + + /// + /// Gets or sets the timestamp of the last modification. + /// + public DateTime ModifyTime { get; set; } + + /// + /// Gets or sets the user id. + /// + public int UserId { get; set; } + + /// + /// Gets or sets the group id. + /// + public int GroupId { get; set; } + + /// + /// Gets or sets the access mode in decimal (not octal!). + /// + /// + /// To see the octal representation use Convert.ToString(Mode, 8). + /// + public int Mode { get; set; } + } + + internal class ArFileInfoExtended : ArFileInfo + { + public long HeaderPosition { get; set; } + + public long DataPosition { get; set; } + } +} diff --git a/AMWD.Common/Packing/Ar/ArReader.cs b/AMWD.Common/Packing/Ar/ArReader.cs new file mode 100644 index 0000000..e59f1e3 --- /dev/null +++ b/AMWD.Common/Packing/Ar/ArReader.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace AMWD.Common.Packing.Ar +{ + /// + /// Reads UNIX ar (archive) files in the GNU format. + /// + public class ArReader + { + // Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29 + + private readonly Stream inStream; + private readonly List files = new(); + private readonly long streamStartPosition; + + private static readonly DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Initializes a new instance of the class. + /// + /// The stream to read the archive from. + public ArReader(Stream inStream) + { + if (!inStream.CanRead || !inStream.CanSeek) + throw new ArgumentException("Stream not readable or seekable", nameof(inStream)); + + streamStartPosition = inStream.Position; + this.inStream = inStream; + + Initialize(); + } + + /// + /// Returns a list with all filenames of the archive. + /// + public IEnumerable GetFileList() + { + return files.Select(fi => fi.FileName).ToList(); + } + + /// + /// Returns the file info of a specific file in the archive. + /// + /// The name of the specific file. + public ArFileInfo GetFileInfo(string fileName) + { + return files + .Where(fi => fi.FileName == fileName) + .Select(fi => new ArFileInfo + { + FileName = fi.FileName, + FileSize = fi.FileSize, + GroupId = fi.GroupId, + Mode = fi.Mode, + ModifyTime = fi.ModifyTime, + UserId = fi.UserId + }) + .FirstOrDefault(); + } + + /// + /// Reads a file from the archive into a stream. + /// + /// The file name in the archive. + /// The output stream. + public void ReadFile(string fileName, Stream outStream) + { + if (!outStream.CanWrite) + throw new ArgumentException("Stream not writable", nameof(outStream)); + + var info = files.Where(fi => fi.FileName == fileName).FirstOrDefault(); + if (info == null) + return; + + long bytesToRead = info.FileSize; + byte[] buffer = new byte[1024 * 1024]; + + inStream.Seek(info.DataPosition, SeekOrigin.Begin); + while (bytesToRead > 0) + { + int readCount = (int)Math.Min(bytesToRead, buffer.Length); + inStream.Read(buffer, 0, readCount); + outStream.Write(buffer, 0, readCount); + + bytesToRead -= readCount; + } + inStream.Seek(streamStartPosition, SeekOrigin.Begin); + } + + /// + /// Reads a fie from the archive and saves it to disk. + /// + /// The file name in the archive. + /// The destination path on disk. + public void ReadFile(string fileName, string destinationPath) + { + var info = files.Where(fi => fi.FileName == fileName).FirstOrDefault(); + if (info == null) + return; + + using (var fs = File.OpenWrite(destinationPath)) + { + ReadFile(fileName, fs); + } + File.SetLastWriteTimeUtc(destinationPath, info.ModifyTime); + } + + private void Initialize() + { + // Reset stream + inStream.Seek(streamStartPosition, SeekOrigin.Begin); + + // Read header + string header = ReadAsciiString(8); + if (header != "!\n") + throw new FormatException("The file stream is no archive"); + + // Create file list + while (inStream.Position < inStream.Length) + { + var info = ReadFileHeader(); + files.Add(info); + + // Move stream behind file content + inStream.Seek(info.FileSize, SeekOrigin.Current); + + // Align to even offsets (padded with LF bytes) + if (inStream.Position % 2 != 0) + inStream.Seek(1, SeekOrigin.Current); + } + + // Reset stream + inStream.Seek(streamStartPosition, SeekOrigin.Begin); + } + + private string ReadAsciiString(int byteCount) + { + byte[] buffer = new byte[byteCount]; + inStream.Read(buffer, 0, byteCount); + return Encoding.ASCII.GetString(buffer); + } + + private ArFileInfoExtended ReadFileHeader() + { + long startPosition = inStream.Position; + + string fileName = ReadAsciiString(16).Trim(); + + int.TryParse(ReadAsciiString(12).Trim(), out int unixTimestamp); + int.TryParse(ReadAsciiString(6).Trim(), out int userId); + int.TryParse(ReadAsciiString(6).Trim(), out int groupId); + int mode = Convert.ToInt32(ReadAsciiString(8).Trim(), 8); + + long.TryParse(ReadAsciiString(10).Trim(), out long fileSize); + + // file magic + byte[] magic = new byte[2]; + inStream.Read(magic, 0, magic.Length); + + if (magic[0] != 0x60 || magic[1] != 0x0A) + 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 + }; + } + } +} diff --git a/AMWD.Common/Packing/Ar/ArWriter.cs b/AMWD.Common/Packing/Ar/ArWriter.cs new file mode 100644 index 0000000..321149c --- /dev/null +++ b/AMWD.Common/Packing/Ar/ArWriter.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Text; + +namespace AMWD.Common.Packing.Ar +{ + /// + /// Writes UNIX ar (archive) files in the GNU format. + /// + /// + /// Copied from + /// + 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); + + /// + /// Initialises a new instance of the class. + /// + /// The stream to write the archive to. + public ArWriter(Stream outStream) + { + if (!outStream.CanWrite) + throw new ArgumentException("Stream not writable", nameof(outStream)); + + this.outStream = outStream; + Initialize(); + } + + /// + /// Writes a file from disk to the archive. + /// + /// The name of the file to copy. + /// The user ID of the file in the archive. + /// The group ID of the file in the archive. + /// The mode of the file in the archive (decimal). + 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); + } + + /// + /// Writes a file from a Stream to the archive. + /// + /// The stream to read the file contents from. + /// The name of the file in the archive. + /// The last modification time of the file in the archive. + /// The user ID of the file in the archive. + /// The group ID of the file in the archive. + /// The mode of the file in the archive (decimal). + 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); + } + } + + /// + /// Writes the archive header. + /// + private void Initialize() + { + WriteAsciiString("!\n"); + } + + /// + /// Writes a file header. + /// + 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); + } + + /// + /// Writes a string using ASCII encoding. + /// + /// The string to write to the output stream. + private void WriteAsciiString(string str) + { + byte[] bytes = Encoding.ASCII.GetBytes(str); + outStream.Write(bytes, 0, bytes.Length); + } + } +} diff --git a/AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs b/AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs new file mode 100644 index 0000000..d621ed3 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs @@ -0,0 +1,26 @@ +namespace AMWD.Common.Packing.Tar.Interfaces +{ + /// + /// Interface of a archive writer. + /// + public interface IArchiveDataWriter + { + /// + /// Write bytes of data from to corresponding archive. + /// + /// The data storage. + /// How many bytes to be written to the corresponding archive. + int Write(byte[] buffer, int count); + + /// + /// Gets a value indicating whether the writer can write. + /// + bool CanWrite { get; } + } + + /// + /// The writer delegate. + /// + /// The writer. + public delegate void WriteDataDelegate(IArchiveDataWriter writer); +} diff --git a/AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs b/AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs new file mode 100644 index 0000000..f4a474f --- /dev/null +++ b/AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs @@ -0,0 +1,125 @@ +using System; +using AMWD.Common.Packing.Tar.Utils; + +namespace AMWD.Common.Packing.Tar.Interfaces +{ + /// + /// See "struct star_header" in + /// + public interface ITarHeader + { + /// + /// The name field is the file name of the file, with directory names (if any) preceding the file name, + /// separated by slashes. + /// + /// + /// name + ///
+ /// Byte offset: 0 + ///
+ string FileName { get; set; } + + /// + /// 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. + /// + /// + /// mode + ///
+ /// Byte offset: 100 + ///
+ int Mode { get; set; } + + /// + /// 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. + /// + /// + /// uid + ///
+ /// Byte offset: 108 + ///
+ int UserId { get; set; } + + /// + /// 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. + /// + /// + /// gid + ///
+ /// Byte offset: 116 + ///
+ int GroupId { get; set; } + + /// + /// The size field is the size of the file in bytes; + /// linked files are archived with this field specified as zero. + /// + /// + /// size + ///
+ /// Byte offset: 124 + ///
+ long SizeInBytes { get; set; } + + /// + /// mtime + /// byte offset: 136 + /// 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. + /// + /// + /// mtime + ///
+ /// Byte offset: 136 + ///
+ DateTime LastModification { get; set; } + + /// + /// 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. + /// + /// + /// typeflag + ///
+ /// Byte offset: 156 + ///
+ EntryType EntryType { get; set; } + + /// + /// 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. + /// + /// + /// uname + ///
+ /// Byte offset: 265 + ///
+ string UserName { get; set; } + + /// + /// 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. + /// + /// + /// gname + ///
+ /// Byte offset: 297 + ///
+ string GroupName { get; set; } + + /// + /// The size of this header. + /// + int HeaderSize { get; } + } +} diff --git a/AMWD.Common/Packing/Tar/TarReader.cs b/AMWD.Common/Packing/Tar/TarReader.cs new file mode 100644 index 0000000..edb5bac --- /dev/null +++ b/AMWD.Common/Packing/Tar/TarReader.cs @@ -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 +{ + /// + /// Extract contents of a tar file represented by a stream for the TarReader constructor + /// + /// + /// https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarReader.cs + /// + public class TarReader + { + private readonly byte[] dataBuffer = new byte[512]; + private readonly UsTarHeader header; + private readonly Stream inStream; + private long remainingBytesInFile; + + /// + /// Constructs TarReader object to read data from `tarredData` stream + /// + /// A stream to read tar archive from + public TarReader(Stream tarredData) + { + inStream = tarredData; + header = new UsTarHeader(); + } + + public ITarHeader FileInfo + { + get { return header; } + } + + /// + /// Read all files from an archive to a directory. It creates some child directories to + /// reproduce a file structure from the archive. + /// + /// The out directory. + /// + /// 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); + } + } + } + + /// + /// Read data from a current file to a Stream. + /// + /// A stream to read data to + /// + /// + 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; + } + + /// + /// Check if all bytes in buffer are zeroes + /// + /// buffer to check + /// true if all bytes are zeroes, otherwise false + private static bool IsEmpty(IEnumerable buffer) + { + foreach (byte b in buffer) + { + if (b != 0) return false; + } + return true; + } + + /// + /// Move internal pointer to a next file in archive. + /// + /// Should be true if you want to read a header only, otherwise false + /// false on End Of File otherwise true + /// + /// Example: + /// while(MoveNext()) + /// { + /// Read(dataDestStream); + /// } + /// + 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; + } + } +} diff --git a/AMWD.Common/Packing/Tar/TarWriter.cs b/AMWD.Common/Packing/Tar/TarWriter.cs new file mode 100644 index 0000000..740864b --- /dev/null +++ b/AMWD.Common/Packing/Tar/TarWriter.cs @@ -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); + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/DataWriter.cs b/AMWD.Common/Packing/Tar/Utils/DataWriter.cs new file mode 100644 index 0000000..10a1b23 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/DataWriter.cs @@ -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; + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/EntryType.cs b/AMWD.Common/Packing/Tar/Utils/EntryType.cs new file mode 100644 index 0000000..2eadbec --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/EntryType.cs @@ -0,0 +1,73 @@ +namespace AMWD.Common.Packing.Tar.Utils +{ + ///
+ /// See "Values used in typeflag field." in + /// + public enum EntryType : byte + { + /// + /// AREGTYPE, regular file + /// + File = 0, + + /// + /// REGTYPE, regular file + /// + FileObsolete = 0x30, + + /// + /// LNKTYPE, link + /// + HardLink = 0x31, + + /// + /// SYMTYPE, reserved + /// + SymLink = 0x32, + + /// + /// CHRTYPE, character special + /// + CharDevice = 0x33, + + /// + /// BLKTYPE, block special + /// + BlockDevice = 0x34, + + /// + /// DIRTYPE, directory + /// + Directory = 0x35, + + /// + /// FIFOTYPE, FIFO special + /// + Fifo = 0x36, + + /// + /// CONTTYPE, reserved + /// + Content = 0x37, + + /// + /// XHDTYPE, Extended header referring to the next file in the archive + /// + ExtendedHeader = 0x78, + + /// + /// XGLTYPE, Global extended header + /// + GlobalExtendedHeader = 0x67, + + /// + /// GNUTYPE_LONGLINK, Identifies the *next* file on the tape as having a long linkname. + /// + LongLink = 0x4b, + + /// + /// GNUTYPE_LONGNAME, Identifies the *next* file on the tape as having a long name. + /// + LongName = 0x4c + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs new file mode 100644 index 0000000..ad249aa --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs @@ -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 +{ + /// + /// Implements a legacy TAR writer. + /// + /// + /// Copied from + /// + public class LegacyTarWriter : IDisposable + { + private readonly Stream outStream; + protected byte[] buffer = new byte[1024]; + private bool isClosed; + public bool ReadOnZero = true; + + /// + /// Writes tar (see GNU tar) archive to a stream + /// + /// stream to write archive to + 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); + } + + /// + /// Handle long file or path names (> 99 characters). + /// Write header and content, which its content contain the long (complete) file/path name. + /// This handling method is adapted from https://github.com/qmfrederik/dotnet-packaging/pull/50/files#diff-f64c58cc18e8e445cee6ffed7a0d765cdb442c0ef21a3ed80bd20514057967b1 + /// + /// File name or path name. + /// User ID. + /// Group ID. + /// Mode. + /// Last modification time. + 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; + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/TarException.cs b/AMWD.Common/Packing/Tar/Utils/TarException.cs new file mode 100644 index 0000000..c3dbf70 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/TarException.cs @@ -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) + { + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/TarHeader.cs b/AMWD.Common/Packing/Tar/Utils/TarHeader.cs new file mode 100644 index 0000000..01ccbc1 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/TarHeader.cs @@ -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; + } + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs b/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs new file mode 100644 index 0000000..f9d530c --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs @@ -0,0 +1,136 @@ +using System; +using System.Net; +using System.Text; + +namespace AMWD.Common.Packing.Tar.Utils +{ + /// + /// UsTar header implementation. + /// + 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; + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab18bf..d95ecb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Directory.Build.props b/Directory.Build.props index 6383836..e32b6ca 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - {semvertag:main} + {semvertag:main}{!:-mod} true false diff --git a/Directory.Build.targets b/Directory.Build.targets index d2a3986..84ed1ef 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,8 +1,13 @@ - + - + - + + + + + + diff --git a/UnitTests/Common/Packing/Ar/ArReaderTests.cs b/UnitTests/Common/Packing/Ar/ArReaderTests.cs new file mode 100644 index 0000000..5b0bc48 --- /dev/null +++ b/UnitTests/Common/Packing/Ar/ArReaderTests.cs @@ -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 files; + + private Stream inStream; + + [TestInitialize] + public void Initialize() + { + files = new Dictionary + { + { + "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("!\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("!\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(); + 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(); + 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; } + } + } +} diff --git a/UnitTests/Common/Packing/Ar/ArWriterTests.cs b/UnitTests/Common/Packing/Ar/ArWriterTests.cs new file mode 100644 index 0000000..f872687 --- /dev/null +++ b/UnitTests/Common/Packing/Ar/ArWriterTests.cs @@ -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 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("!\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; + } + } +} From 9611de0dde4c3d2ed935d8a57994c65254937056 Mon Sep 17 00:00:00 2001 From: Andreas Mueller Date: Sat, 18 Mar 2023 13:47:19 +0100 Subject: [PATCH 2/4] Implemented Ar Reader/Writer and UnitTests --- AMWD.Common/Packing/Ar/ArFileInfo.cs | 2 - AMWD.Common/Packing/Ar/ArReader.cs | 2 +- UnitTests/Common/Packing/Ar/ArReaderTests.cs | 124 +++++++++++++++++++ 3 files changed, 125 insertions(+), 3 deletions(-) diff --git a/AMWD.Common/Packing/Ar/ArFileInfo.cs b/AMWD.Common/Packing/Ar/ArFileInfo.cs index fdd05be..f0cbc38 100644 --- a/AMWD.Common/Packing/Ar/ArFileInfo.cs +++ b/AMWD.Common/Packing/Ar/ArFileInfo.cs @@ -1,7 +1,5 @@ using System; -//[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")] - namespace AMWD.Common.Packing.Ar { /// diff --git a/AMWD.Common/Packing/Ar/ArReader.cs b/AMWD.Common/Packing/Ar/ArReader.cs index e59f1e3..a260bb6 100644 --- a/AMWD.Common/Packing/Ar/ArReader.cs +++ b/AMWD.Common/Packing/Ar/ArReader.cs @@ -161,7 +161,7 @@ namespace AMWD.Common.Packing.Ar byte[] magic = new byte[2]; inStream.Read(magic, 0, magic.Length); - if (magic[0] != 0x60 || magic[1] != 0x0A) + if (magic[0] != 0x60 || magic[1] != 0x0A) // `\n throw new FormatException("Invalid file magic"); return new ArFileInfoExtended diff --git a/UnitTests/Common/Packing/Ar/ArReaderTests.cs b/UnitTests/Common/Packing/Ar/ArReaderTests.cs index 5b0bc48..688f54e 100644 --- a/UnitTests/Common/Packing/Ar/ArReaderTests.cs +++ b/UnitTests/Common/Packing/Ar/ArReaderTests.cs @@ -217,6 +217,126 @@ namespace UnitTests.Common.Packing.Ar } } + [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; @@ -226,6 +346,10 @@ namespace UnitTests.Common.Packing.Ar public override bool CanSeek => CanSeekOR; public bool CanSeekOR { get; set; } + + public override bool CanRead => CanReadOR; + + public bool CanReadOR { get; set; } } } } From 9cd13442661839690569d0472eaaf0f40eaef4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 10 Jan 2024 09:33:51 +0100 Subject: [PATCH 3/4] Fixed naming conventions --- AMWD.Common/Packing/Ar/ArReader.cs | 52 +++---- AMWD.Common/Packing/Ar/ArWriter.cs | 17 +- AMWD.Common/Packing/Tar/TarReader.cs | 102 ++++++------ AMWD.Common/Packing/Tar/TarWriter.cs | 6 +- AMWD.Common/Packing/Tar/Utils/DataWriter.cs | 22 +-- .../Packing/Tar/Utils/LegacyTarWriter.cs | 42 +++-- AMWD.Common/Packing/Tar/Utils/TarException.cs | 2 - AMWD.Common/Packing/Tar/Utils/TarHeader.cs | 145 ++++++++---------- AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs | 59 +++---- CHANGELOG.md | 8 +- Directory.Build.props | 4 +- 11 files changed, 218 insertions(+), 241 deletions(-) diff --git a/AMWD.Common/Packing/Ar/ArReader.cs b/AMWD.Common/Packing/Ar/ArReader.cs index a260bb6..28b2e41 100644 --- a/AMWD.Common/Packing/Ar/ArReader.cs +++ b/AMWD.Common/Packing/Ar/ArReader.cs @@ -13,11 +13,9 @@ namespace AMWD.Common.Packing.Ar { // Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29 - private readonly Stream inStream; - private readonly List files = new(); - private readonly long streamStartPosition; - - private static readonly DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + private readonly Stream _inStream; + private readonly List _files = new(); + private readonly long _streamStartPosition; /// /// Initializes a new instance of the class. @@ -28,8 +26,8 @@ namespace AMWD.Common.Packing.Ar if (!inStream.CanRead || !inStream.CanSeek) throw new ArgumentException("Stream not readable or seekable", nameof(inStream)); - streamStartPosition = inStream.Position; - this.inStream = inStream; + _streamStartPosition = inStream.Position; + _inStream = inStream; Initialize(); } @@ -38,9 +36,7 @@ namespace AMWD.Common.Packing.Ar /// Returns a list with all filenames of the archive. /// public IEnumerable GetFileList() - { - return files.Select(fi => fi.FileName).ToList(); - } + => _files.Select(fi => fi.FileName).ToList(); /// /// Returns the file info of a specific file in the archive. @@ -48,7 +44,7 @@ namespace AMWD.Common.Packing.Ar /// The name of the specific file. public ArFileInfo GetFileInfo(string fileName) { - return files + return _files .Where(fi => fi.FileName == fileName) .Select(fi => new ArFileInfo { @@ -72,23 +68,23 @@ namespace AMWD.Common.Packing.Ar if (!outStream.CanWrite) throw new ArgumentException("Stream not writable", nameof(outStream)); - var info = files.Where(fi => fi.FileName == fileName).FirstOrDefault(); + 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); + _inStream.Seek(info.DataPosition, SeekOrigin.Begin); while (bytesToRead > 0) { int readCount = (int)Math.Min(bytesToRead, buffer.Length); - inStream.Read(buffer, 0, readCount); + _inStream.Read(buffer, 0, readCount); outStream.Write(buffer, 0, readCount); bytesToRead -= readCount; } - inStream.Seek(streamStartPosition, SeekOrigin.Begin); + _inStream.Seek(_streamStartPosition, SeekOrigin.Begin); } /// @@ -98,7 +94,7 @@ namespace AMWD.Common.Packing.Ar /// The destination path on disk. public void ReadFile(string fileName, string destinationPath) { - var info = files.Where(fi => fi.FileName == fileName).FirstOrDefault(); + var info = _files.Where(fi => fi.FileName == fileName).FirstOrDefault(); if (info == null) return; @@ -112,7 +108,7 @@ namespace AMWD.Common.Packing.Ar private void Initialize() { // Reset stream - inStream.Seek(streamStartPosition, SeekOrigin.Begin); + _inStream.Seek(_streamStartPosition, SeekOrigin.Begin); // Read header string header = ReadAsciiString(8); @@ -120,33 +116,33 @@ namespace AMWD.Common.Packing.Ar throw new FormatException("The file stream is no archive"); // Create file list - while (inStream.Position < inStream.Length) + while (_inStream.Position < _inStream.Length) { var info = ReadFileHeader(); - files.Add(info); + _files.Add(info); // Move stream behind file content - inStream.Seek(info.FileSize, SeekOrigin.Current); + _inStream.Seek(info.FileSize, SeekOrigin.Current); // Align to even offsets (padded with LF bytes) - if (inStream.Position % 2 != 0) - inStream.Seek(1, SeekOrigin.Current); + if (_inStream.Position % 2 != 0) + _inStream.Seek(1, SeekOrigin.Current); } // Reset stream - inStream.Seek(streamStartPosition, SeekOrigin.Begin); + _inStream.Seek(_streamStartPosition, SeekOrigin.Begin); } private string ReadAsciiString(int byteCount) { byte[] buffer = new byte[byteCount]; - inStream.Read(buffer, 0, byteCount); + _inStream.Read(buffer, 0, byteCount); return Encoding.ASCII.GetString(buffer); } private ArFileInfoExtended ReadFileHeader() { - long startPosition = inStream.Position; + long startPosition = _inStream.Position; string fileName = ReadAsciiString(16).Trim(); @@ -159,7 +155,7 @@ namespace AMWD.Common.Packing.Ar // file magic byte[] magic = new byte[2]; - inStream.Read(magic, 0, magic.Length); + _inStream.Read(magic, 0, magic.Length); if (magic[0] != 0x60 || magic[1] != 0x0A) // `\n throw new FormatException("Invalid file magic"); @@ -167,9 +163,9 @@ namespace AMWD.Common.Packing.Ar return new ArFileInfoExtended { HeaderPosition = startPosition, - DataPosition = inStream.Position, + DataPosition = _inStream.Position, FileName = fileName, - ModifyTime = unixEpoch.AddSeconds(unixTimestamp), + ModifyTime = DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).DateTime, UserId = userId, GroupId = groupId, Mode = mode, diff --git a/AMWD.Common/Packing/Ar/ArWriter.cs b/AMWD.Common/Packing/Ar/ArWriter.cs index 321149c..c871079 100644 --- a/AMWD.Common/Packing/Ar/ArWriter.cs +++ b/AMWD.Common/Packing/Ar/ArWriter.cs @@ -14,8 +14,7 @@ namespace AMWD.Common.Packing.Ar { // 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); + private readonly Stream _outStream; /// /// Initialises a new instance of the class. @@ -26,7 +25,7 @@ namespace AMWD.Common.Packing.Ar if (!outStream.CanWrite) throw new ArgumentException("Stream not writable", nameof(outStream)); - this.outStream = outStream; + _outStream = outStream; Initialize(); } @@ -60,13 +59,13 @@ namespace AMWD.Common.Packing.Ar WriteFileHeader(fileName, modifyTime, userId, groupId, mode, stream.Length); // Write file contents - stream.CopyTo(outStream); + stream.CopyTo(_outStream); // Align to even offsets, pad with LF bytes - if ((outStream.Position % 2) != 0) + if ((_outStream.Position % 2) != 0) { byte[] bytes = new byte[] { 0x0A }; - outStream.Write(bytes, 0, 1); + _outStream.Write(bytes, 0, 1); } } @@ -90,7 +89,7 @@ namespace AMWD.Common.Packing.Ar WriteAsciiString(fileName.PadRight(16, ' ')); // File modification timestamp - int unixTime = (int)(modifyTime - unixEpoch).TotalSeconds; + long unixTime = ((DateTimeOffset)DateTime.SpecifyKind(modifyTime, DateTimeKind.Utc)).ToUnixTimeSeconds(); WriteAsciiString(unixTime.ToString().PadRight(12, ' ')); // User ID @@ -131,7 +130,7 @@ namespace AMWD.Common.Packing.Ar // File magic byte[] bytes = new byte[] { 0x60, 0x0A }; - outStream.Write(bytes, 0, 2); + _outStream.Write(bytes, 0, 2); } /// @@ -141,7 +140,7 @@ namespace AMWD.Common.Packing.Ar private void WriteAsciiString(string str) { byte[] bytes = Encoding.ASCII.GetBytes(str); - outStream.Write(bytes, 0, bytes.Length); + _outStream.Write(bytes, 0, bytes.Length); } } } diff --git a/AMWD.Common/Packing/Tar/TarReader.cs b/AMWD.Common/Packing/Tar/TarReader.cs index edb5bac..90de862 100644 --- a/AMWD.Common/Packing/Tar/TarReader.cs +++ b/AMWD.Common/Packing/Tar/TarReader.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +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; @@ -16,10 +14,10 @@ namespace AMWD.Common.Packing.Tar /// public class TarReader { - private readonly byte[] dataBuffer = new byte[512]; - private readonly UsTarHeader header; - private readonly Stream inStream; - private long remainingBytesInFile; + private readonly byte[] _dataBuffer = new byte[512]; + private readonly UsTarHeader _header; + private readonly Stream _inStream; + private long _remainingBytesInFile; /// /// Constructs TarReader object to read data from `tarredData` stream @@ -27,14 +25,11 @@ namespace AMWD.Common.Packing.Tar /// A stream to read tar archive from public TarReader(Stream tarredData) { - inStream = tarredData; - header = new UsTarHeader(); + _inStream = tarredData; + _header = new UsTarHeader(); } - public ITarHeader FileInfo - { - get { return header; } - } + public ITarHeader FileInfo => _header; /// /// Read all files from an archive to a directory. It creates some child directories to @@ -49,7 +44,7 @@ namespace AMWD.Common.Packing.Tar /// to your business logic. public void ReadToEnd(string destDirectory) { - while (MoveNext(false)) + while (MoveNext(skipData: false)) { string fileNameFromArchive = FileInfo.FileName; string totalPath = destDirectory + Path.DirectorySeparatorChar + fileNameFromArchive; @@ -59,14 +54,14 @@ namespace AMWD.Common.Packing.Tar 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); - } + + using FileStream file = File.Create(totalPath); + Read(file); } } @@ -74,55 +69,57 @@ namespace AMWD.Common.Packing.Tar /// Read data from a current file to a Stream. /// /// A stream to read data to - /// /// public void Read(Stream dataDestanation) { - Debug.WriteLine("tar stream position Read in: " + inStream.Position); + Debug.WriteLine("tar stream position Read in: " + _inStream.Position); + int readBytes; - byte[] read; - while ((readBytes = Read(out read)) != -1) + while ((readBytes = Read(out byte[] read)) != -1) { - Debug.WriteLine("tar stream position Read while(...) : " + inStream.Position); + Debug.WriteLine("tar stream position Read while(...) : " + _inStream.Position); dataDestanation.Write(read, 0, readBytes); } - Debug.WriteLine("tar stream position Read out: " + inStream.Position); + + Debug.WriteLine("tar stream position Read out: " + _inStream.Position); } protected int Read(out byte[] buffer) { - if (remainingBytesInFile == 0) + if (_remainingBytesInFile == 0) { buffer = null; return -1; } + int align512 = -1; - long toRead = remainingBytesInFile - 512; + long toRead = _remainingBytesInFile - 512; if (toRead > 0) + { toRead = 512; + } else { - align512 = 512 - (int)remainingBytesInFile; - toRead = remainingBytesInFile; + align512 = 512 - (int)_remainingBytesInFile; + toRead = _remainingBytesInFile; } + int bytesRead = _inStream.Read(_dataBuffer, 0, (int)toRead); + _remainingBytesInFile -= bytesRead; - int bytesRead = inStream.Read(dataBuffer, 0, (int)toRead); - remainingBytesInFile -= bytesRead; - - if (inStream.CanSeek && align512 > 0) + if (_inStream.CanSeek && align512 > 0) { - inStream.Seek(align512, SeekOrigin.Current); + _inStream.Seek(align512, SeekOrigin.Current); } else while (align512 > 0) { - inStream.ReadByte(); + _inStream.ReadByte(); --align512; } - buffer = dataBuffer; + buffer = _dataBuffer; return bytesRead; } @@ -135,7 +132,8 @@ namespace AMWD.Common.Packing.Tar { foreach (byte b in buffer) { - if (b != 0) return false; + if (b != 0) + return false; } return true; } @@ -154,56 +152,52 @@ namespace AMWD.Common.Packing.Tar /// public bool MoveNext(bool skipData) { - Debug.WriteLine("tar stream position MoveNext in: " + inStream.Position); - if (remainingBytesInFile > 0) + 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) + if (_inStream.CanSeek) { - long remainer = (remainingBytesInFile % 512); - inStream.Seek(remainingBytesInFile + (512 - (remainer == 0 ? 512 : remainer)), SeekOrigin.Current); + long remainer = _remainingBytesInFile % 512; + _inStream.Seek(_remainingBytesInFile + (512 - (remainer == 0 ? 512 : remainer)), SeekOrigin.Current); } else { - byte[] buffer; - while (Read(out buffer) != -1) - { - } + while (Read(out _) != -1) ; } } - byte[] bytes = header.GetBytes(); + byte[] bytes = _header.GetBytes(); - int headerRead = inStream.Read(bytes, 0, header.HeaderSize); + 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); + headerRead = _inStream.Read(bytes, 0, _header.HeaderSize); if (headerRead == 512 && IsEmpty(bytes)) { - Debug.WriteLine("tar stream position MoveNext out(false): " + inStream.Position); + Debug.WriteLine("tar stream position MoveNext out(false): " + _inStream.Position); return false; } throw new TarException("Broken archive"); } - if (header.UpdateHeaderFromBytes()) + if (_header.UpdateHeaderFromBytes()) { throw new TarException("Checksum check failed"); } - remainingBytesInFile = header.SizeInBytes; + _remainingBytesInFile = _header.SizeInBytes; - Debug.WriteLine("tar stream position MoveNext out(true): " + inStream.Position); + Debug.WriteLine("tar stream position MoveNext out(true): " + _inStream.Position); return true; } } diff --git a/AMWD.Common/Packing/Tar/TarWriter.cs b/AMWD.Common/Packing/Tar/TarWriter.cs index 740864b..77c8f7b 100644 --- a/AMWD.Common/Packing/Tar/TarWriter.cs +++ b/AMWD.Common/Packing/Tar/TarWriter.cs @@ -8,9 +8,9 @@ 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) - { - } + public TarWriter(Stream outStream) + : base(outStream) + { } protected override void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType) { diff --git a/AMWD.Common/Packing/Tar/Utils/DataWriter.cs b/AMWD.Common/Packing/Tar/Utils/DataWriter.cs index 10a1b23..2401fcd 100644 --- a/AMWD.Common/Packing/Tar/Utils/DataWriter.cs +++ b/AMWD.Common/Packing/Tar/Utils/DataWriter.cs @@ -5,39 +5,39 @@ namespace AMWD.Common.Packing.Tar.Utils { internal class DataWriter : IArchiveDataWriter { - private readonly long size; - private long remainingBytes; - private readonly Stream stream; + private readonly long _size; + private long _remainingBytes; + private readonly Stream _stream; public DataWriter(Stream data, long dataSizeInBytes) { - size = dataSizeInBytes; - remainingBytes = size; - stream = data; + _size = dataSizeInBytes; + _remainingBytes = _size; + _stream = data; } public bool CanWrite { get; private set; } = true; public int Write(byte[] buffer, int count) { - if (remainingBytes == 0) + if (_remainingBytes == 0) { CanWrite = false; return -1; } int bytesToWrite; - if (remainingBytes - count < 0) + if (_remainingBytes - count < 0) { - bytesToWrite = (int)remainingBytes; + bytesToWrite = (int)_remainingBytes; } else { bytesToWrite = count; } - stream.Write(buffer, 0, bytesToWrite); - remainingBytes -= bytesToWrite; + _stream.Write(buffer, 0, bytesToWrite); + _remainingBytes -= bytesToWrite; return bytesToWrite; } diff --git a/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs index ad249aa..82612d0 100644 --- a/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs +++ b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs @@ -14,10 +14,13 @@ namespace AMWD.Common.Packing.Tar.Utils /// public class LegacyTarWriter : IDisposable { - private readonly Stream outStream; + private readonly Stream _outStream; + private bool _isClosed; + + /// + /// The buffer for writing. + /// protected byte[] buffer = new byte[1024]; - private bool isClosed; - public bool ReadOnZero = true; /// /// Writes tar (see GNU tar) archive to a stream @@ -25,16 +28,25 @@ namespace AMWD.Common.Packing.Tar.Utils /// stream to write archive to public LegacyTarWriter(Stream outStream) { - this.outStream = outStream; + _outStream = outStream; } + /// + /// Gets or sets a value indicating whether to read on zero. + /// + public bool ReadOnZero { get; set; } = true; + + /// + /// Gets the output stream. + /// protected virtual Stream OutStream { - get { return outStream; } + get { return _outStream; } } #region IDisposable Members + /// public void Dispose() { Close(); @@ -85,13 +97,13 @@ namespace AMWD.Common.Packing.Tar.Utils WriteDirectoryEntry(directory, 0, 0, 0755); string[] files = Directory.GetFiles(directory); - foreach (var fileName in files) + foreach (string fileName in files) { Write(fileName); } string[] directories = Directory.GetDirectories(directory); - foreach (var dirName in directories) + foreach (string dirName in directories) { WriteDirectoryEntry(dirName, 0, 0, 0755); if (doRecursive) @@ -134,10 +146,9 @@ namespace AMWD.Common.Packing.Tar.Utils AlignTo512(dataSizeInBytes, false); } - public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode, - DateTime lastModificationTime) + public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode, DateTime lastModificationTime) { - if (isClosed) + if (_isClosed) throw new TarException("Can not write to the closed writer"); // handle long file names (> 99 characters) @@ -172,7 +183,7 @@ namespace AMWD.Common.Packing.Tar.Utils private void WriteLongName(string name, int userId, int groupId, int mode, DateTime lastModificationTime) { // must include a trailing \0 - var nameLength = Encoding.UTF8.GetByteCount(name); + int nameLength = Encoding.UTF8.GetByteCount(name); byte[] entryName = new byte[nameLength + 1]; Encoding.UTF8.GetBytes(name, 0, name.Length, entryName, 0); @@ -238,7 +249,7 @@ namespace AMWD.Common.Packing.Tar.Utils public void AlignTo512(long size, bool acceptZero) { - size = size % 512; + size %= 512; if (size == 0 && !acceptZero) return; while (size < 512) { @@ -249,10 +260,13 @@ namespace AMWD.Common.Packing.Tar.Utils public virtual void Close() { - if (isClosed) return; + if (_isClosed) + return; + AlignTo512(0, true); AlignTo512(0, true); - isClosed = true; + + _isClosed = true; } } } diff --git a/AMWD.Common/Packing/Tar/Utils/TarException.cs b/AMWD.Common/Packing/Tar/Utils/TarException.cs index c3dbf70..34ef3f4 100644 --- a/AMWD.Common/Packing/Tar/Utils/TarException.cs +++ b/AMWD.Common/Packing/Tar/Utils/TarException.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace AMWD.Common.Packing.Tar.Utils { diff --git a/AMWD.Common/Packing/Tar/Utils/TarHeader.cs b/AMWD.Common/Packing/Tar/Utils/TarHeader.cs index 01ccbc1..c1f1107 100644 --- a/AMWD.Common/Packing/Tar/Utils/TarHeader.cs +++ b/AMWD.Common/Packing/Tar/Utils/TarHeader.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Text; using AMWD.Common.Packing.Tar.Interfaces; @@ -7,8 +8,11 @@ namespace AMWD.Common.Packing.Tar.Utils { internal class TarHeader : ITarHeader { - private readonly byte[] buffer = new byte[512]; - private long headerChecksum; + private static readonly byte[] _spaces = Encoding.ASCII.GetBytes(" "); + private readonly byte[] _buffer = new byte[512]; + + private string _fileName; + private long _headerChecksum; public TarHeader() { @@ -18,63 +22,51 @@ namespace AMWD.Common.Packing.Tar.Utils 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); - } + get => _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; + + _fileName = value; } } + public int Mode { get; set; } public string ModeString - { - get { return Convert.ToString(Mode, 8).PadLeft(7, '0'); } - } + => 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); } + get => UserId.ToString(); + set => UserId = int.Parse(value); } public string UserIdString - { - get { return Convert.ToString(UserId, 8).PadLeft(7, '0'); } - } + => 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); } + get => GroupId.ToString(); + set => GroupId = int.Parse(value); } public string GroupIdString - { - get { return Convert.ToString(GroupId, 8).PadLeft(7, '0'); } - } + => Convert.ToString(GroupId, 8).PadLeft(7, '0'); public long SizeInBytes { get; set; } public string SizeString - { - get { return Convert.ToString(SizeInBytes, 8).PadLeft(11, '0'); } - } + => Convert.ToString(SizeInBytes, 8).PadLeft(11, '0'); public DateTime LastModification { get; set; } @@ -82,71 +74,63 @@ namespace AMWD.Common.Packing.Tar.Utils { get { - return Convert.ToString((long)(LastModification - TheEpoch).TotalSeconds, 8).PadLeft(11, '0'); + long unixTime = ((DateTimeOffset)DateTime.SpecifyKind(LastModification, DateTimeKind.Utc)).ToUnixTimeSeconds(); + return Convert.ToString(unixTime, 8).PadLeft(11, '0'); } } public string HeaderChecksumString - { - get { return Convert.ToString(headerChecksum, 8).PadLeft(6, '0'); } - } + => Convert.ToString(_headerChecksum, 8).PadLeft(6, '0'); - public virtual int HeaderSize - { - get { return 512; } - } + public virtual int HeaderSize => 512; - public byte[] GetBytes() - { - return buffer; - } + public byte[] GetBytes() => _buffer.ToArray(); 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); + FileName = Encoding.ASCII.GetString(_buffer, 0, 100); - EntryType = (EntryType)buffer[156]; + // 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); - if ((buffer[124] & 0x80) == 0x80) // if size in binary + EntryType = (EntryType)_buffer[156]; + + if ((_buffer[124] & 0x80) == 0x80) // if size in binary { - long sizeBigEndian = BitConverter.ToInt64(buffer, 0x80); + long sizeBigEndian = BitConverter.ToInt64(_buffer, 0x80); SizeInBytes = IPAddress.NetworkToHostOrder(sizeBigEndian); } else { - SizeInBytes = Convert.ToInt64(Encoding.ASCII.GetString(buffer, 124, 11), 8); + 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); + long unixTimeStamp = Convert.ToInt64(Encoding.ASCII.GetString(_buffer, 136, 11), 8); + LastModification = DateTimeOffset.FromUnixTimeSeconds(unixTimeStamp).DateTime; - var storedChecksum = Convert.ToInt32(Encoding.ASCII.GetString(buffer, 148, 6)); - RecalculateChecksum(buffer); - if (storedChecksum == headerChecksum) - { + int storedChecksum = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, 148, 6)); + RecalculateChecksum(_buffer); + if (storedChecksum == _headerChecksum) return true; - } - RecalculateAltChecksum(buffer); - return storedChecksum == headerChecksum; + RecalculateAltChecksum(_buffer); + return storedChecksum == _headerChecksum; } private void RecalculateAltChecksum(byte[] buf) { - spaces.CopyTo(buf, 148); - headerChecksum = 0; + _spaces.CopyTo(buf, 148); + _headerChecksum = 0; foreach (byte b in buf) { if ((b & 0x80) == 0x80) { - headerChecksum -= b ^ 0x80; + _headerChecksum -= b ^ 0x80; } else { - headerChecksum += b; + _headerChecksum += b; } } } @@ -154,41 +138,42 @@ namespace AMWD.Common.Packing.Tar.Utils public virtual byte[] GetHeaderValue() { // Clean old values - Array.Clear(buffer, 0, buffer.Length); + 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."); + 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); + 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); + _buffer[156] = ((byte)EntryType); - RecalculateChecksum(buffer); + RecalculateChecksum(_buffer); // Write checksum - Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(buffer, 148); + Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(_buffer, 148); - return buffer; + return _buffer; } protected virtual void RecalculateChecksum(byte[] buf) { // Set default value for checksum. That is 8 spaces. - spaces.CopyTo(buf, 148); + _spaces.CopyTo(buf, 148); // Calculate checksum - headerChecksum = 0; + _headerChecksum = 0; foreach (byte b in buf) - { - headerChecksum += b; - } + _headerChecksum += b; } } } diff --git a/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs b/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs index f9d530c..4f26bf3 100644 --- a/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs +++ b/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs @@ -9,59 +9,48 @@ namespace AMWD.Common.Packing.Tar.Utils /// internal class UsTarHeader : TarHeader { - private const string magic = "ustar"; - private const string version = " "; - private string groupName; + private const string Magic = "ustar"; + private const string Version = " "; - private string namePrefix = string.Empty; - private string userName; + private string _groupName; + + private string _namePrefix = string.Empty; + private string _userName; public override string UserName { - get - { - return userName.Replace("\0", string.Empty); - } + get => _userName.Replace("\0", string.Empty); set { if (value.Length > 32) - { throw new TarException("user name can not be longer than 32 chars"); - } - userName = value; + + _userName = value; } } public override string GroupName { - get - { - return groupName.Replace("\0", string.Empty); - } + get => _groupName.Replace("\0", string.Empty); set { if (value.Length > 32) - { throw new TarException("group name can not be longer than 32 chars"); - } - groupName = value; + + _groupName = value; } } public override string FileName { - get - { - return namePrefix.Replace("\0", string.Empty) + base.FileName.Replace("\0", string.Empty); - } + get => _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 @@ -75,7 +64,8 @@ namespace AMWD.Common.Packing.Tar.Utils } if (position == value.Length) position = value.Length - 100; - namePrefix = value.Substring(0, position); + _namePrefix = value.Substring(0, position); + base.FileName = value.Substring(position, value.Length - position); } else @@ -88,26 +78,26 @@ namespace AMWD.Common.Packing.Tar.Utils 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); + _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. - } + => 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(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); + Encoding.ASCII.GetBytes(_namePrefix).CopyTo(header, 347); if (SizeInBytes >= 0x1FFFFFFFF) { @@ -117,6 +107,7 @@ namespace AMWD.Common.Packing.Tar.Utils RecalculateChecksum(header); Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(header, 148); + return header; } @@ -128,7 +119,7 @@ namespace AMWD.Common.Packing.Tar.Utils private static byte[] AlignTo12(byte[] bytes) { - var retVal = new byte[12]; + byte[] retVal = new byte[12]; bytes.CopyTo(retVal, 12 - bytes.Length); return retVal; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bdc6ca..df739fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ###### Diffs -- [AMWD.Common](https://git.am-wd.de/AM.WD/common/compare/v2.0.1...main) -- [AMWD.Common.AspNetCore](https://git.am-wd.de/AM.WD/common/compare/asp/v3.0.0...main) -- [AMWD.Common.EntityFrameworkCore](https://git.am-wd.de/AM.WD/common/compare/efc/v3.0.0...main) -- [AMWD.Common.Test](https://git.am-wd.de/AM.WD/common/compare/test/v2.1.1...main) +- [AMWD.Common](https://git.am-wd.de/AM.WD/common/compare/v2.0.1...HEAD) +- [AMWD.Common.AspNetCore](https://git.am-wd.de/AM.WD/common/compare/asp/v3.0.0...HEAD) +- [AMWD.Common.EntityFrameworkCore](https://git.am-wd.de/AM.WD/common/compare/efc/v3.0.0...HEAD) +- [AMWD.Common.Test](https://git.am-wd.de/AM.WD/common/compare/test/v2.1.1...HEAD) ### Added diff --git a/Directory.Build.props b/Directory.Build.props index 06b7292..3d0cb4f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,11 +30,11 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 27cd54fb30f53992176f970c8ba4fa85cd387d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Sun, 14 Jan 2024 13:10:33 +0100 Subject: [PATCH 4/4] Updated to C# 12 --- .gitlab-ci.yml | 21 ++- .../AMWD.Common.AspNetCore.csproj | 2 +- .../Extensions/HttpContextExtensions.cs | 6 +- .../InvariantFloatingPointModelBinder.cs | 29 ++-- .../BasicAuthenticationHandler.cs | 66 ++++---- .../BasicAuthenticationMiddleware.cs | 38 +++-- .../PathProtection/ProtectedPathMiddleware.cs | 25 ++-- .../TagHelpers/ConditionClassTagHelper.cs | 2 +- .../TagHelpers/IntegrityHashTagHelper.cs | 25 ++-- .../TagHelpers/NumberInputTagHelper.cs | 14 +- .../AMWD.Common.EntityFrameworkCore.csproj | 12 +- .../Exceptions/DatabaseProviderException.cs | 14 +- .../Extensions/DatabaseFacadeExtensions.cs | 38 ++++- .../DbContextOptionsBuilderExtensions.cs | 6 +- .../AMWD.Common.MessagePack.csproj | 32 ++++ .../Formatters/IPAddressArrayFormatter.cs | 4 +- .../Formatters/IPAddressFormatter.cs | 0 .../Formatters/IPAddressListFormatter.cs | 4 +- .../Formatters/IPNetworkArrayFormatter.cs | 11 +- .../Formatters/IPNetworkFormatter.cs | 13 +- .../Formatters/IPNetworkListFormatter.cs | 7 +- .../Utilities/NetworkHelper.cs | 17 +++ AMWD.Common.Test/AMWD.Common.Test.csproj | 4 +- AMWD.Common/AMWD.Common.csproj | 21 ++- AMWD.Common/Cli/CommandLineParser.cs | 12 +- AMWD.Common/Cli/EnumerableWalker.cs | 19 +-- AMWD.Common/Cli/Option.cs | 2 +- .../Converters/ByteArrayHexConverter.cs | 10 +- AMWD.Common/Converters/IPAddressConverter.cs | 10 +- AMWD.Common/Converters/IPNetworkConverter.cs | 33 ++-- AMWD.Common/Extensions/DateTimeExtensions.cs | 2 - AMWD.Common/Extensions/EnumExtensions.cs | 9 -- .../ReaderWriterLockSlimExtensions.cs | 13 +- AMWD.Common/Extensions/StreamExtensions.cs | 4 +- AMWD.Common/Extensions/StringExtensions.cs | 21 ++- AMWD.Common/Logging/FileLogger.cs | 2 - AMWD.Common/Packing/Ar/ArWriter.cs | 2 +- AMWD.Common/Packing/Tar/TarReader.cs | 29 ++-- AMWD.Common/Packing/Tar/TarWriter.cs | 52 ++++++- .../Packing/Tar/Utils/LegacyTarWriter.cs | 141 ++++++++++++------ AMWD.Common/Packing/Tar/Utils/TarException.cs | 47 +++++- AMWD.Common/Utilities/AsyncQueue.cs | 10 +- AMWD.Common/Utilities/CryptographyHelper.cs | 42 +++++- AMWD.Common/Utilities/NetworkHelper.cs | 59 ++------ Common.sln | 7 + Directory.Build.props | 3 +- .../BasicAuthenticationMiddlewareTests.cs | 12 +- .../Common/Extensions/EnumExtensionsTests.cs | 17 --- UnitTests/Common/Packing/Ar/ArReaderTests.cs | 30 ++-- .../Common/Packing/Tar/TarReaderTests.cs | 12 ++ UnitTests/UnitTests.csproj | 5 +- 51 files changed, 637 insertions(+), 379 deletions(-) create mode 100644 AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj rename {AMWD.Common => AMWD.Common.MessagePack}/Formatters/IPAddressArrayFormatter.cs (92%) rename {AMWD.Common => AMWD.Common.MessagePack}/Formatters/IPAddressFormatter.cs (100%) rename {AMWD.Common => AMWD.Common.MessagePack}/Formatters/IPAddressListFormatter.cs (91%) rename {AMWD.Common => AMWD.Common.MessagePack}/Formatters/IPNetworkArrayFormatter.cs (86%) rename {AMWD.Common => AMWD.Common.MessagePack}/Formatters/IPNetworkFormatter.cs (82%) rename {AMWD.Common => AMWD.Common.MessagePack}/Formatters/IPNetworkListFormatter.cs (87%) create mode 100644 AMWD.Common.MessagePack/Utilities/NetworkHelper.cs create mode 100644 UnitTests/Common/Packing/Tar/TarReaderTests.cs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b8a4a9a..53b649e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,7 +28,9 @@ build-debug: - mv ./AMWD.Common.AspNetCore/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Common.EntityFrameworkCore/bin/Debug/*.nupkg ./artifacts/ - - mv ./AMWD.Common.EntityFrameworkCore/bin/Debug/*.snupkg ./artifacts/ + - mv ./AMWD.Common.EntityFrameworkCore/bin/Debug/*.snupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Debug/*.nupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Debug/*.snupkg ./artifacts/ artifacts: @@ -72,7 +74,9 @@ build-release: - mv ./AMWD.Common.AspNetCore/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Common.EntityFrameworkCore/bin/Release/*.nupkg ./artifacts/ - - mv ./AMWD.Common.EntityFrameworkCore/bin/Release/*.snupkg ./artifacts/ + - mv ./AMWD.Common.EntityFrameworkCore/bin/Release/*.snupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Release/*.nupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Release/*.snupkg ./artifacts/ artifacts: @@ -136,6 +140,19 @@ deploy-entityframework: - if: $CI_COMMIT_TAG =~ /^efc\/v[0-9.]+/ script: - dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/AMWD.Common.EntityFrameworkCore.*.nupkg + +deploy-messagepack: + stage: deploy + dependencies: + - build-release + - test-release + tags: + - docker + - lnx + rules: + - if: $CI_COMMIT_TAG =~ /^msgpack\/v[0-9.]+/ + script: + - dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/AMWD.Common.MessagePack.*.nupkg deploy-test: stage: deploy diff --git a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj index e7d5277..7cece0b 100644 --- a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj +++ b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj @@ -2,7 +2,7 @@ net6.0;net8.0 - 10.0 + 12.0 asp/v[0-9]* AMWD.Common.AspNetCore diff --git a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs index c341fca..b39bb3f 100644 --- a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs @@ -12,12 +12,12 @@ namespace Microsoft.AspNetCore.Http public static class HttpContextExtensions { // Search these additional headers for a remote client ip address. - private static readonly string[] _defaultIpHeaderNames = new[] - { + private static readonly string[] _defaultIpHeaderNames = + [ "Cf-Connecting-Ip", // set by Cloudflare "X-Real-IP", // wide-spread alternative to X-Forwarded-For "X-Forwarded-For", // commonly used on all known proxies - }; + ]; /// /// Retrieves the antiforgery token. diff --git a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs index 515b572..812b39f 100644 --- a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs +++ b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs @@ -9,26 +9,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their issue with other cultures than en-US. /// + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + /// The . [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class InvariantFloatingPointModelBinder : IModelBinder + public class InvariantFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory) + : IModelBinder { - private readonly NumberStyles _supportedNumberStyles; - private readonly ILogger _logger; - private readonly CultureInfo _cultureInfo; - - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - /// The . - public InvariantFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory) - { - _cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo)); - - _supportedNumberStyles = supportedStyles; - _logger = loggerFactory?.CreateLogger(); - } + private readonly NumberStyles _supportedNumberStyles = supportedStyles; + private readonly ILogger _logger = loggerFactory?.CreateLogger(); + private readonly CultureInfo _cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo)); /// public Task BindModelAsync(ModelBindingContext bindingContext) diff --git a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs index f6b90ab..da20985 100644 --- a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs @@ -13,49 +13,39 @@ using Microsoft.Extensions.Options; namespace AMWD.Common.AspNetCore.Security.BasicAuthentication { +#if NET8_0_OR_GREATER /// /// Implements the for Basic Authentication. /// + /// + /// Initializes a new instance of the class. + /// + /// The monitor for the options instance. + /// The . + /// The . + /// An basic autentication validator implementation. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class BasicAuthenticationHandler : AuthenticationHandler + public class BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IBasicAuthenticationValidator validator) + : AuthenticationHandler(options, logger, encoder) +#else + /// + /// Implements the for Basic Authentication. + /// + /// + /// Initializes a new instance of the class. + /// + /// The monitor for the options instance. + /// The . + /// The . + /// The . + /// An basic autentication validator implementation. + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicAuthenticationValidator validator) + : AuthenticationHandler(options, logger, encoder, clock) +#endif { - private readonly ILogger _logger; - private readonly IBasicAuthenticationValidator _validator; - -#if NET8_0_OR_GREATER - /// - /// Initializes a new instance of the class. - /// - /// The monitor for the options instance. - /// The . - /// The . - /// An basic autentication validator implementation. - public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IBasicAuthenticationValidator validator) - : base(options, logger, encoder) - { - _logger = logger.CreateLogger(); - _validator = validator; - } -#endif - -#if NET6_0 - - /// - /// Initializes a new instance of the class. - /// - /// The monitor for the options instance. - /// The . - /// The . - /// The . - /// An basic autentication validator implementation. - public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicAuthenticationValidator validator) - : base(options, logger, encoder, clock) - { - _logger = logger.CreateLogger(); - _validator = validator; - } - -#endif + private readonly ILogger _logger = logger.CreateLogger(); + private readonly IBasicAuthenticationValidator _validator = validator; /// protected override async Task HandleAuthenticateAsync() diff --git a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs index 9bde87a..ca7475d 100644 --- a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs +++ b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs @@ -11,21 +11,15 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication /// /// Implements a basic authentication. /// - public class BasicAuthenticationMiddleware + /// + /// Initializes a new instance of the class. + /// + /// The following delegate in the process chain. + /// A basic authentication validator. + public class BasicAuthenticationMiddleware(RequestDelegate next, IBasicAuthenticationValidator validator) { - private readonly RequestDelegate _next; - private readonly IBasicAuthenticationValidator _validator; - - /// - /// Initializes a new instance of the class. - /// - /// The following delegate in the process chain. - /// A basic authentication validator. - public BasicAuthenticationMiddleware(RequestDelegate next, IBasicAuthenticationValidator validator) - { - _next = next; - _validator = validator; - } + private readonly RequestDelegate _next = next; + private readonly IBasicAuthenticationValidator _validator = validator; /// /// The delegate invokation. @@ -35,15 +29,27 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication /// An awaitable task. public async Task InvokeAsync(HttpContext httpContext) { +#if NET8_0_OR_GREATER + if (!httpContext.Request.Headers.TryGetValue("Authorization", out var authHeaderValue)) + { + SetAuthenticateRequest(httpContext, _validator.Realm); + return; + } +#else if (!httpContext.Request.Headers.ContainsKey("Authorization")) { SetAuthenticateRequest(httpContext, _validator.Realm); return; } +#endif try { +#if NET8_0_OR_GREATER + var authHeader = AuthenticationHeaderValue.Parse(authHeaderValue); +#else var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]); +#endif byte[] decoded = Convert.FromBase64String(authHeader.Parameter); string plain = Encoding.UTF8.GetString(decoded); @@ -70,9 +76,9 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication private static void SetAuthenticateRequest(HttpContext httpContext, string realm) { - httpContext.Response.Headers["WWW-Authenticate"] = "Basic"; + httpContext.Response.Headers.WWWAuthenticate = "Basic"; if (!string.IsNullOrWhiteSpace(realm)) - httpContext.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{realm.Replace("\"", "")}\""; + httpContext.Response.Headers.WWWAuthenticate = $"Basic realm=\"{realm.Replace("\"", "")}\""; httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; } diff --git a/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs index 8800f3f..41ab28d 100644 --- a/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs +++ b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs @@ -8,23 +8,16 @@ namespace AMWD.Common.AspNetCore.Security.PathProtection /// /// Implements a check to provide protected paths. /// - public class ProtectedPathMiddleware + /// + /// Initializes a new instance of the class. + /// + /// The following delegate in the process chain. + /// The options to configure the middleware. + public class ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options) { - private readonly RequestDelegate _next; - private readonly PathString _path; - private readonly string _policyName; - - /// - /// Initializes a new instance of the class. - /// - /// The following delegate in the process chain. - /// The options to configure the middleware. - public ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options) - { - _next = next; - _path = options.Path; - _policyName = options.PolicyName; - } + private readonly RequestDelegate _next = next; + private readonly PathString _path = options.Path; + private readonly string _policyName = options.PolicyName; /// /// The delegate invokation. diff --git a/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs index 8492cea..b19d552 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers if (items.Any()) { - string classes = string.Join(" ", items.ToArray()); + string classes = string.Join(" ", [.. items]); output.Attributes.Add("class", classes); } } diff --git a/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs index c523a8d..d0290f1 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs @@ -13,24 +13,19 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// /// A tag helper to dynamically create integrity checks for linked sources. /// + /// + /// Initializes a new instance of the class. + /// + /// The web host environment. + /// The application configuration. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement("link")] [HtmlTargetElement("script")] - public class IntegrityHashTagHelper : TagHelper + public class IntegrityHashTagHelper(IWebHostEnvironment env, IConfiguration configuration) + : TagHelper { - private readonly IWebHostEnvironment _env; - private readonly string _hostUrl; - - /// - /// Initializes a new instance of the class. - /// - /// The web host environment. - /// The application configuration. - public IntegrityHashTagHelper(IWebHostEnvironment env, IConfiguration configuration) - { - _env = env; - _hostUrl = configuration.GetValue("ASPNETCORE_APPL_URL", "http://localhost/"); - } + private readonly IWebHostEnvironment _env = env; + private readonly string _hostUrl = configuration.GetValue("ASPNETCORE_APPL_URL", "http://localhost/"); /// /// Gets or sets a value indicating whether the integrity should be calculated. @@ -118,7 +113,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers } string type; - byte[] hashBytes = Array.Empty(); + byte[] hashBytes = []; switch (IntegrityStrength) { case 512: diff --git a/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs index 10aad90..41ae538 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs @@ -9,17 +9,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers /// /// Adds additional behavior to the modelbinding for numeric properties. /// + /// + /// Initializes a new instance of the class. + /// + /// The HTML generator. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement("input", Attributes = "asp-for")] - public class NumberInputTagHelper : InputTagHelper + public class NumberInputTagHelper(IHtmlGenerator generator) + : InputTagHelper(generator) { - /// - /// Initializes a new instance of the class. - /// - /// The HTML generator. - public NumberInputTagHelper(IHtmlGenerator generator) - : base(generator) - { } /// public override void Process(TagHelperContext context, TagHelperOutput output) diff --git a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj index 8409a10..4d76a0a 100644 --- a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj +++ b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ net6.0;net8.0 - 10.0 + 12.0 efc/v[0-9]* AMWD.Common.EntityFrameworkCore @@ -22,17 +22,17 @@ - - + + - - + + - + diff --git a/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs b/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs index 6475393..18dbd7f 100644 --- a/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs +++ b/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs @@ -1,6 +1,4 @@ -using System.Runtime.Serialization; - -namespace System +namespace System { /// /// A DatabaseProvider specific exception. @@ -33,16 +31,16 @@ namespace System : base(message, innerException) { } -#if NET6_0 +#if !NET8_0_OR_GREATER /// /// Initializes a new instance of the class with serialized data. /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. /// The info parameter is null. - /// The class name is null or is zero (0). - protected DatabaseProviderException(SerializationInfo info, StreamingContext context) + /// The class name is null or is zero (0). + protected DatabaseProviderException(Runtime.Serialization.SerializationInfo info, Runtime.Serialization.StreamingContext context) : base(info, context) { } diff --git a/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs b/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs index 1e6f27c..dbcb0b2 100644 --- a/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs +++ b/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs @@ -15,7 +15,11 @@ namespace Microsoft.EntityFrameworkCore /// /// Extensions for the . /// +#if NET8_0_OR_GREATER + public static partial class DatabaseFacadeExtensions +#else public static class DatabaseFacadeExtensions +#endif { /// /// Applies migration files to the database. @@ -23,7 +27,7 @@ namespace Microsoft.EntityFrameworkCore /// The database connection. /// An action to set additional options. /// The cancellation token. - /// true on success, otherwise false or an exception is thrown. + /// on success, otherwise false or an exception is thrown. [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208")] public static async Task ApplyMigrationsAsync(this DatabaseFacade database, Action optionsAction, CancellationToken cancellationToken = default) { @@ -211,20 +215,20 @@ END;" if (options.SourceAssembly == null) { availableMigrationFiles = Directory.GetFiles(options.Path) - .Where(f => f.ToLower().StartsWith(options.Path.ToLower())) - .Where(f => f.ToLower().EndsWith(".sql")) + .Where(f => f.StartsWith(options.Path, StringComparison.OrdinalIgnoreCase)) + .Where(f => f.EndsWith(".sql", StringComparison.OrdinalIgnoreCase)) .ToList(); } else { availableMigrationFiles = options.SourceAssembly .GetManifestResourceNames() - .Where(f => f.ToLower().StartsWith(options.Path.ToLower())) - .Where(f => f.ToLower().EndsWith(".sql")) + .Where(f => f.StartsWith(options.Path, StringComparison.OrdinalIgnoreCase)) + .Where(f => f.EndsWith(".sql", StringComparison.OrdinalIgnoreCase)) .ToList(); } - if (!availableMigrationFiles.Any()) + if (availableMigrationFiles.Count == 0) return true; using var command = connection.CreateCommand(); @@ -270,7 +274,11 @@ END;" { using var stream = options.SourceAssembly.GetManifestResourceStream(migrationFile); using var sr = new StreamReader(stream); +#if NET8_0_OR_GREATER + sqlScript = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); +#else sqlScript = await sr.ReadToEndAsync().ConfigureAwait(false); +#endif } if (string.IsNullOrWhiteSpace(sqlScript)) @@ -316,7 +324,11 @@ END;" { int affectedRows = 0; // Split script by a single slash in a line +#if NET8_0_OR_GREATER + string[] parts = FindSingleSlashInLine().Split(text); +#else string[] parts = Regex.Split(text, @"\r?\n[ \t]*/[ \t]*\r?\n"); +#endif foreach (string part in parts) { // Make writable copy @@ -325,7 +337,11 @@ END;" // Remove the trailing semicolon from commands where they're not supported // (Oracle doesn't like semicolons. To keep the semicolon, it must be directly // preceeded by "end".) - pt = Regex.Replace(pt.TrimEnd(), @"(?The with applied settings. public static DbContextOptionsBuilder UseDatabaseProvider(this DbContextOptionsBuilder optionsBuilder, IConfiguration configuration, Action optionsAction = null) { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(optionsBuilder); + ArgumentNullException.ThrowIfNull(configuration); +#else if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); +#endif var options = new DatabaseProviderOptions(); optionsAction?.Invoke(options); diff --git a/AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj b/AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj new file mode 100644 index 0000000..986d0c6 --- /dev/null +++ b/AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0;net6.0;net8.0 + 12.0 + + msgpack/v[0-9]* + AMWD.Common.MessagePack + AMWD.Common.MessagePack + + true + AMWD.Common.MessagePack + icon.png + README.md + + AM.WD Common Library for MessagePack + + + + + + + + + + + + + + + + diff --git a/AMWD.Common/Formatters/IPAddressArrayFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPAddressArrayFormatter.cs similarity index 92% rename from AMWD.Common/Formatters/IPAddressArrayFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPAddressArrayFormatter.cs index 6e9a119..0e40405 100644 --- a/AMWD.Common/Formatters/IPAddressArrayFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPAddressArrayFormatter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using AMWD.Common.Utilities; +using AMWD.Common.MessagePack.Utilities; namespace MessagePack.Formatters { @@ -66,7 +66,7 @@ namespace MessagePack.Formatters bytes.AddRange(buffer); } - options.Resolver.GetFormatterWithVerify().Serialize(ref writer, bytes.ToArray(), options); + options.Resolver.GetFormatterWithVerify().Serialize(ref writer, [.. bytes], options); } } } diff --git a/AMWD.Common/Formatters/IPAddressFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPAddressFormatter.cs similarity index 100% rename from AMWD.Common/Formatters/IPAddressFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPAddressFormatter.cs diff --git a/AMWD.Common/Formatters/IPAddressListFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPAddressListFormatter.cs similarity index 91% rename from AMWD.Common/Formatters/IPAddressListFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPAddressListFormatter.cs index 0a750bb..1a205b2 100644 --- a/AMWD.Common/Formatters/IPAddressListFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPAddressListFormatter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using AMWD.Common.Utilities; +using AMWD.Common.MessagePack.Utilities; namespace MessagePack.Formatters { @@ -61,7 +61,7 @@ namespace MessagePack.Formatters bytes.AddRange(buffer); } - options.Resolver.GetFormatterWithVerify().Serialize(ref writer, bytes.ToArray(), options); + options.Resolver.GetFormatterWithVerify().Serialize(ref writer, [.. bytes], options); } } } diff --git a/AMWD.Common/Formatters/IPNetworkArrayFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPNetworkArrayFormatter.cs similarity index 86% rename from AMWD.Common/Formatters/IPNetworkArrayFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPNetworkArrayFormatter.cs index 9866b1f..dfb8550 100644 --- a/AMWD.Common/Formatters/IPNetworkArrayFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPNetworkArrayFormatter.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; -using AMWD.Common.Utilities; -using MessagePack; -using MessagePack.Formatters; +using AMWD.Common.MessagePack.Utilities; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif -namespace AMWD.Common.Formatters +namespace MessagePack.Formatters { /// /// Serialization of an array to and from . diff --git a/AMWD.Common/Formatters/IPNetworkFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPNetworkFormatter.cs similarity index 82% rename from AMWD.Common/Formatters/IPNetworkFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPNetworkFormatter.cs index 9c408d9..a0aeffb 100644 --- a/AMWD.Common/Formatters/IPNetworkFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPNetworkFormatter.cs @@ -1,7 +1,12 @@ using System; using System.Linq; using System.Net; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif namespace MessagePack.Formatters { @@ -15,7 +20,7 @@ namespace MessagePack.Formatters public IPNetwork Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { if (reader.IsNil) - return null; + return default; byte[] bytes = options.Resolver.GetFormatterWithVerify().Deserialize(ref reader, options); return DeserializeInternal(bytes); @@ -24,7 +29,7 @@ namespace MessagePack.Formatters /// public void Serialize(ref MessagePackWriter writer, IPNetwork value, MessagePackSerializerOptions options) { - if (value == null) + if (value == default) { writer.WriteNil(); return; @@ -38,7 +43,11 @@ namespace MessagePack.Formatters { // IP network prefix has a maximum of 128 bit - therefore the length can be covered with a byte. byte prefixLength = (byte)network.PrefixLength; +#if NET8_0_OR_GREATER + byte[] prefixBytes = network.BaseAddress.GetAddressBytes(); +#else byte[] prefixBytes = network.Prefix.GetAddressBytes(); +#endif byte[] bytes = new byte[prefixBytes.Length + 1]; bytes[0] = prefixLength; diff --git a/AMWD.Common/Formatters/IPNetworkListFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPNetworkListFormatter.cs similarity index 87% rename from AMWD.Common/Formatters/IPNetworkListFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPNetworkListFormatter.cs index 79b3499..7b0f53e 100644 --- a/AMWD.Common/Formatters/IPNetworkListFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPNetworkListFormatter.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using AMWD.Common.Utilities; +using AMWD.Common.MessagePack.Utilities; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif namespace MessagePack.Formatters { diff --git a/AMWD.Common.MessagePack/Utilities/NetworkHelper.cs b/AMWD.Common.MessagePack/Utilities/NetworkHelper.cs new file mode 100644 index 0000000..768adab --- /dev/null +++ b/AMWD.Common.MessagePack/Utilities/NetworkHelper.cs @@ -0,0 +1,17 @@ +using System; + +namespace AMWD.Common.MessagePack.Utilities +{ + /// + /// Provides some network utils. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal static class NetworkHelper + { + public static void SwapBigEndian(byte[] array) + { + if (BitConverter.IsLittleEndian) + Array.Reverse(array); + } + } +} diff --git a/AMWD.Common.Test/AMWD.Common.Test.csproj b/AMWD.Common.Test/AMWD.Common.Test.csproj index f68fc70..fcb3652 100644 --- a/AMWD.Common.Test/AMWD.Common.Test.csproj +++ b/AMWD.Common.Test/AMWD.Common.Test.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 10.0 + 12.0 test/v[0-9]* AMWD.Common.Test @@ -22,7 +22,7 @@ - + diff --git a/AMWD.Common/AMWD.Common.csproj b/AMWD.Common/AMWD.Common.csproj index e2a49d9..d28ffb4 100644 --- a/AMWD.Common/AMWD.Common.csproj +++ b/AMWD.Common/AMWD.Common.csproj @@ -1,8 +1,8 @@  - netstandard2.0 - 10.0 + netstandard2.0;net8.0 + 12.0 AMWD.Common AMWD.Common @@ -21,10 +21,23 @@ - - + + + + + + + + + + + + <_Parameter1>UnitTests + + + diff --git a/AMWD.Common/Cli/CommandLineParser.cs b/AMWD.Common/Cli/CommandLineParser.cs index 76e3537..a7ceecd 100644 --- a/AMWD.Common/Cli/CommandLineParser.cs +++ b/AMWD.Common/Cli/CommandLineParser.cs @@ -14,7 +14,7 @@ namespace AMWD.Common.Cli private string[] _args; private List _parsedArguments; - private readonly List [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class IPNetworkConverter : JsonConverter + public class IpNetworkConverter : JsonConverter { /// /// List of known types to use this converver. /// - public static readonly Type[] KnownTypes = new[] - { + public static readonly Type[] KnownTypes = + [ typeof(IPNetwork), typeof(IPNetwork[]), typeof(List), typeof(IEnumerable) - }; + ]; /// public override bool CanConvert(Type objectType) - { - return KnownTypes.Contains(objectType); - } + => KnownTypes.Contains(objectType); /// public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) @@ -40,7 +43,7 @@ namespace Newtonsoft.Json if (typeof(IPNetwork) == objectType) return Parse(str); - var networks = str.Split(';').Select(s => Parse(s)); + var networks = str.Split(';').Select(Parse); if (typeof(IPNetwork[]) == objectType) return networks.ToArray(); @@ -65,23 +68,27 @@ namespace Newtonsoft.Json str = ToString(net); if (value is IPNetwork[] netArray) - str = string.Join(";", netArray.Select(n => ToString(n))); + str = string.Join(";", netArray.Select(ToString)); if (value is List netList) - str = string.Join(";", netList.Select(n => ToString(n))); + str = string.Join(";", netList.Select(ToString)); if (value is IEnumerable netEnum) - str = string.Join(";", netEnum.Select(n => ToString(n))); + str = string.Join(";", netEnum.Select(ToString)); writer.WriteValue(str); } - private string ToString(IPNetwork net) + private static string ToString(IPNetwork net) { +#if NET8_0_OR_GREATER + return $"{net.BaseAddress}/{net.PrefixLength}"; +#else return $"{net.Prefix}/{net.PrefixLength}"; +#endif } - private IPNetwork Parse(string str) + private static IPNetwork Parse(string str) { string[] parts = str.Split('/'); var prefix = IPAddress.Parse(parts.First()); diff --git a/AMWD.Common/Extensions/DateTimeExtensions.cs b/AMWD.Common/Extensions/DateTimeExtensions.cs index 2da0626..becc9f1 100644 --- a/AMWD.Common/Extensions/DateTimeExtensions.cs +++ b/AMWD.Common/Extensions/DateTimeExtensions.cs @@ -1,7 +1,5 @@ using System.Text; -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")] - namespace System { /// diff --git a/AMWD.Common/Extensions/EnumExtensions.cs b/AMWD.Common/Extensions/EnumExtensions.cs index ee4d431..1a5cece 100644 --- a/AMWD.Common/Extensions/EnumExtensions.cs +++ b/AMWD.Common/Extensions/EnumExtensions.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.DataAnnotations; using System.Linq; namespace System @@ -41,13 +40,5 @@ namespace System /// The description or the string representation of the value. public static string GetDescription(this Enum value) => value.GetAttribute()?.Description ?? value.ToString(); - - /// - /// Returns the name from . - /// - /// The enum value. - /// The display name or the string representation of the value. - public static string GetDisplayName(this Enum value) - => value.GetAttribute()?.Name ?? value.ToString(); } } diff --git a/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs b/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs index eb644a0..3562252 100644 --- a/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs +++ b/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs @@ -56,16 +56,11 @@ return new DisposableReadWriteLock(rwLock, LockMode.Write); } - private struct DisposableReadWriteLock : IDisposable + private struct DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode) + : IDisposable { - private readonly ReaderWriterLockSlim _rwLock; - private LockMode _lockMode; - - public DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode) - { - _rwLock = rwLock; - _lockMode = lockMode; - } + private readonly ReaderWriterLockSlim _rwLock = rwLock; + private LockMode _lockMode = lockMode; public void Dispose() { diff --git a/AMWD.Common/Extensions/StreamExtensions.cs b/AMWD.Common/Extensions/StreamExtensions.cs index c56409e..481147b 100644 --- a/AMWD.Common/Extensions/StreamExtensions.cs +++ b/AMWD.Common/Extensions/StreamExtensions.cs @@ -40,7 +40,7 @@ namespace System.IO } while (ch != eol); - return encoding.GetString(bytes.ToArray()).Trim(); + return encoding.GetString([.. bytes]).Trim(); } /// @@ -73,7 +73,7 @@ namespace System.IO } while (ch != eol); - return encoding.GetString(bytes.ToArray()).Trim(); + return encoding.GetString([.. bytes]).Trim(); } } } diff --git a/AMWD.Common/Extensions/StringExtensions.cs b/AMWD.Common/Extensions/StringExtensions.cs index cb520b3..7a7e802 100644 --- a/AMWD.Common/Extensions/StringExtensions.cs +++ b/AMWD.Common/Extensions/StringExtensions.cs @@ -15,7 +15,12 @@ namespace System /// /// String extensions. /// +#if NET8_0_OR_GREATER + public static partial class StringExtensions +#else + public static class StringExtensions +#endif { /// /// Converts a hex string into a byte array. @@ -32,8 +37,13 @@ namespace System if (str.Length % 2 == 1) yield break; +#if NET8_0_OR_GREATER + if (InvalidHexCharRegex().IsMatch(str)) + yield break; +#else if (Regex.IsMatch(str, "[^0-9a-fA-F]")) yield break; +#endif for (int i = 0; i < str.Length; i += 2) yield return Convert.ToByte(str.Substring(i, 2), 16); @@ -179,14 +189,14 @@ namespace System { var dnsClientType = Type.GetType("DNS.Client.DnsClient, DNS") ?? throw new DllNotFoundException("The DNS NuGet package is required: https://www.nuget.org/packages/DNS/7.0.0"); var recordTypeType = Type.GetType("DNS.Protocol.RecordType, DNS"); - var resolveMethodInfo = dnsClientType.GetMethod("Resolve", new[] { typeof(string), recordTypeType, typeof(CancellationToken) }); + var resolveMethodInfo = dnsClientType.GetMethod("Resolve", [typeof(string), recordTypeType, typeof(CancellationToken)]); bool exists = false; foreach (var nameserver in nameservers) { - object dnsClient = Activator.CreateInstance(dnsClientType, new object[] { nameserver }); + object dnsClient = Activator.CreateInstance(dnsClientType, [nameserver]); - var waitTask = Task.Run(async () => await resolveMethodInfo.InvokeAsync(dnsClient, new object[] { mailAddress.Host, 15, CancellationToken.None })); // 15 = MX Record + var waitTask = Task.Run(async () => await resolveMethodInfo.InvokeAsync(dnsClient, [mailAddress.Host, 15, CancellationToken.None])); // 15 = MX Record waitTask.Wait(); object response = waitTask.Result; @@ -232,5 +242,10 @@ namespace System /// public static StringBuilder AppendLine(this StringBuilder sb, string value, string newLine) => sb.Append(value).Append(newLine); + +#if NET8_0_OR_GREATER + [GeneratedRegex("[^0-9a-fA-F]")] + private static partial Regex InvalidHexCharRegex(); +#endif } } diff --git a/AMWD.Common/Logging/FileLogger.cs b/AMWD.Common/Logging/FileLogger.cs index 11f3ae1..63c2452 100644 --- a/AMWD.Common/Logging/FileLogger.cs +++ b/AMWD.Common/Logging/FileLogger.cs @@ -6,8 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")] - namespace AMWD.Common.Logging { /// diff --git a/AMWD.Common/Packing/Ar/ArWriter.cs b/AMWD.Common/Packing/Ar/ArWriter.cs index c871079..fbb948f 100644 --- a/AMWD.Common/Packing/Ar/ArWriter.cs +++ b/AMWD.Common/Packing/Ar/ArWriter.cs @@ -8,7 +8,7 @@ namespace AMWD.Common.Packing.Ar /// Writes UNIX ar (archive) files in the GNU format. /// /// - /// Copied from + /// Copied from: DotnetMakeDeb /// public class ArWriter { diff --git a/AMWD.Common/Packing/Tar/TarReader.cs b/AMWD.Common/Packing/Tar/TarReader.cs index 90de862..51f891f 100644 --- a/AMWD.Common/Packing/Tar/TarReader.cs +++ b/AMWD.Common/Packing/Tar/TarReader.cs @@ -7,28 +7,24 @@ using AMWD.Common.Packing.Tar.Utils; namespace AMWD.Common.Packing.Tar { /// - /// Extract contents of a tar file represented by a stream for the TarReader constructor + /// Extract contents of a tar file represented by a stream for the TarReader constructor. /// /// - /// https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarReader.cs + /// Constructs TarReader object to read data from `tarredData` stream. + ///
+ /// Copied from: DotnetMakeDeb ///
- public class TarReader + /// A stream to read tar archive from + public class TarReader(Stream tarredData) { private readonly byte[] _dataBuffer = new byte[512]; - private readonly UsTarHeader _header; - private readonly Stream _inStream; + private readonly UsTarHeader _header = new(); + private readonly Stream _inStream = tarredData; private long _remainingBytesInFile; /// - /// Constructs TarReader object to read data from `tarredData` stream + /// Gets the file info (the header). /// - /// A stream to read tar archive from - public TarReader(Stream tarredData) - { - _inStream = tarredData; - _header = new UsTarHeader(); - } - public ITarHeader FileInfo => _header; /// @@ -66,7 +62,7 @@ namespace AMWD.Common.Packing.Tar } /// - /// Read data from a current file to a Stream. + /// Read data from the current archive to a Stream. /// /// A stream to read data to /// @@ -84,6 +80,11 @@ namespace AMWD.Common.Packing.Tar Debug.WriteLine("tar stream position Read out: " + _inStream.Position); } + /// + /// Reads data from the current archive to a buffer array. + /// + /// The buffer array. + /// The nuber of bytes read. protected int Read(out byte[] buffer) { if (_remainingBytesInFile == 0) diff --git a/AMWD.Common/Packing/Tar/TarWriter.cs b/AMWD.Common/Packing/Tar/TarWriter.cs index 77c8f7b..12f90f9 100644 --- a/AMWD.Common/Packing/Tar/TarWriter.cs +++ b/AMWD.Common/Packing/Tar/TarWriter.cs @@ -5,13 +5,32 @@ using AMWD.Common.Packing.Tar.Utils; namespace AMWD.Common.Packing.Tar { - // https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarWriter.cs + /// + /// Writes a tar (see GNU tar) archive to a stream. + /// + /// + /// Copied from: DotnetMakeDeb + /// public class TarWriter : LegacyTarWriter { + /// + /// Initilizes a new instance of the class. + /// + /// The stream to write the archive to. public TarWriter(Stream outStream) : base(outStream) { } + /// + /// Writes an entry header (file, dir, ...) to the archive. + /// + /// The name. + /// The last modification time. + /// The number of bytes. + /// The user id. + /// The group id. + /// The access mode. + /// The entry type. protected override void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType) { var tarHeader = new UsTarHeader() @@ -29,6 +48,17 @@ namespace AMWD.Common.Packing.Tar OutStream.Write(tarHeader.GetHeaderValue(), 0, tarHeader.HeaderSize); } + /// + /// Writes an entry header (file, dir, ...) to the archive. + /// Hashes the username and groupname down to a HashCode. + /// + /// The name. + /// The last modification time. + /// The number of bytes. + /// The username. + /// The group name. + /// The access mode. + /// The entry type. protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, string userName, string groupName, int mode, EntryType entryType) { WriteHeader( @@ -41,6 +71,16 @@ namespace AMWD.Common.Packing.Tar entryType: entryType); } + /// + /// Writes a file to the archive. + /// + /// The file name. + /// The filesize in bytes. + /// The username. + /// The group name. + /// The access mode. + /// The last modification time. + /// The write handle. public virtual void Write(string name, long dataSizeInBytes, string userName, string groupName, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate) { var writer = new DataWriter(OutStream, dataSizeInBytes); @@ -52,6 +92,16 @@ namespace AMWD.Common.Packing.Tar AlignTo512(dataSizeInBytes, false); } + /// + /// Writes a file to the archive. + /// + /// The file stream to add to the archive. + /// The filesize in bytes. + /// The file name. + /// The user id. + /// The group id. + /// The access mode. + /// The last modification time. public void Write(Stream data, long dataSizeInBytes, string fileName, string userId, string groupId, int mode, DateTime lastModificationTime) { diff --git a/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs index 82612d0..1944326 100644 --- a/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs +++ b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs @@ -10,11 +10,14 @@ namespace AMWD.Common.Packing.Tar.Utils /// Implements a legacy TAR writer. /// /// - /// Copied from
+ /// Writes tar (see GNU tar) archive to a stream + ///
+ /// Copied from: DotnetMakeDeb /// - public class LegacyTarWriter : IDisposable + /// stream to write archive to + public class LegacyTarWriter(Stream outStream) : IDisposable { - private readonly Stream _outStream; + private readonly Stream _outStream = outStream; private bool _isClosed; /// @@ -22,15 +25,6 @@ namespace AMWD.Common.Packing.Tar.Utils /// protected byte[] buffer = new byte[1024]; - /// - /// Writes tar (see GNU tar) archive to a stream - /// - /// stream to write archive to - public LegacyTarWriter(Stream outStream) - { - _outStream = outStream; - } - /// /// Gets or sets a value indicating whether to read on zero. /// @@ -48,20 +42,25 @@ namespace AMWD.Common.Packing.Tar.Utils /// public void Dispose() - { - Close(); - } + => Close(); #endregion IDisposable Members + /// + /// Writes a directory entry. + /// + /// The path to the directory. + /// The user id. + /// The group id. + /// The access mode. + /// is not set. public void WriteDirectoryEntry(string path, int userId, int groupId, int mode) { if (string.IsNullOrEmpty(path)) - throw new ArgumentNullException("path"); + throw new ArgumentNullException(nameof(path), "The path is not set."); if (path[path.Length - 1] != '/') - { path += '/'; - } + DateTime lastWriteTime; if (Directory.Exists(path)) { @@ -89,40 +88,50 @@ namespace AMWD.Common.Packing.Tar.Utils WriteHeader(path, lastWriteTime, 0, userId, groupId, mode, EntryType.Directory); } + /// + /// Writes a directory and its contents. + /// + /// The directory. + /// Write also sub-directories. + /// is not set. public void WriteDirectory(string directory, bool doRecursive) { if (string.IsNullOrEmpty(directory)) - throw new ArgumentNullException("directory"); + throw new ArgumentNullException(nameof(directory), "The directory is not set."); WriteDirectoryEntry(directory, 0, 0, 0755); string[] files = Directory.GetFiles(directory); foreach (string fileName in files) - { Write(fileName); - } string[] directories = Directory.GetDirectories(directory); foreach (string dirName in directories) { WriteDirectoryEntry(dirName, 0, 0, 0755); if (doRecursive) - { WriteDirectory(dirName, true); - } } } + /// + /// Writes a file. + /// + /// The file. + /// is not set. 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)); - } + throw new ArgumentNullException(nameof(fileName), "The file name is not set."); + + using var fileStream = File.OpenRead(fileName); + Write(fileStream, fileStream.Length, fileName, 61, 61, 511, File.GetLastWriteTime(fileStream.Name)); } + /// + /// Writes a file stream. + /// + /// The file stream. public void Write(FileStream file) { string path = Path.GetFullPath(file.Name).Replace(Path.GetPathRoot(file.Name), string.Empty); @@ -130,22 +139,48 @@ namespace AMWD.Common.Packing.Tar.Utils Write(file, file.Length, path, 61, 61, 511, File.GetLastWriteTime(file.Name)); } + /// + /// Writes a stream. + /// + /// The contents. + /// The file size in bytes. + /// The file name. public void Write(Stream data, long dataSizeInBytes, string name) - { - Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now); - } + => Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now); + /// + /// Writes a file to the archive. + /// + /// The file name. + /// The file size in bytes. + /// The user id. + /// The group id. + /// The access mode. + /// The last modification timestamp. + /// The . public virtual void Write(string name, long dataSizeInBytes, int userId, int groupId, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate) { - IArchiveDataWriter writer = new DataWriter(OutStream, dataSizeInBytes); + var writer = new DataWriter(OutStream, dataSizeInBytes); + WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File); + while (writer.CanWrite) - { writeDelegate(writer); - } + AlignTo512(dataSizeInBytes, false); } + /// + /// Writes a stream as file to the archive. + /// + /// The content as . + /// The file size in bytes. + /// The file name. + /// The user id. + /// The group id. + /// The access mode. + /// The last modification timestamp. + /// This writer is already closed. public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode, DateTime lastModificationTime) { if (_isClosed) @@ -189,21 +224,26 @@ namespace AMWD.Common.Packing.Tar.Utils 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); - } + using var nameStream = new MemoryStream(entryName); + WriteHeader("././@LongLink", lastModificationTime, entryName.Length, userId, groupId, mode, EntryType.LongName); + WriteContent(entryName.Length, nameStream); + AlignTo512(entryName.Length, false); } + /// + /// Writes a stream as file to the archive. + /// + /// The size of the file in bytes. + /// The file content as stream. + /// has not enough to read from. 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"); + throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream"); + if (bytesRead == 0) { if (ReadOnZero) @@ -218,7 +258,8 @@ namespace AMWD.Common.Packing.Tar.Utils { int bytesRead = data.Read(buffer, 0, (int)count); if (bytesRead < 0) - throw new IOException("LegacyTarWriter unable to read from provided stream"); + throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream"); + if (bytesRead == 0) { while (count > 0) @@ -232,6 +273,16 @@ namespace AMWD.Common.Packing.Tar.Utils } } + /// + /// Writes a entry header to the archive. + /// + /// The file name. + /// The last modification time. + /// The number of bytes. + /// The user id. + /// The group id. + /// The file mode. + /// The entry type protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType) { var header = new TarHeader @@ -247,6 +298,9 @@ namespace AMWD.Common.Packing.Tar.Utils OutStream.Write(header.GetHeaderValue(), 0, header.HeaderSize); } + /// + /// Aligns the entry to 512 bytes. + /// public void AlignTo512(long size, bool acceptZero) { size %= 512; @@ -258,6 +312,9 @@ namespace AMWD.Common.Packing.Tar.Utils } } + /// + /// Closes the writer and aligns to 512 bytes. + /// public virtual void Close() { if (_isClosed) diff --git a/AMWD.Common/Packing/Tar/Utils/TarException.cs b/AMWD.Common/Packing/Tar/Utils/TarException.cs index 34ef3f4..ed83e55 100644 --- a/AMWD.Common/Packing/Tar/Utils/TarException.cs +++ b/AMWD.Common/Packing/Tar/Utils/TarException.cs @@ -2,10 +2,51 @@ namespace AMWD.Common.Packing.Tar.Utils { + /// + /// Represents errors that occur during tar archive execution. + /// public class TarException : Exception { - public TarException(string message) : base(message) - { - } + /// + /// Initializes a new instance of the class. + /// + public TarException() + : base() + { } + + /// + /// Initializes a new instance of the class with a specified + /// error message. + /// + /// The message that describes the error. + public TarException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the System.Exception class with a specified error + /// message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// if no inner exception is specified. + public TarException(string message, Exception innerException) + : base(message, innerException) + { } + +#if !NET8_0_OR_GREATER + + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The that holds the serialized + /// object data about the exception being thrown. + /// The that contains contextual information + /// about the source or destination. + protected TarException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { } + +#endif } } diff --git a/AMWD.Common/Utilities/AsyncQueue.cs b/AMWD.Common/Utilities/AsyncQueue.cs index a3e2ba9..9ad8c10 100644 --- a/AMWD.Common/Utilities/AsyncQueue.cs +++ b/AMWD.Common/Utilities/AsyncQueue.cs @@ -94,7 +94,7 @@ namespace System.Collections.Generic /// Determines whether an element is in the . /// /// The object to locate in the . The value can be null for reference types. - /// true if item is found in the ; otherwise, false. + /// if item is found in the , otherwise . [ExcludeFromCodeCoverage] public bool Contains(T item) { @@ -172,7 +172,7 @@ namespace System.Collections.Generic { lock (_queue) { - return _queue.ToArray(); + return [.. _queue]; } } @@ -304,7 +304,7 @@ namespace System.Collections.Generic /// Removes the object at the beginning of the , and copies it to the parameter. /// /// The removed object. - /// true if the object is successfully removed; false if the is empty. + /// if the object is successfully removed, if the is empty. public bool TryDequeue(out T result) { try @@ -325,7 +325,7 @@ namespace System.Collections.Generic /// parameter. The object is not removed from the . /// /// If present, the object at the beginning of the ; otherwise, the default value of . - /// true if there is an object at the beginning of the ; false if the is empty. + /// if there is an object at the beginning of the , if the is empty. public bool TryPeek(out T result) { try @@ -344,7 +344,7 @@ namespace System.Collections.Generic /// Removes the first occurrence of a specific object from the . /// /// The object to remove from the . The value can be null for reference types. - /// true if item is successfully removed; otherwise, false. This method also returns false if item was not found in the . + /// if item is successfully removed, otherwise . This method also returns if item was not found in the . public bool Remove(T item) { lock (_queue) diff --git a/AMWD.Common/Utilities/CryptographyHelper.cs b/AMWD.Common/Utilities/CryptographyHelper.cs index 1842f97..5967256 100644 --- a/AMWD.Common/Utilities/CryptographyHelper.cs +++ b/AMWD.Common/Utilities/CryptographyHelper.cs @@ -171,6 +171,7 @@ namespace System.Security.Cryptography #region Static methods #region Encryption +#pragma warning disable SYSLIB0041 #region AES @@ -185,7 +186,11 @@ namespace System.Security.Cryptography byte[] salt = new byte[_saltLength]; Array.Copy(cipher, salt, _saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; @@ -225,7 +230,11 @@ namespace System.Security.Cryptography { byte[] salt = GetRandomBytes(_saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; @@ -271,7 +280,11 @@ namespace System.Security.Cryptography byte[] salt = new byte[_saltLength]; Array.Copy(cipher, salt, _saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var tdes = TripleDES.Create(); tdes.Mode = CipherMode.CBC; @@ -298,7 +311,11 @@ namespace System.Security.Cryptography { byte[] salt = GetRandomBytes(_saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var tdes = TripleDES.Create(); tdes.Mode = CipherMode.CBC; @@ -344,6 +361,7 @@ namespace System.Security.Cryptography #endregion Triple DES +#pragma warning restore SYSLIB0041 #endregion Encryption #region Hashing @@ -379,8 +397,12 @@ namespace System.Security.Cryptography /// The MD5 hash value, in hexadecimal notation. public static string Md5(byte[] bytes) { +#if NET8_0_OR_GREATER + return MD5.HashData(bytes).BytesToHex(); +#else using var md5 = MD5.Create(); return md5.ComputeHash(bytes).BytesToHex(); +#endif } #endregion MD5 @@ -416,8 +438,12 @@ namespace System.Security.Cryptography /// The SHA-1 hash value, in hexadecimal notation. public static string Sha1(byte[] bytes) { +#if NET8_0_OR_GREATER + return SHA1.HashData(bytes).BytesToHex(); +#else using var sha1 = SHA1.Create(); return sha1.ComputeHash(bytes).BytesToHex(); +#endif } #endregion SHA-1 @@ -453,8 +479,12 @@ namespace System.Security.Cryptography /// The SHA-256 hash value, in hexadecimal notation. public static string Sha256(byte[] bytes) { +#if NET8_0_OR_GREATER + return SHA256.HashData(bytes).BytesToHex(); +#else using var sha256 = SHA256.Create(); return sha256.ComputeHash(bytes).BytesToHex(); +#endif } #endregion SHA-256 @@ -490,8 +520,12 @@ namespace System.Security.Cryptography /// The SHA-512 hash value, in hexadecimal notation. public static string Sha512(byte[] bytes) { +#if NET8_0_OR_GREATER + return SHA512.HashData(bytes).BytesToHex(); +#else using var sha512 = SHA512.Create(); return sha512.ComputeHash(bytes).BytesToHex(); +#endif } #endregion SHA-512 diff --git a/AMWD.Common/Utilities/NetworkHelper.cs b/AMWD.Common/Utilities/NetworkHelper.cs index a71dc76..9faf02e 100644 --- a/AMWD.Common/Utilities/NetworkHelper.cs +++ b/AMWD.Common/Utilities/NetworkHelper.cs @@ -4,7 +4,12 @@ using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif namespace AMWD.Common.Utilities { @@ -23,7 +28,7 @@ namespace AMWD.Common.Utilities public static List ResolveHost(string hostname, AddressFamily addressFamily = default) { if (string.IsNullOrWhiteSpace(hostname)) - return new(); + return []; if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) addressFamily = AddressFamily.Unspecified; @@ -31,7 +36,7 @@ namespace AMWD.Common.Utilities var ipAddress = ResolveIpAddress(hostname, addressFamily); // the name was an ip address, should not happen but experience tells other stories if (ipAddress != null) - return new() { ipAddress }; + return [ipAddress]; try { @@ -41,7 +46,7 @@ namespace AMWD.Common.Utilities } catch { - return new(); + return []; } } @@ -54,7 +59,7 @@ namespace AMWD.Common.Utilities public static List ResolveInterface(string interfaceName, AddressFamily addressFamily = default) { if (string.IsNullOrWhiteSpace(interfaceName)) - return new(); + return []; if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) addressFamily = AddressFamily.Unspecified; @@ -62,7 +67,7 @@ namespace AMWD.Common.Utilities var ipAddress = ResolveIpAddress(interfaceName, addressFamily); // the name was an ip address, should not happen but experience tells other stories if (ipAddress != null) - return new() { ipAddress }; + return [ipAddress]; try { @@ -74,45 +79,7 @@ namespace AMWD.Common.Utilities } catch { - return new(); - } - } - - /// - /// Parses a CIDR network definition. - /// - /// The network in CIDR. - /// The or null. - public static IPNetwork ParseNetwork(string network) - { - TryParseNetwork(network, out var ipNetwork); - return ipNetwork; - } - - /// - /// Tries to parse a CIDR network definition. - /// - /// The network in CIDR. - /// The parsed . - /// true on success, otherwise false. - public static bool TryParseNetwork(string network, out IPNetwork ipNetwork) - { - try - { - string[] parts = network.Split('/'); - if (parts.Length != 2) - throw new ArgumentException($"Invalid network type"); - - var prefix = IPAddress.Parse(parts.First()); - int prefixLength = int.Parse(parts.Last()); - - ipNetwork = new IPNetwork(prefix, prefixLength); - return true; - } - catch - { - ipNetwork = null; - return false; + return []; } } @@ -128,7 +95,11 @@ namespace AMWD.Common.Utilities { var list = new List(); +#if NET8_0_OR_GREATER + var ipAddress = network.BaseAddress; +#else var ipAddress = network.Prefix; +#endif while (network.Contains(ipAddress)) { list.Add(ipAddress); diff --git a/Common.sln b/Common.sln index f5bfa05..c1d89ff 100644 --- a/Common.sln +++ b/Common.sln @@ -41,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{93EC8B16 nuget.config = nuget.config EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Common.MessagePack", "AMWD.Common.MessagePack\AMWD.Common.MessagePack.csproj", "{EA014C15-93B6-4F2C-8229-1C13E22BF84A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +69,10 @@ Global {9469D87B-126E-4338-92E3-701F762CB54D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9469D87B-126E-4338-92E3-701F762CB54D}.Release|Any CPU.ActiveCfg = Release|Any CPU {9469D87B-126E-4338-92E3-701F762CB54D}.Release|Any CPU.Build.0 = Release|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -80,6 +86,7 @@ Global {86DE1B7C-3ECF-49B1-AB28-A976A3973FF5} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB} {7196DA2B-D858-4B25-BC23-865175CFCDEC} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB} {93EC8B16-7DEF-4E39-B590-E804DEF7C607} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB} + {EA014C15-93B6-4F2C-8229-1C13E22BF84A} = {F2C7556A-99EB-43EB-8954-56A24AFE928F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {961E8DF8-DDF5-4D10-A510-CE409E9962AC} diff --git a/Directory.Build.props b/Directory.Build.props index 3d0cb4f..553ca7c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,6 @@ true false - false true true @@ -34,7 +33,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs b/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs index 9377d14..20ac3b5 100644 --- a/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs +++ b/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using AMWD.Common.AspNetCore.Security.BasicAuthentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -156,12 +157,16 @@ namespace UnitTests.AspNetCore.Security.BasicAuthentication var requestHeaderMock = new Mock(); foreach (var header in _requestHeaders) { + var strVal = new StringValues(header.Value); requestHeaderMock .Setup(h => h.ContainsKey(header.Key)) .Returns(true); requestHeaderMock .Setup(h => h[header.Key]) - .Returns(header.Value); + .Returns(strVal); + requestHeaderMock + .Setup(h => h.TryGetValue(header.Key, out strVal)) + .Returns(true); } var requestMock = new Mock(); @@ -174,6 +179,11 @@ namespace UnitTests.AspNetCore.Security.BasicAuthentication responseHeaderMock .SetupSet(h => h[It.IsAny()] = It.IsAny()) .Callback((key, value) => _responseHeadersCallback[key] = value); +#pragma warning disable CS0618 + responseHeaderMock + .SetupSet(h => h.WWWAuthenticate) + .Callback((value) => _responseHeadersCallback[HeaderNames.WWWAuthenticate] = value); +#pragma warning restore CS0618 var responseMock = new Mock(); responseMock diff --git a/UnitTests/Common/Extensions/EnumExtensionsTests.cs b/UnitTests/Common/Extensions/EnumExtensionsTests.cs index 7f0cd93..0099f7a 100644 --- a/UnitTests/Common/Extensions/EnumExtensionsTests.cs +++ b/UnitTests/Common/Extensions/EnumExtensionsTests.cs @@ -108,22 +108,6 @@ namespace UnitTests.Common.Extensions Assert.IsFalse(list.Any()); } - [TestMethod] - public void ShouldReturnDisplayNameOrStringRepresentation() - { - // arrange - var enumWithDisplayName = TestEnum.Two; - var enumWithoutDisplayName = TestEnum.Zero; - - // act - string displayName = enumWithDisplayName.GetDisplayName(); - string noDisplayName = enumWithoutDisplayName.GetDisplayName(); - - // assert - Assert.AreEqual("Zwei", displayName); - Assert.AreEqual(enumWithoutDisplayName.ToString(), noDisplayName); - } - internal enum TestEnum { [CustomMultiple("nix")] @@ -132,7 +116,6 @@ namespace UnitTests.Common.Extensions Zero, [Description("Eins")] One, - [Display(Name = "Zwei")] Two, } } diff --git a/UnitTests/Common/Packing/Ar/ArReaderTests.cs b/UnitTests/Common/Packing/Ar/ArReaderTests.cs index 688f54e..712a59a 100644 --- a/UnitTests/Common/Packing/Ar/ArReaderTests.cs +++ b/UnitTests/Common/Packing/Ar/ArReaderTests.cs @@ -12,16 +12,16 @@ namespace UnitTests.Common.Packing.Ar [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class ArReaderTests { - private readonly DateTime fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc); + private readonly DateTime _fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc); - private Dictionary files; + private Dictionary _files; private Stream inStream; [TestInitialize] public void Initialize() { - files = new Dictionary + _files = new Dictionary { { "abcd.tmp", @@ -31,7 +31,7 @@ namespace UnitTests.Common.Packing.Ar FileSize = 14, GroupId = 456, Mode = 33188, - ModifyTime = fixedDateTime, + ModifyTime = _fixedDateTime, UserId = 123 } }, @@ -43,7 +43,7 @@ namespace UnitTests.Common.Packing.Ar FileSize = 14, GroupId = 456, Mode = 33188, - ModifyTime = fixedDateTime, + ModifyTime = _fixedDateTime, UserId = 123 } }, @@ -55,7 +55,7 @@ namespace UnitTests.Common.Packing.Ar FileSize = 13, GroupId = 456, Mode = 33188, - ModifyTime = fixedDateTime, + ModifyTime = _fixedDateTime, UserId = 123 } } @@ -64,7 +64,7 @@ namespace UnitTests.Common.Packing.Ar inStream = new MemoryStream(); inStream.Write(Encoding.ASCII.GetBytes("!\n")); - foreach (var file in files) + foreach (var file in _files) { int unixSeconds = (int)file.Value.ModifyTime.Subtract(DateTime.UnixEpoch).TotalSeconds; @@ -125,9 +125,9 @@ namespace UnitTests.Common.Packing.Ar // Assert Assert.IsNotNull(reader); - Assert.AreEqual(files.Count, fileList.Count); + Assert.AreEqual(_files.Count, fileList.Count); - foreach (string name in files.Keys) + foreach (string name in _files.Keys) Assert.IsTrue(fileList.Contains(name)); } @@ -139,14 +139,14 @@ namespace UnitTests.Common.Packing.Ar var reader = new ArReader(inStream); // Act - foreach (string name in files.Keys) + foreach (string name in _files.Keys) infos.Add(reader.GetFileInfo(name)); // Assert Assert.IsNotNull(reader); - Assert.AreEqual(files.Count, infos.Count); + Assert.AreEqual(_files.Count, infos.Count); - foreach (var expected in files.Values) + foreach (var expected in _files.Values) { var actual = infos.Single(fi => fi.FileName == expected.FileName); @@ -167,7 +167,7 @@ namespace UnitTests.Common.Packing.Ar var reader = new ArReader(inStream); // Act - foreach (string name in files.Keys) + foreach (string name in _files.Keys) { using var ms = new MemoryStream(); reader.ReadFile(name, ms); @@ -178,9 +178,9 @@ namespace UnitTests.Common.Packing.Ar // Assert Assert.IsNotNull(reader); - Assert.AreEqual(files.Count, contents.Count); + Assert.AreEqual(_files.Count, contents.Count); - foreach (var expected in files.Values) + foreach (var expected in _files.Values) { string content = contents[expected.FileName]; diff --git a/UnitTests/Common/Packing/Tar/TarReaderTests.cs b/UnitTests/Common/Packing/Tar/TarReaderTests.cs new file mode 100644 index 0000000..3e5abc7 --- /dev/null +++ b/UnitTests/Common/Packing/Tar/TarReaderTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UnitTests.Common.Packing.Tar +{ + internal class TarReaderTests + { + } +} diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 6c1489a..aed5779 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -2,6 +2,7 @@ net8.0 + 12.0 false true false @@ -13,8 +14,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + +