// 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;
}
}
}