[gnome-games/wip/aplazas/playstation-disc-image] Parse PlayStation disc images properly



commit bbcce3aea8ef1a29ed7f42664e88a20c4b5008ea
Author: Adrien Plazas <kekun plazas laposte net>
Date:   Fri Dec 23 06:10:22 2016 +0100

    Parse PlayStation disc images properly

 plugins/playstation/src/Makefile.am              |    1 +
 plugins/playstation/src/playstation-disc-image.c |  510 ++++++++++++++++++++++
 plugins/playstation/src/playstation-header.vala  |  103 +----
 3 files changed, 536 insertions(+), 78 deletions(-)
---
diff --git a/plugins/playstation/src/Makefile.am b/plugins/playstation/src/Makefile.am
index 1b0067e..ca69157 100644
--- a/plugins/playstation/src/Makefile.am
+++ b/plugins/playstation/src/Makefile.am
@@ -36,6 +36,7 @@ libgames_playstation_plugin_la_DEPENDENCIES = \
 
 libgames_playstation_plugin_la_SOURCES = \
        $(BUILT_SOURCES) \
+       playstation-disc-image.c \
        playstation-error.vala \
        playstation-game-factory.vala \
        playstation-header.vala \
diff --git a/plugins/playstation/src/playstation-disc-image.c 
b/plugins/playstation/src/playstation-disc-image.c
new file mode 100644
index 0000000..be2cd1d
--- /dev/null
+++ b/plugins/playstation/src/playstation-disc-image.c
@@ -0,0 +1,510 @@
+// This file is part of GNOME Games. License: GPL-3.0+.
+
+#include <errno.h>
+#include <glib.h>
+#include <string.h>
+#include <stdio.h>
+
+////////////////////////////////////////////////////////////////////////
+// GamesDiscImageTime
+////////////////////////////////////////////////////////////////////////
+
+#define GAMES_DISC_IMAGE_FRAMES_PER_SECOND 75
+
+typedef struct {
+  guint8 minute;
+  guint8 second;
+  guint8 frame;
+} GamesDiscImageTime;
+
+typedef enum  {
+  GAMES_DISC_IMAGE_ERROR_CANT_OPEN_FILE,
+} GamesPlayStationError;
+
+#define GAMES_DISC_IMAGE_ERROR games_disc_image_error_quark ()
+
+GQuark games_disc_image_error_quark (void);
+
+// FIXME What is BCD?
+static guint8
+bcd_to_integer (guint8 bcd)
+{
+  return bcd / 16 * 10 + bcd % 16;
+}
+
+// FIXME What is BCD?
+static guint8
+integer_to_bcd (guint8 integer)
+{
+  return integer / 10 * 16 + integer % 10;
+}
+
+static void
+games_disc_image_time_set_from_minute_second_frame (GamesDiscImageTime *time,
+                                                    guint8              minute,
+                                                    guint8              second,
+                                                    guint8              frame)
+{
+  time->minute = minute;
+  time->second = second;
+  time->frame = frame;
+}
+
+static void
+games_disc_image_time_set_from_time_reference (GamesDiscImageTime *time,
+                                               guint8             *time_reference)
+{
+  gint32 block; // The value of the clock containing the target time
+  int minute, second, frame;
+
+  // Get the time reference
+  // FIXME Make these ifdefs cleaner.
+#if defined(__arm__)
+  guchar *u = (guchar *) time_reference;
+  block = (u[3] << 24) | (u[2] << 16) | (u[1] << 8) | u[0];
+#elif defined(__BIGENDIAN__)
+  block = (time_reference[0] & 0xff) | ((time_reference[1] & 0xff) << 8) | ((time_reference[2] & 0xff) << 
16) | (time_reference[3] << 24);
+#else
+  block = *((gint32 *) time_reference);
+#endif
+
+  block += 2 * GAMES_DISC_IMAGE_FRAMES_PER_SECOND;
+  minute = block / (60 * GAMES_DISC_IMAGE_FRAMES_PER_SECOND);
+  block = block - minute * (60 * GAMES_DISC_IMAGE_FRAMES_PER_SECOND);
+  second = block / GAMES_DISC_IMAGE_FRAMES_PER_SECOND;
+  frame = block - second * GAMES_DISC_IMAGE_FRAMES_PER_SECOND;
+
+  minute = ((minute / 10) << 4) | minute % 10;
+  second = ((second / 10) << 4) | second % 10;
+  frame = ((frame / 10) << 4) | frame % 10;
+
+  time->minute = minute;
+  time->second = second;
+  time->frame = frame;
+}
+
+static gint
+games_disc_image_time_get_sector (const GamesDiscImageTime *time)
+{
+  return (bcd_to_integer (time->minute) * 60 + bcd_to_integer (time->second) - 2) * 
GAMES_DISC_IMAGE_FRAMES_PER_SECOND + bcd_to_integer (time->frame);
+}
+
+static void
+games_disc_image_time_increment (GamesDiscImageTime *time)
+{
+  guint8 i_minute, i_second, i_frame;
+
+  i_minute = bcd_to_integer (time->minute);
+  i_second = bcd_to_integer (time->second);
+  i_frame = bcd_to_integer (time->frame);
+
+  i_frame++;
+  if (i_frame == GAMES_DISC_IMAGE_FRAMES_PER_SECOND) {
+    i_frame = 0;
+    i_second++;
+    if (i_second == 60) {
+      i_second = 0;
+      i_minute++;
+    }
+  }
+
+  time->minute = integer_to_bcd (i_minute);
+  time->second = integer_to_bcd (i_second);
+  time->frame = integer_to_bcd (i_frame);
+}
+
+////////////////////////////////////////////////////////////////////////
+// GamesDiscFrame
+////////////////////////////////////////////////////////////////////////
+
+#define GAMES_DISC_IMAGE_FRAME_SIZE           2352
+#define GAMES_DISC_IMAGE_FRAME_HEADER_SIZE    12
+
+typedef struct _GamesDiscFrameMode1 GamesDiscFrameMode1;
+struct _GamesDiscFrameMode1 {
+  guint8 synchronization[12];
+  guint8 header[12];
+  guint8 content[2048];
+  guint8 error_correction_code[280];
+};
+
+typedef struct _GamesDiscFrameMode2 GamesDiscFrameMode2;
+struct _GamesDiscFrameMode2 {
+  guint8 synchronization[12];
+  guint8 content[2340];
+};
+
+typedef union _GamesDiscFrame GamesDiscFrame;
+union _GamesDiscFrame {
+  GamesDiscFrameMode1 mode1;
+  GamesDiscFrameMode2 mode2;
+};
+
+////////////////////////////////////////////////////////////////////////
+// GamesDiscFileInfo
+////////////////////////////////////////////////////////////////////////
+
+typedef struct _GamesDiscFileInfo GamesDiscFileInfo;
+struct _GamesDiscFileInfo {
+  guint8 length;
+  guint8 ext_attr_length;
+  guint8 extent[8];
+  guint8 size[8];
+  guint8 date[7];
+  guint8 flags;
+  guint8 file_unit_size;
+  guint8 interleave;
+  guint8 volume_sequence_number[4];
+  guint8 name_length;
+};
+
+typedef gboolean (*GamesDiscFileInfoForeachCallback) (const GamesDiscFileInfo *file_info, gpointer 
user_data);
+
+static gboolean
+games_disc_file_info_is_directory (GamesDiscFileInfo *file_info)
+{
+  g_return_val_if_fail (file_info != NULL, FALSE);
+
+  return file_info->flags & 0x2;
+}
+
+static gboolean
+games_disc_file_info_is_valid (const GamesDiscFileInfo *file_info)
+{
+  const gsize magic_size = 47; // FIXME Magic number, I have no ida what it it but it works.
+
+  g_return_val_if_fail (file_info != NULL, FALSE);
+
+  return file_info->length >= magic_size + file_info->name_length;
+}
+
+static gchar *
+games_disc_file_info_access_name (GamesDiscFileInfo *file_info)
+{
+  g_return_val_if_fail (file_info != NULL, NULL);
+
+  return (gchar *) file_info + sizeof (GamesDiscFileInfo);
+}
+
+static gchar *
+games_disc_file_info_get_name (GamesDiscFileInfo *file_info)
+{
+  g_return_val_if_fail (file_info != NULL, NULL);
+
+  return g_strndup (games_disc_file_info_access_name (file_info), file_info->name_length);
+}
+
+static GamesDiscFileInfo *
+games_disc_file_info_get_next (const GamesDiscFileInfo *file_info)
+{
+  g_return_val_if_fail (file_info != NULL, NULL);
+
+  if (!games_disc_file_info_is_valid (file_info))
+    return NULL;
+
+  return (GamesDiscFileInfo *) ((gpointer) file_info + file_info->length);
+}
+
+static void
+games_disc_file_info_foreach_file (const GamesDiscFileInfo          *file_info,
+                                   gsize                             size,
+                                   GamesDiscFileInfoForeachCallback  callback,
+                                   gpointer                          user_data)
+{
+  const GamesDiscFileInfo *current;
+  GamesDiscFileInfo *next;
+
+  g_return_if_fail (file_info != NULL);
+
+  for (current = file_info; current != NULL && games_disc_file_info_is_valid (current); current = 
games_disc_file_info_get_next (current)) {
+    // The file info should never go beyond the end of the buffer.
+    if ((gpointer) current - (gpointer) file_info + sizeof (GamesDiscFileInfo) >= size ||
+        (gpointer) current - (gpointer) file_info + current->length >= size)
+      break;
+    // FIXME Why is that commented?
+/*  for (current = file_info; games_disc_file_info_is_valid (current); current = next) {*/
+/*    // If the next starts after the expected buffer size, then the current is somehow invalid.*/
+/*    // Try to avoid segfault if the format is unexpected.*/
+/*    next = games_disc_file_info_get_next (current);*/
+/*    if (next != NULL && next >= file_info + 2048) // FIXME Magic value.*/
+/*      break;*/
+
+    if (!callback (current, user_data))
+      break;
+  }
+    // FIXME Why is that commented?
+/*       )*/
+
+/*  while (file_info->length != 0) {*/
+/*    if (!callback (file_info, user_data))*/
+/*      break;*/
+
+/*    i += file_info->length;*/
+/*    if (i )*/
+/*    file_info += file_info->length;*/
+/*  }*/
+}
+
+////////////////////////////////////////////////////////////////////////
+// GamesDisc
+////////////////////////////////////////////////////////////////////////
+
+struct _GamesDiscImage {
+  FILE *file_handle;
+};
+
+typedef struct _GamesDiscImage GamesDiscImage;
+
+void
+games_disc_image_open (GamesDiscImage  *disc,
+                       const char      *filename,
+                       GError         **error)
+{
+  disc->file_handle = fopen (filename, "rb");
+  if (disc->file_handle == 0)
+    g_set_error (error,
+                 GAMES_DISC_IMAGE_ERROR,
+                 GAMES_DISC_IMAGE_ERROR_CANT_OPEN_FILE,
+                 "Couldn’t open disc image file: %s",
+                 g_strerror (errno));
+}
+
+gboolean
+games_disc_image_read_frame (GamesDiscImage           *disc,
+                             const GamesDiscImageTime *time,
+                             GamesDiscFrame           *frame)
+{
+  gsize read;
+  gint sector;
+
+  g_return_val_if_fail (disc != NULL, FALSE);
+  g_return_val_if_fail (time != NULL, FALSE);
+  g_return_val_if_fail (frame != NULL, FALSE);
+
+  sector = games_disc_image_time_get_sector (time);
+  // TODO Check multiplication won't overflow by dividing the type's max by sizeof (GamesDiscFrame) and 
compare it to sector.
+  fseek (disc->file_handle, sector * sizeof (GamesDiscFrame), SEEK_SET);
+  // TODO Check errors.
+  read = fread (frame, 1, sizeof (GamesDiscFrame), disc->file_handle);
+  // TODO Check errors.
+
+  return read == sizeof (GamesDiscFrame);
+}
+
+gboolean
+games_disc_image_read_directory (GamesDiscImage     *disc,
+                                 GamesDiscImageTime *time,
+                                 guint8             *dst)
+{
+  gsize read;
+  gint sector;
+
+  sector = games_disc_image_time_get_sector(time);
+  fseek(disc->file_handle, sector * GAMES_DISC_IMAGE_FRAME_SIZE + GAMES_DISC_IMAGE_FRAME_HEADER_SIZE + 12, 
SEEK_SET);
+  read = fread (dst, 1, 2048, disc->file_handle);
+  if (read == -1)
+    return FALSE;
+
+  games_disc_image_time_increment (time); // FIXME We should either always incerment it or never increment 
it.
+
+  sector = games_disc_image_time_get_sector(time);
+  fseek(disc->file_handle, sector * GAMES_DISC_IMAGE_FRAME_SIZE + GAMES_DISC_IMAGE_FRAME_HEADER_SIZE + 12, 
SEEK_SET);
+  read = fread (dst + 2048, 1, 2048, disc->file_handle);
+  if (read == -1)
+    return FALSE;
+
+  return TRUE;
+}
+
+typedef struct {
+  const gchar        *filename;
+  GamesDiscImageTime *time;
+  gboolean            is_dir;
+  gboolean            found;
+} GetFileData;
+
+static gboolean
+get_file_co (GamesDiscFileInfo *file_info,
+             gpointer           user_data)
+{
+  GetFileData *data = (GetFileData *) user_data;
+
+  if (games_disc_file_info_is_directory (file_info)) {
+    if (g_ascii_strncasecmp (games_disc_file_info_access_name (file_info), data->filename, 
file_info->name_length) == 0) {
+      if (data->filename[file_info->name_length] != '\\')
+        return TRUE;
+
+      data->filename += file_info->name_length + 1;
+
+      games_disc_image_time_set_from_time_reference (data->time, file_info->extent);
+      data->is_dir = TRUE;
+      data->found = TRUE;
+
+      return FALSE;
+    }
+  }
+  else {
+    if (g_ascii_strncasecmp (games_disc_file_info_access_name (file_info), data->filename, strlen 
(data->filename)) == 0) {
+      games_disc_image_time_set_from_time_reference (data->time, file_info->extent);
+      data->is_dir = FALSE;
+      data->found = TRUE;
+
+      return FALSE;
+    }
+  }
+
+  return TRUE;
+}
+
+static gboolean
+games_disc_image_get_file (const GamesDiscImage    *disc,
+                           const GamesDiscFileInfo *file_info,
+                           const gchar             *filename,
+                           GamesDiscImageTime      *time)
+{
+  guint8 ddir[4096];
+  GetFileData data = { 0 };
+  data.filename = filename;
+  data.time = time;
+  data.is_dir = TRUE;
+  data.found = FALSE;
+
+  g_return_val_if_fail (filename != NULL, FALSE);
+
+  while (data.is_dir) {
+    data.filename = filename;
+    data.time = time;
+    data.is_dir = FALSE;
+    data.found = FALSE;
+
+    games_disc_file_info_foreach_file (file_info, 4096, get_file_co, &data);
+
+    if (data.found && data.is_dir) {
+      if (!games_disc_image_read_directory (disc, time, ddir))
+        return FALSE;
+
+      file_info = (GamesDiscFileInfo *) ddir;
+
+      break; // Parse the sub directory.
+    }
+  }
+
+  return data.found;
+}
+
+////////////////////////////////////////////////////////////////////////
+// PlayStation
+////////////////////////////////////////////////////////////////////////
+
+typedef struct _PlayStationGamesDiscInfo {
+  gchar *label;
+  gchar *exe;
+} PlayStationGamesDiscInfo;
+
+gboolean
+games_disc_image_get_playstation_info (GamesDiscImage           *disc,
+                                       PlayStationGamesDiscInfo *games_disc_image_info)
+{
+  gchar label_buffer[33] = "";
+
+  GamesDiscFileInfo *dir;
+  GamesDiscImageTime time;
+  guchar mdir[4096];
+  gchar exe_buffer[256];
+  gint i, len, c;
+
+  GamesDiscFrame frame;
+
+  games_disc_image_time_set_from_minute_second_frame (&time, integer_to_bcd (0), integer_to_bcd (2), 
integer_to_bcd (0x10));
+  if (!games_disc_image_read_frame (disc, &time, &frame))
+    return FALSE;
+
+  memset (label_buffer, 0, sizeof (label_buffer));
+  memset (exe_buffer, 0, sizeof (exe_buffer));
+
+  strncpy (label_buffer, (const char *) frame.mode2.content + 52, 32);
+
+  // Skip head and sub, and go to the root directory record
+  dir = (GamesDiscFileInfo *) (frame.mode1.content + 156); 
+
+  games_disc_image_time_set_from_time_reference (&time, dir->extent);
+
+  if (!games_disc_image_read_directory (disc, &time, mdir))
+    return FALSE;
+
+  if (games_disc_image_get_file (disc, (GamesDiscFileInfo *) mdir, "SYSTEM.CNF;1", &time)) {
+    if (!games_disc_image_read_frame (disc, &time, &frame))
+      return FALSE;
+
+    sscanf ((char *) frame.mode1.content, "BOOT = cdrom:\\%255s", exe_buffer);
+    if (!games_disc_image_get_file (disc, (GamesDiscFileInfo *) mdir, exe_buffer, &time)) {
+      sscanf ((char *) frame.mode1.content, "BOOT = cdrom:%255s", exe_buffer);
+      if (!games_disc_image_get_file (disc, (GamesDiscFileInfo *) mdir, exe_buffer, &time)) {
+        char *ptr = strstr((char *) frame.mode1.content, "cdrom:"); // Possibly the executable is in some 
subdir.
+        if (ptr == NULL)
+          return FALSE;
+
+        // Skip "cdrom:".
+        ptr += 6;
+
+        // Skip slashes.
+        while (*ptr == '\\' || *ptr == '/')
+          ptr++;
+
+        strncpy (exe_buffer, ptr, 255);
+        exe_buffer[255] = '\0';
+        ptr = exe_buffer;
+
+        // Keep only the first line.
+        while (*ptr != '\0' && *ptr != '\r' && *ptr != '\n')
+          ptr++;
+        *ptr = '\0';
+
+        if (!games_disc_image_get_file (disc, (GamesDiscFileInfo *) mdir, exe_buffer, &time))
+          return FALSE; // Executable not found.
+      }
+    }
+  }
+  else if (games_disc_image_get_file (disc, (GamesDiscFileInfo *) mdir, "PSX.EXE;1", &time))
+    strcpy (exe_buffer, "PSX.EXE;1");
+  else
+    return FALSE; // SYSTEM.CNF and PSX.EXE not found
+
+  if (games_disc_image_info != NULL) {
+    games_disc_image_info->label = strndup (label_buffer, sizeof (label_buffer));
+    games_disc_image_info->exe   = strndup (exe_buffer, sizeof (exe_buffer));
+  }
+
+  return TRUE;
+}
+
+gboolean
+get_playstation_info (const gchar  *image_filename,
+                      gchar       **label,
+                      gchar       **exe)
+{
+  GError *error =  NULL;
+  GamesDiscImage disc = { 0 };
+
+  games_disc_image_open (&disc, image_filename, &error);
+  if (error != NULL) {
+    g_debug ("%s", error->message);
+    g_clear_error (&error);
+
+    return FALSE;
+  }
+
+  PlayStationGamesDiscInfo games_disc_image_info = { 0 };
+  if (!games_disc_image_get_playstation_info (&disc, &games_disc_image_info))
+    return FALSE;
+
+  *label = games_disc_image_info.label;
+  *exe = games_disc_image_info.exe;
+
+  return TRUE;
+}
+
+GQuark games_disc_image_error_quark (void)
+{
+  return g_quark_from_static_string ("games-disc-image-error-quark");
+}
diff --git a/plugins/playstation/src/playstation-header.vala b/plugins/playstation/src/playstation-header.vala
index 19d1275..ff3eb12 100644
--- a/plugins/playstation/src/playstation-header.vala
+++ b/plugins/playstation/src/playstation-header.vala
@@ -1,24 +1,7 @@
 // This file is part of GNOME Games. License: GPL-3.0+.
 
 private class Games.PlayStationHeader : Object {
-       private const size_t[] HEADER_OFFSETS = {
-               0x85D2, // .bin.ecm
-               0x9320, // .bin
-               0x9360, // .iso 
-       };
-       private const size_t HEADER_TITLE_OFFSET = 0x20;
-       private const string HEADER_MAGIC_VALUE = "PLAYSTATION";
-
-       private const size_t[] BOOT_OFFSETS = {
-               0xBE64, // .bin.ecm
-               0xD368, // .bin
-               0xD3A8, // .iso
-               0xA9F98, // .bin
-       };
-       private const string BOOT_MAGIC_VALUE = "BOOT";
-
        // The ID prefixes must always be in uppercase.
-       private const string[] IDS = { "SLUS", "SCUS", "SLES", "SCES", "SLPS", "SLPM", "SCPS" };
        private const size_t DISC_ID_SIZE = 10;
 
        private static Regex disc_id_regex;
@@ -38,83 +21,44 @@ private class Games.PlayStationHeader : Object {
                if (_disc_id != null)
                        return;
 
-               _disc_id = get_id_from_boot ();
+               string label;
+               string exe;
+               get_playstation_info (file.get_path (), out label, out exe);
+
+               _disc_id = parse_id_from_exe (exe);
                if (_disc_id != null)
                        return;
 
-               _disc_id = search_id_in_header ();
+               _disc_id = parse_id_from_label (label);
                if (_disc_id != null)
                        return;
 
                throw new PlayStationError.INVALID_HEADER (_("Invalid PlayStation header: disc ID not found 
in “%s”."), file.get_uri ());
        }
 
-       private string? search_id_in_header () throws Error {
-               var offset = get_header_offset ();
-               if (offset == null)
-                       return null;
-
-               var stream = new StringInputStream (file);
-               var header = stream.read_string_for_size (offset + HEADER_TITLE_OFFSET, DISC_ID_SIZE);
-
-               var raw_id = header.up ();
-               raw_id = raw_id.replace ("_", "-");
-
-               foreach (var id in IDS) {
-                       if (!(id in header))
-                               continue;
-
-                       if (is_a_disc_id (raw_id))
-                               return raw_id;
-               }
-
-               return null;
-       }
-
-       private size_t? get_header_offset () throws Error {
-               var stream = new StringInputStream (file);
+       private string? parse_id_from_exe (string exe) throws Error {
+               var disc_id = exe.strip ();
+               disc_id = disc_id.split (";")[0];
+               disc_id = disc_id.replace ("_", "-");
+               disc_id = disc_id.replace (".", "");
+               disc_id = disc_id.up ();
 
-               foreach (var offset in HEADER_OFFSETS)
-                       if (stream.has_string (offset, HEADER_MAGIC_VALUE))
-                               return offset;
-
-               return null;
-       }
-
-       private string? get_id_from_boot () throws Error {
-               var offset = get_boot_offset ();
-               if (offset == null)
+               if (!is_a_disc_id (disc_id))
                        return null;
 
-               var stream = new StringInputStream (file);
-               var header = stream.read_string (offset);
-               header = header.up ();
-
-               foreach (var id in IDS) {
-                       if (!(id in header))
-                               continue;
-
-                       var raw_id = header.split (id)[1];
-                       raw_id = raw_id.split (";")[0];
-                       raw_id = raw_id.replace ("_", "-");
-                       raw_id = raw_id.replace (".", "");
-                       raw_id = (id + raw_id).up ();
-
-                       if (is_a_disc_id (raw_id))
-                               return raw_id;
-               }
-
-               return null;
+               return disc_id;
        }
 
-       private size_t? get_boot_offset () throws Error {
-               var stream = new StringInputStream (file);
+       private string? parse_id_from_label (string label) throws Error {
+               var disc_id = label.strip ();
+               disc_id = disc_id.replace ("_", "-");
+               disc_id = disc_id.strip ();
+               disc_id = disc_id.up ();
 
-               foreach (var offset in BOOT_OFFSETS)
-                       if (stream.has_string (offset, BOOT_MAGIC_VALUE))
-                               return offset;
+               if (!is_a_disc_id (disc_id))
+                       return null;
 
-               return null;
+               return disc_id;
        }
 
        private static bool is_a_disc_id (string disc_id) {
@@ -123,4 +67,7 @@ private class Games.PlayStationHeader : Object {
 
                return disc_id_regex.match (disc_id);
        }
+
+       [CCode (cname = "get_playstation_info")]
+       private static extern bool get_playstation_info (string filename, out string label, out string exe);
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]