Repository: superg/redumper
Pull request #235: Fix skeleton issues
Author: login: Deterous
Created: 2025-04-22T05:24:26Z | Merged: 2025-04-27T14:19:15Z

- Read track type from CUE and allow 0-valued sector size in PVD Fixes #124
- Ignores str_to_int errors and falls back to identifier version 0 (for Romeo filesystems, UTF-16 encoded strings, and other identifiers that break ISO9660 spec). Fixes #149
- If Parent Record Number is 0 or some large number, assume it is in the root folder. Fixes #213 and Fixes #123
- Don't replace an Area in the Area map if the Area size is smaller. Fixes #111 

Linked issue #124: skeleton: unsupported logical block size
The following output is produced when dumping a CD-i disc:
```
*** SKELETON

error: unsupported logical block size (block size: 0)
```
Linked issue comments: user: login: Barthax
created_at: 2024-10-15T09:56:44Z
body: For reference (in case it is not known): CD-i specification is available online at http://www.icdia.co.uk/docs/cdi_may94_r2.pdf .
author_association: NONE

Diff:
diff --git a/cd/cd_common.ixx b/cd/cd_common.ixx
index 7cf6435d..eb617502 100644
--- a/cd/cd_common.ixx
+++ b/cd/cd_common.ixx
@@ -43,6 +43,41 @@ export constexpr int32_t LBA_START = -45150; // MSVC internal compiler error: MS
 export constexpr uint32_t LEADOUT_OVERREAD_COUNT = 100;
 
 
+export enum class TrackType
+{
+    AUDIO,
+    MODE1_2352,
+    MODE2_2352,
+    CDI_2352,
+    MODE1_2048,
+    MODE2_2336,
+    CDI_2336,
+    CDG,
+    MODE0_2352,
+    MODE2_2048,
+    MODE2_2324,
+    UNKNOWN
+};
+
+
+export bool track_type_is_data_raw(TrackType track_type)
+{
+    return track_type == TrackType::MODE1_2352 || track_type == TrackType::MODE2_2352 || track_type == TrackType::CDI_2352 || track_type == TrackType::MODE0_2352;
+}
+
+
+export bool track_type_is_data_iso(TrackType track_type)
+{
+    return track_type == TrackType::MODE1_2048 || track_type == TrackType::MODE2_2048;
+}
+
+
+export bool track_type_is_data(TrackType track_type)
+{
+    return track_type_is_data_raw(track_type) || track_type_is_data_iso(track_type);
+}
+
+
 export int32_t sample_offset_a2r(uint32_t absolute)
 {
     return absolute + (LBA_START * CD_DATA_SIZE_SAMPLES);
@@ -574,26 +609,49 @@ export std::string track_extract_basename(std::string str)
 }
 
 
-export std::list<std::pair<std::string, bool>> cue_get_entries(const std::filesystem::path &cue_path)
+export std::list<std::pair<std::string, TrackType>> cue_get_entries(const std::filesystem::path &cue_path)
 {
-    std::list<std::pair<std::string, bool>> entries;
+    std::list<std::pair<std::string, TrackType>> entries;
 
     std::fstream fs(cue_path, std::fstream::in);
     if(!fs.is_open())
         throw_line("unable to open file ({})", cue_path.filename().string());
 
-    std::pair<std::string, bool> entry;
+    std::pair<std::string, TrackType> entry;
     std::string line;
     while(std::getline(fs, line))
     {
-        auto tokens(tokenize(line, " \t", "\"\""));
+        auto tokens(tokenize(line, " \t\r", "\"\""));
         if(tokens.size() == 3)
         {
             if(tokens[0] == "FILE")
                 entry.first = tokens[1];
             else if(tokens[0] == "TRACK" && !entry.first.empty())
             {
-                entry.second = tokens[2] != "AUDIO";
+                if(tokens[2] == "AUDIO")
+                    entry.second = TrackType::AUDIO;
+                else if(tokens[2] == "MODE1/2352")
+                    entry.second = TrackType::MODE1_2352;
+                else if(tokens[2] == "MODE2/2352")
+                    entry.second = TrackType::MODE2_2352;
+                else if(tokens[2] == "CDI/2352")
+                    entry.second = TrackType::CDI_2352;
+                else if(tokens[2] == "MODE1/2048")
+                    entry.second = TrackType::MODE1_2048;
+                else if(tokens[2] == "MODE2/2336")
+                    entry.second = TrackType::MODE2_2336;
+                else if(tokens[2] == "CDI/2336")
+                    entry.second = TrackType::CDI_2336;
+                else if(tokens[2] == "CDG")
+                    entry.second = TrackType::CDG;
+                else if(tokens[2] == "MODE0/2352")
+                    entry.second = TrackType::MODE0_2352;
+                else if(tokens[2] == "MODE2/2048")
+                    entry.second = TrackType::MODE2_2048;
+                else if(tokens[2] == "MODE2/2324")
+                    entry.second = TrackType::MODE2_2324;
+                else
+                    entry.second = TrackType::UNKNOWN;
                 entries.push_back(entry);
                 entry.first.clear();
             }
diff --git a/cd/fix_msf.ixx b/cd/fix_msf.ixx
index c2bc40ae..66e87eac 100644
--- a/cd/fix_msf.ixx
+++ b/cd/fix_msf.ixx
@@ -124,7 +124,7 @@ export int redumper_fix_msf(Context &ctx, Options &options)
     std::list<std::filesystem::path> tracks;
     if(std::filesystem::exists(image_prefix + ".cue"))
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
-            if(t.second)
+            if(track_type_is_data_raw(t.second))
                 tracks.push_back(std::filesystem::path(options.image_path) / t.first);
 
     if(tracks.empty())
diff --git a/filesystem/iso9660/iso9660_defs.ixx b/filesystem/iso9660/iso9660_defs.ixx
index 56a54826..2a10ec6d 100644
--- a/filesystem/iso9660/iso9660_defs.ixx
+++ b/filesystem/iso9660/iso9660_defs.ixx
@@ -300,7 +300,14 @@ std::string split_identifier(uint32_t &version, std::string identifier)
 {
     auto s = identifier.find_last_of((char)iso9660::Characters::SEPARATOR2);
 
-    version = (s == std::string::npos ? 0 : str_to_int(identifier.substr(s + 1)));
+    if(s == std::string::npos)
+        version = 0;
+    else
+    {
+        auto v = str_to_int64(identifier.substr(s + 1));
+        if(!v)
+            version = 0;
+    }
 
     return identifier.substr(0, s);
 }
diff --git a/filesystem/iso9660/iso9660_map.ixx b/filesystem/iso9660/iso9660_map.ixx
index 390b353a..334ddcce 100644
--- a/filesystem/iso9660/iso9660_map.ixx
+++ b/filesystem/iso9660/iso9660_map.ixx
@@ -89,7 +89,7 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
         return area_vector;
 
     uint32_t sector_size = sector_reader->sectorSize();
-    if(pvd.logical_block_size.lsb != sector_size)
+    if(pvd.logical_block_size.lsb && pvd.logical_block_size.lsb != sector_size)
         throw_line("unsupported logical block size (block size: {})", pvd.logical_block_size.lsb);
 
     std::map<uint32_t, Area> area_map;
@@ -138,7 +138,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
             std::string identifier((const char *)&path_table[i], pr.length);
             if(identifier == std::string(1, (char)iso9660::Characters::DIR_CURRENT))
                 identifier.clear();
-            std::string name((pr.parent_directory_number == 1 ? "" : names[pr.parent_directory_number - 1]) + "/" + identifier);
+            std::string name;
+            if(pr.parent_directory_number <= 1 || pr.parent_directory_number > names.size())
+                name = "";
+            else
+                name = names[pr.parent_directory_number - 1] + "/" + identifier;
             names.push_back(name);
 
             i += round_up(pr.length, (uint8_t)2) + pr.xa_length;
@@ -167,7 +171,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
                     std::string dr_name = split_identifier(dr_version, dr.first);
 
                     o = base_offset + dr.second.offset.lsb - sector_reader->sectorsBase();
-                    area_map.emplace(o, Area{ o, Area::Type::FILE_EXTENT, dr.second.data_length.lsb, (name == "/" ? "" : name) + "/" + dr_name });
+                    auto area = Area{ o, Area::Type::FILE_EXTENT, dr.second.data_length.lsb, (name == "/" ? "" : name) + "/" + dr_name };
+                    if(auto it = area_map.find(o); it == area_map.end())
+                        area_map.emplace(o, area);
+                    else if(area.size > it->second.size)
+                        it->second = area;
                 }
             }
         }
