// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : CompactDisc.cs // Author(s) : Natalia Portillo // // Component : Core. // // --[ Description ] ---------------------------------------------------------- // // Retrieves information from CompactDisc media. // // --[ License ] -------------------------------------------------------------- // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // ---------------------------------------------------------------------------- // Copyright © 2011-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Structs; using Aaru.Core.Logging; using Aaru.Core.Media.Detection; using Aaru.Database.Models; using Aaru.Decoders.CD; using Aaru.Devices; using Device = Aaru.Database.Models.Device; namespace Aaru.Core.Media.Info; /// Core operations for retrieving information about CD based media public static class CompactDisc { /// Gets the offset bytes from a Compact Disc /// Offset entry from database /// Device entry from database /// Debug /// Opened device /// Detected disk type /// Dump log if applicable /// Disc track list /// UpdateStatus event /// Drive offset /// Combined offset /// Set to true if drive supports PLEXTOR READ CD-DA vendor command /// true if offset could be found, false otherwise [SuppressMessage("ReSharper", "TooWideLocalVariableScope")] public static void GetOffset(CdOffset cdOffset, Device dbDev, bool debug, Aaru.Devices.Device dev, MediaType dskType, DumpLog dumpLog, Track[] tracks, UpdateStatusHandler updateStatus, out int? driveOffset, out int? combinedOffset, out bool supportsPlextorReadCdDa) { byte[] cmdBuf; bool sense; int minute; int second; int frame; byte[] sectorSync; byte[] tmpBuf; int lba; int diff; Track dataTrack = default; Track audioTrack = default; var offsetFound = false; const uint sectorSize = 2352; driveOffset = cdOffset?.Offset * 4; combinedOffset = null; supportsPlextorReadCdDa = false; if(dskType != MediaType.VideoNowColor) { if(tracks.Any(t => t.Type != TrackType.Audio)) { dataTrack = tracks.FirstOrDefault(t => t.Type != TrackType.Audio); if(dataTrack != null) { // Build sync sectorSync = [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00]; tmpBuf = new byte[sectorSync.Length]; // Ensure to be out of the pregap, or multi-session discs give funny values var wantedLba = (uint)(dataTrack.StartSector + 151); // Plextor READ CDDA if(dbDev?.ATAPI?.RemovableMedias?.Any(d => d.SupportsPlextorReadCDDA == true) == true || dbDev?.SCSI?.RemovableMedias?.Any(d => d.SupportsPlextorReadCDDA == true) == true || dev.Manufacturer.Equals("plextor", StringComparison.InvariantCultureIgnoreCase)) { sense = dev.PlextorReadCdDa(out cmdBuf, out _, wantedLba, sectorSize, 3, PlextorSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { supportsPlextorReadCdDa = true; for(var i = 0; i < cmdBuf.Length - sectorSync.Length; i++) { Array.Copy(cmdBuf, i, tmpBuf, 0, sectorSync.Length); if(!tmpBuf.SequenceEqual(sectorSync)) continue; // De-scramble M and S minute = cmdBuf[i + 12] ^ 0x01; second = cmdBuf[i + 13] ^ 0x80; frame = cmdBuf[i + 14]; // Convert to binary minute = minute / 16 * 10 + (minute & 0x0F); second = second / 16 * 10 + (second & 0x0F); frame = frame / 16 * 10 + (frame & 0x0F); // Calculate the first found LBA lba = minute * 60 * 75 + second * 75 + frame - 150; // Calculate the difference between the found LBA and the requested one diff = (int)wantedLba - lba; combinedOffset = i + 2352 * diff; offsetFound = true; break; } } } if(!offsetFound && (debug || dbDev?.ATAPI?.RemovableMedias?.Any(d => d.CanReadCdScrambled == true) == true || dbDev?.SCSI?.RemovableMedias?.Any(d => d.CanReadCdScrambled == true) == true || dbDev?.SCSI?.MultiMediaDevice?.TestedMedia?.Any(d => d.CanReadCdScrambled == true) == true || dev.Manufacturer.Equals("hl-dt-st", StringComparison.InvariantCultureIgnoreCase))) { sense = dev.ReadCd(out cmdBuf, out _, wantedLba, sectorSize, 3, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { // Clear cache for(var i = 0; i < 63; i++) { sense = dev.ReadCd(out _, out _, (uint)(wantedLba + 3 + 16 * i), sectorSize, 16, MmcSectorTypes.AllTypes, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) break; } dev.ReadCd(out cmdBuf, out _, wantedLba, sectorSize, 3, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); for(var i = 0; i < cmdBuf.Length - sectorSync.Length; i++) { Array.Copy(cmdBuf, i, tmpBuf, 0, sectorSync.Length); if(!tmpBuf.SequenceEqual(sectorSync)) continue; // De-scramble M and S minute = cmdBuf[i + 12] ^ 0x01; second = cmdBuf[i + 13] ^ 0x80; frame = cmdBuf[i + 14]; // Convert to binary minute = minute / 16 * 10 + (minute & 0x0F); second = second / 16 * 10 + (second & 0x0F); frame = frame / 16 * 10 + (frame & 0x0F); // Calculate the first found LBA lba = minute * 60 * 75 + second * 75 + frame - 150; // Calculate the difference between the found LBA and the requested one diff = (int)wantedLba - lba; combinedOffset = i + 2352 * diff; offsetFound = true; break; } } } } } if(offsetFound) return; // Try to get another the offset some other way, we need an audio track just after a data track, same session for(var i = 1; i < tracks.Length; i++) { if(tracks[i - 1].Type == TrackType.Audio || tracks[i].Type != TrackType.Audio) continue; dataTrack = tracks[i - 1]; audioTrack = tracks[i]; break; } if(dataTrack is null || audioTrack is null) return; // Found them sense = dev.ReadCd(out cmdBuf, out _, (uint)audioTrack.StartSector, sectorSize, 3, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) return; dataTrack.EndSector += 150; // Calculate MSF minute = (int)dataTrack.EndSector / 4500; second = ((int)dataTrack.EndSector - minute * 4500) / 75; frame = (int)dataTrack.EndSector - minute * 4500 - second * 75; dataTrack.EndSector -= 150; // Convert to BCD minute = (minute / 10 << 4) + minute % 10; second = (second / 10 << 4) + second % 10; frame = (frame / 10 << 4) + frame % 10; // Scramble M and S minute ^= 0x01; second ^= 0x80; // Build sync sectorSync = [ 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, (byte)minute, (byte)second, (byte)frame ]; tmpBuf = new byte[sectorSync.Length]; for(var i = 0; i < cmdBuf.Length - sectorSync.Length; i++) { Array.Copy(cmdBuf, i, tmpBuf, 0, sectorSync.Length); if(!tmpBuf.SequenceEqual(sectorSync)) continue; combinedOffset = i + 2352; offsetFound = true; break; } if(offsetFound || audioTrack.Pregap <= 0) return; sense = dev.ReadCd(out byte[] dataBuf, out _, (uint)dataTrack.EndSector, sectorSize, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) return; for(var i = 0; i < dataBuf.Length; i++) dataBuf[i] ^= Sector.ScrambleTable[i]; for(var i = 0; i < 2352; i++) { var dataSide = new byte[2352 - i]; var audioSide = new byte[2352 - i]; Array.Copy(dataBuf, i, dataSide, 0, dataSide.Length); Array.Copy(cmdBuf, 0, audioSide, 0, audioSide.Length); if(!dataSide.SequenceEqual(audioSide)) continue; combinedOffset = audioSide.Length; break; } } else { var videoNowColorFrame = new byte[9 * sectorSize]; sense = dev.ReadCd(out cmdBuf, out _, 0, sectorSize, 9, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) { sense = dev.ReadCd(out cmdBuf, out _, 0, sectorSize, 9, MmcSectorTypes.Cdda, false, false, true, MmcHeaderCodes.None, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) videoNowColorFrame = null; } if(videoNowColorFrame is null) { dumpLog?.WriteLine(Localization.Core.Could_not_find_VideoNow_Color_frame_offset); updateStatus?.Invoke("Could not find VideoNow Color frame offset, dump may not be correct."); } else { combinedOffset = MMC.GetVideoNowColorOffset(videoNowColorFrame); dumpLog?.WriteLine(string.Format(Localization.Core.VideoNow_Color_frame_is_offset_0_bytes, combinedOffset)); updateStatus?.Invoke($"VideoNow Color frame is offset {combinedOffset} bytes."); } } } }