diff --git a/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs b/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs
new file mode 100644
index 000000000..033c0d1f8
--- /dev/null
+++ b/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Structs;
+using Aaru.Filesystems;
+
+namespace Aaru.Core.Image;
+
+internal static class AacsBdExtentResolver
+{
+ internal readonly struct LbaRange
+ {
+ internal ulong Start { get; init; }
+ internal ulong End { get; init; }
+ }
+
+ ///
+ /// Resolves the stream file extents for a Blu-ray optical media image.
+ /// It searches for the stream file extents in the /BDMV/STREAM and
+ /// /BDMV/STREAM/SSIF directories.
+ ///
+ /// Input optical media image.
+ /// Plugin register.
+ /// Stream file extents.
+ /// Error message.
+ /// Error number.
+ internal static ErrorNumber ResolveStreamFileExtents(IOpticalMediaImage inputOptical,
+ PluginRegister plugins,
+ out List ranges,
+ out string error)
+ {
+ ranges = [];
+ error = null;
+
+ IReadOnlyFilesystem udfRo = null;
+ UDF udf = null;
+
+ foreach(IFilesystem fs in plugins.Filesystems.Values)
+ {
+ if(fs is not UDF udfPlugin) continue;
+
+ Partition wholeImage = new()
+ {
+ Start = 0,
+ Length = inputOptical.Info.Sectors,
+ Size = inputOptical.Info.Sectors * inputOptical.Info.SectorSize,
+ Sequence = 0,
+ Type = "UDF"
+ };
+
+ if(!udfPlugin.Identify(inputOptical, wholeImage)) continue;
+
+ ErrorNumber mountErr = udfPlugin.Mount(inputOptical, wholeImage, Encoding.UTF8, null, null);
+
+ if(mountErr != ErrorNumber.NoError)
+ {
+ error = "UDF mount failed while resolving Blu-ray stream extents.";
+
+ return mountErr;
+ }
+
+ udfRo = udfPlugin;
+ udf = udfPlugin;
+
+ break;
+ }
+
+ if(udfRo is null || udf is null)
+ {
+ error = "Could not mount UDF filesystem for Blu-ray stream extent discovery.";
+
+ return ErrorNumber.NotSupported;
+ }
+
+ try
+ {
+ ErrorNumber errno = AddDirectoryExtents(udfRo, udf, "/BDMV/STREAM", ".M2TS", ref ranges);
+
+ if(errno != ErrorNumber.NoError)
+ {
+ error = "Could not resolve extents for /BDMV/STREAM.";
+
+ return errno;
+ }
+
+ errno = AddDirectoryExtents(udfRo, udf, "/BDMV/STREAM/SSIF", ".SSIF", ref ranges);
+
+ if(errno != ErrorNumber.NoError && errno != ErrorNumber.NoSuchFile)
+ {
+ error = "Could not resolve extents for /BDMV/STREAM/SSIF.";
+
+ return errno;
+ }
+
+ if(ranges.Count == 0)
+ {
+ error = "No stream file extents found under /BDMV/STREAM.";
+
+ return ErrorNumber.NoData;
+ }
+
+ ranges.Sort(static (a, b) => a.Start.CompareTo(b.Start));
+ ranges = MergeRanges(ranges);
+
+ return ErrorNumber.NoError;
+ }
+ finally
+ {
+ udfRo.Unmount();
+ }
+ }
+
+ /// Checks if a given LBA is allowed for decryption.
+ /// Stream file extents.
+ /// LBA to check.
+ /// True if the LBA is allowed for decryption.
+ internal static bool IsLbaAllowed(List ranges, ulong lba)
+ {
+ int lo = 0;
+ int hi = ranges.Count - 1;
+
+ while(lo <= hi)
+ {
+ int mid = lo + (hi - lo) / 2;
+ LbaRange range = ranges[mid];
+
+ if(lba < range.Start)
+ hi = mid - 1;
+ else if(lba > range.End)
+ lo = mid + 1;
+ else
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /// Adds the extents for a given directory to the list of stream file extents.
+ /// Read-only filesystem.
+ /// UDF filesystem.
+ /// Directory path.
+ /// Extension of the files to add.
+ /// List of stream file extents.
+ /// Error number.
+ static ErrorNumber AddDirectoryExtents(IReadOnlyFilesystem roFs, UDF udf, string dirPath, string extension,
+ ref List ranges)
+ {
+ ErrorNumber errno = roFs.OpenDir(dirPath, out IDirNode dirNode);
+
+ if(errno != ErrorNumber.NoError) return errno;
+
+ try
+ {
+ while(true)
+ {
+ errno = roFs.ReadDir(dirNode, out string filename);
+
+ if(errno != ErrorNumber.NoError) return errno;
+
+ if(filename is null) break;
+
+ if(!filename.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) continue;
+
+ string fullPath = dirPath + "/" + filename;
+
+ errno = udf.GetFilePhysicalSectorExtents(fullPath, out List<(ulong startSector, uint sectorCount)> extents);
+
+ if(errno != ErrorNumber.NoError) return errno;
+
+ foreach((ulong startSector, uint sectorCount) extent in extents)
+ {
+ if(extent.sectorCount == 0) continue;
+
+ ranges.Add(new LbaRange
+ {
+ Start = extent.startSector,
+ End = extent.startSector + extent.sectorCount - 1
+ });
+ }
+ }
+ }
+ finally
+ {
+ roFs.CloseDir(dirNode);
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ /// Merges the extents for a given directory to the list of stream file extents.
+ /// List of stream file extents.
+ /// Merged list of stream file extents.
+ static List MergeRanges(List ranges)
+ {
+ if(ranges.Count <= 1) return ranges;
+
+ List merged = [];
+ LbaRange current = ranges[0];
+
+ for(int i = 1; i < ranges.Count; i++)
+ {
+ LbaRange next = ranges[i];
+
+ if(next.Start <= current.End + 1)
+ {
+ current = new LbaRange { Start = current.Start, End = Math.Max(current.End, next.End) };
+ continue;
+ }
+
+ merged.Add(current);
+ current = next;
+ }
+
+ merged.Add(current);
+
+ return merged;
+ }
+}
diff --git a/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs b/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs
new file mode 100644
index 000000000..eea731224
--- /dev/null
+++ b/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs
@@ -0,0 +1,472 @@
+using System;
+using System.Collections.Generic;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Structs;
+using Aaru.Decryption.Aacs;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+/// Blu-ray AACS sector pipeline for convert/merge: LBA-aligned 6144-byte CPS units with output staging.
+internal static class AacsBdOpticalPipeline
+{
+ internal readonly struct Ui
+ {
+ public Action OnInitProgress { get; init; }
+ public Action OnInitProgress2 { get; init; }
+ public Action OnEndProgress { get; init; }
+ public Action OnEndProgress2 { get; init; }
+ public Action OnTrackProgress { get; init; }
+ public Action OnSectorRangeProgress { get; init; }
+ public Action ErrorMessage { get; init; }
+ public Action StoppingErrorMessage { get; init; }
+ }
+
+ /// Runs the Blu-ray AACS sector pipeline.
+ /// Input optical media image.
+ /// Output optical media image.
+ /// Batch count.
+ /// Force flag.
+ /// Aborted flag.
+ /// Decrypted CPS unit keys.
+ /// Allow decrypt LBA function.
+ /// UI.
+ /// Error number.
+ internal static ErrorNumber Run(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, uint batchCount,
+ bool force, ref bool aborted, byte[][] decryptedCpsUnitKeys,
+ Func allowDecryptLba, Ui ui)
+ {
+ ui.OnInitProgress?.Invoke();
+ ui.OnInitProgress2?.Invoke();
+
+ int currentTrack = 0;
+
+ foreach(Track track in inputOptical.Tracks)
+ {
+ if(aborted) break;
+
+ ui.OnTrackProgress?.Invoke(currentTrack, inputOptical.Tracks.Count);
+
+ ulong doneSectors = 0;
+ ulong trackSectors = track.EndSector - track.StartSector + 1;
+
+ List pending = [];
+ List pendingLba = [];
+ List pendingSt = [];
+
+ while(doneSectors < trackSectors)
+ {
+ if(aborted) break;
+
+ uint sectorsToDo = trackSectors - doneSectors >= batchCount
+ ? batchCount
+ : (uint)(trackSectors - doneSectors);
+
+ ui.OnSectorRangeProgress?.Invoke(doneSectors + track.StartSector,
+ doneSectors + sectorsToDo + track.StartSector,
+ track.Sequence,
+ (long)doneSectors,
+ (long)trackSectors);
+
+ ErrorNumber errno = inputOptical.ReadSectors(doneSectors + track.StartSector,
+ false,
+ sectorsToDo,
+ out byte[] sectorData,
+ out SectorStatus[] sectorStatusArray);
+
+ if(errno != ErrorNumber.NoError)
+ {
+ if(force)
+ {
+ ErrorNumber pend = FlushPendingOnReadSkip(outputOptical, ref pending, ref pendingLba,
+ ref pendingSt, force, ui);
+
+ if(pend != ErrorNumber.NoError) return pend;
+
+ ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_continuing,
+ errno,
+ doneSectors + track.StartSector));
+ }
+ else
+ {
+ if(pending.Count > 0)
+ {
+ ui.StoppingErrorMessage?.Invoke(string.Format(
+ Aaru.Localization.Core.Aacs_incomplete_cps_unit_pending_before_read_error,
+ pendingLba[0]));
+
+ return errno;
+ }
+
+ ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing,
+ errno,
+ doneSectors + track.StartSector));
+
+ return errno;
+ }
+
+ doneSectors += sectorsToDo;
+
+ continue;
+ }
+
+ int bps = sectorData.Length / (int)sectorsToDo;
+
+ if(bps != AacsStreamDecrypt.SectorLen || sectorData.Length % sectorsToDo != 0)
+ {
+ ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_bd_decrypt_requires_0_byte_sectors,
+ bps));
+
+ return ErrorNumber.InOutError;
+ }
+
+ for(uint i = 0; i < sectorsToDo; i++)
+ {
+ byte[] one = new byte[AacsStreamDecrypt.SectorLen];
+ Buffer.BlockCopy(sectorData, (int)(i * AacsStreamDecrypt.SectorLen), one, 0, AacsStreamDecrypt.SectorLen);
+ ulong lba = doneSectors + track.StartSector + i;
+ SectorStatus st = sectorStatusArray[i];
+
+ bool allowDecrypt = allowDecryptLba is null || allowDecryptLba(lba);
+
+ ErrorNumber step = ProcessOneUserSector(outputOptical,
+ decryptedCpsUnitKeys,
+ one,
+ lba,
+ allowDecrypt,
+ st,
+ ref pending,
+ ref pendingLba,
+ ref pendingSt,
+ force,
+ ui);
+
+ if(step != ErrorNumber.NoError) return step;
+ }
+
+ doneSectors += sectorsToDo;
+ }
+
+ ErrorNumber flushErr = FlushPendingAtTrackEnd(outputOptical,
+ ref pending,
+ ref pendingLba,
+ ref pendingSt,
+ force,
+ ui);
+
+ if(flushErr != ErrorNumber.NoError) return flushErr;
+
+ currentTrack++;
+ }
+
+ ui.OnEndProgress2?.Invoke();
+ ui.OnEndProgress?.Invoke();
+
+ return ErrorNumber.NoError;
+ }
+
+ /// Flushes the pending sectors on read skip.
+ /// Output optical media image.
+ /// List of pending sectors.
+ /// List of pending LBA.
+ /// List of pending sector statuses.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber FlushPendingOnReadSkip(IWritableOpticalImage outputOptical, ref List pending,
+ ref List pendingLba, ref List pendingSt, bool force,
+ Ui ui)
+ {
+ if(pending.Count == 0) return ErrorNumber.NoError;
+
+ ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_after_read_error_continuing,
+ pending.Count,
+ pendingLba[0]));
+
+ for(int i = 0; i < pending.Count; i++)
+ {
+ ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui);
+
+ if(step != ErrorNumber.NoError) return step;
+ }
+
+ pending.Clear();
+ pendingLba.Clear();
+ pendingSt.Clear();
+
+ return ErrorNumber.NoError;
+ }
+
+ /// Processes one user sector.
+ /// Output optical media image.
+ /// Decrypted CPS unit keys.
+ /// Sector data.
+ /// LBA of the sector.
+ /// Allow decrypt flag.
+ /// Read status of the sector.
+ /// List of pending sectors.
+ /// List of pending LBA.
+ /// List of pending sector statuses.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber ProcessOneUserSector(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys,
+ byte[] sector, ulong lba, bool allowDecrypt, SectorStatus readStatus,
+ ref List pending,
+ ref List pendingLba, ref List pendingSt, bool force,
+ Ui ui)
+ {
+ if(!allowDecrypt)
+ {
+ ErrorNumber flush = FlushPendingAsDumped(outputOptical, ref pending, ref pendingLba, ref pendingSt, force, ui);
+
+ if(flush != ErrorNumber.NoError) return flush;
+
+ return WriteOne(outputOptical, lba, sector, SectorStatus.Dumped, force, ui);
+ }
+
+ if(pending.Count == 0)
+ {
+ if((sector[0] & 0xc0) == 0)
+ return WriteOne(outputOptical, lba, sector, SectorStatus.Dumped, force, ui);
+
+ pending.Add(sector);
+ pendingLba.Add(lba);
+ pendingSt.Add(readStatus);
+
+ return ErrorNumber.NoError;
+ }
+
+ pending.Add(sector);
+ pendingLba.Add(lba);
+ pendingSt.Add(readStatus);
+
+ if(pending.Count < 3) return ErrorNumber.NoError;
+
+ return FlushCompleteUnit(outputOptical,
+ decryptedCpsUnitKeys,
+ ref pending,
+ ref pendingLba,
+ ref pendingSt,
+ force,
+ ui);
+ }
+
+ /// Flushes a complete unit.
+ /// Output optical media image.
+ /// Decrypted CPS unit keys.
+ /// List of pending sectors.
+ /// List of pending LBA.
+ /// List of pending sector statuses.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber FlushCompleteUnit(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys,
+ ref List pending, ref List pendingLba,
+ ref List pendingSt, bool force, Ui ui)
+ {
+ byte[] unit = new byte[AacsStreamDecrypt.AlignedUnitLen];
+ Buffer.BlockCopy(pending[0], 0, unit, 0, AacsStreamDecrypt.SectorLen);
+ Buffer.BlockCopy(pending[1], 0, unit, AacsStreamDecrypt.SectorLen, AacsStreamDecrypt.SectorLen);
+ Buffer.BlockCopy(pending[2], 0, unit, AacsStreamDecrypt.SectorLen * 2, AacsStreamDecrypt.SectorLen);
+
+ if(!AacsStreamDecrypt.TryDecryptAlignedUnit(unit, decryptedCpsUnitKeys))
+ {
+ if(!force)
+ {
+ ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_could_not_decrypt_cps_unit_starting_at_0,
+ pendingLba[0]));
+
+ return ErrorNumber.WriteError;
+ }
+
+ ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_could_not_decrypt_cps_unit_starting_at_0_continuing,
+ pendingLba[0]));
+
+ ErrorNumber w = WriteThree(outputOptical,
+ pending[0],
+ pending[1],
+ pending[2],
+ pendingLba[0],
+ pendingSt[0],
+ pendingSt[1],
+ pendingSt[2],
+ force,
+ ui);
+
+ pending.Clear();
+ pendingLba.Clear();
+ pendingSt.Clear();
+
+ return w;
+ }
+
+ SectorStatus[] ok = [SectorStatus.Unencrypted, SectorStatus.Unencrypted, SectorStatus.Unencrypted];
+ ErrorNumber e = WriteSectorsFromBuffer(outputOptical, unit, pendingLba[0], ok, force, ui);
+
+ pending.Clear();
+ pendingLba.Clear();
+ pendingSt.Clear();
+
+ return e;
+ }
+
+ /// Writes three sectors from a buffer.
+ /// Output optical media image.
+ /// Unit data.
+ /// First LBA of the sectors.
+ /// Statuses of the sectors.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber WriteSectorsFromBuffer(IWritableOpticalImage outputOptical, byte[] unit, ulong firstLba,
+ SectorStatus[] threeStatuses, bool force, Ui ui)
+ {
+ bool result = outputOptical.WriteSectors(unit, firstLba, false, 3, threeStatuses);
+
+ if(!result)
+ {
+ if(force)
+ {
+ ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing,
+ outputOptical.ErrorMessage,
+ firstLba));
+ }
+ else
+ {
+ ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputOptical.ErrorMessage,
+ firstLba));
+
+ return ErrorNumber.WriteError;
+ }
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ /// Flushes the pending sectors as dumped.
+ /// Output optical media image.
+ /// List of pending sectors.
+ /// List of pending LBA.
+ /// List of pending sector statuses.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber FlushPendingAsDumped(IWritableOpticalImage outputOptical, ref List pending,
+ ref List pendingLba, ref List pendingSt, bool force,
+ Ui ui)
+ {
+ for(int i = 0; i < pending.Count; i++)
+ {
+ ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui);
+
+ if(step != ErrorNumber.NoError) return step;
+ }
+
+ pending.Clear();
+ pendingLba.Clear();
+ pendingSt.Clear();
+
+ return ErrorNumber.NoError;
+ }
+
+ /// Writes three sectors from a buffer.
+ /// Output optical media image.
+ /// First sector data.
+ /// Second sector data.
+ /// Third sector data.
+ /// First LBA of the sectors.
+ /// Statuses of the sectors.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber WriteThree(IWritableOpticalImage outputOptical, byte[] a, byte[] b, byte[] c, ulong firstLba,
+ SectorStatus sa, SectorStatus sb, SectorStatus sc, bool force, Ui ui)
+ {
+ byte[] buf = new byte[AacsStreamDecrypt.AlignedUnitLen];
+ Buffer.BlockCopy(a, 0, buf, 0, AacsStreamDecrypt.SectorLen);
+ Buffer.BlockCopy(b, 0, buf, AacsStreamDecrypt.SectorLen, AacsStreamDecrypt.SectorLen);
+ Buffer.BlockCopy(c, 0, buf, AacsStreamDecrypt.SectorLen * 2, AacsStreamDecrypt.SectorLen);
+
+ SectorStatus[] sts = [sa, sb, sc];
+
+ return WriteSectorsFromBuffer(outputOptical, buf, firstLba, sts, force, ui);
+ }
+
+ /// Writes one sector.
+ /// Output optical media image.
+ /// LBA of the sector.
+ /// Sector data.
+ /// Status of the sector.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber WriteOne(IWritableOpticalImage outputOptical, ulong lba, byte[] sector, SectorStatus status,
+ bool force, Ui ui)
+ {
+ bool result = outputOptical.WriteSector(sector, lba, false, status);
+
+ if(!result)
+ {
+ if(force)
+ {
+ ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing,
+ outputOptical.ErrorMessage,
+ lba));
+ }
+ else
+ {
+ ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
+ outputOptical.ErrorMessage,
+ lba));
+
+ return ErrorNumber.WriteError;
+ }
+ }
+
+ return ErrorNumber.NoError;
+ }
+
+ /// Flushes the pending sectors at track end.
+ /// Output optical media image.
+ /// List of pending sectors.
+ /// List of pending LBA.
+ /// List of pending sector statuses.
+ /// Force flag.
+ /// UI.
+ /// Error number.
+ static ErrorNumber FlushPendingAtTrackEnd(IWritableOpticalImage outputOptical, ref List pending,
+ ref List pendingLba, ref List pendingSt, bool force,
+ Ui ui)
+ {
+ if(pending.Count == 0) return ErrorNumber.NoError;
+
+ if(!force)
+ {
+ ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_at_track_end,
+ pending.Count,
+ pendingLba[0]));
+
+ return ErrorNumber.WriteError;
+ }
+
+ ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_at_track_end_continuing,
+ pending.Count,
+ pendingLba[0]));
+
+ for(int i = 0; i < pending.Count; i++)
+ {
+ ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui);
+
+ if(step != ErrorNumber.NoError) return step;
+ }
+
+ pending.Clear();
+ pendingLba.Clear();
+ pendingSt.Clear();
+
+ return ErrorNumber.NoError;
+ }
+}
diff --git a/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs b/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs
new file mode 100644
index 000000000..6035c36b8
--- /dev/null
+++ b/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Text;
+using Aaru.CommonTypes;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Structs;
+using Aaru.Decryption.Aacs;
+
+namespace Aaru.Core.Image;
+
+/// Loads VUK and decrypts CPS unit keys for Blu-ray AACS conversion.
+public static class AacsKeyResolver
+{
+ static readonly string[] UnitKeyPaths =
+ [
+ "AACS/Unit_Key_RO.inf",
+ "AACS\\Unit_Key_RO.inf"
+ ];
+
+ /// Checks if every byte in the buffer is zero.
+ /// Buffer to check.
+ /// True if every byte in the buffer is zero.
+ public static bool IsAllZero(ReadOnlySpan buffer)
+ {
+ for(int i = 0; i < buffer.Length; i++)
+ {
+ if(buffer[i] != 0)
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Obtains decrypted 16-byte CPS unit keys in disc order, or if keys cannot be loaded.
+ ///
+ /// Input optical media image.
+ /// Plugin register.
+ /// Encoding.
+ /// Error message.
+ /// Decrypted CPS unit keys.
+ public static byte[][]? TryGetDecryptedCpsUnitKeys(IOpticalMediaImage image, PluginRegister plugins,
+ Encoding? encoding, out string? errorMessage)
+ {
+ errorMessage = null;
+ encoding ??= Encoding.UTF8;
+
+ if(!TryResolveVolumeUniqueKey(image, out byte[] vuk, out errorMessage))
+ return null;
+
+ if(!TryLoadEncryptedCpsUnitKeys(image, plugins, encoding, out byte[][]? encKeys, out errorMessage) ||
+ encKeys is null)
+ return null;
+
+ byte[][] decrypted = new byte[encKeys.Length][];
+
+ for(int i = 0; i < encKeys.Length; i++)
+ {
+ if(encKeys[i].Length != 16)
+ {
+ errorMessage = Aaru.Localization.Core.Aacs_encrypted_unit_key_invalid_length;
+
+ return null;
+ }
+
+ decrypted[i] = new byte[16];
+ AacsCrypto.DecryptCpsUnitKey(vuk, encKeys[i], decrypted[i]);
+ }
+
+ return decrypted;
+ }
+
+ /// Tries to resolve the volume unique key from the media tags.
+ /// Input optical media image.
+ /// Volume unique key.
+ /// Error message.
+ /// True if the volume unique key was resolved successfully.
+ static bool TryResolveVolumeUniqueKey(IOpticalMediaImage image, out byte[] vuk, out string? errorMessage)
+ {
+ vuk = new byte[16];
+ errorMessage = null;
+
+ if(image.ReadMediaTag(MediaTagType.AacsVolumeUniqueKey, out byte[]? tagVuk) == ErrorNumber.NoError &&
+ tagVuk is { Length: 16 } &&
+ !IsAllZero(tagVuk))
+ {
+ Buffer.BlockCopy(tagVuk, 0, vuk, 0, 16);
+
+ return true;
+ }
+
+ if(image.ReadMediaTag(MediaTagType.AacsMediaKey, out byte[]? mk) != ErrorNumber.NoError ||
+ mk is not { Length: 16 } ||
+ IsAllZero(mk))
+ {
+ errorMessage = Aaru.Localization.Core.Aacs_missing_volume_unique_key;
+
+ return false;
+ }
+
+ if(image.ReadMediaTag(MediaTagType.AACS_VolumeIdentifier, out byte[]? vid) != ErrorNumber.NoError ||
+ vid is not { Length: 16 } ||
+ IsAllZero(vid))
+ {
+ errorMessage = Aaru.Localization.Core.Aacs_missing_volume_unique_key;
+
+ return false;
+ }
+
+ AacsCrypto.DeriveVolumeUniqueKey(mk, vid, vuk);
+
+ return true;
+ }
+
+ /// Tries to load the encrypted CPS unit keys from the media tags or filesystem.
+ /// Input optical media image.
+ /// Plugin register.
+ /// Encoding.
+ /// Encrypted CPS unit keys.
+ /// Error message.
+ /// True if the encrypted CPS unit keys were loaded successfully.
+ static bool TryLoadEncryptedCpsUnitKeys(IOpticalMediaImage image, PluginRegister plugins, Encoding encoding,
+ out byte[][]? encKeys, out string? errorMessage)
+ {
+ encKeys = null;
+ errorMessage = null;
+
+ if(image.ReadMediaTag(MediaTagType.AACS_DataKeys, out byte[]? dataKeys) == ErrorNumber.NoError &&
+ dataKeys is { Length: > 0 })
+ {
+ UnitKeyRoParseResult? parsed = UnitKeyRoParseResult.TryParse(dataKeys);
+
+ if(parsed != null)
+ {
+ if(parsed.IsAacs2Layout)
+ {
+ errorMessage = Aaru.Localization.Core.Aacs2_unit_keys_not_supported;
+
+ return false;
+ }
+
+ encKeys = parsed.EncryptedCpsUnitKeys;
+
+ if(encKeys.Length > 0)
+ return true;
+ }
+
+ UnitKeyRoParseResult? raw = UnitKeyRoParseResult.TryParseRawEncryptedKeys(dataKeys);
+
+ if(raw != null)
+ {
+ encKeys = raw.EncryptedCpsUnitKeys;
+
+ if(encKeys.Length > 0)
+ return true;
+ }
+ }
+
+ if(TryReadUnitKeyInfFromFilesystem(image, plugins, encoding, out byte[]? fileBytes) &&
+ fileBytes is { Length: > 0 })
+ {
+ UnitKeyRoParseResult? parsed = UnitKeyRoParseResult.TryParse(fileBytes);
+
+ if(parsed == null)
+ {
+ errorMessage = Aaru.Localization.Core.Aacs_unit_key_inf_invalid;
+
+ return false;
+ }
+
+ if(parsed.IsAacs2Layout)
+ {
+ errorMessage = Aaru.Localization.Core.Aacs2_unit_keys_not_supported;
+
+ return false;
+ }
+
+ encKeys = parsed.EncryptedCpsUnitKeys;
+
+ return encKeys.Length > 0;
+ }
+
+ errorMessage = Aaru.Localization.Core.Aacs_missing_unit_keys;
+
+ return false;
+ }
+
+ /// Tries to read the Unit_Key_RO.inf file from the filesystem.
+ /// Input optical media image.
+ /// Plugin register.
+ /// Encoding.
+ /// File bytes.
+ /// True if the Unit_Key_RO.inf file was read successfully.
+ static bool TryReadUnitKeyInfFromFilesystem(IOpticalMediaImage image, PluginRegister plugins, Encoding encoding,
+ out byte[]? fileBytes)
+ {
+ fileBytes = null;
+
+ foreach(Partition partition in Partitions.GetAll(image))
+ {
+ foreach(IFilesystem fs in plugins.Filesystems.Values)
+ {
+ if(fs is not IReadOnlyFilesystem rofs)
+ continue;
+
+ if(!fs.Identify(image, partition))
+ continue;
+
+ if(rofs.Mount(image, partition, encoding, null, null) != ErrorNumber.NoError)
+ continue;
+
+ try
+ {
+ foreach(string path in UnitKeyPaths)
+ {
+ if(rofs.OpenFile(path, out IFileNode? node) != ErrorNumber.NoError || node is null)
+ continue;
+
+ try
+ {
+ if(node.Length <= 0 || node.Length > 10 * 1024 * 1024)
+ continue;
+
+ byte[] buf = new byte[node.Length];
+
+ if(rofs.ReadFile(node, node.Length, buf, out long read) != ErrorNumber.NoError ||
+ read != node.Length)
+ continue;
+
+ fileBytes = buf;
+
+ return true;
+ }
+ finally
+ {
+ rofs.CloseFile(node);
+ }
+ }
+ }
+ finally
+ {
+ rofs.Unmount();
+ }
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs b/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs
new file mode 100644
index 000000000..71b4bd692
--- /dev/null
+++ b/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.Localization;
+
+namespace Aaru.Core.Image;
+
+public partial class Convert
+{
+ /// Blu-ray AACS: sector-aligned CPS units, staging, and per-sector .
+ ErrorNumber ConvertAacsBdOpticalSectors(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical)
+ {
+ ErrorNumber errno = AacsBdExtentResolver.ResolveStreamFileExtents(inputOptical,
+ _plugins,
+ out List ranges,
+ out string err);
+
+ Func allowDecryptLba = null;
+
+ if(errno == ErrorNumber.NoError)
+ allowDecryptLba = lba => AacsBdExtentResolver.IsLbaAllowed(ranges, lba);
+ else if(_force)
+ ErrorMessage?.Invoke(err + " Continuing with broad Blu-ray AACS decrypt scope.");
+ else
+ {
+ StoppingErrorMessage?.Invoke(err);
+
+ return errno;
+ }
+
+ return AacsBdOpticalPipeline.Run(inputOptical,
+ outputOptical,
+ _count,
+ _force,
+ ref _aborted,
+ _aacsDecryptedCpsUnitKeys,
+ allowDecryptLba,
+ new AacsBdOpticalPipeline.Ui
+ {
+ OnInitProgress = () => InitProgress?.Invoke(),
+ OnInitProgress2 = () => InitProgress2?.Invoke(),
+ OnEndProgress = () => EndProgress?.Invoke(),
+ OnEndProgress2 = () => EndProgress2?.Invoke(),
+ OnTrackProgress = (i, n) => UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_in_track_0_of_1, i + 1, n), i, n),
+ OnSectorRangeProgress = (start, end, seq, done, tot) =>
+ UpdateProgress2?.Invoke(string.Format(UI.Converting_sectors_0_to_1_in_track_2,
+ start,
+ end,
+ seq),
+ done,
+ tot),
+ ErrorMessage = msg => ErrorMessage?.Invoke(msg),
+ StoppingErrorMessage = msg => StoppingErrorMessage?.Invoke(msg)
+ });
+ }
+}