// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. namespace WixToolset.Data { using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; /// /// Class that understands the standard file structures in the WiX toolset. /// public class FileStructure : IDisposable { private long dataStreamOffset; private long[] embeddedFileSizes; private Stream stream; private bool disposed; private static readonly Dictionary SupportedFileFormats = new Dictionary() { { "wir", FileFormat.WixIR }, { "wixirf", FileFormat.WixIR }, { "wixipl", FileFormat.WixIR }, { "wixobj", FileFormat.Wixobj }, { "wixlib", FileFormat.Wixlib }, { "wixout", FileFormat.Wixout }, { "wixpdb", FileFormat.Wixpdb }, { "wixmst", FileFormat.Wixout }, { "wixmsp", FileFormat.Wixout }, }; /// /// Use Create or Read to create a FileStructure. /// private FileStructure() { } /// /// Count of embedded files in the file structure. /// public int EmbeddedFileCount => this.embeddedFileSizes.Length; /// /// File format of the file structure. /// public FileFormat FileFormat { get; private set; } /// /// Creates a new file structure. /// /// Stream to write the file structure to. /// File format for the file structure. /// Paths to files to embedd in the file structure. /// Newly created file structure. public static FileStructure Create(Stream stream, FileFormat fileFormat, IEnumerable embedFilePaths) { var fs = new FileStructure(); using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) { fs.WriteType(writer, fileFormat); fs.WriteEmbeddedFiles(writer, embedFilePaths?.ToArray() ?? Array.Empty()); // Remember the data stream offset, which is right after the embedded files have been written. fs.dataStreamOffset = stream.Position; } fs.stream = stream; return fs; } /// /// Reads a file structure from an open stream. /// /// Stream to read from. /// File structure populated from the stream. public static FileStructure Read(Stream stream) { var fs = new FileStructure(); using (var reader = new BinaryReader(stream, Encoding.UTF8, true)) { fs.FileFormat = FileStructure.ReadFileFormat(reader); if (fs.FileFormat != FileFormat.Unknown) { fs.embeddedFileSizes = FileStructure.ReadEmbeddedFileSizes(reader); // Remember the data stream offset, which is right after the embedded files have been written. fs.dataStreamOffset = stream.Position; foreach (long size in fs.embeddedFileSizes) { fs.dataStreamOffset += size; } } } fs.stream = stream; return fs; } /// /// Guess at the file format based on the file extension. /// /// File extension to guess the file format for. /// Best guess at file format. public static FileFormat GuessFileFormatFromExtension(string extension) { return FileStructure.SupportedFileFormats.TryGetValue(extension.TrimStart('.').ToLowerInvariant(), out var format) ? format : FileFormat.Unknown; } /// /// Probes a stream to determine the file format. /// /// Stream to test. /// The file format. public static FileFormat TestFileFormat(Stream stream) { FileFormat format = FileFormat.Unknown; long position = stream.Position; try { using (var reader = new BinaryReader(stream, Encoding.UTF8, true)) { format = FileStructure.ReadFileFormat(reader); } } finally { stream.Seek(position, SeekOrigin.Begin); } return format; } /// /// Extracts an embedded file. /// /// Index to the file to extract. /// Path to write the extracted file to. public void ExtractEmbeddedFile(int embeddedIndex, string outputPath) { if (this.EmbeddedFileCount <= embeddedIndex) { throw new ArgumentOutOfRangeException("embeddedIndex"); } long header = 6 + 4 + (this.embeddedFileSizes.Length * 8); // skip the type + the count of embedded files + all the sizes of embedded files. long position = this.embeddedFileSizes.Take(embeddedIndex).Sum(); // skip to the embedded file we want. long size = this.embeddedFileSizes[embeddedIndex]; this.stream.Seek(header + position, SeekOrigin.Begin); Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); using (FileStream output = File.OpenWrite(outputPath)) { int read; int total = 0; byte[] buffer = new byte[64 * 1024]; while (0 < (read = this.stream.Read(buffer, 0, (int)Math.Min(buffer.Length, size - total)))) { output.Write(buffer, 0, read); total += read; } } } /// /// Gets a non-closing stream to the data of the file. /// /// Stream to the data of the file. public Stream GetDataStream() { this.stream.Seek(this.dataStreamOffset, SeekOrigin.Begin); return new NonClosingStreamWrapper(this.stream); } /// /// Gets the data of the file as a string. /// /// String contents data of the file. public string GetData() { var bytes = new byte[this.stream.Length - this.dataStreamOffset]; this.stream.Seek(this.dataStreamOffset, SeekOrigin.Begin); this.stream.Read(bytes, 0, bytes.Length); return Encoding.UTF8.GetString(bytes); } /// /// Disposes of the internal state of the file structure. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes of the internsl state of the file structure. /// /// True if disposing. protected virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { if (null != this.stream) { // We do not own the stream, so we don't close it. We're just resetting our internal state. this.embeddedFileSizes = null; this.dataStreamOffset = 0; this.stream = null; } } } this.disposed = true; } private static FileFormat ReadFileFormat(BinaryReader reader) { FileFormat format = FileFormat.Unknown; string type = new string(reader.ReadChars(3)); if (FileStructure.SupportedFileFormats.TryGetValue(type, out format)) { return format; } type += new string(reader.ReadChars(3)); FileStructure.SupportedFileFormats.TryGetValue(type, out format); return format; } private static long[] ReadEmbeddedFileSizes(BinaryReader reader) { uint count = reader.ReadUInt32(); long[] embeddedFileSizes = new long[count]; for (int i = 0; i < embeddedFileSizes.Length; ++i) { embeddedFileSizes[i] = (long)reader.ReadUInt64(); } return embeddedFileSizes; } private BinaryWriter WriteType(BinaryWriter writer, FileFormat fileFormat) { string type = null; foreach (var supported in FileStructure.SupportedFileFormats) { if (supported.Value.Equals(fileFormat)) { type = supported.Key; break; } } if (String.IsNullOrEmpty(type)) { throw new ArgumentException("Unknown file format type", "fileFormat"); } this.FileFormat = fileFormat; Debug.Assert(3 == type.ToCharArray().Length || 6 == type.ToCharArray().Length); writer.Write(type.ToCharArray()); return writer; } private BinaryWriter WriteEmbeddedFiles(BinaryWriter writer, string[] embedFilePaths) { // First write the count of embedded files as a Uint32; writer.Write((uint)embedFilePaths.Length); this.embeddedFileSizes = new long[embedFilePaths.Length]; // Next write out the size of each file as a Uint64 in order. FileInfo[] files = new FileInfo[embedFilePaths.Length]; for (int i = 0; i < embedFilePaths.Length; ++i) { files[i] = new FileInfo(embedFilePaths[i]); this.embeddedFileSizes[i] = files[i].Length; writer.Write((ulong)this.embeddedFileSizes[i]); } // Next write out the content of each file *after* the sizes of // *all* of the files were written. foreach (FileInfo file in files) { using (FileStream stream = file.OpenRead()) { stream.CopyTo(writer.BaseStream); } } return writer; } } }