Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package redumper for openSUSE:Factory 
checked in at 2026-10-26 10:00:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/redumper (Old)
 and      /work/SRC/openSUSE:Factory/.redumper.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "redumper"

Mon Oct 26 10:00:33 2026 rev:11 rq:1392389 version:768

Changes:
--------
--- /work/SRC/openSUSE:Factory/redumper/redumper.changes        2026-09-28 
10:45:23.765296642 +0200
+++ /work/SRC/openSUSE:Factory/.redumper.new.2252680/redumper.changes   
2026-10-26 10:01:37.872812430 +0100
@@ -1,0 +2,18 @@
+Sun Oct 25 11:08:15 UTC 2026 - Martin Hauke <[email protected]>
+
+- Update to version 768
+  * READ CD: request the exact transfer length (#459).
+  * Fix READ CDDA short transfer check rejecting oversized buffers
+    (#468).
+  * Add LG GP57ES40 drive (#466).
+  * Add ATAPI DVD D DH16D2S GD0D drive (#463).
+  * Print MKB version (#462).
+  * Added support for flashing encrypted firmware (#456).
+  * Add LG SP80NB60 drive (#460).
+  * Fix layer break printing (#458).
+  * AES-128 and CMAC implementation.
+  * Improve MT1959 flashing support (#452).
+  * Update raw OmniDrive dump read size handling (#451).
+  * Add entry support to UDF implementation (#450).
+
+-------------------------------------------------------------------

Old:
----
  redumper-b752.tar.gz

New:
----
  redumper-b768.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ redumper.spec ++++++
--- /var/tmp/diff_new_pack.jfRrcR/_old  2026-10-26 10:01:38.708847506 +0100
+++ /var/tmp/diff_new_pack.jfRrcR/_new  2026-10-26 10:01:38.711847631 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           redumper
-Version:        752
+Version:        768
 Release:        0
 Summary:        Low level CD dumper utility
 License:        GPL-3.0-only

++++++ redumper-b752.tar.gz -> redumper-b768.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/CMakeLists.txt 
new/redumper-b768/CMakeLists.txt
--- old/redumper-b752/CMakeLists.txt    2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/CMakeLists.txt    2026-10-23 03:56:38.000000000 +0200
@@ -126,7 +126,9 @@
     "drive/plextor.ixx"
     "drive/mediatek.ixx"
     "drive/test.ixx"
+    "dvd/aacs.ixx"
     "dvd/bd.ixx"
+    "dvd/bd_key.ixx"
     "dvd/bd_scrambler.ixx"
     "dvd/css/css.ixx"
     "dvd/dvd.ixx"
@@ -153,8 +155,11 @@
     "filesystem/udf/udf.ixx"
     "filesystem/udf/udf_browser.ixx"
     "filesystem/udf/udf_defs.ixx"
+    "filesystem/udf/udf_entry.ixx"
     "filesystem/udf/udf_size.ixx"
     "hash/block_hasher.ixx"
+    "hash/aes128.ixx"
+    "hash/cmac.ixx"
     "hash/md5.ixx"
     "hash/sha1.ixx"
     "readers/disc_read_cdda_reader.ixx"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/README.md new/redumper-b768/README.md
--- old/redumper-b752/README.md 2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/README.md 2026-10-23 03:56:38.000000000 +0200
@@ -110,7 +110,7 @@
 redumper disc --drive=<drive> --bd-raw
 ```
 
-OmniDrive also enables redumper to dump Xbox, Xbox 360, Nintendo GameCube, 
Wii, and Wii U discs. redumper detects these proprietary formats and 
automatically uses the appropriate raw acquisition path; Nintendo discs are 
also descrambled as part of the dump. No additional raw-mode option is required.
+OmniDrive also enables redumper to dump Xbox, Xbox 360, Nintendo GameCube, 
Wii, and Wii U discs. redumper detects these proprietary formats and 
automatically uses the appropriate raw acquisition path; Nintendo discs are 
also descrambled as part of the dump. No additional raw-mode option is 
required. If you are getting SCSI errors with OmniDrive, try lower the transfer 
length (`--dump-read-size`).
 
 ### Test an unknown drive
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/drive/flash_mt1959.ixx 
new/redumper-b768/drive/flash_mt1959.ixx
--- old/redumper-b752/drive/flash_mt1959.ixx    2026-09-18 02:05:14.000000000 
+0200
+++ new/redumper-b768/drive/flash_mt1959.ixx    2026-10-23 03:56:38.000000000 
+0200
@@ -2,11 +2,13 @@
 #include <algorithm>
 #include <chrono>
 #include <cstdint>
+#include <cstring>
 #include <fstream>
 #include <span>
 #include <string>
 #include <string_view>
 #include <thread>
+#include <unordered_set>
 #include <vector>
 #include "throw_line.hh"
 
@@ -20,6 +22,7 @@
 import utils.file_io;
 import utils.hex_bin;
 import utils.logger;
+import hash.aes128;
 
 
 
@@ -28,34 +31,74 @@
 
 struct DriveEntry
 {
-    std::string vendor_id;
     std::string product_id;
-    std::string config_id;
+    std::string bootstring_suffix;
+    std::string svccode;
 };
 
+const std::vector<std::unordered_set<std::string>> MT1959_BOOTSTRING_SUFFIXES{
+    { "BUP1" }, // slim (old)
+    { "BU5 ", "BU51", "BUP3", "BP32", "BUP5", "BP52" }, // slim
+    { "JB8 ", "JB9 ", "JBC6", "JBP6" }, // half-height
+};
+
+// OEM firmwares lack the SVC code
 const std::vector<DriveEntry> MT1959_SUPPORTED_DRIVES = {
-    { "HL-DT-ST", "BD-RE BU40N",    "MT1959 Boot BU5 " },
-    { "HL-DT-ST", "BD-RE BU50N",    "MT1959 Boot BU5 " }, // MT1959 Boot BU51
-    { "ASUS",     "BW-16D1HT",      "MT1959 Boot JB8 " },
-    { "ASUS",     "BW-16D1H-U",     "MT1959 Boot JB8 " },
-    { "HL-DT-ST", "BD-RE BH16NS60", "MT1959 Boot JB8 " }, // NS60
-    { "HL-DT-ST", "BD-RE WH16NS60", "MT1959 Boot JB8 " }, // NS60
-    { "HL-DT-ST", "BD-RE BH16NS58", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE WH16NS58", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE BH16NS55", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE BH16NS50", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE BH14NS50", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE BH16NS40", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE WH16NS40", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE BH14NS40", "MT1959 Boot JB8 " }, // NS50
-    { "HL-DT-ST", "BD-RE WH14NS40", "MT1959 Boot JB8 " }, // NS50
+    { "BDDVDRW CH12NS40", "JBC6", "NS40" },
+    { "BDDVDRW UH12NS40", "JBC6", "NS40" },
+    { "BC-12B1ST b",      "JBC6", "    " },
+    { "BC-12D2HT",        "JBC6", "    " },
+    { "BD-RE BE16NU50",   "JBP6", "NU50" },
+    { "BD-RE BH14NS40",   "JB8 ", "NS50" },
+    { "BD-RE WH14NS40",   "JB8 ", "NS50" },
+    { "BD-RE BH14NS50",   "JB8 ", "NS50" },
+    { "BD-RE WH14NS50",   "JB8 ", "NS50" },
+    { "BD-RE BH14NS58",   "JB8 ", "NS50" },
+    { "BD-RE WH14NS58",   "JB8 ", "NS50" },
+    { "BD-RE BH16NS40",   "JB8 ", "NS50" },
+    { "BD-RE WH16NS40",   "JB8 ", "NS50" },
+    { "BD-RE WH16NS46",   "JB8 ", "NS50" },
+    { "BD-RE BH16NS50",   "JB8 ", "NS50" },
+    { "BD-RE WH16NS50",   "JB8 ", "NS50" },
+    { "BD-RE BH16NS55",   "JB8 ", "NS50" },
+    { "BD-RE WH16NS55",   "JB8 ", "NS50" },
+    { "BD-RE BH16NS58",   "JB8 ", "NS50" },
+    { "BD-RE WH16NS58",   "JB8 ", "NS50" },
+    { "BD-RE WH16NS58",   "JB8 ", "    " }, // Vinpower OEM
+    { "BD-RE BH16NS60",   "JB8 ", "NS60" }, // JB9 board
+    { "BD-RE WH16NS60",   "JB8 ", "NS60" }, // JB9 board
+    { "BD-RE BH-A10AME",  "JB8 ", "    " },
+    { "BD-RE BH40N",      "JB8 ", "    " },
+    { "BD-RE BH50N",      "JB8 ", "    " },
+    { "BW-16D1HT",        "JB8 ", "    " },
+    { "BW-16D1H-U",       "JB8 ", "    " },
+    { "BW-16D1X-U",       "JB8 ", "    " },
+    { "BD-RE BP50NB40",   "BUP3", "NB50" },
+    { "BD-RE BP50NB40",   "BP32", "NB52" },
+    { "BD-RE WP50NB40",   "BUP3", "NB50" },
+    { "BD-RE WP50NB40",   "BP32", "NB52" },
+    { "BD-RE BP55EB40",   "BUP3", "NB50" }, // SVC code not a typo here
+    { "BD-RE BP55EB40",   "BP32", "EB52" },
+    { "BD-RE BP60NB10",   "BUP5", "NB10" },
+    { "BD-RE BP60NB10",   "BP52", "NB12" },
+    { "BD-RE BU40N",      "BU5 ", "BU40" },
+    { "BD-RE BU40N",      "BU5 ", "    " },
+    { "BDRE BU40N",       "BU5 ", "    " }, // HP version
+    { "BD-RE BU50N",      "BU51", "    " },
+    { "BDRE BU50N",       "BU51", "    " }, // HP version
+    // { "BD-RE BP71N     ", "", "" }, // needs checking
 };
 
 
 void flash_mt1959(SPTD &sptd, std::span<const uint8_t> firmware_data, uint32_t 
block_size)
 {
-    if(auto status = cmd_write_buffer(sptd, nullptr, 0, 
WRITE_BUFFER_Mode::VENDOR_SPECIFIC, 0, 0); status.status_code)
-        throw_line("failed to flash firmware, SCSI ({})", 
SPTD::StatusMessage(status));
+    if(auto status = cmd_write_buffer(sptd, nullptr, 0, 
WRITE_BUFFER_Mode::DOWNLOAD_MICROCODE_WITH_OFFSETS, 0x67, 0, 0); 
status.status_code)
+    {
+        // slim flash unlocked failed, try standard
+        status = cmd_write_buffer(sptd, nullptr, 0, 
WRITE_BUFFER_Mode::VENDOR_SPECIFIC, 0, 0, 0);
+        if(status.status_code)
+            throw_line("failed to flash firmware, SCSI ({})", 
SPTD::StatusMessage(status));
+    }
 
     for(uint32_t offset = 0; offset < firmware_data.size();)
     {
@@ -64,7 +107,7 @@
 
         LOGC_RF("[{:3}%] flashing: [{:08X} .. {:08X})", 100 * offset / 
(uint32_t)firmware_data.size(), offset, offset_next);
 
-        if(auto status = cmd_write_buffer(sptd, &firmware_data[offset], size, 
WRITE_BUFFER_Mode::DOWNLOAD_MICROCODE_WITH_OFFSETS, offset, size); 
status.status_code)
+        if(auto status = cmd_write_buffer(sptd, &firmware_data[offset], size, 
WRITE_BUFFER_Mode::DOWNLOAD_MICROCODE_WITH_OFFSETS, 0, offset, size); 
status.status_code)
             throw_line("failed to flash firmware, SCSI ({})", 
SPTD::StatusMessage(status));
 
         offset = offset_next;
@@ -72,17 +115,25 @@
     LOGC("");
     LOGC("");
 
-    constexpr uint32_t wait_seconds = 15;
-    uint32_t ready_count = 0;
-    for(uint32_t i = 0; i < wait_seconds; ++i)
-    {
-        LOGC_RF("waiting for drive to become ready... [{}/{}]", i + 1, 
wait_seconds);
-        if(!cmd_drive_ready(sptd).status_code)
-            ++ready_count;
+    constexpr uint8_t completion_value = 32;
+    auto status = cmd_drive_ready(sptd);
 
-        if(ready_count >= 2)
-            break;
+    if(!status.status_code)
+    {
+        // if it's ready immediately the flash failed
+        throw_line("failed to flash firmware, firmware file failed drive 
verification");
+    }
 
+    constexpr uint32_t timeout = 60;
+    for(uint32_t i = 0; i <= timeout; i++)
+    {
+        if(i >= timeout)
+            throw_line("failed to flash firmware, flash timed out");
+        if(status.sense_key != 0x02 || status.asc != 0x04)
+            throw_line("failed to flash firmware, SCSI ({})", 
SPTD::StatusMessage(status));
+        LOGC_RF("waiting for flash to complete... [{}/{}]", 
status.information[1], completion_value);
+        if(status = cmd_drive_ready(sptd); !status.status_code)
+            break;
         std::this_thread::sleep_for(std::chrono::seconds(1));
     }
 
@@ -91,27 +142,72 @@
 }
 
 
-std::vector<uint8_t> extract_firmware_config(std::span<const uint8_t> 
firmware_data, uint32_t config_offset, uint32_t config_size)
+std::string_view extract_firmware_bootstring(std::span<const uint8_t> 
firmware_data, uint32_t bootstring_offset, uint32_t bootstring_size)
 {
-    return std::vector<uint8_t>(&firmware_data[config_offset], 
&firmware_data[config_offset + config_size]);
+    return std::string_view((const char *)&firmware_data[bootstring_offset], 
bootstring_size);
 }
 
 
-std::vector<uint8_t> read_mt1959_config(SPTD &sptd, uint32_t config_offset, 
uint32_t config_size)
+std::string read_mt1959_bootstring(SPTD &sptd, uint32_t bootstring_offset, 
uint32_t bootstring_size)
 {
-    std::vector<uint8_t> config(config_size);
+    std::vector<uint8_t> bootstring(bootstring_size);
 
-    if(auto status = cmd_read_buffer(sptd, config.data(), config.size(), 
READ_BUFFER_Mode::DOWNLOAD_MICROCODE_WITH_OFFSETS, config_offset, 
config.size()); status.status_code)
-        throw_line("failed to read drive patch configuration, SCSI ({})", 
SPTD::StatusMessage(status));
+    if(auto status = cmd_read_buffer(sptd, bootstring.data(), 
bootstring.size(), READ_BUFFER_Mode::DOWNLOAD_MICROCODE_WITH_OFFSETS, 0, 
bootstring_offset, bootstring_size); status.status_code)
+        throw_line("failed to read drive bootstring, SCSI ({})", 
SPTD::StatusMessage(status));
 
-    return config;
+    return std::string((const char *)bootstring.data(), bootstring_size);
 }
 
 
-void modify_firmware(std::span<uint8_t> firmware_data, uint32_t clear_size, 
uint32_t config_offset, std::span<const uint8_t> drive_config)
+std::string read_mt1959_svccode(SPTD &sptd, uint32_t svccode_offset, uint32_t 
svccode_size)
+{
+    std::vector<uint8_t> svccode(svccode_size);
+
+    if(auto status = cmd_read_buffer(sptd, svccode.data(), svccode.size(), 
READ_BUFFER_Mode::DOWNLOAD_MICROCODE_WITH_OFFSETS, 0, svccode_offset, 
svccode_size); status.status_code)
+        throw_line("failed to read drive SVC code, SCSI ({})", 
SPTD::StatusMessage(status));
+
+    return std::string((const char *)svccode.data(), svccode_size);
+}
+
+
+bool needs_encryption(SPTD &sptd, std::string &vendor_specific)
+{
+    GET_CONFIGURATION_FirmwareInformationBody firmware_information;
+
+    if(auto status = cmd_get_configuration_firmware_information(sptd, 
&firmware_information); status.status_code)
+        throw_line("failed to read current firmware build date/time, SCSI 
({})", SPTD::StatusMessage(status));
+
+    // if year < 2020
+    if(const uint8_t decade = firmware_information.year[0] - '0'; decade < 2)
+        return false;
+
+    if(vendor_specific.empty() || vendor_specific.size() < 2)
+    {
+        LOGC("invalid vendor-specific data; assuming encrypted firmware is 
required");
+        return true;
+    }
+
+    return vendor_specific[1] != 'M';
+}
+
+
+void modify_firmware(std::span<uint8_t> firmware_data, uint32_t clear_size, 
uint32_t bootstring_offset, std::string &bootstring, bool encrypt_firmware)
 {
     std::fill(firmware_data.begin(), firmware_data.begin() + clear_size, 
(uint8_t)0xFF);
-    std::copy(drive_config.begin(), drive_config.end(), 
&firmware_data[config_offset]);
+    std::copy(bootstring.begin(), bootstring.end(), 
&firmware_data[bootstring_offset]);
+
+    if(!encrypt_firmware)
+        return;
+
+    constexpr std::array<uint8_t, AES128::BLOCK_SIZE> MT1959_ENC_KEY{ 0x5E, 
0x9E, 0x4F, 0x00, 0x94, 0xEF, 0x20, 0xAB, 0x52, 0xE3, 0x5E, 0x73, 0x6A, 0xCB, 
0x23, 0x24 };
+    const AES128 aes128(MT1959_ENC_KEY);
+    for(auto i = 0; i < firmware_data.size(); i += AES128::BLOCK_SIZE)
+    {
+        std::array<uint8_t, AES128::BLOCK_SIZE> block;
+        memcpy(block.data(), firmware_data.data() + i, block.size());
+        block = aes128.encryptBlock(block);
+        memcpy(firmware_data.data() + i, block.data(), block.size());
+    }
 }
 
 
@@ -120,36 +216,65 @@
     int exit_code = 0;
 
     constexpr uint32_t block_size = 0x4000;
-    constexpr uint32_t config_offset = 0x3000;
-    constexpr uint32_t config_size = 0x20;
-    constexpr uint32_t id_size = 0x10;
+    constexpr uint32_t bootstring_offset = 0x3000;
+    constexpr uint32_t bootstring_size = 0x10;
+    constexpr std::string_view bootstring_prefix = "MT1959 Boot ";
+    constexpr uint32_t svccode_offset = 0x1EC04C;
+    constexpr uint32_t svccode_size = 0x4;
     constexpr uint32_t clear_size = 0x10000;
 
     auto firmware_data = read_vector(options.firmware);
     if(firmware_data.size() < clear_size)
         throw_line("firmware data too small (size: {:#x}, required: {:#x})", 
firmware_data.size(), clear_size);
-    auto firmware_config = extract_firmware_config(firmware_data, 
config_offset, config_size);
-    std::string_view firmware_id((const char *)firmware_config.data(), 
id_size);
+    if(firmware_data.size() % 16 != 0)
+        throw_line("firmware size is incorrect (not a multiple of 16) (size: 
{:#x})", firmware_data.size());
+
+    auto drive_bootstring = read_mt1959_bootstring(*ctx.sptd, 
bootstring_offset, bootstring_size);
+    auto drive_svccode = read_mt1959_svccode(*ctx.sptd, svccode_offset, 
svccode_size);
 
     if(!options.force_flash)
     {
-        auto it = std::find_if(MT1959_SUPPORTED_DRIVES.begin(), 
MT1959_SUPPORTED_DRIVES.end(),
-            [&](const auto &d) { return d.vendor_id == 
ctx.drive_config.vendor_id && d.product_id == ctx.drive_config.product_id; });
-        if(it == MT1959_SUPPORTED_DRIVES.end())
-            throw_line("flashing of this drive is unsupported");
-        else if(firmware_id != it->config_id)
-            throw_line("unexpected firmware config id (current: {}, expected: 
{})", firmware_id, it->config_id);
-    }
-
-    auto drive_config = read_mt1959_config(*ctx.sptd, config_offset, 
config_size);
-    std::string_view drive_id((const char *)drive_config.data(), id_size);
-    if(drive_id != firmware_id && !(drive_id == "MT1959 Boot BU51" && 
firmware_id == "MT1959 Boot BU5 "))
-        throw_line("firmware id mismatch (drive: {}, firmware: {})", drive_id, 
firmware_id);
+        auto firmware_bootstring = extract_firmware_bootstring(firmware_data, 
bootstring_offset, bootstring_size);
 
-    LOG("drive patch configuration (offset: 0x{:04X}, size: 0x{:X}): ", 
config_offset, config_size);
-    LOG("{}", hexdump(drive_config.data(), 0, (uint32_t)drive_config.size()));
+        // compare prefix
+        if(drive_bootstring.compare(0, bootstring_prefix.length(), 
bootstring_prefix) != 0)
+            throw_line("unexpected drive bootstring prefix (current: {}, 
expected: {})", drive_bootstring.substr(0, bootstring_prefix.length()), 
bootstring_prefix);
+        if(firmware_bootstring.compare(0, bootstring_prefix.length(), 
bootstring_prefix) != 0)
+            throw_line("unexpected firmware bootstring prefix (current: {}, 
expected: {})", firmware_bootstring.substr(0, bootstring_prefix.length()), 
bootstring_prefix);
+
+        auto firmware_bootstring_suffix = 
firmware_bootstring.substr(bootstring_prefix.length());
+        auto drive_bootstring_suffix = 
drive_bootstring.substr(bootstring_prefix.length());
+
+        bool is_compatible = false;
+        for(auto &compatible_suffixes : MT1959_BOOTSTRING_SUFFIXES)
+        {
+            
if(compatible_suffixes.contains(std::string(firmware_bootstring_suffix)) && 
compatible_suffixes.contains(std::string(drive_bootstring_suffix)))
+                is_compatible = true;
+        }
+        if(!is_compatible)
+            throw_line("incompatible firmware bootstring for drive (firmware 
bootstring: {}, drive bootstring: {})", firmware_bootstring_suffix, 
drive_bootstring_suffix);
+    }
+
+
+    auto bootstring = 
std::string(drive_bootstring.substr(bootstring_prefix.length()));
+    // if drive is in boot mode we should use the detected string, otherwise 
we derive string based on product ID and SVC code
+    if(ctx.drive_config.product_revision_level != "BOOT")
+    {
+        for(auto &drive_entry : MT1959_SUPPORTED_DRIVES)
+        {
+            if(drive_entry.product_id == ctx.drive_config.product_id)
+            {
+                bootstring = drive_entry.bootstring_suffix;
+                // exit loop if SVC code matches, since that guarantees 
correct bootstring
+                if(drive_entry.svccode == drive_svccode)
+                    break;
+            }
+        }
+    }
+    bootstring = std::string(bootstring_prefix) + bootstring;
 
-    modify_firmware(firmware_data, clear_size, config_offset, drive_config);
+    const bool encrypt_firmware = needs_encryption(*ctx.sptd, 
ctx.drive_config.vendor_specific);
+    modify_firmware(firmware_data, clear_size, bootstring_offset, bootstring, 
encrypt_firmware);
 
     flash_mt1959(*ctx.sptd, firmware_data, block_size);
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/drive/flash_plextor.ixx 
new/redumper-b768/drive/flash_plextor.ixx
--- old/redumper-b752/drive/flash_plextor.ixx   2026-09-18 02:05:14.000000000 
+0200
+++ new/redumper-b768/drive/flash_plextor.ixx   2026-10-23 03:56:38.000000000 
+0200
@@ -54,7 +54,7 @@
         cmd_drive_ready(sptd);
 
         auto mode = offset_next < firmware_data.size() ? 
WRITE_BUFFER_Mode::DOWNLOAD_MICROCODE : 
WRITE_BUFFER_Mode::DOWNLOAD_MICROCODE_SAVE;
-        SPTD::Status status = cmd_write_buffer(sptd, &firmware_data[offset], 
size, mode, offset, size);
+        SPTD::Status status = cmd_write_buffer(sptd, &firmware_data[offset], 
size, mode, 0, offset, size);
         if(status.status_code)
             throw_line("failed to flash firmware, SCSI ({})", 
SPTD::StatusMessage(status));
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/drive.ixx new/redumper-b768/drive.ixx
--- old/redumper-b752/drive.ixx 2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/drive.ixx 2026-10-23 03:56:38.000000000 +0200
@@ -1,5 +1,6 @@
 module;
 #include <algorithm>
+#include <array>
 #include <chrono>
 #include <cstdint>
 #include <cstring>
@@ -69,6 +70,18 @@
     MTK2B
 };
 
+export enum class JamlessPlayMode : uint8_t
+{
+    OFF = 0xDD,
+    ON = 0xEE
+};
+
+export enum class JamlessPlayPersist : uint8_t
+{
+    TEMPORARY = 0x00,
+    FLASH = 0xFF
+};
+
 export struct DriveConfig
 {
     std::string vendor_id;
@@ -181,6 +194,7 @@
     { "TSSTcorp", "DVD-ROM TS-H353C" , "ZZ00", ""                    , "",   
+6,   0, -135, ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // 
MoriGM
     { "HL-DT-ST", "DVD-ROM DH41N"    , "ZZ00", ""                    , "",   
+6,   0, -135, ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // 
MoriGM
     { "PLDS"    , "DVD-ROM DH-16D5S" , "ZZ00", "2025-06-18 15:03 Fen", "",   
+6,   0, -135, ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // 
MoriGM
+    { "ATAPI"   , "DVD D DH16D2S"    , "GD0D", "2007/06/04 17:00"    , "",   
+6,   0, -135, ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // 
wakasio
     { "hp"      , "DVD RW AD-7290H5" , "SE02", ""                    , "",  
+48,   0, -135, ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // 
seri
     { "Optiarc" , "DVD RW AD-7173A"  , "1-04", "seri-v1    BT-LIGGY" , "",  
+48,   0, -135, ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // 
seri
     { "NEC"     , "DVD_RW ND-3520AW" , "3.07", "seri-01 BT-LIGGY"    , "",  
+48,   0, -135, ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // 
seri
@@ -224,6 +238,8 @@
     { "HL-DT-ST", "BD-RE WH16NS40"   , "1.05", "", "",    +6,  0, -135, 
ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // scrambled
     { "HL-DT-ST", "BD-RE WH16NS60"   , "1.02", "", "",    +6,  0, -135, 
ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  }, // scrambled
     { "HL-DT-ST", "DVD+-RW GH50N"    , "B103", "", "",  +667,  0, -135, 
ReadMethod::BE, SectorOrder::DATA_SUB   , Type::GENERIC  },
+    { "HL-DT-ST", "DVDRAM SP80NB60"  , "PD00", "", "",    +6,  0,    0, 
ReadMethod::BE, SectorOrder::DATA_SUB_C2, Type::GENERIC  },
+    { "HL-DT-ST", "DVDRAM GP57ES40"  , "RF01", "", "",    +6,  0,    0, 
ReadMethod::BE, SectorOrder::DATA_SUB_C2, Type::GENERIC  },
     { "CREATIVE", "CD5233E-N"        , "0.20", "", "",   +12,  0, -135, 
ReadMethod::BE, SectorOrder::DATA_C2_SUB, Type::GENERIC  },
     { "PLEXTOR" , "DVDR PX-740A"     , "1.02", "", "",  +618,  0, -135, 
ReadMethod::BE, SectorOrder::DATA_SUB   , Type::GENERIC  }, // doesn't stop on 
lead-out but always returns same sector
     { "PLEXTOR" , "DVDR PX-L890SA"   , "1.07", "", "",    +6,  0, -135, 
ReadMethod::BE, SectorOrder::DATA_SUB_C2, Type::GENERIC  }, // scrambled
@@ -438,6 +454,26 @@
 }
 
 
+export bool is_jamless_play_firmware(const DriveConfig &drive_config)
+{
+    return drive_config.vendor_id == "HL-DT-ST" && 
(drive_config.product_id.starts_with("DV") || 
drive_config.product_id.starts_with("BD"));
+}
+
+
+export bool drive_set_jamless_play(SPTD &sptd, JamlessPlayMode mode, 
JamlessPlayPersist persist)
+{
+    std::array<uint8_t, 4> response = {};
+    uint32_t transferred = 0;
+    uint32_t allocation_length = (uint32_t)mode << 16 | (uint32_t)persist << 8;
+    auto status = cmd_read_buffer(sptd, response.data(), response.size(), 
READ_BUFFER_Mode::LG_JAMLESS_PLAY_CONTROL, 0x01, 0, allocation_length);
+    if(status.status_code)
+        return false;
+
+    std::array<uint8_t, 4> expected_response = mode == JamlessPlayMode::OFF ? 
std::array<uint8_t, 4>{ 0xDD, 0x00, 0x00, 0x00 } : std::array<uint8_t, 4>{ 
0xEE, 0x01, 0x00, 0x00 };
+    return response == expected_response;
+}
+
+
 export bool is_kreon_firmware(const DriveConfig &drive_config)
 {
     return drive_config.vendor_specific.starts_with("KREON V1.00");
@@ -479,4 +515,35 @@
 }
 
 
+export std::optional<uint32_t> is_spiraldrive_firmware(const DriveConfig 
&drive_config)
+{
+    constexpr std::string_view prefix = "SpiralDrive v";
+    if(!drive_config.vendor_specific.starts_with(prefix))
+        return std::nullopt;
+
+    auto version = 
tokenize(drive_config.vendor_specific.substr(prefix.size()), ".", nullptr);
+    if(version.size() != 3 || std::count(drive_config.vendor_specific.begin(), 
drive_config.vendor_specific.end(), '.') != 2)
+        return std::nullopt;
+
+    auto major = str_to_uint64(version[0]);
+    auto minor = str_to_uint64(version[1]);
+    auto patch = str_to_uint64(version[2]);
+    if(!major || !minor || !patch || *major > 0xff || *minor > 0xff || *patch 
> 0xff)
+        return std::nullopt;
+
+    return (uint32_t)((*major << 16) | (*minor << 8) | *patch);
+}
+
+
+export std::string spiraldrive_version_string(uint32_t version)
+{
+    return omnidrive_version_string(version);
+}
+
+
+export uint32_t spiraldrive_minimum_version()
+{
+    return 0x00010000;
+}
+
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/dvd/aacs.ixx 
new/redumper-b768/dvd/aacs.ixx
--- old/redumper-b752/dvd/aacs.ixx      1970-01-01 01:00:00.000000000 +0100
+++ new/redumper-b768/dvd/aacs.ixx      2026-10-23 03:56:38.000000000 +0200
@@ -0,0 +1,47 @@
+// based on aacskeys
+
+module;
+
+#include <cstdint>
+#include <cstring>
+
+export module bd.aacs;
+
+import filesystem.udf;
+import readers.data_reader;
+import utils.endian;
+
+
+
+namespace gpsxre
+{
+
+export class AACS
+{
+public:
+    static int getMKBVersion(DataReader *reader)
+    {
+        auto root_directory = udf::Browser::rootDirectory(reader);
+        if(!root_directory)
+        {
+            return -1;
+        }
+
+        auto media_key_block = root_directory->subEntry("AACS/MKB_RO.inf");
+        if(!media_key_block)
+        {
+            return -1;
+        }
+
+        auto data = media_key_block->read();
+
+        // The media key block version is stored as a big endian integer from 
0x00000008 to 0x0000000B
+        uint32_t mkbv;
+        memcpy(&mkbv, data.data() + 8, sizeof(uint32_t));
+        mkbv = endian_swap(mkbv);
+
+        return mkbv;
+    }
+};
+
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/dvd/bd_key.ixx 
new/redumper-b768/dvd/bd_key.ixx
--- old/redumper-b752/dvd/bd_key.ixx    1970-01-01 01:00:00.000000000 +0100
+++ new/redumper-b768/dvd/bd_key.ixx    2026-10-23 03:56:38.000000000 +0200
@@ -0,0 +1,37 @@
+module;
+#include <string>
+
+export module bd.key;
+
+import bd.aacs;
+import common;
+import filesystem.udf;
+import options;
+import readers.disc_read_reader;
+import utils.logger;
+
+
+
+namespace gpsxre
+{
+
+export int redumper_bdkey(Context &ctx, Options &options)
+{
+    int exit_code = 0;
+
+    if(ctx.disc_type != DiscType::BLURAY)
+        return exit_code;
+
+    Disc_READ_Reader reader(*ctx.sptd, 0);
+
+    int mkbv = AACS::getMKBVersion(&reader);
+    if(mkbv != -1)
+    {
+        LOG("BD-Video Protection: AACS (MKB version {})", mkbv);
+    }
+
+    return exit_code;
+}
+
+
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/dvd/dvd_dump.ixx 
new/redumper-b768/dvd/dvd_dump.ixx
--- old/redumper-b752/dvd/dvd_dump.ixx  2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/dvd/dvd_dump.ixx  2026-10-23 03:56:38.000000000 +0200
@@ -834,6 +834,10 @@
             LOG("kreon: failed to unlock drive, SCSI ({})", 
SPTD::StatusMessage(status));
     }
 
+    // disable jamless play on LG firmware to prevent false-negative read 
errors
+    if(is_jamless_play_firmware(ctx.drive_config) && 
!drive_set_jamless_play(*ctx.sptd, JamlessPlayMode::OFF, 
JamlessPlayPersist::TEMPORARY))
+        LOG("warning: failed to disable Jamless Play");
+
     // get capacity
     uint32_t sectors_count_capacity = get_sectors_count_capacity(*ctx.sptd);
 
@@ -979,7 +983,7 @@
                         for(uint32_t i = 0; i + 1 < layer_lengths.size(); ++i)
                         {
                             layer_break += layer_lengths[i];
-                            LOG("layer break{}: {}", layer_lengths.size() > 1 
? std::format(" (layer: {})", i) : "", layer_break);
+                            LOG("layer break{}: {}", layer_lengths.size() > 2 
? std::format(" (layer: {})", i) : "", layer_break);
                         }
 
                         LOG("");
@@ -1120,18 +1124,9 @@
     }
     else if(raw && omnidrive_firmware)
     {
-        if(ctx.disc_type == DiscType::DVD)
-        {
-            // ensure default total transfer length is less than 65536 bytes 
(31 * 2064)
-            LOG("warning: setting dump read size to 31 for raw DVD dumping");
-            dump_read_size = 31;
-        }
-        else if(ctx.disc_type == DiscType::BLURAY || ctx.disc_type == 
DiscType::BLURAY_R)
-        {
-            // ensure default total transfer length is less than 16384 (7 * 
2072)
-            LOG("warning: setting dump read size to 7 for raw BD dumping");
-            dump_read_size = 7;
-        }
+        // ensure default total transfer length is less than 65536 bytes (31 * 
2070)
+        LOG("warning: setting dump read size to 31 for raw dumping");
+        dump_read_size = 31;
     }
     else
         dump_read_size = DVD_READ_SIZE;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/filesystem/udf/udf.ixx 
new/redumper-b768/filesystem/udf/udf.ixx
--- old/redumper-b752/filesystem/udf/udf.ixx    2026-09-18 02:05:14.000000000 
+0200
+++ new/redumper-b768/filesystem/udf/udf.ixx    2026-10-23 03:56:38.000000000 
+0200
@@ -2,3 +2,4 @@
 
 export import :browser;
 export import :defs;
+export import :entry;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/filesystem/udf/udf_browser.ixx 
new/redumper-b768/filesystem/udf/udf_browser.ixx
--- old/redumper-b752/filesystem/udf/udf_browser.ixx    2026-09-18 
02:05:14.000000000 +0200
+++ new/redumper-b768/filesystem/udf/udf_browser.ixx    2026-10-23 
03:56:38.000000000 +0200
@@ -1,15 +1,14 @@
 module;
 #include <cstdint>
-#include <cstring>
 #include <memory>
 #include <optional>
 #include <string_view>
-#include <variant>
 #include <vector>
 
 export module filesystem.udf:browser;
 
 import :defs;
+import :entry;
 
 import cd.cdrom;
 import readers.data_reader;
@@ -59,7 +58,7 @@
         return {};
     }
 
-    static std::variant<std::shared_ptr<udf::FileEntry>, 
std::shared_ptr<udf::ExtendedFileEntry>> rootDirectory(DataReader *data_reader)
+    static std::shared_ptr<Entry> rootDirectory(DataReader *data_reader)
     {
         // Try and find the AVDP so we can look through the volume descriptor 
sequence.
         auto const avdp = findAnchorVolumeDescriptorPointer(data_reader);
@@ -129,29 +128,7 @@
                 auto const &file_set_descriptor = (udf::FileSetDescriptor 
&)fsd_data[0];
 
                 auto const &root_directory_icb_extent = 
file_set_descriptor.root_directory_icb;
-
-                // If we have the partition the root directory is in, read and 
return it.
-                if(partition_starting_locations.size() - 1 >= 
root_directory_icb_extent.extent_location.partition_reference_number)
-                {
-                    std::vector<uint8_t> root_directory_data(FORM1_DATA_SIZE);
-                    data_reader->read(root_directory_data.data(),
-                        
partition_starting_locations[root_directory_icb_extent.extent_location.partition_reference_number]
 + root_directory_icb_extent.extent_location.logical_block_number, 1);
-
-                    auto const &tag = (udf::DescriptorTag 
&)root_directory_data[0];
-                    if(tag.tag_identifier == udf::TagIdentifier::FILE_ENTRY)
-                    {
-                        auto root_directory_file_entry = 
std::shared_ptr<udf::FileEntry>(new udf::FileEntry());
-                        std::memcpy(root_directory_file_entry.get(), 
root_directory_data.data(), sizeof(udf::FileEntry));
-                        return root_directory_file_entry;
-                    }
-                    else if(tag.tag_identifier == 
udf::TagIdentifier::EXTENDED_FILE_ENTRY)
-                    {
-                        auto root_directory_file_entry = 
std::shared_ptr<udf::ExtendedFileEntry>(new udf::ExtendedFileEntry());
-                        std::memcpy(root_directory_file_entry.get(), 
root_directory_data.data(), sizeof(udf::ExtendedFileEntry));
-
-                        return root_directory_file_entry;
-                    }
-                }
+                return std::make_shared<Entry>(data_reader, "", 
root_directory_icb_extent, partition_starting_locations);
             }
         }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/filesystem/udf/udf_defs.ixx 
new/redumper-b768/filesystem/udf/udf_defs.ixx
--- old/redumper-b752/filesystem/udf/udf_defs.ixx       2026-09-18 
02:05:14.000000000 +0200
+++ new/redumper-b768/filesystem/udf/udf_defs.ixx       2026-10-23 
03:56:38.000000000 +0200
@@ -43,6 +43,16 @@
     EXTENDED_FILE_ENTRY = 266
 };
 
+// From ECMA 167 14.6.6, not fully filled out since we only handle DIRECTORY 
here anyway.
+enum class FileType : uint8_t
+{
+    UNSPECIFIED,
+    UNALLOCATED_SPACE_ENTRY,
+    PARTITION_INTEGRITY_ENTRY,
+    INDIRECT_ENTRY,
+    DIRECTORY
+};
+
 #pragma pack(push, 1)
 struct DescriptorTag
 {
@@ -196,11 +206,22 @@
     uint8_t strategy_parameter[2];
     uint16_t maximum_number_of_entries;
     uint8_t reserved;
-    uint8_t file_type;
+    FileType file_type;
     lb_addr parent_icb_location;
     uint16_t flags;
 };
 
+struct FileIdentifierDescriptor
+{
+    DescriptorTag descriptor_tag;
+    uint16_t file_version_number;
+    uint8_t file_characteristics;
+    uint8_t length_of_file_identifier;
+    long_ad icb;
+    uint16_t length_of_implementation_use;
+    uint8_t implementation_use_and_file_identifier_and_padding[];
+};
+
 struct FileEntry
 {
     DescriptorTag descriptor_tag;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/filesystem/udf/udf_entry.ixx 
new/redumper-b768/filesystem/udf/udf_entry.ixx
--- old/redumper-b752/filesystem/udf/udf_entry.ixx      1970-01-01 
01:00:00.000000000 +0100
+++ new/redumper-b768/filesystem/udf/udf_entry.ixx      2026-10-23 
03:56:38.000000000 +0200
@@ -0,0 +1,204 @@
+module;
+#include <cstdint>
+#include <cstring>
+#include <list>
+#include <memory>
+#include <string>
+#include <variant>
+#include <vector>
+
+export module filesystem.udf:entry;
+
+import :defs;
+import readers.data_reader;
+import cd.cdrom;
+import utils.misc;
+import utils.strings;
+
+
+
+namespace gpsxre::udf
+{
+
+static std::variant<std::shared_ptr<udf::FileEntry>, 
std::shared_ptr<udf::ExtendedFileEntry>> getFileEntry(DataReader *data_reader, 
std::vector<uint32_t> partition_starting_locations,
+    lb_addr file_entry_extent)
+{
+    if(partition_starting_locations.size() - 1 >= 
file_entry_extent.partition_reference_number)
+    {
+        std::vector<uint8_t> data(FORM1_DATA_SIZE);
+        data_reader->read(data.data(), 
partition_starting_locations[file_entry_extent.partition_reference_number] + 
file_entry_extent.logical_block_number, 1);
+
+        auto const &tag = (udf::DescriptorTag &)data[0];
+
+        if(tag.tag_identifier == udf::TagIdentifier::FILE_ENTRY)
+        {
+            // Parse the static part of the file entry and use it to determine 
the true size of the file entry.
+            auto const &static_file_entry = (udf::FileEntry &)data[0];
+            std::size_t true_entry_size = sizeof(udf::FileEntry) + 
static_file_entry.length_of_extended_attributes + 
static_file_entry.length_of_allocation_descriptors;
+
+            // Allocate the memory to store the entire file entry and read the 
file entry into that memory.
+            auto storage = std::make_shared<uint8_t[]>(true_entry_size);
+            auto file_entry = std::shared_ptr<udf::FileEntry>(storage, 
(udf::FileEntry *)storage.get());
+            std::memcpy(file_entry.get(), data.data(), true_entry_size);
+
+            return file_entry;
+        }
+        else if(tag.tag_identifier == udf::TagIdentifier::EXTENDED_FILE_ENTRY)
+        {
+            // Parse the static part of the file entry and use it to determine 
the true size of the file entry.
+            auto const &static_file_entry = (udf::ExtendedFileEntry &)data[0];
+            std::size_t true_entry_size = sizeof(udf::ExtendedFileEntry) + 
static_file_entry.length_of_extended_attributes + 
static_file_entry.length_of_allocation_descriptors;
+
+            // Allocate the memory to store the entire file entry and read the 
file entry into that memory.
+            auto storage = std::make_shared<uint8_t[]>(true_entry_size);
+            auto file_entry = std::shared_ptr<udf::ExtendedFileEntry>(storage, 
(udf::ExtendedFileEntry *)storage.get());
+            std::memcpy(file_entry.get(), data.data(), true_entry_size);
+
+            return file_entry;
+        }
+    }
+
+    return {};
+}
+
+}
+
+export namespace gpsxre::udf
+{
+
+class Entry
+{
+public:
+    Entry(DataReader *data_reader, std::string name, const udf::long_ad &icb, 
const std::vector<uint32_t> &partition_starting_locations)
+        : _dataReader(data_reader)
+        , _name(name)
+        , _icb(icb)
+        , _partitionStartingLocations(partition_starting_locations)
+        , _fileEntry(getFileEntry(data_reader, partition_starting_locations, 
icb.extent_location))
+    {
+        ;
+    }
+
+    const std::string &name() const
+    {
+        return _name;
+    }
+
+
+    bool isDirectory() const
+    {
+        return std::visit([](const auto &entry) { return 
entry->icb_tag.file_type == udf::FileType::DIRECTORY; }, _fileEntry);
+    }
+
+    std::list<std::shared_ptr<Entry>> entries()
+    {
+        std::list<std::shared_ptr<Entry>> entries;
+
+        if(isDirectory())
+        {
+            // Read the entire directory into memory.
+            auto const directory_data = read();
+
+            // Loop over each file identifier descriptor and record their 
matching entries.
+            uint32_t offset = 0;
+            while(offset < directory_data.size())
+            {
+                auto const &fid = (const udf::FileIdentifierDescriptor 
&)directory_data[offset];
+
+                // As per ECMA 167 14.4.3, use bit 3 of the file 
characteristics to determine if this descriptors ICB refers to the files parent 
directory,
+                // in which case we don't need to record it as an entry.
+                if((fid.file_characteristics & 0x08) == 0)
+                {
+                    // Read the file identifier into a string, ignoring the 
first byte as that contains the Compression ID of the string.
+                    const uint8_t *name_data = 
fid.implementation_use_and_file_identifier_and_padding + 
fid.length_of_implementation_use;
+                    // The file identifier is technically stored in the OSTA 
Compressed Unicode format, but for now we just ignore that.
+                    std::string name(name_data + 1, name_data + 
fid.length_of_file_identifier);
+
+                    entries.push_back(std::make_shared<Entry>(_dataReader, 
name, fid.icb, _partitionStartingLocations));
+                }
+
+                // Replicate the effect of the padding bytes at the end of 
FileIdentifierDescriptor as specified by ECMA 167 14.4.9
+                offset += round_up(sizeof(udf::FileIdentifierDescriptor) + 
fid.length_of_implementation_use + fid.length_of_file_identifier, 4u);
+            }
+        }
+
+        return entries;
+    }
+
+
+    std::shared_ptr<Entry> subEntry(const std::string &path)
+    {
+        std::shared_ptr<Entry> entry;
+
+        auto components = tokenize(path, "/\\", nullptr);
+        for(auto const &c : components)
+        {
+            auto directory_entries = entry ? entry->entries() : entries();
+            auto prospective_entry = std::ranges::find(directory_entries, c, 
[](auto const &e) { return e->name(); });
+
+            if(prospective_entry == directory_entries.end())
+            {
+                entry.reset();
+                break;
+            }
+            else
+            {
+                entry = *prospective_entry;
+            }
+        }
+
+        return entry;
+    }
+
+    std::vector<uint8_t> read()
+    {
+        // Determine how large the entry is and allocate the memory to read it 
into.
+        auto information_length = std::visit([](const auto &entry) -> uint32_t 
{ return entry->information_length; }, _fileEntry);
+        std::vector<uint8_t> data(round_up(information_length, 
FORM1_DATA_SIZE));
+
+        // Determine where the allocation descriptors start and how long they 
continue for.
+        const uint8_t *start_of_allocation_descriptors =
+            std::visit([](const auto &entry) -> const uint8_t * { return 
entry->extended_attributes_and_allocation_descriptors + 
entry->length_of_extended_attributes; }, _fileEntry);
+        auto length_of_allocation_descriptors = std::visit([](const auto 
&entry) -> uint32_t { return entry->length_of_allocation_descriptors; }, 
_fileEntry);
+
+        auto icb_tag = std::visit([](const auto &entry) -> auto { return 
entry->icb_tag; }, _fileEntry);
+        // As per ECMA 167 14.6.8, use bits 0-2 of the flags as a 3-bit 
unsigned binary number to determine the type of allocation descriptors.
+        auto short_descriptors = (icb_tag.flags & 0x07) == 0;
+        auto descriptor_size = short_descriptors ? sizeof(udf::short_ad) : 
sizeof(udf::long_ad);
+
+        uint64_t write_offset = 0;
+        // Loop over every allocation descriptor and read them into data.
+        for(uint32_t i = 0; i < length_of_allocation_descriptors; i += 
descriptor_size)
+        {
+            if(short_descriptors)
+            {
+                udf::short_ad ad = (udf::short_ad 
&)*(start_of_allocation_descriptors + i);
+                // TODO: It might be possible to avoid needing to use _icb 
here, which would allow us to remove it entirely.
+                auto lba = 
_partitionStartingLocations[_icb.extent_location.partition_reference_number] + 
ad.extent_position;
+
+                _dataReader->read(data.data() + write_offset, lba, 
scale_up(ad.extent_length, FORM1_DATA_SIZE));
+                write_offset += ad.extent_length;
+            }
+            else
+            {
+                udf::long_ad ad = (udf::long_ad 
&)*(start_of_allocation_descriptors + i);
+                auto lba = 
_partitionStartingLocations[ad.extent_location.partition_reference_number] + 
ad.extent_location.logical_block_number;
+
+                _dataReader->read(data.data() + write_offset, lba, 
scale_up(ad.extent_length, FORM1_DATA_SIZE));
+                write_offset += ad.extent_length;
+            }
+        }
+
+        data.resize(information_length);
+        return data;
+    }
+
+private:
+    DataReader *_dataReader;
+    std::string _name;
+    udf::long_ad _icb;
+    std::vector<uint32_t> _partitionStartingLocations;
+    std::variant<std::shared_ptr<udf::FileEntry>, 
std::shared_ptr<udf::ExtendedFileEntry>> _fileEntry;
+};
+
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/filesystem/udf/udf_size.ixx 
new/redumper-b768/filesystem/udf/udf_size.ixx
--- old/redumper-b752/filesystem/udf/udf_size.ixx       2026-09-18 
02:05:14.000000000 +0200
+++ new/redumper-b768/filesystem/udf/udf_size.ixx       2026-10-23 
03:56:38.000000000 +0200
@@ -24,7 +24,7 @@
     if(metadata_end >= std::numeric_limits<uint32_t>::max())
         return std::nullopt;
 
-    return metadata_end + 1;
+    return static_cast<uint32_t>(metadata_end + 1);
 }
 
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/hash/aes128.ixx 
new/redumper-b768/hash/aes128.ixx
--- old/redumper-b752/hash/aes128.ixx   1970-01-01 01:00:00.000000000 +0100
+++ new/redumper-b768/hash/aes128.ixx   2026-10-23 03:56:38.000000000 +0200
@@ -0,0 +1,121 @@
+module;
+#include <array>
+#include <cstdint>
+
+export module hash.aes128;
+
+
+
+namespace gpsxre
+{
+
+export class AES128
+{
+public:
+    static constexpr uint32_t BLOCK_SIZE = 16;
+
+    explicit AES128(const std::array<uint8_t, BLOCK_SIZE> &key)
+    {
+        for(uint32_t i = 0; i < BLOCK_SIZE; ++i)
+            _roundKeys[i] = key[i];
+
+        uint8_t round_constant = 1;
+        for(uint32_t offset = BLOCK_SIZE; offset < _roundKeys.size(); offset 
+= 4)
+        {
+            std::array<uint8_t, 4> word;
+            for(uint32_t i = 0; i < word.size(); ++i)
+                word[i] = _roundKeys[offset - word.size() + i];
+
+            if(offset % BLOCK_SIZE == 0)
+            {
+                uint8_t first = word[0];
+                word[0] = _SBOX[word[1]] ^ round_constant;
+                word[1] = _SBOX[word[2]];
+                word[2] = _SBOX[word[3]];
+                word[3] = _SBOX[first];
+                round_constant = xtime(round_constant);
+            }
+
+            for(uint32_t i = 0; i < word.size(); ++i)
+                _roundKeys[offset + i] = _roundKeys[offset - BLOCK_SIZE + i] ^ 
word[i];
+        }
+    }
+
+
+    std::array<uint8_t, BLOCK_SIZE> encryptBlock(const std::array<uint8_t, 
BLOCK_SIZE> &block) const
+    {
+        auto state = block;
+        addRoundKey(state, 0);
+
+        for(uint32_t round = 1; round < 10; ++round)
+        {
+            subBytes(state);
+            shiftRows(state);
+            mixColumns(state);
+            addRoundKey(state, round);
+        }
+
+        subBytes(state);
+        shiftRows(state);
+        addRoundKey(state, 10);
+        return state;
+    }
+
+private:
+    std::array<uint8_t, 11 * BLOCK_SIZE> _roundKeys;
+
+    static constexpr std::array<uint8_t, 256> _SBOX{ 0x63, 0x7C, 0x77, 0x7B, 
0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 
0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0,
+        0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 
0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A,
+        0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 
0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B,
+        0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 
0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5,
+        0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 
0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88,
+        0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 
0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9,
+        0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 
0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E,
+        0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 
0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68,
+        0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 };
+
+    static uint8_t xtime(uint8_t value)
+    {
+        return (uint8_t)((value << 1) ^ (value & 0x80 ? 0x1B : 0));
+    }
+
+    void addRoundKey(std::array<uint8_t, BLOCK_SIZE> &state, uint32_t round) 
const
+    {
+        for(uint32_t i = 0; i < BLOCK_SIZE; ++i)
+            state[i] ^= _roundKeys[round * BLOCK_SIZE + i];
+    }
+
+    static void subBytes(std::array<uint8_t, BLOCK_SIZE> &state)
+    {
+        for(auto &byte : state)
+            byte = _SBOX[byte];
+    }
+
+    static void shiftRows(std::array<uint8_t, BLOCK_SIZE> &state)
+    {
+        auto old = state;
+        for(uint32_t column = 0; column < 4; ++column)
+            for(uint32_t row = 1; row < 4; ++row)
+                state[4 * column + row] = old[4 * ((column + row) % 4) + row];
+    }
+
+    static void mixColumns(std::array<uint8_t, BLOCK_SIZE> &state)
+    {
+        for(uint32_t column = 0; column < 4; ++column)
+        {
+            uint32_t offset = 4 * column;
+            uint8_t a = state[offset];
+            uint8_t b = state[offset + 1];
+            uint8_t c = state[offset + 2];
+            uint8_t d = state[offset + 3];
+            uint8_t all = a ^ b ^ c ^ d;
+
+            state[offset] ^= all ^ xtime(a ^ b);
+            state[offset + 1] ^= all ^ xtime(b ^ c);
+            state[offset + 2] ^= all ^ xtime(c ^ d);
+            state[offset + 3] ^= all ^ xtime(d ^ a);
+        }
+    }
+};
+
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/hash/cmac.ixx 
new/redumper-b768/hash/cmac.ixx
--- old/redumper-b752/hash/cmac.ixx     1970-01-01 01:00:00.000000000 +0100
+++ new/redumper-b768/hash/cmac.ixx     2026-10-23 03:56:38.000000000 +0200
@@ -0,0 +1,111 @@
+module;
+#include <algorithm>
+#include <array>
+#include <cstdint>
+
+export module hash.cmac;
+
+import hash.aes128;
+
+
+
+namespace gpsxre
+{
+
+export class CMAC
+{
+public:
+    explicit CMAC(const std::array<uint8_t, AES128::BLOCK_SIZE> &key)
+        : _aes(key)
+    {
+        std::array<uint8_t, AES128::BLOCK_SIZE> zero_block;
+        zero_block.fill(0);
+        auto l = _aes.encryptBlock(zero_block);
+        _subkey1 = doubleBlock(l);
+        _subkey2 = doubleBlock(_subkey1);
+        reset();
+    }
+
+    CMAC &reset()
+    {
+        _chain.fill(0);
+        _tail.fill(0);
+        _tailSize = 0;
+        return *this;
+    }
+
+    CMAC &update(const uint8_t *data, uint64_t size)
+    {
+        while(size != 0)
+        {
+            if(_tailSize == AES128::BLOCK_SIZE)
+            {
+                updateBlock(_tail);
+                _tail.fill(0);
+                _tailSize = 0;
+            }
+
+            uint32_t count = (uint32_t)std::min<uint64_t>(AES128::BLOCK_SIZE - 
_tailSize, size);
+            std::copy_n(data, count, _tail.begin() + _tailSize);
+            data += count;
+            size -= count;
+            _tailSize += count;
+        }
+        return *this;
+    }
+
+    std::array<uint8_t, AES128::BLOCK_SIZE> final()
+    {
+        auto last = _tail;
+        if(_tailSize == AES128::BLOCK_SIZE)
+        {
+            for(uint32_t i = 0; i < AES128::BLOCK_SIZE; ++i)
+                last[i] ^= _subkey1[i];
+        }
+        else
+        {
+            last[_tailSize] = 0x80;
+            for(uint32_t i = 0; i < AES128::BLOCK_SIZE; ++i)
+                last[i] ^= _subkey2[i];
+        }
+
+        for(uint32_t i = 0; i < AES128::BLOCK_SIZE; ++i)
+            last[i] ^= _chain[i];
+
+        auto result = _aes.encryptBlock(last);
+        reset();
+        return result;
+    }
+
+private:
+    AES128 _aes;
+    std::array<uint8_t, AES128::BLOCK_SIZE> _subkey1;
+    std::array<uint8_t, AES128::BLOCK_SIZE> _subkey2;
+    std::array<uint8_t, AES128::BLOCK_SIZE> _chain;
+    std::array<uint8_t, AES128::BLOCK_SIZE> _tail;
+    uint32_t _tailSize;
+
+    static std::array<uint8_t, AES128::BLOCK_SIZE> doubleBlock(const 
std::array<uint8_t, AES128::BLOCK_SIZE> &block)
+    {
+        std::array<uint8_t, AES128::BLOCK_SIZE> doubled;
+        uint8_t carry = 0;
+        for(uint32_t i = AES128::BLOCK_SIZE; i > 0; --i)
+        {
+            doubled[i - 1] = (uint8_t)((block[i - 1] << 1) | carry);
+            carry = block[i - 1] >> 7;
+        }
+        if(carry)
+            doubled.back() ^= 0x87;
+        return doubled;
+    }
+
+    void updateBlock(const std::array<uint8_t, AES128::BLOCK_SIZE> &block)
+    {
+        auto input = block;
+        for(uint32_t i = 0; i < AES128::BLOCK_SIZE; ++i)
+            input[i] ^= _chain[i];
+        _chain = _aes.encryptBlock(input);
+    }
+};
+
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/options.ixx 
new/redumper-b768/options.ixx
--- old/redumper-b752/options.ixx       2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/options.ixx       2026-10-23 03:56:38.000000000 +0200
@@ -431,11 +431,11 @@
         LOG("\t--disc-type=VALUE               \toverride detected disc type 
(current profile), possible values: CD, DVD, BLURAY, BLURAY-R, HD-DVD");
         LOG("");
         LOG("\t(drive configuration)");
-        LOG("\t--drive-type=VALUE              \toverride drive type, possible 
values: GENERIC, PLEXTOR, MTK2, MTK3, MTK8A, MTK8B, MTK8C");
+        LOG("\t--drive-type=VALUE              \toverride drive type, possible 
values: GENERIC, PLEXTOR, MTK2, MTK2B, MTK3, MTK8A, MTK8B, MTK8C");
         LOG("\t--drive-read-offset=VALUE       \toverride drive read offset");
         LOG("\t--drive-c2-shift=VALUE          \toverride drive C2 shift");
         LOG("\t--drive-pregap-start=VALUE      \toverride drive pre-gap start 
LBA");
-        LOG("\t--drive-read-method=VALUE       \toverride drive read method, 
possible values: BE, D8, BE_CDDA");
+        LOG("\t--drive-read-method=VALUE       \toverride drive read method, 
possible values: BE, D8");
         LOG("\t--drive-sector-order=VALUE      \toverride drive sector order, 
possible values: DATA_C2_SUB, DATA_SUB_C2, DATA_SUB, DATA_C2");
         LOG("\t--auto-detect                   \tattempt to automatically 
detect drive sector order for generic drives");
         LOG("");
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/redumper.ixx 
new/redumper-b768/redumper.ixx
--- old/redumper-b752/redumper.ixx      2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/redumper.ixx      2026-10-23 03:56:38.000000000 +0200
@@ -14,6 +14,7 @@
 
 export module redumper;
 
+import bd.key;
 import cd.cd;
 import cd.dump;
 import cd.dump_extra;
@@ -131,6 +132,7 @@
     { "dump::extra",          { true, true, true, true, false, 
redumper_dump_extra }       },
     { "refine",               { true, true, true, true, false, redumper_refine 
}           },
     { "dvdkey",               { true, true, true, false, false, 
redumper_dvdkey }          },
+    { "bdkey",                { true, true, true, false, false, redumper_bdkey 
}           },
     { "eject",                { true, false, false, false, false, 
redumper_eject }         },
     { "dvdisokey",            { false, false, false, true, false, 
redumper_dvdisokey }     },
     { "protection",           { false, false, false, true, false, 
redumper_protection }    },
@@ -527,6 +529,13 @@
             else
                 LOG("  firmware: OmniDrive {}", 
omnidrive_version_string(*version));
         }
+        else if(auto version = is_spiraldrive_firmware(ctx.drive_config))
+        {
+            if(*version < spiraldrive_minimum_version())
+                LOG("  warning: outdated SpiralDrive drive firmware (current: 
{}, recommended: {})", spiraldrive_version_string(*version), 
spiraldrive_version_string(spiraldrive_minimum_version()));
+            else
+                LOG("  firmware: SpiralDrive {}", 
spiraldrive_version_string(*version));
+        }
     }
 
     if(!options.image_name.empty())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/scsi/cmd.ixx 
new/redumper-b768/scsi/cmd.ixx
--- old/redumper-b752/scsi/cmd.ixx      2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/scsi/cmd.ixx      2026-10-23 03:56:38.000000000 +0200
@@ -273,7 +273,20 @@
     cdb.include_sync_data = expected_sector_type == 
READ_CD_ExpectedSectorType::CD_DA ? 0 : 1;
     cdb.sub_channel_selection = (uint8_t)sub_channel;
 
-    return sptd.sendCommand(&cdb, sizeof(cdb), sectors, block_size * 
transfer_length).first;
+    // request exactly what the drive will return: some USB bridges (e.g. 
ASMedia ASM1153) corrupt
+    // data when the allocation length exceeds the actual transfer
+    uint32_t sector_size = CD_DATA_SIZE;
+    if(error_field == READ_CD_ErrorField::C2)
+        sector_size += CD_C2_SIZE;
+    else if(error_field == READ_CD_ErrorField::C2_BEB)
+        sector_size += CD_C2_SIZE + 2;
+    if(sub_channel == READ_CD_SubChannel::Q)
+        sector_size += 16;
+    else if(sub_channel != READ_CD_SubChannel::NONE)
+        sector_size += CD_SUBCODE_SIZE;
+    sector_size = std::min(sector_size, block_size);
+
+    return sptd.sendCommand(&cdb, sizeof(cdb), sectors, sector_size * 
transfer_length).first;
 }
 
 
@@ -288,7 +301,7 @@
     cdb.sub_code = (uint8_t)sub_code;
 
     auto [status, transferred_length] = sptd.sendCommand(&cdb, sizeof(cdb), 
sectors, block_size * transfer_length);
-    if(!status.status_code && transferred_length != 
READ_CDDA_SIZES[(uint8_t)sub_code] * transfer_length)
+    if(!status.status_code && transferred_length < 
READ_CDDA_SIZES[(uint8_t)sub_code] * transfer_length)
         status.status_code = SPTD::HOST_SHORT_TRANSFER;
 
     return status;
@@ -353,6 +366,33 @@
 }
 
 
+export SPTD::Status cmd_get_configuration_firmware_information(SPTD &sptd, 
GET_CONFIGURATION_FirmwareInformationBody *firmware_information)
+{
+    CDB10_GetConfiguration cdb = {};
+    cdb.operation_code = (uint8_t)CDB_OperationCode::GET_CONFIGURATION;
+    cdb.requested_type = (uint8_t)GET_CONFIGURATION_RequestedType::ONE;
+    cdb.starting_feature_number = endian_swap<uint16_t>(0x10C);
+
+    uint16_t size = sizeof(GET_CONFIGURATION_FeatureHeader) + 
sizeof(GET_CONFIGURATION_FeatureDescriptor) + 
sizeof(GET_CONFIGURATION_FirmwareInformationBody);
+    *(uint16_t *)cdb.allocation_length = endian_swap(size);
+    std::vector<uint8_t> buffer(size);
+
+    auto status = sptd.sendCommand(&cdb, sizeof(cdb), buffer.data(), 
buffer.size()).first;
+
+    GET_CONFIGURATION_FeatureHeader feature_header = {};
+    memcpy(&feature_header, buffer.data(), 
sizeof(GET_CONFIGURATION_FeatureHeader));
+    if(!status.status_code && feature_header.data_length == 4)
+    {
+        status.status_code = SPTD::HOST_SHORT_TRANSFER;
+        return status;
+    }
+
+    memcpy(firmware_information, buffer.data() + 
sizeof(GET_CONFIGURATION_FeatureHeader) + 
sizeof(GET_CONFIGURATION_FeatureDescriptor), sizeof(*firmware_information));
+
+    return status;
+}
+
+
 SPTD::Status cmd_get_configuration(SPTD &sptd)
 {
     CDB10_GetConfiguration cdb = {};
@@ -443,11 +483,12 @@
 }
 
 
-export SPTD::Status cmd_write_buffer(SPTD &sptd, const uint8_t *data, uint32_t 
data_size, WRITE_BUFFER_Mode mode, uint32_t buffer_offset, uint32_t 
parameter_list_length)
+export SPTD::Status cmd_write_buffer(SPTD &sptd, const uint8_t *data, uint32_t 
data_size, WRITE_BUFFER_Mode mode, uint8_t buffer_id, uint32_t buffer_offset, 
uint32_t parameter_list_length)
 {
     CDB10_WriteBuffer cdb = {};
     cdb.operation_code = (uint8_t)CDB_OperationCode::WRITE_BUFFER;
     cdb.mode = (uint8_t)mode;
+    cdb.buffer_id = buffer_id;
     cdb.buffer_offset[0] = ((uint8_t *)&buffer_offset)[2];
     cdb.buffer_offset[1] = ((uint8_t *)&buffer_offset)[1];
     cdb.buffer_offset[2] = ((uint8_t *)&buffer_offset)[0];
@@ -459,11 +500,12 @@
 }
 
 
-export SPTD::Status cmd_read_buffer(SPTD &sptd, uint8_t *data, uint32_t 
data_size, READ_BUFFER_Mode mode, uint32_t buffer_offset, uint32_t 
allocation_length)
+export SPTD::Status cmd_read_buffer(SPTD &sptd, uint8_t *data, uint32_t 
data_size, READ_BUFFER_Mode mode, uint8_t buffer_id, uint32_t buffer_offset, 
uint32_t allocation_length)
 {
     CDB10_ReadBuffer cdb = {};
     cdb.operation_code = (uint8_t)CDB_OperationCode::READ_BUFFER;
     cdb.mode = (uint8_t)mode;
+    cdb.buffer_id = buffer_id;
     cdb.buffer_offset[0] = ((uint8_t *)&buffer_offset)[2];
     cdb.buffer_offset[1] = ((uint8_t *)&buffer_offset)[1];
     cdb.buffer_offset[2] = ((uint8_t *)&buffer_offset)[0];
@@ -489,7 +531,24 @@
     *(int32_t *)cdb.address = endian_swap(address);
     *(uint32_t *)cdb.transfer_blocks = endian_swap(transfer_length);
 
-    return sptd.sendCommand(&cdb, sizeof(cdb), buffer, block_size * 
transfer_length).first;
+    auto [status, transferred] = sptd.sendCommand(&cdb, sizeof(cdb), buffer, 
block_size * transfer_length);
+
+    if(!status.status_code && transferred != block_size * transfer_length)
+        status.status_code = SPTD::HOST_SHORT_TRANSFER;
+
+    return status;
+}
+
+
+export SPTD::Status cmd_spiraldrive(SPTD &sptd, uint8_t *buffer, uint32_t 
buffer_size, SpiralDrive_Operation operation, uint16_t timeout, int32_t value)
+{
+    CDB12_SpiralDrive cdb = {};
+    cdb.operation_code = (uint8_t)CDB_OperationCode::SPIRALDRIVE;
+    cdb.operation = (uint8_t)operation;
+    cdb.timeout = endian_swap(timeout);
+    cdb.value = endian_swap(value);
+
+    return sptd.sendCommand(&cdb, sizeof(cdb), buffer, buffer_size).first;
 }
 
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/scsi/mmc.ixx 
new/redumper-b768/scsi/mmc.ixx
--- old/redumper-b752/scsi/mmc.ixx      2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/scsi/mmc.ixx      2026-10-23 03:56:38.000000000 +0200
@@ -28,6 +28,7 @@
     READ_CD_MSF = 0xB9,
     READ_CD = 0xBE,
     READ_OMNIDRIVE = 0xC0,
+    SPIRALDRIVE = 0xC4,
     READ_CD_MSF_D5 = 0xD5,
     READ_CDDA = 0xD8,
     PLEXTOR_RESET = 0xEE,
@@ -286,6 +287,13 @@
 };
 
 
+export enum class SpiralDrive_Operation : uint8_t
+{
+    SEEK = 1,
+    READ = 2
+};
+
+
 export struct CMD_ParameterListHeader
 {
     uint16_t data_length;
@@ -397,6 +405,17 @@
     uint8_t feature_dependent_data[0];
 };
 
+export struct GET_CONFIGURATION_FirmwareInformationBody
+{
+    char century[2];
+    char year[2];
+    char month[2];
+    char day[2];
+    char hour[2];
+    char minutes[2];
+    char seconds[2];
+    uint8_t reserved[2];
+};
 
 export struct READ_DVD_STRUCTURE_StructureListEntry
 {
@@ -914,7 +933,8 @@
     DOWNLOAD_MICROCODE_WITH_OFFSETS_SAVE,
     ECHO_BUFFER = 0x0A,
     ECHO_BUFFER_DESCRIPTOR,
-    RESERVED2
+    RESERVED2,
+    LG_JAMLESS_PLAY_CONTROL
 };
 
 
@@ -946,4 +966,14 @@
     uint8_t control;
 };
 
+
+export struct CDB12_SpiralDrive
+{
+    uint8_t operation_code;
+    uint8_t operation;
+    uint16_t timeout;
+    uint32_t value;
+    uint32_t reserved;
+};
+
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/scsi/sptd.ixx 
new/redumper-b768/scsi/sptd.ixx
--- old/redumper-b752/scsi/sptd.ixx     2026-09-18 02:05:14.000000000 +0200
+++ new/redumper-b768/scsi/sptd.ixx     2026-10-23 03:56:38.000000000 +0200
@@ -54,6 +54,7 @@
         uint8_t sense_key;
         uint8_t asc;
         uint8_t ascq;
+        uint8_t information[4];
     };
 
 
@@ -187,6 +188,7 @@
             status.sense_key = sptd_sd.sd.SenseKey;
             status.asc = sptd_sd.sd.AdditionalSenseCode;
             status.ascq = sptd_sd.sd.AdditionalSenseCodeQualifier;
+            memcpy(status.information, sptd_sd.sd.Information, 
sizeof(sptd_sd.sd.Information));
         }
 #elif defined(__APPLE__)
         auto task = 
make_unique_resource_checked((*_scsiTaskDeviceInterface.get())->CreateSCSITask(_scsiTaskDeviceInterface.get()),
 (SCSITaskInterface **)nullptr,
@@ -226,6 +228,10 @@
             status.sense_key = sense_data.SENSE_KEY & 0x0F;
             status.asc = sense_data.ADDITIONAL_SENSE_CODE;
             status.ascq = sense_data.ADDITIONAL_SENSE_CODE_QUALIFIER;
+            status.information[0] = sense_data.INFORMATION_1;
+            status.information[1] = sense_data.INFORMATION_2;
+            status.information[2] = sense_data.INFORMATION_3;
+            status.information[3] = sense_data.INFORMATION_4;
         }
 #else
         SenseData sense_data;
@@ -253,6 +259,7 @@
             status.sense_key = sense_data.sense_key;
             status.asc = sense_data.additional_sense_code;
             status.ascq = sense_data.additional_sense_code_qualifier;
+            memcpy(status.information, sense_data.information, 
sizeof(sense_data.information));
         }
 #endif
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/tests/gtest/CMakeLists.txt 
new/redumper-b768/tests/gtest/CMakeLists.txt
--- old/redumper-b752/tests/gtest/CMakeLists.txt        2026-09-18 
02:05:14.000000000 +0200
+++ new/redumper-b768/tests/gtest/CMakeLists.txt        2026-10-23 
03:56:38.000000000 +0200
@@ -76,6 +76,13 @@
         "${CMAKE_SOURCE_DIR}/utils/misc.ixx"
 )
 
+add_gtest(gtest_aes_cmac
+    SOURCE  "test_aes_cmac.cc"
+    MODULES
+        "${CMAKE_SOURCE_DIR}/hash/aes128.ixx"
+        "${CMAKE_SOURCE_DIR}/hash/cmac.ixx"
+)
+
 add_gtest(gtest_range
     SOURCE  "test_range.cc"
     MODULES "${CMAKE_SOURCE_DIR}/range.ixx"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/tests/gtest/test_aes_cmac.cc 
new/redumper-b768/tests/gtest/test_aes_cmac.cc
--- old/redumper-b752/tests/gtest/test_aes_cmac.cc      1970-01-01 
01:00:00.000000000 +0100
+++ new/redumper-b768/tests/gtest/test_aes_cmac.cc      2026-10-23 
03:56:38.000000000 +0200
@@ -0,0 +1,68 @@
+#include <algorithm>
+#include <array>
+#include <cstdint>
+#include <gtest/gtest.h>
+#include <string_view>
+#include <vector>
+
+import hash.aes128;
+import hash.cmac;
+
+using namespace gpsxre;
+
+
+uint8_t hexDigit(char c)
+{
+    return (uint8_t)(c <= '9' ? c - '0' : c - 'a' + 10);
+}
+
+std::vector<uint8_t> hexBytes(std::string_view hex)
+{
+    std::vector<uint8_t> bytes;
+    for(uint32_t i = 0; i < hex.size(); i += 2)
+        bytes.push_back((uint8_t)((hexDigit(hex[i]) << 4) | hexDigit(hex[i + 
1])));
+    return bytes;
+}
+
+std::array<uint8_t, AES128::BLOCK_SIZE> hexBlock(std::string_view hex)
+{
+    auto bytes = hexBytes(hex);
+    std::array<uint8_t, AES128::BLOCK_SIZE> block;
+    std::copy(bytes.begin(), bytes.end(), block.begin());
+    return block;
+}
+
+const auto CMAC_KEY = hexBlock("2b7e151628aed2a6abf7158809cf4f3c");
+const auto CMAC_MESSAGE = hexBytes("6bc1bee22e409f96e93d7e117393172a" 
"ae2d8a571e03ac9c9eb76fac45af8e51" "30c81c46a35ce411e5fbc1191a0a52ef" 
"f69f2445df4f9b17ad2b417be66c3710");
+
+
+TEST(Aes128, EncryptBlock)
+{
+    AES128 aes(hexBlock("000102030405060708090a0b0c0d0e0f"));
+    EXPECT_EQ(aes.encryptBlock(hexBlock("00112233445566778899aabbccddeeff")), 
hexBlock("69c4e0d86a7b0430d8cdb78070b4c55a"));
+}
+
+
+TEST(Cmac, NistKnownAnswers)
+{
+    EXPECT_EQ(CMAC(CMAC_KEY).final(), 
hexBlock("bb1d6929e95937287fa37d129b756746"));
+    EXPECT_EQ(CMAC(CMAC_KEY).update(CMAC_MESSAGE.data(), 16).final(), 
hexBlock("070a16b46b4d4144f79bdd9dd04a287c"));
+    EXPECT_EQ(CMAC(CMAC_KEY).update(CMAC_MESSAGE.data(), 40).final(), 
hexBlock("dfa66747de9ae63030ca32611497c827"));
+    EXPECT_EQ(CMAC(CMAC_KEY).update(CMAC_MESSAGE.data(), 64).final(), 
hexBlock("51f0bebf7e3b9d92fc49741779363cfe"));
+}
+
+
+TEST(Cmac, SplitUpdatesAndReset)
+{
+    auto expected = hexBlock("dfa66747de9ae63030ca32611497c827");
+    CMAC cmac(CMAC_KEY);
+    cmac.update(CMAC_MESSAGE.data(), 1).update(CMAC_MESSAGE.data() + 1, 
15).update(CMAC_MESSAGE.data() + 16, 16).update(CMAC_MESSAGE.data() + 32, 8);
+    EXPECT_EQ(cmac.final(), expected);
+
+    // final() resets the message state while retaining the key.
+    EXPECT_EQ(cmac.update(CMAC_MESSAGE.data(), 40).final(), expected);
+
+    cmac.update(CMAC_MESSAGE.data(), 7).reset();
+    EXPECT_EQ(cmac.update(CMAC_MESSAGE.data(), 40).final(), expected);
+    EXPECT_EQ(cmac.update(nullptr, 0).final(), 
hexBlock("bb1d6929e95937287fa37d129b756746"));
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redumper-b752/tests/gtest/test_udf.cc 
new/redumper-b768/tests/gtest/test_udf.cc
--- old/redumper-b752/tests/gtest/test_udf.cc   2026-09-18 02:05:14.000000000 
+0200
+++ new/redumper-b768/tests/gtest/test_udf.cc   2026-10-23 03:56:38.000000000 
+0200
@@ -36,3 +36,10 @@
     EXPECT_EQ(udf::get_volume_sectors_count(1000, 0, 0, 0), std::nullopt);
     EXPECT_EQ(udf::get_volume_sectors_count(1000, 
std::numeric_limits<uint32_t>::max(), 2048, 2048), std::nullopt);
 }
+
+
+TEST(UDF, VolumeSectorsCountHandlesUpperBoundary)
+{
+    
EXPECT_EQ(udf::get_volume_sectors_count(std::numeric_limits<uint32_t>::max() - 
1, 0, 0, 2048), std::numeric_limits<uint32_t>::max());
+    
EXPECT_EQ(udf::get_volume_sectors_count(std::numeric_limits<uint32_t>::max(), 
0, 0, 2048), std::nullopt);
+}

Reply via email to