// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : MMC.cs // Author(s) : Natalia Portillo // // Component : Core. // // --[ Description ] ---------------------------------------------------------- // // Detects media types in MultiMediaCommand devices // // --[ 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 // ****************************************************************************/ // ReSharper disable JoinDeclarationAndInitializer using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Aaru.Checksums; using Aaru.CommonTypes; using Aaru.Decoders.Bluray; using Aaru.Decoders.CD; using Aaru.Decoders.DVD; using Aaru.Decoders.SCSI.MMC; using Aaru.Decoders.Sega; using Aaru.Devices; using Aaru.Helpers; using Aaru.Logging; using Sentry; using DMI = Aaru.Decoders.Xbox.DMI; using Sector = Aaru.Decoders.CD.Sector; namespace Aaru.Core.Media.Detection; /// Detects media type for MMC class devices public static class MMC { /// SHA256 of PlayStation 2 boot sectors, seen in PAL discs const string PS2_PAL_HASH = "5d04ff236613e1d8adcf9c201874acd6f6deed1e04306558b86f91cfb626f39d"; /// SHA256 of PlayStation 2 boot sectors, seen in Japanese, American, Malaysian and Korean discs const string PS2_NTSC_HASH = "0bada1426e2c0351b872ef2a9ad2e5a0ac3918f4c53aa53329cb2911a8e16c23"; /// SHA256 of PlayStation 2 boot sectors, seen in Japanese discs const string PS2_JAPANESE_HASH = "b82bffb809070d61fe050b7e1545df53d8f3cc648257cdff7502bc0ba6b38870"; const string MODULE_NAME = "Media detection"; static readonly byte[] _ps3Id = "PlayStation3\0\0\0\0"u8.ToArray(); static readonly byte[] _ps4Id = "PlayStation4\0\0\0\0"u8.ToArray(); static readonly byte[] _ps5Id = "PlayStation5\0\0\0\0"u8.ToArray(); static readonly byte[] _operaId = [0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01]; // Only present on bootable CDs, but those make more than 99% of all available static readonly byte[] _fmTownsBootId = [0x49, 0x50, 0x4C, 0x34, 0xEB, 0x55, 0x06]; /// Present on first two seconds of second track, says "COPYRIGHT BANDAI" static readonly byte[] _playdiaCopyright = "COPYRIGHT BANDAI"u8.ToArray(); static readonly byte[] _pcEngineSignature = "PC Engine CD-ROM SYSTEM"u8.ToArray(); static readonly byte[] _pcFxSignature = "PC-FX:Hu_CD-ROM"u8.ToArray(); static readonly byte[] _atariSignature = "TAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTAIRTARA IPARPVODED TA AEHDAREA RT"u8.ToArray(); /// This is some kind of header. Every 10 bytes there's an audio byte. static readonly byte[] _videoNowColorFrameMarker = [ 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x81, 0xE3, 0xE3, 0xC7, 0xC7, 0x81, 0x81, 0xE3, 0xC7, 0x00, 0x00, 0x00, 0x02, 0x01, 0x04, 0x02, 0x06, 0x03, 0xFF, 0x00, 0x08, 0x04, 0x0A, 0x05, 0x0C, 0x06, 0x0E, 0x07, 0xFF, 0x00, 0x11, 0x08, 0x13, 0x09, 0x15, 0x0A, 0x17, 0x0B, 0xFF, 0x00, 0x19, 0x0C, 0x1B, 0x0D, 0x1D, 0x0E, 0x1F, 0x0F, 0xFF, 0x00, 0x00, 0x28, 0x02, 0x29, 0x04, 0x2A, 0x06, 0x2B, 0xFF, 0x00, 0x08, 0x2C, 0x0A, 0x2D, 0x0C, 0x2E, 0x0E, 0x2F, 0xFF, 0x00, 0x11, 0x30, 0x13, 0x31, 0x15, 0x32, 0x17, 0x33, 0xFF, 0x00, 0x19, 0x34, 0x1B, 0x35, 0x1D, 0x36, 0x1F, 0x37, 0xFF, 0x00, 0x00, 0x38, 0x02, 0x39, 0x04, 0x3A, 0x06, 0x3B, 0xFF, 0x00, 0x08, 0x3C, 0x0A, 0x3D, 0x0C, 0x3E, 0x0E, 0x3F, 0xFF, 0x00, 0x11, 0x40, 0x13, 0x41, 0x15, 0x42, 0x17, 0x43, 0xFF, 0x00, 0x19, 0x44, 0x1B, 0x45, 0x1D, 0x46, 0x1F, 0x47, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 ]; static bool IsData(byte[] sector) { if(sector?.Length != 2352) return false; byte[] syncMark = [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00]; var testMark = new byte[12]; Array.Copy(sector, 0, testMark, 0, 12); return syncMark.SequenceEqual(testMark) && (sector[0xF] == 0 || sector[0xF] == 1 || sector[0xF] == 2); } static bool IsScrambledData(byte[] sector, int wantedLba, out int offset) { offset = 0; if(sector?.Length != 2352) return false; byte[] syncMark = [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00]; var testMark = new byte[12]; for(var i = 0; i <= 2336; i++) { Array.Copy(sector, i, testMark, 0, 12); if(!syncMark.SequenceEqual(testMark) || sector[i + 0xF] != 0x60 && sector[i + 0xF] != 0x61 && sector[i + 0xF] != 0x62) continue; // De-scramble M and S int minute = sector[i + 12] ^ 0x01; int second = sector[i + 13] ^ 0x80; int frame = sector[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 int lba = minute * 60 * 75 + second * 75 + frame - 150; // Calculate the difference between the found LBA and the requested one int diff = wantedLba - lba; offset = i + 2352 * diff; return true; } return false; } static byte[] DescrambleAndFixOffset(byte[] sector, int offsetBytes, int sectorsForOffset) { var descrambled = new byte[2352]; int offsetFix = offsetBytes < 0 ? 2352 * sectorsForOffset + offsetBytes : offsetBytes; Array.Copy(sector, offsetFix, descrambled, 0, 2352); return Sector.Scramble(descrambled); } /// Checks if the media corresponds to CD-i. /// Contents of LBA 0, with all headers. /// Contents of LBA 0, with all headers. /// true if it corresponds to a CD-i, falseotherwise. static bool IsCdi(byte[] sector0, byte[] sector16) { if(sector16?.Length != 2352) return false; byte[] cdiMark = [0x01, 0x43, 0x44, 0x2D]; bool isData = IsData(sector0); if(!isData || sector0[0xF] != 2 && sector0[0xF] != 1) return false; var testMark = new byte[4]; Array.Copy(sector16, 24, testMark, 0, 4); return cdiMark.SequenceEqual(testMark); } static bool IsVideoNowColor(byte[] videoFrame) { if(videoFrame is null || videoFrame.Length < _videoNowColorFrameMarker.Length) return false; var buffer = new byte[_videoNowColorFrameMarker.Length]; for(var framePosition = 0; framePosition + buffer.Length < videoFrame.Length; framePosition++) { Array.Copy(videoFrame, framePosition, buffer, 0, buffer.Length); for(var ab = 9; ab < buffer.Length; ab += 10) buffer[ab] = 0; if(!_videoNowColorFrameMarker.SequenceEqual(buffer)) continue; return true; } return false; } internal static int GetVideoNowColorOffset(byte[] data) { var buffer = new byte[_videoNowColorFrameMarker.Length]; for(var framePosition = 0; framePosition + buffer.Length < data.Length; framePosition++) { Array.Copy(data, framePosition, buffer, 0, buffer.Length); for(var ab = 9; ab < buffer.Length; ab += 10) buffer[ab] = 0; if(!_videoNowColorFrameMarker.SequenceEqual(buffer)) continue; return 18032 - framePosition; } return 0; } internal static void DetectDiscType(ref MediaType mediaType, int sessions, FullTOC.CDFullTOC? decodedToc, Device dev, out bool hiddenTrack, out bool hiddenData, int firstTrackLastSession, ulong blocks) { uint startOfFirstDataTrack = uint.MaxValue; DI.DiscInformation? blurayDi = null; bool sense; byte secondSessionFirstTrack = 0; byte[] sector0; byte[] sector1 = null; byte[] ps2BootSectors = null; byte[] playdia1 = null; byte[] playdia2 = null; byte[] firstDataSectorNotZero = null; byte[] secondDataSectorNotZero = null; byte[] firstTrackSecondSession = null; byte[] firstTrackSecondSessionAudio = null; byte[] videoNowColorFrame; hiddenTrack = false; hiddenData = false; sense = dev.GetConfiguration(out byte[] cmdBuf, out _, 0, MmcGetConfigurationRt.Current, dev.Timeout, out _); if(!sense) { Features.SeparatedFeatures ftr = Features.Separate(cmdBuf); AaruLogging.Debug(MODULE_NAME, Localization.Core.GET_CONFIGURATION_current_profile_is_0, ftr.CurrentProfile); mediaType = ftr.CurrentProfile switch { 0x0001 => MediaType.GENERIC_HDD, 0x0005 => MediaType.CDMO, 0x0008 => MediaType.CD, 0x0009 => MediaType.CDR, 0x000A => MediaType.CDRW, 0x0010 => MediaType.DVDROM, 0x0011 => MediaType.DVDR, 0x0012 => MediaType.DVDRAM, 0x0013 or 0x0014 => MediaType.DVDRW, 0x0015 or 0x0016 => MediaType.DVDRDL, 0x0017 => MediaType.DVDRWDL, 0x0018 => MediaType.DVDDownload, 0x001A => MediaType.DVDPRW, 0x001B => MediaType.DVDPR, 0x0020 => MediaType.DDCD, 0x0021 => MediaType.DDCDR, 0x0022 => MediaType.DDCDRW, 0x002A => MediaType.DVDPRWDL, 0x002B => MediaType.DVDPRDL, 0x0040 => MediaType.BDROM, 0x0041 or 0x0042 => MediaType.BDR, 0x0043 => MediaType.BDRE, 0x0050 => MediaType.HDDVDROM, 0x0051 => MediaType.HDDVDR, 0x0052 => MediaType.HDDVDRAM, 0x0053 => MediaType.HDDVDRW, 0x0058 => MediaType.HDDVDRDL, 0x005A => MediaType.HDDVDRWDL, _ => mediaType }; } if(decodedToc?.TrackDescriptors.Any(static t => t.SessionNumber == 2) == true) { secondSessionFirstTrack = decodedToc.Value.TrackDescriptors.Where(static t => t.SessionNumber == 2) .Min(static t => t.POINT); } if(mediaType is MediaType.CD or MediaType.CDROMXA or MediaType.CDI) { sense = dev.ReadAtip(out cmdBuf, out _, dev.Timeout, out _); if(!sense) { ATIP.CDATIP atip = ATIP.Decode(cmdBuf); if(atip != null) // Only CD-R and CD-RW have ATIP mediaType = atip.DiscType ? MediaType.CDRW : MediaType.CDR; } } if(mediaType is MediaType.CD or MediaType.CDROMXA) { var hasDataTrack = false; var hasAudioTrack = false; var allFirstSessionTracksAreAudio = true; var hasVideoTrack = false; if(decodedToc.HasValue) { FullTOC.TrackDataDescriptor a0Track = decodedToc.Value.TrackDescriptors.FirstOrDefault(static t => t is { POINT: 0xA0, ADR: 1 }); if(a0Track.POINT == 0xA0) { switch(a0Track.PSEC) { case 0x10: AaruLogging.Debug(MODULE_NAME, Localization.Core.TOC_says_disc_type_is_CD_i); mediaType = MediaType.CDI; break; case 0x20: AaruLogging.Debug(MODULE_NAME, Localization.Core.TOC_says_disc_type_is_CD_ROM_XA); mediaType = MediaType.CDROMXA; break; } } foreach(FullTOC.TrackDataDescriptor track in decodedToc.Value.TrackDescriptors.Where(static t => t.POINT is > 0 and <= 0x99)) { if(track.TNO == 1 && ((TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrack || (TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrackIncremental)) allFirstSessionTracksAreAudio &= firstTrackLastSession != 1; if((TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrack || (TocControl)(track.CONTROL & 0x0D) == TocControl.DataTrackIncremental) { var startAddress = (uint)(track.PHOUR * 3600 * 75 + track.PMIN * 60 * 75 + track.PSEC * 75 + track.PFRAME - 150); if(startAddress < startOfFirstDataTrack) startOfFirstDataTrack = startAddress; hasDataTrack = true; allFirstSessionTracksAreAudio &= track.POINT >= firstTrackLastSession; } else hasAudioTrack = true; hasVideoTrack |= track.ADR == 4; } } if(mediaType != MediaType.CDI) { switch(hasDataTrack) { case true when hasAudioTrack && allFirstSessionTracksAreAudio && sessions == 2: AaruLogging.Debug(MODULE_NAME, Localization.Core .Disc_has_audio_and_data_two_sessions_all_data_second_CD_Plus); mediaType = MediaType.CDPLUS; break; case false when hasAudioTrack && sessions == 1: AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_only_audio_in_a_session_CD_Digital_Audio); mediaType = MediaType.CDDA; break; } if(hasDataTrack && !hasAudioTrack && sessions == 1) { AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_only_data_in_a_session_CD_ROM); mediaType = MediaType.CDROM; } if(hasVideoTrack && !hasDataTrack && sessions == 1) { AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_video_tracks_CD_Video); mediaType = MediaType.CDV; } } if(mediaType is MediaType.CD or MediaType.CDROM && hasDataTrack) { foreach(uint startAddress in decodedToc.Value.TrackDescriptors .Where(static t => t.POINT is > 0 and <= 0x99 && ((TocControl)(t.CONTROL & 0x0D) == TocControl.DataTrack || (TocControl)(t.CONTROL & 0x0D) == TocControl.DataTrackIncremental)) .Select(static track => (uint)(track.PHOUR * 3600 * 75 + track.PMIN * 60 * 75 + track.PSEC * 75 + track.PFRAME - 150) + 16)) { sense = dev.ReadCd(out cmdBuf, out _, startAddress, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) continue; if(cmdBuf[0] != 0x00 || cmdBuf[1] != 0xFF || cmdBuf[2] != 0xFF || cmdBuf[3] != 0xFF || cmdBuf[4] != 0xFF || cmdBuf[5] != 0xFF || cmdBuf[6] != 0xFF || cmdBuf[7] != 0xFF || cmdBuf[8] != 0xFF || cmdBuf[9] != 0xFF || cmdBuf[10] != 0xFF || cmdBuf[11] != 0x00 || cmdBuf[15] != 0x02) continue; AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_a_mode_two_data_track_CD_ROM_XA); mediaType = MediaType.CDROMXA; break; } } } if(secondSessionFirstTrack != 0 && decodedToc?.TrackDescriptors.Any(t => t.POINT == secondSessionFirstTrack) == true) { FullTOC.TrackDataDescriptor secondSessionFirstTrackTrack = decodedToc.Value.TrackDescriptors.First(t => t.POINT == secondSessionFirstTrack); var firstSectorSecondSessionFirstTrack = (uint)(secondSessionFirstTrackTrack.PHOUR * 3600 * 75 + secondSessionFirstTrackTrack.PMIN * 60 * 75 + secondSessionFirstTrackTrack.PSEC * 75 + secondSessionFirstTrackTrack.PFRAME - 150); sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) firstTrackSecondSession = cmdBuf; else { sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack, 2352, 1, MmcSectorTypes.Cdda, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) firstTrackSecondSession = cmdBuf; } sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack - 1, 2352, 3, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) firstTrackSecondSessionAudio = cmdBuf; else { sense = dev.ReadCd(out cmdBuf, out _, firstSectorSecondSessionFirstTrack - 1, 2352, 3, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) firstTrackSecondSessionAudio = cmdBuf; } } videoNowColorFrame = new byte[9 * 2352]; for(var i = 0; i < 9; i++) { sense = dev.ReadCd(out cmdBuf, out _, (uint)i, 2352, 1, 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 _, (uint)i, 2352, 1, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || !dev.Error) { videoNowColorFrame = null; break; } } Array.Copy(cmdBuf, 0, videoNowColorFrame, i * 2352, 2352); } FullTOC.TrackDataDescriptor? firstTrack = decodedToc?.TrackDescriptors.FirstOrDefault(t => t.POINT == decodedToc.Value.TrackDescriptors .Min(static m => m.POINT)); if(firstTrack?.POINT is >= 1 and < 0xA0) { var firstTrackSector = (uint)(firstTrack.Value.PHOUR * 3600 * 75 + firstTrack.Value.PMIN * 60 * 75 + firstTrack.Value.PSEC * 75 + firstTrack.Value.PFRAME - 150); // Check for hidden data before start of track 1 if(firstTrackSector > 0) { sense = dev.ReadCd(out sector0, out _, 0, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!dev.Error && !sense) { hiddenTrack = true; hiddenData = IsData(sector0); if(hiddenData) { sense = dev.ReadCd(out byte[] sector16, out _, 16, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && IsCdi(sector0, sector16)) { mediaType = MediaType.CDIREADY; AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_hidden_CD_i_pregap_CD_i_Ready); return; } } else { hiddenData = IsScrambledData(sector0, 0, out int combinedOffset); if(hiddenData) { int sectorsForOffset = combinedOffset / 2352; if(sectorsForOffset < 0) sectorsForOffset *= -1; if(combinedOffset % 2352 != 0) sectorsForOffset++; var lba0 = 0; var lba16 = 16; if(combinedOffset < 0) { lba0 -= sectorsForOffset; lba16 -= sectorsForOffset; } sense = dev.ReadCd(out sector0, out _, (uint)lba0, 2352, (uint)sectorsForOffset + 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); // Drive does not support reading negative sectors? if(sense && lba0 < 0) { dev.ReadCd(out sector0, out _, 0, 2352, 2, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); sector0 = DescrambleAndFixOffset(sector0, combinedOffset, sectorsForOffset); } else sector0 = DescrambleAndFixOffset(sector0, combinedOffset, sectorsForOffset); dev.ReadCd(out byte[] sector16, out _, (uint)lba16, 2352, (uint)sectorsForOffset + 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); sector16 = DescrambleAndFixOffset(sector16, combinedOffset, sectorsForOffset); if(IsCdi(sector0, sector16)) { mediaType = MediaType.CDIREADY; AaruLogging.Debug(MODULE_NAME, Localization.Core.Disc_has_hidden_CD_i_pregap_CD_i_Ready); return; } } } } } } sector0 = null; switch(mediaType) { case MediaType.CD: case MediaType.CDDA: case MediaType.CDPLUS: case MediaType.CDROM: case MediaType.CDROMXA: { sense = dev.ReadCd(out cmdBuf, out _, 0, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { sector0 = new byte[2048]; Array.Copy(cmdBuf, 16, sector0, 0, 2048); sense = dev.ReadCd(out cmdBuf, out _, 1, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { sector1 = new byte[2048]; Array.Copy(cmdBuf, 16, sector1, 0, 2048); } sense = dev.ReadCd(out cmdBuf, out _, 4200, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { playdia1 = new byte[2048]; Array.Copy(cmdBuf, 24, playdia1, 0, 2048); } sense = dev.ReadCd(out cmdBuf, out _, 4201, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { playdia2 = new byte[2048]; Array.Copy(cmdBuf, 24, playdia2, 0, 2048); } if(startOfFirstDataTrack != uint.MaxValue) { sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { firstDataSectorNotZero = new byte[2048]; Array.Copy(cmdBuf, 16, firstDataSectorNotZero, 0, 2048); } sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack + 1, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { secondDataSectorNotZero = new byte[2048]; Array.Copy(cmdBuf, 16, secondDataSectorNotZero, 0, 2048); } } var ps2Ms = new MemoryStream(); for(uint p = 0; p < 12; p++) { sense = dev.ReadCd(out cmdBuf, out _, p, 2352, 1, MmcSectorTypes.AllTypes, false, false, true, MmcHeaderCodes.AllHeaders, true, true, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) break; ps2Ms.Write(cmdBuf, cmdBuf[0x0F] == 0x02 ? 24 : 16, 2048); } if(ps2Ms.Length == 0x6000) ps2BootSectors = ps2Ms.ToArray(); } else { sense = dev.ReadCd(out cmdBuf, out _, 0, 2324, 1, MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { sector0 = new byte[2048]; Array.Copy(cmdBuf, 0, sector0, 0, 2048); sense = dev.ReadCd(out cmdBuf, out _, 1, 2324, 1, MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { sector1 = new byte[2048]; Array.Copy(cmdBuf, 1, sector0, 0, 2048); } sense = dev.ReadCd(out cmdBuf, out _, 4200, 2324, 1, MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { playdia1 = new byte[2048]; Array.Copy(cmdBuf, 0, playdia1, 0, 2048); } sense = dev.ReadCd(out cmdBuf, out _, 4201, 2324, 1, MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { playdia2 = new byte[2048]; Array.Copy(cmdBuf, 0, playdia2, 0, 2048); } if(startOfFirstDataTrack != uint.MaxValue) { sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack, 2324, 1, MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { firstDataSectorNotZero = new byte[2048]; Array.Copy(cmdBuf, 0, firstDataSectorNotZero, 0, 2048); } sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack + 1, 2324, 1, MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { secondDataSectorNotZero = new byte[2048]; Array.Copy(cmdBuf, 0, secondDataSectorNotZero, 0, 2048); } } var ps2Ms = new MemoryStream(); for(uint p = 0; p < 12; p++) { sense = dev.ReadCd(out cmdBuf, out _, p, 2324, 1, MmcSectorTypes.Mode2, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(sense || dev.Error) break; ps2Ms.Write(cmdBuf, 0, 2048); } if(ps2Ms.Length == 0x6000) ps2BootSectors = ps2Ms.ToArray(); } else { sense = dev.ReadCd(out cmdBuf, out _, 0, 2048, 1, MmcSectorTypes.Mode1, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) { sector0 = cmdBuf; sense = dev.ReadCd(out cmdBuf, out _, 0, 2048, 1, MmcSectorTypes.Mode1, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) sector1 = cmdBuf; sense = dev.ReadCd(out cmdBuf, out _, 0, 2048, 12, MmcSectorTypes.Mode1, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) ps2BootSectors = cmdBuf; if(startOfFirstDataTrack != uint.MaxValue) { sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack, 2048, 1, MmcSectorTypes.Mode1, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) firstDataSectorNotZero = cmdBuf; sense = dev.ReadCd(out cmdBuf, out _, startOfFirstDataTrack + 1, 2048, 1, MmcSectorTypes.Mode1, false, false, false, MmcHeaderCodes.None, true, false, MmcErrorField.None, MmcSubchannel.None, dev.Timeout, out _); if(!sense && !dev.Error) secondDataSectorNotZero = cmdBuf; } } else goto case MediaType.DVDROM; } } break; } // TODO: Check for CD-i Ready case MediaType.CDI: break; case MediaType.DVDROM: case MediaType.HDDVDROM: case MediaType.BDROM: case MediaType.UHDBD: case MediaType.Unknown: if(mediaType is MediaType.BDROM or MediaType.UHDBD) { sense = dev.ReadDiscStructure(out cmdBuf, out _, MmcDiscStructureMediaType.Bd, 0, 0, MmcDiscStructureFormat.DiscInformation, 0, dev.Timeout, out _); if(!sense) blurayDi = DI.Decode(cmdBuf); } sense = dev.Read16(out cmdBuf, out _, 0, false, false, false, 0, 2048, 0, 1, false, dev.Timeout, out _); if(!sense && !dev.Error) { sector0 = cmdBuf; sense = dev.Read16(out cmdBuf, out _, 0, false, false, false, 1, 2048, 0, 1, false, dev.Timeout, out _); if(!sense && !dev.Error) sector1 = cmdBuf; sense = dev.Read16(out cmdBuf, out _, 0, false, false, false, 0, 2048, 0, 12, false, dev.Timeout, out _); if(!sense && !dev.Error && cmdBuf.Length == 0x6000) ps2BootSectors = cmdBuf; } else { sense = dev.Read12(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 1, false, dev.Timeout, out _); if(!sense && !dev.Error) { sector0 = cmdBuf; sense = dev.Read12(out cmdBuf, out _, 0, false, false, false, false, 1, 2048, 0, 1, false, dev.Timeout, out _); if(!sense && !dev.Error) sector1 = cmdBuf; sense = dev.Read12(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 12, false, dev.Timeout, out _); if(!sense && !dev.Error && cmdBuf.Length == 0x6000) ps2BootSectors = cmdBuf; } else { sense = dev.Read10(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 1, dev.Timeout, out _); if(!sense && !dev.Error) { sector0 = cmdBuf; sense = dev.Read10(out cmdBuf, out _, 0, false, false, false, false, 1, 2048, 0, 1, dev.Timeout, out _); if(!sense && !dev.Error) sector1 = cmdBuf; sense = dev.Read10(out cmdBuf, out _, 0, false, false, false, false, 0, 2048, 0, 12, dev.Timeout, out _); if(!sense && !dev.Error && cmdBuf.Length == 0x6000) ps2BootSectors = cmdBuf; } else { sense = dev.Read6(out cmdBuf, out _, 0, 2048, 1, dev.Timeout, out _); if(!sense && !dev.Error) { sector0 = cmdBuf; sense = dev.Read6(out cmdBuf, out _, 1, 2048, 1, dev.Timeout, out _); if(!sense && !dev.Error) sector1 = cmdBuf; sense = dev.Read6(out cmdBuf, out _, 0, 2048, 12, dev.Timeout, out _); if(!sense && !dev.Error && cmdBuf.Length == 0x6000) ps2BootSectors = cmdBuf; } } } } if(mediaType == MediaType.DVDROM) { sense = dev.ReadDiscStructure(out cmdBuf, out _, MmcDiscStructureMediaType.Dvd, 0, 0, MmcDiscStructureFormat.PhysicalInformation, 0, dev.Timeout, out _); if(!sense) { PFI.PhysicalFormatInformation? pfi = PFI.Decode(cmdBuf, mediaType); if(pfi != null) { mediaType = pfi.Value.DiskCategory switch { DiskCategory.DVDPR => MediaType.DVDPR, DiskCategory.DVDPRDL => MediaType.DVDPRDL, DiskCategory.DVDPRW => MediaType.DVDPRW, DiskCategory.DVDPRWDL => MediaType.DVDPRWDL, DiskCategory.DVDR => pfi.Value.PartVersion >= 6 ? MediaType.DVDRDL : MediaType.DVDR, DiskCategory.DVDRAM => MediaType.DVDRAM, DiskCategory.DVDRW => pfi.Value.PartVersion >= 15 ? MediaType.DVDRWDL : MediaType.DVDRW, DiskCategory.HDDVDR => MediaType.HDDVDR, DiskCategory.HDDVDRAM => MediaType.HDDVDRAM, DiskCategory.HDDVDROM => MediaType.HDDVDROM, DiskCategory.HDDVDRW => MediaType.HDDVDRW, DiskCategory.Nintendo => pfi.Value.DiscSize == DVDSize.Eighty ? MediaType.GOD : MediaType.WOD, DiskCategory.UMD => MediaType.UMD, _ => mediaType }; } } sense = dev.ReadDiscStructure(out cmdBuf, out _, MmcDiscStructureMediaType.Dvd, 0, 0, MmcDiscStructureFormat.DiscManufacturingInformation, 0, dev.Timeout, out _); if(!sense) { if(DMI.IsXbox(cmdBuf)) { AaruLogging.Debug(MODULE_NAME, "Found Xbox DMI, setting disc type to Xbox Game Disc (XGD)."); mediaType = MediaType.XGD; return; } if(DMI.IsXbox360(cmdBuf)) { // All XGD3 all have the same number of blocks if(blocks is 25063 or 4229664 or 4246304) // Wxripper unlock { AaruLogging.Debug(MODULE_NAME, "Found Xbox 360 DMI with {0} blocks, setting disc type to Xbox 360 Game Disc 3 (XGD3)."); mediaType = MediaType.XGD3; return; } AaruLogging.Debug(MODULE_NAME, "Found Xbox 360 DMI with {0} blocks, setting disc type to Xbox 360 Game Disc 2 (XGD2)."); mediaType = MediaType.XGD2; return; } } } break; // Recordables will be checked for PhotoCD only case MediaType.CDR: // Check if ISO9660 sense = dev.Read12(out byte[] isoSector, out _, 0, false, false, false, false, 16, 2048, 0, 1, false, dev.Timeout, out _); // Sector 16 reads, and contains "CD001" magic? if(sense || isoSector[1] != 0x43 || isoSector[2] != 0x44 || isoSector[3] != 0x30 || isoSector[4] != 0x30 || isoSector[5] != 0x31) return; // From sectors 16 to 31 uint isoSectorPosition = 16; while(isoSectorPosition < 32) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, isoSectorPosition, 2048, 0, 1, false, dev.Timeout, out _); // If sector cannot be read, break here if(sense) break; // If sector does not contain "CD001" magic, break if(isoSector[1] != 0x43 || isoSector[2] != 0x44 || isoSector[3] != 0x30 || isoSector[4] != 0x30 || isoSector[5] != 0x31) break; // If it is PVD or end of descriptor chain, break if(isoSector[0] == 1 || isoSector[0] == 255) break; isoSectorPosition++; } // If it's not an ISO9660 PVD, return if(isoSector[0] != 1 || isoSector[1] != 0x43 || isoSector[2] != 0x44 || isoSector[3] != 0x30 || isoSector[4] != 0x30 || isoSector[5] != 0x31) return; var rootStart = BitConverter.ToUInt32(isoSector, 158); var rootLength = BitConverter.ToUInt32(isoSector, 166); if(rootStart == 0 || rootLength == 0) return; rootLength /= 2048; try { using var rootMs = new MemoryStream(); for(uint i = 0; i < rootLength; i++) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, rootStart + i, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; rootMs.Write(isoSector, 0, 2048); } isoSector = rootMs.ToArray(); } catch(Exception ex) { SentrySdk.CaptureException(ex); return; } if(isoSector.Length < 2048) return; var rootPos = 0; uint pcdStart = 0; uint pcdLength = 0; while(isoSector[rootPos] > 0 && rootPos < isoSector.Length && rootPos + isoSector[rootPos] <= isoSector.Length) { int nameLen = isoSector[rootPos + 32]; var tmpName = new byte[nameLen]; Array.Copy(isoSector, rootPos + 33, tmpName, 0, nameLen); string name = StringHandlers.CToString(tmpName).ToUpperInvariant(); if(name.EndsWith(";1", StringComparison.InvariantCulture)) name = name[..^2]; if(name == "PHOTO_CD" && (isoSector[rootPos + 25] & 0x02) == 0x02) { pcdStart = BitConverter.ToUInt32(isoSector, rootPos + 2); pcdLength = BitConverter.ToUInt32(isoSector, rootPos + 10) / 2048; } rootPos += isoSector[rootPos]; } if(pcdLength > 0) { try { using var pcdMs = new MemoryStream(); for(uint i = 0; i < pcdLength; i++) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, pcdStart + i, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; pcdMs.Write(isoSector, 0, 2048); } isoSector = pcdMs.ToArray(); } catch(Exception ex) { SentrySdk.CaptureException(ex); return; } if(isoSector.Length < 2048) return; for(var pi = 0; pi < pcdLength; pi++) { int pcdPos = pi * 2048; uint infoPos = 0; while(isoSector[pcdPos] > 0 && pcdPos < isoSector.Length && pcdPos + isoSector[pcdPos] <= isoSector.Length) { int nameLen = isoSector[pcdPos + 32]; var tmpName = new byte[nameLen]; Array.Copy(isoSector, pcdPos + 33, tmpName, 0, nameLen); string name = StringHandlers.CToString(tmpName).ToUpperInvariant(); if(name.EndsWith(";1", StringComparison.InvariantCulture)) name = name[..^2]; if(name == "INFO.PCD") { infoPos = BitConverter.ToUInt32(isoSector, pcdPos + 2); break; } pcdPos += isoSector[pcdPos]; } if(infoPos > 0) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, infoPos, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; var systemId = new byte[8]; Array.Copy(isoSector, 0, systemId, 0, 8); string id = StringHandlers.CToString(systemId).TrimEnd(); switch(id) { case "PHOTO_CD": mediaType = MediaType.PCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Photo_CD_description_file); return; } } } } break; // Other recordables will not be checked case MediaType.CDRW: case MediaType.CDMRW: case MediaType.DDCDR: case MediaType.DDCDRW: case MediaType.DVDR: case MediaType.DVDRW: case MediaType.DVDPR: case MediaType.DVDPRW: case MediaType.DVDPRWDL: case MediaType.DVDRDL: case MediaType.DVDPRDL: case MediaType.DVDRAM: case MediaType.DVDRWDL: case MediaType.DVDDownload: case MediaType.HDDVDRAM: case MediaType.HDDVDR: case MediaType.HDDVDRW: case MediaType.HDDVDRDL: case MediaType.HDDVDRWDL: case MediaType.BDR: case MediaType.BDRE: case MediaType.BDRXL: case MediaType.BDREXL: return; } if(sector0 == null) return; switch(mediaType) { case MediaType.CD: case MediaType.CDDA: case MediaType.CDPLUS: case MediaType.CDROM: case MediaType.CDROMXA: // TODO: Pippin requires interpreting Apple Partition Map, reading HFS and checking for Pippin signatures { if(CD.DecodeIPBin(sector0).HasValue) { mediaType = MediaType.MEGACD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Mega_Sega_CD_IP_BIN); return; } if(Saturn.DecodeIPBin(sector0).HasValue) { mediaType = MediaType.SATURNCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sega_Saturn_IP_BIN); return; } // Are GDR detectable ??? if(Dreamcast.DecodeIPBin(sector0).HasValue) { mediaType = MediaType.GDROM; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sega_Dreamcast_IP_BIN); return; } if(ps2BootSectors is { Length: 0x6000 }) { // The decryption key is applied as XOR. As first byte is originally always NULL, it gives us the key :) byte decryptByte = ps2BootSectors[0]; for(var i = 0; i < 0x6000; i++) ps2BootSectors[i] ^= decryptByte; string ps2BootSectorsHash = Sha256Context.Data(ps2BootSectors, out _); AaruLogging.Debug(MODULE_NAME, Localization.Core.PlayStation_2_boot_sectors_SHA256_0, ps2BootSectorsHash); if(ps2BootSectorsHash is PS2_PAL_HASH or PS2_NTSC_HASH or PS2_JAPANESE_HASH) { mediaType = MediaType.PS2CD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_2_boot_sectors); goto hasPs2CdBoot; } } if(sector0 != null) { var syncBytes = new byte[7]; Array.Copy(sector0, 0, syncBytes, 0, 7); if(_operaId.SequenceEqual(syncBytes)) { mediaType = MediaType.ThreeDO; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Opera_filesystem); return; } if(_fmTownsBootId.SequenceEqual(syncBytes)) { mediaType = MediaType.FMTOWNS; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_FM_Towns_boot); return; } } if(playdia1 != null && playdia2 != null) { var pd1 = new byte[_playdiaCopyright.Length]; var pd2 = new byte[_playdiaCopyright.Length]; Array.Copy(playdia1, 38, pd1, 0, pd1.Length); Array.Copy(playdia2, 0, pd2, 0, pd1.Length); if(_playdiaCopyright.SequenceEqual(pd1) && _playdiaCopyright.SequenceEqual(pd2)) { mediaType = MediaType.Playdia; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Playdia_copyright); return; } } if(secondDataSectorNotZero != null) { var pce = new byte[_pcEngineSignature.Length]; Array.Copy(secondDataSectorNotZero, 32, pce, 0, pce.Length); if(_pcEngineSignature.SequenceEqual(pce)) { mediaType = MediaType.SuperCDROM2; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_PC_Engine_CD_signature); return; } } if(firstDataSectorNotZero != null) { var pcfx = new byte[_pcFxSignature.Length]; Array.Copy(firstDataSectorNotZero, 0, pcfx, 0, pcfx.Length); if(_pcFxSignature.SequenceEqual(pcfx)) { mediaType = MediaType.PCFX; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_PC_FX_copyright); return; } } if(firstTrackSecondSessionAudio != null) { var jaguar = new byte[_atariSignature.Length]; for(var i = 0; i + jaguar.Length <= firstTrackSecondSessionAudio.Length; i += 2) { Array.Copy(firstTrackSecondSessionAudio, i, jaguar, 0, jaguar.Length); if(!_atariSignature.SequenceEqual(jaguar)) continue; mediaType = MediaType.JaguarCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Atari_signature); break; } } if(firstTrackSecondSession?.Length >= 2336) { var milcd = new byte[2048]; Array.Copy(firstTrackSecondSession, 24, milcd, 0, 2048); if(Dreamcast.DecodeIPBin(milcd).HasValue) { mediaType = MediaType.MilCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sega_Dreamcast_IP_BIN_on_second_session); return; } } // TODO: Detect black and white VideoNow // TODO: Detect VideoNow XP if(IsVideoNowColor(videoNowColorFrame)) { mediaType = MediaType.VideoNowColor; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_VideoNow_Color_frame); return; } // Check CD+G, CD+EG and CD+MIDI if(mediaType == MediaType.CDDA) { sense = dev.ReadCd(out byte[] subBuf, out _, 150, 96, 8, MmcSectorTypes.Cdda, false, false, false, MmcHeaderCodes.None, false, false, MmcErrorField.None, MmcSubchannel.Raw, dev.Timeout, out _); if(!sense) { var cdg = false; var cdeg = false; var cdmidi = false; for(var i = 0; i < 8; i++) { var tmpSub = new byte[96]; Array.Copy(subBuf, i * 96, tmpSub, 0, 96); DetectRwPackets(tmpSub, out bool cdgPacket, out bool cdegPacket, out bool cdmidiPacket); if(cdgPacket) cdg = true; if(cdegPacket) cdeg = true; if(cdmidiPacket) cdmidi = true; } if(cdeg) { mediaType = MediaType.CDEG; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_enhanced_graphics_RW_packet); return; } if(cdg) { mediaType = MediaType.CDG; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_graphics_RW_packet); return; } if(cdmidi) { mediaType = MediaType.CDMIDI; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_MIDI_RW_packet); return; } } } // If it has a PS2 boot area it can still be PS1, so check for SYSTEM.CNF below hasPs2CdBoot: // Check if ISO9660 sense = dev.Read12(out byte[] isoSector, out _, 0, false, false, false, false, 16, 2048, 0, 1, false, dev.Timeout, out _); // Sector 16 reads, and contains "CD001" magic? if(sense || isoSector[1] != 0x43 || isoSector[2] != 0x44 || isoSector[3] != 0x30 || isoSector[4] != 0x30 || isoSector[5] != 0x31) return; // From sectors 16 to 31 uint isoSectorPosition = 16; while(isoSectorPosition < 32) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, isoSectorPosition, 2048, 0, 1, false, dev.Timeout, out _); // If sector cannot be read, break here if(sense) break; // If sector does not contain "CD001" magic, break if(isoSector[1] != 0x43 || isoSector[2] != 0x44 || isoSector[3] != 0x30 || isoSector[4] != 0x30 || isoSector[5] != 0x31) break; // If it is PVD or end of descriptor chain, break if(isoSector[0] == 1 || isoSector[0] == 255) break; isoSectorPosition++; } // If it's not an ISO9660 PVD, return if(isoSector[0] != 1 || isoSector[1] != 0x43 || isoSector[2] != 0x44 || isoSector[3] != 0x30 || isoSector[4] != 0x30 || isoSector[5] != 0x31) return; var rootStart = BitConverter.ToUInt32(isoSector, 158); var rootLength = BitConverter.ToUInt32(isoSector, 166); if(rootStart == 0 || rootLength == 0) return; rootLength /= 2048; try { using var rootMs = new MemoryStream(); for(uint i = 0; i < rootLength; i++) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, rootStart + i, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; rootMs.Write(isoSector, 0, 2048); } isoSector = rootMs.ToArray(); } catch(Exception ex) { SentrySdk.CaptureException(ex); return; } if(isoSector.Length < 2048) return; List rootEntries = []; uint ngcdIplStart = 0; uint ngcdIplLength = 0; uint vcdStart = 0; uint vcdLength = 0; uint pcdStart = 0; uint pcdLength = 0; uint ps1Start = 0; uint ps1Length = 0; for(var ri = 0; ri < rootLength; ri++) { int rootPos = ri * 2048; while(rootPos < isoSector.Length && isoSector[rootPos] > 0 && rootPos + isoSector[rootPos] <= isoSector.Length) { int nameLen = isoSector[rootPos + 32]; var tmpName = new byte[nameLen]; Array.Copy(isoSector, rootPos + 33, tmpName, 0, nameLen); string name = StringHandlers.CToString(tmpName).ToUpperInvariant(); if(name.EndsWith(";1", StringComparison.InvariantCulture)) name = name[..^2]; rootEntries.Add(name); switch(name) { case "IPL.TXT": ngcdIplStart = BitConverter.ToUInt32(isoSector, rootPos + 2); ngcdIplLength = BitConverter.ToUInt32(isoSector, rootPos + 10); break; case "VCD" when (isoSector[rootPos + 25] & 0x02) == 0x02: case "SVCD" when (isoSector[rootPos + 25] & 0x02) == 0x02: case "HQVCD" when (isoSector[rootPos + 25] & 0x02) == 0x02: vcdStart = BitConverter.ToUInt32(isoSector, rootPos + 2); vcdLength = BitConverter.ToUInt32(isoSector, rootPos + 10) / 2048; break; case "PHOTO_CD" when (isoSector[rootPos + 25] & 0x02) == 0x02: pcdStart = BitConverter.ToUInt32(isoSector, rootPos + 2); pcdLength = BitConverter.ToUInt32(isoSector, rootPos + 10) / 2048; break; case "SYSTEM.CNF": ps1Start = BitConverter.ToUInt32(isoSector, rootPos + 2); ps1Length = BitConverter.ToUInt32(isoSector, rootPos + 10); break; } rootPos += isoSector[rootPos]; } } if(rootEntries.Count == 0) return; if(rootEntries.Contains("CD32.TM")) { mediaType = MediaType.CD32; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_CD32_TM_file_in_root); return; } if(rootEntries.Contains("CDTV.TM")) { mediaType = MediaType.CDTV; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_CDTV_TM_file_in_root); return; } // "IPL.TXT" length if(ngcdIplLength > 0) { uint ngcdSectors = ngcdIplLength / 2048; if(ngcdIplLength % 2048 > 0) ngcdSectors++; string iplTxt; // Read "IPL.TXT" try { using var ngcdMs = new MemoryStream(); for(uint i = 0; i < ngcdSectors; i++) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, ngcdIplStart + i, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; ngcdMs.Write(isoSector, 0, 2048); } isoSector = ngcdMs.ToArray(); iplTxt = Encoding.ASCII.GetString(isoSector); } catch(Exception ex) { SentrySdk.CaptureException(ex); iplTxt = null; } // Check "IPL.TXT" lines if(iplTxt != null) { using var sr = new StringReader(iplTxt); var correctNeoGeoCd = true; var lineNumber = 0; while(sr.Peek() > 0) { string line = sr.ReadLine(); // End of file if(line is null || line.Length == 0) { if(lineNumber == 0) correctNeoGeoCd = false; break; } // Split line by comma string[] split = line.Split(','); // Empty line if(split.Length == 0) continue; // More than 3 entries if(split.Length != 3) { if(line[0] < 0x20) break; correctNeoGeoCd = false; break; } // Split filename string[] split2 = split[0].Split('.'); // Filename must have two parts, name and extension if(split2.Length != 2) { correctNeoGeoCd = false; break; } // Name must be smaller or equal to 8 characters if(split2[0].Length > 8) { correctNeoGeoCd = false; break; } // Extension must be smaller or equal to 8 characters if(split2[1].Length > 3) { correctNeoGeoCd = false; break; } // Second part must be a single digit if(split[1].Length != 1 || !byte.TryParse(split[1], out _)) { correctNeoGeoCd = false; break; } // Third part must be bigger or equal to 1 and smaller or equal to 8 if(split[2].Length is < 1 or > 8) { correctNeoGeoCd = false; break; } try { _ = Convert.ToUInt32(split[2], 16); } catch(Exception ex) { SentrySdk.CaptureException(ex); correctNeoGeoCd = false; break; } lineNumber++; } if(correctNeoGeoCd) { mediaType = MediaType.NeoGeoCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_correct_IPL_TXT_file_in_root); return; } } } if(vcdLength > 0) { try { using var vcdMs = new MemoryStream(); for(uint i = 0; i < vcdLength; i++) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, vcdStart + i, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; vcdMs.Write(isoSector, 0, 2048); } isoSector = vcdMs.ToArray(); } catch(Exception ex) { SentrySdk.CaptureException(ex); return; } if(isoSector.Length < 2048) return; uint infoPos = 0; for(var vi = 0; vi < vcdLength; vi++) { int vcdPos = vi * 2048; while(vcdPos < isoSector.Length && isoSector[vcdPos] > 0 && vcdPos + isoSector[vcdPos] <= isoSector.Length) { int nameLen = isoSector[vcdPos + 32]; var tmpName = new byte[nameLen]; Array.Copy(isoSector, vcdPos + 33, tmpName, 0, nameLen); string name = StringHandlers.CToString(tmpName).ToUpperInvariant(); if(name.EndsWith(";1", StringComparison.InvariantCulture)) name = name[..^2]; if(name is "INFO.VCD" or "INFO.SVD") { infoPos = BitConverter.ToUInt32(isoSector, vcdPos + 2); break; } vcdPos += isoSector[vcdPos]; } } if(infoPos > 0) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, infoPos, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; var systemId = new byte[8]; Array.Copy(isoSector, 0, systemId, 0, 8); string id = StringHandlers.CToString(systemId).TrimEnd(); switch(id) { case "VIDEO_CD": mediaType = MediaType.VCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Video_CD_description_file); return; case "SUPERVCD": mediaType = MediaType.SVCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Super_Video_CD_description_file); break; case "HQ-VCD": mediaType = MediaType.CVD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_China_Video_Disc_description_file); break; } } } if(pcdLength > 0) { try { using var pcdMs = new MemoryStream(); for(uint i = 0; i < pcdLength; i++) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, pcdStart + i, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; pcdMs.Write(isoSector, 0, 2048); } isoSector = pcdMs.ToArray(); } catch(Exception ex) { SentrySdk.CaptureException(ex); return; } if(isoSector.Length < 2048) return; uint infoPos = 0; for(var pi = 0; pi < pcdLength; pi++) { int pcdPos = pi * 2048; while(pcdPos < isoSector.Length && isoSector[pcdPos] > 0 && pcdPos + isoSector[pcdPos] <= isoSector.Length) { int nameLen = isoSector[pcdPos + 32]; var tmpName = new byte[nameLen]; Array.Copy(isoSector, pcdPos + 33, tmpName, 0, nameLen); string name = StringHandlers.CToString(tmpName).ToUpperInvariant(); if(name.EndsWith(";1", StringComparison.InvariantCulture)) name = name[..^2]; if(name == "INFO.PCD") { infoPos = BitConverter.ToUInt32(isoSector, pcdPos + 2); break; } pcdPos += isoSector[pcdPos]; } } if(infoPos > 0) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, infoPos, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; var systemId = new byte[8]; Array.Copy(isoSector, 0, systemId, 0, 8); string id = StringHandlers.CToString(systemId).TrimEnd(); switch(id) { case "PHOTO_CD": mediaType = MediaType.PCD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Photo_CD_description_file); return; } } } // "SYSTEM.CNF" length if(ps1Length > 0) { uint ps1Sectors = ps1Length / 2048; if(ps1Length % 2048 > 0) ps1Sectors++; string ps1Txt; // Read "SYSTEM.CNF" try { using var ps1Ms = new MemoryStream(); for(uint i = 0; i < ps1Sectors; i++) { sense = dev.Read12(out isoSector, out _, 0, false, false, false, false, ps1Start + i, 2048, 0, 1, false, dev.Timeout, out _); if(sense) break; ps1Ms.Write(isoSector, 0, 2048); } isoSector = ps1Ms.ToArray(); ps1Txt = Encoding.ASCII.GetString(isoSector); } catch(Exception ex) { SentrySdk.CaptureException(ex); ps1Txt = null; } // Check "SYSTEM.CNF" lines if(ps1Txt != null) { using var sr = new StringReader(ps1Txt); string ps1BootFile = null; string ps2BootFile = null; while(sr.Peek() > 0) { string line = sr.ReadLine(); // End of file if(line is null || line.Length == 0) break; line = line.Replace(" ", ""); if(line.StartsWith("BOOT=cdrom:", StringComparison.InvariantCultureIgnoreCase)) { ps1BootFile = line[11..]; if(ps1BootFile.StartsWith('\\')) ps1BootFile = ps1BootFile[1..]; if(ps1BootFile.EndsWith(";1", StringComparison.InvariantCultureIgnoreCase)) ps1BootFile = ps1BootFile[..^2]; break; } if(!line.StartsWith("BOOT2=cdrom0:", StringComparison.InvariantCultureIgnoreCase)) continue; ps2BootFile = line[13..]; if(ps2BootFile.StartsWith('\\')) ps2BootFile = ps2BootFile[1..]; if(ps2BootFile.EndsWith(";1", StringComparison.InvariantCultureIgnoreCase)) ps2BootFile = ps2BootFile[..^2]; break; } if(ps1BootFile != null && rootEntries.Contains(ps1BootFile.ToUpperInvariant())) { mediaType = MediaType.PS1CD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_correct_SYSTEM_CNF_file_in_root_PS1); } if(ps2BootFile != null && rootEntries.Contains(ps2BootFile.ToUpperInvariant())) { mediaType = MediaType.PS2CD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_correct_SYSTEM_CNF_file_in_root_PS2); } } } break; } // TODO: Check for CD-i Ready case MediaType.CDI: break; case MediaType.DVDROM: case MediaType.HDDVDROM: case MediaType.BDROM: case MediaType.UHDBD: case MediaType.Unknown: // TODO: Nuon requires reading the filesystem, searching for a file called "/NUON/NUON.RUN" if(ps2BootSectors is { Length: 0x6000 }) { // The decryption key is applied as XOR. As first byte is originally always NULL, it gives us the key :) byte decryptByte = ps2BootSectors[0]; for(var i = 0; i < 0x6000; i++) ps2BootSectors[i] ^= decryptByte; string ps2BootSectorsHash = Sha256Context.Data(ps2BootSectors, out _); AaruLogging.Debug(MODULE_NAME, Localization.Core.PlayStation_2_boot_sectors_SHA256_0, ps2BootSectorsHash); if(ps2BootSectorsHash is PS2_PAL_HASH or PS2_NTSC_HASH or PS2_JAPANESE_HASH) { AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_2_boot_sectors_DVD); mediaType = MediaType.PS2DVD; } } if(sector1 != null) { var tmp = new byte[_ps3Id.Length]; Array.Copy(sector1, 0, tmp, 0, tmp.Length); if(tmp.SequenceEqual(_ps3Id)) { switch(mediaType) { case MediaType.BDROM: AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_3_boot_sectors); mediaType = MediaType.PS3BD; break; case MediaType.DVDROM: AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_3_boot_sectors_DVD); mediaType = MediaType.PS3DVD; break; } } tmp = new byte[_ps4Id.Length]; Array.Copy(sector1, 512, tmp, 0, tmp.Length); if(tmp.SequenceEqual(_ps4Id) && mediaType == MediaType.BDROM) { mediaType = MediaType.PS4BD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_4_boot_sectors); } } if(blurayDi is { Units.Length: > 0 } && blurayDi?.Units[0].DiscTypeIdentifier != null) { string blurayType = StringHandlers.CToString(blurayDi?.Units[0].DiscTypeIdentifier); switch(blurayType) { case "BDR": AaruLogging.Debug(MODULE_NAME, Localization.Core.Blu_ray_type_BDR_setting_disc_type_BDR); mediaType = MediaType.BDR; break; case "XG4": AaruLogging.Debug(MODULE_NAME, Localization.Core.Blu_ray_type_XG4_setting_disc_type_XGD4); mediaType = MediaType.XGD4; break; // TODO: PS5 case "BDU": if(sector1 != null) { var tmp = new byte[_ps5Id.Length]; Array.Copy(sector1, 1024, tmp, 0, tmp.Length); if(tmp.SequenceEqual(_ps5Id)) { mediaType = MediaType.PS5BD; AaruLogging.Debug(MODULE_NAME, Localization.Core.Found_Sony_PlayStation_5_boot_sectors); break; } } AaruLogging.Debug(MODULE_NAME, Localization.Core.Blu_ray_type_BDU_setting_disc_type_UHD); mediaType = MediaType.UHDBD; break; } } break; } } static void DetectRwPackets(byte[] subchannel, out bool cdgPacket, out bool cdegPacket, out bool cdmidiPacket) { cdgPacket = false; cdegPacket = false; cdmidiPacket = false; var cdSubRwPack1 = new byte[24]; var cdSubRwPack2 = new byte[24]; var cdSubRwPack3 = new byte[24]; var cdSubRwPack4 = new byte[24]; var i = 0; for(var j = 0; j < 24; j++) cdSubRwPack1[j] = (byte)(subchannel[i++] & 0x3F); for(var j = 0; j < 24; j++) cdSubRwPack2[j] = (byte)(subchannel[i++] & 0x3F); for(var j = 0; j < 24; j++) cdSubRwPack3[j] = (byte)(subchannel[i++] & 0x3F); for(var j = 0; j < 24; j++) cdSubRwPack4[j] = (byte)(subchannel[i++] & 0x3F); switch(cdSubRwPack1[0]) { case 0x08: case 0x09: cdgPacket = true; break; case 0x0A: cdegPacket = true; break; case 0x38: cdmidiPacket = true; break; } switch(cdSubRwPack2[0]) { case 0x08: case 0x09: cdgPacket = true; break; case 0x0A: cdegPacket = true; break; case 0x38: cdmidiPacket = true; break; } switch(cdSubRwPack3[0]) { case 0x08: case 0x09: cdgPacket = true; break; case 0x0A: cdegPacket = true; break; case 0x38: cdmidiPacket = true; break; } switch(cdSubRwPack4[0]) { case 0x08: case 0x09: cdgPacket = true; break; case 0x0A: cdegPacket = true; break; case 0x38: cdmidiPacket = true; break; } } }