Files
Aaru/Aaru.Core/Sidecar/OpticalDisc.cs

746 lines
30 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : OpticalDisc.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Core algorithms.
//
// --[ Description ] ----------------------------------------------------------
//
// Contains logic to create sidecar from an optical media dump.
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2025 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.AaruMetadata;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Decoders.CD;
using Aaru.Decoders.DVD;
using Sentry;
using DMI = Aaru.Decoders.Xbox.DMI;
using Dump = Aaru.Core.Devices.Dumping.Dump;
using Partition = Aaru.CommonTypes.Partition;
using Session = Aaru.CommonTypes.Structs.Session;
using Track = Aaru.CommonTypes.Structs.Track;
using TrackType = Aaru.CommonTypes.Enums.TrackType;
namespace Aaru.Core;
public sealed partial class Sidecar
{
/// <summary>Creates a metadata sidecar for an optical disc (e.g. CD, DVD, GD, BD, XGD, GOD)</summary>
/// <param name="image">Image</param>
/// <param name="filterId">Filter uuid</param>
/// <param name="imagePath">Image path</param>
/// <param name="fi">Image file information</param>
/// <param name="plugins">Image plugins</param>
/// <param name="imgChecksums">List of image checksums</param>
/// <param name="sidecar">Metadata sidecar</param>
/// <param name="encoding">Encoding to be used for filesystem plugins</param>
void OpticalDisc(IOpticalMediaImage image, Guid filterId, string imagePath, FileInfo fi, PluginRegister plugins,
List<CommonTypes.AaruMetadata.Checksum> imgChecksums, ref Metadata sidecar, Encoding encoding)
{
if(_aborted) return;
sidecar.OpticalDiscs =
[
new OpticalDisc
{
Checksums = imgChecksums,
Image = new Image
{
Format = image.Format,
Offset = 0,
Value = Path.GetFileName(imagePath)
},
Size = (ulong)fi.Length,
Sequence = new Sequence
{
Title = image.Info.MediaTitle
}
}
];
if(image.Info.MediaSequence != 0 && image.Info.LastMediaSequence != 0)
{
sidecar.OpticalDiscs[0].Sequence.MediaSequence = (uint)image.Info.MediaSequence;
sidecar.OpticalDiscs[0].Sequence.TotalMedia = (uint)image.Info.LastMediaSequence;
}
else
{
sidecar.OpticalDiscs[0].Sequence.MediaSequence = 1;
sidecar.OpticalDiscs[0].Sequence.TotalMedia = 1;
}
MediaType dskType = image.Info.MediaType;
ErrorNumber errno;
UpdateStatus(Localization.Core.Hashing_media_tags);
foreach(MediaTagType tagType in image.Info.ReadableMediaTags)
{
if(_aborted) return;
errno = image.ReadMediaTag(tagType, out byte[] tag);
if(errno != ErrorNumber.NoError) continue;
Dump.AddMediaTagToSidecar(imagePath, tagType, tag, ref sidecar);
switch(tagType)
{
case MediaTagType.CD_ATIP:
ATIP.CDATIP atip = ATIP.Decode(tag);
if(atip != null)
{
if(atip.DDCD)
dskType = atip.DiscType ? MediaType.DDCDRW : MediaType.DDCDR;
else
dskType = atip.DiscType ? MediaType.CDRW : MediaType.CDR;
}
break;
case MediaTagType.DVD_DMI:
if(DMI.IsXbox(tag))
{
dskType = MediaType.XGD;
sidecar.OpticalDiscs[0].Dimensions = new Dimensions
{
Diameter = 120,
Thickness = 1.2
};
}
else if(DMI.IsXbox360(tag))
{
dskType = MediaType.XGD2;
sidecar.OpticalDiscs[0].Dimensions = new Dimensions
{
Diameter = 120,
Thickness = 1.2
};
}
break;
case MediaTagType.DVD_PFI:
PFI.PhysicalFormatInformation? pfi = PFI.Decode(tag, dskType);
if(pfi.HasValue)
{
if(dskType != MediaType.XGD &&
dskType != MediaType.XGD2 &&
dskType != MediaType.XGD3 &&
dskType != MediaType.PS2DVD &&
dskType != MediaType.PS3DVD &&
dskType != MediaType.Nuon)
{
dskType = pfi.Value.DiskCategory switch
{
DiskCategory.DVDPR => MediaType.DVDPR,
DiskCategory.DVDPRDL => MediaType.DVDPRDL,
DiskCategory.DVDPRW => MediaType.DVDPRW,
DiskCategory.DVDPRWDL => MediaType.DVDPRWDL,
DiskCategory.DVDR => MediaType.DVDR,
DiskCategory.DVDRAM => MediaType.DVDRAM,
DiskCategory.DVDROM => MediaType.DVDROM,
DiskCategory.DVDRW => MediaType.DVDRW,
DiskCategory.HDDVDR => MediaType.HDDVDR,
DiskCategory.HDDVDRAM => MediaType.HDDVDRAM,
DiskCategory.HDDVDROM => MediaType.HDDVDROM,
DiskCategory.HDDVDRW => MediaType.HDDVDRW,
DiskCategory.Nintendo => MediaType.GOD,
DiskCategory.UMD => MediaType.UMD,
_ => dskType
};
if(dskType == MediaType.DVDR && pfi.Value.PartVersion >= 6) dskType = MediaType.DVDRDL;
if(dskType == MediaType.DVDRW && pfi.Value.PartVersion >= 15) dskType = MediaType.DVDRWDL;
if(dskType == MediaType.GOD && pfi.Value.DiscSize == DVDSize.OneTwenty)
dskType = MediaType.WOD;
sidecar.OpticalDiscs[0].Dimensions = new Dimensions();
if(dskType == MediaType.UMD)
{
sidecar.OpticalDiscs[0].Dimensions.Height = 64;
sidecar.OpticalDiscs[0].Dimensions.Width = 63;
sidecar.OpticalDiscs[0].Dimensions.Thickness = 4;
}
else
{
switch(pfi.Value.DiscSize)
{
case DVDSize.Eighty:
sidecar.OpticalDiscs[0].Dimensions.Diameter = 80;
sidecar.OpticalDiscs[0].Dimensions.Thickness = 1.2;
break;
case DVDSize.OneTwenty:
sidecar.OpticalDiscs[0].Dimensions.Diameter = 120;
sidecar.OpticalDiscs[0].Dimensions.Thickness = 1.2;
break;
}
}
}
}
break;
}
}
try
{
List<Session> sessions = image.Sessions;
sidecar.OpticalDiscs[0].Sessions = (uint)(sessions?.Count ?? 1);
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
sidecar.OpticalDiscs[0].Sessions = 1;
}
List<Track> tracks = image.Tracks;
List<CommonTypes.AaruMetadata.Track> trksLst = null;
if(tracks != null)
{
sidecar.OpticalDiscs[0].Tracks = new uint[1];
sidecar.OpticalDiscs[0].Tracks[0] = (uint)tracks.Count;
trksLst = [];
}
if(sidecar.OpticalDiscs[0].Dimensions == null && image.Info.MediaType != MediaType.Unknown)
sidecar.OpticalDiscs[0].Dimensions = Dimensions.FromMediaType(image.Info.MediaType);
if(_aborted) return;
InitProgress();
UpdateStatus(Localization.Core.Checking_filesystems);
List<Partition> partitions = Partitions.GetAll(image);
Partitions.AddSchemesToStats(partitions);
UpdateStatus(Localization.Core.Hashing_tracks);
foreach(Track trk in tracks)
{
if(_aborted)
{
EndProgress();
return;
}
var xmlTrk = new CommonTypes.AaruMetadata.Track();
xmlTrk.Type = trk.Type switch
{
TrackType.Audio => CommonTypes.AaruMetadata.TrackType.Audio,
TrackType.CdMode2Form2 => CommonTypes.AaruMetadata.TrackType.Mode2Form2,
TrackType.CdMode2Formless => CommonTypes.AaruMetadata.TrackType.Mode2,
TrackType.CdMode2Form1 => CommonTypes.AaruMetadata.TrackType.Mode2Form1,
TrackType.CdMode1 => CommonTypes.AaruMetadata.TrackType.Mode1,
TrackType.Data => sidecar.OpticalDiscs[0].DiscType switch
{
"BD" => CommonTypes.AaruMetadata.TrackType.Bluray,
"DDCD" => CommonTypes.AaruMetadata.TrackType.Ddcd,
"DVD" => CommonTypes.AaruMetadata.TrackType.Dvd,
"HD DVD" => CommonTypes.AaruMetadata.TrackType.HdDvd,
_ => CommonTypes.AaruMetadata.TrackType.Mode1
},
_ => xmlTrk.Type
};
xmlTrk.Sequence = new TrackSequence
{
Session = trk.Session,
Number = trk.Sequence
};
xmlTrk.StartSector = trk.StartSector;
xmlTrk.EndSector = trk.EndSector;
if(trk.Indexes?.TryGetValue(0, out int idx0) == true && idx0 >= 0) xmlTrk.StartSector = (ulong)idx0;
switch(sidecar.OpticalDiscs[0].DiscType)
{
case "CD":
case "GD":
xmlTrk.StartMsf = LbaToMsf((long)xmlTrk.StartSector);
xmlTrk.EndMsf = LbaToMsf((long)xmlTrk.EndSector);
break;
case "DDCD":
xmlTrk.StartMsf = DdcdLbaToMsf((long)xmlTrk.StartSector);
xmlTrk.EndMsf = DdcdLbaToMsf((long)xmlTrk.EndSector);
break;
}
xmlTrk.Image = new Image
{
Value = Path.GetFileName(trk.File),
Format = trk.FileType
};
if(trk.FileOffset > 0) xmlTrk.Image.Offset = trk.FileOffset;
xmlTrk.Size = (xmlTrk.EndSector - xmlTrk.StartSector + 1) * (ulong)trk.RawBytesPerSector;
xmlTrk.BytesPerSector = (uint)trk.BytesPerSector;
const uint sectorsToRead = 512;
ulong sectors = xmlTrk.EndSector - xmlTrk.StartSector + 1;
ulong doneSectors = 0;
// If there is only one track, and it's the same as the image file (e.g. ".iso" files), don't re-checksum.
if(image.Id == new Guid("12345678-AAAA-BBBB-CCCC-123456789000") &&
// Only if filter is none...
(filterId == new Guid("12345678-AAAA-BBBB-CCCC-123456789000") ||
// ...or AppleDouble
filterId == new Guid("1b2165ee-c9df-4b21-bbbb-9e5892b2df4d")))
xmlTrk.Checksums = sidecar.OpticalDiscs[0].Checksums;
else
{
UpdateProgress(Localization.Core.Track_0_of_1, trk.Sequence, tracks.Count);
// For fast debugging, skip checksum
//goto skipChecksum;
var trkChkWorker = new Checksum();
InitProgress2();
while(doneSectors < sectors)
{
if(_aborted)
{
EndProgress();
EndProgress2();
return;
}
byte[] sector;
if(sectors - doneSectors >= sectorsToRead)
{
errno = image.ReadSectorsLong(doneSectors, sectorsToRead, xmlTrk.Sequence.Number, out sector);
UpdateProgress2(Localization.Core.Hashing_sector_0_of_1,
(long)doneSectors,
(long)(trk.EndSector - trk.StartSector + 1));
if(errno != ErrorNumber.NoError)
{
UpdateStatus(string.Format(Localization.Core.Error_0_reading_sector_1, errno, doneSectors));
EndProgress2();
return;
}
doneSectors += sectorsToRead;
}
else
{
errno = image.ReadSectorsLong(doneSectors,
(uint)(sectors - doneSectors),
xmlTrk.Sequence.Number,
out sector);
UpdateProgress2(Localization.Core.Hashing_sector_0_of_1,
(long)doneSectors,
(long)(trk.EndSector - trk.StartSector + 1));
if(errno != ErrorNumber.NoError)
{
UpdateStatus(string.Format(Localization.Core.Error_0_reading_sector_1, errno, doneSectors));
EndProgress2();
return;
}
doneSectors += sectors - doneSectors;
}
trkChkWorker.Update(sector);
}
xmlTrk.Checksums = trkChkWorker.End();
EndProgress2();
}
if(trk.SubchannelType != TrackSubchannelType.None)
{
xmlTrk.SubChannel = new SubChannel
{
Image = new Image
{
Value = trk.SubchannelFile
},
// TODO: Packed subchannel has different size?
Size = (xmlTrk.EndSector - xmlTrk.StartSector + 1) * 96
};
xmlTrk.SubChannel.Image.Format = trk.SubchannelType switch
{
TrackSubchannelType.Packed
or TrackSubchannelType.PackedInterleaved => "rw",
TrackSubchannelType.Raw
or TrackSubchannelType.RawInterleaved => "rw_raw",
TrackSubchannelType.Q16
or TrackSubchannelType.Q16Interleaved => "q16",
_ => xmlTrk.SubChannel.Image.Format
};
if(trk.FileOffset > 0) xmlTrk.SubChannel.Image.Offset = trk.SubchannelOffset;
var subChkWorker = new Checksum();
sectors = xmlTrk.EndSector - xmlTrk.StartSector + 1;
doneSectors = 0;
InitProgress2();
while(doneSectors < sectors)
{
if(_aborted)
{
EndProgress();
EndProgress2();
return;
}
byte[] sector;
if(sectors - doneSectors >= sectorsToRead)
{
errno = image.ReadSectorsTag(doneSectors,
sectorsToRead,
xmlTrk.Sequence.Number,
SectorTagType.CdSectorSubchannel,
out sector);
UpdateProgress2(Localization.Core.Hashing_subchannel_sector_0_of_1,
(long)doneSectors,
(long)(trk.EndSector - trk.StartSector + 1));
if(errno != ErrorNumber.NoError)
{
UpdateStatus(string.Format(Localization.Core.Error_0_reading_sector_1, errno, doneSectors));
EndProgress2();
return;
}
doneSectors += sectorsToRead;
}
else
{
errno = image.ReadSectorsTag(doneSectors,
(uint)(sectors - doneSectors),
xmlTrk.Sequence.Number,
SectorTagType.CdSectorSubchannel,
out sector);
UpdateProgress2(Localization.Core.Hashing_subchannel_sector_0_of_1,
(long)doneSectors,
(long)(trk.EndSector - trk.StartSector + 1));
if(errno != ErrorNumber.NoError)
{
UpdateStatus(string.Format(Localization.Core.Error_0_reading_sector_1, errno, doneSectors));
EndProgress2();
return;
}
doneSectors += sectors - doneSectors;
}
subChkWorker.Update(sector);
}
xmlTrk.SubChannel.Checksums = subChkWorker.End();
EndProgress2();
}
// For fast debugging, skip checksum
//skipChecksum:
var trkPartitions = partitions.Where(p => p.Start >= trk.StartSector && p.End <= trk.EndSector).ToList();
xmlTrk.FileSystemInformation = [];
if(trkPartitions.Count > 0)
{
foreach(Partition partition in trkPartitions)
{
var metadataPartition = new CommonTypes.AaruMetadata.Partition
{
Description = partition.Description,
EndSector = partition.End,
Name = partition.Name,
Sequence = (uint)partition.Sequence,
StartSector = partition.Start,
Type = partition.Type
};
List<FileSystem> lstFs = [];
foreach(IFilesystem fs in plugins.Filesystems.Values)
{
try
{
if(_aborted)
{
EndProgress();
return;
}
if(fs is null) continue;
if(!fs.Identify(image, partition)) continue;
FileSystem fsMetadata = new();
if(fs is IReadOnlyFilesystem rofs &&
rofs.Mount(image, partition, encoding, null, null) == ErrorNumber.NoError)
{
UpdateStatus(string.Format(Localization.Core.Mounting_0, rofs.Metadata.Type));
rofs.Metadata.Contents = Files(rofs);
lstFs.Add(rofs.Metadata);
Statistics.AddFilesystem(rofs.Metadata.Type);
rofs.Unmount();
}
else
{
fs.GetInformation(image, partition, encoding, out _, out fsMetadata);
lstFs.Add(fsMetadata);
Statistics.AddFilesystem(fsMetadata.Type);
}
dskType = fsMetadata.Type switch
{
"Opera" => MediaType.ThreeDO,
"PC Engine filesystem" => MediaType.SuperCDROM2,
"Nintendo Wii filesystem" => MediaType.WOD,
"Nintendo Gamecube filesystem" => MediaType.GOD,
_ => dskType
};
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
}
}
if(lstFs.Count > 0) metadataPartition.FileSystems = lstFs;
xmlTrk.FileSystemInformation.Add(metadataPartition);
}
}
else
{
var metadataPartition = new CommonTypes.AaruMetadata.Partition
{
EndSector = xmlTrk.EndSector,
StartSector = xmlTrk.StartSector
};
List<FileSystem> lstFs = [];
var xmlPart = new Partition
{
Start = xmlTrk.StartSector,
Length = xmlTrk.EndSector - xmlTrk.StartSector + 1,
Type = xmlTrk.Type.ToString(),
Size = xmlTrk.Size,
Sequence = xmlTrk.Sequence.Number
};
foreach(IFilesystem fs in plugins.Filesystems.Values)
{
try
{
if(_aborted)
{
EndProgress();
return;
}
if(fs is null) continue;
if(!fs.Identify(image, xmlPart)) continue;
FileSystem fsMetadata = new();
if(fs is IReadOnlyFilesystem rofs &&
rofs.Mount(image, xmlPart, encoding, null, null) == ErrorNumber.NoError)
{
UpdateStatus(string.Format(Localization.Core.Mounting_0, rofs.Metadata.Type));
rofs.Metadata.Contents = Files(rofs);
lstFs.Add(rofs.Metadata);
Statistics.AddFilesystem(rofs.Metadata.Type);
rofs.Unmount();
}
else
{
fs.GetInformation(image, xmlPart, encoding, out _, out fsMetadata);
lstFs.Add(fsMetadata);
Statistics.AddFilesystem(fsMetadata.Type);
}
dskType = fsMetadata.Type switch
{
"Opera" => MediaType.ThreeDO,
"PC Engine filesystem" => MediaType.SuperCDROM2,
"Nintendo Wii filesystem" => MediaType.WOD,
"Nintendo Gamecube filesystem" => MediaType.GOD,
_ => dskType
};
}
catch(Exception ex)
{
SentrySdk.CaptureException(ex);
}
}
if(lstFs.Count > 0) metadataPartition.FileSystems = lstFs;
xmlTrk.FileSystemInformation.Add(metadataPartition);
}
errno = image.ReadSectorTag(trk.Sequence, SectorTagType.CdTrackIsrc, out byte[] isrcData);
if(errno == ErrorNumber.NoError) xmlTrk.ISRC = Encoding.UTF8.GetString(isrcData);
errno = image.ReadSectorTag(trk.Sequence, SectorTagType.CdTrackFlags, out byte[] flagsData);
if(errno == ErrorNumber.NoError)
{
var trackFlags = (CdFlags)flagsData[0];
xmlTrk.Flags = new TrackFlags
{
PreEmphasis = trackFlags.HasFlag(CdFlags.PreEmphasis),
CopyPermitted = trackFlags.HasFlag(CdFlags.CopyPermitted),
Data = trackFlags.HasFlag(CdFlags.DataTrack),
Quadraphonic = trackFlags.HasFlag(CdFlags.FourChannel)
};
}
if(trk.Indexes?.Count > 0)
{
xmlTrk.Indexes = trk.Indexes?.OrderBy(i => i.Key)
.Select(i => new TrackIndex
{
Index = i.Key,
Value = i.Value
})
.ToList();
}
trksLst.Add(xmlTrk);
}
EndProgress();
if(trksLst != null) sidecar.OpticalDiscs[0].Track = trksLst;
// All XGD3 all have the same number of blocks
if(dskType == MediaType.XGD2 && sidecar.OpticalDiscs[0].Track.Count == 1)
{
ulong blocks = sidecar.OpticalDiscs[0].Track[0].EndSector -
sidecar.OpticalDiscs[0].Track[0].StartSector +
1;
if(blocks is 25063 or 4229664 or 4246304) // Wxripper unlock
dskType = MediaType.XGD3;
}
(string type, string subType) discType = CommonTypes.Metadata.MediaType.MediaTypeToString(dskType);
sidecar.OpticalDiscs[0].DiscType = discType.type;
sidecar.OpticalDiscs[0].DiscSubType = discType.subType;
Statistics.AddMedia(dskType, false);
if(image.DumpHardware != null)
sidecar.OpticalDiscs[0].DumpHardware = image.DumpHardware;
else if(!string.IsNullOrEmpty(image.Info.DriveManufacturer) ||
!string.IsNullOrEmpty(image.Info.DriveModel) ||
!string.IsNullOrEmpty(image.Info.DriveFirmwareRevision) ||
!string.IsNullOrEmpty(image.Info.DriveSerialNumber))
{
sidecar.OpticalDiscs[0].DumpHardware =
[
new DumpHardware
{
Extents =
[
new Extent
{
Start = 0,
End = image.Info.Sectors
}
],
Manufacturer = image.Info.DriveManufacturer,
Model = image.Info.DriveModel,
Firmware = image.Info.DriveFirmwareRevision,
Serial = image.Info.DriveSerialNumber,
Software = new Software
{
Name = image.Info.Application,
Version = image.Info.ApplicationVersion
}
}
];
}
}
}