diff --git a/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj b/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj index b7b9fc676..5a7f27db6 100644 --- a/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj +++ b/DiscImageChef.Filesystems/DiscImageChef.Filesystems.csproj @@ -71,6 +71,8 @@ + + @@ -184,7 +186,7 @@ - + diff --git a/DiscImageChef.Filesystems/ISO9660/Consts/Internal.cs b/DiscImageChef.Filesystems/ISO9660/Consts/Internal.cs new file mode 100644 index 000000000..e0e674838 --- /dev/null +++ b/DiscImageChef.Filesystems/ISO9660/Consts/Internal.cs @@ -0,0 +1,53 @@ +// /*************************************************************************** +// The Disc Image Chef +// ---------------------------------------------------------------------------- +// +// Filename : AAIP.cs +// Author(s) : Natalia Portillo +// +// Component : ISO9660 filesystem plugin. +// +// --[ Description ] ---------------------------------------------------------- +// +// AAIP extensions constants and enumerations. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2019 Natalia Portillo +// ****************************************************************************/ + +using DiscImageChef.Helpers; + +namespace DiscImageChef.Filesystems.ISO9660 +{ + public partial class ISO9660 + { + static readonly int DirectoryRecordSize = Marshal.SizeOf(); + + enum Namespace + { + Normal, + Vms, + Joliet, + JolietNormal, + Rrip, + RripNormal, + RripJoliet, + RripJolietNormal + } + } +} \ No newline at end of file diff --git a/DiscImageChef.Filesystems/ISO9660/Date.cs b/DiscImageChef.Filesystems/ISO9660/Date.cs new file mode 100644 index 000000000..20810a984 --- /dev/null +++ b/DiscImageChef.Filesystems/ISO9660/Date.cs @@ -0,0 +1,9 @@ +using System; + +namespace DiscImageChef.Filesystems.ISO9660 +{ + public partial class ISO9660 + { + DateTime DecodeIsoDateTime(byte[] date) => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/DiscImageChef.Filesystems/ISO9660/Dir.cs b/DiscImageChef.Filesystems/ISO9660/Dir.cs index cfcf074be..0a431c7a3 100644 --- a/DiscImageChef.Filesystems/ISO9660/Dir.cs +++ b/DiscImageChef.Filesystems/ISO9660/Dir.cs @@ -1,11 +1,61 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using DiscImageChef.CommonTypes.Structs; +using DiscImageChef.Helpers; namespace DiscImageChef.Filesystems.ISO9660 { public partial class ISO9660 { public Errno ReadDir(string path, out List contents) => throw new NotImplementedException(); + + List DecodeCdiDirectory(byte[] data) => throw new NotImplementedException(); + + List DecodeHighSierraDirectory(byte[] data) => throw new NotImplementedException(); + + // TODO: Implement system area + List DecodeIsoDirectory(byte[] data) + { + List entries = new List(); + int entryOff = 0; + + while(entryOff + DirectoryRecordSize < data.Length) + { + DirectoryRecord record = + Marshal.ByteArrayToStructureLittleEndian(data, entryOff, + Marshal.SizeOf()); + + if(record.length == 0) break; + + // Special entries for current and parent directories, skip them + if(record.name_len == 1) + if(data[entryOff + DirectoryRecordSize] == 0 || data[entryOff + DirectoryRecordSize] == 1) + { + entryOff += record.length; + continue; + } + + DecodedDirectoryEntry entry = new DecodedDirectoryEntry(); + + entry.Extent = record.extent; + entry.Size = record.size; + entry.Flags = record.flags; + entry.FileUnitSize = record.file_unit_size; + entry.Interleave = record.interleave; + entry.VolumeSequenceNumber = record.volume_sequence_number; + entry.IsoFilename = + Encoding.ASCII.GetString(data, entryOff + DirectoryRecordSize, record.name_len); + entry.Timestamp = DecodeIsoDateTime(record.date); + + // TODO: Multi-extent files + if(entries.All(e => e.IsoFilename != entry.IsoFilename)) entries.Add(entry); + + entryOff += record.length; + } + + return entries; + } } } \ No newline at end of file diff --git a/DiscImageChef.Filesystems/ISO9660/ISO9660.cs b/DiscImageChef.Filesystems/ISO9660/ISO9660.cs index 860b23dc8..bee38803c 100644 --- a/DiscImageChef.Filesystems/ISO9660/ISO9660.cs +++ b/DiscImageChef.Filesystems/ISO9660/ISO9660.cs @@ -42,6 +42,8 @@ namespace DiscImageChef.Filesystems.ISO9660 // This is coded following ECMA-119. public partial class ISO9660 : IReadOnlyFilesystem { + Namespace @namespace; + public FileSystemType XmlFsType { get; private set; } public Encoding Encoding { get; private set; } public string Name => "ISO9660 Filesystem"; @@ -57,7 +59,11 @@ namespace DiscImageChef.Filesystems.ISO9660 {"normal", "Primary Volume Descriptor, ignoring ;1 suffixes"}, {"vms", "Primary Volume Descriptor, showing version suffixes"}, {"joliet", "Joliet Volume Descriptor"}, - {"joliet+normal", "Joliet with fallback to normal"} + {"joliet+normal", "Joliet with fallback to normal"}, + {"rrip", "Rock Ridge"}, + {"rrip+normal", "Rock Ridge with fallback to normal"}, + {"rrip+joliet", "Rock Ridge with fallback to Joliet"}, + {"rrip+joliet+normal", "Rock Ridge with fallback to Joliet and then to normal (default)"} }; public Errno ReadLink(string path, out string dest) diff --git a/DiscImageChef.Filesystems/ISO9660/Structs/Common.cs b/DiscImageChef.Filesystems/ISO9660/Structs/Internal.cs similarity index 83% rename from DiscImageChef.Filesystems/ISO9660/Structs/Common.cs rename to DiscImageChef.Filesystems/ISO9660/Structs/Internal.cs index dcf60bc3c..ed9932fc9 100644 --- a/DiscImageChef.Filesystems/ISO9660/Structs/Common.cs +++ b/DiscImageChef.Filesystems/ISO9660/Structs/Internal.cs @@ -54,5 +54,19 @@ namespace DiscImageChef.Filesystems.ISO9660 public ushort BlockSize; public uint Blocks; } + + struct DecodedDirectoryEntry + { + public uint Extent; + public uint Size; + public FileFlags Flags; + public byte FileUnitSize; + public byte Interleave; + public ushort VolumeSequenceNumber; + public string IsoFilename; + public DateTime Timestamp; + + public override string ToString() => IsoFilename; + } } } \ No newline at end of file diff --git a/DiscImageChef.Filesystems/ISO9660/Super.cs b/DiscImageChef.Filesystems/ISO9660/Super.cs index 5adf9c052..f50f23216 100644 --- a/DiscImageChef.Filesystems/ISO9660/Super.cs +++ b/DiscImageChef.Filesystems/ISO9660/Super.cs @@ -4,16 +4,342 @@ using System.Text; using DiscImageChef.CommonTypes; using DiscImageChef.CommonTypes.Interfaces; using DiscImageChef.CommonTypes.Structs; +using DiscImageChef.Console; +using DiscImageChef.Decoders.Sega; +using DiscImageChef.Helpers; +using Schemas; namespace DiscImageChef.Filesystems.ISO9660 { public partial class ISO9660 { - public Errno Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding, - Dictionary options, string @namespace) => - throw new NotImplementedException(); + bool debug; + IMediaImage image; + bool mounted; + List rootDirectory; - public Errno Unmount() => throw new NotImplementedException(); + public Errno Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding, + Dictionary options, string @namespace) + { + Encoding = encoding ?? Encoding.ASCII; + byte[] vdMagic = new byte[5]; // Volume Descriptor magic "CD001" + byte[] hsMagic = new byte[5]; // Volume Descriptor magic "CDROM" + + if(options == null) options = GetDefaultOptions(); + if(options.TryGetValue("debug", out string debugString)) bool.TryParse(debugString, out debug); + + // Default namespace + if(@namespace is null) @namespace = "rrip+joliet+normal"; + + switch(@namespace.ToLowerInvariant()) + { + case "normal": + this.@namespace = Namespace.Normal; + break; + case "vms": + this.@namespace = Namespace.Vms; + break; + case "joliet": + this.@namespace = Namespace.Joliet; + break; + case "joliet+normal": + this.@namespace = Namespace.JolietNormal; + break; + case "rrip": + this.@namespace = Namespace.Rrip; + break; + case "rrip+normal": + this.@namespace = Namespace.RripNormal; + break; + case "rrip+joliet": + this.@namespace = Namespace.RripJoliet; + break; + case "rrip+joliet+normal": + this.@namespace = Namespace.RripJolietNormal; + break; + default: return Errno.InvalidArgument; + } + + PrimaryVolumeDescriptor? pvd = null; + PrimaryVolumeDescriptor? jolietvd = null; + BootRecord? bvd = null; + HighSierraPrimaryVolumeDescriptor? hsvd = null; + FileStructureVolumeDescriptor? fsvd = null; + + // ISO9660 is designed for 2048 bytes/sector devices + if(imagePlugin.Info.SectorSize < 2048) return Errno.InvalidArgument; + + // ISO9660 Primary Volume Descriptor starts at sector 16, so that's minimal size. + if(partition.End < 16) return Errno.InvalidArgument; + + ulong counter = 0; + + byte[] vdSector = imagePlugin.ReadSector(16 + counter + partition.Start); + int xaOff = vdSector.Length == 2336 ? 8 : 0; + Array.Copy(vdSector, 0x009 + xaOff, hsMagic, 0, 5); + bool highSierra = Encoding.GetString(hsMagic) == HIGH_SIERRA_MAGIC; + int hsOff = 0; + if(highSierra) hsOff = 8; + bool cdi = false; + + while(true) + { + DicConsole.DebugWriteLine("ISO9660 plugin", "Processing VD loop no. {0}", counter); + // Seek to Volume Descriptor + DicConsole.DebugWriteLine("ISO9660 plugin", "Reading sector {0}", 16 + counter + partition.Start); + byte[] vdSectorTmp = imagePlugin.ReadSector(16 + counter + partition.Start); + vdSector = new byte[vdSectorTmp.Length - xaOff]; + Array.Copy(vdSectorTmp, xaOff, vdSector, 0, vdSector.Length); + + byte vdType = vdSector[0 + hsOff]; // Volume Descriptor Type, should be 1 or 2. + DicConsole.DebugWriteLine("ISO9660 plugin", "VDType = {0}", vdType); + + if(vdType == 255) // Supposedly we are in the PVD. + { + if(counter == 0) return Errno.InvalidArgument; + + break; + } + + Array.Copy(vdSector, 0x001, vdMagic, 0, 5); + Array.Copy(vdSector, 0x009, hsMagic, 0, 5); + + if(Encoding.GetString(vdMagic) != ISO_MAGIC && Encoding.GetString(hsMagic) != HIGH_SIERRA_MAGIC && + Encoding.GetString(vdMagic) != CDI_MAGIC + ) // Recognized, it is an ISO9660, now check for rest of data. + { + if(counter == 0) return Errno.InvalidArgument; + + break; + } + + cdi |= Encoding.GetString(vdMagic) == CDI_MAGIC; + + switch(vdType) + { + case 0: + { + bvd = Marshal.ByteArrayToStructureLittleEndian(vdSector, hsOff, 2048 - hsOff); + + // TODO: Add boot file to debug root directory + + break; + } + + case 1: + { + if(highSierra) + hsvd = Marshal + .ByteArrayToStructureLittleEndian(vdSector); + else if(cdi) + fsvd = Marshal.ByteArrayToStructureBigEndian(vdSector); + else pvd = Marshal.ByteArrayToStructureLittleEndian(vdSector); + + break; + } + + case 2: + { + PrimaryVolumeDescriptor svd = + Marshal.ByteArrayToStructureLittleEndian(vdSector); + + // Check if this is Joliet + if(svd.escape_sequences[0] == '%' && svd.escape_sequences[1] == '/') + if(svd.escape_sequences[2] == '@' || svd.escape_sequences[2] == 'C' || + svd.escape_sequences[2] == 'E') jolietvd = svd; + else + DicConsole.WriteLine("ISO9660 plugin", "Found unknown supplementary volume descriptor"); + + break; + } + } + + counter++; + } + + DecodedVolumeDescriptor decodedVd; + DecodedVolumeDescriptor decodedJolietVd = new DecodedVolumeDescriptor(); + + XmlFsType = new FileSystemType(); + + if(pvd == null && hsvd == null && fsvd == null) + { + DicConsole.ErrorWriteLine("ERROR: Could not find primary volume descriptor"); + return Errno.InvalidArgument; + } + + if(highSierra) decodedVd = DecodeVolumeDescriptor(hsvd.Value); + else if(cdi) decodedVd = DecodeVolumeDescriptor(fsvd.Value); + else decodedVd = DecodeVolumeDescriptor(pvd.Value); + + if(jolietvd != null) decodedJolietVd = DecodeJolietDescriptor(jolietvd.Value); + + string fsFormat; + if(highSierra) fsFormat = "High Sierra Format"; + else if(cdi) + { + fsFormat = "CD-i"; + // TODO: Implement CD-i + return Errno.NotImplemented; + } + else fsFormat = "ISO9660"; + + uint rootLocation = 0; + uint rootSize = 0; + + // TODO: Read CD-i root directory + if(!cdi) + { + rootLocation = highSierra + ? hsvd.Value.root_directory_record.extent + : pvd.Value.root_directory_record.extent; + + if(highSierra) + { + rootSize = hsvd.Value.root_directory_record.size / hsvd.Value.logical_block_size; + if(hsvd.Value.root_directory_record.size % hsvd.Value.logical_block_size > 0) rootSize++; + } + else + { + rootSize = pvd.Value.root_directory_record.size / pvd.Value.logical_block_size; + if(pvd.Value.root_directory_record.size % pvd.Value.logical_block_size > 0) rootSize++; + } + } + + if(rootLocation + rootSize >= imagePlugin.Info.Sectors) return Errno.InvalidArgument; + + byte[] rootDir = imagePlugin.ReadSectors(rootLocation, rootSize); + + byte[] ipbinSector = imagePlugin.ReadSector(0 + partition.Start); + CD.IPBin? segaCd = CD.DecodeIPBin(ipbinSector); + Saturn.IPBin? saturn = Saturn.DecodeIPBin(ipbinSector); + Dreamcast.IPBin? dreamcast = Dreamcast.DecodeIPBin(ipbinSector); + + // TODO: Add IP.BIN to debug root directory + // TODO: Add volume descriptors to debug root directory + // TODO: Decode Joliet directory + + rootDirectory = cdi + ? DecodeCdiDirectory(rootDir) + : highSierra + ? DecodeHighSierraDirectory(rootDir) + : DecodeIsoDirectory(rootDir); + + XmlFsType.Type = fsFormat; + + if(jolietvd != null && this.@namespace != Namespace.Normal && + this.@namespace != Namespace.Vms && + this.@namespace != Namespace.Rrip && this.@namespace != Namespace.RripNormal) + { + XmlFsType.VolumeName = decodedJolietVd.VolumeIdentifier; + + if(string.IsNullOrEmpty(decodedJolietVd.SystemIdentifier) || + decodedVd.SystemIdentifier.Length > decodedJolietVd.SystemIdentifier.Length) + XmlFsType.SystemIdentifier = decodedVd.SystemIdentifier; + else + XmlFsType.SystemIdentifier = string.IsNullOrEmpty(decodedJolietVd.SystemIdentifier) + ? null + : decodedJolietVd.SystemIdentifier; + + if(string.IsNullOrEmpty(decodedJolietVd.VolumeSetIdentifier) || decodedVd.VolumeSetIdentifier.Length > + decodedJolietVd.VolumeSetIdentifier.Length) + XmlFsType.VolumeSetIdentifier = decodedVd.VolumeSetIdentifier; + else + XmlFsType.VolumeSetIdentifier = string.IsNullOrEmpty(decodedJolietVd.VolumeSetIdentifier) + ? null + : decodedJolietVd.VolumeSetIdentifier; + + if(string.IsNullOrEmpty(decodedJolietVd.PublisherIdentifier) || decodedVd.PublisherIdentifier.Length > + decodedJolietVd.PublisherIdentifier.Length) + XmlFsType.PublisherIdentifier = decodedVd.PublisherIdentifier; + else + XmlFsType.PublisherIdentifier = string.IsNullOrEmpty(decodedJolietVd.PublisherIdentifier) + ? null + : decodedJolietVd.PublisherIdentifier; + + if(string.IsNullOrEmpty(decodedJolietVd.DataPreparerIdentifier) || + decodedVd.DataPreparerIdentifier.Length > decodedJolietVd.DataPreparerIdentifier.Length) + XmlFsType.DataPreparerIdentifier = decodedVd.DataPreparerIdentifier; + else + XmlFsType.DataPreparerIdentifier = string.IsNullOrEmpty(decodedJolietVd.DataPreparerIdentifier) + ? null + : decodedJolietVd.DataPreparerIdentifier; + + if(string.IsNullOrEmpty(decodedJolietVd.ApplicationIdentifier) || + decodedVd.ApplicationIdentifier.Length > decodedJolietVd.ApplicationIdentifier.Length) + XmlFsType.ApplicationIdentifier = decodedVd.ApplicationIdentifier; + else + XmlFsType.ApplicationIdentifier = string.IsNullOrEmpty(decodedJolietVd.ApplicationIdentifier) + ? null + : decodedJolietVd.ApplicationIdentifier; + + XmlFsType.CreationDate = decodedJolietVd.CreationTime; + XmlFsType.CreationDateSpecified = true; + if(decodedJolietVd.HasModificationTime) + { + XmlFsType.ModificationDate = decodedJolietVd.ModificationTime; + XmlFsType.ModificationDateSpecified = true; + } + + if(decodedJolietVd.HasExpirationTime) + { + XmlFsType.ExpirationDate = decodedJolietVd.ExpirationTime; + XmlFsType.ExpirationDateSpecified = true; + } + + if(decodedJolietVd.HasEffectiveTime) + { + XmlFsType.EffectiveDate = decodedJolietVd.EffectiveTime; + XmlFsType.EffectiveDateSpecified = true; + } + } + else + { + XmlFsType.SystemIdentifier = decodedVd.SystemIdentifier; + XmlFsType.VolumeName = decodedVd.VolumeIdentifier; + XmlFsType.VolumeSetIdentifier = decodedVd.VolumeSetIdentifier; + XmlFsType.PublisherIdentifier = decodedVd.PublisherIdentifier; + XmlFsType.DataPreparerIdentifier = decodedVd.DataPreparerIdentifier; + XmlFsType.ApplicationIdentifier = decodedVd.ApplicationIdentifier; + XmlFsType.CreationDate = decodedVd.CreationTime; + XmlFsType.CreationDateSpecified = true; + if(decodedVd.HasModificationTime) + { + XmlFsType.ModificationDate = decodedVd.ModificationTime; + XmlFsType.ModificationDateSpecified = true; + } + + if(decodedVd.HasExpirationTime) + { + XmlFsType.ExpirationDate = decodedVd.ExpirationTime; + XmlFsType.ExpirationDateSpecified = true; + } + + if(decodedVd.HasEffectiveTime) + { + XmlFsType.EffectiveDate = decodedVd.EffectiveTime; + XmlFsType.EffectiveDateSpecified = true; + } + } + + XmlFsType.Bootable |= bvd != null || segaCd != null || saturn != null || dreamcast != null; + XmlFsType.Clusters = decodedVd.Blocks; + XmlFsType.ClusterSize = decodedVd.BlockSize; + + image = imagePlugin; + mounted = true; + return Errno.NoError; + } + + public Errno Unmount() + { + if(!mounted) return Errno.AccessDenied; + + rootDirectory = null; + mounted = false; + + return Errno.NoError; + } public Errno StatFs(out FileSystemInfo stat) => throw new NotImplementedException(); }