diff --git a/info.ixx b/info.ixx
index e016fe96..4b03e5e6 100644
--- a/info.ixx
+++ b/info.ixx
@@ -31,14 +31,6 @@ import utils.strings;
 namespace gpsxre
 {
 
-enum class TrackType
-{
-    DATA,
-    AUDIO,
-    ISO
-};
-
-
 export int redumper_info(Context &ctx, Options &options)
 {
     int exit_code = 0;
@@ -49,11 +41,11 @@ export int redumper_info(Context &ctx, Options &options)
     if(std::filesystem::exists(image_prefix + ".cue"))
     {
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
-            tracks.emplace_back(std::filesystem::path(options.image_path) / t.first, t.second ? TrackType::DATA : TrackType::AUDIO);
+            tracks.emplace_back(std::filesystem::path(options.image_path) / t.first, t.second);
     }
     else if(std::filesystem::exists(image_prefix + ".iso"))
     {
-        tracks.emplace_back(image_prefix + ".iso", TrackType::ISO);
+        tracks.emplace_back(image_prefix + ".iso", TrackType::MODE1_2048);
     }
     else
         throw_line("image file not found");
@@ -64,9 +56,9 @@ export int redumper_info(Context &ctx, Options &options)
         std::shared_ptr<SectorReader> raw_reader;
         std::shared_ptr<SectorReader> form1_reader;
 
-        if(t.second == TrackType::ISO)
+        if(track_type_is_data_iso(t.second))
             form1_reader = std::make_shared<Image_ISO_Reader>(t.first);
-        else if(t.second == TrackType::DATA)
+        else if(track_type_is_data_raw(t.second))
         {
             raw_reader = std::make_shared<Image_RawReader>(t.first);
             form1_reader = std::make_shared<Image_BIN_Form1Reader>(t.first);
diff --git a/skeleton.ixx b/skeleton.ixx
index 189c8471..47a01267 100644
--- a/skeleton.ixx
+++ b/skeleton.ixx
@@ -203,12 +203,12 @@ export int redumper_skeleton(Context &ctx, Options &options)
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
         {
             // skip audio tracks
-            if(!t.second)
+            if(!track_type_is_data(t.second))
                 continue;
 
             auto track_prefix = (std::filesystem::path(options.image_path) / std::filesystem::path(t.first).stem()).string();
 
-            skeleton(track_prefix, (std::filesystem::path(options.image_path) / t.first).string(), false, options);
+            skeleton(track_prefix, (std::filesystem::path(options.image_path) / t.first).string(), track_type_is_data_iso(t.second), options);
         }
     }
     else if(std::filesystem::exists(image_prefix + ".iso"))


Reviews:
user: login: superg
body: I am very iffy about excluding tracks based on some CUE sheet settings.

If track is not AUDIO, it's data. If it's data, it should be passed further to filesystem handling code that should make a decision if this is something it can work with.
By filtering early CDI/2352 you don't fix the problem, you just skip all discs that it can work with.

In [this] (https://github.com/superg/redumper/issues/124) case, area_map() is finding legit PVD descriptor so it can probably create skeleton for it but it throws an exception because PVD block size is zeroed. This likely happens because CDI came a bit earlier and `pvd.logical_block_size.lsb` field initially was as reserved, I think we can just change the check to `if(pvd.logical_block_size.lsb && pvd.logical_block_size.lsb != sector_size)` and verify that skeleton is getting created and remove everything related to CUE sheet track type handling and keep it as it was.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-25T23:18:00Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T02:48:08Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T02:55:24Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T05:20:03Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T05:27:07Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T06:34:36Z
user: login: superg
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-27T00:42:48Z
user: login: superg
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-27T01:22:44Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T01:54:22Z
user: login: superg
body: Ok let's keep TrackType logic. 
The only standing up issue is path table invalid PDN, I want to make sure we construct a full file path correctly so if you could either get me debug printout I asked or even share the data so I can debug myself that would be great. 

EDIT: Don't forget to change `if(pvd.logical_block_size.lsb && pvd.logical_block_size.lsb != sector_size)` in iso9660_map.ixx
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-27T02:03:50Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T02:23:06Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T02:57:09Z
user: login: Deterous
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-27T03:12:01Z
user: login: superg
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-27T14:18:09Z

Review comments:
diff_hunk: @@ -574,26 +591,49 @@ export std::string track_extract_basename(std::string str)
 }
 
 
-export std::list<std::pair<std::string, bool>> cue_get_entries(const std::filesystem::path &cue_path)
+export std::list<std::pair<std::string, TrackType>> cue_get_entries(const std::filesystem::path &cue_path)
 {
-    std::list<std::pair<std::string, bool>> entries;
+    std::list<std::pair<std::string, TrackType>> entries;
 
     std::fstream fs(cue_path, std::fstream::in);
     if(!fs.is_open())
         throw_line("unable to open file ({})", cue_path.filename().string());
 
-    std::pair<std::string, bool> entry;
+    std::pair<std::string, TrackType> entry;
     std::string line;
     while(std::getline(fs, line))
     {
         auto tokens(tokenize(line, " \t", "\"\""));
         if(tokens.size() == 3)
         {
-            if(tokens[0] == "FILE")
+            if(tokens[0].substr(0, 4) == "FILE")
                 entry.first = tokens[1];
-            else if(tokens[0] == "TRACK" && !entry.first.empty())
+            else if(tokens[0].substr(0, 5) == "TRACK" && !entry.first.empty())
             {
-                entry.second = tokens[2] != "AUDIO";
+                if(tokens[2].substr(0, 5) == "AUDIO")
+                    entry.second = TrackType::AUDIO;
+                else if(tokens[2].substr(0, 10) == "MODE1/2352")
+                    entry.second = TrackType::MODE1_2352;
+                else if(tokens[2].substr(0, 10) == "MODE2/2352")
+                    entry.second = TrackType::MODE2_2352;
+                else if(tokens[2].substr(0, 8) == "CDI/2352")
+                    entry.second = TrackType::CDI_2352;
+                else if(tokens[2].substr(0, 10) == "MODE1/2048")
+                    entry.second = TrackType::MODE1_2048;
+                else if(tokens[2].substr(0, 10) == "MODE2/2336")
+                    entry.second = TrackType::MODE2_2336;
+                else if(tokens[2].substr(0, 8) == "CDI/2336")
+                    entry.second = TrackType::CDI_2336;
+                else if(tokens[2].substr(0, 3) == "CDG")
+                    entry.second = TrackType::CDG;
+                else if(tokens[2].substr(0, 10) == "MODE0/2352")
+                    entry.second = TrackType::MODE0_2352;
+                else if(tokens[2].substr(0, 10) == "MODE2/2048")
+                    entry.second = TrackType::MODE2_2048;
+                else if(tokens[2].substr(0, 10) == "MODE2/2324")
path: cd/cd_common.ixx
user: login: superg
body: I don't understand why substr() have to be used here?
All substr() like this are error prone because you hardcode size, even if you need to check if std::string starts with a prefix, you should use `str.starts_with("ASDF")`

But here, for a record `TRACK 11 MODE2/2352` you get
3 tokens:
token[0]: "TRACK"
token[1]: "11"
token[2]: "MODE2/2352"

why can't you do `if(tokens[0] == "TRACK")` and `if(tokens[2] == "MODE1/2352")`?
created_at: 2025-04-25T21:21:47Z
author_association: OWNER
diff_hunk: @@ -124,7 +124,7 @@ export int redumper_fix_msf(Context &ctx, Options &options)
     std::list<std::filesystem::path> tracks;
     if(std::filesystem::exists(image_prefix + ".cue"))
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
-            if(t.second)
+            if(t.second == TrackType::MODE1_2352 || t.second == TrackType::MODE2_2352 || t.second == TrackType::CDI_2352)
path: cd/fix_msf.ixx
user: login: superg
body: What specifically does this fix?
It actually makes things worse as if you encounter MODE0/2352 that has MSF field or other modes (not super familiar with CUE mode layouts), it will not get repaired.
Please revert this to check for AUDIO.
created_at: 2025-04-25T21:35:19Z
author_association: OWNER
diff_hunk: @@ -300,7 +300,14 @@ std::string split_identifier(uint32_t &version, std::string identifier)
 {
     auto s = identifier.find_last_of((char)iso9660::Characters::SEPARATOR2);
 
-    version = (s == std::string::npos ? 0 : str_to_int(identifier.substr(s + 1)));
+    if(s == std::string::npos || s + 1 >= identifier.size())
path: filesystem/iso9660/iso9660_defs.ixx
user: login: superg
body: I think ` || s + 1 >= identifier.size()` part can be removed, find_last_of() guarantees to return only index inside string or npos and `identifier.substr(s + 1)` will work correctly by returning an empty string.

created_at: 2025-04-25T21:49:52Z
author_association: OWNER
diff_hunk: @@ -300,7 +300,14 @@ std::string split_identifier(uint32_t &version, std::string identifier)
 {
     auto s = identifier.find_last_of((char)iso9660::Characters::SEPARATOR2);
 
-    version = (s == std::string::npos ? 0 : str_to_int(identifier.substr(s + 1)));
+    if(s == std::string::npos || s + 1 >= identifier.size())
+        version = 0;
+    else
+    {
+        auto version = str_to_int64(identifier.substr(s + 1));
+        if(!version)
+            version = 0;
path: filesystem/iso9660/iso9660_defs.ixx
user: login: superg
body: This shadows version argument, please rename it to `auto v = ....` to avoid future confusion.
created_at: 2025-04-25T21:51:08Z
author_association: OWNER
diff_hunk: @@ -138,7 +138,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
             std::string identifier((const char *)&path_table[i], pr.length);
             if(identifier == std::string(1, (char)iso9660::Characters::DIR_CURRENT))
                 identifier.clear();
-            std::string name((pr.parent_directory_number == 1 ? "" : names[pr.parent_directory_number - 1]) + "/" + identifier);
+            std::string name;
+            if(pr.parent_directory_number <= 1 || pr.parent_directory_number > names.size())
path: filesystem/iso9660/iso9660_map.ixx
user: login: superg
body: Did you verify that such pr.parent_directory_number == 0 records cases do not affect names ordering?
(e.g. add to names vector or not)

I am concerned about `pr.parent_directory_number > names.size()` case as names are filled in as we go. 
Can you do a raw debug printout of all PathRecord structures and identifiers for the affected iso? 
I'm just thinking about the cases where this list might be unsorted or of there are gaps in pr.parent_directory_number because in this case I think we can handle that better.
created_at: 2025-04-25T22:21:39Z
author_association: OWNER
diff_hunk: @@ -167,7 +171,17 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
                     std::string dr_name = split_identifier(dr_version, dr.first);
 
                     o = base_offset + dr.second.offset.lsb - sector_reader->sectorsBase();
-                    area_map.emplace(o, Area{ o, Area::Type::FILE_EXTENT, dr.second.data_length.lsb, (name == "/" ? "" : name) + "/" + dr_name });
+                    auto it = area_map.find(o);
+                    if(it != area_map.end())
+                    {
+                        Area new_area = Area{ o, Area::Type::FILE_EXTENT, dr.second.data_length.lsb, (name == "/" ? "" : name) + "/" + dr_name };
+                        if(new_area.size > it->second.size)
+                            it->second = new_area;
+                    }
+                    else
+                    {
+                        area_map.emplace(o, Area{ o, Area::Type::FILE_EXTENT, dr.second.data_length.lsb, (name == "/" ? "" : name) + "/" + dr_name });
+                    }
path: filesystem/iso9660/iso9660_map.ixx
user: login: superg
body: Oof, so we get overlapping files?

Let's avoid some code duplication and simplify, something like this:
```
auto area = Area{ o, Area::Type::FILE_EXTENT, dr.second.data_length.lsb, (name == "/" ? "" : name) + "/" + dr_name };

if(auto it = area_map.find(o); it == area_map.end())
    area_map.emplace(o, area });
else if(area.size > it->second.size)
        it->second = area;
```
created_at: 2025-04-25T22:34:15Z
author_association: OWNER
diff_hunk: @@ -66,7 +58,7 @@ export int redumper_info(Context &ctx, Options &options)
 
         if(t.second == TrackType::ISO)
             form1_reader = std::make_shared<Image_ISO_Reader>(t.first);
-        else if(t.second == TrackType::DATA)
+        else if(t.second == TrackType::MODE1_2352 || t.second == TrackType::MODE2_2352 || t.second == TrackType::CDI_2352)
path: info.ixx
user: login: superg
body: We shouldn't do this, I will leave PR comment on this.
created_at: 2025-04-25T22:45:40Z
author_association: OWNER
diff_hunk: @@ -202,8 +202,8 @@ export int redumper_skeleton(Context &ctx, Options &options)
     {
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
         {
-            // skip audio tracks
-            if(!t.second)
+            // only support MODE1/MODE2 CD-ROM dumps
+            if(t.second != TrackType::MODE1_2352 && t.second != TrackType::MODE2_2352)
path: skeleton.ixx
user: login: superg
body: I will leave PR comment on this.
created_at: 2025-04-25T22:46:00Z
author_association: OWNER
diff_hunk: @@ -132,8 +132,8 @@ void skeleton(const std::string &image_prefix, const std::string &image_path, bo
         {
             uint32_t gap_size = area_map[i + 1].offset - gap_start;
 
-            // 5% or more in relation to the total filesystem size
-            if((uint64_t)gap_size * 100 / sectors_count > 5)
+            // >2048 sectors or >5% or more in relation to the total filesystem size
+            if(gap_size > 2048 || (uint64_t)gap_size * 100 / sectors_count > 5)
path: skeleton.ixx
user: login: superg
body: This is still not deterministic and my 5% logic seem to be wrong here. 
I know that some PS2 DVD's have assets that aren't defined in ISO9660 or maybe I am missing a good way to detect those assets, it definitely needs some investigation. If in the end there is no way to tell based on iso9660, we shouldn't make a decision based off the size and percentage but instead scan gap area sector contents and if it's "mostly zeroed", do not exclude it, if it's "mostly data", then exclude.
This will allow us not to miss a lot of gaps for a lot of discs and still generate good skeletons.

created_at: 2025-04-25T23:01:39Z
author_association: OWNER
diff_hunk: @@ -574,26 +591,49 @@ export std::string track_extract_basename(std::string str)
 }
 
 
-export std::list<std::pair<std::string, bool>> cue_get_entries(const std::filesystem::path &cue_path)
+export std::list<std::pair<std::string, TrackType>> cue_get_entries(const std::filesystem::path &cue_path)
 {
-    std::list<std::pair<std::string, bool>> entries;
+    std::list<std::pair<std::string, TrackType>> entries;
 
     std::fstream fs(cue_path, std::fstream::in);
     if(!fs.is_open())
         throw_line("unable to open file ({})", cue_path.filename().string());
 
-    std::pair<std::string, bool> entry;
+    std::pair<std::string, TrackType> entry;
     std::string line;
     while(std::getline(fs, line))
     {
         auto tokens(tokenize(line, " \t", "\"\""));
         if(tokens.size() == 3)
         {
-            if(tokens[0] == "FILE")
+            if(tokens[0].substr(0, 4) == "FILE")
                 entry.first = tokens[1];
-            else if(tokens[0] == "TRACK" && !entry.first.empty())
+            else if(tokens[0].substr(0, 5) == "TRACK" && !entry.first.empty())
             {
-                entry.second = tokens[2] != "AUDIO";
+                if(tokens[2].substr(0, 5) == "AUDIO")
+                    entry.second = TrackType::AUDIO;
+                else if(tokens[2].substr(0, 10) == "MODE1/2352")
+                    entry.second = TrackType::MODE1_2352;
+                else if(tokens[2].substr(0, 10) == "MODE2/2352")
+                    entry.second = TrackType::MODE2_2352;
+                else if(tokens[2].substr(0, 8) == "CDI/2352")
+                    entry.second = TrackType::CDI_2352;
+                else if(tokens[2].substr(0, 10) == "MODE1/2048")
+                    entry.second = TrackType::MODE1_2048;
+                else if(tokens[2].substr(0, 10) == "MODE2/2336")
+                    entry.second = TrackType::MODE2_2336;
+                else if(tokens[2].substr(0, 8) == "CDI/2336")
+                    entry.second = TrackType::CDI_2336;
+                else if(tokens[2].substr(0, 3) == "CDG")
+                    entry.second = TrackType::CDG;
+                else if(tokens[2].substr(0, 10) == "MODE0/2352")
+                    entry.second = TrackType::MODE0_2352;
+                else if(tokens[2].substr(0, 10) == "MODE2/2048")
+                    entry.second = TrackType::MODE2_2048;
+                else if(tokens[2].substr(0, 10) == "MODE2/2324")
path: cd/cd_common.ixx
user: login: Deterous
body: Without substr, CRLF CUEs don't match. I could try having carriage return as a delimiter in tokenize to avoid this.
created_at: 2025-04-26T02:48:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,7 +58,7 @@ export int redumper_info(Context &ctx, Options &options)
 
         if(t.second == TrackType::ISO)
             form1_reader = std::make_shared<Image_ISO_Reader>(t.first);
-        else if(t.second == TrackType::DATA)
+        else if(t.second == TrackType::MODE1_2352 || t.second == TrackType::MODE2_2352 || t.second == TrackType::CDI_2352)
path: info.ixx
user: login: Deterous
body: I've added MODE0. Other non-2048/2352 byte modes are not supported currently.
For reference, those three CUE types are the only modes that redump.org CUEs have.
created_at: 2025-04-26T02:55:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -124,7 +124,7 @@ export int redumper_fix_msf(Context &ctx, Options &options)
     std::list<std::filesystem::path> tracks;
     if(std::filesystem::exists(image_prefix + ".cue"))
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
-            if(t.second)
+            if(t.second == TrackType::MODE1_2352 || t.second == TrackType::MODE2_2352 || t.second == TrackType::CDI_2352)
path: cd/fix_msf.ixx
user: login: Deterous
body: fix_msf doesn't work for non-2352-byte data tracks, so this prevents it from happening. I've added MODE0. Redumper and redump.org don't generate data-track CUEs that don't pass this.
created_at: 2025-04-26T05:20:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,8 +202,8 @@ export int redumper_skeleton(Context &ctx, Options &options)
     {
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
         {
-            // skip audio tracks
-            if(!t.second)
+            // only support MODE1/MODE2 CD-ROM dumps
+            if(t.second != TrackType::MODE1_2352 && t.second != TrackType::MODE2_2352)
path: skeleton.ixx
user: login: Deterous
body: I've fixed it such that skeleton supports all CUE data modes that have 2352 or 2048 sectors.
created_at: 2025-04-26T05:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,7 +138,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
             std::string identifier((const char *)&path_table[i], pr.length);
             if(identifier == std::string(1, (char)iso9660::Characters::DIR_CURRENT))
                 identifier.clear();
-            std::string name((pr.parent_directory_number == 1 ? "" : names[pr.parent_directory_number - 1]) + "/" + identifier);
+            std::string name;
+            if(pr.parent_directory_number <= 1 || pr.parent_directory_number > names.size())
path: filesystem/iso9660/iso9660_map.ixx
user: login: Deterous
body: Dumps with PRN of 0 and PRN of 2999 (or some other unexpected large number) were tested and skeleton command successfully wiped those files. I can find and re-run skeleton on one of them to get the ISO map debug output for you
created_at: 2025-04-26T06:34:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,7 +138,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
             std::string identifier((const char *)&path_table[i], pr.length);
             if(identifier == std::string(1, (char)iso9660::Characters::DIR_CURRENT))
                 identifier.clear();
-            std::string name((pr.parent_directory_number == 1 ? "" : names[pr.parent_directory_number - 1]) + "/" + identifier);
+            std::string name;
+            if(pr.parent_directory_number <= 1 || pr.parent_directory_number > names.size())
path: filesystem/iso9660/iso9660_map.ixx
user: login: superg
body: names[pr.parent_directory_number - 1] takes previously added directory name. In case of invalid PDN if you push back empty or not push back empty it might skip a whole directory level.
created_at: 2025-04-27T00:42:48Z
author_association: OWNER
diff_hunk: @@ -574,26 +591,49 @@ export std::string track_extract_basename(std::string str)
 }
 
 
-export std::list<std::pair<std::string, bool>> cue_get_entries(const std::filesystem::path &cue_path)
+export std::list<std::pair<std::string, TrackType>> cue_get_entries(const std::filesystem::path &cue_path)
 {
-    std::list<std::pair<std::string, bool>> entries;
+    std::list<std::pair<std::string, TrackType>> entries;
 
     std::fstream fs(cue_path, std::fstream::in);
     if(!fs.is_open())
         throw_line("unable to open file ({})", cue_path.filename().string());
 
-    std::pair<std::string, bool> entry;
+    std::pair<std::string, TrackType> entry;
     std::string line;
     while(std::getline(fs, line))
     {
-        auto tokens(tokenize(line, " \t", "\"\""));
+        auto tokens(tokenize(line, " \t\r", "\"\""));
path: cd/cd_common.ixx
user: login: superg
body: This happens only if you read Windows generated cue sheets on Linux, but I admit that's legit usecase and it fixes it.
created_at: 2025-04-27T01:22:44Z
author_association: OWNER
diff_hunk: @@ -43,6 +43,23 @@ export constexpr int32_t LBA_START = -45150; // MSVC internal compiler error: MS
 export constexpr uint32_t LEADOUT_OVERREAD_COUNT = 100;
 
 
+export enum class TrackType : uint8_t
+{
+    AUDIO = 0,
+    MODE1_2352 = 1,
+    MODE2_2352 = 2,
+    CDI_2352 = 3,
+    MODE1_2048 = 4,
+    MODE2_2336 = 5,
+    CDI_2336 = 6,
+    CDG = 7,
+    MODE0_2352 = 8,
+    MODE2_2048 = 9,
+    MODE2_2324 = 10,
+    ISO = 255
+};
+
path: cd/cd_common.ixx
user: login: superg
body: Ok, let's keep this functionality for future.

Let's change enum to:
```
export enum class TrackType
{
    AUDIO,
    MODE1_2352,
    MODE2_2352,
    CDI_2352,
    MODE1_2048,
    MODE2_2336,
    CDI_2336,
    CDG,
    MODE0_2352,
    MODE2_2048,
    MODE2_2324,
    UNKNOWN
};
```
(no need for storage type as we don't serialize it, I don't like ISO being the catch all value and enum values aren't needed)

and have couple functions in the same file:
```
export bool track_type_is_data_raw(TrackType track_type)
{
    return track_type == TrackType::MODE1_2352 || track_type == TrackType::MODE2_2352 || track_type == TrackType::CDI_2352 || track_type == TrackType::MODE0_2352;
}

export bool track_type_is_data_iso(TrackType track_type)
{
    return track_type == TrackType::MODE1_2048 || track_type == TrackType::MODE2_2048;
}

export bool track_type_is_data(TrackType track_type)
{
    return track_type_is_data_raw(track_type) || track_type_is_data_iso(track_type);
}
```

Reuse these functions everywhere we need to check what track is.
created_at: 2025-04-27T01:36:53Z
author_association: OWNER
diff_hunk: @@ -124,7 +124,7 @@ export int redumper_fix_msf(Context &ctx, Options &options)
     std::list<std::filesystem::path> tracks;
     if(std::filesystem::exists(image_prefix + ".cue"))
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
-            if(t.second)
+            if(t.second == TrackType::MODE1_2352 || t.second == TrackType::MODE2_2352 || t.second == TrackType::CDI_2352 || t.second == TrackType::MODE0_2352)
path: cd/fix_msf.ixx
user: login: superg
body: Use `track_type_is_data_raw(t.second)`
created_at: 2025-04-27T01:38:51Z
author_association: OWNER
diff_hunk: @@ -64,9 +56,9 @@ export int redumper_info(Context &ctx, Options &options)
         std::shared_ptr<SectorReader> raw_reader;
         std::shared_ptr<SectorReader> form1_reader;
 
-        if(t.second == TrackType::ISO)
+        if(t.second == TrackType::ISO || t.second == TrackType::MODE1_2048 || t.second == TrackType::MODE2_2048)
             form1_reader = std::make_shared<Image_ISO_Reader>(t.first);
-        else if(t.second == TrackType::DATA)
+        else if(t.second == TrackType::MODE1_2352 || t.second == TrackType::MODE2_2352 || t.second == TrackType::CDI_2352 || t.second == TrackType::MODE0_2352)
path: info.ixx
user: login: superg
body: use `track_type_is_data_raw(TrackType track_type)` here
created_at: 2025-04-27T01:43:21Z
author_association: OWNER
diff_hunk: @@ -64,9 +56,9 @@ export int redumper_info(Context &ctx, Options &options)
         std::shared_ptr<SectorReader> raw_reader;
         std::shared_ptr<SectorReader> form1_reader;
 
-        if(t.second == TrackType::ISO)
+        if(t.second == TrackType::ISO || t.second == TrackType::MODE1_2048 || t.second == TrackType::MODE2_2048)
path: info.ixx
user: login: superg
body: use `track_type_is_data_iso(TrackType track_type)` here and for .iso if above please use TrackType::MODE1_2048, I don't want to passthrough UNKNOWN here because it's technically all the other "invalid" modes redumper doesn't want to support.
created_at: 2025-04-27T01:43:35Z
author_association: OWNER
diff_hunk: @@ -203,12 +203,20 @@ export int redumper_skeleton(Context &ctx, Options &options)
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
         {
             // skip audio tracks
-            if(!t.second)
+            if(t.second == TrackType::AUDIO || t.second == TrackType::CDG)
path: skeleton.ixx
user: login: superg
body: Please use `if(!track_type_is_data(t.second))`
created_at: 2025-04-27T01:49:06Z
author_association: OWNER
diff_hunk: @@ -203,12 +203,20 @@ export int redumper_skeleton(Context &ctx, Options &options)
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
         {
             // skip audio tracks
-            if(!t.second)
+            if(t.second == TrackType::AUDIO || t.second == TrackType::CDG)
                 continue;
 
+            // skip unsupported sector sizes
+            if(t.second == TrackType::MODE2_2336 || t.second == TrackType::CDI_2336 || t.second == TrackType::MODE2_2324)
+                continue;
+
+            bool iso = false;
+            if(t.second == TrackType::ISO || t.second == TrackType::MODE1_2048 || t.second == TrackType::MODE2_2048)
+                iso = true;
+
             auto track_prefix = (std::filesystem::path(options.image_path) / std::filesystem::path(t.first).stem()).string();
 
-            skeleton(track_prefix, (std::filesystem::path(options.image_path) / t.first).string(), false, options);
+            skeleton(track_prefix, (std::filesystem::path(options.image_path) / t.first).string(), iso, options);
path: skeleton.ixx
user: login: superg
body: And finally here `skeleton(track_prefix, (std::filesystem::path(options.image_path) / t.first).string(), track_type_is_data_iso(t.second), options);`
created_at: 2025-04-27T01:50:54Z
author_association: OWNER
diff_hunk: @@ -203,12 +203,20 @@ export int redumper_skeleton(Context &ctx, Options &options)
         for(auto const &t : cue_get_entries(image_prefix + ".cue"))
         {
             // skip audio tracks
-            if(!t.second)
+            if(t.second == TrackType::AUDIO || t.second == TrackType::CDG)
                 continue;
 
+            // skip unsupported sector sizes
+            if(t.second == TrackType::MODE2_2336 || t.second == TrackType::CDI_2336 || t.second == TrackType::MODE2_2324)
+                continue;
+
+            bool iso = false;
+            if(t.second == TrackType::ISO || t.second == TrackType::MODE1_2048 || t.second == TrackType::MODE2_2048)
+                iso = true;
+
path: skeleton.ixx
user: login: superg
body: This block can be removed now.
created_at: 2025-04-27T01:51:11Z
author_association: OWNER
diff_hunk: @@ -132,8 +132,8 @@ void skeleton(const std::string &image_prefix, const std::string &image_path, bo
         {
             uint32_t gap_size = area_map[i + 1].offset - gap_start;
 
-            // 5% or more in relation to the total filesystem size
-            if((uint64_t)gap_size * 100 / sectors_count > 5)
+            // >2048 sectors or >5% or more in relation to the total filesystem size
+            if(gap_size > 2048 || (uint64_t)gap_size * 100 / sectors_count > 5)
path: skeleton.ixx
user: login: Deterous
body: Okay, it could be more intelligent. Look for contiguous non-zero sector ranges in the gap that sum to at least X sectors or >X% of the dump size (or the gap size), and append those?
created_at: 2025-04-27T01:54:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -132,8 +132,8 @@ void skeleton(const std::string &image_prefix, const std::string &image_path, bo
         {
             uint32_t gap_size = area_map[i + 1].offset - gap_start;
 
-            // 5% or more in relation to the total filesystem size
-            if((uint64_t)gap_size * 100 / sectors_count > 5)
+            // >2048 sectors or >5% or more in relation to the total filesystem size
+            if(gap_size > 2048 || (uint64_t)gap_size * 100 / sectors_count > 5)
path: skeleton.ixx
user: login: superg
body: I would count a number of empty sectors in a gap and do fraction empty_sectors_count / total_gap_sectors_count and see if it's above X% threshold, treat it as empty gap.

This code might be a little bit annoying because gap can mix different mode sectors. I can add that myself based on a similar code that I wrote somewhere, have to find it.

If you would prefer that, let me know.

created_at: 2025-04-27T02:03:43Z
author_association: OWNER
diff_hunk: @@ -138,7 +138,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
             std::string identifier((const char *)&path_table[i], pr.length);
             if(identifier == std::string(1, (char)iso9660::Characters::DIR_CURRENT))
                 identifier.clear();
-            std::string name((pr.parent_directory_number == 1 ? "" : names[pr.parent_directory_number - 1]) + "/" + identifier);
+            std::string name;
+            if(pr.parent_directory_number <= 1 || pr.parent_directory_number > names.size())
path: filesystem/iso9660/iso9660_map.ixx
user: login: Deterous
body: You're right that it's not printing the proper directory so something needs fixing. 
The example I have is from a disc with DRM (issue #123), so the record table is likely purposefully weird. I am testing it now, but also included the skeleton here if you want to have a look.
[viking.zip](https://github.com/user-attachments/files/19926550/viking.zip)
created_at: 2025-04-27T02:23:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,7 +138,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
             std::string identifier((const char *)&path_table[i], pr.length);
             if(identifier == std::string(1, (char)iso9660::Characters::DIR_CURRENT))
                 identifier.clear();
-            std::string name((pr.parent_directory_number == 1 ? "" : names[pr.parent_directory_number - 1]) + "/" + identifier);
+            std::string name;
+            if(pr.parent_directory_number <= 1 || pr.parent_directory_number > names.size())
path: filesystem/iso9660/iso9660_map.ixx
user: login: Deterous
body: Actually in this case the unusual PRN is not affecting the name, but its still incorrect due to VIDEO_TS having PRN 1?
```
table[8]: PRN: 1, identifier:
table[18]: PRN: 1, identifier: AUDIO_TS
table[34]: PRN: 1, identifier: VIDEO_TS
table[42]: file name:
table[42]: file name: VIDEO_TS.BUP
table[42]: file name: VIDEO_TS.IFO
...
table[50]: PRN: 0, identifier:
table[58]: PRN: 0, identifier:
...
```

```
ISO9660 map:
LBA: [     0 ..     15], count:     16, type: SYSTEM_AREA
LBA: [    16 ..     17], count:      2, type: DESCRIPTORS
LBA: [   257 ..    257], count:      1, type: DIRECTORY_EXTENT
LBA: [   258 ..    258], count:      1, type: DIRECTORY_EXTENT
LBA: [   259 ..    259], count:      1, type: DIRECTORY_EXTENT
LBA: [   260 ..    260], count:      1, type: PATH_TABLE_L
LBA: [   261 ..    261], count:      1, type: PATH_TABLE_M
LBA: [   262 ..    263], count:      2, type: FILE_EXTENT, name: /
LBA: [   294 ..    301], count:      8, type: FILE_EXTENT, name: /VIDEO_TS.IFO
LBA: [   302 ..    358], count:     57, type: FILE_EXTENT, name: /VIDEO_TS.VOB
...
```
created_at: 2025-04-27T02:57:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -132,8 +132,8 @@ void skeleton(const std::string &image_prefix, const std::string &image_path, bo
         {
             uint32_t gap_size = area_map[i + 1].offset - gap_start;
 
-            // 5% or more in relation to the total filesystem size
-            if((uint64_t)gap_size * 100 / sectors_count > 5)
+            // >2048 sectors or >5% or more in relation to the total filesystem size
+            if(gap_size > 2048 || (uint64_t)gap_size * 100 / sectors_count > 5)
path: skeleton.ixx
user: login: Deterous
body: A pure % check can be annoying with small gaps (1 sector of data in a 5 sector gap would get wiped) and with very large gaps (1% of a 1GB gap is still a lot of data to not be wiped).
created_at: 2025-04-27T03:12:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,7 +138,11 @@ std::vector<Area> area_map(SectorReader *sector_reader, uint32_t base_offset, ui
             std::string identifier((const char *)&path_table[i], pr.length);
             if(identifier == std::string(1, (char)iso9660::Characters::DIR_CURRENT))
                 identifier.clear();
-            std::string name((pr.parent_directory_number == 1 ? "" : names[pr.parent_directory_number - 1]) + "/" + identifier);
+            std::string name;
+            if(pr.parent_directory_number <= 1 || pr.parent_directory_number > names.size())
path: filesystem/iso9660/iso9660_map.ixx
user: login: superg
body: Tracking ticket: https://github.com/superg/redumper/issues/237
created_at: 2025-04-27T14:18:09Z
author_association: OWNER

PR comments:
user: login: Deterous
created_at: 2025-04-26T04:16:22Z
body: Sorry, I forgot MODE0 in a few places. MODE0 is not in the cue sheet spec, but I know redumper does use it. I should have included it in the if(mode == DATA) statements.

However I do believe the changes to cue sheet parsing is an improvement. While redumper and redump.org do not use the other modes, anyone using redumper on CUEs with other modes would cause an error.

Also, skeletons created from CUEs with CDI/2352 are currently functionally useless (just SYSTEM_AREA and a single GAP file). Redumper doesn't generate CDI/2352 mode, but redump.org CUE downloads do use it. I thought it would be better to purposefully exclude skeleton generation until CDI format is better parsed for skeleton creation later. I've re-added them as they don't break anything.
author_association: CONTRIBUTOR
user: login: superg
created_at: 2025-04-27T00:53:47Z
body: https://github.com/superg/redumper/blob/main/cd/toc.ixx#L709

![image](https://github.com/user-attachments/assets/86ef82a6-e330-4e03-a271-d3fc96f90033)

```
Common Disc Info:
	Title: Hondenliefde
	System: Philips CD-i
	Media Type: CD-ROM
	Category: Video
	Region: Netherlands
	Languages: Dutch
	Disc Serial: 819 1028

	Ringcode Information:
		Mastering Code (laser branded/etched): 8191028V107 50513129 01/
		Mastering SID Code: IFPI L003
		Data-Side Mould SID Code: IFPI 0140
		Additional Mould: MADE IN GERMANY BY PMDC	B
	Barcode: 8 712581 910280
	Error Count: 0

Version and Editions:
	Edition/Release: Original

Extras:
	Primary Volume Descriptor (PVD):

0320 : 20 20 20 20 20 20 20 20  00 00 00 00 00 31 39 39           .....199
0330 : 35 30 35 31 35 31 30 31  39 34 39 30 30 00 30 30   5051510194900.00
0340 : 30 30 30 30 30 30 30 30  30 30 30 30 30 30 00 30   00000000000000.0
0350 : 30 30 30 30 30 30 30 30  30 30 30 30 30 30 30 00   000000000000000.
0360 : 30 30 30 30 30 30 30 30  30 30 30 30 30 30 30 30   0000000000000000
0370 : 00 01 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

Tracks and Write Offsets:
	DAT:

<rom name="Hondenliefde (Netherlands).bin" size="738057600" crc="2e4a7424" md5="ee2e613a3a140a790a9ae3150dc832a7" sha1="100b9489a016bf13b70c8db90a1c5e89e8daeee2" />

	Cuesheet:

CATALOG 0000000000000
FILE "Hondenliefde (Netherlands).bin" BINARY
  TRACK 01 CDI/2352
    INDEX 01 00:00:00

	Write Offset: 0

```
![image](https://github.com/user-attachments/assets/c83b8d68-dfce-4c4d-b847-3f4de8b9f2cc)

author_association: OWNER
user: login: Deterous
created_at: 2025-04-27T01:43:50Z
body: I'm definitely wrong on redumper not creating CDI CUEs. However here's the skeleton output from this CDI disc:  http://redump.org/disc/29717/

```
ISO9660 map: 
LBA: [     0 ..     15], count:     16, type: SYSTEM_AREA
LBA: [    16 ..     18], count:      3, type: DESCRIPTORS
LBA: [275736 .. 275735], count:      0, type: SPACE_END_MARKER
```
.hash file:
```
da39a3ee5e6b4b0d3255bfef95601890afd80709 SYSTEM_AREA
6f6e04dcea08b4c4cd8a5eaf64bdaddbf4cecbe0 SYSTEM_AREA.XA
482430354cfa0abc43e7db4d8e45847da6a520e9 GAP_0000019
165a7c9a5302adb4563813e3c60fe9d2b78f885a GAP_0000019.XA
```

CDI filesystem parsing needs adding for it to be useful, but at least it doesn't error out anymore
author_association: CONTRIBUTOR
user: login: superg
created_at: 2025-04-27T02:10:38Z
body: > I'm definitely wrong on redumper not creating CDI CUEs. However here's the skeleton output from this CDI disc: http://redump.org/disc/29717/
> 
> ```
> ISO9660 map: 
> LBA: [     0 ..     15], count:     16, type: SYSTEM_AREA
> LBA: [    16 ..     18], count:      3, type: DESCRIPTORS
> LBA: [275736 .. 275735], count:      0, type: SPACE_END_MARKER
> ```
> 
> .hash file:
> 
> ```
> da39a3ee5e6b4b0d3255bfef95601890afd80709 SYSTEM_AREA
> 6f6e04dcea08b4c4cd8a5eaf64bdaddbf4cecbe0 SYSTEM_AREA.XA
> 482430354cfa0abc43e7db4d8e45847da6a520e9 GAP_0000019
> 165a7c9a5302adb4563813e3c60fe9d2b78f885a GAP_0000019.XA
> ```
> 
> CDI filesystem parsing needs adding for it to be useful, but at least it doesn't error out anymore

That's ok, skeleton operates with ISO9660 knowledge and CDI contains ISO9660 filesystem. It's ok that maybe we don't parse CDI part yet (as it goes into the gap) but it's good behavior. Also as you can see from the IsoBuster screenshot, those discs are all different and might have extra iso9660 stuff there.
author_association: OWNER
user: login: Deterous
created_at: 2025-04-27T13:01:17Z
body: I have returned to the original 5% threshold for the GAP detection. I have better ideas for the skeleton support that are outside of scope for a PR that just aims to fix skeleton creation.
author_association: CONTRIBUTOR