chovy-sign/DiscUtils/Iso9660/CDBuilder.cs

498 lines
22 KiB
C#

//
// Copyright (c) 2008-2011, Kenneth Bell
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using DiscUtils.Streams;
namespace DiscUtils.Iso9660
{
/// <summary>
/// Class that creates ISO images.
/// </summary>
/// <example>
/// <code>
/// CDBuilder builder = new CDBuilder();
/// builder.VolumeIdentifier = "MYISO";
/// builder.UseJoliet = true;
/// builder.AddFile("Hello.txt", Encoding.ASCII.GetBytes("hello world!"));
/// builder.Build(@"C:\TEMP\myiso.iso");
/// </code>
/// </example>
public sealed class CDBuilder : StreamBuilder
{
private const long DiskStart = 0x8000;
private BootInitialEntry _bootEntry;
private Stream _bootImage;
private readonly BuildParameters _buildParams;
private readonly List<BuildDirectoryInfo> _dirs;
private readonly List<BuildFileInfo> _files;
private readonly BuildDirectoryInfo _rootDirectory;
/// <summary>
/// Initializes a new instance of the CDBuilder class.
/// </summary>
public CDBuilder()
{
_files = new List<BuildFileInfo>();
_dirs = new List<BuildDirectoryInfo>();
_rootDirectory = new BuildDirectoryInfo("\0", null);
_dirs.Add(_rootDirectory);
_buildParams = new BuildParameters();
_buildParams.UseJoliet = true;
}
/// <summary>
/// Gets or sets a value indicating whether to update the ISOLINUX info table at the
/// start of the boot image. Use with ISOLINUX only.
/// </summary>
/// <remarks>
/// ISOLINUX has an 'information table' at the start of the boot loader that verifies
/// the CD has been loaded correctly by the BIOS. This table needs to be updated
/// to match the actual ISO.
/// </remarks>
public bool UpdateIsolinuxBootTable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Joliet file-system extensions should be used.
/// </summary>
public bool UseJoliet
{
get { return _buildParams.UseJoliet; }
set { _buildParams.UseJoliet = value; }
}
/// <summary>
/// Gets or sets the Volume Identifier for the ISO file.
/// </summary>
/// <remarks>
/// Must be a valid identifier, i.e. max 32 characters in the range A-Z, 0-9 or _.
/// Lower-case characters are not permitted.
/// </remarks>
public string VolumeIdentifier
{
get { return _buildParams.VolumeIdentifier; }
set
{
if (value.Length > 32)
{
throw new ArgumentException("Not a valid volume identifier");
}
_buildParams.VolumeIdentifier = value;
}
}
/// <summary>
/// Sets the boot image for the ISO image.
/// </summary>
/// <param name="image">Stream containing the boot image.</param>
/// <param name="emulation">The type of emulation requested of the BIOS.</param>
/// <param name="loadSegment">The memory segment to load the image to (0 for default).</param>
public void SetBootImage(Stream image, BootDeviceEmulation emulation, int loadSegment)
{
if (_bootEntry != null)
{
throw new InvalidOperationException("Boot image already set");
}
_bootEntry = new BootInitialEntry();
_bootEntry.BootIndicator = 0x88;
_bootEntry.BootMediaType = emulation;
_bootEntry.LoadSegment = (ushort)loadSegment;
_bootEntry.SystemType = 0;
_bootImage = image;
}
/// <summary>
/// Adds a directory to the ISO image.
/// </summary>
/// <param name="name">The name of the directory on the ISO image.</param>
/// <returns>The object representing this directory.</returns>
/// <remarks>
/// The name is the full path to the directory, for example:
/// <example><code>
/// builder.AddDirectory(@"DIRA\DIRB\DIRC");
/// </code></example>
/// </remarks>
public BuildDirectoryInfo AddDirectory(string name)
{
string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
return GetDirectory(nameElements, nameElements.Length, true);
}
/// <summary>
/// Adds a byte array to the ISO image as a file.
/// </summary>
/// <param name="name">The name of the file on the ISO image.</param>
/// <param name="content">The contents of the file.</param>
/// <returns>The object representing this file.</returns>
/// <remarks>
/// The name is the full path to the file, for example:
/// <example><code>
/// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", new byte[]{0,1,2});
/// </code></example>
/// <para>Note the version number at the end of the file name is optional, if not
/// specified the default of 1 will be used.</para>
/// </remarks>
public BuildFileInfo AddFile(string name, byte[] content)
{
string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true);
BuildDirectoryMember existing;
if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing))
{
throw new IOException("File already exists");
}
BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, content);
_files.Add(fi);
dir.Add(fi);
return fi;
}
/// <summary>
/// Adds a disk file to the ISO image as a file.
/// </summary>
/// <param name="name">The name of the file on the ISO image.</param>
/// <param name="sourcePath">The name of the file on disk.</param>
/// <returns>The object representing this file.</returns>
/// <remarks>
/// The name is the full path to the file, for example:
/// <example><code>
/// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", @"C:\temp\tempfile.bin");
/// </code></example>
/// <para>Note the version number at the end of the file name is optional, if not
/// specified the default of 1 will be used.</para>
/// </remarks>
public BuildFileInfo AddFile(string name, string sourcePath)
{
string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true);
BuildDirectoryMember existing;
if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing))
{
throw new IOException("File already exists");
}
BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, sourcePath);
_files.Add(fi);
dir.Add(fi);
return fi;
}
/// <summary>
/// Adds a stream to the ISO image as a file.
/// </summary>
/// <param name="name">The name of the file on the ISO image.</param>
/// <param name="source">The contents of the file.</param>
/// <returns>The object representing this file.</returns>
/// <remarks>
/// The name is the full path to the file, for example:
/// <example><code>
/// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", stream);
/// </code></example>
/// <para>Note the version number at the end of the file name is optional, if not
/// specified the default of 1 will be used.</para>
/// </remarks>
public BuildFileInfo AddFile(string name, Stream source)
{
if (!source.CanSeek)
{
throw new ArgumentException("source doesn't support seeking", nameof(source));
}
string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true);
BuildDirectoryMember existing;
if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing))
{
throw new IOException("File already exists");
}
BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, source);
_files.Add(fi);
dir.Add(fi);
return fi;
}
protected override List<BuilderExtent> FixExtents(out long totalLength)
{
List<BuilderExtent> fixedRegions = new List<BuilderExtent>();
DateTime buildTime = DateTime.UtcNow;
Encoding suppEncoding = _buildParams.UseJoliet ? Encoding.BigEndianUnicode : Encoding.ASCII;
Dictionary<BuildDirectoryMember, uint> primaryLocationTable = new Dictionary<BuildDirectoryMember, uint>();
Dictionary<BuildDirectoryMember, uint> supplementaryLocationTable =
new Dictionary<BuildDirectoryMember, uint>();
long focus = DiskStart + 3 * IsoUtilities.SectorSize; // Primary, Supplementary, End (fixed at end...)
if (_bootEntry != null)
{
focus += IsoUtilities.SectorSize;
}
// ####################################################################
// # 0. Fix boot image location
// ####################################################################
long bootCatalogPos = 0;
if (_bootEntry != null)
{
long bootImagePos = focus;
Stream realBootImage = PatchBootImage(_bootImage, (uint)(DiskStart / IsoUtilities.SectorSize),
(uint)(bootImagePos / IsoUtilities.SectorSize));
BuilderStreamExtent bootImageExtent = new BuilderStreamExtent(focus, realBootImage);
fixedRegions.Add(bootImageExtent);
focus += MathUtilities.RoundUp(bootImageExtent.Length, IsoUtilities.SectorSize);
bootCatalogPos = focus;
byte[] bootCatalog = new byte[IsoUtilities.SectorSize];
BootValidationEntry bve = new BootValidationEntry();
bve.WriteTo(bootCatalog, 0x00);
_bootEntry.ImageStart = (uint)MathUtilities.Ceil(bootImagePos, IsoUtilities.SectorSize);
_bootEntry.SectorCount = (ushort)MathUtilities.Ceil(_bootImage.Length, Sizes.Sector);
_bootEntry.WriteTo(bootCatalog, 0x20);
fixedRegions.Add(new BuilderBufferExtent(bootCatalogPos, bootCatalog));
focus += IsoUtilities.SectorSize;
}
// ####################################################################
// # 1. Fix file locations
// ####################################################################
// Find end of the file data, fixing the files in place as we go
foreach (BuildFileInfo fi in _files)
{
primaryLocationTable.Add(fi, (uint)(focus / IsoUtilities.SectorSize));
supplementaryLocationTable.Add(fi, (uint)(focus / IsoUtilities.SectorSize));
FileExtent extent = new FileExtent(fi, focus);
// Only remember files of non-zero length (otherwise we'll stomp on a valid file)
if (extent.Length != 0)
{
fixedRegions.Add(extent);
}
focus += MathUtilities.RoundUp(extent.Length, IsoUtilities.SectorSize);
}
// ####################################################################
// # 2. Fix directory locations
// ####################################################################
// There are two directory tables
// 1. Primary (std ISO9660)
// 2. Supplementary (Joliet)
// Find start of the second set of directory data, fixing ASCII directories in place.
long startOfFirstDirData = focus;
foreach (BuildDirectoryInfo di in _dirs)
{
primaryLocationTable.Add(di, (uint)(focus / IsoUtilities.SectorSize));
DirectoryExtent extent = new DirectoryExtent(di, primaryLocationTable, Encoding.ASCII, focus);
fixedRegions.Add(extent);
focus += MathUtilities.RoundUp(extent.Length, IsoUtilities.SectorSize);
}
// Find end of the second directory table, fixing supplementary directories in place.
long startOfSecondDirData = focus;
foreach (BuildDirectoryInfo di in _dirs)
{
supplementaryLocationTable.Add(di, (uint)(focus / IsoUtilities.SectorSize));
DirectoryExtent extent = new DirectoryExtent(di, supplementaryLocationTable, suppEncoding, focus);
fixedRegions.Add(extent);
focus += MathUtilities.RoundUp(extent.Length, IsoUtilities.SectorSize);
}
// ####################################################################
// # 3. Fix path tables
// ####################################################################
// There are four path tables:
// 1. LE, ASCII
// 2. BE, ASCII
// 3. LE, Supp Encoding (Joliet)
// 4. BE, Supp Encoding (Joliet)
// Find end of the path table
long startOfFirstPathTable = focus;
PathTable pathTable = new PathTable(false, Encoding.ASCII, _dirs, primaryLocationTable, focus);
fixedRegions.Add(pathTable);
focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize);
long primaryPathTableLength = pathTable.Length;
long startOfSecondPathTable = focus;
pathTable = new PathTable(true, Encoding.ASCII, _dirs, primaryLocationTable, focus);
fixedRegions.Add(pathTable);
focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize);
long startOfThirdPathTable = focus;
pathTable = new PathTable(false, suppEncoding, _dirs, supplementaryLocationTable, focus);
fixedRegions.Add(pathTable);
focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize);
long supplementaryPathTableLength = pathTable.Length;
long startOfFourthPathTable = focus;
pathTable = new PathTable(true, suppEncoding, _dirs, supplementaryLocationTable, focus);
fixedRegions.Add(pathTable);
focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize);
// Find the end of the disk
totalLength = focus;
// ####################################################################
// # 4. Prepare volume descriptors now other structures are fixed
// ####################################################################
int regionIdx = 0;
focus = DiskStart;
PrimaryVolumeDescriptor pvDesc = new PrimaryVolumeDescriptor(
(uint)(totalLength / IsoUtilities.SectorSize), // VolumeSpaceSize
(uint)primaryPathTableLength, // PathTableSize
(uint)(startOfFirstPathTable / IsoUtilities.SectorSize), // TypeLPathTableLocation
(uint)(startOfSecondPathTable / IsoUtilities.SectorSize), // TypeMPathTableLocation
(uint)(startOfFirstDirData / IsoUtilities.SectorSize), // RootDirectory.LocationOfExtent
(uint)_rootDirectory.GetDataSize(Encoding.ASCII), // RootDirectory.DataLength
buildTime);
pvDesc.VolumeIdentifier = _buildParams.VolumeIdentifier;
PrimaryVolumeDescriptorRegion pvdr = new PrimaryVolumeDescriptorRegion(pvDesc, focus);
fixedRegions.Insert(regionIdx++, pvdr);
focus += IsoUtilities.SectorSize;
if (_bootEntry != null)
{
BootVolumeDescriptor bvDesc = new BootVolumeDescriptor(
(uint)(bootCatalogPos / IsoUtilities.SectorSize));
BootVolumeDescriptorRegion bvdr = new BootVolumeDescriptorRegion(bvDesc, focus);
fixedRegions.Insert(regionIdx++, bvdr);
focus += IsoUtilities.SectorSize;
}
SupplementaryVolumeDescriptor svDesc = new SupplementaryVolumeDescriptor(
(uint)(totalLength / IsoUtilities.SectorSize), // VolumeSpaceSize
(uint)supplementaryPathTableLength, // PathTableSize
(uint)(startOfThirdPathTable / IsoUtilities.SectorSize), // TypeLPathTableLocation
(uint)(startOfFourthPathTable / IsoUtilities.SectorSize), // TypeMPathTableLocation
(uint)(startOfSecondDirData / IsoUtilities.SectorSize), // RootDirectory.LocationOfExtent
(uint)_rootDirectory.GetDataSize(suppEncoding), // RootDirectory.DataLength
buildTime,
suppEncoding);
svDesc.VolumeIdentifier = _buildParams.VolumeIdentifier;
SupplementaryVolumeDescriptorRegion svdr = new SupplementaryVolumeDescriptorRegion(svDesc, focus);
fixedRegions.Insert(regionIdx++, svdr);
focus += IsoUtilities.SectorSize;
VolumeDescriptorSetTerminator evDesc = new VolumeDescriptorSetTerminator();
VolumeDescriptorSetTerminatorRegion evdr = new VolumeDescriptorSetTerminatorRegion(evDesc, focus);
fixedRegions.Insert(regionIdx++, evdr);
return fixedRegions;
}
/// <summary>
/// Patches a boot image (esp. for ISOLINUX) before it is written to the disk.
/// </summary>
/// <param name="bootImage">The original (master) boot image.</param>
/// <param name="pvdLba">The logical block address of the primary volume descriptor.</param>
/// <param name="bootImageLba">The logical block address of the boot image itself.</param>
/// <returns>A stream containing the patched boot image - does not need to be disposed.</returns>
private Stream PatchBootImage(Stream bootImage, uint pvdLba, uint bootImageLba)
{
// Early-exit if no patching to do...
if (!UpdateIsolinuxBootTable)
{
return bootImage;
}
byte[] bootData = StreamUtilities.ReadExact(bootImage, (int)bootImage.Length);
Array.Clear(bootData, 8, 56);
uint checkSum = 0;
for (int i = 64; i < bootData.Length; i += 4)
{
checkSum += EndianUtilities.ToUInt32LittleEndian(bootData, i);
}
EndianUtilities.WriteBytesLittleEndian(pvdLba, bootData, 8);
EndianUtilities.WriteBytesLittleEndian(bootImageLba, bootData, 12);
EndianUtilities.WriteBytesLittleEndian(bootData.Length, bootData, 16);
EndianUtilities.WriteBytesLittleEndian(checkSum, bootData, 20);
return new MemoryStream(bootData, false);
}
private BuildDirectoryInfo GetDirectory(string[] path, int pathLength, bool createMissing)
{
BuildDirectoryInfo di = TryGetDirectory(path, pathLength, createMissing);
if (di == null)
{
throw new DirectoryNotFoundException("Directory not found");
}
return di;
}
private BuildDirectoryInfo TryGetDirectory(string[] path, int pathLength, bool createMissing)
{
BuildDirectoryInfo focus = _rootDirectory;
for (int i = 0; i < pathLength; ++i)
{
BuildDirectoryMember next;
if (!focus.TryGetMember(path[i], out next))
{
if (createMissing)
{
// This directory doesn't exist, create it...
BuildDirectoryInfo di = new BuildDirectoryInfo(path[i], focus);
focus.Add(di);
_dirs.Add(di);
focus = di;
}
else
{
return null;
}
}
else
{
BuildDirectoryInfo nextAsBuildDirectoryInfo = next as BuildDirectoryInfo;
if (nextAsBuildDirectoryInfo == null)
{
throw new IOException("File with conflicting name exists");
}
focus = nextAsBuildDirectoryInfo;
}
}
return focus;
}
}
}