chovy-sign/DiscUtils/Iso9660/VfsCDReader.cs

524 lines
19 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 DiscUtils.Streams;
using DiscUtils.Vfs;
namespace DiscUtils.Iso9660
{
internal class VfsCDReader : VfsReadOnlyFileSystem<ReaderDirEntry, File, ReaderDirectory, IsoContext>,
IClusterBasedFileSystem, IUnixFileSystem
{
private static readonly Iso9660Variant[] DefaultVariantsNoJoliet = { Iso9660Variant.RockRidge, Iso9660Variant.Iso9660 };
private static readonly Iso9660Variant[] DefaultVariantsWithJoliet = { Iso9660Variant.Joliet, Iso9660Variant.RockRidge, Iso9660Variant.Iso9660 };
private byte[] _bootCatalog;
private readonly BootVolumeDescriptor _bootVolDesc;
private readonly Stream _data;
private readonly bool _hideVersions;
/// <summary>
/// Initializes a new instance of the VfsCDReader class.
/// </summary>
/// <param name="data">The stream to read the ISO image from.</param>
/// <param name="joliet">Whether to read Joliet extensions.</param>
/// <param name="hideVersions">Hides version numbers (e.g. ";1") from the end of files.</param>
public VfsCDReader(Stream data, bool joliet, bool hideVersions)
: this(data, joliet ? DefaultVariantsWithJoliet : DefaultVariantsNoJoliet, hideVersions) {}
/// <summary>
/// Initializes a new instance of the VfsCDReader class.
/// </summary>
/// <param name="data">The stream to read the ISO image from.</param>
/// <param name="variantPriorities">Which possible file system variants to use, and with which priority.</param>
/// <param name="hideVersions">Hides version numbers (e.g. ";1") from the end of files.</param>
/// <remarks>
/// <para>
/// The implementation considers each of the file system variants in <c>variantProperties</c> and selects
/// the first which is determined to be present. In this example Joliet, then Rock Ridge, then vanilla
/// Iso9660 will be considered:
/// </para>
/// <code lang="cs">
/// VfsCDReader(stream, new Iso9660Variant[] {Joliet, RockRidge, Iso9660}, true);
/// </code>
/// <para>The Iso9660 variant should normally be specified as the final entry in the list. Placing it earlier
/// in the list will effectively mask later items and not including it may prevent some ISOs from being read.</para>
/// </remarks>
public VfsCDReader(Stream data, Iso9660Variant[] variantPriorities, bool hideVersions)
: base(new DiscFileSystemOptions())
{
_data = data;
_hideVersions = hideVersions;
long vdpos = 0x8000; // Skip lead-in
byte[] buffer = new byte[IsoUtilities.SectorSize];
long pvdPos = 0;
long svdPos = 0;
BaseVolumeDescriptor bvd;
do
{
data.Position = vdpos;
int numRead = data.Read(buffer, 0, IsoUtilities.SectorSize);
if (numRead != IsoUtilities.SectorSize)
{
break;
}
bvd = new BaseVolumeDescriptor(buffer, 0);
if (bvd.StandardIdentifier != BaseVolumeDescriptor.Iso9660StandardIdentifier)
{
throw new InvalidFileSystemException("Volume is not ISO-9660");
}
switch (bvd.VolumeDescriptorType)
{
case VolumeDescriptorType.Boot:
_bootVolDesc = new BootVolumeDescriptor(buffer, 0);
if (_bootVolDesc.SystemId != BootVolumeDescriptor.ElToritoSystemIdentifier)
{
_bootVolDesc = null;
}
break;
case VolumeDescriptorType.Primary: // Primary Vol Descriptor
pvdPos = vdpos;
break;
case VolumeDescriptorType.Supplementary: // Supplementary Vol Descriptor
svdPos = vdpos;
break;
case VolumeDescriptorType.Partition: // Volume Partition Descriptor
break;
case VolumeDescriptorType.SetTerminator: // Volume Descriptor Set Terminator
break;
}
vdpos += IsoUtilities.SectorSize;
} while (bvd.VolumeDescriptorType != VolumeDescriptorType.SetTerminator);
ActiveVariant = Iso9660Variant.None;
foreach (Iso9660Variant variant in variantPriorities)
{
switch (variant)
{
case Iso9660Variant.Joliet:
if (svdPos != 0)
{
data.Position = svdPos;
data.Read(buffer, 0, IsoUtilities.SectorSize);
SupplementaryVolumeDescriptor volDesc = new SupplementaryVolumeDescriptor(buffer, 0);
Context = new IsoContext { VolumeDescriptor = volDesc, DataStream = _data };
RootDirectory = new ReaderDirectory(Context,
new ReaderDirEntry(Context, volDesc.RootDirectory));
ActiveVariant = Iso9660Variant.Iso9660;
}
break;
case Iso9660Variant.RockRidge:
case Iso9660Variant.Iso9660:
if (pvdPos != 0)
{
data.Position = pvdPos;
data.Read(buffer, 0, IsoUtilities.SectorSize);
PrimaryVolumeDescriptor volDesc = new PrimaryVolumeDescriptor(buffer, 0);
IsoContext context = new IsoContext { VolumeDescriptor = volDesc, DataStream = _data };
DirectoryRecord rootSelfRecord = ReadRootSelfRecord(context);
InitializeSusp(context, rootSelfRecord);
if (variant == Iso9660Variant.Iso9660
||
(variant == Iso9660Variant.RockRidge &&
!string.IsNullOrEmpty(context.RockRidgeIdentifier)))
{
Context = context;
RootDirectory = new ReaderDirectory(context, new ReaderDirEntry(context, rootSelfRecord));
ActiveVariant = variant;
}
}
break;
}
if (ActiveVariant != Iso9660Variant.None)
{
break;
}
}
if (ActiveVariant == Iso9660Variant.None)
{
throw new IOException("None of the permitted ISO9660 file system variants was detected");
}
}
public Iso9660Variant ActiveVariant { get; }
public BootDeviceEmulation BootEmulation
{
get
{
BootInitialEntry initialEntry = GetBootInitialEntry();
if (initialEntry != null)
{
return initialEntry.BootMediaType;
}
return BootDeviceEmulation.NoEmulation;
}
}
public long BootImageStart
{
get
{
BootInitialEntry initialEntry = GetBootInitialEntry();
if (initialEntry != null)
{
return initialEntry.ImageStart * IsoUtilities.SectorSize;
}
return 0;
}
}
public int BootLoadSegment
{
get
{
BootInitialEntry initialEntry = GetBootInitialEntry();
if (initialEntry != null)
{
return initialEntry.LoadSegment;
}
return 0;
}
}
/// <summary>
/// Provides the friendly name for the CD filesystem.
/// </summary>
public override string FriendlyName
{
get { return "ISO 9660 (CD-ROM)"; }
}
public bool HasBootImage
{
get
{
if (_bootVolDesc == null)
{
return false;
}
byte[] bootCatalog = GetBootCatalog();
if (bootCatalog == null)
{
return false;
}
BootValidationEntry entry = new BootValidationEntry(bootCatalog, 0);
return entry.ChecksumValid;
}
}
/// <summary>
/// Gets the Volume Identifier.
/// </summary>
public override string VolumeLabel
{
get { return Context.VolumeDescriptor.VolumeIdentifier; }
}
public long ClusterSize
{
get { return IsoUtilities.SectorSize; }
}
public long TotalClusters
{
get { return Context.VolumeDescriptor.VolumeSpaceSize; }
}
public long ClusterToOffset(long cluster)
{
return cluster * ClusterSize;
}
public long OffsetToCluster(long offset)
{
return offset / ClusterSize;
}
/// <summary>
/// Size of the Filesystem in bytes
/// </summary>
public override long Size
{
get { throw new NotSupportedException("Filesystem size is not (yet) supported"); }
}
/// <summary>
/// Used space of the Filesystem in bytes
/// </summary>
public override long UsedSpace
{
get { throw new NotSupportedException("Filesystem size is not (yet) supported"); }
}
/// <summary>
/// Available space of the Filesystem in bytes
/// </summary>
public override long AvailableSpace
{
get { throw new NotSupportedException("Filesystem size is not (yet) supported"); }
}
public Range<long, long>[] PathToClusters(string path)
{
ReaderDirEntry entry = GetDirectoryEntry(path);
if (entry == null)
{
throw new FileNotFoundException("File not found", path);
}
if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0)
{
throw new NotSupportedException("Non-contiguous extents not supported");
}
return new[]
{
new Range<long, long>(entry.Record.LocationOfExtent,
MathUtilities.Ceil(entry.Record.DataLength, IsoUtilities.SectorSize))
};
}
public StreamExtent[] PathToExtents(string path)
{
ReaderDirEntry entry = GetDirectoryEntry(path);
if (entry == null)
{
throw new FileNotFoundException("File not found", path);
}
if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0)
{
throw new NotSupportedException("Non-contiguous extents not supported");
}
return new[]
{ new StreamExtent(entry.Record.LocationOfExtent * IsoUtilities.SectorSize, entry.Record.DataLength) };
}
public ClusterMap BuildClusterMap()
{
long totalClusters = TotalClusters;
ClusterRoles[] clusterToRole = new ClusterRoles[totalClusters];
object[] clusterToFileId = new object[totalClusters];
Dictionary<object, string[]> fileIdToPaths = new Dictionary<object, string[]>();
ForAllDirEntries(
string.Empty,
(path, entry) =>
{
string[] paths = null;
if (fileIdToPaths.ContainsKey(entry.UniqueCacheId))
{
paths = fileIdToPaths[entry.UniqueCacheId];
}
if (paths == null)
{
fileIdToPaths[entry.UniqueCacheId] = new[] { path };
}
else
{
string[] newPaths = new string[paths.Length + 1];
Array.Copy(paths, newPaths, paths.Length);
newPaths[paths.Length] = path;
fileIdToPaths[entry.UniqueCacheId] = newPaths;
}
if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0)
{
throw new NotSupportedException("Non-contiguous extents not supported");
}
long clusters = MathUtilities.Ceil(entry.Record.DataLength, IsoUtilities.SectorSize);
for (long i = 0; i < clusters; ++i)
{
clusterToRole[i + entry.Record.LocationOfExtent] = ClusterRoles.DataFile;
clusterToFileId[i + entry.Record.LocationOfExtent] = entry.UniqueCacheId;
}
});
return new ClusterMap(clusterToRole, clusterToFileId, fileIdToPaths);
}
public UnixFileSystemInfo GetUnixFileInfo(string path)
{
File file = GetFile(path);
return file.UnixFileInfo;
}
public Stream OpenBootImage()
{
BootInitialEntry initialEntry = GetBootInitialEntry();
if (initialEntry != null)
{
return new SubStream(_data, initialEntry.ImageStart * IsoUtilities.SectorSize,
initialEntry.SectorCount * Sizes.Sector);
}
throw new InvalidOperationException("No valid boot image");
}
protected override File ConvertDirEntryToFile(ReaderDirEntry dirEntry)
{
if (dirEntry.IsDirectory)
{
return new ReaderDirectory(Context, dirEntry);
}
return new File(Context, dirEntry);
}
protected override string FormatFileName(string name)
{
if (_hideVersions)
{
int pos = name.LastIndexOf(';');
if (pos > 0)
{
return name.Substring(0, pos);
}
}
return name;
}
private static void InitializeSusp(IsoContext context, DirectoryRecord rootSelfRecord)
{
// Stage 1 - SUSP present?
List<SuspExtension> extensions = new List<SuspExtension>();
if (!SuspRecords.DetectSharingProtocol(rootSelfRecord.SystemUseData, 0))
{
context.SuspExtensions = new List<SuspExtension>();
context.SuspDetected = false;
return;
}
context.SuspDetected = true;
SuspRecords suspRecords = new SuspRecords(context, rootSelfRecord.SystemUseData, 0);
// Stage 2 - Init general SUSP params
SharingProtocolSystemUseEntry spEntry =
(SharingProtocolSystemUseEntry)suspRecords.GetEntries(null, "SP")[0];
context.SuspSkipBytes = spEntry.SystemAreaSkip;
// Stage 3 - Init extensions
List<SystemUseEntry> extensionEntries = suspRecords.GetEntries(null, "ER");
if (extensionEntries != null)
{
foreach (ExtensionSystemUseEntry extension in extensionEntries)
{
switch (extension.ExtensionIdentifier)
{
case "RRIP_1991A":
case "IEEE_P1282":
case "IEEE_1282":
extensions.Add(new RockRidgeExtension(extension.ExtensionIdentifier));
context.RockRidgeIdentifier = extension.ExtensionIdentifier;
break;
default:
extensions.Add(new GenericSuspExtension(extension.ExtensionIdentifier));
break;
}
}
}
else if (suspRecords.GetEntries(null, "RR") != null)
{
// Some ISO creators don't add the 'ER' record for RockRidge, but write the (legacy)
// RR record anyway
extensions.Add(new RockRidgeExtension("RRIP_1991A"));
context.RockRidgeIdentifier = "RRIP_1991A";
}
context.SuspExtensions = extensions;
}
private static DirectoryRecord ReadRootSelfRecord(IsoContext context)
{
context.DataStream.Position = context.VolumeDescriptor.RootDirectory.LocationOfExtent *
context.VolumeDescriptor.LogicalBlockSize;
byte[] firstSector = StreamUtilities.ReadExact(context.DataStream, context.VolumeDescriptor.LogicalBlockSize);
DirectoryRecord rootSelfRecord;
DirectoryRecord.ReadFrom(firstSector, 0, context.VolumeDescriptor.CharacterEncoding, out rootSelfRecord);
return rootSelfRecord;
}
private BootInitialEntry GetBootInitialEntry()
{
byte[] bootCatalog = GetBootCatalog();
if (bootCatalog == null)
{
return null;
}
BootValidationEntry validationEntry = new BootValidationEntry(bootCatalog, 0);
if (!validationEntry.ChecksumValid)
{
return null;
}
return new BootInitialEntry(bootCatalog, 0x20);
}
private byte[] GetBootCatalog()
{
if (_bootCatalog == null && _bootVolDesc != null)
{
_data.Position = _bootVolDesc.CatalogSector * IsoUtilities.SectorSize;
_bootCatalog = StreamUtilities.ReadExact(_data, IsoUtilities.SectorSize);
}
return _bootCatalog;
}
}
}