Last active
November 18, 2020 01:01
-
-
Save Holzhaus/ae3dacf6a2e83dd00421 to your computer and use it in GitHub Desktop.
Simple python Dreamcast image parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# (c) 2015 Jan Holthuis | |
# A simple python script to get some info about Dreamcast images | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in | |
# all copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
# THE SOFTWARE. | |
import os | |
import sys | |
import csv | |
import struct | |
CDI_V2 = 0x80000004 | |
CDI_V3 = 0x80000005 | |
CDI_V35 = 0x80000006 | |
CDI_V4 = CDI_V35 | |
TRACK_START_MARK = (0, 0, 0x01, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF) | |
TRACK_SECTOR_SIZES = {0: 2048, 1: 2336, 2: 2352} | |
TRACK_MODES = {0: 'audio', 1: 'mode1', 2: 'mode2'} | |
class Image(object): | |
def __init__(self, filename, sessions): | |
self._filename = filename | |
self._sessions = tuple(sessions) | |
@staticmethod | |
def parse(filename): | |
ext = os.path.splitext(filename)[1].lower() | |
if ext == '.cdi': | |
return CdiImage.parse(filename) | |
elif ext == '.gdi': | |
return GdiImage.parse(filename) | |
elif ext == '.chd': | |
print("CHD images are not supported right now!") | |
else: | |
print("Unknown image format") | |
@staticmethod | |
def parse_ip_info(ip_bin_data): | |
return (s.decode('ascii') for s in | |
struct.unpack("<16s16s16s8s8s10s6s16s16s16s128s", | |
ip_bin_data)) | |
@property | |
def filename(self): | |
return self._filename | |
@property | |
def sessions(self): | |
return self._sessions | |
@property | |
def ip_info_raw(self): | |
raise NotImplementedError() | |
@property | |
def ip_info(self): | |
return self.parse_ip_info(self.ip_info_raw) | |
@property | |
def disk_type(self): | |
modes = (False, False, False) | |
for session in self.sessions: | |
for track in session.tracks: | |
modes[track.mode[0]] = True | |
if modes != (True, False, False): | |
if modes[2] is True: | |
return 'cdrom_xa' | |
elif modes[0] is True and modes[1] is True: | |
return 'cdrom_extra' | |
return 'cdrom' | |
def print_info(self): | |
print("IP.BIN info:") | |
(hardware_id, maker_id, disc_id, areas, peripherals, product_id, | |
product_version, release_date, bootfile, publisher, | |
product_name) = self.ip_info | |
print(" hardware_id: %s" % hardware_id) | |
print(" maker_id: %s" % maker_id) | |
print(" disc_id: %s" % disc_id) | |
print(" areas: %s" % areas) | |
print(" peripherals: %s" % peripherals) | |
print(" product_id: %s" % product_id) | |
print(" product_version: %s" % product_version) | |
print(" release_date: %s" % release_date) | |
print(" bootfile: %s" % bootfile) | |
print(" publisher: %s" % publisher) | |
print(" product_name: %s" % product_name) | |
print("Sessions: %d" % len(self.sessions)) | |
for sid, session in enumerate(self.sessions, start=1): | |
print("Session #%s (%d tracks):" % (sid, len(session.tracks))) | |
for tid, track in enumerate(session.tracks, start=1): | |
print(" Track #%d: %s" % (tid, track.mode[1])) | |
print(" position: %d" % track.position) | |
print(" filename: %s" % track.filename) | |
print(" pregap_length: %d" % track.pregap_length) | |
print(" length: %d" % track.length) | |
print(" total_length: %d" % track.total_length) | |
print(" start_lba: %d" % track.start_lba) | |
print(" sector_size: %d" % track.sector_size) | |
print(" start_fad: %d" % track.start_fad) | |
print(" end_fad: %d" % track.end_fad) | |
print(" offset: %d" % track.offset) | |
class CdiImage(Image): | |
def __init__(self, version, header_offset, *args): | |
self._version = version | |
self._header_offset = header_offset | |
super(CdiImage, self).__init__(*args) | |
@property | |
def version(self): | |
return self._version | |
@property | |
def header_offset(self): | |
return self._header_offset | |
@property | |
def ip_info_raw(self): | |
# Extract IP.BIN data | |
last_data_track = self.sessions[-1].tracks[-1] | |
ip_bin_position = last_data_track.position | |
if last_data_track.sector_size == 2336: | |
ip_bin_position += 8 | |
with open(self.filename, mode="rb") as f: | |
f.seek(ip_bin_position) | |
return f.read(256) | |
@classmethod | |
def parse(cls, filename): | |
file_size = os.path.getsize(filename) | |
if file_size < 8: | |
print("Image size too short") | |
return | |
with open(filename, mode="rb") as f: | |
f.seek(file_size-8) | |
image_version = struct.unpack("<I", f.read(4))[0] | |
image_header_offset = struct.unpack("<I", f.read(4))[0] | |
if image_header_offset == 0: | |
print("Bad image format") | |
return | |
if image_version not in (CDI_V2, CDI_V3, CDI_V35, CDI_V4): | |
print("Unsupported CDI version!") | |
sys.exit(1) | |
f.seek(image_header_offset) | |
num_sessions = struct.unpack("<H", f.read(2))[0] | |
sessions = [] | |
track_offset = 0 | |
for s in range(num_sessions): | |
num_tracks = struct.unpack("<H", f.read(2))[0] | |
tracks = [] | |
for t in range(num_tracks): | |
track = cls._parse_track(f, track_offset, filename) | |
if track is not None: | |
tracks.append(track) | |
track_offset += track.total_length*track.sector_size | |
f.seek(29, 1) | |
if image_version != CDI_V2: | |
f.seek(5, 1) | |
temp_value = struct.unpack("<I", f.read(4))[0] | |
if temp_value == 0xffffffff: | |
# extra data (DJ 3.00.780 and up) | |
f.seek(78, 1) | |
sessions.append(Session(tracks)) | |
# Skip to next session | |
f.seek(4, 1) | |
f.seek(8, 1) | |
if image_version != CDI_V2: | |
f.seek(1, 1) | |
return cls(image_version, image_header_offset, filename, sessions) | |
@staticmethod | |
def _parse_track(f, position, image_filename): | |
temp_value = struct.unpack("<I", f.read(4))[0] | |
if temp_value != 0: | |
# extra data (DJ 3.00.780 and up) | |
f.seek(8, 1) | |
current_start_mark = struct.unpack("<10B", f.read(10)) | |
if current_start_mark != TRACK_START_MARK: | |
print("Unsupported format: Missing track start mark") | |
current_start_mark = struct.unpack("<10B", f.read(10)) | |
if current_start_mark != TRACK_START_MARK: | |
print("Unsupported format: Missing track start mark") | |
f.seek(4, 1) | |
filename_length = struct.unpack("<B", f.read(1))[0] | |
filename = f.read(filename_length).decode('utf-8') | |
f.seek(11, 1) | |
f.seek(4, 1) | |
f.seek(4, 1) | |
temp_value = struct.unpack("<I", f.read(4))[0] | |
if temp_value == 0x80000000: | |
# DiscJuggler 4 | |
f.seek(8, 1) | |
f.seek(2, 1) | |
pregap_length = struct.unpack("<I", f.read(4))[0] | |
length = struct.unpack("<I", f.read(4))[0] | |
f.seek(6, 1) | |
mode = struct.unpack("<I", f.read(4))[0] | |
f.seek(12, 1) | |
start_lba = struct.unpack("<I", f.read(4))[0] | |
total_length = struct.unpack("<I", f.read(4))[0] | |
if total_length != (pregap_length + length): | |
print("total_length %d != pregap_length %d + length %d" % | |
(total_length, pregap_length, length)) | |
f.seek(16, 1) | |
sector_size_id = struct.unpack("<I", f.read(4))[0] | |
if sector_size_id not in TRACK_SECTOR_SIZES: | |
print("Unsupported sector size") | |
return None | |
sector_size = TRACK_SECTOR_SIZES[sector_size_id] | |
if mode not in TRACK_MODES: | |
print("Unsupported format: Track mode not supported") | |
return None | |
ctrl = 0 if mode == 0 else 4 | |
return Track(filename, pregap_length, length, mode, ctrl, | |
start_lba, sector_size, position, image_filename) | |
def print_info(self): | |
if self.version == CDI_V2: | |
version_string = "CDI_V2" | |
elif self.version == CDI_V3: | |
version_string = "CDI_V3" | |
elif self.version == CDI_V35: | |
version_string = "CDI_V35" | |
else: | |
version_string = "CDI_V4" | |
print("Type: DiscJuggler (CDI)") | |
print("CDI version: %s (0x%x)" % (version_string, self.version)) | |
print("Header offset: %d" % self.header_offset) | |
super(CdiImage, self).print_info() | |
class GdiImage(Image): | |
@classmethod | |
def parse(cls, filename): | |
sessions = [] | |
tracks = [] | |
with open(filename, mode="r") as f: | |
num_tracks = int(f.readline().strip()) | |
gdi_reader = csv.reader(f, delimiter=' ', quotechar='"') | |
for row in gdi_reader: | |
track_index = int(row[0]) | |
if track_index == 3: | |
# The high density disc area always starts with track 03, | |
# so we just start a new session here. | |
# (see GD-ROM Format Basic Specifications Ver. 2.14, p. 9 | |
# by Sega Enterprises, Ltd. for details) | |
sessions.append(Session(tracks)) | |
tracks = [] | |
start_lba = int(row[1]) | |
ctrl = int(row[2]) | |
mode = 0 if ctrl == 0 else 1 | |
sector_size = int(row[3]) | |
track_filename = os.path.abspath(os.path.join( | |
os.path.dirname(filename), row[4])) | |
pregap_length = 150 | |
length = os.path.getsize(track_filename)/sector_size | |
offset = int(row[5]) | |
track = Track(track_filename, pregap_length, length, mode, | |
ctrl, start_lba, sector_size, offset, filename) | |
tracks.append(track) | |
if num_tracks != track_index: | |
print("Incorrect track number in gdi file!") | |
sessions.append(Session(tracks)) | |
return cls(filename, sessions) | |
@property | |
def ip_info_raw(self): | |
# Extract IP.BIN data | |
last_data_track = self.sessions[-1].tracks[-1] | |
with open(last_data_track.filename, mode="rb") as f: | |
ip_bin_position = 0x10 | |
f.seek(ip_bin_position) | |
return f.read(256) | |
def print_info(self): | |
print("Type: GD-ROM disc dump (GDI)") | |
super(GdiImage, self).print_info() | |
class Session(object): | |
def __init__(self, tracks): | |
self._tracks = tuple(tracks) | |
@property | |
def tracks(self): | |
return self._tracks | |
@property | |
def start_fad(self): | |
return self._tracks[0].start_fad | |
class Track(object): | |
def __init__(self, filename, pregap_length, length, mode, ctrl, | |
start_lba, sector_size, offset, image_filename): | |
self._filename = filename | |
self._pregap_length = pregap_length | |
self._length = length | |
self._mode = mode | |
self._ctrl = ctrl | |
self._start_lba = start_lba | |
self._sector_size = sector_size | |
self._offset = offset | |
self._image_filename = image_filename | |
@property | |
def filename(self): | |
return self._filename | |
@property | |
def pregap_length(self): | |
return self._pregap_length | |
@property | |
def length(self): | |
return self._length | |
@property | |
def total_length(self): | |
return self.pregap_length + self.length | |
@property | |
def mode(self): | |
return (self._mode, TRACK_MODES[self._mode]) | |
@property | |
def ctrl(self): | |
return self._ctrl | |
@property | |
def start_lba(self): | |
return self._start_lba | |
@property | |
def sector_size(self): | |
return self._sector_size | |
@property | |
def position(self): | |
return self.offset + self.pregap_length * self.sector_size | |
@property | |
def offset(self): | |
return self._offset | |
@property | |
def start_fad(self): | |
return self.start_lba + self.pregap_length | |
@property | |
def end_fad(self): | |
return self.start_fad + self.length - 1 | |
def read_sectors(self, fad, sectors=1): | |
with open(self._image_filename, mode="rb") as f: | |
f.seek(self.position+fad*self.sector_size) | |
print(f.tell()) | |
return f.read(sectors*self.sector_size) | |
if __name__ == "__main__": | |
filename = sys.argv[1] | |
img = Image.parse(filename) | |
img.print_info() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment