diff --git a/Aaru.Images/Localization/Localization.Designer.cs b/Aaru.Images/Localization/Localization.Designer.cs
index cceab5663..254fad829 100644
--- a/Aaru.Images/Localization/Localization.Designer.cs
+++ b/Aaru.Images/Localization/Localization.Designer.cs
@@ -6135,5 +6135,17 @@ namespace Aaru.Images {
return ResourceManager.GetString("MagicIso_NRG_trailer_has_no_tracks", resourceCulture);
}
}
+
+ internal static string Verifying_NDIF_image_CRC {
+ get {
+ return ResourceManager.GetString("Verifying_NDIF_image_CRC", resourceCulture);
+ }
+ }
+
+ internal static string NDIF_CRC_mismatch_expected_0_got_1 {
+ get {
+ return ResourceManager.GetString("NDIF_CRC_mismatch_expected_0_got_1", resourceCulture);
+ }
+ }
}
}
diff --git a/Aaru.Images/Localization/Localization.es.resx b/Aaru.Images/Localization/Localization.es.resx
index 6de37284d..868a4352f 100644
--- a/Aaru.Images/Localization/Localization.es.resx
+++ b/Aaru.Images/Localization/Localization.es.resx
@@ -3060,4 +3060,10 @@
El bloque NRG incrustado en la imagen UIF de MagicISO no contiene pistas.
+
+ [slateblue1]Verificando el CRC de la imagen NDIF…[/]
+
+
+ CRC de la imagen NDIF no coincide: se esperaba 0x{0:X7}, se obtuvo 0x{1:X7}
+
\ No newline at end of file
diff --git a/Aaru.Images/Localization/Localization.resx b/Aaru.Images/Localization/Localization.resx
index 69b7f6491..0a8960666 100644
--- a/Aaru.Images/Localization/Localization.resx
+++ b/Aaru.Images/Localization/Localization.resx
@@ -3079,4 +3079,10 @@
The NRG trailer embedded in the MagicISO UIF image has no tracks.
+
+ [slateblue1]Verifying NDIF image CRC…[/]
+
+
+ NDIF image CRC mismatch: expected 0x{0:X7}, got 0x{1:X7}
+
\ No newline at end of file
diff --git a/Aaru.Images/NDIF/NDIF.cs b/Aaru.Images/NDIF/NDIF.cs
index bc25863d9..f87192348 100644
--- a/Aaru.Images/NDIF/NDIF.cs
+++ b/Aaru.Images/NDIF/NDIF.cs
@@ -38,12 +38,10 @@ using Aaru.CommonTypes.Structs;
namespace Aaru.Images;
// TODO: Detect OS X encrypted images
-// TODO: Check checksum
// TODO: Implement segments
-// TODO: Implement compression
-///
+///
/// Implements reading Apple New Disk Image Format disk images
-public sealed partial class Ndif : IMediaImage
+public sealed partial class Ndif : IMediaImage, IVerifiableImage
{
const string MODULE_NAME = "NDIF plugin";
uint _bufferSize;
diff --git a/Aaru.Images/NDIF/NdifCrc28.cs b/Aaru.Images/NDIF/NdifCrc28.cs
new file mode 100644
index 000000000..beb1a4eaa
--- /dev/null
+++ b/Aaru.Images/NDIF/NdifCrc28.cs
@@ -0,0 +1,104 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : NdifCrc28.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Implements the 28-bit CRC used to protect Apple New Disk Image Format
+// images.
+//
+// Reverse-engineered from the 68k code fragment (`oneb` 128) embedded in
+// the Apple DiskCopy 6.3.3 resource fork (exports `.CRC28_Initialize`,
+// `.CRC28_Start`, `.CRC28_ProcessBuffer`, `.CRC28_Finish`):
+//
+// table[i] = i
+// for each of 8 bits: if (table[i] & 1)
+// table[i] = (table[i] >> 1) ^ 0x04C11DB7
+// else
+// table[i] >>= 1
+//
+// crc = 0xFFFFFFFF
+// for each byte b of the decoded sector stream:
+// crc = (crc >> 8) ^ table[(crc ^ b) & 0xFF]
+//
+// CRC28_Finish is a no-op; the low 28 bits of the accumulator are what the
+// NDIF `bcem` header stores in its `crc` field.
+//
+// Buffer contents are the reconstructed 512-byte sectors of the image, in
+// image order; `CHUNK_TYPE_NOCOPY` (and otherwise unwritten) sectors feed
+// in as zeros.
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+namespace Aaru.Images;
+
+/// Table-driven implementation of Apple NDIF's 28-bit CRC.
+static class NdifCrc28
+{
+ const uint POLYNOMIAL = 0x04C11DB7;
+ const uint SEED = 0xFFFFFFFF;
+ const uint MASK_28 = 0x0FFFFFFF;
+
+ static readonly uint[] _table = BuildTable();
+
+ static uint[] BuildTable()
+ {
+ var table = new uint[256];
+
+ for(uint i = 0; i < 256; i++)
+ {
+ uint c = i;
+
+ for(var j = 0; j < 8; j++)
+ {
+ if((c & 1) != 0)
+ c = c >> 1 ^ POLYNOMIAL;
+ else
+ c >>= 1;
+ }
+
+ table[i] = c;
+ }
+
+ return table;
+ }
+
+ /// Returns the initial CRC accumulator seed.
+ public static uint Init() => SEED;
+
+ ///
+ /// Feeds bytes from into the running
+ /// .
+ ///
+ public static uint Update(uint crc, byte[] buffer, int length)
+ {
+ for(var i = 0; i < length; i++) crc = crc >> 8 ^ _table[(crc ^ buffer[i]) & 0xFF];
+
+ return crc;
+ }
+
+ /// Applies the final 28-bit mask to , yielding the value stored in the NDIF header.
+ public static uint Finish(uint crc) => crc & MASK_28;
+}
\ No newline at end of file
diff --git a/Aaru.Images/NDIF/Verify.cs b/Aaru.Images/NDIF/Verify.cs
new file mode 100644
index 000000000..4169329a3
--- /dev/null
+++ b/Aaru.Images/NDIF/Verify.cs
@@ -0,0 +1,105 @@
+// /***************************************************************************
+// Aaru Data Preservation Suite
+// ----------------------------------------------------------------------------
+//
+// Filename : Verify.cs
+// Author(s) : Natalia Portillo
+//
+// Component : Disk image plugins.
+//
+// --[ Description ] ----------------------------------------------------------
+//
+// Verifies the internal 28-bit CRC of an Apple New Disk Image Format
+// image.
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2011-2026 Natalia Portillo
+// ****************************************************************************/
+
+using System.Linq;
+using Aaru.CommonTypes.Enums;
+using Aaru.Logging;
+
+namespace Aaru.Images;
+
+public sealed partial class Ndif
+{
+#region IVerifiableImage Members
+
+ ///
+ /// Verifies the image against the 28-bit CRC stored in the NDIF bcem header. The CRC is computed over the
+ /// reconstructed 512-byte-per-sector stream in image order; CHUNK_TYPE_NOCOPY sectors feed in as zeros,
+ /// matching Apple DiskCopy's behaviour.
+ ///
+ ///
+ /// true if the recomputed CRC matches the one stored in the header; false otherwise; null when
+ /// there are no chunks to verify.
+ ///
+ public bool? VerifyMediaImage()
+ {
+ if(_chunks is null || _chunks.Count == 0) return null;
+
+ AaruLogging.WriteLine(Localization.Verifying_NDIF_image_CRC);
+
+ var zeroSector = new byte[SECTOR_SIZE];
+ uint crc = NdifCrc28.Init();
+
+ // NDIF BlockChunk does not carry an explicit sector count, so we derive it from the start of the next chunk (or
+ // the image total for the last one) after sorting by starting sector.
+ var ordered = _chunks.OrderBy(static kvp => kvp.Key).ToList();
+
+ for(var i = 0; i < ordered.Count; i++)
+ {
+ ulong start = ordered[i].Key;
+ BlockChunk chunk = ordered[i].Value;
+
+ if(chunk.type == CHUNK_TYPE_END) continue;
+
+ ulong end = i + 1 < ordered.Count ? ordered[i + 1].Key : _imageInfo.Sectors;
+ ulong chunkSectorCt = end - start;
+
+ if(chunk.type == CHUNK_TYPE_NOCOPY)
+ {
+ // Fast path: avoid going through ReadSector for empty regions.
+ for(ulong s = 0; s < chunkSectorCt; s++) crc = NdifCrc28.Update(crc, zeroSector, (int)SECTOR_SIZE);
+
+ continue;
+ }
+
+ for(ulong s = 0; s < chunkSectorCt; s++)
+ {
+ ErrorNumber errno = ReadSector(start + s, false, out byte[] sector, out _);
+
+ if(errno != ErrorNumber.NoError || sector is null) return false;
+
+ crc = NdifCrc28.Update(crc, sector, (int)SECTOR_SIZE);
+ }
+ }
+
+ uint calculated = NdifCrc28.Finish(crc);
+ uint stored = _header.crc & 0x0FFFFFFF;
+
+ if(calculated == stored) return true;
+
+ AaruLogging.WriteLine(Localization.NDIF_CRC_mismatch_expected_0_got_1, stored, calculated);
+
+ return false;
+ }
+
+#endregion
+}
\ No newline at end of file