diff --git a/Aaru.Images/Localization/Localization.Designer.cs b/Aaru.Images/Localization/Localization.Designer.cs index ad511e728..cceab5663 100644 --- a/Aaru.Images/Localization/Localization.Designer.cs +++ b/Aaru.Images/Localization/Localization.Designer.cs @@ -6087,5 +6087,53 @@ namespace Aaru.Images { return ResourceManager.GetString("Ewf_Identify_Signature_0", resourceCulture); } } + + internal static string MagicIso_disc_image { + get { + return ResourceManager.GetString("MagicIso_disc_image", resourceCulture); + } + } + + internal static string MagicIso_bbis_signature_not_found { + get { + return ResourceManager.GetString("MagicIso_bbis_signature_not_found", resourceCulture); + } + } + + internal static string MagicIso_blhr_signature_not_found { + get { + return ResourceManager.GetString("MagicIso_blhr_signature_not_found", resourceCulture); + } + } + + internal static string MagicIso_password_protected_images_are_not_supported { + get { + return ResourceManager.GetString("MagicIso_password_protected_images_are_not_supported", resourceCulture); + } + } + + internal static string MagicIso_unsupported_image_type_0 { + get { + return ResourceManager.GetString("MagicIso_unsupported_image_type_0", resourceCulture); + } + } + + internal static string MagicIso_unsupported_block_type_0 { + get { + return ResourceManager.GetString("MagicIso_unsupported_block_type_0", resourceCulture); + } + } + + internal static string MagicIso_NRG_trailer_not_found { + get { + return ResourceManager.GetString("MagicIso_NRG_trailer_not_found", resourceCulture); + } + } + + internal static string MagicIso_NRG_trailer_has_no_tracks { + get { + return ResourceManager.GetString("MagicIso_NRG_trailer_has_no_tracks", resourceCulture); + } + } } } diff --git a/Aaru.Images/Localization/Localization.es.resx b/Aaru.Images/Localization/Localization.es.resx index bf361e78f..6de37284d 100644 --- a/Aaru.Images/Localization/Localization.es.resx +++ b/Aaru.Images/Localization/Localization.es.resx @@ -3036,4 +3036,28 @@ Firma: {0} + + Imagen de disco UIF de MagicISO + + + No se encontró la firma del pie UIF de MagicISO. + + + No se encontró la cabecera BLHR en la imagen UIF de MagicISO. + + + Las imágenes UIF de MagicISO protegidas con contraseña no están soportadas. + + + El tipo de imagen UIF de MagicISO {0} no está soportado. + + + El tipo de bloque UIF de MagicISO {0} no está soportado. + + + No se encontró el ancla NRG dentro de la imagen UIF de MagicISO. + + + El bloque NRG incrustado en la imagen UIF de MagicISO no contiene pistas. + \ No newline at end of file diff --git a/Aaru.Images/Localization/Localization.resx b/Aaru.Images/Localization/Localization.resx index a96075015..69b7f6491 100644 --- a/Aaru.Images/Localization/Localization.resx +++ b/Aaru.Images/Localization/Localization.resx @@ -3055,4 +3055,28 @@ Signature: {0} + + MagicISO UIF disc image + + + MagicISO UIF footer signature not found. + + + MagicISO UIF BLHR block list header not found. + + + Password-protected MagicISO UIF images are not supported. + + + MagicISO UIF image type {0} is not supported. + + + MagicISO UIF block type {0} is not supported. + + + NRG trailer anchor was not found inside the MagicISO UIF image. + + + The NRG trailer embedded in the MagicISO UIF image has no tracks. + \ No newline at end of file diff --git a/Aaru.Images/MagicIso/Constants.cs b/Aaru.Images/MagicIso/Constants.cs new file mode 100644 index 000000000..dd8749cfe --- /dev/null +++ b/Aaru.Images/MagicIso/Constants.cs @@ -0,0 +1,73 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Constants.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains constants for MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +namespace Aaru.Images; + +public sealed partial class MagicIso +{ + const string MODULE_NAME = "MagicISO plugin"; + const uint MAX_CACHE_SIZE = 16777216; + + // Descriptor signatures, stored little-endian in the UIF file. + // ASCII "bbis", "blhr", "bsdr", "blms", "blss" as 32-bit little-endian integers. + const uint BBIS_SIGNATURE = 0x73696262; + const uint BLHR_SIGNATURE = 0x72686C62; + const uint BSDR_SIGNATURE = 0x72647362; + const uint BLMS_SIGNATURE = 0x736D6C62; + const uint BLSS_SIGNATURE = 0x73736C62; + + // Image variants recorded in BBIS.imageType + const ushort IMAGE_TYPE_ISO = 8; + const ushort IMAGE_TYPE_MIXED = 9; + + // Block entry compression types + const uint BLOCK_TYPE_RAW = 1; + const uint BLOCK_TYPE_ZERO = 3; + const uint BLOCK_TYPE_ZLIB = 5; + + // NRG trailer chunk tags, stored big-endian. Equal to the ASCII four-byte id. + const uint NRG_ANCHOR_NERO = 0x4E45524F; // "NERO" + const uint NRG_ANCHOR_NER5 = 0x4E455235; // "NER5" + const uint NRG_CHUNK_DAOI = 0x44414F49; // "DAOI" + const uint NRG_CHUNK_DAOX = 0x44414F58; // "DAOX" + const uint NRG_CHUNK_ETNF = 0x45544E46; // "ETNF" + const uint NRG_CHUNK_ETN2 = 0x45544E32; // "ETN2" + const uint NRG_CHUNK_CDTX = 0x43445458; // "CDTX" + const uint NRG_CHUNK_SINF = 0x53494E46; // "SINF" + const uint NRG_CHUNK_END = 0x454E4421; // "END!" + + // Nero DAO mode byte values + const byte NRG_MODE_MODE1 = 0x00; + const byte NRG_MODE_MODE2_FORM1 = 0x02; + const byte NRG_MODE_MODE2_RAW = 0x03; + const byte NRG_MODE_AUDIO = 0x07; +} \ No newline at end of file diff --git a/Aaru.Images/MagicIso/Enums.cs b/Aaru.Images/MagicIso/Enums.cs new file mode 100644 index 000000000..b51182035 --- /dev/null +++ b/Aaru.Images/MagicIso/Enums.cs @@ -0,0 +1,49 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Enums.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains enumerations for MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +namespace Aaru.Images; + +public sealed partial class MagicIso +{ +#region Nested type: UifVariant + + /// Describes how the decompressed data fork should be interpreted. + enum UifVariant + { + /// Single cooked ISO data track, 2048 bytes per sector. + Iso = 0, + /// Mixed disc whose track layout is described by an embedded NRG trailer. + NrgTrailer = 1 + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/MagicIso/Identify.cs b/Aaru.Images/MagicIso/Identify.cs new file mode 100644 index 000000000..f49cc6764 --- /dev/null +++ b/Aaru.Images/MagicIso/Identify.cs @@ -0,0 +1,58 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Identify.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Identifies MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.IO; +using Aaru.CommonTypes.Interfaces; +using Aaru.Helpers; + +namespace Aaru.Images; + +public sealed partial class MagicIso +{ + /// Footer size as stored on disc (packed, 64 bytes). + const int BBIS_FOOTER_SIZE = 64; + + /// + public bool Identify(IFilter imageFilter) + { + Stream stream = imageFilter.GetDataForkStream(); + + if(stream.Length < BBIS_FOOTER_SIZE) return false; + + stream.Seek(-BBIS_FOOTER_SIZE, SeekOrigin.End); + var footer = new byte[BBIS_FOOTER_SIZE]; + stream.EnsureRead(footer, 0, BBIS_FOOTER_SIZE); + + return BitConverter.ToUInt32(footer, 0) == BBIS_SIGNATURE; + } +} \ No newline at end of file diff --git a/Aaru.Images/MagicIso/MagicIso.cs b/Aaru.Images/MagicIso/MagicIso.cs new file mode 100644 index 000000000..6f4587166 --- /dev/null +++ b/Aaru.Images/MagicIso/MagicIso.cs @@ -0,0 +1,78 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : MagicIso.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Manages MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +using System.Collections.Generic; +using System.IO; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decoders.CD; + +namespace Aaru.Images; + +public sealed partial class MagicIso : IOpticalMediaImage +{ + SortedList _blhrEntries; + uint[] _blhrStartSectors; + byte[] _cdText; + Dictionary _chunkCache; + uint _currentChunkCacheSize; + BbisFooter _footer; + IFilter _imageFilter; + ImageInfo _imageInfo; + Stream _imageStream; + List _magicIsoTracks; + SectorBuilder _sectorBuilder; + Dictionary _sectorCache; + byte[] _upc; + UifVariant _variant; + + public MagicIso() => _imageInfo = new ImageInfo + { + ReadableSectorTags = [], + ReadableMediaTags = [], + HasPartitions = true, + HasSessions = true, + Version = null, + ApplicationVersion = null, + MediaTitle = null, + Creator = null, + MediaManufacturer = null, + MediaModel = null, + MediaPartNumber = null, + MediaSequence = 0, + LastMediaSequence = 0, + DriveManufacturer = null, + DriveModel = null, + DriveSerialNumber = null, + DriveFirmwareRevision = null + }; +} \ No newline at end of file diff --git a/Aaru.Images/MagicIso/Properties.cs b/Aaru.Images/MagicIso/Properties.cs new file mode 100644 index 000000000..01e84fc0b --- /dev/null +++ b/Aaru.Images/MagicIso/Properties.cs @@ -0,0 +1,68 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Properties.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Exposes properties for MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Partition = Aaru.CommonTypes.Partition; +using Track = Aaru.CommonTypes.Structs.Track; + +namespace Aaru.Images; + +public sealed partial class MagicIso : IOpticalMediaImage +{ + /// + public string Author => Authors.NataliaPortillo; + /// + public Metadata AaruMetadata => null; + /// + public List DumpHardware => null; + /// + public string Format => "MagicISO UIF"; + /// + public Guid Id => new("EF5CBF5E-0F9F-4C57-9F6B-ADE4F4D4C0B1"); + /// + + // ReSharper disable once ConvertToAutoProperty + public ImageInfo Info => _imageInfo; + + /// + public string Name => Localization.MagicIso_disc_image; + /// + public List Partitions { get; set; } + /// + public List Tracks { get; set; } + /// + public List Sessions { get; set; } +} \ No newline at end of file diff --git a/Aaru.Images/MagicIso/Read.cs b/Aaru.Images/MagicIso/Read.cs new file mode 100644 index 000000000..acf03ab4e --- /dev/null +++ b/Aaru.Images/MagicIso/Read.cs @@ -0,0 +1,1294 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Read.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Reads MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Decoders.CD; +using Aaru.Helpers; +using Aaru.Logging; +using Partition = Aaru.CommonTypes.Partition; +using Session = Aaru.CommonTypes.Structs.Session; +using Track = Aaru.CommonTypes.Structs.Track; + +namespace Aaru.Images; + +public sealed partial class MagicIso +{ + /// Determines media type from the total sector count of an optical disc. + static MediaType CalculateMediaType(ulong totalSectors) => totalSectors switch + { + <= 360000 => MediaType.CD, + <= 2295104 => MediaType.DVDPR, + <= 2298496 => MediaType.DVDR, + <= 4171712 => MediaType.DVDRDL, + <= 4173824 => MediaType.DVDPRDL, + <= 24438784 => MediaType.BDR, + <= 62500864 => MediaType.BDRXL, + _ => MediaType.Unknown + }; + + /// Parses the 64-byte little-endian BBIS footer from a raw buffer. + static BbisFooter ParseFooter(byte[] buffer) => new() + { + signature = BitConverter.ToUInt32(buffer, 0), + footerSize = BitConverter.ToUInt32(buffer, 4), + version = BitConverter.ToUInt16(buffer, 8), + imageType = BitConverter.ToUInt16(buffer, 10), + unknown1 = BitConverter.ToUInt16(buffer, 12), + padding = BitConverter.ToUInt16(buffer, 14), + sectors = BitConverter.ToUInt32(buffer, 16), + sectorSize = BitConverter.ToUInt32(buffer, 20), + unknown2 = BitConverter.ToUInt32(buffer, 24), + blhrOffset = BitConverter.ToUInt64(buffer, 28), + blhrBbisSize = BitConverter.ToUInt32(buffer, 36), + hash = buffer[40..56], + unknown3 = BitConverter.ToUInt32(buffer, 56), + unknown4 = BitConverter.ToUInt32(buffer, 60) + }; + + /// Inflates a zlib-compressed payload using . + static byte[] InflateZlib(byte[] compressed, int expectedSize) + { + var output = new byte[expectedSize]; + + using var ms = new MemoryStream(compressed); + using var zlib = new ZLibStream(ms, CompressionMode.Decompress); + + var read = 0; + + while(read < expectedSize) + { + int n = zlib.Read(output, read, expectedSize - read); + + if(n == 0) break; + + read += n; + } + + if(read != expectedSize) Array.Resize(ref output, read); + + return output; + } + + /// Reads a BLHR-style descriptor header (16 bytes, little-endian) at the stream's current position. + static BlhrHeader ReadDescriptorHeader(Stream stream) + { + var buffer = new byte[16]; + stream.EnsureRead(buffer, 0, 16); + + return new BlhrHeader + { + signature = BitConverter.ToUInt32(buffer, 0), + size = BitConverter.ToUInt32(buffer, 4), + version = BitConverter.ToUInt32(buffer, 8), + num = BitConverter.ToUInt32(buffer, 12) + }; + } + + /// Parses the BLHR entry array after decompression. + static BlhrEntry[] ParseBlhrEntries(byte[] decompressed, uint count) + { + var entries = new BlhrEntry[count]; + + for(uint i = 0; i < count; i++) + { + var off = (int)(i * 24); + + entries[i] = new BlhrEntry + { + offset = BitConverter.ToUInt64(decompressed, off), + compressedSize = BitConverter.ToUInt32(decompressed, off + 8), + startSector = BitConverter.ToUInt32(decompressed, off + 12), + sectorCount = BitConverter.ToUInt32(decompressed, off + 16), + type = BitConverter.ToUInt32(decompressed, off + 20) + }; + } + + return entries; + } + + /// Locates the BLHR entry that covers a given output sector. Returns the index or -1 when not found. + int FindEntryIndex(uint outputSector) + { + var lo = 0; + int hi = _blhrStartSectors.Length - 1; + int result = -1; + + while(lo <= hi) + { + int mid = lo + hi >> 1; + + if(_blhrStartSectors[mid] <= outputSector) + { + result = mid; + lo = mid + 1; + } + else + hi = mid - 1; + } + + return result; + } + + /// Materialises a byte range from the virtual decompressed image. + ErrorNumber DecodeLinearRange(ulong byteOffset, int length, out byte[] buffer) + { + buffer = null; + + if(length <= 0) + { + buffer = []; + + return ErrorNumber.NoError; + } + + ulong totalBytes = (ulong)_footer.sectors * _footer.sectorSize; + + if(byteOffset >= totalBytes || byteOffset + (ulong)length > totalBytes) return ErrorNumber.OutOfRange; + + buffer = new byte[length]; + var filled = 0; + + ulong sectorSize = _footer.sectorSize; + + while(filled < length) + { + ulong currentOffset = byteOffset + (ulong)filled; + var currentSector = (uint)(currentOffset / sectorSize); + var sectorOffset = (uint)(currentOffset % sectorSize); + + int idx = FindEntryIndex(currentSector); + + if(idx < 0) return ErrorNumber.SectorNotFound; + + BlhrEntry entry = _blhrEntries.Values[idx]; + ulong entrySectorEnd = (ulong)entry.startSector + entry.sectorCount; + + if(currentSector >= entrySectorEnd) return ErrorNumber.SectorNotFound; + + var bytesInEntry = (int)(entrySectorEnd * sectorSize - currentOffset); + int toCopy = Math.Min(length - filled, bytesInEntry); + + ErrorNumber errno = GetChunkData(idx, entry, out byte[] chunkData); + + if(errno != ErrorNumber.NoError) return errno; + + uint sectorWithinChunk = currentSector - entry.startSector; + var chunkDataStartOffset = (int)(sectorWithinChunk * sectorSize + sectorOffset); + + Array.Copy(chunkData, chunkDataStartOffset, buffer, filled, toCopy); + filled += toCopy; + } + + return ErrorNumber.NoError; + } + + /// Returns (decompressing if needed) the raw bytes for a BLHR entry. + ErrorNumber GetChunkData(int entryIndex, BlhrEntry entry, out byte[] chunkData) + { + chunkData = null; + + var cacheKey = (uint)entryIndex; + + if(_chunkCache.TryGetValue(cacheKey, out chunkData)) return ErrorNumber.NoError; + + var decodedSize = (int)(entry.sectorCount * _footer.sectorSize); + + switch(entry.type) + { + case BLOCK_TYPE_ZERO: + chunkData = new byte[decodedSize]; + + break; + case BLOCK_TYPE_RAW: + { + chunkData = new byte[decodedSize]; + + if(entry.compressedSize > 0) + { + _imageStream.Seek((long)entry.offset, SeekOrigin.Begin); + var copyLen = (int)Math.Min(entry.compressedSize, (uint)decodedSize); + _imageStream.EnsureRead(chunkData, 0, copyLen); + } + + break; + } + case BLOCK_TYPE_ZLIB: + { + var compressed = new byte[entry.compressedSize]; + _imageStream.Seek((long)entry.offset, SeekOrigin.Begin); + _imageStream.EnsureRead(compressed, 0, (int)entry.compressedSize); + chunkData = InflateZlib(compressed, decodedSize); + + if(chunkData.Length < decodedSize) + { + var padded = new byte[decodedSize]; + Array.Copy(chunkData, 0, padded, 0, chunkData.Length); + chunkData = padded; + } + + break; + } + default: + AaruLogging.Error(Localization.MagicIso_unsupported_block_type_0, entry.type); + + return ErrorNumber.NotSupported; + } + + if(_currentChunkCacheSize + decodedSize > MAX_CACHE_SIZE) + { + _chunkCache.Clear(); + _currentChunkCacheSize = 0; + } + + _chunkCache[cacheKey] = chunkData; + _currentChunkCacheSize += (uint)decodedSize; + + return ErrorNumber.NoError; + } + + /// Builds the single-track layout used for ISO-type UIF images. + void BuildIsoLayout(IFilter imageFilter) + { + ulong totalSectors = _footer.sectors; + + _magicIsoTracks = + [ + new MagicIsoTrack + { + sequence = 1, + session = 1, + virtualByteOffset = 0, + sectorSize = 2048, + cookedBytesPerSector = 2048, + startSector = 0, + endSector = totalSectors - 1, + index0 = -150, + index1 = 0, + nrgMode = NRG_MODE_MODE1 + } + ]; + + Tracks = + [ + new Track + { + Sequence = 1, + Session = 1, + Type = TrackType.CdMode1, + StartSector = 0, + EndSector = totalSectors - 1, + BytesPerSector = 2048, + RawBytesPerSector = 2048, + File = imageFilter.Filename, + FileType = "BINARY", + Filter = imageFilter, + SubchannelType = TrackSubchannelType.None, + Description = string.Format(Localization.Track_0, 1), + Indexes = new Dictionary + { + [0] = -150, + [1] = 0 + } + } + ]; + + Sessions = + [ + new Session + { + Sequence = 1, + StartTrack = 1, + EndTrack = 1, + StartSector = 0, + EndSector = totalSectors - 1 + } + ]; + + Partitions = + [ + new Partition + { + Sequence = 0, + Start = 0, + Length = totalSectors, + Offset = 0, + Size = totalSectors * 2048, + Description = string.Format(Localization.Track_0, 1), + Type = "MODE1/2048" + } + ]; + + _imageInfo.MediaType = CalculateMediaType(totalSectors); + } + + /// + /// Parses the BLMS track descriptor and produces a multi-track BIN-style layout. Returns + /// when the descriptor is absent or malformed so the caller can fall back. + /// + ErrorNumber BuildBlmsLayout(byte[] blms, IFilter imageFilter) + { + if(blms == null || blms.Length < 0x40) return ErrorNumber.NoData; + + int blmsLen = blms.Length; + var tracks = new List(); + + for(var p = 0x40; p + 68 <= blmsLen; p += 68) + { + // High "Q sub-channel" control flags (0xA0/0xA1/0xA2) signal lead-in/lead-out entries, not tracks. + if((blms[p + 3] & 0xA0) != 0) continue; + + byte trackNumber = blms[p + 3]; + byte adr = blms[p + 1]; + byte mm = blms[p + 8]; + byte ssRaw = blms[p + 9]; + byte ff = blms[p + 10]; + byte mode = blms[p + 11]; + var sectorSize = BitConverter.ToUInt32(blms, p + 24); + + if(sectorSize is not (2048 or 2336 or 2352)) sectorSize = 2352; + + // Stored MSF includes the 2-second lead-in used by NRG/BIN formats. + int absoluteMsf = mm * 60 * 75 + ssRaw * 75 + ff; + int startLba = absoluteMsf - 150; + if(startLba < 0) startLba = 0; + + bool isAudio = adr is 0x10 or 0x12; + + uint nrgMode; + var rawMode1 = false; + var rawMode2 = false; + uint cookedBps; + + if(isAudio) + { + nrgMode = NRG_MODE_AUDIO; + cookedBps = 2352; + sectorSize = 2352; + } + else + { + switch(mode) + { + case 1: + nrgMode = NRG_MODE_MODE1; + cookedBps = 2048; + rawMode1 = sectorSize == 2352; + + break; + case 2: + nrgMode = NRG_MODE_MODE2_FORM1; + cookedBps = 2048; + rawMode2 = sectorSize == 2352; + + break; + default: + nrgMode = NRG_MODE_MODE1; + cookedBps = sectorSize == 2352 ? 2048u : sectorSize; + rawMode1 = sectorSize == 2352; + + break; + } + } + + var track = new MagicIsoTrack + { + sequence = trackNumber, + session = 1, + virtualByteOffset = (ulong)startLba * sectorSize, + sectorSize = sectorSize, + cookedBytesPerSector = cookedBps, + startSector = (ulong)startLba, + index0 = startLba == 0 ? -150 : startLba, + index1 = startLba == 0 ? 0 : startLba + 150, + nrgMode = nrgMode, + rawMode1 = rawMode1, + rawMode2 = rawMode2 + }; + + tracks.Add(track); + } + + if(tracks.Count == 0) return ErrorNumber.NoData; + + tracks.Sort((a, b) => a.sequence.CompareTo(b.sequence)); + + // Compute end sectors from the byte layout of the decompressed stream. + for(var i = 0; i < tracks.Count; i++) + { + MagicIsoTrack t = tracks[i]; + + ulong endByte = i + 1 < tracks.Count + ? tracks[i + 1].virtualByteOffset + : (ulong)_footer.sectors * _footer.sectorSize; + + ulong lengthSectors = (endByte - t.virtualByteOffset) / t.sectorSize; + t.endSector = t.startSector + lengthSectors - 1; + tracks[i] = t; + } + + return FinalizeOpticalLayout(tracks, imageFilter); + } + + /// Parses the NRG trailer embedded in the decompressed stream and produces a multi-track layout. + ErrorNumber BuildNrgLayout(IFilter imageFilter) + { + ulong totalBytes = (ulong)_footer.sectors * _footer.sectorSize; + uint sectorSize = _footer.sectorSize; + + // Read the last sector to locate the NER5/NERO anchor. + ErrorNumber errno = DecodeLinearRange(totalBytes - sectorSize, (int)sectorSize, out byte[] lastSector); + + if(errno != ErrorNumber.NoError) return errno; + + ulong nrgChunkOffset = 0; + var foundAnchor = false; + var anchorIsV2 = false; + + for(int i = lastSector.Length - 12; i >= 0; i--) + { + uint tag = BinaryPrimitives.ReadUInt32BigEndian(lastSector.AsSpan(i, 4)); + + if(tag == NRG_ANCHOR_NER5) + { + nrgChunkOffset = BinaryPrimitives.ReadUInt64BigEndian(lastSector.AsSpan(i + 4, 8)); + foundAnchor = true; + anchorIsV2 = true; + + break; + } + + if(tag != NRG_ANCHOR_NERO) continue; + + nrgChunkOffset = BinaryPrimitives.ReadUInt32BigEndian(lastSector.AsSpan(i + 4, 4)); + foundAnchor = true; + + break; + } + + if(!foundAnchor || nrgChunkOffset >= totalBytes) + { + AaruLogging.Error(Localization.MagicIso_NRG_trailer_not_found); + + return ErrorNumber.InvalidArgument; + } + + AaruLogging.Debug(MODULE_NAME, "NRG anchor offset = {0} (v{1})", nrgChunkOffset, anchorIsV2 ? 2 : 1); + + // Read the trailer payload into memory (bounded from the anchor offset up to the end of the stream). + var trailerLength = (int)(totalBytes - nrgChunkOffset); + + if(trailerLength < 8) return ErrorNumber.InvalidArgument; + + errno = DecodeLinearRange(nrgChunkOffset, trailerLength, out byte[] trailer); + + if(errno != ErrorNumber.NoError) return errno; + + var tracks = new List(); + ulong totalLba = 0; + var pos = 0; + uint currentTrack = 1; + + while(pos + 8 <= trailer.Length) + { + uint chunkId = BinaryPrimitives.ReadUInt32BigEndian(trailer.AsSpan(pos, 4)); + uint chunkSize = BinaryPrimitives.ReadUInt32BigEndian(trailer.AsSpan(pos + 4, 4)); + pos += 8; + + if(chunkId == NRG_CHUNK_END || chunkId == NRG_ANCHOR_NER5 || chunkId == NRG_ANCHOR_NERO) break; + + if(pos + chunkSize > trailer.Length) break; + + ReadOnlySpan payload = trailer.AsSpan(pos, (int)chunkSize); + pos += (int)chunkSize; + + switch(chunkId) + { + case NRG_CHUNK_DAOX: + case NRG_CHUNK_DAOI: + { + if(chunkSize < 22) break; + + int indexWidth = chunkId == NRG_CHUNK_DAOX ? 8 : 4; + int recordSize = 10 + 4 + 4 + indexWidth * 3; + var cursor = 22; + + while(cursor + recordSize <= chunkSize) + { + int recStart = cursor; + + // ISRC + other metadata occupies the first 10 bytes of each record. + cursor += 10; + + ushort recSectorSize = BinaryPrimitives.ReadUInt16BigEndian(payload.Slice(cursor + 2, 2)); + cursor += 4; + byte modeByte = payload[cursor]; + cursor += 4; + ulong index0 = ReadNrgIndex(payload, cursor, indexWidth); + cursor += indexWidth; + ulong index1 = ReadNrgIndex(payload, cursor, indexWidth); + cursor += indexWidth; + ulong index2 = ReadNrgIndex(payload, cursor, indexWidth); + cursor += indexWidth; + + if(recSectorSize == 0) recSectorSize = (ushort)sectorSize; + + // MagicISO mis-declares raw data tracks as mode 0/2/3 with sector size 2352. + var rawMode1 = false; + var rawMode2 = false; + uint nrgMode = modeByte; + + if(recSectorSize == 2352) + { + switch(modeByte) + { + case NRG_MODE_MODE1: + rawMode1 = true; + + break; + case NRG_MODE_MODE2_FORM1: + case NRG_MODE_MODE2_RAW: + rawMode2 = true; + nrgMode = NRG_MODE_MODE2_FORM1; + + break; + } + } + + uint cookedBps = nrgMode == NRG_MODE_AUDIO + ? 2352u + : recSectorSize == 2352 + ? 2048u + : recSectorSize == 2336 + ? 2336u + : 2048u; + + ulong trackBytes = index2 - index1; + ulong sectorCount = recSectorSize == 0 ? 0 : trackBytes / recSectorSize; + + var track = new MagicIsoTrack + { + sequence = currentTrack, + session = 1, + virtualByteOffset = index1, + sectorSize = recSectorSize, + cookedBytesPerSector = cookedBps, + startSector = totalLba, + endSector = totalLba + sectorCount - 1, + index0 = (int)totalLba - + (int)((index1 - index0) / (recSectorSize == 0 ? 1u : recSectorSize)), + index1 = (int)totalLba, + nrgMode = nrgMode, + rawMode1 = rawMode1, + rawMode2 = rawMode2 + }; + + tracks.Add(track); + totalLba += sectorCount; + currentTrack++; + + // Discard any remainder inside the record (shouldn't happen but keeps the parser robust). + int recEnd = recStart + recordSize; + if(cursor < recEnd) cursor = recEnd; + } + + break; + } + case NRG_CHUNK_ETNF: + case NRG_CHUNK_ETN2: + { + int indexWidth = chunkId == NRG_CHUNK_ETN2 ? 8 : 4; + int recordSize = indexWidth * 2 + 4 + 4 + 4; + var cursor = 0; + + while(cursor + recordSize <= chunkSize) + { + ulong start = ReadNrgIndex(payload, cursor, indexWidth); + cursor += indexWidth; + ulong end = ReadNrgIndex(payload, cursor, indexWidth); + cursor += indexWidth; + byte modeByte = payload[cursor]; + cursor += 4; + cursor += 4 + 4; + + const ushort recSectorSize = 2352; + ulong trackBytes = end - start; + ulong sectorCount = trackBytes / recSectorSize; + + uint nrgMode = modeByte; + bool rawMode1 = modeByte == NRG_MODE_MODE1; + bool rawMode2 = modeByte is NRG_MODE_MODE2_FORM1 or NRG_MODE_MODE2_RAW; + + if(rawMode2) nrgMode = NRG_MODE_MODE2_FORM1; + + uint cookedBps = modeByte == NRG_MODE_AUDIO ? 2352u : 2048u; + + tracks.Add(new MagicIsoTrack + { + sequence = currentTrack, + session = 1, + virtualByteOffset = start, + sectorSize = recSectorSize, + cookedBytesPerSector = cookedBps, + startSector = totalLba, + endSector = totalLba + sectorCount - 1, + index0 = (int)totalLba, + index1 = (int)totalLba, + nrgMode = nrgMode, + rawMode1 = rawMode1, + rawMode2 = rawMode2 + }); + + totalLba += sectorCount; + currentTrack++; + } + + break; + } + case NRG_CHUNK_CDTX: + _cdText = payload.ToArray(); + + if(!_imageInfo.ReadableMediaTags.Contains(MediaTagType.CD_TEXT)) + _imageInfo.ReadableMediaTags.Add(MediaTagType.CD_TEXT); + + break; + } + } + + if(tracks.Count == 0) + { + AaruLogging.Error(Localization.MagicIso_NRG_trailer_has_no_tracks); + + return ErrorNumber.InvalidArgument; + } + + return FinalizeOpticalLayout(tracks, imageFilter); + } + + /// Reads an index value (32-bit or 64-bit big-endian) from an NRG DAO/ETN record. + static ulong ReadNrgIndex(ReadOnlySpan payload, int offset, int width) => width == 8 + ? BinaryPrimitives.ReadUInt64BigEndian(payload.Slice(offset, 8)) + : BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(offset, 4)); + + /// Finalises , and from parsed tracks. + ErrorNumber FinalizeOpticalLayout(List tracks, IFilter imageFilter) + { + _magicIsoTracks = tracks; + Tracks = []; + Sessions = []; + Partitions = []; + + ulong discSectors = 0; + + foreach(MagicIsoTrack t in tracks) + { + var track = new Track + { + Sequence = t.sequence, + Session = t.session, + StartSector = t.startSector, + EndSector = t.endSector, + BytesPerSector = (int)t.cookedBytesPerSector, + RawBytesPerSector = (int)t.sectorSize, + File = imageFilter.Filename, + FileType = "BINARY", + Filter = imageFilter, + SubchannelType = TrackSubchannelType.None, + Description = string.Format(Localization.Track_0, t.sequence), + Type = NrgModeToTrackType(t.nrgMode), + Indexes = new Dictionary + { + [1] = t.index1 + } + }; + + if(t.index0 < t.index1) track.Indexes[0] = t.index0; + + Tracks.Add(track); + + Partitions.Add(new Partition + { + Sequence = t.sequence - 1, + Start = t.startSector, + Length = t.endSector - t.startSector + 1, + Offset = t.virtualByteOffset, + Size = (t.endSector - t.startSector + 1) * t.sectorSize, + Description = string.Format(Localization.Track_0, t.sequence), + Type = track.Type.ToString() + }); + + if(t.endSector + 1 > discSectors) discSectors = t.endSector + 1; + + if(t.rawMode1 || t.rawMode2) + { + if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSync)) + _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSync); + + if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorHeader)) + _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorHeader); + + if(!_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorEdc)) + _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorEdc); + + if(t.rawMode2 && !_imageInfo.ReadableSectorTags.Contains(SectorTagType.CdSectorSubHeader)) + _imageInfo.ReadableSectorTags.Add(SectorTagType.CdSectorSubHeader); + } + } + + Sessions.Add(new Session + { + Sequence = 1, + StartTrack = tracks[0].sequence, + EndTrack = tracks[^1].sequence, + StartSector = tracks[0].startSector, + EndSector = tracks[^1].endSector + }); + + _imageInfo.MediaType = CalculateMediaType(discSectors); + + return ErrorNumber.NoError; + } + + static TrackType NrgModeToTrackType(uint nrgMode) => nrgMode switch + { + NRG_MODE_AUDIO => TrackType.Audio, + NRG_MODE_MODE2_FORM1 => TrackType.CdMode2Form1, + NRG_MODE_MODE2_RAW => TrackType.CdMode2Formless, + _ => TrackType.CdMode1 + }; + + static ErrorNumber CopyTagSlice(byte[] raw, uint length, int offset, int size, out byte[] buffer) + { + buffer = new byte[length * size]; + + for(uint i = 0; i < length; i++) Array.Copy(raw, i * 2352 + offset, buffer, i * size, size); + + return ErrorNumber.NoError; + } + + MagicIsoTrack? FindTrackBySequence(uint sequence) + { + foreach(MagicIsoTrack t in _magicIsoTracks) + if(t.sequence == sequence) + return t; + + return null; + } + + MagicIsoTrack? FindTrackByAbsoluteLba(ulong lba) + { + foreach(MagicIsoTrack t in _magicIsoTracks) + if(lba >= t.startSector && lba <= t.endSector) + return t; + + return null; + } + + static ErrorNumber SetStatusAndReturn(out SectorStatus status, SectorStatus[] arr) + { + status = arr is { Length: > 0 } ? arr[0] : SectorStatus.NotDumped; + + return ErrorNumber.NoError; + } + + static ErrorNumber StatusFail(out SectorStatus status) + { + status = SectorStatus.NotDumped; + + return ErrorNumber.InvalidArgument; + } + +#region IOpticalMediaImage Members + + /// + public ErrorNumber Open(IFilter imageFilter) + { + Stream stream = imageFilter.GetDataForkStream(); + + if(stream.Length < BBIS_FOOTER_SIZE) return ErrorNumber.InvalidArgument; + + stream.Seek(-BBIS_FOOTER_SIZE, SeekOrigin.End); + var footerBytes = new byte[BBIS_FOOTER_SIZE]; + stream.EnsureRead(footerBytes, 0, BBIS_FOOTER_SIZE); + _footer = ParseFooter(footerBytes); + + if(_footer.signature != BBIS_SIGNATURE) + { + AaruLogging.Error(Localization.MagicIso_bbis_signature_not_found); + + return ErrorNumber.InvalidArgument; + } + + AaruLogging.Debug(MODULE_NAME, "footer.version = {0}", _footer.version); + AaruLogging.Debug(MODULE_NAME, "footer.imageType = {0}", _footer.imageType); + AaruLogging.Debug(MODULE_NAME, "footer.sectors = {0}", _footer.sectors); + AaruLogging.Debug(MODULE_NAME, "footer.sectorSize = {0}", _footer.sectorSize); + AaruLogging.Debug(MODULE_NAME, "footer.blhrOffset = {0}", _footer.blhrOffset); + + if(_footer.sectors == 0 || _footer.sectorSize == 0) return ErrorNumber.InvalidArgument; + + // Seek to the BLHR descriptor and read its header. + stream.Seek((long)_footer.blhrOffset, SeekOrigin.Begin); + BlhrHeader blhrHeader = ReadDescriptorHeader(stream); + + if(blhrHeader.signature == BSDR_SIGNATURE) + { + AaruLogging.Error(Localization.MagicIso_password_protected_images_are_not_supported); + + return ErrorNumber.NotSupported; + } + + if(blhrHeader.signature != BLHR_SIGNATURE) + { + AaruLogging.Error(Localization.MagicIso_blhr_signature_not_found); + + return ErrorNumber.InvalidArgument; + } + + var compressedBlhrSize = (int)(blhrHeader.size - 8); + var uncompressedSize = (int)(blhrHeader.num * 24u); + + if(compressedBlhrSize <= 0 || uncompressedSize <= 0) return ErrorNumber.InvalidArgument; + + var compressedBlhr = new byte[compressedBlhrSize]; + stream.EnsureRead(compressedBlhr, 0, compressedBlhrSize); + + byte[] blhrTable = InflateZlib(compressedBlhr, uncompressedSize); + BlhrEntry[] entries = ParseBlhrEntries(blhrTable, blhrHeader.num); + + _blhrEntries = new SortedList(entries.Length); + _blhrStartSectors = new uint[entries.Length]; + + for(var i = 0; i < entries.Length; i++) + { + _blhrEntries[entries[i].startSector] = entries[i]; + _blhrStartSectors[i] = entries[i].startSector; + } + + Array.Sort(_blhrStartSectors); + + _chunkCache = new Dictionary(); + _currentChunkCacheSize = 0; + _sectorCache = new Dictionary(); + _sectorBuilder = new SectorBuilder(); + _imageStream = stream; + _imageFilter = imageFilter; + + // Common image info + _imageInfo.CreationTime = imageFilter.CreationTime; + _imageInfo.LastModificationTime = imageFilter.LastWriteTime; + _imageInfo.MediaTitle = Path.GetFileNameWithoutExtension(imageFilter.Filename); + _imageInfo.Application = "MagicISO"; + _imageInfo.Version = $"{_footer.version}"; + _imageInfo.ImageSize = (ulong)_footer.sectors * _footer.sectorSize; + _imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc; + _imageInfo.SectorSize = _footer.sectorSize; + + // Attempt to read optional BLMS/BLSS descriptors after the BLHR payload. + byte[] blmsData = null; + var blssIsNrg = false; + + if(_footer.imageType == IMAGE_TYPE_MIXED && stream.Position + 16 <= stream.Length) + { + BlhrHeader blms = ReadDescriptorHeader(stream); + + if(blms.signature == BLMS_SIGNATURE) + { + var compBlms = new byte[blms.size - 8]; + stream.EnsureRead(compBlms, 0, compBlms.Length); + + try + { + blmsData = InflateZlib(compBlms, (int)blms.num); + } + catch(Exception ex) + { + AaruLogging.Debug(MODULE_NAME, "BLMS inflate failed: {0}", ex.Message); + } + } + + if(stream.Position + 16 <= stream.Length) + { + BlhrHeader blss = ReadDescriptorHeader(stream); + + if(blss.signature == BLSS_SIGNATURE) + { + // BLSS descriptor header is followed by 4 extra bytes before its payload. + stream.Seek(4, SeekOrigin.Current); + + if(blss.num == 0) blssIsNrg = true; + } + } + } + + _variant = _footer.imageType switch + { + IMAGE_TYPE_ISO => UifVariant.Iso, + IMAGE_TYPE_MIXED when blssIsNrg => UifVariant.NrgTrailer, + IMAGE_TYPE_MIXED => UifVariant.NrgTrailer, + _ => UifVariant.Iso + }; + + if(_footer.imageType == IMAGE_TYPE_ISO) + BuildIsoLayout(imageFilter); + else if(_footer.imageType == IMAGE_TYPE_MIXED) + { + ErrorNumber layoutError = ErrorNumber.NoData; + + if(!blssIsNrg && blmsData != null) layoutError = BuildBlmsLayout(blmsData, imageFilter); + + if(layoutError != ErrorNumber.NoError) layoutError = BuildNrgLayout(imageFilter); + + if(layoutError != ErrorNumber.NoError) return layoutError; + } + else + { + AaruLogging.Error(Localization.MagicIso_unsupported_image_type_0, _footer.imageType); + + return ErrorNumber.NotSupported; + } + + _imageInfo.Sectors = Tracks[^1].EndSector + 1; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSector(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus) => + ReadSectors(sectorAddress, 1, track, out buffer, out SectorStatus[] s) == ErrorNumber.NoError + ? SetStatusAndReturn(out sectorStatus, s) + : StatusFail(out sectorStatus); + + /// + public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus) + { + buffer = null; + sectorStatus = SectorStatus.NotDumped; + + if(negative) return ErrorNumber.NotSupported; + + MagicIsoTrack? found = FindTrackByAbsoluteLba(sectorAddress); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + return ReadSector(sectorAddress - t.startSector, t.sequence, out buffer, out sectorStatus); + } + + /// + public ErrorNumber ReadSectors(ulong sectorAddress, uint length, uint track, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + MagicIsoTrack? found = FindTrackBySequence(track); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + if(sectorAddress + length > t.endSector - t.startSector + 1) return ErrorNumber.OutOfRange; + + // Read raw sectors from the virtual stream, then strip to the cooked user-data size when required. + ulong byteOffset = t.virtualByteOffset + sectorAddress * t.sectorSize; + var rawLength = (int)(length * t.sectorSize); + ErrorNumber errno = DecodeLinearRange(byteOffset, rawLength, out byte[] raw); + + if(errno != ErrorNumber.NoError) return errno; + + if(t.sectorSize == t.cookedBytesPerSector) + buffer = raw; + else + { + // Raw 2352 → cooked user data + buffer = new byte[length * t.cookedBytesPerSector]; + + for(uint i = 0; i < length; i++) + { + var srcOff = (int)(i * t.sectorSize); + + if(t.rawMode1 && t.sectorSize == 2352) + Array.Copy(raw, + srcOff + 16, + buffer, + (int)(i * t.cookedBytesPerSector), + (int)t.cookedBytesPerSector); + else if(t.rawMode2 && t.sectorSize == 2352) + Array.Copy(raw, + srcOff + 24, + buffer, + (int)(i * t.cookedBytesPerSector), + (int)t.cookedBytesPerSector); + else + Array.Copy(raw, srcOff, buffer, (int)(i * t.cookedBytesPerSector), (int)t.cookedBytesPerSector); + } + } + + sectorStatus = Enumerable.Repeat(SectorStatus.Dumped, (int)length).ToArray(); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSectors(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + if(negative) return ErrorNumber.NotSupported; + + MagicIsoTrack? found = FindTrackByAbsoluteLba(sectorAddress); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + return ReadSectors(sectorAddress - t.startSector, length, t.sequence, out buffer, out sectorStatus); + } + + /// + public ErrorNumber ReadSectorLong(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus) + { + ErrorNumber errno = ReadSectorsLong(sectorAddress, 1, track, out buffer, out SectorStatus[] statuses); + sectorStatus = errno == ErrorNumber.NoError ? statuses[0] : SectorStatus.NotDumped; + + return errno; + } + + /// + public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer, + out SectorStatus sectorStatus) + { + buffer = null; + sectorStatus = SectorStatus.NotDumped; + + if(negative) return ErrorNumber.NotSupported; + + MagicIsoTrack? found = FindTrackByAbsoluteLba(sectorAddress); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + return ReadSectorLong(sectorAddress - t.startSector, t.sequence, out buffer, out sectorStatus); + } + + /// + public ErrorNumber ReadSectorsLong(ulong sectorAddress, uint length, uint track, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + MagicIsoTrack? found = FindTrackBySequence(track); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + if(sectorAddress + length > t.endSector - t.startSector + 1) return ErrorNumber.OutOfRange; + + // If underlying data is already raw 2352, return it verbatim. + if(t.sectorSize == 2352) + { + ulong byteOffset = t.virtualByteOffset + sectorAddress * 2352; + ErrorNumber errno = DecodeLinearRange(byteOffset, (int)(length * 2352), out buffer); + + if(errno != ErrorNumber.NoError) return errno; + + sectorStatus = Enumerable.Repeat(SectorStatus.Dumped, (int)length).ToArray(); + + return ErrorNumber.NoError; + } + + // Otherwise reconstruct 2352-byte sectors from 2048-byte user data. + if(t.cookedBytesPerSector != 2048 || t.nrgMode != NRG_MODE_MODE1) return ErrorNumber.NotSupported; + + ErrorNumber userErrno = + ReadSectors(sectorAddress, length, track, out byte[] userData, out SectorStatus[] userStatus); + + if(userErrno != ErrorNumber.NoError) return userErrno; + + buffer = new byte[length * 2352]; + sectorStatus = userStatus; + + for(uint i = 0; i < length; i++) + { + var raw = new byte[2352]; + Array.Copy(userData, i * 2048, raw, 16, 2048); + _sectorBuilder.ReconstructPrefix(ref raw, TrackType.CdMode1, (long)(t.startSector + sectorAddress + i)); + _sectorBuilder.ReconstructEcc(ref raw, TrackType.CdMode1); + Array.Copy(raw, 0, buffer, i * 2352, 2352); + } + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSectorsLong(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + if(negative) return ErrorNumber.NotSupported; + + MagicIsoTrack? found = FindTrackByAbsoluteLba(sectorAddress); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + return ReadSectorsLong(sectorAddress - t.startSector, length, t.sequence, out buffer, out sectorStatus); + } + + /// + public ErrorNumber ReadSectorTag(ulong sectorAddress, uint track, SectorTagType tag, out byte[] buffer) => + ReadSectorsTag(sectorAddress, 1, track, tag, out buffer); + + /// + public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer) + { + buffer = null; + + if(negative) return ErrorNumber.NotSupported; + + MagicIsoTrack? found = FindTrackByAbsoluteLba(sectorAddress); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + return ReadSectorTag(sectorAddress - t.startSector, t.sequence, tag, out buffer); + } + + /// + public ErrorNumber ReadSectorsTag(ulong sectorAddress, uint length, uint track, SectorTagType tag, + out byte[] buffer) + { + buffer = null; + + if(tag == SectorTagType.CdTrackFlags) + { + MagicIsoTrack? trackFlagsTrack = FindTrackBySequence(track); + + if(trackFlagsTrack is null) return ErrorNumber.SectorNotFound; + + buffer = [trackFlagsTrack.Value.nrgMode == NRG_MODE_AUDIO ? (byte)0 : (byte)4]; + + return ErrorNumber.NoError; + } + + ErrorNumber errno = ReadSectorsLong(sectorAddress, length, track, out byte[] raw, out _); + + if(errno != ErrorNumber.NoError) return errno; + + switch(tag) + { + case SectorTagType.CdSectorSync: + return CopyTagSlice(raw, length, 0, 12, out buffer); + case SectorTagType.CdSectorHeader: + return CopyTagSlice(raw, length, 12, 4, out buffer); + case SectorTagType.CdSectorSubHeader: + return CopyTagSlice(raw, length, 16, 8, out buffer); + case SectorTagType.CdSectorEdc: + return CopyTagSlice(raw, length, 2064, 4, out buffer); + case SectorTagType.CdSectorEccP: + return CopyTagSlice(raw, length, 2076, 172, out buffer); + case SectorTagType.CdSectorEccQ: + return CopyTagSlice(raw, length, 2248, 104, out buffer); + case SectorTagType.CdSectorEcc: + return CopyTagSlice(raw, length, 2076, 276, out buffer); + default: + return ErrorNumber.NotSupported; + } + } + + /// + public ErrorNumber ReadSectorsTag(ulong sectorAddress, bool negative, uint length, SectorTagType tag, + out byte[] buffer) + { + buffer = null; + + if(negative) return ErrorNumber.NotSupported; + + MagicIsoTrack? found = FindTrackByAbsoluteLba(sectorAddress); + + if(found is null) return ErrorNumber.SectorNotFound; + + MagicIsoTrack t = found.Value; + + return ReadSectorsTag(sectorAddress - t.startSector, length, t.sequence, tag, out buffer); + } + + /// + public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer) + { + buffer = null; + + switch(tag) + { + case MediaTagType.CD_TEXT: + if(_cdText == null) return ErrorNumber.NoData; + + buffer = (byte[])_cdText.Clone(); + + return ErrorNumber.NoError; + case MediaTagType.CD_MCN: + if(_upc == null) return ErrorNumber.NoData; + + buffer = (byte[])_upc.Clone(); + + return ErrorNumber.NoError; + default: + return ErrorNumber.NotSupported; + } + } + + /// + public List GetSessionTracks(Session session) => session.Sequence == 1 ? Tracks : null; + + /// + public List GetSessionTracks(ushort session) => session == 1 ? Tracks : null; + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/MagicIso/Structs.cs b/Aaru.Images/MagicIso/Structs.cs new file mode 100644 index 000000000..0a608d01e --- /dev/null +++ b/Aaru.Images/MagicIso/Structs.cs @@ -0,0 +1,129 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Structs.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains structures for MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +namespace Aaru.Images; + +public sealed partial class MagicIso +{ +#region Nested type: BbisFooter + + /// + /// UIF footer located at the end of the file. 64 bytes, little-endian. The blhr field is 64-bit; with the + /// original packing (two-byte) it sits at offset 28 within the structure. + /// + struct BbisFooter + { + /// "bbis" signature. + public uint signature; + /// Declared footer size in bytes. + public uint footerSize; + /// Version number. + public ushort version; + /// Image variant: 8 for ISO, 9 for mixed/NRG-based layouts. + public ushort imageType; + public ushort unknown1; + public ushort padding; + /// Total number of virtual sectors in the decompressed image. + public uint sectors; + /// Sector size used by the BLHR entries. 2048 for ISO, 2352 for mixed. + public uint sectorSize; + public uint unknown2; + /// Absolute file offset of the BLHR descriptor. + public ulong blhrOffset; + /// Combined size of the BLHR and BBIS headers in bytes. + public uint blhrBbisSize; + /// Hash used for password validation (unused when not encrypted). + public byte[] hash; + public uint unknown3; + public uint unknown4; + } + +#endregion + +#region Nested type: BlhrHeader + + /// BLHR / BLMS / BLSS descriptor header. 16 bytes, little-endian. + struct BlhrHeader + { + public uint signature; + /// Size of the payload that follows, including the version and num fields (minus the 8-byte header). + public uint size; + public uint version; + /// Number of BLHR entries (BLHR) or payload bytes (BLMS/BLSS). + public uint num; + } + +#endregion + +#region Nested type: BlhrEntry + + /// + /// Single BLHR entry. 24 bytes, little-endian. Describes a contiguous run of output sectors and its compressed + /// representation inside the UIF file. + /// + struct BlhrEntry + { + /// Absolute offset within the UIF file where the compressed payload starts. + public ulong offset; + /// Compressed size in bytes. + public uint compressedSize; + /// First output sector covered by this entry, expressed in BbisFooter.sectorSize units. + public uint startSector; + /// Number of output sectors covered by this entry. + public uint sectorCount; + /// Block compression type (1 raw, 3 zero, 5 zlib). + public uint type; + } + +#endregion + +#region Nested type: MagicIsoTrack + + /// In-memory track record mapping a disc track onto the decompressed linear stream. + struct MagicIsoTrack + { + public uint sequence; + public ushort session; + public ulong virtualByteOffset; + public uint sectorSize; + public uint cookedBytesPerSector; + public ulong startSector; + public ulong endSector; + public int index0; + public int index1; + public uint nrgMode; + public bool rawMode1; + public bool rawMode2; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/MagicIso/Verify.cs b/Aaru.Images/MagicIso/Verify.cs new file mode 100644 index 000000000..306403e96 --- /dev/null +++ b/Aaru.Images/MagicIso/Verify.cs @@ -0,0 +1,65 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Verify.cs +// Author(s) : Natalia Portillo +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Verifies MagicISO UIF disc images. +// +// --[ 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-2026 Natalia Portillo +// ****************************************************************************/ + +using System.Collections.Generic; + +namespace Aaru.Images; + +public sealed partial class MagicIso +{ + /// + public bool? VerifySector(ulong sectorAddress) => null; + + /// + public bool? VerifySectors(ulong sectorAddress, uint length, out List failingLbas, + out List unknownLbas) + { + failingLbas = []; + unknownLbas = new List((int)length); + + for(ulong i = 0; i < length; i++) unknownLbas.Add(sectorAddress + i); + + return null; + } + + /// + public bool? VerifySectors(ulong sectorAddress, uint length, uint track, out List failingLbas, + out List unknownLbas) + { + failingLbas = []; + unknownLbas = new List((int)length); + + for(ulong i = 0; i < length; i++) unknownLbas.Add(sectorAddress + i); + + return null; + } +} \ No newline at end of file diff --git a/README.md b/README.md index 4021d0efd..3c609ec4b 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Media image formats we can read * Expert Witness Format (EWF) * HD-Copy disk images * HxCStream +* MagicISO (.UIF) * MAME Compressed Hunks of Data (CHD) * Microsoft VHDX * Nero Burning ROM (both image formats)