Skip to content

Instantly share code, notes, and snippets.

@infval
Forked from einstein95/qd2fds.py
Last active December 4, 2023 01:09
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save infval/18d65dd034290fb908f589dcc10c6d25 to your computer and use it in GitHub Desktop.
Save infval/18d65dd034290fb908f589dcc10c6d25 to your computer and use it in GitHub Desktop.
Converts between QD and FDS disk images (Family Computer Disk System / Famicom)
#!/usr/bin/env python3
"""
Converts between QD and FDS disk images
"""
import struct
def create_fds_header(side_count):
return b"FDS\x1A" + bytes([side_count & 0xFF]) + bytes(11)
def fds_crc(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.
s = 0x8000
for byte in data + bytes(2):
s |= byte << 16
for _ in range(8):
if (s & 1):
s ^= 0x8408 << 1
s >>= 1
return s
def convert_to_fds(disk):
"""
Convert a .qd to .fds
"""
del disk[0x38:0x3A] # 01 block chksum
# fn = disk[0x39] # get num of files from 02 block
pos = 0x3A
del disk[pos: pos + 2] # 02 block chksum
try:
while disk[pos] == 3: # if there's any more files, like in Doki Doki Panic
print(disk[pos + 0x3: pos + 0xB].decode('latin_1'))
filesize, = struct.unpack('<H', disk[pos + 0xD: pos + 0xF])
del disk[pos + 0x10: pos + 0x12] # 03 block chksum
pos = pos + 0x10 + 1 + filesize
del disk[pos: pos + 2] # 04 block chksum
except IndexError:
pass
if len(disk) > 65500:
disk = disk[:65500]
else:
disk = disk.ljust(65500, b"\0")
return disk
def convert_to_qd(disk):
"""
Convert a .fds to .qd
"""
def insert_crc(start, end):
crc = fds_crc(disk[start:end])
disk.insert(end + 0, crc & 0xFF)
disk.insert(end + 1, crc >> 0x8)
if disk[0] != 0x01:
return bytearray()
insert_crc(0, 0x38) # 01 block chksum
# fn = disk[0x3B] # get num of files from 02 block
pos = 0x3A
insert_crc(pos, pos + 2) # 02 block chksum
pos = 0x3E
try:
while disk[pos] == 3: # if there's any more files, like in Doki Doki Panic
print(disk[pos + 0x3: pos + 0xB].decode('latin_1'))
filesize, = struct.unpack('<H', disk[pos + 0xD: pos + 0xF])
insert_crc(pos, pos + 0x10) # 03 block chksum
pos += 0x10 + 2
insert_crc(pos, pos + 1 + filesize) # 04 block chksum
pos += 1 + filesize + 2
except IndexError:
pass
if len(disk) > 0x10000:
disk = disk[:0x10000]
else:
disk = disk.ljust(0x10000, b"\0")
return disk
if __name__ == '__main__':
import sys
import argparse
from pathlib import Path
parser = argparse.ArgumentParser(description='Converts between QD and FDS disk images')
parser.add_argument('rom', metavar='ROM', help='QD or FDS file')
parser.add_argument('-o', '--output')
parser.add_argument('-f', '--fds-header', action='store_true', help='add FDS header')
args = parser.parse_args()
path = Path(args.rom)
try:
size = path.stat().st_size
except FileNotFoundError as err:
print("File Not Found: {0}".format(err))
sys.exit(1)
if not path.is_file():
print("Error: It's not file: '{0}'".format(args.rom))
sys.exit(1)
side_size = 0
start_pos = 0
convert_func = None
ext = ''
if size % 0x10000 == 0:
side_size = 0x10000
convert_func = convert_to_fds
ext = '.fds'
elif size % 65500 == 0 or size % 65500 == 16:
side_size = 65500
start_pos = size % 65500
convert_func = convert_to_qd
ext = '.qd'
if convert_func:
in_bytes = bytearray(path.read_bytes())
out_bytes = bytearray()
if ext == '.fds' and args.fds_header:
out_bytes += create_fds_header(size // side_size)
for i in range(start_pos, size, side_size):
out_bytes += convert_func(in_bytes[i: i + side_size])
path = path.with_suffix(ext)
if args.output:
path = Path(args.output)
path.write_bytes(out_bytes)
@einstein95
Copy link

Holy shit, great work.

btw, since 0 is Falsey, you can do if not size % 0x10000 :p

@yupyuptrp
Copy link

this worked

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment