From 74ca861d2b4a2ce0980440ff475e5071164b6502 Mon Sep 17 00:00:00 2001 From: Frederik Carlier Date: Sat, 29 Apr 2017 22:46:46 +0200 Subject: [PATCH] Support writing data to a cpio file. --- Packaging.Targets.Tests/IO/CpioFileTests.cs | 36 ++++ .../IO/ValidatingCompositeStream.cs | 156 ++++++++++++++++++ Packaging.Targets/IO/CpioFile.cs | 66 ++++++++ Packaging.Targets/IO/CpioHeader.cs | 97 +++++++---- Packaging.Targets/StreamExtensions.cs | 88 ++++++++-- 5 files changed, 403 insertions(+), 40 deletions(-) create mode 100644 Packaging.Targets.Tests/IO/ValidatingCompositeStream.cs diff --git a/Packaging.Targets.Tests/IO/CpioFileTests.cs b/Packaging.Targets.Tests/IO/CpioFileTests.cs index 06cc020..ce0ce79 100644 --- a/Packaging.Targets.Tests/IO/CpioFileTests.cs +++ b/Packaging.Targets.Tests/IO/CpioFileTests.cs @@ -9,8 +9,15 @@ using Xunit; namespace Packaging.Targets.Tests.IO { + /// + /// Tests the class. + /// public class CpioFileTests { + /// + /// Tests the opening an reading of entries from a + /// object. + /// [Fact] public void OpenCpioFileTests() { @@ -59,5 +66,34 @@ namespace Packaging.Targets.Tests.IO Assert.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", entryHashes[1]); Assert.Equal("811ee67ae433927d702c675c53dfe53811b1479aa9eedd91666b548a4797a7bc", entryHashes[2]); } + + /// + /// Tests the saving of data to a object, by copying one CPIO + /// stream to another. + /// + [Fact] + public void WriteCpioFileTests() + { + using (Stream stream = File.OpenRead(@"IO\test.cpio")) + using (CpioFile source = new CpioFile(stream, true)) + using (MemoryStream output = new MemoryStream()) + using (Stream expectedOutput = File.OpenRead(@"IO\test.cpio")) + using (Stream validatingOutput = new ValidatingCompositeStream(null, output, expectedOutput)) + using (CpioFile target = new CpioFile(validatingOutput, true)) + { + int index = 0; + + while (source.Read()) + { + using (Stream file = source.Open()) + { + target.Write(source.EntryHeader, source.EntryName, file); + index++; + } + } + + target.WriteTrailer(); + } + } } } diff --git a/Packaging.Targets.Tests/IO/ValidatingCompositeStream.cs b/Packaging.Targets.Tests/IO/ValidatingCompositeStream.cs new file mode 100644 index 0000000..39c06be --- /dev/null +++ b/Packaging.Targets.Tests/IO/ValidatingCompositeStream.cs @@ -0,0 +1,156 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Packaging.Targets.Tests +{ + /// + /// A which reads its input from a , + /// writes its output to a and validates that the data which is being written to + /// the output stream matches the data in a reference stream (usually a recorded trace). + /// Used with unit tests which replay recorded traces. + /// + internal class ValidatingCompositeStream : Stream + { + private Stream input; + private Stream output; + private Stream expectedOutput; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The from which to read data. + /// + /// + /// The to which to write data. + /// + /// + /// The reference stream for . + /// + public ValidatingCompositeStream(Stream input, Stream output, Stream expectedOutput) + { + if (output == null) + { + throw new ArgumentNullException(nameof(output)); + } + + if (expectedOutput == null) + { + throw new ArgumentNullException(nameof(expectedOutput)); + } + + this.input = input; + this.output = output; + this.expectedOutput = expectedOutput; + } + + /// + public override bool CanRead + { + get { return true; } + } + + /// + public override bool CanSeek + { + get { return false; } + } + + /// + public override bool CanWrite + { + get { return true; } + } + + /// + public override long Length + { + get { throw new NotImplementedException(); } + } + + /// + public override long Position + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + /// + public override void Flush() + { + this.input?.Flush(); + this.output.Flush(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (this.input == null) + { + throw new InvalidOperationException(); + } + + return this.input.Read(buffer, offset, count); + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (this.input == null) + { + throw new InvalidOperationException(); + } + + return this.input.ReadAsync(buffer, offset, count, cancellationToken); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + byte[] expected = new byte[buffer.Length]; + this.expectedOutput.Read(expected, offset, count); + + byte[] bufferChunk = buffer.Skip(offset).Take(count).ToArray(); + byte[] expectedChunk = expected.Skip(offset).Take(count).ToArray(); + + // Make sure the data being writen matches the data which was written to the expected stream. + // This will detect any errors as the invalid data is being written out - as opposed to post- + // test binary validation. + Assert.Equal(expectedChunk, bufferChunk); + this.output.Write(buffer, offset, count); + } + + /// + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + byte[] expected = new byte[buffer.Length]; + await this.expectedOutput.ReadAsync(expected, offset, count, cancellationToken).ConfigureAwait(false); + + byte[] bufferChunk = buffer.Skip(offset).Take(count).ToArray(); + byte[] expectedChunk = expected.Skip(offset).Take(count).ToArray(); + + // Make sure the data being writen matches the data which was written to the expected stream. + // This will detect any errors as the invalid data is being written out - as opposed to post- + // test binary validation. + Assert.Equal(expectedChunk, bufferChunk); + + await this.output.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Packaging.Targets/IO/CpioFile.cs b/Packaging.Targets/IO/CpioFile.cs index 770ab89..487616f 100644 --- a/Packaging.Targets/IO/CpioFile.cs +++ b/Packaging.Targets/IO/CpioFile.cs @@ -104,6 +104,72 @@ namespace Packaging.Targets.IO get { return this.entryDataLength == 0; } } + /// + /// Adds an entry to the + /// + /// + /// A with the item metaata. The , + /// and values are overwritten. + /// + /// + /// The file name of the entry. + /// + /// + /// A which contains the file data. + /// + public void Write(CpioHeader header, string name, Stream data) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + byte[] nameBytes = Encoding.UTF8.GetBytes(name); + + // We make sure the magic and size fields have the correct values. All other fields + // are the responsibility of the caller. + header.Signature = "070701"; + header.NameSize = (uint)(nameBytes.Length + 1); + header.FileSize = (uint)data.Length; + + this.stream.WriteStruct(header); + this.stream.Write(nameBytes, 0, nameBytes.Length); + + // The pathname is followed by NUL bytes so that the total size of the fixed + // header plus pathname is a multiple of four. + var headerSize = Marshal.SizeOf() + nameBytes.Length; + var paddingSize = PaddingSize(headerSize); + + for (int i = 0; i < paddingSize; i++) + { + this.stream.WriteByte(0); + } + + data.Position = 0; + data.CopyTo(this.stream); + + // The file data is padded to a multiple of four bytes. + paddingSize = PaddingSize((int)data.Length); + + for (int i = 0; i < paddingSize; i++) + { + this.stream.WriteByte(0); + } + } + + /// + /// Writes the trailer entry. + /// + public void WriteTrailer() + { + this.Write(CpioHeader.Empty, "TRAILER!!!", new MemoryStream(Array.Empty())); + } + /// /// Reads the next entry in the . /// diff --git a/Packaging.Targets/IO/CpioHeader.cs b/Packaging.Targets/IO/CpioHeader.cs index 43bcdfd..a6c2258 100644 --- a/Packaging.Targets/IO/CpioHeader.cs +++ b/Packaging.Targets/IO/CpioHeader.cs @@ -15,7 +15,7 @@ namespace Packaging.Targets.IO /// whether this archive is written with little-endian or big-endian integers, /// or ASCII. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 6)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 6)] // 0 through 5 private char[] signature; /// @@ -24,25 +24,25 @@ namespace Packaging.Targets.IO /// refer to the same file. Programs that synthesize cpio archives /// should be careful to set these to distinct values for each entry. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 6 through 13 private char[] ino; /// /// The mode specifies both the regular permissions and the file type. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 14 through 21 private char[] mode; /// /// The numeric user id of the owner. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 22 through 29 private char[] uid; /// /// The numeric group id of the owner. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 30 through 37 private char[] gid; /// @@ -50,7 +50,7 @@ namespace Packaging.Targets.IO /// value of at least two here. Note that hardlinked files include /// file data with every copy in the archive. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 38 through 45 private char[] nlink; /// @@ -60,7 +60,7 @@ namespace Packaging.Targets.IO /// first followed by the least-significant 16 bits.Each of the two /// 16 bit values are stored in machine-native byte order. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 46 through 53 private char[] mtime; /// @@ -68,13 +68,13 @@ namespace Packaging.Targets.IO /// to four gigabyte file sizes.See mtime above for a description /// of the storage of four-byte integers. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 54 through 61 private char[] filesize; - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 62 through 69 private char[] devMajor; - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 70 through 77 private char[] devMinor; /// @@ -82,135 +82,176 @@ namespace Packaging.Targets.IO /// the associated device number. For all other entry types, /// it should be set to zero by writers and ignored by readers. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 78 through 85 private char[] rdevMajor; - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 86 through 93 private char[] rdevMinor; /// /// The number of bytes in the pathname that follows the header. /// This count includes the trailing NUL byte. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 94 through 101 private char[] namesize; /// /// This field is always set to zero by writers and ignored by readers. /// - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 8)] // 102 through 109 private char[] check; /// - /// Gets the value of the field as a . + /// Gets an empty entry. + /// + public static CpioHeader Empty + { + get + { + return new CpioHeader() + { + Check = 0, + DevMajor = 0, + DevMinor = 0, + FileSize = 0, + Gid = 0, + Ino = 0, + Mode = 0, + Mtime = 0, + NameSize = 0, + Nlink = 1, + RDevMajor = 0, + RDevMinor = 0, + Signature = "070701", + Uid = 0 + }; + } + } + + /// + /// Gets or sets the value of the field as a . /// public string Signature { get { return new string(this.signature); } + set { this.signature = value.ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint Ino { get { return Convert.ToUInt32(new string(this.ino), 16); } + set { this.ino = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint Mode { get { return Convert.ToUInt32(new string(this.mode), 16); } + set { this.mode = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint Uid { get { return Convert.ToUInt32(new string(this.uid), 16); } + set { this.uid = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint Gid { get { return Convert.ToUInt32(new string(this.gid), 16); } + set { this.gid = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint Nlink { get { return Convert.ToUInt32(new string(this.nlink), 16); } + set { this.nlink = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint Mtime { get { return Convert.ToUInt32(new string(this.mtime), 16); } + set { this.mtime = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint FileSize { get { return Convert.ToUInt32(new string(this.filesize), 16); } + set { this.filesize = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint DevMajor { get { return Convert.ToUInt32(new string(this.devMajor), 16); } + set { this.devMajor = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint DevMinor { get { return Convert.ToUInt32(new string(this.devMinor), 16); } + set { this.devMinor = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint RDevMajor { get { return Convert.ToUInt32(new string(this.rdevMajor), 16); } + set { this.rdevMajor = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint RDevMinor { get { return Convert.ToUInt32(new string(this.rdevMinor), 16); } + set { this.rdevMinor = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint NameSize { get { return Convert.ToUInt32(new string(this.namesize), 16); } + set { this.namesize = value.ToString("x8").ToCharArray(); } } /// - /// Gets the value of the field as a . + /// Gets or sets the value of the field as a . /// public uint Check { get { return Convert.ToUInt32(new string(this.check), 16); } + set { this.check = value.ToString("x8").ToCharArray(); } } } } diff --git a/Packaging.Targets/StreamExtensions.cs b/Packaging.Targets/StreamExtensions.cs index 10910b2..b21afb8 100644 --- a/Packaging.Targets/StreamExtensions.cs +++ b/Packaging.Targets/StreamExtensions.cs @@ -5,10 +5,31 @@ using System.Runtime.InteropServices; namespace Packaging.Targets { + /// + /// Provides extension methods for the class. + /// internal static class StreamExtensions { + /// + /// Reads a struct from the stream. + /// + /// + /// The type of the struct to read. + /// + /// + /// The to read the struct from. + /// + /// + /// A new struct, with the data read + /// from the stream. + /// public static T ReadStruct(this Stream stream) where T : struct { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + var size = Marshal.SizeOf(); var data = new byte[size]; @@ -32,6 +53,61 @@ namespace Packaging.Targets } // Convert from network byte order (big endian) to little endian. + RespectEndianness(data); + + var pinnedData = GCHandle.Alloc(data, GCHandleType.Pinned); + + try + { + var ptr = pinnedData.AddrOfPinnedObject(); + return Marshal.PtrToStructure(ptr); + } + finally + { + pinnedData.Free(); + } + } + + /// + /// Writes a struct to a stream. + /// + /// + /// The type of the struct to write. + /// + /// + /// The stream to which to write the struct. + /// + /// + /// The struct to write to the stram. + /// + public static void WriteStruct(this Stream stream, T data) + where T : struct + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + byte[] bytes = new byte[Marshal.SizeOf()]; + + GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + + try + { + Marshal.StructureToPtr(data, handle.AddrOfPinnedObject(), true); + } + finally + { + handle.Free(); + } + + RespectEndianness(bytes); + + stream.Write(bytes, 0, bytes.Length); + } + + private static void RespectEndianness(byte[] data) + { foreach (var field in typeof(T).GetTypeInfo().DeclaredFields) { int length = 0; @@ -58,18 +134,6 @@ namespace Packaging.Targets Array.Reverse(data, offset, length); } } - - var pinnedData = GCHandle.Alloc(data, GCHandleType.Pinned); - - try - { - var ptr = pinnedData.AddrOfPinnedObject(); - return Marshal.PtrToStructure(ptr); - } - finally - { - pinnedData.Free(); - } } } }