diff --git a/SabreTools.Serialization/CrossModel/FDS.Deserializer.cs b/SabreTools.Serialization/CrossModel/FDS.Deserializer.cs
new file mode 100644
index 00000000..1552bf93
--- /dev/null
+++ b/SabreTools.Serialization/CrossModel/FDS.Deserializer.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using SabreTools.Data.Models.NES;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.CrossModel
+{
+ ///
+ public partial class FamicomDiskSystem : ICrossModel
+ {
+ ///
+ public bool Debug { get; set; } = false;
+
+ ///
+ public FDS? Deserialize(QD? obj)
+ {
+ // Ignore invalid data
+ if (obj?.Data is null || obj.Data.Length == 0)
+ return null;
+
+ // Ignore incomplete data
+ if ((obj.Data.Length % 65536) != 0)
+ return null;
+
+ // Determine the number of sides
+ byte sides = (byte)(obj.Data.Length / 65536);
+ if (sides < 1)
+ return null;
+
+ // Create the FDS for output
+ var fds = new FDS
+ {
+ Header = new FDSHeader
+ {
+ IdentificationString = Constants.FDSSignatureBytes,
+ DiskSides = sides,
+ }
+ };
+
+ // Loop over the sides and convert
+ List converted = [];
+ for (int i = 0; i < sides; i++)
+ {
+ // Convert the data to manipulate
+ List data = [.. obj.Data];
+
+ // Delete block 01 checksum
+ data.RemoveRange(0x38, 2);
+
+ // Delete block 02 checksum
+ int pos = 0x3A;
+ data.RemoveRange(pos, 2);
+
+ // Loop while there are more files
+ while (data[pos] == 3)
+ {
+ // Get the filesize
+ ushort filesize = (ushort)((data[pos + 0x0E] << 8) | data[pos + 0x0D]);
+
+ // Delete block 03 checksum
+ data.RemoveRange(pos + 0x10, 2);
+
+ // Skip the file data
+ pos = pos + 0x10 + 1 + filesize;
+
+ // Delete block 04 checksum
+ data.RemoveRange(pos, 2);
+ }
+
+ // Ensure the data is correctly sized
+ if (data.Count > 65500)
+ data = data.GetRange(0, 65500);
+ else if (data.Count < 65500)
+ data.AddRange(new byte[65500 - data.Count]);
+
+ // Add the data to the output
+ converted.AddRange(data);
+ }
+
+ // Assign the converted data and return
+ fds.Data = [.. converted];
+ return fds;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/CrossModel/FDS.Serializer.cs b/SabreTools.Serialization/CrossModel/FDS.Serializer.cs
new file mode 100644
index 00000000..a03208c6
--- /dev/null
+++ b/SabreTools.Serialization/CrossModel/FDS.Serializer.cs
@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using SabreTools.Data.Models.NES;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.CrossModel
+{
+ ///
+ public partial class FamicomDiskSystem : ICrossModel
+ {
+ ///
+ public QD? Serialize(FDS? obj)
+ => Serialize(obj, false);
+
+ ///
+ /// True to insert null checksums on conversion, false otherwise
+ public QD? Serialize(FDS? obj, bool nul)
+ {
+ // Ignore invalid data
+ if (obj?.Data is null || obj.Data.Length == 0)
+ return null;
+
+ // Ignore incomplete data
+ if ((obj.Data.Length % 65500) != 0)
+ return null;
+
+ // Determine the number of sides
+ byte sides = obj.Header != null
+ ? obj.Header.DiskSides
+ : (byte)(obj.Data.Length / 65500);
+ if (sides < 1)
+ return null;
+
+ // Create the QD for output
+ var qd = new QD();
+
+ // Loop over the sides and convert
+ List converted = [];
+ for (int i = 0; i < sides; i++)
+ {
+ // Convert the data to manipulate
+ List data = [.. obj.Data];
+
+ // If the data is unexpected
+ if (data[0] != 0x01)
+ break;
+
+ // Insert block 01 checksum
+ InsertCrc(data, 0, 0x38, nul);
+
+ // Insert block 02 checksum
+ int pos = 0x3A;
+ InsertCrc(data, pos, pos + 2, nul);
+
+ // Loop while there are more files
+ pos = 0x3E;
+ while (data[pos] == 3)
+ {
+ // Get the filesize
+ ushort filesize = (ushort)((data[pos + 0x0E] << 8) | data[pos + 0x0D]);
+
+ // Insert block 03 checksum
+ InsertCrc(data, pos, pos + 0x10, nul);
+
+ // Skip the file data
+ pos += 0x10 + 2;
+
+ // Insert block 04 checksum
+ InsertCrc(data, pos, pos + 1 + filesize, nul);
+ pos += 1 + filesize + 2;
+ }
+
+ // Ensure the data is correctly sized
+ if (data.Count > 65536)
+ data = data.GetRange(0, 65536);
+ else if (data.Count < 65536)
+ data.AddRange(new byte[65536 - data.Count]);
+
+ // Add the data to the output
+ converted.AddRange(data);
+ }
+
+ // Assign the converted data and return
+ qd.Data = [.. converted];
+ return qd;
+ }
+
+ private void InsertCrc(List data, int start, int end, bool nul)
+ {
+ if (!nul)
+ {
+ byte[] temp = new byte[end - start];
+ data.CopyTo(start, temp, 0, temp.Length);
+ ushort crc = FdsCrc(temp);
+ data.Insert(end + 0, (byte)(crc & 0xFF));
+ data.Insert(end + 1, (byte)(crc >> 0x08));
+ }
+ else
+ {
+ data.Insert(end + 0, 0);
+ data.Insert(end + 1, 0);
+ }
+ }
+
+ // TODO: Replace with CRC-16/KERMIT from Hashing
+ private ushort FdsCrc(byte[] data)
+ {
+ // http://forums.nesdev.com/viewtopic.php?p=194867
+ // Do not include any existing checksum, not even the blank checksums 00 00 or FF FF.
+ // The formula will automatically count 2 0x00 bytes without the programmer adding them manually.
+ // Also, do not include the gap terminator (0x80) in the data.
+ // If you wish to do so, change sum to 0x0000.
+ ushort s = 0x8000;
+ byte[] padded = [.. data, 0x00, 0x00];
+ foreach (byte b in padded)
+ {
+ s |= (ushort)(b << 16);
+ for (int i = 0; i < 8; i++)
+ {
+ if ((s & 1) != 0)
+ {
+ unchecked { s ^= (ushort)(0x8408 << 1); }
+ }
+
+ s >>= 1;
+ }
+ }
+
+ return s;
+ }
+ }
+}