├── src ├── mkpsxiso │ ├── global.h │ ├── edcecc.h │ ├── edcecc.cpp │ ├── cdwriter.h │ ├── miniaudio_helpers.h │ ├── iso.h │ ├── miniaudio_pcm.h │ ├── cdwriter.cpp │ └── iso.cpp ├── shared │ ├── fs.cpp │ ├── fs.h │ ├── xa.h │ ├── mmappedfile.h │ ├── platform.h │ ├── listview.h │ ├── xml.h │ ├── common.h │ ├── mmappedfile.cpp │ ├── platform.cpp │ ├── common.cpp │ └── cd.h └── dumpsxiso │ ├── cdreader.h │ ├── cdreader.cpp │ └── main.cpp ├── .gitignore ├── .gitmodules ├── CMakePresets.json ├── .github └── workflows │ └── build.yml ├── CMakeLists.txt ├── examples └── example.xml ├── README.md └── LICENSE.md /src/mkpsxiso/global.h: -------------------------------------------------------------------------------- 1 | #ifndef _GLOBAL_H 2 | #define _GLOBAL_H 3 | 4 | #include 5 | 6 | namespace global { 7 | 8 | extern time_t BuildTime; 9 | extern int QuietMode; 10 | extern int noXA; 11 | extern int trackNum; 12 | }; 13 | 14 | #endif // _GLOBAL_H 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin*/ 2 | CMakeFiles/ 3 | pages/ 4 | test/ 5 | nbproject/private/ 6 | Makefile 7 | CMakeCache.txt 8 | CMakeUserPresets.json 9 | CMakeSettings.json 10 | *.cmake 11 | *.layout 12 | *.depend 13 | *.exe 14 | *.bin 15 | *.cue 16 | .dep.inc 17 | build 18 | dist 19 | out 20 | .vs 21 | .vscode 22 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tinyxml2"] 2 | path = tinyxml2 3 | url = https://github.com/leethomason/tinyxml2 4 | [submodule "miniaudio"] 5 | path = miniaudio 6 | url = https://github.com/mackron/miniaudio 7 | [submodule "flac"] 8 | path = flac 9 | url = https://github.com/xiph/flac 10 | [submodule "ghc"] 11 | path = ghc 12 | url = https://github.com/gulrak/filesystem 13 | [submodule "ThreadPool"] 14 | path = ThreadPool 15 | url = https://github.com/progschj/ThreadPool -------------------------------------------------------------------------------- /src/shared/fs.cpp: -------------------------------------------------------------------------------- 1 | #ifdef __APPLE__ // for deployment target to support pre-catalina targets without std::fs 2 | #include 3 | #endif 4 | #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) 5 | #if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) 6 | #define GHC_USE_STD_FS 7 | #endif 8 | #endif 9 | #ifndef GHC_USE_STD_FS 10 | #define GHC_FILESYSTEM_IMPLEMENTATION 11 | #include 12 | #endif 13 | -------------------------------------------------------------------------------- /src/shared/fs.h: -------------------------------------------------------------------------------- 1 | #ifdef __APPLE__ 2 | #include // for deployment target to support pre-catalina targets without std::fs 3 | #endif 4 | #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) 5 | #if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) 6 | #define GHC_USE_STD_FS 7 | #include 8 | namespace fs { 9 | using namespace std::filesystem; 10 | } 11 | #endif 12 | #endif 13 | #ifndef GHC_USE_STD_FS 14 | #include 15 | namespace fs { 16 | using namespace ghc::filesystem; 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /src/shared/xa.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // CD-XA extension namespace 4 | // As per "System Description CD-ROM XA" by NV Philips and Sony Corporation. 5 | namespace cdxa { 6 | 7 | // XA attribute struct (located right after the identifier string) 8 | // Fields are big endian 9 | struct ISO_XA_ATTRIB { 10 | unsigned short ownergroupid; // Usually 0x0000 11 | unsigned short owneruserid; // Usually 0x0000 12 | unsigned short attributes; 13 | char id[2]; 14 | unsigned char filenum; // Usually 0x00 15 | unsigned char reserved[5]; 16 | }; 17 | 18 | // Masks for ISO_XA_ATTRIB.attributes 19 | constexpr unsigned short XA_PERMISSIONS_MASK = 0x7FF; 20 | constexpr unsigned short XA_ATTRIBUTES_MASK = ~XA_PERMISSIONS_MASK; 21 | } -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 20, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "default", 11 | "displayName": "Default configuration", 12 | "description": "Use this preset when building mkpsxiso for local installation.", 13 | "generator": "Ninja", 14 | "binaryDir": "${sourceDir}/build" 15 | }, 16 | { 17 | "name": "ci", 18 | "displayName": "CI build", 19 | "description": "This preset is used by GitHub Actions to build mkpsxiso.", 20 | "generator": "Ninja", 21 | "binaryDir": "${sourceDir}/../build", 22 | "cacheVariables": { 23 | "CMAKE_BUILD_TYPE": "Release" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/shared/mmappedfile.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Cross-platform memory mapped file wrapper 4 | 5 | #include 6 | #include "fs.h" 7 | 8 | class MMappedFile 9 | { 10 | public: 11 | class View 12 | { 13 | public: 14 | View(void* handle, uint64_t offset, size_t size); 15 | ~View(); 16 | 17 | void* GetBuffer() const { return m_data; } 18 | 19 | private: 20 | void* m_mapping = nullptr; // Aligned down to allocation granularity 21 | void* m_data = nullptr; 22 | size_t m_size = 0; // Real size, with adjustments to granularity 23 | }; 24 | 25 | MMappedFile(); 26 | ~MMappedFile(); 27 | 28 | bool Create(const fs::path& filePath, uint64_t size); 29 | View GetView(uint64_t offset, size_t size) const; 30 | 31 | private: 32 | void* m_handle = nullptr; // Opaque, platform-specific 33 | }; -------------------------------------------------------------------------------- /src/shared/platform.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "fs.h" 11 | 12 | // Printf format for fs::path::c_str() 13 | #ifdef _WIN32 14 | #define stat64 _stat64 15 | #define PRFILESYSTEM_PATH "ls" 16 | #elif defined(__APPLE__) && defined(__arm64__) 17 | // __DARWIN_ONLY_64_BIT_INO_T is set on ARM-based Macs (which then sets __DARWIN_64_BIT_INO_T). 18 | // This sets the following in Apple SDK's stat.h: struct stat __DARWIN_STRUCT_STAT64; 19 | // So use stat over stat64 for ARM-based Macs 20 | #define stat64 stat 21 | #define PRFILESYSTEM_PATH "s" 22 | #else 23 | #define PRFILESYSTEM_PATH "s" 24 | #endif 25 | 26 | namespace cd 27 | { 28 | struct ISO_DATESTAMP; 29 | } 30 | 31 | FILE* OpenFile(const fs::path& path, const char* mode); 32 | std::optional Stat(const fs::path& path); 33 | int64_t GetSize(const fs::path& path); 34 | void UpdateTimestamps(const fs::path& path, const cd::ISO_DATESTAMP& entryDate); 35 | -------------------------------------------------------------------------------- /src/mkpsxiso/edcecc.h: -------------------------------------------------------------------------------- 1 | #ifndef _EDC_ECC_H 2 | #define _EDC_ECC_H 3 | 4 | #ifdef _WIN32 5 | #ifndef NOMINMAX 6 | #define NOMINMAX 7 | #endif 8 | #include 9 | #else 10 | #include 11 | #endif 12 | 13 | class EDCECC { 14 | 15 | // Tables for EDC and ECC calculation 16 | unsigned char ecc_f_lut[256]; 17 | unsigned char ecc_b_lut[256]; 18 | unsigned int edc_lut[256]; 19 | 20 | public: 21 | 22 | // Initializer 23 | EDCECC(); 24 | 25 | // Computes the EDC of *src and returns the result 26 | unsigned int ComputeEdcBlockPartial(unsigned int edc, const unsigned char *src, size_t len) const; 27 | 28 | // Computes the EDC of *src and stores the result to an unsigned char array *dest 29 | void ComputeEdcBlock(const unsigned char *src, size_t len, unsigned char *dest) const; 30 | 31 | // Computes the ECC data of *src and stores the result to an unsigned char array *dest 32 | void ComputeEccBlock(const unsigned char *address, const unsigned char *src, unsigned int major_count, unsigned int minor_count, unsigned int major_mult, unsigned int minor_inc, unsigned char *dest) const; 33 | 34 | }; 35 | 36 | #endif // _EDC_ECC_H 37 | -------------------------------------------------------------------------------- /src/shared/listview.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // A helper class to hold a reference to a "global" list 4 | // with "local" views on top of that list. Used e.g. to store 5 | // information about files in a directory while having those 6 | // files spread freely on the disc/array. 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | template 14 | class ListView 15 | { 16 | public: 17 | using type = T; 18 | 19 | explicit ListView(std::list& list) 20 | : m_list(list) 21 | { 22 | } 23 | 24 | // Create a new view over the same list 25 | ListView NewView() const 26 | { 27 | return ListView(m_list); 28 | } 29 | 30 | template 31 | auto& emplace(Args&&... args) 32 | { 33 | auto& ref = m_list.emplace_back(std::forward(args)...); 34 | m_view.emplace_back(ref); 35 | return ref; 36 | } 37 | 38 | template 39 | void SortView(Compare&& comp) 40 | { 41 | std::sort(m_view.begin(), m_view.end(), std::forward(comp)); 42 | } 43 | 44 | // Access to the view. 45 | std::vector>& GetView() { return m_view; } 46 | const std::vector>& GetView() const { return m_view; } 47 | 48 | // Access to the entire list. Use sparingly. 49 | std::list& GetUnderlyingList() { return m_list; } 50 | const std::list& GetUnderlyingList() const { return m_list; } 51 | 52 | private: 53 | std::vector> m_view; 54 | std::list& m_list; 55 | }; 56 | -------------------------------------------------------------------------------- /src/shared/xml.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // Shared XML element and attribute names 6 | namespace xml 7 | { 8 | 9 | namespace elem 10 | { 11 | constexpr const char* ISO_PROJECT = "iso_project"; 12 | constexpr const char* IDENTIFIERS = "identifiers"; 13 | constexpr const char* LICENSE = "license"; 14 | constexpr const char* DEFAULT_ATTRIBUTES = "default_attributes"; 15 | constexpr const char* TRACK = "track"; 16 | constexpr const char* DIRECTORY_TREE = "directory_tree"; 17 | constexpr const char* FILE = "file"; 18 | constexpr const char* TRACK_PREGAP = "pregap"; 19 | } 20 | 21 | namespace attrib 22 | { 23 | constexpr const char* IMAGE_NAME = "image_name"; 24 | constexpr const char* CUE_SHEET = "cue_sheet"; 25 | constexpr const char* NO_XA = "no_xa"; 26 | 27 | constexpr const char* TRACK_TYPE = "type"; 28 | constexpr const char* TRACK_SOURCE = "source"; 29 | constexpr const char* TRACK_ID = "trackid"; 30 | constexpr const char* PREGAP_DURATION = "duration"; 31 | 32 | constexpr const char* ENTRY_NAME = "name"; 33 | constexpr const char* ENTRY_SOURCE = "source"; 34 | constexpr const char* ENTRY_TYPE = "type"; 35 | 36 | constexpr const char* LICENSE_FILE = "file"; 37 | 38 | constexpr const char* GMT_OFFSET = "gmt_offs"; 39 | constexpr const char* XA_ATTRIBUTES = "xa_attrib"; 40 | constexpr const char* XA_PERMISSIONS = "xa_perm"; 41 | constexpr const char* XA_GID = "xa_gid"; 42 | constexpr const char* XA_UID = "xa_uid"; 43 | 44 | constexpr const char* ID_FILE = "id_file"; 45 | constexpr const char* SYSTEM_ID = "system"; 46 | constexpr const char* VOLUME_ID = "volume"; 47 | constexpr const char* APPLICATION = "application"; 48 | constexpr const char* VOLUME_SET = "volume_set"; 49 | constexpr const char* PUBLISHER = "publisher"; 50 | constexpr const char* DATA_PREPARER = "data_preparer"; 51 | constexpr const char* COPYRIGHT = "copyright"; 52 | constexpr const char* CREATION_DATE = "creation_date"; 53 | constexpr const char* MODIFICATION_DATE = "modification_date"; 54 | 55 | constexpr const char* NUM_DUMMY_SECTORS = "sectors"; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/mkpsxiso/edcecc.cpp: -------------------------------------------------------------------------------- 1 | /* EDC and ECC calculation routines from ecmtools by Neill Corlett 2 | * 3 | * Its the only program where I can find routines for proper EDC/ECC calculation. 4 | */ 5 | 6 | #include "edcecc.h" 7 | 8 | EDCECC::EDCECC() { 9 | 10 | unsigned int i,j,edc; 11 | 12 | for(i=0; i<256; i++) { 13 | 14 | j = (i<<1)^(i&0x80?0x11D:0); 15 | ecc_f_lut[i] = j; 16 | ecc_b_lut[i^j] = i; 17 | edc = i; 18 | 19 | for(j=0; j<8; j++) 20 | edc=(edc>>1)^(edc&1?0xD8018001:0); 21 | 22 | edc_lut[i] = edc; 23 | 24 | } 25 | 26 | } 27 | 28 | unsigned int EDCECC::ComputeEdcBlockPartial(unsigned int edc, const unsigned char *src, size_t len) const { 29 | 30 | while(len--) 31 | edc = (edc>>8)^edc_lut[(edc^(*src++))&0xFF]; 32 | 33 | return edc; 34 | 35 | } 36 | 37 | void EDCECC::ComputeEdcBlock(const unsigned char *src, size_t len, unsigned char *dest) const { 38 | 39 | unsigned int edc = ComputeEdcBlockPartial(0, src, len); 40 | 41 | dest[0] = (edc>>0)&0xFF; 42 | dest[1] = (edc>>8)&0xFF; 43 | dest[2] = (edc>>16)&0xFF; 44 | dest[3] = (edc>>24)&0xFF; 45 | 46 | } 47 | 48 | void EDCECC::ComputeEccBlock(const unsigned char *address, const unsigned char *src, unsigned int major_count, unsigned int minor_count, unsigned int major_mult, unsigned int minor_inc, unsigned char *dest) const { 49 | 50 | unsigned int len = major_count*minor_count; 51 | unsigned int major,minor; 52 | 53 | for(major = 0; major < major_count; major++) { 54 | 55 | unsigned int index = (major >> 1) * major_mult + (major & 1); 56 | unsigned char ecc_a = 0; 57 | unsigned char ecc_b = 0; 58 | 59 | for(minor = 0; minor < minor_count; minor++) { 60 | 61 | unsigned char temp; 62 | if (index < 4) { 63 | temp = address[index]; 64 | } else { 65 | temp = src[index - 4]; 66 | } 67 | 68 | index += minor_inc; 69 | 70 | if(index >= len) 71 | index -= len; 72 | 73 | ecc_a ^= temp; 74 | ecc_b ^= temp; 75 | ecc_a = ecc_f_lut[ecc_a]; 76 | 77 | } 78 | 79 | ecc_a = ecc_b_lut[ecc_f_lut[ecc_a]^ecc_b]; 80 | dest[major] = ecc_a; 81 | dest[major+major_count] = ecc_a^ecc_b; 82 | 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions CI script for mkpsxiso 2 | # (C) 2021 spicyjpeg 3 | 4 | name: Build mkpsxiso 5 | on: [ push, pull_request ] 6 | 7 | jobs: 8 | build-windows: 9 | name: Build mkpsxiso on Windows 10 | runs-on: windows-2022 11 | 12 | steps: 13 | - name: Fetch repo contents 14 | uses: actions/checkout@v3 15 | with: 16 | path: mkpsxiso 17 | submodules: recursive 18 | 19 | - name: Build and package mkpsxiso 20 | run: | 21 | cmake --preset ci -S mkpsxiso -G "Visual Studio 17 2022" 22 | cmake --build build --config Release -t package 23 | 24 | - name: Upload build artifacts 25 | uses: actions/upload-artifact@v3 26 | with: 27 | name: mkpsxiso-windows 28 | path: | 29 | build/packages/* 30 | !build/packages/_CPack_Packages 31 | 32 | build-linux: 33 | name: Build mkpsxiso on Linux 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: Install prerequisites 38 | run: | 39 | sudo apt-get update -y 40 | sudo apt-get install -y --no-install-recommends build-essential ninja-build 41 | 42 | - name: Fetch repo contents 43 | uses: actions/checkout@v3 44 | with: 45 | path: mkpsxiso 46 | submodules: recursive 47 | 48 | - name: Build and package mkpsxiso 49 | run: | 50 | cmake --preset ci -S mkpsxiso 51 | cmake --build build -t package 52 | 53 | - name: Upload build artifacts 54 | uses: actions/upload-artifact@v3 55 | with: 56 | name: mkpsxiso-linux 57 | path: | 58 | build/packages/* 59 | !build/packages/_CPack_Packages 60 | 61 | # This job takes care of creating a new release and upload the build 62 | # artifacts if the last commit is associated to a tag. 63 | create-release: 64 | name: Create release 65 | runs-on: ubuntu-latest 66 | needs: [ build-windows, build-linux ] 67 | 68 | steps: 69 | - name: Fetch build artifacts 70 | if: ${{ github.ref_type == 'tag' }} 71 | uses: actions/download-artifact@v3 72 | with: 73 | path: . 74 | 75 | - name: Publish release 76 | if: ${{ github.ref_type == 'tag' }} 77 | uses: softprops/action-gh-release@v1 78 | with: 79 | fail_on_unmatched_files: true 80 | #generate_release_notes: true 81 | files: | 82 | mkpsxiso-windows/* 83 | mkpsxiso-linux/* 84 | -------------------------------------------------------------------------------- /src/shared/common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cd.h" 4 | #include "fs.h" 5 | #include 6 | #include 7 | #include 8 | 9 | enum class EntryType 10 | { 11 | EntryFile, 12 | EntryDir, 13 | EntryXA, 14 | EntryXA_DO, 15 | EntryDA, 16 | EntryDummy 17 | }; 18 | 19 | struct cdtrack 20 | { 21 | cdtrack(unsigned int lba, unsigned int size, std::string source = std::string()) 22 | : lba(lba), size(size), source(std::move(source)) 23 | { 24 | } 25 | 26 | unsigned int lba; 27 | unsigned int size; 28 | std::string source; 29 | }; 30 | 31 | class EntryAttributes 32 | { 33 | private: 34 | static constexpr signed char DEFAULT_GMTOFFS = 0; 35 | static constexpr unsigned char DEFAULT_XAATRIB = 0xFF; 36 | static constexpr unsigned short DEFAULT_XAPERM = 0x555; // rx 37 | static constexpr unsigned short DEFAULT_OWNER_ID = 0; 38 | 39 | public: 40 | signed char GMTOffs = DEFAULT_GMTOFFS; 41 | unsigned char XAAttrib = DEFAULT_XAATRIB; 42 | unsigned short XAPerm = DEFAULT_XAPERM; 43 | unsigned short GID = DEFAULT_OWNER_ID; 44 | unsigned short UID = DEFAULT_OWNER_ID; 45 | }; 46 | 47 | // Helper functions for datestamp manipulation 48 | cd::ISO_DATESTAMP GetDateFromString(const char* str, bool* success = nullptr); 49 | 50 | cd::ISO_LONG_DATESTAMP GetLongDateFromDate(const cd::ISO_DATESTAMP& src); 51 | cd::ISO_LONG_DATESTAMP GetUnspecifiedLongDate(); 52 | std::string LongDateToString(const cd::ISO_LONG_DATESTAMP& src); 53 | 54 | uint32_t GetSizeInSectors(uint64_t size, uint32_t sectorSize = 2048); 55 | 56 | std::string SectorsToTimecode(const unsigned sectors); 57 | 58 | // Endianness swap 59 | unsigned short SwapBytes16(unsigned short val); 60 | unsigned int SwapBytes32(unsigned int val); 61 | 62 | // Scoped helpers for a few resources 63 | struct file_deleter 64 | { 65 | void operator()(FILE* file) const 66 | { 67 | if (file != nullptr) 68 | { 69 | std::fclose(file); 70 | } 71 | } 72 | }; 73 | using unique_file = std::unique_ptr; 74 | unique_file OpenScopedFile(const fs::path& path, const char* mode); 75 | 76 | bool CompareICase(std::string_view strLeft, std::string_view strRight); 77 | 78 | // Argument parsing 79 | bool ParseArgument(char** argv, std::string_view command, std::string_view longCommand = std::string_view{}); 80 | std::optional ParsePathArgument(char**& argv, std::string_view command, std::string_view longCommand = std::string_view{}); 81 | std::optional ParseStringArgument(char**& argv, std::string_view command, std::string_view longCommand = std::string_view{}); 82 | -------------------------------------------------------------------------------- /src/mkpsxiso/cdwriter.h: -------------------------------------------------------------------------------- 1 | #ifndef _CDWRITER_H 2 | #define _CDWRITER_H 3 | 4 | #include "cd.h" 5 | #include "mmappedfile.h" 6 | #include "fs.h" 7 | #include 8 | #include 9 | #include 10 | 11 | namespace cd { 12 | 13 | class IsoWriter 14 | { 15 | public: 16 | enum class EdcEccForm 17 | { 18 | None = 0, 19 | Form1, 20 | Form2, 21 | Autodetect, 22 | }; 23 | 24 | enum { 25 | SubData = 0x00080000, 26 | SubSTR = 0x00480100, 27 | SubEOL = 0x00090000, 28 | SubEOF = 0x00890000, 29 | }; 30 | 31 | class SectorView 32 | { 33 | public: 34 | SectorView(ThreadPool* threadPool, MMappedFile* mappedFile, unsigned int offsetLBA, unsigned int sizeLBA, EdcEccForm edcEccForm); 35 | virtual ~SectorView(); 36 | 37 | virtual void WriteFile(FILE* file) = 0; 38 | virtual void WriteMemory(const void* memory, size_t size) = 0; 39 | virtual void WriteBlankSectors(unsigned int count) = 0; 40 | virtual size_t GetSpaceInCurrentSector() const = 0; 41 | virtual void NextSector() = 0; 42 | virtual void SetSubheader(unsigned int subHead) = 0; 43 | 44 | void WaitForChecksumJobs(); 45 | 46 | protected: 47 | void PrepareSectorHeader() const; 48 | 49 | void CalculateForm1(); 50 | void CalculateForm2(); 51 | 52 | protected: 53 | void* m_currentSector = nullptr; 54 | size_t m_offsetInSector = 0; 55 | unsigned int m_currentLBA = 0; 56 | 57 | const unsigned int m_endLBA = 0; 58 | const EdcEccForm m_edcEccForm = EdcEccForm::None; 59 | 60 | private: 61 | std::forward_list> m_checksumJobs; 62 | ThreadPool* m_threadPool; 63 | MMappedFile::View m_view; 64 | }; 65 | 66 | class RawSectorView 67 | { 68 | public: 69 | RawSectorView(MMappedFile* mappedFile, unsigned int offsetLBA, unsigned int sizeLBA); 70 | 71 | void* GetRawBuffer() const; 72 | void WriteBlankSectors(); 73 | 74 | private: 75 | MMappedFile::View m_view; 76 | unsigned int m_endLBA; 77 | }; 78 | 79 | IsoWriter() = default; 80 | 81 | bool Create(const fs::path& fileName, unsigned int sizeLBA); 82 | void Close(); 83 | 84 | std::unique_ptr GetSectorViewM2F1(unsigned int offsetLBA, unsigned int sizeLBA, EdcEccForm edcEccForm) const; 85 | std::unique_ptr GetSectorViewM2F2(unsigned int offsetLBA, unsigned int sizeLBA, EdcEccForm edcEccForm) const; 86 | std::unique_ptr GetRawSectorView(unsigned int offsetLBA, unsigned int sizeLBA) const; 87 | 88 | private: 89 | std::unique_ptr m_mmap; 90 | std::unique_ptr m_threadPool; 91 | }; 92 | 93 | ISO_USHORT_PAIR SetPair16(unsigned short val); 94 | ISO_UINT_PAIR SetPair32(unsigned int val); 95 | 96 | }; 97 | 98 | #endif // _CDWRITER_H 99 | -------------------------------------------------------------------------------- /src/shared/mmappedfile.cpp: -------------------------------------------------------------------------------- 1 | #include "mmappedfile.h" 2 | 3 | #ifdef _WIN32 4 | #include 5 | #else 6 | #include 7 | #include 8 | #include 9 | #include 10 | #endif 11 | 12 | MMappedFile::MMappedFile() 13 | { 14 | } 15 | 16 | MMappedFile::~MMappedFile() 17 | { 18 | #ifdef _WIN32 19 | if (m_handle != nullptr) 20 | { 21 | CloseHandle(reinterpret_cast(m_handle)); 22 | } 23 | #else 24 | if (m_handle != nullptr) 25 | { 26 | close(static_cast(reinterpret_cast(m_handle))); 27 | } 28 | #endif 29 | } 30 | 31 | bool MMappedFile::Create(const fs::path& filePath, uint64_t size) 32 | { 33 | bool result = false; 34 | 35 | #ifdef _WIN32 36 | HANDLE file = CreateFileW(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); 37 | if (file != INVALID_HANDLE_VALUE) 38 | { 39 | ULARGE_INTEGER ulSize; 40 | ulSize.QuadPart = size; 41 | 42 | HANDLE fileMapping = CreateFileMappingW(file, nullptr, PAGE_READWRITE, ulSize.HighPart, ulSize.LowPart, nullptr); 43 | if (fileMapping != nullptr) 44 | { 45 | m_handle = fileMapping; 46 | result = true; 47 | } 48 | 49 | CloseHandle(file); 50 | } 51 | #else 52 | int file = open(filePath.c_str(), O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH); 53 | if (file != -1) 54 | { 55 | if (ftruncate(file, size) == 0) 56 | { 57 | m_handle = reinterpret_cast(file); 58 | result = true; 59 | } 60 | else 61 | { 62 | close(file); 63 | } 64 | } 65 | #endif 66 | return result; 67 | } 68 | 69 | MMappedFile::View MMappedFile::GetView(uint64_t offset, size_t size) const 70 | { 71 | return View(m_handle, offset, size); 72 | } 73 | 74 | MMappedFile::View::View(void* handle, uint64_t offset, size_t size) 75 | { 76 | #ifdef _WIN32 77 | SYSTEM_INFO SysInfo; 78 | GetSystemInfo(&SysInfo); 79 | const DWORD allocGranularity = SysInfo.dwAllocationGranularity; 80 | #else 81 | const long allocGranularity = sysconf(_SC_PAGE_SIZE); 82 | #endif 83 | 84 | const uint64_t mapStartOffset = (offset / allocGranularity) * allocGranularity; 85 | const uint64_t viewDelta = offset - mapStartOffset; 86 | size += viewDelta; 87 | 88 | #ifdef _WIN32 89 | ULARGE_INTEGER ulOffset; 90 | ulOffset.QuadPart = mapStartOffset; 91 | void* mapping = MapViewOfFile(reinterpret_cast(handle), FILE_MAP_ALL_ACCESS, ulOffset.HighPart, ulOffset.LowPart, size); 92 | if (mapping != nullptr) 93 | #else 94 | void* mapping = mmap(nullptr, size, PROT_READ|PROT_WRITE, MAP_SHARED, static_cast(reinterpret_cast(handle)), mapStartOffset); 95 | if (mapping != MAP_FAILED) 96 | #endif 97 | { 98 | m_mapping = mapping; 99 | m_data = static_cast(m_mapping) + viewDelta; 100 | m_size = size; 101 | } 102 | } 103 | 104 | MMappedFile::View::~View() 105 | { 106 | #ifdef _WIN32 107 | if (m_mapping != nullptr) 108 | { 109 | UnmapViewOfFile(m_mapping); 110 | } 111 | #else 112 | if (m_mapping != nullptr) 113 | { 114 | munmap(m_mapping, m_size); 115 | } 116 | #endif 117 | } -------------------------------------------------------------------------------- /src/dumpsxiso/cdreader.h: -------------------------------------------------------------------------------- 1 | #ifndef _CDREADER_H 2 | #define _CDREADER_H 3 | 4 | #include "cd.h" 5 | #include "xa.h" 6 | #include "listview.h" 7 | #include "fs.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace cd { 14 | 15 | // ISO reader class which allows you to read data from an ISO image whilst filtering out CD encoding 16 | // data such as Sync, address and mode codes as well as the EDC/ECC data. 17 | class IsoReader { 18 | 19 | // File pointer to opened ISO file 20 | FILE* filePtr = nullptr; 21 | // Sector buffer size 22 | unsigned char sectorBuff[CD_SECTOR_SIZE] {}; 23 | // Mode 2 Form 1 sector struct for simplified reading of sectors (usually points to sectorBuff[]) 24 | SECTOR_M2F1* sectorM2F1 = nullptr; 25 | // Mode 2 Form 2 sector struct for simplified reading of sectors (usually points to sectorBuff[]) 26 | SECTOR_M2F2* sectorM2F2 = nullptr; 27 | // Current sector number 28 | int currentSector = 0; 29 | // Current data offset in current sector 30 | size_t currentByte = 0; 31 | // Total number of sectors in the iso 32 | int totalSectors = 0; 33 | public: 34 | 35 | // Initializer 36 | IsoReader(); 37 | // De-initializer 38 | ~IsoReader(); 39 | 40 | // Open ISO image 41 | bool Open(const fs::path& fileName); 42 | 43 | // Read data sectors in bytes (supports sequential reading) 44 | size_t ReadBytes(void* ptr, size_t bytes, bool singleSector = false); 45 | 46 | size_t ReadBytesXA(void* ptr, size_t bytes, bool singleSector = false); 47 | 48 | size_t ReadBytesDA(void* ptr, size_t bytes, bool singleSector = false); 49 | 50 | // Skip bytes in data sectors (supports sequential skipping) 51 | void SkipBytes(size_t bytes, bool singleSector = false); 52 | 53 | // Seek to a sector in the ISO image in sector units 54 | int SeekToSector(int sector); 55 | 56 | // Seek to a data offset in the ISO image in byte units 57 | size_t SeekToByte(size_t offs); 58 | 59 | // Get current offset in byte units 60 | size_t GetPos() const; 61 | 62 | // Close ISO file 63 | void Close(); 64 | 65 | private: 66 | bool PrepareNextSector(); 67 | 68 | }; 69 | 70 | class IsoPathTable 71 | { 72 | public: 73 | struct Entry 74 | { 75 | ISO_PATHTABLE_ENTRY entry; 76 | std::string name; 77 | }; 78 | 79 | std::vector pathTableList; 80 | 81 | void FreePathTable(); 82 | size_t ReadPathTable(cd::IsoReader* reader, int lba); 83 | 84 | fs::path GetFullDirPath(int dirEntry) const; 85 | }; 86 | 87 | 88 | class IsoDirEntries 89 | { 90 | public: 91 | struct Entry 92 | { 93 | ISO_DIR_ENTRY entry; 94 | cdxa::ISO_XA_ATTRIB extData; 95 | std::string identifier; 96 | fs::path virtualPath; 97 | 98 | std::unique_ptr subdir; 99 | }; 100 | ListView dirEntryList; 101 | 102 | IsoDirEntries(ListView view); 103 | void ReadDirEntries(cd::IsoReader* reader, int lba, int sectors); 104 | void ReadRootDir(cd::IsoReader* reader, int lba); 105 | 106 | private: 107 | std::optional ReadEntry(cd::IsoReader* reader) const; 108 | }; 109 | 110 | } 111 | 112 | #endif // _CDREADER_H 113 | -------------------------------------------------------------------------------- /src/mkpsxiso/miniaudio_helpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef __cplusplus 4 | #include "miniaudio.h" 5 | #include "miniaudio_pcm.h" 6 | 7 | ma_result ma_decoder_init_path(const fs::path& pFilePath, const ma_decoder_config* pConfig, ma_decoder* pDecoder); 8 | 9 | ma_result ma_redbook_decoder_init_path_by_ext(const fs::path& filePath, ma_decoder* pDecoder, VirtualWavEx *vw, bool& isLossy); 10 | 11 | #if defined(MINIAUDIO_IMPLEMENTATION) || defined(MA_IMPLEMENTATION) 12 | 13 | // Helper wrapper to simplify dealing with paths on Windows 14 | ma_result ma_decoder_init_path(const fs::path& pFilePath, const ma_decoder_config* pConfig, ma_decoder* pDecoder) 15 | { 16 | #ifdef _WIN32 17 | return ma_decoder_init_file_w(pFilePath.c_str(), pConfig, pDecoder); 18 | #else 19 | return ma_decoder_init_file(pFilePath.c_str(), pConfig, pDecoder); 20 | #endif 21 | } 22 | 23 | typedef enum { 24 | DAF_WAV, 25 | DAF_FLAC, 26 | DAF_MP3, 27 | DAF_PCM 28 | } DecoderAudioFormats; 29 | 30 | // Helper wrapper to open as redbook (44100kHz stereo s16le) audio and use the file extension to determine the order to try decoders 31 | ma_result ma_redbook_decoder_init_path_by_ext(const fs::path& filePath, ma_decoder* pDecoder, VirtualWavEx *vw, bool& isLossy) 32 | { 33 | ma_decoder_config decoderConfig = ma_decoder_config_init(ma_format_s16, 2, 44100); 34 | isLossy = false; 35 | 36 | DecoderAudioFormats tryorder[4] = {DAF_WAV, DAF_FLAC, DAF_MP3, DAF_PCM}; 37 | const auto& extension = filePath.extension().u8string(); 38 | 39 | // determine which format to try based on magic numbers 40 | bool magicvalid = false; 41 | char magic[12]; 42 | { 43 | auto file = OpenScopedFile(filePath, "rb"); 44 | if(file) 45 | { 46 | magicvalid = (fread(magic, 12, 1, file.get()) == 1); 47 | } 48 | } 49 | if(magicvalid && (memcmp(magic, "RIFF", 4) == 0) && (memcmp(&magic[8], "WAVE", 4) == 0)) 50 | { 51 | // it's wave, default try order is good 52 | } 53 | else if(magicvalid && (memcmp(magic, "fLaC", 4) == 0)) 54 | { 55 | tryorder[0] = DAF_FLAC; 56 | tryorder[1] = DAF_WAV; 57 | } 58 | //fallback - determine which format to try based on file extension 59 | else if(extension.size() >= 4) 60 | { 61 | //nothing to change if wav 62 | if(CompareICase(extension.c_str(), ".flac")) 63 | { 64 | tryorder[0] = DAF_FLAC; 65 | tryorder[1] = DAF_WAV; 66 | } 67 | else if(CompareICase(extension.c_str(), ".mp3")) 68 | { 69 | tryorder[0] = DAF_MP3; 70 | tryorder[1] = DAF_WAV; 71 | tryorder[2] = DAF_FLAC; 72 | } 73 | else if(CompareICase(extension.c_str(), ".pcm") || CompareICase(extension.c_str(), ".raw")) 74 | { 75 | tryorder[0] = DAF_PCM; 76 | tryorder[1] = DAF_WAV; 77 | tryorder[2] = DAF_FLAC; 78 | tryorder[3] = DAF_MP3; 79 | } 80 | } 81 | 82 | const int num_tries = std::size(tryorder); 83 | int i; 84 | for(i = 0; i < num_tries; i++) 85 | { 86 | if(tryorder[i] == DAF_WAV) 87 | { 88 | decoderConfig.encodingFormat = ma_encoding_format_wav; 89 | if(MA_SUCCESS == ma_decoder_init_path(filePath, &decoderConfig, pDecoder)) break; 90 | } 91 | else if(tryorder[i] == DAF_FLAC) 92 | { 93 | decoderConfig.encodingFormat = ma_encoding_format_flac; 94 | if(MA_SUCCESS == ma_decoder_init_path(filePath, &decoderConfig, pDecoder)) break; 95 | } 96 | else if(tryorder[i] == DAF_MP3) 97 | { 98 | decoderConfig.encodingFormat = ma_encoding_format_mp3; 99 | if(MA_SUCCESS == ma_decoder_init_path(filePath, &decoderConfig, pDecoder)) 100 | { 101 | isLossy = true; 102 | break; 103 | } 104 | } 105 | else if(tryorder[i] == DAF_PCM) 106 | { 107 | printf("\n WARN: Guessing it's just signed 16 bit stereo @ 44100 kHz pcm audio\n"); 108 | if(MA_SUCCESS == ma_decoder_init_path_pcm(filePath, &decoderConfig, pDecoder, vw)) break; 109 | } 110 | } 111 | if(i == num_tries) 112 | { 113 | // no more formats to try, return false 114 | printf(" ERROR: No valid format found\n"); 115 | return !MA_SUCCESS; 116 | } 117 | return MA_SUCCESS; 118 | } 119 | 120 | #endif 121 | 122 | #endif -------------------------------------------------------------------------------- /src/shared/platform.cpp: -------------------------------------------------------------------------------- 1 | #include "platform.h" 2 | #include "cd.h" 3 | 4 | #ifdef _WIN32 5 | #ifndef NOMINMAX 6 | #define NOMINMAX 7 | #endif 8 | #include 9 | #include 10 | #endif 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #ifdef _WIN32 18 | static std::wstring UTF8ToUTF16(std::string_view str) 19 | { 20 | std::wstring result; 21 | const int count = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), nullptr, 0); 22 | if (count != 0) 23 | { 24 | result.resize(count); 25 | MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), result.data(), count); 26 | } 27 | return result; 28 | } 29 | 30 | static std::string UTF16ToUTF8(std::wstring_view str) 31 | { 32 | std::string result; 33 | int count = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), nullptr, 0, nullptr, nullptr); 34 | if (count != 0) 35 | { 36 | result.resize(count); 37 | WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), result.data(), count, nullptr, nullptr); 38 | } 39 | return result; 40 | } 41 | 42 | static FILETIME TimetToFileTime(time_t t) 43 | { 44 | FILETIME ft; 45 | LARGE_INTEGER ll; 46 | ll.QuadPart = t * 10000000ll + 116444736000000000ll; 47 | ft.dwLowDateTime = ll.LowPart; 48 | ft.dwHighDateTime = ll.HighPart; 49 | return ft; 50 | } 51 | 52 | time_t timegm(struct tm* tm) 53 | { 54 | return _mkgmtime(tm); 55 | } 56 | #endif 57 | 58 | FILE* OpenFile(const fs::path& path, const char* mode) 59 | { 60 | #ifdef _WIN32 61 | FILE* file = nullptr; 62 | _wfopen_s(&file, path.c_str(), UTF8ToUTF16(mode).c_str()); 63 | return file; 64 | #else 65 | return ::fopen(path.c_str(), mode); 66 | #endif 67 | } 68 | 69 | std::optional Stat(const fs::path& path) 70 | { 71 | struct stat64 fileAttrib; 72 | #ifdef _WIN32 73 | if (_wstat64(path.c_str(), &fileAttrib) != 0) 74 | #else 75 | if (stat64(path.c_str(), &fileAttrib) != 0) 76 | #endif 77 | { 78 | return std::nullopt; 79 | } 80 | 81 | return fileAttrib; 82 | } 83 | 84 | int64_t GetSize(const fs::path& path) 85 | { 86 | auto fileAttrib = Stat(path); 87 | return fileAttrib.has_value() ? fileAttrib->st_size : -1; 88 | } 89 | 90 | 91 | void UpdateTimestamps(const fs::path& path, const cd::ISO_DATESTAMP& entryDate) 92 | { 93 | tm timeBuf {}; 94 | timeBuf.tm_year = entryDate.year; 95 | timeBuf.tm_mon = entryDate.month - 1; 96 | timeBuf.tm_mday = entryDate.day; 97 | timeBuf.tm_hour = entryDate.hour; 98 | timeBuf.tm_min = entryDate.minute - (15 * entryDate.GMToffs); 99 | timeBuf.tm_sec = entryDate.second; 100 | const time_t time = timegm(&timeBuf); 101 | 102 | // utime can't update timestamps of directories on Windows, so a platform-specific approach is needed 103 | #ifdef _WIN32 104 | HANDLE file = CreateFileW(path.c_str(), FILE_WRITE_ATTRIBUTES, 0, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); 105 | if (file != INVALID_HANDLE_VALUE) 106 | { 107 | const FILETIME ft = TimetToFileTime(time); 108 | if(0 == SetFileTime(file, &ft, nullptr, &ft)) 109 | { 110 | printf("ERROR: unable to update timestamps for %ls\n", path.c_str()); 111 | } 112 | 113 | CloseHandle(file); 114 | } 115 | #else 116 | struct timespec times[2]; 117 | times[0].tv_nsec = UTIME_OMIT; 118 | 119 | times[1].tv_sec = time; 120 | times[1].tv_nsec = 0; 121 | if(0 != utimensat(AT_FDCWD, path.c_str(), times, 0)) 122 | { 123 | printf("ERROR: unable to update timestamps for %s\n", path.c_str()); 124 | } 125 | #endif 126 | } 127 | 128 | extern int Main(int argc, char* argv[]); 129 | 130 | #ifdef _WIN32 131 | int wmain(int argc, wchar_t* argv[]) 132 | { 133 | std::vector u8Arguments; 134 | u8Arguments.reserve(argc); 135 | for (int i = 0; i < argc; ++i) 136 | { 137 | u8Arguments.emplace_back(UTF16ToUTF8(argv[i])); 138 | } 139 | 140 | std::vector u8argv; 141 | u8Arguments.reserve(argc + 1); 142 | for (std::string& str : u8Arguments) 143 | { 144 | u8argv.emplace_back(str.data()); 145 | } 146 | u8argv.emplace_back(nullptr); 147 | 148 | return Main(argc, u8argv.data()); 149 | } 150 | #else 151 | int main(int argc, char* argv[]) 152 | { 153 | return Main(argc, argv); 154 | } 155 | #endif 156 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # mkpsxiso build script (rewritten) 2 | # (C) 2021 spicyjpeg 3 | 4 | cmake_minimum_required(VERSION 3.20) 5 | 6 | project( 7 | mkpsxiso 8 | LANGUAGES C CXX 9 | VERSION 2.03 10 | DESCRIPTION "PlayStation ISO image maker & dumping tool" 11 | HOMEPAGE_URL "https://github.com/Lameguy64/mkpsxiso" 12 | ) 13 | 14 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 15 | set(CMAKE_C_STANDARD 11) 16 | set(CMAKE_CXX_STANDARD 17) 17 | 18 | # Useful paths 19 | set(mkpsxiso_dir "src/mkpsxiso") 20 | set(dumpsxiso_dir "src/dumpsxiso") 21 | set(shared_dir "src/shared") 22 | 23 | ## External dependencies 24 | 25 | if(NOT EXISTS ${PROJECT_SOURCE_DIR}/tinyxml2/tinyxml2.cpp) 26 | message(FATAL_ERROR "The tinyxml2 directory is empty. Run 'git submodule update --init --recursive' to populate it.") 27 | endif() 28 | 29 | if(NOT EXISTS ${PROJECT_SOURCE_DIR}/miniaudio/miniaudio.h) 30 | message(FATAL_ERROR "The miniaudio directory is empty. Run 'git submodule update --init --recursive' to populate it.") 31 | endif() 32 | 33 | if(NOT EXISTS ${PROJECT_SOURCE_DIR}/ghc/include/ghc/filesystem.hpp) 34 | message(FATAL_ERROR "The ghc directory is empty. Run 'git submodule update --init --recursive' to populate it.") 35 | endif() 36 | 37 | if(NOT EXISTS ${PROJECT_SOURCE_DIR}/ThreadPool/ThreadPool.h) 38 | message(FATAL_ERROR "The ThreadPool directory is empty. Run 'git submodule update --init --recursive' to populate it.") 39 | endif() 40 | 41 | # Build tinyxml2. I didn't bother with tinyxml2's actual CMake integration 42 | # because it's far easier do do this. It is a single-file library after all. 43 | add_library (tinyxml2 STATIC tinyxml2/tinyxml2.cpp) 44 | target_include_directories(tinyxml2 PUBLIC tinyxml2) 45 | 46 | # Build libFLAC, unless explicitly requested not too 47 | if(NOT MKPSXISO_NO_LIBFLAC) 48 | if(NOT EXISTS ${PROJECT_SOURCE_DIR}/flac/include/FLAC/stream_encoder.h) 49 | message(FATAL_ERROR "The flac directory is empty. Run 'git submodule update --init --recursive' to populate it.") 50 | endif() 51 | 52 | set(BUILD_CXXLIBS OFF CACHE INTERNAL "") 53 | set(BUILD_PROGRAMS OFF CACHE INTERNAL "") 54 | set(BUILD_EXAMPLES OFF CACHE INTERNAL "") 55 | set(BUILD_DOCS OFF CACHE INTERNAL "") 56 | set(BUILD_TESTING OFF CACHE INTERNAL "") 57 | set(INSTALL_MANPAGES OFF CACHE INTERNAL "") 58 | set(INSTALL_PKGCONFIG_MODULES OFF CACHE INTERNAL "") 59 | set(INSTALL_CMAKE_CONFIG_MODULE OFF CACHE INTERNAL "") 60 | set(WITH_OGG OFF CACHE INTERNAL "") 61 | if(MINGW) 62 | set(WITH_STACK_PROTECTOR, OFF CACHE INTERNAL "") 63 | endif() 64 | add_subdirectory(flac EXCLUDE_FROM_ALL) 65 | endif() 66 | 67 | # Add ghc to support filesystem on MacOS 68 | add_subdirectory(ghc EXCLUDE_FROM_ALL) 69 | 70 | ## Internal dependencies 71 | 72 | # Populate shared files 73 | add_library(iso_shared OBJECT 74 | ${shared_dir}/common.cpp 75 | ${shared_dir}/fs.cpp 76 | ${shared_dir}/mmappedfile.cpp 77 | ${shared_dir}/platform.cpp 78 | ) 79 | target_include_directories(iso_shared PUBLIC ${shared_dir}) 80 | target_compile_definitions(iso_shared PUBLIC VERSION="${PROJECT_VERSION}") 81 | target_link_libraries(iso_shared ghc_filesystem) 82 | 83 | find_package(Threads REQUIRED) 84 | 85 | ## Executables 86 | 87 | add_executable(mkpsxiso 88 | ${mkpsxiso_dir}/cdwriter.cpp 89 | ${mkpsxiso_dir}/edcecc.cpp 90 | ${mkpsxiso_dir}/iso.cpp 91 | ${mkpsxiso_dir}/main.cpp 92 | ) 93 | target_include_directories(mkpsxiso PUBLIC "miniaudio" "ThreadPool") 94 | target_link_libraries(mkpsxiso tinyxml2 iso_shared Threads::Threads) 95 | if(MINGW) 96 | target_link_libraries(mkpsxiso "-municode") 97 | endif() 98 | 99 | add_executable(dumpsxiso 100 | ${dumpsxiso_dir}/cdreader.cpp 101 | ${dumpsxiso_dir}/main.cpp 102 | ) 103 | target_link_libraries(dumpsxiso tinyxml2 iso_shared) 104 | if(NOT MKPSXISO_NO_LIBFLAC) 105 | target_link_libraries(dumpsxiso FLAC) 106 | else() 107 | target_compile_definitions(dumpsxiso PUBLIC MKPSXISO_NO_LIBFLAC) 108 | endif() 109 | if(MINGW) 110 | target_link_libraries(dumpsxiso "-municode") 111 | endif() 112 | 113 | ## Installation 114 | 115 | include(GNUInstallDirs) 116 | install(TARGETS mkpsxiso dumpsxiso) 117 | install(FILES ${PROJECT_SOURCE_DIR}/README.md ${PROJECT_SOURCE_DIR}/LICENSE.md DESTINATION ${CMAKE_INSTALL_DATADIR}/psn00bsdk/doc/mkpsxiso) 118 | 119 | ## CPack (used by GitHub Actions to package releases) 120 | 121 | if(NOT DEFINED CPACK_GENERATOR) 122 | if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 123 | set(CPACK_GENERATOR ZIP DEB RPM) 124 | else() 125 | set(CPACK_GENERATOR ZIP) 126 | endif() 127 | endif() 128 | 129 | set(CPACK_VERBATIM_VARIABLES ON) 130 | set(CPACK_ARCHIVE_THREADS 0) 131 | set(CPACK_PACKAGE_DIRECTORY ${PROJECT_BINARY_DIR}/packages) 132 | set(CPACK_PACKAGE_NAME mkpsxiso) 133 | set(CPACK_PACKAGE_VENDOR Lameguy64) 134 | set(CPACK_PACKAGE_CONTACT Lameguy64) 135 | set(CPACK_RESOURCE_FILE_README ${PROJECT_SOURCE_DIR}/README.md) 136 | set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE.md) 137 | #set(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/icon.ico) 138 | set(CPACK_PACKAGE_INSTALL_DIRECTORY mkpsxiso) 139 | 140 | set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.28)") 141 | set(CPACK_DEBIAN_PACKAGE_SECTION devel) 142 | #set(CPACK_RPM_PACKAGE_RELOCATABLE ON) 143 | 144 | include(CPack) 145 | -------------------------------------------------------------------------------- /src/shared/common.cpp: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | #include "platform.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace cd; 9 | 10 | static void snprintfZeroPad(char* s, size_t n, const char* format, ...) 11 | { 12 | // We need a temporary buffer that is 1 byte bigger than the specified one, 13 | // then memcpy without the null terminator/pad with zeroes 14 | auto buf = std::make_unique(n + 1); 15 | 16 | va_list args; 17 | va_start(args, format); 18 | 19 | const int bytesWritten = vsnprintf(buf.get(), n + 1, format, args); 20 | memcpy(s, buf.get(), bytesWritten); 21 | std::fill(s + bytesWritten, s + n, '\0'); 22 | 23 | va_end(args); 24 | } 25 | 26 | ISO_LONG_DATESTAMP GetLongDateFromDate(const ISO_DATESTAMP& src) 27 | { 28 | ISO_LONG_DATESTAMP result; 29 | 30 | snprintfZeroPad(result.year, std::size(result.year), "%04d", src.year != 0 ? 1900 + src.year : 0); 31 | snprintfZeroPad(result.month, std::size(result.month), "%02d", src.month); 32 | snprintfZeroPad(result.day, std::size(result.day), "%02d", src.day); 33 | snprintfZeroPad(result.hour, std::size(result.hour), "%02d", src.hour); 34 | snprintfZeroPad(result.minute, std::size(result.minute), "%02d", src.minute); 35 | snprintfZeroPad(result.second, std::size(result.second), "%02d", src.second); 36 | strncpy(result.hsecond, "00", std::size(result.hsecond)); 37 | result.GMToffs = src.GMToffs; 38 | 39 | return result; 40 | } 41 | 42 | ISO_DATESTAMP GetDateFromString(const char* str, bool* success) 43 | { 44 | bool succeeded = false; 45 | 46 | ISO_DATESTAMP result {}; 47 | 48 | short int year; 49 | const int argsRead = sscanf( str, "%04hd%02hhu%02hhu%02hhu%02hhu%02hhu%*02u%hhd", 50 | &year, &result.month, &result.day, 51 | &result.hour, &result.minute, &result.second, &result.GMToffs ); 52 | if (argsRead >= 6) 53 | { 54 | result.year = year != 0 ? year - 1900 : 0; 55 | if (argsRead < 7) 56 | { 57 | // Consider GMToffs optional 58 | result.GMToffs = 0; 59 | } 60 | succeeded = true; 61 | } 62 | 63 | if (success != nullptr) 64 | { 65 | *success = succeeded; 66 | } 67 | return result; 68 | } 69 | 70 | ISO_LONG_DATESTAMP GetUnspecifiedLongDate() 71 | { 72 | ISO_LONG_DATESTAMP result; 73 | 74 | strncpy(result.year, "0000", std::size(result.year)); 75 | strncpy(result.month, "00", std::size(result.month)); 76 | strncpy(result.day, "00", std::size(result.day)); 77 | strncpy(result.hour, "00", std::size(result.hour)); 78 | strncpy(result.minute, "00", std::size(result.minute)); 79 | strncpy(result.second, "00", std::size(result.second)); 80 | strncpy(result.hsecond, "00", std::size(result.hsecond)); 81 | result.GMToffs = 0; 82 | 83 | return result; 84 | } 85 | 86 | std::string LongDateToString(const cd::ISO_LONG_DATESTAMP& src) 87 | { 88 | // Interpret ISO_LONG_DATESTAMP as 16 characters, manually write out GMT offset 89 | const char* srcStr = reinterpret_cast(&src); 90 | 91 | std::string result(srcStr, srcStr+16); 92 | 93 | char GMTbuf[8]; 94 | sprintf(GMTbuf, "%+hhd", src.GMToffs); 95 | result.append(GMTbuf); 96 | 97 | return result; 98 | } 99 | 100 | uint32_t GetSizeInSectors(uint64_t size, uint32_t sectorSize) 101 | { 102 | return static_cast((size + (sectorSize - 1)) / sectorSize); 103 | } 104 | 105 | std::string SectorsToTimecode(const unsigned sectors) 106 | { 107 | char timecode[16]; 108 | snprintf( timecode, sizeof(timecode), "%02u:%02u:%02u", (sectors/75)/60, (sectors/75)%60, sectors%75); 109 | return std::string(timecode); 110 | } 111 | 112 | unsigned short SwapBytes16(unsigned short val) 113 | { 114 | return ((val & 0xFF) << 8) | 115 | ((val & 0xFF00) >> 8); 116 | } 117 | 118 | unsigned int SwapBytes32(unsigned int val) 119 | { 120 | return ((val & 0xFF) << 24) | 121 | ((val & 0xFF00) << 8) | 122 | ((val & 0xFF0000) >> 8) | 123 | ((val & 0xFF000000) >> 24); 124 | } 125 | 126 | unique_file OpenScopedFile(const fs::path& path, const char* mode) 127 | { 128 | return unique_file { OpenFile(path, mode) }; 129 | } 130 | 131 | bool CompareICase(std::string_view strLeft, std::string_view strRight) 132 | { 133 | return std::equal(strLeft.begin(), strLeft.end(), strRight.begin(), strRight.end(), [](char left, char right) 134 | { 135 | return left == right || std::tolower(left) == std::tolower(right); 136 | }); 137 | } 138 | 139 | bool ParseArgument(char** argv, std::string_view command, std::string_view longCommand) 140 | { 141 | const std::string_view arg(*argv); 142 | // Try the long command first, case insensitively 143 | if (!longCommand.empty() && arg.length() > 2 && arg[0] == '-' && arg[1] == '-' && CompareICase(arg.substr(2), longCommand)) 144 | { 145 | return true; 146 | } 147 | 148 | // Short commands are case sensitive 149 | if (!command.empty() && arg.length() > 1 && arg[0] == '-' && arg.substr(1) == command) 150 | { 151 | return true; 152 | } 153 | return false; 154 | } 155 | 156 | std::optional ParsePathArgument(char**& argv, std::string_view command, std::string_view longCommand) 157 | { 158 | if (ParseArgument(argv, command, longCommand) && *(argv+1) != nullptr) 159 | { 160 | argv++; 161 | return fs::u8path(*argv); 162 | } 163 | return std::nullopt; 164 | } 165 | 166 | std::optional ParseStringArgument(char**& argv, std::string_view command, std::string_view longCommand) 167 | { 168 | if (ParseArgument(argv, command, longCommand) && *(argv+1) != nullptr) 169 | { 170 | argv++; 171 | return std::string(*argv); 172 | } 173 | return std::nullopt; 174 | } 175 | -------------------------------------------------------------------------------- /src/mkpsxiso/iso.h: -------------------------------------------------------------------------------- 1 | #ifndef _ISO_H 2 | #define _ISO_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "fs.h" 13 | #include "cdwriter.h" 14 | #include "common.h" 15 | 16 | namespace iso 17 | { 18 | typedef struct 19 | { 20 | const char* SystemID; 21 | const char* VolumeID; 22 | const char* VolumeSet; 23 | const char* Publisher; 24 | const char* DataPreparer; 25 | const char* Application; 26 | const char* Copyright; 27 | const char* CreationDate; 28 | const char* ModificationDate; 29 | } IDENTIFIERS; 30 | 31 | struct DIRENTRY 32 | { 33 | std::string id; /// Entry identifier (empty if invisible dummy) 34 | int64_t length; /// Length of file in bytes 35 | int lba; /// File LBA (in sectors) 36 | 37 | fs::path srcfile; /// Filename with path to source file (empty if directory or dummy) 38 | EntryType type; /// File type (0 - file, 1 - directory) 39 | unsigned char attribs; /// XA attributes, 0xFF is not set 40 | unsigned short perms; /// XA permissions 41 | unsigned short GID; /// Owner group ID 42 | unsigned short UID; /// Owner user ID 43 | std::unique_ptr subdir; 44 | 45 | cd::ISO_DATESTAMP date; 46 | std::string trackid; /// only used for DA files 47 | 48 | }; 49 | 50 | // EntryList must have stable references! 51 | using EntryList = std::list; 52 | 53 | class PathEntryClass { 54 | public: 55 | std::string dir_id; 56 | unsigned short dir_index = 0; 57 | unsigned short dir_parent_index = 0; 58 | int dir_lba = 0; 59 | 60 | std::unique_ptr sub; 61 | }; 62 | 63 | class PathTableClass { 64 | public: 65 | unsigned char* GenTableData(unsigned char* buff, bool msb); 66 | 67 | std::vector entries; 68 | }; 69 | 70 | class DirTreeClass 71 | { 72 | private: 73 | // TODO: Once DirTreeClass stores a reference to its own entry, this will be pointless 74 | // Same for all 'dir' arguments to methods of this class 75 | std::string name; 76 | 77 | DirTreeClass* parent = nullptr; // Non-owning 78 | 79 | /// Internal function for generating and writing directory records 80 | bool WriteDirEntries(cd::IsoWriter* writer, const DIRENTRY& dir, const DIRENTRY& parentDir) const; 81 | 82 | /// Internal function for recursive path table generation 83 | std::unique_ptr GenPathTableSub(unsigned short& index, unsigned short parentIndex) const; 84 | 85 | public: 86 | static int GetAudioSize(const fs::path& audioFile); 87 | EntryList& entries; // List of all entries on the disc 88 | std::vector> entriesInDir; // References to entries in this directory 89 | 90 | DirTreeClass(EntryList& entries, DirTreeClass* parent = nullptr); 91 | ~DirTreeClass(); 92 | 93 | static DIRENTRY& CreateRootDirectory(EntryList& entries, const cd::ISO_DATESTAMP& volumeDate); 94 | 95 | void PrintRecordPath(); 96 | 97 | void OutputHeaderListing(FILE* fp, int level) const; 98 | 99 | /** Calculates the length of the directory record to be produced by this class in bytes. 100 | * 101 | * Returns: Length of directory record in bytes. 102 | */ 103 | int CalculateDirEntryLen() const; 104 | 105 | /** Calculates the LBA of all file and directory entries in the directory record and returns the next LBA 106 | * address. 107 | * 108 | * lba - LBA address where the first directory record begins. 109 | */ 110 | int CalculateTreeLBA(int lba); 111 | 112 | /** Adds a file entry to the directory record. 113 | * 114 | * *id - The name of the file entry. It will be converted to uppercase and adds the file version 115 | * identifier (;1) automatically. 116 | * type - The type of file to add, EntryFile is for standard files, EntryXA is for XA streams and 117 | * EntryStr is for STR streams. To add directories, use AddDirEntry(). 118 | * *srcfile - Path and filename to the source file. 119 | * attributes - GMT offset/XA permissions for the file, if applicable. 120 | */ 121 | bool AddFileEntry(const char* id, EntryType type, const fs::path& srcfile, const EntryAttributes& attributes, const char *trackid = nullptr); 122 | 123 | /** Adds an invisible dummy file entry to the directory record. Its invisible because its file entry 124 | * is not actually added to the directory record. 125 | * 126 | * sectors - The size of the dummy file in sector units (1 = 2048 bytes, 1024 = 2MB). 127 | * type - 0 for form1 (data) dummy, 1 for form2 (XA) dummy 128 | */ 129 | void AddDummyEntry(int sectors, int type); 130 | 131 | /** Generates a path table of all directories and subdirectories within this class' directory record. 132 | * 133 | * root - Directory entry of this path 134 | * *buff - Pointer to a 2048 byte buffer to generate the path table to. 135 | * msb - If true, generates a path table encoded in big-endian format, little-endian otherwise. 136 | * 137 | * Returns: Length of path table in bytes. 138 | */ 139 | int GeneratePathTable(const DIRENTRY& root, unsigned char* buff, bool msb) const; 140 | 141 | /** Adds a subdirectory to the directory record. 142 | * 143 | * *id - The name of the subdirectory to add. It will be converted to uppercase automatically. 144 | * attributes - GMT offset/XA permissions for the file, if applicable. 145 | * alreadyExists - set to true if a returned DirTreeClass already existed 146 | * 147 | * Returns: Pointer to another DirTreeClass for accessing the directory record of the subdirectory. 148 | */ 149 | DirTreeClass* AddSubDirEntry(const char* id, const fs::path& srcDir, const EntryAttributes& attributes, bool& alreadyExists); 150 | 151 | /** Writes the source files assigned to the directory entries to a CD image. Its recommended to execute 152 | * this first before writing the actual file system. 153 | * 154 | * *writer - Pointer to a cd::IsoWriter class that is ready for writing. 155 | */ 156 | bool WriteFiles(cd::IsoWriter* writer) const; 157 | 158 | /** Writes the file system of the directory records to a CD image. Execute this after the source files 159 | * have been written to the CD image. 160 | * 161 | * *writer - Pointer to a cd::IsoWriter class that is ready for writing. 162 | * LBA - Current directory LBA 163 | * parentLBA - Parent directory LBA 164 | * currentDirDate - Timestamp to use for . and .. directories. 165 | */ 166 | bool WriteDirectoryRecords(cd::IsoWriter* writer, const DIRENTRY& dir, const DIRENTRY& parentDir); 167 | 168 | void SortDirectoryEntries(); 169 | 170 | int CalculatePathTableLen(const DIRENTRY& dirEntry) const; 171 | 172 | int GetFileCountTotal() const; 173 | int GetDirCountTotal() const; 174 | 175 | void OutputLBAlisting(FILE* fp, int level) const; 176 | }; 177 | 178 | void WriteLicenseData(cd::IsoWriter* writer, void* data); 179 | 180 | void WriteDescriptor(cd::IsoWriter* writer, const IDENTIFIERS& id, const DIRENTRY& root, int imageLen); 181 | 182 | const int DA_FILE_PLACEHOLDER_LBA = 0xDEADBEEF; 183 | 184 | }; 185 | 186 | #endif 187 | -------------------------------------------------------------------------------- /src/mkpsxiso/miniaudio_pcm.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef struct { 4 | uint8_t header[44]; 5 | uint64_t pos; // actual file position 6 | uint64_t vpos; // virtual file position 7 | uint64_t vsize; // virtual file size 8 | FILE *file; 9 | } VirtualWav; 10 | 11 | MA_API ma_result ma_decoder_init_FILE_pcm(FILE *file, ma_decoder_config* pConfig, ma_decoder* pDecoder, VirtualWav *pUserData); 12 | 13 | #ifdef __cplusplus 14 | #include "platform.h" 15 | #include "common.h" 16 | 17 | class VirtualWavEx : public VirtualWav { 18 | public: 19 | unique_file pcmFp; 20 | }; 21 | 22 | MA_API ma_result ma_decoder_init_path_pcm(const fs::path& pFilePath, ma_decoder_config* pConfig, ma_decoder* pDecoder, VirtualWavEx *pUserData); 23 | #endif 24 | 25 | #if defined(MINIAUDIO_IMPLEMENTATION) || defined(MA_IMPLEMENTATION) 26 | 27 | static size_t virtual_wav_read(ma_decoder *pDecoder, void *pBufferOut, size_t bytesToRead) 28 | { 29 | VirtualWav *vw = (VirtualWav *)pDecoder->pUserData; 30 | size_t bytesRead = 0; 31 | if(vw->vpos < 44) 32 | { 33 | const size_t headerread = drwav_min(bytesToRead, 44-vw->vpos); 34 | memcpy(pBufferOut, &vw->header[vw->vpos], headerread); 35 | vw->vpos += headerread; 36 | bytesRead += headerread; 37 | bytesToRead -= headerread; 38 | pBufferOut = ((uint8_t*)pBufferOut) + headerread; 39 | } 40 | if(bytesToRead > 0) 41 | { 42 | const size_t actualread = fread(pBufferOut, 1, bytesToRead, vw->file); 43 | bytesRead += actualread; 44 | vw->vpos += actualread; 45 | vw->pos += actualread; 46 | } 47 | return bytesRead; 48 | } 49 | 50 | static ma_bool32 virtual_wav_seek(ma_decoder *pDecoder, ma_int64 byteOffset, ma_seek_origin origin) 51 | { 52 | int whence; 53 | int result; 54 | VirtualWav *vw = (VirtualWav *)pDecoder->pUserData; 55 | 56 | if (origin == ma_seek_origin_start) { 57 | if(byteOffset < 0) return MA_FALSE; 58 | if(byteOffset > vw->vsize) return MA_FALSE; 59 | vw->vpos = byteOffset; 60 | byteOffset = drwav_max(byteOffset - 44, 0); 61 | vw->pos = byteOffset; 62 | whence = SEEK_SET; 63 | } else if (origin == ma_seek_origin_end) { 64 | if(byteOffset > 0) return MA_FALSE; 65 | if((byteOffset + vw->vsize) < 0) return MA_FALSE; 66 | vw->vpos = vw->vsize + byteOffset; 67 | byteOffset = drwav_max(byteOffset, -(vw->vsize - 44)); 68 | vw->pos = (vw->vsize - 44) + byteOffset; 69 | whence = SEEK_END; 70 | } else { 71 | if((byteOffset+vw->vpos) > vw->vsize) return MA_FALSE; 72 | if((byteOffset+vw->vpos) < 0) return MA_FALSE; 73 | vw->vpos += byteOffset; 74 | uint64_t abspos = vw->pos + byteOffset; 75 | if(abspos < 0) 76 | { 77 | byteOffset = -vw->pos; 78 | } 79 | else if(abspos > (vw->vsize-44)) 80 | { 81 | byteOffset = (vw->vsize-44) - vw->pos; 82 | } 83 | vw->pos += byteOffset; 84 | whence = SEEK_CUR; 85 | } 86 | 87 | #if defined(_WIN32) 88 | #if defined(_MSC_VER) && _MSC_VER > 1200 89 | result = _fseeki64(vw->file, byteOffset, whence); 90 | #else 91 | /* No _fseeki64() so restrict to 31 bits. */ 92 | if (origin > 0x7FFFFFFF) { 93 | return MA_FALSE; 94 | } 95 | 96 | result = fseek(vw->file, (int)byteOffset, whence); 97 | #endif 98 | #else 99 | result = fseek(vw->file, (long int)byteOffset, whence); 100 | #endif 101 | if (result != 0) { 102 | return MA_FALSE; 103 | } 104 | 105 | return MA_TRUE; 106 | } 107 | 108 | #if !defined(_MSC_VER) && !((defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 1) || defined(_XOPEN_SOURCE) || defined(_POSIX_SOURCE)) && !defined(MA_BSD) 109 | int fileno(FILE *stream); 110 | #endif 111 | 112 | static ma_result stdio_file_size(FILE *file, uint64_t *pSizeInBytes) 113 | { 114 | int fd; 115 | struct stat info; 116 | 117 | MA_ASSERT(file != NULL); 118 | 119 | #if defined(_MSC_VER) 120 | fd = _fileno(file); 121 | #else 122 | fd = fileno(file); 123 | #endif 124 | 125 | if (fstat(fd, &info) != 0) { 126 | return ma_result_from_errno(errno); 127 | } 128 | 129 | *pSizeInBytes = info.st_size; 130 | 131 | return MA_SUCCESS; 132 | } 133 | 134 | MA_API ma_result ma_decoder_init_FILE_pcm(FILE *file, ma_decoder_config* pConfig, ma_decoder* pDecoder, VirtualWav *pUserData) 135 | { 136 | uint64_t pcmSize; 137 | if(stdio_file_size(file, &pcmSize) != MA_SUCCESS) 138 | { 139 | return !MA_SUCCESS; 140 | } 141 | else if(pcmSize == 0) 142 | { 143 | printf(" ERROR: (PCM) byte count is 0\n"); 144 | return !MA_SUCCESS; 145 | } 146 | // 2 channels of 16 bit samples 147 | else if((pcmSize % (2 * sizeof(int16_t))) != 0) 148 | { 149 | printf(" ERROR: (PCM) byte count indicates non-integer sample count\n"); 150 | return !MA_SUCCESS; 151 | } 152 | 153 | pUserData->pos = 0; 154 | pUserData->vpos = 0; 155 | pUserData->vsize = pcmSize+44; 156 | 157 | memcpy(&pUserData->header[0], "RIFF", 4); 158 | const unsigned chunksize = (44 - 8) + pcmSize; 159 | pUserData->header[4] = chunksize; 160 | pUserData->header[5] = chunksize >> 8; 161 | pUserData->header[6] = chunksize >> 16; 162 | pUserData->header[7] = chunksize >> 24; 163 | memcpy(&pUserData->header[8], "WAVE", 4); 164 | memcpy(&pUserData->header[12], "fmt ", 4); 165 | const unsigned subchunk1size = 16; 166 | pUserData->header[16] = subchunk1size; 167 | pUserData->header[17] = subchunk1size >> 8; 168 | pUserData->header[18] = subchunk1size >> 16; 169 | pUserData->header[19] = subchunk1size >> 24; 170 | pUserData->header[20] = 1; 171 | pUserData->header[21] = 0; 172 | const unsigned numchannels = 2; 173 | pUserData->header[22] = numchannels; 174 | pUserData->header[23] = 0; 175 | const unsigned samplerate = 44100; 176 | pUserData->header[24] = (uint8_t)samplerate; 177 | pUserData->header[25] = samplerate >> 8; 178 | pUserData->header[26] = samplerate >> 16; 179 | pUserData->header[27] = samplerate >> 24; 180 | const unsigned bitspersample = 16; 181 | const unsigned byteRate = (samplerate * numchannels * (bitspersample/8)); 182 | pUserData->header[28] = (uint8_t)byteRate; 183 | pUserData->header[29] = (uint8_t)(byteRate >> 8); 184 | pUserData->header[30] = byteRate >> 16; 185 | pUserData->header[31] = byteRate >> 24; 186 | const uint16_t blockalign = numchannels * (bitspersample/8);; 187 | pUserData->header[32] = blockalign; 188 | pUserData->header[33] = blockalign >> 8; 189 | pUserData->header[34] = bitspersample; 190 | pUserData->header[35] = bitspersample >> 8; 191 | memcpy(&pUserData->header[36], "data", 4); 192 | pUserData->header[40] = pcmSize; 193 | pUserData->header[41] = pcmSize >> 8; 194 | pUserData->header[42] = pcmSize >> 16; 195 | pUserData->header[43] = pcmSize >> 24; 196 | 197 | pUserData->file = file; 198 | 199 | pConfig->encodingFormat = ma_encoding_format_wav; 200 | if(ma_decoder_init(&virtual_wav_read, &virtual_wav_seek, pUserData, pConfig, pDecoder) != MA_SUCCESS) 201 | { 202 | return !MA_SUCCESS; 203 | } 204 | 205 | return MA_SUCCESS; 206 | } 207 | #ifdef __cplusplus 208 | // feed to pcm file to miniaudio as a wav file 209 | MA_API ma_result ma_decoder_init_path_pcm(const fs::path& pFilePath, ma_decoder_config* pConfig, ma_decoder* pDecoder, VirtualWavEx *pUserData) 210 | { 211 | unique_file file(OpenFile(pFilePath, "rb")); 212 | if(!file) 213 | { 214 | return !MA_SUCCESS; 215 | } 216 | if(ma_decoder_init_FILE_pcm(file.get(), pConfig, pDecoder, pUserData) != MA_SUCCESS) 217 | { 218 | return !MA_SUCCESS; 219 | } 220 | pUserData->pcmFp = std::move(file); 221 | return MA_SUCCESS; 222 | } 223 | #endif 224 | 225 | #endif -------------------------------------------------------------------------------- /examples/example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | 33 | 34 | 35 | 52 | 61 | 62 | 73 | 74 | 75 | 83 | 84 | 85 | 106 | 107 | 108 | 109 | 110 | 111 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 163 | 164 | 165 | 175 | 176 | 177 | 178 | 179 | 180 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /src/dumpsxiso/cdreader.cpp: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | #define NOMINMAX 3 | #include 4 | #else 5 | #include 6 | #endif 7 | 8 | #include "cd.h" 9 | #include "xa.h" 10 | #include "common.h" 11 | #include "cdreader.h" 12 | #include "platform.h" 13 | #include 14 | #include 15 | 16 | cd::IsoReader::IsoReader() 17 | { 18 | } 19 | 20 | cd::IsoReader::~IsoReader() 21 | { 22 | if (filePtr != NULL) 23 | fclose(filePtr); 24 | 25 | } 26 | 27 | 28 | bool cd::IsoReader::Open(const fs::path& fileName) 29 | { 30 | Close(); 31 | 32 | filePtr = OpenFile(fileName, "rb"); 33 | 34 | if (filePtr == NULL) 35 | return(false); 36 | 37 | fseek(filePtr, 0, SEEK_END); 38 | totalSectors = ftell(filePtr) / CD_SECTOR_SIZE; 39 | fseek(filePtr, 0, SEEK_SET); 40 | 41 | fread(sectorBuff, CD_SECTOR_SIZE, 1, filePtr); 42 | 43 | currentByte = 0; 44 | currentSector = 0; 45 | 46 | sectorM2F1 = (cd::SECTOR_M2F1*)sectorBuff; 47 | sectorM2F2 = (cd::SECTOR_M2F2*)sectorBuff; 48 | 49 | return(true); 50 | 51 | } 52 | 53 | size_t cd::IsoReader::ReadBytes(void* ptr, size_t bytes, bool singleSector) 54 | { 55 | size_t bytesRead = 0; 56 | char* const dataPtr = (char*)ptr; 57 | constexpr size_t DATA_SIZE = sizeof(sectorM2F1->data); 58 | 59 | while(bytes > 0) 60 | { 61 | const size_t toRead = std::min(DATA_SIZE - currentByte, bytes); 62 | 63 | memcpy(dataPtr+bytesRead, §orM2F1->data[currentByte], toRead); 64 | 65 | currentByte += toRead; 66 | bytesRead += toRead; 67 | bytes -= toRead; 68 | 69 | if (currentByte >= DATA_SIZE) 70 | { 71 | if (singleSector || !PrepareNextSector()) 72 | { 73 | return bytesRead; 74 | } 75 | } 76 | } 77 | 78 | return bytesRead; 79 | 80 | } 81 | 82 | size_t cd::IsoReader::ReadBytesXA(void* ptr, size_t bytes, bool singleSector) 83 | { 84 | size_t bytesRead = 0; 85 | char* const dataPtr = (char*)ptr; 86 | constexpr size_t DATA_SIZE = sizeof(sectorM2F2->data); 87 | 88 | while(bytes > 0) 89 | { 90 | const size_t toRead = std::min(DATA_SIZE - currentByte, bytes); 91 | 92 | memcpy(dataPtr+bytesRead, §orM2F2->data[currentByte], toRead); 93 | 94 | currentByte += toRead; 95 | bytesRead += toRead; 96 | bytes -= toRead; 97 | 98 | if (currentByte >= DATA_SIZE) 99 | { 100 | if (singleSector || !PrepareNextSector()) 101 | { 102 | return bytesRead; 103 | } 104 | } 105 | } 106 | 107 | return(bytesRead); 108 | 109 | } 110 | 111 | size_t cd::IsoReader::ReadBytesDA(void* ptr, size_t bytes, bool singleSector) 112 | { 113 | size_t bytesRead = 0; 114 | char* const dataPtr = (char*)ptr; 115 | constexpr size_t DATA_SIZE = sizeof(sectorBuff); 116 | 117 | while(bytes > 0) 118 | { 119 | const size_t toRead = std::min(DATA_SIZE - currentByte, bytes); 120 | 121 | memcpy(dataPtr+bytesRead, §orBuff[currentByte], toRead); 122 | 123 | currentByte += toRead; 124 | bytesRead += toRead; 125 | bytes -= toRead; 126 | 127 | if (currentByte >= DATA_SIZE) 128 | { 129 | if (singleSector || !PrepareNextSector()) 130 | { 131 | return bytesRead; 132 | } 133 | } 134 | } 135 | 136 | return bytesRead; 137 | 138 | } 139 | 140 | void cd::IsoReader::SkipBytes(size_t bytes, bool singleSector) { 141 | 142 | constexpr size_t DATA_SIZE = sizeof(sectorM2F1->data); 143 | 144 | while(bytes > 0) { 145 | 146 | const size_t toRead = std::min(DATA_SIZE - currentByte, bytes); 147 | 148 | currentByte += toRead; 149 | bytes -= toRead; 150 | 151 | if (currentByte >= DATA_SIZE) { 152 | 153 | if (singleSector || !PrepareNextSector()) 154 | { 155 | return; 156 | } 157 | } 158 | } 159 | } 160 | 161 | int cd::IsoReader::SeekToSector(int sector) { 162 | 163 | if (sector >= totalSectors) 164 | return -1; 165 | 166 | fseek(filePtr, CD_SECTOR_SIZE*sector, SEEK_SET); 167 | fread(sectorBuff, CD_SECTOR_SIZE, 1, filePtr); 168 | 169 | currentSector = sector; 170 | currentByte = 0; 171 | 172 | sectorM2F1 = (cd::SECTOR_M2F1*)sectorBuff; 173 | sectorM2F2 = (cd::SECTOR_M2F2*)sectorBuff; 174 | 175 | return ferror(filePtr); 176 | 177 | } 178 | 179 | size_t cd::IsoReader::SeekToByte(size_t offs) { 180 | 181 | int sector = (offs/CD_SECTOR_SIZE); 182 | 183 | fseek(filePtr, CD_SECTOR_SIZE*sector, SEEK_SET); 184 | fread(sectorBuff, CD_SECTOR_SIZE, 1, filePtr); 185 | 186 | currentSector = sector; 187 | currentByte = offs%CD_SECTOR_SIZE; 188 | 189 | sectorM2F1 = (cd::SECTOR_M2F1*)sectorBuff; 190 | sectorM2F2 = (cd::SECTOR_M2F2*)sectorBuff; 191 | 192 | return (CD_SECTOR_SIZE*static_cast(currentSector))+currentByte; 193 | 194 | } 195 | 196 | size_t cd::IsoReader::GetPos() const 197 | { 198 | return (CD_SECTOR_SIZE*static_cast(currentSector))+currentByte; 199 | } 200 | 201 | void cd::IsoReader::Close() { 202 | 203 | if (filePtr != NULL) { 204 | fclose(filePtr); 205 | filePtr = NULL; 206 | } 207 | 208 | } 209 | 210 | bool cd::IsoReader::PrepareNextSector() 211 | { 212 | currentByte = 0; 213 | 214 | if (fread(sectorBuff, CD_SECTOR_SIZE, 1, filePtr) != 1) 215 | { 216 | return false; 217 | } 218 | 219 | currentSector++; 220 | 221 | sectorM2F1 = (cd::SECTOR_M2F1*)sectorBuff; 222 | sectorM2F2 = (cd::SECTOR_M2F2*)sectorBuff; 223 | return true; 224 | } 225 | 226 | 227 | void cd::IsoPathTable::FreePathTable() 228 | { 229 | pathTableList.clear(); 230 | } 231 | 232 | size_t cd::IsoPathTable::ReadPathTable(cd::IsoReader* reader, int lba) 233 | { 234 | if (lba >= 0) 235 | reader->SeekToSector(lba); 236 | 237 | FreePathTable(); 238 | 239 | while (true) 240 | { 241 | Entry pathTableEntry; 242 | reader->ReadBytes(&pathTableEntry.entry, sizeof(pathTableEntry.entry)); 243 | 244 | // Its the end of the path table when its nothing but zeros 245 | if (pathTableEntry.entry.nameLength == 0) 246 | break; 247 | 248 | 249 | // Read entry name 250 | { 251 | const size_t length = pathTableEntry.entry.nameLength; 252 | pathTableEntry.name.resize(length); 253 | reader->ReadBytes(pathTableEntry.name.data(), length); 254 | 255 | // ECMA-119 9.4.6 - 00 field present only if entry length is an odd number 256 | if ((length % 2) != 0) 257 | { 258 | reader->SkipBytes(1); 259 | } 260 | 261 | // Strip trailing zeroes, if any 262 | pathTableEntry.name.resize(strlen(pathTableEntry.name.c_str())); 263 | } 264 | 265 | pathTableList.emplace_back(std::move(pathTableEntry)); 266 | } 267 | 268 | return pathTableList.size(); 269 | 270 | } 271 | 272 | fs::path cd::IsoPathTable::GetFullDirPath(int dirEntry) const 273 | { 274 | fs::path path; 275 | 276 | while (true) 277 | { 278 | if (pathTableList[dirEntry].name.empty()) 279 | break; 280 | 281 | // Prepend! 282 | path = pathTableList[dirEntry].name / path; 283 | 284 | dirEntry = pathTableList[dirEntry].entry.parentDirIndex-1; 285 | } 286 | 287 | return path; 288 | } 289 | 290 | cd::IsoDirEntries::IsoDirEntries(ListView view) 291 | : dirEntryList(std::move(view)) 292 | { 293 | } 294 | 295 | void cd::IsoDirEntries::ReadDirEntries(cd::IsoReader* reader, int lba, int sectors) 296 | { 297 | size_t numEntries = 0; // Used to skip the first two entries, . and .. 298 | for (int sec = 0; sec < sectors; sec++) 299 | { 300 | reader->SeekToSector(lba + sec); 301 | while (true) 302 | { 303 | auto entry = ReadEntry(reader); 304 | if (!entry) 305 | { 306 | // Either end of the table, or end of sector 307 | break; 308 | } 309 | 310 | if (numEntries++ >= 2) 311 | { 312 | dirEntryList.emplace(std::move(entry.value())); 313 | } 314 | } 315 | } 316 | 317 | // Sort the directory by LBA for pretty printing 318 | dirEntryList.SortView([](const auto& left, const auto& right) 319 | { 320 | return left.get().entry.entryOffs.lsb < right.get().entry.entryOffs.lsb; 321 | }); 322 | } 323 | 324 | std::optional cd::IsoDirEntries::ReadEntry(cd::IsoReader* reader) const 325 | { 326 | Entry entry; 327 | 328 | // Read 33 byte directory entry 329 | size_t bytesRead = reader->ReadBytes(&entry.entry, sizeof(entry.entry), true); 330 | 331 | // The file entry table usually ends with null bytes so break if we reached that area 332 | if (bytesRead != sizeof(entry.entry) || entry.entry.entryLength == 0) 333 | return std::nullopt; 334 | 335 | // Read identifier string 336 | entry.identifier.resize(entry.entry.identifierLen); 337 | reader->ReadBytes(entry.identifier.data(), entry.entry.identifierLen, true); 338 | 339 | if (entry.identifier == "ST0D_00D.BIN;1") 340 | { 341 | int i = 0; 342 | } 343 | 344 | // Strip trailing zeroes, if any 345 | entry.identifier.resize(strlen(entry.identifier.c_str())); 346 | 347 | // ECMA-119 9.1.12 - 00 field present only if file identifier length is an even number 348 | if ((entry.entry.identifierLen % 2) == 0) 349 | { 350 | reader->SkipBytes(1); 351 | } 352 | 353 | // Read XA attribute data 354 | reader->ReadBytes(&entry.extData, sizeof(entry.extData), true); 355 | 356 | // XA attributes are big endian, swap them 357 | entry.extData.attributes = SwapBytes16(entry.extData.attributes); 358 | entry.extData.ownergroupid = SwapBytes16(entry.extData.ownergroupid); 359 | entry.extData.owneruserid = SwapBytes16(entry.extData.owneruserid); 360 | 361 | return entry; 362 | } 363 | 364 | void cd::IsoDirEntries::ReadRootDir(cd::IsoReader* reader, int lba) 365 | { 366 | reader->SeekToSector(lba); 367 | auto entry = ReadEntry(reader); 368 | if (entry) 369 | { 370 | dirEntryList.emplace(std::move(entry.value())); 371 | } 372 | } -------------------------------------------------------------------------------- /src/shared/cd.h: -------------------------------------------------------------------------------- 1 | #ifndef _CD_H 2 | #define _CD_H 3 | 4 | #include 5 | 6 | #ifdef _WIN32 7 | #ifndef NOMINMAX 8 | #define NOMINMAX 9 | #endif 10 | #include 11 | #else 12 | #include 13 | #endif 14 | 15 | // Sector size in bytes (do not change) 16 | #define CD_SECTOR_SIZE 2352 17 | 18 | // CD and ISO 9660 reader namespace 19 | namespace cd { 20 | 21 | /// Structure for a mode 2 form 1 sector (used in regular files) 22 | typedef struct { 23 | unsigned char sync[12]; /// Sync pattern (usually 00 FF FF FF FF FF FF FF FF FF FF 00) 24 | unsigned char addr[3]; /// Sector address (see below for encoding details) 25 | unsigned char mode; /// Mode (usually 2 for Mode 2 Form 1/2 sectors) 26 | unsigned char subHead[8]; /// Sub-header (00 00 08 00 00 00 08 00 for Form 1 data sectors) 27 | unsigned char data[2048]; /// Data (form 1) 28 | unsigned char edc[4]; /// Error-detection code (CRC32 of data area) 29 | unsigned char ecc[276]; /// Error-correction code (uses Reed-Solomon ECC algorithm) 30 | } SECTOR_M2F1; 31 | 32 | /** Regular data files are usually stored in this sector format as it has ECC error correction which 33 | * is important for program data and other sensitive data files. 34 | * 35 | * The 3 address bytes of an M2F1 and M2F2 format sector are actually just timecode values. The 1st 36 | * byte is minutes, 2nd byte is seconds and the 3rd byte is frames (or sectors, 75 per second on CD). 37 | * 38 | * The unusual thing about how the address bytes are encoded is that values must be specified in hex 39 | * rather than decimal (0x20 instead of 20, 0x15 instead of 15) so values must be converted by using 40 | * the following algorithm: 41 | * 42 | * (16*(v/10))+(v%10) 43 | * 44 | * So, for a sector address of 2 minutes, 30 seconds and 35 frames in timecode format, it should look 45 | * like this in hex: 46 | * 47 | * 0x02 0x35 0x30 48 | * 49 | */ 50 | 51 | /// Structure for a mode 2 form 2 sector (used in STR/XA files) 52 | typedef struct { 53 | unsigned char sync[12]; /// Sync pattern (usually 00 FF FF FF FF FF FF FF FF FF FF 00) 54 | unsigned char addr[3]; /// Sector address (a 24-bit big-endian integer. starts at 200, 201 an onwards) 55 | unsigned char mode; /// Mode (usually 2 for Mode 2 Form 1/2 sectors) 56 | unsigned char data[2336]; /// 8 bytes Subheader, 2324 bytes Data (form 2), and 4 bytes ECC 57 | } SECTOR_M2F2; 58 | 59 | // Set struct alignment to 1 because the ISO file system is not very memory alignment friendly which will 60 | // result to alignment issues when reading data entries with structs. 61 | #pragma pack(push, 1) 62 | 63 | /// Structure of a double-endian unsigned short word 64 | typedef struct { 65 | unsigned short lsb; /// LSB format 16-bit word 66 | unsigned short msb; /// MSB format 16-bit word 67 | } ISO_USHORT_PAIR; 68 | 69 | /// Structure of a double-endian unsigned int word 70 | typedef struct { 71 | unsigned int lsb; /// LSB format 32-bit word 72 | unsigned int msb; /// MSB format 32-bit word 73 | } ISO_UINT_PAIR; 74 | 75 | /// ISO descriptor header structure 76 | typedef struct { 77 | unsigned char type; /// Volume descriptor type (1 is descriptor, 255 is descriptor terminator) 78 | char id[5]; /// Volume descriptor ID (always CD001) 79 | unsigned short version; /// Volume descriptor version (always 0x01) 80 | } ISO_DESCRIPTOR_HEADER; 81 | 82 | /// Structure of a date stamp for ISO_DIR_ENTRY structure 83 | struct ISO_DATESTAMP 84 | { 85 | unsigned char year; /// number of years since 1900 86 | unsigned char month; /// month, where 1=January, 2=February, etc. 87 | unsigned char day; /// day of month, in the range from 1 to 31 88 | unsigned char hour; /// hour, in the range from 0 to 23 89 | unsigned char minute; /// minute, in the range from 0 to 59 90 | unsigned char second; /// Second, in the range from 0 to 59 91 | signed char GMToffs; /// Greenwich Mean Time offset 92 | }; 93 | 94 | /// Structure of a long date time format, specified in Section 8.4.26.1 of ECMA 119 95 | struct ISO_LONG_DATESTAMP 96 | { 97 | char year[4]; /// year from I to 9999 98 | char month[2]; /// month of the year from 1 to 12 99 | char day[2]; /// day of the month from 1 to 31 100 | char hour[2]; /// hour of the day from 0 to 23 101 | char minute[2]; /// minute of the hour from 0 to 59 102 | char second[2]; /// second of the minute from 0 to 59 103 | char hsecond[2]; /// hundredths of a second 104 | signed char GMToffs; /// Greenwich Mean Time offset 105 | }; 106 | 107 | /// Structure of an ISO path table entry 108 | struct ISO_PATHTABLE_ENTRY 109 | { 110 | unsigned char nameLength; /// Name length (or 1 for the root directory) 111 | unsigned char extLength; /// Number of sectors in extended attribute record 112 | unsigned int dirOffs; /// Number of the first sector in the directory, as a double word 113 | short parentDirIndex; /// Index of the directory record's parent directory 114 | // If nameLength is even numbered, a padding byte will be present after the entry name. 115 | }; 116 | 117 | struct ISO_DIR_ENTRY 118 | { 119 | unsigned char entryLength; // Directory entry length (variable, use for parsing through entries) 120 | unsigned char extLength; // Extended entry data length (always 0) 121 | ISO_UINT_PAIR entryOffs; // Points to the LBA of the file/directory entry 122 | ISO_UINT_PAIR entrySize; // Size of the file/directory entry 123 | ISO_DATESTAMP entryDate; // Date & time stamp of entry 124 | unsigned char flags; // File flags (0x02 for directories, 0x00 for files) 125 | unsigned char fileUnitSize; // Unit size (usually 0 even with Form 2 files such as STR/XA) 126 | unsigned char interleaveGapSize; // Interleave gap size (usually 0 even with Form 2 files such as STR/XA) 127 | ISO_USHORT_PAIR volSeqNum; // Volume sequence number (always 1) 128 | unsigned char identifierLen; // Identifier (file/directory name) length in bytes 129 | // If identifierLen is even numbered, a padding byte will be present after the identifier text. 130 | }; 131 | 132 | typedef struct { 133 | unsigned char entryLength; // Always 34 bytes 134 | unsigned char extLength; // Always 0 135 | ISO_UINT_PAIR entryOffs; // Should point to LBA 22 136 | ISO_UINT_PAIR entrySize; // Size of entry extent 137 | ISO_DATESTAMP entryDate; // Record date and time 138 | unsigned char flags; // File flags 139 | unsigned char fileUnitSize; 140 | unsigned char interleaveGapSize; 141 | ISO_USHORT_PAIR volSeqNum; 142 | unsigned char identifierLen; // 0x01 143 | unsigned char identifier; // 0x00 144 | } ISO_ROOTDIR_HEADER; 145 | 146 | // ISO descriptor structure 147 | typedef struct { 148 | 149 | // ISO descriptor header 150 | ISO_DESCRIPTOR_HEADER header; 151 | // System ID (always PLAYSTATION) 152 | char systemID[32]; 153 | // Volume ID (or label, can be blank or anything) 154 | char volumeID[32]; 155 | // Unused null bytes 156 | unsigned char pad2[8]; 157 | // Size of volume in sector units 158 | ISO_UINT_PAIR volumeSize; 159 | // Unused null bytes 160 | unsigned char pad3[32]; 161 | // Number of discs in this volume set (always 1 for single volume) 162 | ISO_USHORT_PAIR volumeSetSize; 163 | // Number of this disc in volume set (always 1 for single volume) 164 | ISO_USHORT_PAIR volumeSeqNumber; 165 | // Size of sector in bytes (always 2048 bytes) 166 | ISO_USHORT_PAIR sectorSize; 167 | // Path table size in bytes (applies to all the path tables) 168 | ISO_UINT_PAIR pathTableSize; 169 | // LBA to Type-L path table 170 | unsigned int pathTable1Offs; 171 | // LBA to optional Type-L path table (usually a copy of the primary path table) 172 | unsigned int pathTable2Offs; 173 | // LBA to Type-L path table but with MSB format values 174 | unsigned int pathTable1MSBoffs; 175 | // LBA to optional Type-L path table but with MSB format values (usually a copy of the main path table) 176 | unsigned int pathTable2MSBoffs; 177 | // Directory entry for the root directory (similar to a directory entry) 178 | ISO_ROOTDIR_HEADER rootDirRecord; 179 | // Volume set identifier (can be blank or anything) 180 | char volumeSetIdentifier[128]; 181 | // Publisher identifier (can be blank or anything) 182 | char publisherIdentifier[128]; 183 | // Data preparer identifier (can be blank or anything) 184 | char dataPreparerIdentifier[128]; 185 | // Application identifier (always PLAYSTATION) 186 | char applicationIdentifier[128]; 187 | // Copyright file in the file system identifier (can be blank or anything) 188 | char copyrightFileIdentifier[37]; 189 | // Abstract file in the file system identifier (can be blank or anything) 190 | char abstractFileIdentifier[37]; 191 | // Bibliographical file identifier in the file system (can be blank or anything) 192 | char bibliographicFilelIdentifier[37]; 193 | // Volume create date 194 | ISO_LONG_DATESTAMP volumeCreateDate; 195 | // Volume modify date 196 | ISO_LONG_DATESTAMP volumeModifyDate; 197 | // Volume expiry date 198 | ISO_LONG_DATESTAMP volumeExpiryDate; 199 | // Volume effective date 200 | ISO_LONG_DATESTAMP volumeEffectiveDate; 201 | // File structure version (always 1) 202 | unsigned char fileStructVersion; 203 | // Padding 204 | unsigned char dummy0; 205 | // Application specific data (says CD-XA001 at [141], the rest are null bytes) 206 | unsigned char appData[512]; 207 | // Padding 208 | unsigned char pad4[653]; 209 | 210 | } ISO_DESCRIPTOR; 211 | 212 | // License data (just a sequence of 28032 bytes) 213 | typedef struct { 214 | char data[28032]; 215 | } ISO_LICENSE; 216 | 217 | // RIFF+WAVE header 218 | typedef struct { 219 | char chunkID[4]; 220 | unsigned int chunkSize; 221 | char format[4]; 222 | 223 | char subchunk1ID[4]; 224 | unsigned int subchunk1Size; 225 | unsigned short audioFormat; 226 | unsigned short numChannels; 227 | unsigned int sampleRate; 228 | unsigned int byteRate; 229 | unsigned short blockAlign; 230 | unsigned short bitsPerSample; 231 | 232 | char subchunk2ID[4]; 233 | unsigned int subchunk2Size; 234 | } RIFF_HEADER; 235 | 236 | // Leave non-aligned structure packing 237 | #pragma pack(pop) 238 | 239 | } 240 | 241 | #endif // _CD_H 242 | -------------------------------------------------------------------------------- /src/mkpsxiso/cdwriter.cpp: -------------------------------------------------------------------------------- 1 | #include "cdwriter.h" 2 | #include "common.h" 3 | #include "edcecc.h" 4 | #include "platform.h" 5 | 6 | #include 7 | #include 8 | 9 | using namespace cd; 10 | 11 | static const EDCECC EDC_ECC_GEN; 12 | 13 | ISO_USHORT_PAIR cd::SetPair16(unsigned short val) { 14 | return { val, SwapBytes16(val) }; 15 | } 16 | 17 | ISO_UINT_PAIR cd::SetPair32(unsigned int val) { 18 | return { val, SwapBytes32(val) }; 19 | } 20 | 21 | bool IsoWriter::Create(const fs::path& fileName, unsigned int sizeLBA) 22 | { 23 | const uint64_t sizeBytes = static_cast(sizeLBA) * CD_SECTOR_SIZE; 24 | 25 | m_threadPool = std::make_unique(std::thread::hardware_concurrency()); 26 | 27 | m_mmap = std::make_unique(); 28 | return m_mmap->Create(fileName, sizeBytes); 29 | } 30 | 31 | void IsoWriter::Close() 32 | { 33 | m_mmap.reset(); 34 | } 35 | 36 | // ====================================================== 37 | 38 | IsoWriter::SectorView::SectorView(ThreadPool* threadPool, MMappedFile* mappedFile, unsigned int offsetLBA, unsigned int sizeLBA, EdcEccForm edcEccForm) 39 | : m_threadPool(threadPool) 40 | , m_view(mappedFile->GetView(static_cast(offsetLBA) * CD_SECTOR_SIZE, static_cast(sizeLBA) * CD_SECTOR_SIZE)) 41 | , m_currentLBA(offsetLBA) 42 | , m_endLBA(offsetLBA + sizeLBA) 43 | , m_edcEccForm(edcEccForm) 44 | { 45 | m_currentSector = m_view.GetBuffer(); 46 | } 47 | 48 | IsoWriter::SectorView::~SectorView() 49 | { 50 | WaitForChecksumJobs(); 51 | } 52 | 53 | static uint8_t ToBCD8(uint8_t num) 54 | { 55 | return ((num / 10) << 4) | (num % 10); 56 | } 57 | 58 | static void WriteSectorAddress(uint8_t* output, unsigned int lsn) 59 | { 60 | unsigned int lba = lsn + 150; 61 | 62 | const uint8_t frame = static_cast(lba % 75); 63 | lba /= 75; 64 | 65 | const uint8_t second = static_cast(lba % 60); 66 | lba /= 60; 67 | 68 | const uint8_t minute = static_cast(lba); 69 | 70 | output[0] = ToBCD8(minute); 71 | output[1] = ToBCD8(second); 72 | output[2] = ToBCD8(frame); 73 | } 74 | 75 | void IsoWriter::SectorView::PrepareSectorHeader() const 76 | { 77 | SECTOR_M2F1* sector = static_cast(m_currentSector); 78 | 79 | static constexpr uint8_t SYNC_PATTERN[12] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 }; 80 | std::copy(std::begin(SYNC_PATTERN), std::end(SYNC_PATTERN), sector->sync); 81 | 82 | WriteSectorAddress(sector->addr, m_currentLBA); 83 | 84 | sector->mode = 2; // Mode 2 85 | } 86 | 87 | void IsoWriter::SectorView::CalculateForm1() 88 | { 89 | SECTOR_M2F1* sector = static_cast(m_currentSector); 90 | 91 | m_checksumJobs.emplace_front(m_threadPool->enqueue([](SECTOR_M2F1* sector) 92 | { 93 | // Encode EDC data 94 | EDC_ECC_GEN.ComputeEdcBlock(sector->subHead, sizeof(sector->subHead) + sizeof(sector->data), sector->edc); 95 | 96 | // Compute ECC P code 97 | static const unsigned char zeroaddress[4] = { 0, 0, 0, 0 }; 98 | EDC_ECC_GEN.ComputeEccBlock(zeroaddress, sector->subHead, 86, 24, 2, 86, sector->ecc); 99 | // Compute ECC Q code 100 | EDC_ECC_GEN.ComputeEccBlock(zeroaddress, sector->subHead, 52, 43, 86, 88, sector->ecc+172); 101 | }, sector)); 102 | } 103 | 104 | void IsoWriter::SectorView::CalculateForm2() 105 | { 106 | SECTOR_M2F2* sector = static_cast(m_currentSector); 107 | m_checksumJobs.emplace_front(m_threadPool->enqueue(&EDCECC::ComputeEdcBlock, &EDC_ECC_GEN, 108 | sector->data, sizeof(sector->data) - 4, sector->data + sizeof(sector->data) - 4)); 109 | } 110 | 111 | void IsoWriter::SectorView::WaitForChecksumJobs() 112 | { 113 | for (auto& job : m_checksumJobs) 114 | { 115 | job.get(); 116 | } 117 | m_checksumJobs.clear(); 118 | } 119 | 120 | // ====================================================== 121 | 122 | class SectorViewM2F1 final : public IsoWriter::SectorView 123 | { 124 | private: 125 | using SectorType = SECTOR_M2F1; 126 | 127 | public: 128 | using IsoWriter::SectorView::SectorView; 129 | 130 | ~SectorViewM2F1() override 131 | { 132 | if (m_offsetInSector != 0) 133 | { 134 | NextSector(); 135 | } 136 | } 137 | 138 | void WriteFile(FILE* file) override 139 | { 140 | SectorType* sector = static_cast(m_currentSector); 141 | const unsigned int lastLBA = m_endLBA - 1; 142 | 143 | while (m_currentLBA < m_endLBA) 144 | { 145 | PrepareSectorHeader(); 146 | SetSubHeader(sector->subHead, m_currentLBA != lastLBA ? m_subHeader : IsoWriter::SubEOF); 147 | 148 | const size_t bytesRead = fread(sector->data, 1, sizeof(sector->data), file); 149 | // Fill the remainder of the sector with zeroes if applicable 150 | std::fill(std::begin(sector->data) + bytesRead, std::end(sector->data), 0); 151 | 152 | if (m_edcEccForm == IsoWriter::EdcEccForm::Form1) 153 | { 154 | CalculateForm1(); 155 | } 156 | else if (m_edcEccForm == IsoWriter::EdcEccForm::Form2) 157 | { 158 | CalculateForm2(); 159 | } 160 | 161 | m_currentLBA++; 162 | m_currentSector = ++sector; 163 | } 164 | } 165 | 166 | void WriteMemory(const void* memory, size_t size) override 167 | { 168 | const char* buf = static_cast(memory); 169 | const unsigned int lastLBA = m_endLBA - 1; 170 | 171 | while (m_currentLBA < m_endLBA && size > 0) 172 | { 173 | SectorType* sector = static_cast(m_currentSector); 174 | 175 | if (m_offsetInSector == 0) 176 | { 177 | PrepareSectorHeader(); 178 | SetSubHeader(sector->subHead, m_currentLBA != lastLBA ? m_subHeader : IsoWriter::SubEOF); 179 | } 180 | 181 | const size_t memToCopy = std::min(GetSpaceInCurrentSector(), size); 182 | std::copy_n(buf, memToCopy, sector->data + m_offsetInSector); 183 | 184 | size -= memToCopy; 185 | buf += memToCopy; 186 | m_offsetInSector += memToCopy; 187 | 188 | if (m_offsetInSector >= sizeof(sector->data)) 189 | { 190 | NextSector(); 191 | } 192 | } 193 | } 194 | 195 | void WriteBlankSectors(unsigned int count) override 196 | { 197 | SectorType* sector = static_cast(m_currentSector); 198 | const bool isForm2 = m_edcEccForm == IsoWriter::EdcEccForm::Form2; 199 | 200 | while (m_currentLBA < m_endLBA && count > 0) 201 | { 202 | PrepareSectorHeader(); 203 | SetSubHeader(sector->subHead, isForm2 ? 0x00200000 : 0); 204 | 205 | std::fill(std::begin(sector->data), std::end(sector->data), 0); 206 | if (m_edcEccForm == IsoWriter::EdcEccForm::Form1) 207 | { 208 | CalculateForm1(); 209 | } 210 | else if (m_edcEccForm == IsoWriter::EdcEccForm::Form2) 211 | { 212 | CalculateForm2(); 213 | } 214 | 215 | count--; 216 | m_currentLBA++; 217 | m_currentSector = ++sector; 218 | } 219 | } 220 | 221 | size_t GetSpaceInCurrentSector() const override 222 | { 223 | return sizeof(SectorType::data) - m_offsetInSector; 224 | } 225 | 226 | void NextSector() override 227 | { 228 | // Fill the remainder of the sector with zeroes if applicable 229 | SectorType* sector = static_cast(m_currentSector); 230 | std::fill(std::begin(sector->data) + m_offsetInSector, std::end(sector->data), 0); 231 | 232 | if (m_edcEccForm == IsoWriter::EdcEccForm::Form1) 233 | { 234 | CalculateForm1(); 235 | } 236 | else if (m_edcEccForm == IsoWriter::EdcEccForm::Form2) 237 | { 238 | CalculateForm2(); 239 | } 240 | 241 | m_offsetInSector = 0; 242 | m_currentLBA++; 243 | m_currentSector = sector + 1; 244 | } 245 | 246 | void SetSubheader(unsigned int subHead) override 247 | { 248 | m_subHeader = subHead; 249 | } 250 | 251 | private: 252 | void SetSubHeader(unsigned char* subHead, unsigned int data) const 253 | { 254 | memcpy(subHead, &data, sizeof(data)); 255 | memcpy(subHead+4, &data, sizeof(data)); 256 | } 257 | 258 | private: 259 | unsigned int m_subHeader = IsoWriter::SubData; 260 | }; 261 | 262 | auto IsoWriter::GetSectorViewM2F1(unsigned int offsetLBA, unsigned int sizeLBA, EdcEccForm edcEccForm) const -> std::unique_ptr 263 | { 264 | return std::make_unique(m_threadPool.get(), m_mmap.get(), offsetLBA, sizeLBA, edcEccForm); 265 | } 266 | 267 | class SectorViewM2F2 final : public IsoWriter::SectorView 268 | { 269 | private: 270 | using SectorType = SECTOR_M2F2; 271 | 272 | public: 273 | using IsoWriter::SectorView::SectorView; 274 | 275 | ~SectorViewM2F2() override 276 | { 277 | if (m_offsetInSector != 0) 278 | { 279 | NextSector(); 280 | } 281 | } 282 | 283 | void WriteFile(FILE* file) override 284 | { 285 | SectorType* sector = static_cast(m_currentSector); 286 | 287 | while (m_currentLBA < m_endLBA) 288 | { 289 | PrepareSectorHeader(); 290 | 291 | const size_t bytesRead = fread(sector->data, 1, sizeof(sector->data), file); 292 | // Fill the remainder of the sector with zeroes if applicable 293 | std::fill(std::begin(sector->data) + bytesRead, std::end(sector->data), 0); 294 | 295 | if (m_edcEccForm != IsoWriter::EdcEccForm::Autodetect) 296 | { 297 | if (m_edcEccForm == IsoWriter::EdcEccForm::Form1) 298 | { 299 | CalculateForm1(); 300 | } 301 | else if (m_edcEccForm == IsoWriter::EdcEccForm::Form2) 302 | { 303 | CalculateForm2(); 304 | } 305 | } 306 | else 307 | { 308 | // Check submode if sector is mode 2 form 2 309 | if ( sector->data[2] & 0x20 ) 310 | { 311 | // If so, write it as an XA sector 312 | CalculateForm2(); 313 | } 314 | else 315 | { 316 | // Otherwise, write it as Mode 2 Form 1 317 | CalculateForm1(); 318 | } 319 | } 320 | 321 | m_currentLBA++; 322 | m_currentSector = ++sector; 323 | } 324 | } 325 | 326 | void WriteMemory(const void* memory, size_t size) override 327 | { 328 | const char* buf = static_cast(memory); 329 | const unsigned int lastLBA = m_endLBA - 1; 330 | 331 | while (m_currentLBA < m_endLBA && size > 0) 332 | { 333 | if (m_offsetInSector == 0) 334 | { 335 | PrepareSectorHeader(); 336 | } 337 | 338 | SectorType* sector = static_cast(m_currentSector); 339 | 340 | const size_t memToCopy = std::min(GetSpaceInCurrentSector(), size); 341 | std::copy_n(buf, memToCopy, sector->data + m_offsetInSector); 342 | 343 | size -= memToCopy; 344 | buf += memToCopy; 345 | m_offsetInSector += memToCopy; 346 | 347 | if (m_offsetInSector >= sizeof(sector->data)) 348 | { 349 | NextSector(); 350 | } 351 | } 352 | } 353 | 354 | void WriteBlankSectors(unsigned int count) override 355 | { 356 | SectorType* sector = static_cast(m_currentSector); 357 | const bool isForm2 = m_edcEccForm == IsoWriter::EdcEccForm::Form2; 358 | 359 | while (m_currentLBA < m_endLBA && count > 0) 360 | { 361 | PrepareSectorHeader(); 362 | 363 | std::fill(std::begin(sector->data), std::end(sector->data), 0); 364 | if (m_edcEccForm == IsoWriter::EdcEccForm::Form1) 365 | { 366 | CalculateForm1(); 367 | } 368 | else if (m_edcEccForm == IsoWriter::EdcEccForm::Form2) 369 | { 370 | CalculateForm2(); 371 | } 372 | 373 | count--; 374 | m_currentLBA++; 375 | m_currentSector = ++sector; 376 | } 377 | } 378 | 379 | size_t GetSpaceInCurrentSector() const override 380 | { 381 | return sizeof(SectorType::data) - m_offsetInSector; 382 | } 383 | 384 | void NextSector() override 385 | { 386 | // Fill the remainder of the sector with zeroes if applicable 387 | SectorType* sector = static_cast(m_currentSector); 388 | std::fill(std::begin(sector->data) + m_offsetInSector, std::end(sector->data), 0); 389 | 390 | if (m_edcEccForm != IsoWriter::EdcEccForm::Autodetect) 391 | { 392 | if (m_edcEccForm == IsoWriter::EdcEccForm::Form1) 393 | { 394 | CalculateForm1(); 395 | } 396 | else if (m_edcEccForm == IsoWriter::EdcEccForm::Form2) 397 | { 398 | CalculateForm2(); 399 | } 400 | } 401 | else 402 | { 403 | // Check submode if sector is mode 2 form 2 404 | if ( sector->data[2] & 0x20 ) 405 | { 406 | // If so, write it as an XA sector 407 | CalculateForm2(); 408 | } 409 | else 410 | { 411 | // Otherwise, write it as Mode 2 Form 1 412 | CalculateForm1(); 413 | } 414 | } 415 | 416 | m_offsetInSector = 0; 417 | m_currentLBA++; 418 | m_currentSector = sector + 1; 419 | } 420 | 421 | void SetSubheader(unsigned int subHead) override 422 | { 423 | // Not applicable to M2F2 sectors 424 | } 425 | }; 426 | 427 | auto IsoWriter::GetSectorViewM2F2(unsigned int offsetLBA, unsigned int sizeLBA, EdcEccForm edcEccForm) const -> std::unique_ptr 428 | { 429 | return std::make_unique(m_threadPool.get(), m_mmap.get(), offsetLBA, sizeLBA, edcEccForm); 430 | } 431 | 432 | // ====================================================== 433 | 434 | IsoWriter::RawSectorView::RawSectorView(MMappedFile* mappedFile, unsigned int offsetLBA, unsigned int sizeLBA) 435 | : m_view(mappedFile->GetView(static_cast(offsetLBA) * CD_SECTOR_SIZE, static_cast(sizeLBA) * CD_SECTOR_SIZE)) 436 | , m_endLBA(sizeLBA) 437 | { 438 | } 439 | 440 | void* IsoWriter::RawSectorView::GetRawBuffer() const 441 | { 442 | return m_view.GetBuffer(); 443 | } 444 | 445 | void IsoWriter::RawSectorView::WriteBlankSectors() 446 | { 447 | char* buf = static_cast(m_view.GetBuffer()); 448 | std::fill_n(buf, static_cast(m_endLBA) * CD_SECTOR_SIZE, 0); 449 | } 450 | 451 | auto IsoWriter::GetRawSectorView(unsigned int offsetLBA, unsigned int sizeLBA) const -> std::unique_ptr 452 | { 453 | return std::make_unique(m_mmap.get(), offsetLBA, sizeLBA); 454 | } 455 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # MKPSXISO 3 | 4 | `mkpsxiso` builds PlayStation CD images from an XML document. 5 | 6 | `dumpsxiso` dumps PlayStation CD images to files and documents the precise structure to a `mkpsxiso` compatible XML document. 7 | 8 | `mkpsxiso` is meant to provide a faster, cross-platform, modern replacement of the BUILDCD from the official development tools. BUILDCD unfortunately only runs on 16 bit DOS compatible systems and it's output format is unusable by modern CD burning tools. Other ISO creation tools such as MKISOFS do not allow controlling the precise order of files (necessary for optimizing access times) and do not support mixed-mode type files for CD streaming such as XA audio and MDEC video streams used by many PlayStation games. `mkpsxiso` outputs either a standard `.bin` and `.cue` or `.iso` ready to burn to CD or use in an emulator! The hope is that `mkpsxiso` tools ease PlayStation homebrew development and ROM hacking and reverse engineer efforts. `mkpsxiso` can also be used as a regular ISO creation tool that complies with the older ISO9660 standard with no Joliet extensions. 9 | 10 | `mkpsxiso` can properly license the image with the Sony license data during ISO building eliminating the use of the extra program. However, you must supply your own copy. It can be found in the PsyQ SDK, see [Starting PSX Development](https://psx.arthus.net/starting.html). `dumpsxiso` can also dump the license data of an existing disk. 11 | 12 | ## Features 13 | 14 | * Uses XML for scripting ISO projects. 15 | * Outputs ISO images directly to iso or bin+cue image format. 16 | * Injects license data into ISO image correctly. 17 | * File LBA controlled by order of files allowing for file seek optimization (just like BUILDCD). 18 | * Supports mixed-mode CD-XA stream files such as XA audio and STR video. 19 | * Supports CDDA audio tracks from wav, flac, pcm, and mp3 files, both as DA files and just as audio tracks 20 | * Can output log of all files packed with details such as LBA, size and timecode offset. 21 | * Extract CDDA tracks from ISO as wav, flac, and pcm. 22 | * Many images can be rebuilt 1:1 now. 23 | * XML generation: by default in strict LBA order, but can instead sort by dir for pretty output. 24 | * Timestamps and XA attributes are preserved. 25 | 26 | ## Binary Download 27 | 28 | [Releases](../../releases/latest) for Win32 and `ubuntu-latest`, both are built by github CI starting at v2.0 29 | 30 | [Ancient releases](https://github.com/Lameguy64/mkpsxiso/tree/gh-pages) (NOT RECOMMENDED) 31 | 32 | ## Compiling 33 | 34 | 1. Set up CMake and a compiler toolchain. Install the `cmake` and `build-essential` packages provided by your Linux distro, or one of the following kits on Windows: 35 | * MSVC (do not install CMake through the Visual Studio installer, download it from [here](https://cmake.org/download) instead) 36 | * MSys2 (use the "MinGW 64-bit" shell) with the following packages: `git`, `mingw-w64-x86_64-make`, `mingw-w64-x86_64-cmake`, `mingw-w64-x86_64-g++` 37 | * Cygwin64 with the following packages: `git`, `make`, `cmake`, `gcc` 38 | 39 | 2. Clone/download the repo, then run the following command from the mkpsxiso directory to ensure `tinyxml2` is also downloaded and updated: 40 | 41 | ```bash 42 | git submodule update --init --recursive 43 | ``` 44 | 45 | 3. Run the following commands: 46 | 47 | ```bash 48 | cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Release 49 | cmake --build ./build 50 | cmake --install ./build 51 | ``` 52 | 53 | If you wish to build dumpsxiso without libFLAC support (libFLAC is required for encoding CDDA/DA audio as FLAC), add `-DMKPSXISO_NO_LIBFLAC=1` to the end of the first command. 54 | 55 | Add `sudo` to the install command if necessary. 56 | 57 | The default installation path is `C:\Program Files\mkpsxiso\bin` on Windows or `/usr/local/bin` on Linux. You can change it to any directory by passing `--install-prefix` to the first command. 58 | 59 | ## Issues 60 | 61 | The only known major issue that hasn't (or cannot) be resolved is that if you create a disc image with the following directory structure: 62 | 63 | ```xml 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | On Windows browsing the subdirectories in 'dirb' and 'dirc' will not list the contents for some reason and trying to access it in a command prompt leads to a permission denied or similar error message. Disc image tools such as CDmage will display the contents of the aforementioned subdirectories without issue and the issue persists with disc images created with BuildCD, suggesting it is likely a bug with the operating system and not mkpsxiso. 92 | 93 | This can be avoided by minimizing identically named directories but its best to test your generated disc image before considering it ready for release. 94 | 95 | 96 | ## Changelog 97 | 98 | **Version 2.03 (xx/xx/2023)** 99 | * On platforms where `std::filesystem` is unavailable, `ghc::filesystem` is now used instead. 100 | * Switched back to main `libflac`. 101 | * mkpsxiso: Resolved a possible crash when building images with big files. 102 | * mkpsxiso: Fixed an ISO generation bug when building images with directories with many files. 103 | * mkpsxiso: Removed a legacy `-nolimit` command line option. 104 | * dumpsxiso: Output a Copyright field to the XML. 105 | 106 | **Version 2.02 (06/24/2022)** 107 | * Fixed NOMINMAX redefinition warnings. 108 | * Added -c|--cuefile argument to specify the cue sheet file name. Overrides the name defined in the XML file. 109 | * Output file is derived from the XML project file name if no output file was specified by arguments or the image_name attribute. 110 | * Added -l|--label argument to specify the volume ID string. Overrides the volume ID defined in the XML file. 111 | * Added id_file attribute for the element. Reads identifiers from a separate XML file containing a single element. 112 | * Added ISO descriptor check in dumpsxiso. 113 | 114 | **Version 2.01 (02/10/2022)** 115 | * Fixed invalid sectors generated when no license file is specified. 116 | * Fixed wide char string specifier for MinGW. 117 | * Added a bunch of fflush() calls to make sure build messages are output at all times. 118 | * Improved help text. 119 | 120 | **Version 2.00 (02/02/2022)** 121 | * Added tinyxml2 as a submodule, so manual installation is no longer needed and built binaries will always be statically linked. 122 | * Add `dumpsxiso` the successor to `isodump`. Use `dumpsxiso` to unpack `.bin` isos and `mkpsxiso` to repack. 123 | * Make xml file paths relative to the XML. 124 | * Unify CDDA tracks and DA files to reflect the reality DA files are just links to CDDA tracks by LBA. 125 | * Add `` element to `` for specifying the pregap. `dumpsxiso` attempts to be intelligent about guessing the pregap. 126 | * Add `` element to specify non-file sectors in an ISO in order to preserve LBA order 127 | * Add packing flac, pcm, and mp3 files as CDDA tracks/DA files in addition to wav. `dumpsxiso` can extract as wav, flac, and pcm. 128 | * Add memory mapped ISO writing to improve packing speed 129 | * Fix directory records spanning more than 1 sector 130 | * dumpsxiso: add group by directory/pretty xml writing 131 | * Fix ECC and timestamp bugs 132 | * Massive refactor and cleanup, too many fixes and changes to list, see the commits for details 133 | 134 | **Version 1.27 (10/25/2021)** 135 | * Fixed stringop overflow bug when temporarily clearing sector address bytes. 136 | * Path is now stripped for the .bin file specification of cue sheets. 137 | 138 | **Version 1.25 (12/30/2020)** 139 | * Replaced xa and str modes with "mixed" mode (see example.xml for details). xa and str modes are now just aliases to the new "mixed" mode, for backwards compatibility. 140 | 141 | **Version 1.24 (12/28/2020)** 142 | * Fixed EDC generation for STR files. 143 | 144 | **Version 1.23 (12/20/2018)** 145 | * Fixed broken LBA and timecode calculation for audio tracks integrated as files (iso::DirTreeClass::GetWavSize returns an incorrect value from the WAV file). 146 | * Updated build instructions (CodeBlocks project had been replaced with Netbeans but forgot to update instructions). 147 | 148 | **Version 1.22 (12/4/2018)** 149 | * Fixed issues with subheader detection logic and made it as a warning instead of a critical error. 150 | * Fixed bug where CD-DA length of possibly being 1 sector larger than it should. 151 | 152 | **Version 1.21 (7/8/2018)** 153 | * Corrected volume size calculation logic when DA audio files are included. Also fixed volume size value being 2 sectors larger than it should. 154 | * Corrected LBA header file output where the 150 sector pregap offset is not needed (libcd already does that). 155 | * Fixed path name being double appended when using srcdir attribute (this made mkpsxiso unable to parse an xml document produced by mkisoxml). 156 | * Added -noxa option and no_xa XML attribute to disable CD-XA attribute creation (plain ISO9660). 157 | * Fixed quiet mode preventing cue_sheet attribute from being parsed. 158 | * Added RIFF header and XA/STR subheader checks to error on improperly ripped or encoded XA/STR source files. 159 | * Improved data sector detection for STR type files (Mode2/Form1 + Mode2/Form2 interleaves). 160 | * Implemented proper EOL/EOF bits for subheaders at the ends of descriptors and files. 161 | * Fixed an XML parser bug where name attribute is used as the source file name when a source directory in a directory element is specified. 162 | * Fixed a major bug where directory record lengths are always set at 2048 bytes even for directory records spanning more than a sector resulting in missing files and directories. 163 | * Fixed incorrectly calculated record length set in the volume descriptor's root directory record. 164 | * Added copyright XML attribute for the identifiers element to specify a copyright identifier for the image file. 165 | 166 | **Version 1.20 (6/21/2018)** 167 | * ISO names being blank or garbage when compiled using Microsoft's compiler has been fixed. 168 | * Replaced tinyxml2::GetErrorLineNum() method calls to tinyxml2::ErrorLineNum() for compatibility with the latest version of tinyxml2. 169 | * Fixed incorrect file size calculated for STR and DA audio files. 170 | * DA audio type files are now set with a CD-Audio attribute making them appear as WAV files when opening them (note: it does not turn the DA audio into a WAV file, the OS supporting said attribute transparently converts it into a WAV container as you read it, one game featuring such files is Wipeout 3 for example). 171 | * Brought back mkisoxml (just needed to be recompiled with the latest GCC). 172 | 173 | **Version 1.19 (6/12/2018)** 174 | * Path table generation logic significantly reworked. Previous implementation was flawed and caused issues on games and operating systems that utilize the path table. MKPSXISO should be truly ISO9660 compliant now (apologies for the misleading remark in the 1.15 changelog). 175 | * Date stamp is now set in the root directory record in the image descriptor. 176 | * Fixed specifying only a source attribute for a file element not behaving as intended. 177 | * Fixed no error on missing CUE file when DA audio type files are found but no cue sheet was specified. 178 | 179 | **Version 1.18 (5/16/2018)** 180 | * Added support for DA audio files (files that map directly to a CD Audio track which games that use CD Audio have). 181 | * XML load error message now reports useful error details. 182 | * Parameter errors in XML script now reports with line numbers. 183 | * Fixed corrupt file names displayed when packing files. 184 | * Fixed corrupt directory names and corrected minor formatting issues of generated LBA header files. 185 | * Improved formatting of generated LBA listings. 186 | * Can now accept raw audio files for CD audio tracks. However, it assumes any file that isn't a WAV file as raw audio. 187 | * Directory record entries now handled using std::vector arrays (possibly better stability). 188 | * Replaced char strings to std::string strings inside DirTreeClass and associated typedefs. 189 | * Replaced all strcasecmp() with custom compare() function to make code fully C++14 compatible. 190 | * Included Netbeans 8.1 project. 191 | * Removed mkisoxml from download temporarily (currently too buggy, source still available). 192 | 193 | **Version 1.15 (6/16/2017)** 194 | * Directory record lengths have been tweaked to now calculate in sector multiples instead of actual bytes. This now makes ISOs generated with MKPSXISO fully ISO9660 compliant and ISO tools that threw a fit when opening ISO images generated with older versions of MKPSXISO should no longer complain. 195 | * Improved XA attribute header encoding (not really necessary but still nice to have). 196 | * Re-done README text. 197 | 198 | **Version 1.14 (6/4/2017, BIG update because I forgot to release 1.12)** 199 | * Name attribute of file entries can now be omitted provided that the source attribute is at least present. 200 | * Changed some char* strings to std::string strings. 201 | * Fixed typo in help text where -lba was referenced as -lbalist. 202 | * Fixed system and application identifiers not defaulting to PLAYSTATION when blank. 203 | * Fixed bug where mkpsxiso sometimes reports an error of specifying -o on a multi-disc ISO project when the project only has one ISO project. 204 | * Fixed a major LBA calculation bug where files written past a directory record spanning more than one sector are not assigned to an LBA address where it can safely be written. This resulted to the file getting overwritten by the directory record and thus, leads to file corruption. 205 | * Fixed file overwrite confirmation bug still being prompted when -noisogen is specified. 206 | * Fixed a bug where the length of directory records is always written with a length of 2048 bytes which may have caused some ISO browsing programs to throw a fit when browsing the generated ISO images. 207 | * Changed the format of the file LBA log completely which no longer uses a tree style format but it outputs more information of the file entries. 208 | * LBA values in generated header files are now offset by 150 sectors to take into account that sectors actually begin past 150 sectors. This may have caused direct LBA seeks to point 150 sectors behind the actual LBA on the CD. 209 | 210 | **Version 1.10 (2/23/2017)** 211 | * Can now handle as many files/directories the ISO9660 filesystem can handle without crashing or reporting an error as both a new feature and a fix for MeganGrass. 212 | * 2 second pregaps are now generated for CD audio tracks 3 and onwards in ISO projects that contain CD Audio. 213 | * Added -noisogen to disable iso generation useful for generating only an LBA listing of files in the ISO project in plain text or C header format. 214 | * Added -lbahead which is similar to -lba but outputs in C header format. 215 | * Fixed a bug where mkpsxiso would produce a 0 byte image file when an error occurs while using the -o parameter to specify the output image file. 216 | * Added optional srcdir attribute for -directory_tree- and -dir- elements (mostly intended for the new mkisoxml tool). 217 | * Added crude mkisoxml utility for quickly generating basic ISO XML projects out of a directory (can be compiled with CMake). 218 | 219 | **Version 1.06 (11/28/2016)** 220 | * Added file overwrite confirmation. 221 | * Element and attribute strings are no longer case sensitive. 222 | * File name strings from arguments are no longer passed as duplicated strings (eliminates the risk of forgetting to dealloc said strings on exit). 223 | * Added -lba option to produce a complete listing of files and directories in the ISO file system with their LBA addresses. 224 | * Added -dummy- element example in example.xml (forgot to put it in since the first release). 225 | 226 | **Version 1.05 (by electroCupcake)** 227 | * Fixed types for linux build, changed u_char and such to unsigned char. 228 | * In cygwin64 version of tinyxml2, "XML_NO_ERROR" is not defined, changed with "XML_SUCCESS" and this works on both Windows and Linux. 229 | * Converted to cmake, if you want a codeblocks project file, just run "cmake . -G "CodeBlocks - Unix Makefiles"" to create it. 230 | 231 | **Version 1.04 (9/1/2016)** 232 | * Fixed a bug where you'll get a 'Data track must only be on first track' error when creating an ISO image with more than 2 tracks even when the extra tracks are CD audio. 233 | * Duplicate file and directory entries are no longer possible to add (it'll result to weird problems anyway). 234 | 235 | **Version 1.00 (8/6/2016)** 236 | * Initial release. 237 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /src/mkpsxiso/iso.cpp: -------------------------------------------------------------------------------- 1 | #include "global.h" 2 | #include "iso.h" 3 | #include "common.h" 4 | #include "cd.h" 5 | #include "xa.h" 6 | #include "platform.h" 7 | #include "miniaudio_helpers.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | char rootname[] = { "" }; 15 | 16 | static bool icompare_func(unsigned char a, unsigned char b) 17 | { 18 | return std::tolower( a ) == std::tolower( b ); 19 | } 20 | 21 | static bool icompare(const std::string& a, const std::string& b) 22 | { 23 | if ( a.length() == b.length() ) 24 | { 25 | return std::equal( b.begin(), b.end(), a.begin(), icompare_func ); 26 | } 27 | else 28 | { 29 | return false; 30 | } 31 | } 32 | 33 | static size_t GetIDLength(std::string_view id) 34 | { 35 | size_t length = std::max(1, id.length()); 36 | length += length % 2; 37 | return length; 38 | } 39 | 40 | static cd::ISO_DATESTAMP GetISODateStamp(time_t time, signed char GMToffs) 41 | { 42 | // GMToffs is specified in 15 minute units 43 | const time_t GMToffsSeconds = static_cast(15) * 60 * GMToffs; 44 | 45 | time += GMToffsSeconds; 46 | const tm timestamp = *gmtime( &time ); 47 | 48 | cd::ISO_DATESTAMP result; 49 | result.hour = timestamp.tm_hour; 50 | result.minute = timestamp.tm_min; 51 | result.second = timestamp.tm_sec; 52 | result.month = timestamp.tm_mon+1; 53 | result.day = timestamp.tm_mday; 54 | result.year = timestamp.tm_year; 55 | result.GMToffs = GMToffs; 56 | 57 | return result; 58 | } 59 | 60 | template 61 | static T RoundToEven(T val) 62 | { 63 | return (val + 1) & -2; 64 | } 65 | 66 | int iso::DirTreeClass::GetAudioSize(const fs::path& audioFile) 67 | { 68 | ma_decoder decoder; 69 | VirtualWavEx vw; 70 | bool isLossy; 71 | if(ma_redbook_decoder_init_path_by_ext(audioFile, &decoder, &vw, isLossy) != MA_SUCCESS) 72 | { 73 | return 0; 74 | } 75 | 76 | const ma_uint64 expectedPCMFrames = ma_decoder_get_length_in_pcm_frames(&decoder); 77 | ma_decoder_uninit(&decoder); 78 | if(expectedPCMFrames == 0) 79 | { 80 | printf("\n ERROR: corrupt file? unable to get_length_in_pcm_frames\n"); 81 | return 0; 82 | } 83 | 84 | return GetSizeInSectors(expectedPCMFrames * 2 * (sizeof(int16_t)), CD_SECTOR_SIZE)*CD_SECTOR_SIZE; 85 | } 86 | 87 | iso::DirTreeClass::DirTreeClass(EntryList& entries, DirTreeClass* parent) 88 | : name(rootname), entries(entries), parent(parent) 89 | { 90 | } 91 | 92 | iso::DirTreeClass::~DirTreeClass() 93 | { 94 | } 95 | 96 | iso::DIRENTRY& iso::DirTreeClass::CreateRootDirectory(EntryList& entries, const cd::ISO_DATESTAMP& volumeDate) 97 | { 98 | DIRENTRY entry {}; 99 | 100 | entry.type = EntryType::EntryDir; 101 | entry.subdir = std::make_unique(entries); 102 | entry.date = volumeDate; 103 | entry.length = 0; // Length is meaningless for directories 104 | 105 | const EntryAttributes attributes; // Leave defaults 106 | entry.attribs = attributes.XAAttrib; 107 | entry.perms = attributes.XAPerm; 108 | entry.GID = attributes.GID; 109 | entry.UID = attributes.UID; 110 | 111 | entries.emplace_back( std::move(entry) ); 112 | 113 | return entries.back(); 114 | } 115 | 116 | bool iso::DirTreeClass::AddFileEntry(const char* id, EntryType type, const fs::path& srcfile, const EntryAttributes& attributes, const char *trackid) 117 | { 118 | auto fileAttrib = Stat(srcfile); 119 | if ( !fileAttrib ) 120 | { 121 | if ( !global::QuietMode ) 122 | { 123 | printf(" "); 124 | } 125 | 126 | printf("ERROR: File not found: %" PRFILESYSTEM_PATH "\n", srcfile.lexically_normal().c_str()); 127 | return false; 128 | } 129 | 130 | // Check if XA data is valid 131 | if ( type == EntryType::EntryXA ) 132 | { 133 | // Check header 134 | bool validHeader = false; 135 | FILE* fp = OpenFile(srcfile, "rb"); 136 | if (fp != nullptr) 137 | { 138 | char buff[4]; 139 | if (fread(buff, 1, std::size(buff), fp) == std::size(buff)) 140 | { 141 | validHeader = strncmp(buff, "RIFF", std::size(buff)) != 0; 142 | } 143 | fclose(fp); 144 | } 145 | 146 | // Check if its a RIFF (WAV container) 147 | if (!validHeader) 148 | { 149 | if (!global::QuietMode) 150 | { 151 | printf(" "); 152 | } 153 | 154 | printf("ERROR: %" PRFILESYSTEM_PATH " is a WAV or is not properly ripped.\n", srcfile.lexically_normal().c_str()); 155 | 156 | return false; 157 | } 158 | 159 | // Check if size is a multiple of 2336 bytes 160 | if ( ( fileAttrib->st_size % 2336 ) != 0 ) 161 | { 162 | if ( ( fileAttrib->st_size % 2048) == 0 ) 163 | { 164 | type = EntryType::EntryXA_DO; 165 | } 166 | else 167 | { 168 | if ( !global::QuietMode ) 169 | { 170 | printf(" "); 171 | } 172 | 173 | printf("ERROR: %" PRFILESYSTEM_PATH " is not a multiple of 2336 or 2048 bytes.\n", 174 | srcfile.lexically_normal().c_str()); 175 | 176 | return false; 177 | } 178 | } 179 | 180 | } 181 | 182 | 183 | std::string temp_name = id; 184 | for ( char& ch : temp_name ) 185 | { 186 | ch = std::toupper( ch ); 187 | } 188 | 189 | temp_name += ";1"; 190 | 191 | 192 | // Check if file entry already exists 193 | for ( const auto& e : entriesInDir ) 194 | { 195 | const DIRENTRY& entry = e.get(); 196 | if ( !entry.id.empty() ) 197 | { 198 | if ( ( entry.type == EntryType::EntryFile ) 199 | && ( icompare( entry.id, temp_name ) ) ) 200 | { 201 | if (!global::QuietMode) 202 | { 203 | printf(" "); 204 | } 205 | 206 | printf("ERROR: Duplicate file entry: %s\n", id); 207 | 208 | return false; 209 | } 210 | 211 | } 212 | 213 | } 214 | 215 | DIRENTRY entry {}; 216 | 217 | entry.id = std::move(temp_name); 218 | entry.type = type; 219 | entry.subdir = nullptr; 220 | entry.attribs = attributes.XAAttrib; 221 | entry.perms = attributes.XAPerm; 222 | entry.GID = attributes.GID; 223 | entry.UID = attributes.UID; 224 | 225 | if ( !srcfile.empty() ) 226 | { 227 | entry.srcfile = srcfile; 228 | } 229 | 230 | if ( type == EntryType::EntryDA ) 231 | { 232 | entry.length = GetAudioSize( srcfile ); 233 | if(trackid == nullptr) 234 | { 235 | printf("ERROR: no trackid for DA track\n"); 236 | return false; 237 | } 238 | entry.trackid = trackid; 239 | } 240 | else if ( type != EntryType::EntryDir ) 241 | { 242 | entry.length = fileAttrib->st_size; 243 | } 244 | 245 | entry.date = GetISODateStamp( fileAttrib->st_mtime, attributes.GMTOffs ); 246 | 247 | entries.emplace_back(std::move(entry)); 248 | entriesInDir.emplace_back(entries.back()); 249 | 250 | return true; 251 | 252 | } 253 | 254 | void iso::DirTreeClass::AddDummyEntry(int sectors, int type) 255 | { 256 | DIRENTRY entry {}; 257 | 258 | // TODO: HUGE HACK, will be removed once EntryDummy is unified with EntryFile again 259 | entry.perms = type; 260 | entry.type = EntryType::EntryDummy; 261 | entry.length = 2048*sectors; 262 | 263 | entries.emplace_back(std::move(entry)); 264 | entriesInDir.emplace_back(entries.back()); 265 | } 266 | 267 | iso::DirTreeClass* iso::DirTreeClass::AddSubDirEntry(const char* id, const fs::path& srcDir, const EntryAttributes& attributes, bool& alreadyExists) 268 | { 269 | // Duplicate directory entries are allowed, but the subsequent occurences will not add 270 | // a new directory to 'entries'. 271 | // TODO: It's not possible now, but a warning should be issued if entry attributes are specified for the subsequent occurences 272 | // of the directory. This check probably needs to be moved outside of the function. 273 | for(auto& e : entriesInDir) 274 | { 275 | const iso::DIRENTRY& entry = e.get(); 276 | if((entry.type == EntryType::EntryDir) && (entry.id == id)) 277 | { 278 | alreadyExists = true; 279 | return entry.subdir.get(); 280 | } 281 | } 282 | 283 | auto fileAttrib = Stat(srcDir); 284 | time_t dirTime; 285 | if ( fileAttrib ) 286 | { 287 | dirTime = fileAttrib->st_mtime; 288 | } 289 | else 290 | { 291 | dirTime = global::BuildTime; 292 | 293 | if ( id != nullptr ) 294 | { 295 | if ( !global::QuietMode ) 296 | { 297 | printf( "\n " ); 298 | } 299 | 300 | printf( "WARNING: 'source' attribute for subdirectory '%s' is invalid or empty.\n", id ); 301 | } 302 | } 303 | 304 | DIRENTRY entry {}; 305 | 306 | if (id != nullptr) 307 | { 308 | entry.id = id; 309 | for ( char& ch : entry.id ) 310 | { 311 | ch = toupper( ch ); 312 | } 313 | } 314 | 315 | entry.type = EntryType::EntryDir; 316 | entry.subdir = std::make_unique(entries, this); 317 | entry.attribs = attributes.XAAttrib; 318 | entry.perms = attributes.XAPerm; 319 | entry.GID = attributes.GID; 320 | entry.UID = attributes.UID; 321 | entry.date = GetISODateStamp( dirTime, attributes.GMTOffs ); 322 | entry.length = 0; // Length is meaningless for directories 323 | 324 | entries.emplace_back(std::move(entry)); 325 | entriesInDir.emplace_back(entries.back()); 326 | 327 | return entries.back().subdir.get(); 328 | } 329 | 330 | void iso::DirTreeClass::PrintRecordPath() 331 | { 332 | if ( parent == nullptr ) 333 | { 334 | return; 335 | } 336 | 337 | parent->PrintRecordPath(); 338 | printf( "/%s", name.c_str() ); 339 | } 340 | 341 | int iso::DirTreeClass::CalculateTreeLBA(int lba) 342 | { 343 | bool firstDAWritten = false; 344 | for ( DIRENTRY& entry : entries ) 345 | { 346 | // Set current LBA to directory record entry 347 | entry.lba = lba; 348 | 349 | // If it is a subdir 350 | if (entry.subdir != nullptr) 351 | { 352 | if (!entry.id.empty()) 353 | { 354 | entry.subdir->name = entry.id; 355 | } 356 | 357 | lba += GetSizeInSectors(entry.subdir->CalculateDirEntryLen()); 358 | } 359 | else 360 | { 361 | // Increment LBA by the size of file 362 | if ( entry.type == EntryType::EntryFile || entry.type == EntryType::EntryXA_DO || entry.type == EntryType::EntryDummy ) 363 | { 364 | lba += GetSizeInSectors(entry.length, 2048); 365 | } 366 | else if ( entry.type == EntryType::EntryXA ) 367 | { 368 | lba += GetSizeInSectors(entry.length, 2336); 369 | } 370 | else if ( entry.type == EntryType::EntryDA ) 371 | { 372 | // DA files don't take up any space in the ISO filesystem, they are just links to CD tracks 373 | entry.lba = iso::DA_FILE_PLACEHOLDER_LBA; // we will write the lba into the filesystem when writing the CDDA track 374 | } 375 | } 376 | } 377 | 378 | return lba; 379 | } 380 | 381 | int iso::DirTreeClass::CalculateDirEntryLen() const 382 | { 383 | int dirEntryLen = 68; 384 | 385 | if ( !global::noXA ) 386 | { 387 | dirEntryLen += 28; 388 | } 389 | 390 | for ( const auto& e : entriesInDir ) 391 | { 392 | const DIRENTRY& entry = e.get(); 393 | if ( entry.id.empty() ) 394 | { 395 | continue; 396 | } 397 | 398 | int dataLen = sizeof(cd::ISO_DIR_ENTRY); 399 | 400 | dataLen += GetIDLength(entry.id); 401 | 402 | if ( !global::noXA ) 403 | { 404 | dataLen = RoundToEven(dataLen); 405 | dataLen += sizeof( cdxa::ISO_XA_ATTRIB ); 406 | } 407 | 408 | if ( ((dirEntryLen%2048)+dataLen) > 2048 ) 409 | { 410 | // Round dirEntryLen to the nearest multiple of 2048 as the rest of that sector is "unusable" 411 | dirEntryLen = ((dirEntryLen + 2047) / 2048) * 2048; 412 | } 413 | 414 | dirEntryLen += dataLen; 415 | } 416 | 417 | return 2048 * GetSizeInSectors(dirEntryLen); 418 | } 419 | 420 | void iso::DirTreeClass::SortDirectoryEntries() 421 | { 422 | // Search for directories 423 | for ( const auto& e : entriesInDir ) 424 | { 425 | const DIRENTRY& entry = e.get(); 426 | if ( entry.type == EntryType::EntryDir ) 427 | { 428 | // Perform recursive call 429 | if ( entry.subdir != nullptr ) 430 | { 431 | entry.subdir->SortDirectoryEntries(); 432 | } 433 | } 434 | } 435 | 436 | std::sort(entriesInDir.begin(), entriesInDir.end(), [](const auto& left, const auto& right) 437 | { 438 | return left.get().id < right.get().id; 439 | }); 440 | } 441 | 442 | bool iso::DirTreeClass::WriteDirEntries(cd::IsoWriter* writer, const DIRENTRY& dir, const DIRENTRY& parentDir) const 443 | { 444 | //char dataBuff[2048] {}; 445 | //char* dataBuffPtr=dataBuff; 446 | //char entryBuff[128]; 447 | //int dirlen; 448 | 449 | auto sectorView = writer->GetSectorViewM2F1(dir.lba, GetSizeInSectors(CalculateDirEntryLen()), cd::IsoWriter::EdcEccForm::Form1); 450 | 451 | //writer->SeekToSector( dir.lba ); 452 | 453 | auto writeOneEntry = [§orView](const DIRENTRY& entry, std::optional currentOrParent = std::nullopt) -> void 454 | { 455 | std::byte buffer[128] {}; 456 | 457 | auto dirEntry = reinterpret_cast(buffer); 458 | int entryLength = sizeof(*dirEntry); 459 | 460 | if ( entry.type == EntryType::EntryDir ) 461 | { 462 | dirEntry->flags = 0x02; 463 | } 464 | else 465 | { 466 | dirEntry->flags = 0x00; 467 | } 468 | 469 | // File length correction for certain file types 470 | int length; 471 | 472 | if ( entry.type == EntryType::EntryXA ) 473 | { 474 | length = 2048* GetSizeInSectors(entry.length, 2336); 475 | } 476 | else if ( entry.type == EntryType::EntryXA_DO ) 477 | { 478 | length = 2048 * GetSizeInSectors(entry.length, 2048); 479 | } 480 | else if ( entry.type == EntryType::EntryDA ) 481 | { 482 | if(entry.lba == iso::DA_FILE_PLACEHOLDER_LBA) 483 | { 484 | printf("ERROR: DA file still has placeholder value 0x%X\n", iso::DA_FILE_PLACEHOLDER_LBA); 485 | } 486 | length = 2048 * GetSizeInSectors(entry.length, 2352); 487 | } 488 | else if (entry.type == EntryType::EntryDir) 489 | { 490 | length = entry.subdir->CalculateDirEntryLen(); 491 | } 492 | else 493 | { 494 | length = entry.length; 495 | } 496 | 497 | dirEntry->entryOffs = cd::SetPair32( entry.lba ); 498 | dirEntry->entrySize = cd::SetPair32( length ); 499 | dirEntry->volSeqNum = cd::SetPair16( 1 ); 500 | dirEntry->entryDate = entry.date; 501 | 502 | // Normal case - write out the identifier 503 | char* identifierBuffer = reinterpret_cast(dirEntry+1); 504 | if (!currentOrParent.has_value()) 505 | { 506 | dirEntry->identifierLen = entry.id.length(); 507 | strncpy(identifierBuffer, entry.id.c_str(), entry.id.length()); 508 | } 509 | else 510 | { 511 | // Special cases - current/parent directory entry 512 | dirEntry->identifierLen = 1; 513 | identifierBuffer[0] = currentOrParent.value() ? '\1' : '\0'; 514 | } 515 | entryLength += dirEntry->identifierLen; 516 | 517 | if ( !global::noXA ) 518 | { 519 | entryLength = RoundToEven(entryLength); 520 | auto xa = reinterpret_cast(buffer+entryLength); 521 | 522 | xa->id[0] = 'X'; 523 | xa->id[1] = 'A'; 524 | 525 | unsigned short attributes = entry.perms; 526 | if ( (entry.type == EntryType::EntryFile) || 527 | (entry.type == EntryType::EntryXA_DO) || 528 | (entry.type == EntryType::EntryDummy) ) 529 | { 530 | attributes |= 0x800; 531 | } 532 | else if (entry.type == EntryType::EntryDA) 533 | { 534 | attributes |= 0x4000; 535 | } 536 | else if (entry.type == EntryType::EntryXA) 537 | { 538 | attributes |= entry.attribs != 0xFFu ? (entry.attribs << 8) : 0x3800; 539 | xa->filenum = 1; 540 | } 541 | else if (entry.type == EntryType::EntryDir) 542 | { 543 | attributes |= 0x8800; 544 | } 545 | 546 | if (entry.type == EntryType::EntryXA) 547 | { 548 | xa->filenum = 1; 549 | } 550 | 551 | xa->attributes = SwapBytes16(attributes); 552 | xa->ownergroupid = SwapBytes16(entry.GID); 553 | xa->owneruserid = SwapBytes16(entry.UID); 554 | 555 | entryLength += sizeof(*xa); 556 | } 557 | 558 | dirEntry->entryLength = entryLength; 559 | 560 | if (sectorView->GetSpaceInCurrentSector() < entryLength) 561 | { 562 | sectorView->NextSector(); 563 | } 564 | sectorView->WriteMemory(buffer, entryLength); 565 | }; 566 | 567 | writeOneEntry(dir, false); 568 | writeOneEntry(parentDir, true); 569 | 570 | for ( const auto& e : entriesInDir ) 571 | { 572 | const DIRENTRY& entry = e.get(); 573 | if ( entry.id.empty() ) 574 | { 575 | continue; 576 | } 577 | 578 | writeOneEntry(e); 579 | } 580 | 581 | return true; 582 | } 583 | 584 | bool iso::DirTreeClass::WriteDirectoryRecords(cd::IsoWriter* writer, const DIRENTRY& dir, const DIRENTRY& parentDir) 585 | { 586 | if(!WriteDirEntries( writer, dir, parentDir )) 587 | { 588 | return false; 589 | } 590 | 591 | for ( const auto& e : entriesInDir ) 592 | { 593 | const DIRENTRY& entry = e.get(); 594 | if ( entry.type == EntryType::EntryDir ) 595 | { 596 | if ( !entry.subdir->WriteDirectoryRecords(writer, entry, dir) ) 597 | { 598 | return false; 599 | } 600 | } 601 | } 602 | 603 | return true; 604 | } 605 | 606 | bool iso::DirTreeClass::WriteFiles(cd::IsoWriter* writer) const 607 | { 608 | for ( const DIRENTRY& entry : entries ) 609 | { 610 | // Write files as regular data sectors 611 | if ( entry.type == EntryType::EntryFile ) 612 | { 613 | if ( !entry.srcfile.empty() ) 614 | { 615 | if ( !global::QuietMode ) 616 | { 617 | printf( " Packing %" PRFILESYSTEM_PATH "... ", entry.srcfile.lexically_normal().c_str() ); 618 | } 619 | 620 | FILE *fp = OpenFile( entry.srcfile, "rb" ); 621 | if (fp != nullptr) 622 | { 623 | auto sectorView = writer->GetSectorViewM2F1(entry.lba, GetSizeInSectors(entry.length), cd::IsoWriter::EdcEccForm::Form1); 624 | sectorView->WriteFile(fp); 625 | 626 | fclose(fp); 627 | } 628 | 629 | if ( !global::QuietMode ) 630 | { 631 | printf("Done.\n"); 632 | } 633 | 634 | } 635 | 636 | // Write XA/STR video streams as Mode 2 Form 1 (video sectors) and Mode 2 Form 2 (XA audio sectors) 637 | // Video sectors have EDC/ECC while XA does not 638 | } 639 | else if ( entry.type == EntryType::EntryXA ) 640 | { 641 | if ( !global::QuietMode ) 642 | { 643 | printf( " Packing XA %" PRFILESYSTEM_PATH "... ", entry.srcfile.lexically_normal().c_str() ); 644 | } 645 | 646 | FILE *fp = OpenFile( entry.srcfile, "rb" ); 647 | if (fp != nullptr) 648 | { 649 | auto sectorView = writer->GetSectorViewM2F2(entry.lba, GetSizeInSectors(entry.length, 2336), cd::IsoWriter::EdcEccForm::Autodetect); 650 | sectorView->WriteFile(fp); 651 | 652 | fclose( fp ); 653 | } 654 | 655 | if (!global::QuietMode) 656 | { 657 | printf( "Done.\n" ); 658 | } 659 | 660 | // Write data only STR streams as Mode 2 Form 1 661 | } 662 | else if ( entry.type == EntryType::EntryXA_DO ) 663 | { 664 | if ( !entry.srcfile.empty() ) 665 | { 666 | if ( !global::QuietMode ) 667 | { 668 | printf( " Packing XA-DO %" PRFILESYSTEM_PATH "... ", entry.srcfile.lexically_normal().c_str() ); 669 | } 670 | 671 | FILE *fp = OpenFile( entry.srcfile, "rb" ); 672 | if (fp != nullptr) 673 | { 674 | auto sectorView = writer->GetSectorViewM2F1(entry.lba, GetSizeInSectors(entry.length), cd::IsoWriter::EdcEccForm::Form1); 675 | sectorView->SetSubheader(cd::IsoWriter::SubSTR); 676 | sectorView->WriteFile(fp); 677 | 678 | fclose(fp); 679 | } 680 | 681 | if ( !global::QuietMode ) 682 | { 683 | printf("Done.\n"); 684 | } 685 | 686 | } 687 | // Write DA files as audio tracks 688 | } 689 | else if ( entry.type == EntryType::EntryDA ) 690 | { 691 | continue; 692 | } 693 | // Write dummies as gaps without data 694 | else if ( entry.type == EntryType::EntryDummy ) 695 | { 696 | // TODO: HUGE HACK, will be removed once EntryDummy is unified with EntryFile again 697 | const bool isForm2 = entry.perms != 0; 698 | 699 | const uint32_t sizeInSectors = GetSizeInSectors(entry.length); 700 | auto sectorView = writer->GetSectorViewM2F1(entry.lba, sizeInSectors, isForm2 ? cd::IsoWriter::EdcEccForm::Form2 : cd::IsoWriter::EdcEccForm::Form1); 701 | 702 | sectorView->WriteBlankSectors(sizeInSectors); 703 | } 704 | } 705 | 706 | return true; 707 | } 708 | 709 | void iso::DirTreeClass::OutputHeaderListing(FILE* fp, int level) const 710 | { 711 | if ( level == 0 ) 712 | { 713 | fprintf( fp, "#ifndef _ISO_FILES\n" ); 714 | fprintf( fp, "#define _ISO_FILES\n\n" ); 715 | } 716 | 717 | fprintf( fp, "/* %s */\n", name.c_str() ); 718 | 719 | for ( const auto& e : entriesInDir ) 720 | { 721 | const DIRENTRY& entry = e.get(); 722 | if ( !entry.id.empty() && entry.type != EntryType::EntryDir ) 723 | { 724 | std::string temp_name = "LBA_" + entry.id; 725 | 726 | for ( char& ch : temp_name ) 727 | { 728 | ch = std::toupper( ch ); 729 | 730 | if ( ch == '.' ) 731 | { 732 | ch = '_'; 733 | } 734 | 735 | if ( ch == ';' ) 736 | { 737 | ch = '\0'; 738 | break; 739 | } 740 | } 741 | 742 | fprintf( fp, "#define %-17s %d\n", temp_name.c_str(), entry.lba ); 743 | } 744 | } 745 | 746 | for ( const auto& e : entriesInDir ) 747 | { 748 | const DIRENTRY& entry = e.get(); 749 | if ( entry.type == EntryType::EntryDir ) 750 | { 751 | fprintf( fp, "\n" ); 752 | entry.subdir->OutputHeaderListing( fp, level+1 ); 753 | } 754 | } 755 | 756 | if ( level == 0 ) 757 | { 758 | fprintf( fp, "\n#endif\n" ); 759 | } 760 | } 761 | 762 | void iso::DirTreeClass::OutputLBAlisting(FILE* fp, int level) const 763 | { 764 | for ( const auto& e : entriesInDir ) 765 | { 766 | const DIRENTRY& entry = e.get(); 767 | fprintf( fp, " " ); 768 | 769 | if ( !entry.id.empty() ) 770 | { 771 | if ( entry.type == EntryType::EntryFile ) 772 | { 773 | fprintf( fp, "File " ); 774 | } 775 | else if ( entry.type == EntryType::EntryDir ) 776 | { 777 | fprintf( fp, "Dir " ); 778 | } 779 | else if ( ( entry.type == EntryType::EntryXA ) || 780 | ( entry.type == EntryType::EntryXA_DO ) ) 781 | { 782 | fprintf( fp, "XA " ); 783 | } 784 | else if ( entry.type == EntryType::EntryDA ) 785 | { 786 | fprintf( fp, "CDDA " ); 787 | } 788 | 789 | fprintf( fp, "%-17s", entry.id.c_str() ); 790 | } else { 791 | 792 | fprintf( fp, "Dummy " ); 793 | 794 | } 795 | 796 | // Write size in sector units 797 | if (entry.type != EntryType::EntryDir) 798 | { 799 | fprintf( fp, "%-10" PRIu32, GetSizeInSectors(entry.length) ); 800 | } 801 | else 802 | { 803 | fprintf( fp, "%-10s", "" ); 804 | } 805 | 806 | // Write LBA offset 807 | fprintf( fp, "%-10d", entry.lba ); 808 | 809 | // Write Timecode 810 | fprintf( fp, "%-12s", SectorsToTimecode(entry.lba).c_str()); 811 | 812 | // Write size in byte units 813 | if (entry.type != EntryType::EntryDir) 814 | { 815 | fprintf( fp, "%-10" PRId64, entry.length ); 816 | } 817 | else 818 | { 819 | fprintf( fp, "%-10s", "" ); 820 | } 821 | 822 | // Write source file path 823 | if ( (!entry.id.empty()) && (entry.type != EntryType::EntryDir) ) 824 | { 825 | fprintf( fp, "%" PRFILESYSTEM_PATH, entry.srcfile.lexically_normal().c_str() ); 826 | } 827 | fprintf( fp, "\n" ); 828 | 829 | if ( entry.type == EntryType::EntryDir ) 830 | { 831 | entry.subdir->OutputLBAlisting( fp, level+1 ); 832 | } 833 | } 834 | 835 | if ( level > 0 ) 836 | { 837 | fprintf( fp, " End %s\n", name.c_str() ); 838 | } 839 | } 840 | 841 | 842 | int iso::DirTreeClass::CalculatePathTableLen(const DIRENTRY& dirEntry) const 843 | { 844 | // Put identifier (empty if first entry) 845 | int len = 8 + GetIDLength(dirEntry.id); 846 | 847 | for ( const auto& e : entriesInDir ) 848 | { 849 | const DIRENTRY& entry = e.get(); 850 | if ( entry.type == EntryType::EntryDir ) 851 | { 852 | len += entry.subdir->CalculatePathTableLen( entry ); 853 | } 854 | } 855 | 856 | return len; 857 | } 858 | 859 | std::unique_ptr iso::DirTreeClass::GenPathTableSub(unsigned short& index, const unsigned short parentIndex) const 860 | { 861 | auto table = std::make_unique(); 862 | for ( const auto& e : entriesInDir ) 863 | { 864 | const DIRENTRY& entry = e.get(); 865 | if ( entry.type == EntryType::EntryDir ) 866 | { 867 | PathEntryClass pathEntry; 868 | 869 | pathEntry.dir_id = entry.id; 870 | pathEntry.dir_index = index++; 871 | pathEntry.dir_parent_index = parentIndex; 872 | pathEntry.dir_lba = entry.lba; 873 | table->entries.emplace_back( std::move(pathEntry) ); 874 | } 875 | } 876 | 877 | size_t entryID = 0; 878 | for ( const auto& e : entriesInDir ) 879 | { 880 | const DIRENTRY& entry = e.get(); 881 | if ( entry.type == EntryType::EntryDir ) 882 | { 883 | auto& pathEntry = table->entries[entryID++]; 884 | auto sub = entry.subdir->GenPathTableSub(index, pathEntry.dir_index); 885 | if (!sub->entries.empty()) 886 | { 887 | pathEntry.sub = std::move(sub); 888 | } 889 | } 890 | } 891 | return table; 892 | } 893 | 894 | int iso::DirTreeClass::GeneratePathTable(const DIRENTRY& root, unsigned char* buff, bool msb) const 895 | { 896 | unsigned short index = 1; 897 | 898 | // Write out root explicitly (since there is no DirTreeClass including it) 899 | PathEntryClass rootEntry; 900 | rootEntry.dir_id = root.id; 901 | rootEntry.dir_parent_index = index; 902 | rootEntry.dir_index = index++; 903 | rootEntry.dir_lba = root.lba; 904 | rootEntry.sub = GenPathTableSub(index, rootEntry.dir_parent_index); 905 | 906 | PathTableClass pathTable; 907 | pathTable.entries.emplace_back(std::move(rootEntry)); 908 | 909 | unsigned char* newbuff = pathTable.GenTableData( buff, msb ); 910 | 911 | return (int)(newbuff-buff); 912 | 913 | } 914 | 915 | int iso::DirTreeClass::GetFileCountTotal() const 916 | { 917 | int numfiles = 0; 918 | 919 | for ( const auto& e : entriesInDir ) 920 | { 921 | const DIRENTRY& entry = e.get(); 922 | if ( entry.type != EntryType::EntryDir ) 923 | { 924 | if ( !entry.id.empty() ) 925 | { 926 | numfiles++; 927 | } 928 | } 929 | else 930 | { 931 | numfiles += entry.subdir->GetFileCountTotal(); 932 | } 933 | } 934 | 935 | return numfiles; 936 | } 937 | 938 | int iso::DirTreeClass::GetDirCountTotal() const 939 | { 940 | int numdirs = 0; 941 | 942 | for ( const auto& e : entriesInDir ) 943 | { 944 | const DIRENTRY& entry = e.get(); 945 | if ( entry.type == EntryType::EntryDir ) 946 | { 947 | numdirs++; 948 | numdirs += entry.subdir->GetDirCountTotal(); 949 | } 950 | } 951 | 952 | return numdirs; 953 | } 954 | 955 | void iso::WriteLicenseData(cd::IsoWriter* writer, void* data) 956 | { 957 | auto licenseSectors = writer->GetSectorViewM2F2(0, 12, cd::IsoWriter::EdcEccForm::Form1); 958 | licenseSectors->WriteMemory(data, 2336 * 12); 959 | 960 | auto licenseBlankSectors = writer->GetSectorViewM2F1(12, 4, cd::IsoWriter::EdcEccForm::Form2); 961 | licenseBlankSectors->WriteBlankSectors(4); 962 | } 963 | 964 | template 965 | static void CopyStringPadWithSpaces(char (&dest)[N], const char* src) 966 | { 967 | auto begin = std::begin(dest); 968 | auto end = std::end(dest); 969 | 970 | if ( src != nullptr ) 971 | { 972 | size_t i = 0; 973 | const size_t len = strlen(src); 974 | for (; begin != end && i < len; ++begin, ++i) 975 | { 976 | *begin = std::toupper( src[i] ); 977 | } 978 | } 979 | 980 | // Pad the remaining space with spaces 981 | std::fill( begin, end, ' ' ); 982 | } 983 | 984 | static cd::ISO_LONG_DATESTAMP GetLongDateFromString(const char* str) 985 | { 986 | cd::ISO_LONG_DATESTAMP date; 987 | 988 | bool gotDate = false; 989 | if (str) 990 | { 991 | cd::ISO_DATESTAMP shortDate = GetDateFromString(str, &gotDate); 992 | if (gotDate) 993 | { 994 | date = GetLongDateFromDate(shortDate); 995 | } 996 | } 997 | 998 | if (!gotDate) 999 | { 1000 | date = GetUnspecifiedLongDate(); 1001 | } 1002 | return date; 1003 | } 1004 | 1005 | void iso::WriteDescriptor(cd::IsoWriter* writer, const iso::IDENTIFIERS& id, const DIRENTRY& root, int imageLen) 1006 | { 1007 | cd::ISO_DESCRIPTOR isoDescriptor {}; 1008 | 1009 | isoDescriptor.header.type = 1; 1010 | isoDescriptor.header.version = 1; 1011 | CopyStringPadWithSpaces( isoDescriptor.header.id, "CD001" ); 1012 | 1013 | // Set System identifier 1014 | CopyStringPadWithSpaces( isoDescriptor.systemID, id.SystemID ); 1015 | 1016 | // Set Volume identifier 1017 | CopyStringPadWithSpaces( isoDescriptor.volumeID, id.VolumeID ); 1018 | 1019 | // Set Application identifier 1020 | CopyStringPadWithSpaces( isoDescriptor.applicationIdentifier, id.Application ); 1021 | 1022 | // Volume Set identifier 1023 | CopyStringPadWithSpaces( isoDescriptor.volumeSetIdentifier, id.VolumeSet ); 1024 | 1025 | // Publisher identifier 1026 | CopyStringPadWithSpaces( isoDescriptor.publisherIdentifier, id.Publisher ); 1027 | 1028 | // Data preparer identifier 1029 | CopyStringPadWithSpaces( isoDescriptor.dataPreparerIdentifier, id.DataPreparer ); 1030 | 1031 | // Copyright (file) identifier 1032 | CopyStringPadWithSpaces( isoDescriptor.copyrightFileIdentifier, id.Copyright ); 1033 | 1034 | // Unneeded identifiers 1035 | CopyStringPadWithSpaces( isoDescriptor.abstractFileIdentifier, nullptr ); 1036 | CopyStringPadWithSpaces( isoDescriptor.bibliographicFilelIdentifier, nullptr ); 1037 | 1038 | isoDescriptor.volumeCreateDate = GetLongDateFromDate( root.date ); 1039 | isoDescriptor.volumeModifyDate = GetLongDateFromString(id.ModificationDate); 1040 | isoDescriptor.volumeEffectiveDate = isoDescriptor.volumeExpiryDate = GetUnspecifiedLongDate(); 1041 | isoDescriptor.fileStructVersion = 1; 1042 | 1043 | if ( !global::noXA ) 1044 | { 1045 | strncpy( (char*)&isoDescriptor.appData[141], "CD-XA001", 8 ); 1046 | } 1047 | 1048 | const DirTreeClass* dirTree = root.subdir.get(); 1049 | unsigned int pathTableLen = dirTree->CalculatePathTableLen(root); 1050 | unsigned int pathTableSectors = GetSizeInSectors(pathTableLen); 1051 | 1052 | isoDescriptor.volumeSetSize = cd::SetPair16( 1 ); 1053 | isoDescriptor.volumeSeqNumber = cd::SetPair16( 1 ); 1054 | isoDescriptor.sectorSize = cd::SetPair16( 2048 ); 1055 | isoDescriptor.pathTableSize = cd::SetPair32( pathTableLen ); 1056 | 1057 | // Setup the root directory record 1058 | isoDescriptor.rootDirRecord.entryLength = 34; 1059 | isoDescriptor.rootDirRecord.extLength = 0; 1060 | isoDescriptor.rootDirRecord.entryOffs = cd::SetPair32( 1061 | 18+(pathTableSectors*4) ); 1062 | isoDescriptor.rootDirRecord.entrySize = cd::SetPair32( 1063 | dirTree->CalculateDirEntryLen() ); 1064 | isoDescriptor.rootDirRecord.flags = 0x02; 1065 | isoDescriptor.rootDirRecord.volSeqNum = cd::SetPair16( 1 ); 1066 | isoDescriptor.rootDirRecord.identifierLen = 1; 1067 | isoDescriptor.rootDirRecord.identifier = 0x0; 1068 | 1069 | isoDescriptor.rootDirRecord.entryDate = root.date; 1070 | 1071 | isoDescriptor.pathTable1Offs = 18; 1072 | isoDescriptor.pathTable2Offs = isoDescriptor.pathTable1Offs+ 1073 | pathTableSectors; 1074 | isoDescriptor.pathTable1MSBoffs = isoDescriptor.pathTable2Offs+1; 1075 | isoDescriptor.pathTable2MSBoffs = 1076 | isoDescriptor.pathTable1MSBoffs+pathTableSectors; 1077 | isoDescriptor.pathTable1MSBoffs = SwapBytes32( isoDescriptor.pathTable1MSBoffs ); 1078 | isoDescriptor.pathTable2MSBoffs = SwapBytes32( isoDescriptor.pathTable2MSBoffs ); 1079 | 1080 | isoDescriptor.volumeSize = cd::SetPair32( imageLen ); 1081 | 1082 | // Write the descriptor 1083 | unsigned int currentHeaderLBA = 16; 1084 | 1085 | auto isoDescriptorSectors = writer->GetSectorViewM2F1(currentHeaderLBA, 2, cd::IsoWriter::EdcEccForm::Form1); 1086 | isoDescriptorSectors->SetSubheader(cd::IsoWriter::SubEOL); 1087 | 1088 | isoDescriptorSectors->WriteMemory(&isoDescriptor, sizeof(isoDescriptor)); 1089 | 1090 | // Write descriptor terminator; 1091 | memset( &isoDescriptor, 0x00, sizeof(cd::ISO_DESCRIPTOR) ); 1092 | isoDescriptor.header.type = 255; 1093 | isoDescriptor.header.version = 1; 1094 | CopyStringPadWithSpaces( isoDescriptor.header.id, "CD001" ); 1095 | 1096 | isoDescriptorSectors->WriteMemory(&isoDescriptor, sizeof(isoDescriptor)); 1097 | currentHeaderLBA += 2; 1098 | 1099 | // Generate and write L-path table 1100 | const size_t pathTableSize = static_cast(2048)*pathTableSectors; 1101 | auto sectorBuff = std::make_unique(pathTableSize); 1102 | 1103 | dirTree->GeneratePathTable( root, sectorBuff.get(), false ); 1104 | auto lpathTable1 = writer->GetSectorViewM2F1(currentHeaderLBA, pathTableSectors, cd::IsoWriter::EdcEccForm::Form1); 1105 | lpathTable1->WriteMemory(sectorBuff.get(), pathTableSize); 1106 | currentHeaderLBA += pathTableSectors; 1107 | 1108 | auto lpathTable2 = writer->GetSectorViewM2F1(currentHeaderLBA, pathTableSectors, cd::IsoWriter::EdcEccForm::Form1); 1109 | lpathTable2->WriteMemory(sectorBuff.get(), pathTableSize); 1110 | currentHeaderLBA += pathTableSectors; 1111 | 1112 | // Generate and write M-path table 1113 | dirTree->GeneratePathTable( root, sectorBuff.get(), true ); 1114 | auto mpathTable1 = writer->GetSectorViewM2F1(currentHeaderLBA, pathTableSectors, cd::IsoWriter::EdcEccForm::Form1); 1115 | mpathTable1->WriteMemory(sectorBuff.get(), pathTableSize); 1116 | currentHeaderLBA += pathTableSectors; 1117 | 1118 | auto mpathTable2 = writer->GetSectorViewM2F1(currentHeaderLBA, pathTableSectors, cd::IsoWriter::EdcEccForm::Form1); 1119 | mpathTable2->WriteMemory(sectorBuff.get(), pathTableSize); 1120 | currentHeaderLBA += pathTableSectors; 1121 | } 1122 | 1123 | unsigned char* iso::PathTableClass::GenTableData(unsigned char* buff, bool msb) 1124 | { 1125 | for ( const PathEntryClass& entry : entries ) 1126 | { 1127 | *buff++ = std::max(1, entry.dir_id.length()); // Directory identifier length 1128 | *buff++ = 0; // Extended attribute record length (unused) 1129 | 1130 | // Write LBA and directory number index 1131 | unsigned int lba = entry.dir_lba; 1132 | unsigned short parentDirNumber = entry.dir_parent_index; 1133 | 1134 | if ( msb ) 1135 | { 1136 | lba = SwapBytes32( lba ); 1137 | parentDirNumber = SwapBytes16( parentDirNumber ); 1138 | } 1139 | memcpy( buff, &lba, sizeof(lba) ); 1140 | memcpy( buff+4, &parentDirNumber, sizeof(parentDirNumber) ); 1141 | 1142 | buff += 6; 1143 | 1144 | // Put identifier (nullptr if first entry) 1145 | strncpy( (char*)buff, entry.dir_id.c_str(), 1146 | entry.dir_id.length() ); 1147 | 1148 | buff += GetIDLength(entry.dir_id); 1149 | } 1150 | 1151 | for ( const PathEntryClass& entry : entries ) 1152 | { 1153 | if ( entry.sub != nullptr ) 1154 | { 1155 | buff = entry.sub->GenTableData( buff, msb ); 1156 | } 1157 | 1158 | } 1159 | 1160 | return buff; 1161 | 1162 | } 1163 | -------------------------------------------------------------------------------- /src/dumpsxiso/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef _WIN32 4 | #define NOMINMAX 5 | #include 6 | 7 | #else 8 | #include 9 | #endif 10 | 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "platform.h" 17 | #include "common.h" 18 | #include "fs.h" 19 | #include "cd.h" 20 | #include "xa.h" 21 | #include "cdreader.h" 22 | #include "xml.h" 23 | 24 | #ifndef MKPSXISO_NO_LIBFLAC 25 | #include "FLAC/stream_encoder.h" 26 | #endif 27 | 28 | #include 29 | 30 | typedef enum { 31 | EAF_WAV = 1 << 0, 32 | EAF_FLAC = 1 << 1, 33 | EAF_PCM = 1 << 2 34 | } EncoderAudioFormats; 35 | 36 | typedef struct { 37 | const char *codec; 38 | EncoderAudioFormats eaf; 39 | const char *notcompiledmessage; 40 | } EncodingCodec; 41 | 42 | static const EncodingCodec EncodingCodecs[] = { 43 | {"wave", EAF_WAV, ""}, 44 | {"flac", EAF_FLAC, "ERROR: dumpsxiso was NOT built with libFLAC support!\n"}, 45 | {"pcm", EAF_PCM, ""} 46 | }; 47 | 48 | const unsigned BUILTIN_CODECS = (EAF_WAV | EAF_PCM); 49 | #define BUILTIN_CODEC_TEXT "wave, pcm" 50 | #ifndef MKPSXISO_NO_LIBFLAC 51 | #define LIBFLAC_SUPPORTED EAF_FLAC 52 | #define LIBFLAC_CODEC_TEXT ", flac" 53 | #else 54 | #define LIBFLAC_SUPPORTED 0 55 | #define LIBFLAC_CODEC_TEXT "" 56 | #endif 57 | const unsigned SUPPORTED_CODECS = (BUILTIN_CODECS | LIBFLAC_SUPPORTED); 58 | #define SUPPORTED_CODEC_TEXT BUILTIN_CODEC_TEXT LIBFLAC_CODEC_TEXT 59 | 60 | namespace param { 61 | 62 | fs::path isoFile; 63 | fs::path outPath; 64 | fs::path xmlFile; 65 | bool outputSortedByDir = false; 66 | EncoderAudioFormats encodingFormat = EAF_WAV; 67 | } 68 | 69 | fs::path GetRealDAFilePath(const fs::path& inputPath) 70 | { 71 | fs::path outputPath(inputPath); 72 | if(param::encodingFormat == EAF_WAV) 73 | { 74 | outputPath.replace_extension(".WAV"); 75 | } 76 | else if(param::encodingFormat == EAF_FLAC) 77 | { 78 | outputPath.replace_extension(".FLAC"); 79 | } 80 | else if(param::encodingFormat == EAF_PCM) 81 | { 82 | outputPath.replace_extension(".PCM"); 83 | } 84 | else 85 | { 86 | printf("ERROR: support for encoding format is not implemented, not changing name\n"); 87 | return inputPath; 88 | } 89 | return outputPath; 90 | } 91 | 92 | template 93 | void PrintId(char (&id)[N]) 94 | { 95 | std::string_view view; 96 | 97 | const std::string_view id_view(id, N); 98 | const size_t last_char = id_view.find_last_not_of(' '); 99 | if (last_char != std::string_view::npos) 100 | { 101 | view = id_view.substr(0, last_char+1); 102 | } 103 | 104 | if (!view.empty()) 105 | { 106 | printf("%.*s", static_cast(view.length()), view.data()); 107 | } 108 | printf("\n"); 109 | } 110 | 111 | void PrintDate(const cd::ISO_LONG_DATESTAMP& date) { 112 | printf("%s\n", LongDateToString(date).c_str()); 113 | } 114 | 115 | template 116 | static std::string_view CleanDescElement(char (&id)[N]) 117 | { 118 | std::string_view result; 119 | 120 | const std::string_view view(id, N); 121 | const size_t last_char = view.find_last_not_of(' '); 122 | if (last_char != std::string_view::npos) 123 | { 124 | result = view.substr(0, last_char+1); 125 | } 126 | 127 | return result; 128 | } 129 | 130 | std::string_view CleanIdentifier(std::string_view id) 131 | { 132 | std::string_view result(id.substr(0, id.find_last_of(';'))); 133 | return result; 134 | } 135 | 136 | void prepareRIFFHeader(cd::RIFF_HEADER* header, int dataSize) { 137 | memcpy(header->chunkID,"RIFF",4); 138 | header->chunkSize = 36 + dataSize; 139 | memcpy(header->format, "WAVE", 4); 140 | 141 | memcpy(header->subchunk1ID, "fmt ", 4); 142 | header->subchunk1Size = 16; 143 | header->audioFormat = 1; 144 | header->numChannels = 2; 145 | header->sampleRate = 44100; 146 | header->bitsPerSample = 16; 147 | header->byteRate = (header->sampleRate * header->numChannels * header->bitsPerSample) / 8; 148 | header->blockAlign = (header->numChannels * header->bitsPerSample) / 8; 149 | 150 | memcpy(header->subchunk2ID, "data", 4); 151 | header->subchunk2Size = dataSize; 152 | } 153 | std::unique_ptr ReadLicense(cd::IsoReader& reader) { 154 | auto license = std::make_unique(); 155 | 156 | reader.SeekToSector(0); 157 | reader.ReadBytesXA(license->data, sizeof(license->data)); 158 | 159 | return license; 160 | } 161 | 162 | void SaveLicense(const cd::ISO_LICENSE& license) { 163 | const fs::path outputPath = param::outPath / "license_data.dat"; 164 | 165 | FILE* outFile = OpenFile(outputPath, "wb"); 166 | 167 | if (outFile == NULL) { 168 | printf("ERROR: Cannot create license file %" PRFILESYSTEM_PATH "...", outputPath.lexically_normal().c_str()); 169 | return; 170 | } 171 | 172 | fwrite(license.data, 1, sizeof(license.data), outFile); 173 | fclose(outFile); 174 | } 175 | 176 | void writePCMFile(FILE *outFile, cd::IsoReader& reader, const size_t cddaSize, const int isInvalid) 177 | { 178 | int bytesLeft = cddaSize; 179 | while (bytesLeft > 0) { 180 | 181 | u_char copyBuff[2352]{}; 182 | 183 | int bytesToRead = bytesLeft; 184 | 185 | if (bytesToRead > 2352) 186 | bytesToRead = 2352; 187 | 188 | if (!isInvalid) 189 | reader.ReadBytesDA(copyBuff, bytesToRead); 190 | 191 | fwrite(copyBuff, 1, bytesToRead, outFile); 192 | 193 | bytesLeft -= bytesToRead; 194 | } 195 | } 196 | 197 | void writeWaveFile(FILE *outFile, cd::IsoReader& reader, const size_t cddaSize, const int isInvalid) 198 | { 199 | cd::RIFF_HEADER riffHeader; 200 | prepareRIFFHeader(&riffHeader, cddaSize); 201 | fwrite((void*)&riffHeader, 1, sizeof(cd::RIFF_HEADER), outFile); 202 | 203 | writePCMFile(outFile, reader, cddaSize, isInvalid); 204 | } 205 | 206 | #ifndef MKPSXISO_NO_LIBFLAC 207 | void writeFLACFile(FILE *outFile, cd::IsoReader& reader, const int cddaSize, const int isInvalid) 208 | { 209 | FLAC__bool ok = true; 210 | FLAC__StreamEncoder *encoder = 0; 211 | FLAC__StreamEncoderInitStatus init_status; 212 | if((encoder = FLAC__stream_encoder_new()) == NULL) 213 | { 214 | fprintf(stderr, "ERROR: allocating encoder\n"); 215 | return; 216 | } 217 | unsigned sample_rate = 44100; 218 | unsigned channels = 2; 219 | unsigned bps = 16; 220 | unsigned total_samples = cddaSize / (channels * (bps/8)); 221 | 222 | ok &= FLAC__stream_encoder_set_verify(encoder, true); 223 | ok &= FLAC__stream_encoder_set_compression_level(encoder, 5); 224 | ok &= FLAC__stream_encoder_set_channels(encoder, channels); 225 | ok &= FLAC__stream_encoder_set_bits_per_sample(encoder, bps); 226 | ok &= FLAC__stream_encoder_set_sample_rate(encoder, sample_rate); 227 | ok &= FLAC__stream_encoder_set_total_samples_estimate(encoder, total_samples); 228 | if(!ok) 229 | { 230 | fprintf(stderr, "ERROR: setting encoder settings\n"); 231 | goto writeFLACFile_cleanup; 232 | } 233 | 234 | init_status = FLAC__stream_encoder_init_FILE(encoder, outFile, /*progress_callback=*/NULL, /*client_data=*/NULL); 235 | if(init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) 236 | { 237 | fprintf(stderr, "ERROR: initializing encoder: %s\n", FLAC__StreamEncoderInitStatusString[init_status]); 238 | goto writeFLACFile_cleanup; 239 | } 240 | 241 | { 242 | size_t left = (size_t)total_samples; 243 | size_t max_pcmframe_read = 2352 / (channels * (bps/8)); 244 | 245 | std::unique_ptr pcm(new int32_t[channels * max_pcmframe_read]); 246 | while (left && ok) { 247 | 248 | u_char copyBuff[2352]{}; 249 | 250 | size_t need = (left > max_pcmframe_read ? max_pcmframe_read : left); 251 | size_t needBytes = need * (channels * (bps/8)); 252 | if(!isInvalid) 253 | reader.ReadBytesDA(copyBuff, needBytes); 254 | 255 | /* convert the packed little-endian 16-bit PCM samples from WAVE into an interleaved FLAC__int32 buffer for libFLAC */ 256 | for(size_t i = 0; i < need*channels; i++) 257 | { 258 | /* inefficient but simple and works on big- or little-endian machines */ 259 | pcm[i] = (FLAC__int32)(((FLAC__int16)(FLAC__int8)copyBuff[2*i+1] << 8) | (FLAC__int16)copyBuff[2*i]); 260 | } 261 | /* feed samples to encoder */ 262 | ok = FLAC__stream_encoder_process_interleaved(encoder, pcm.get(), need); 263 | left -= need; 264 | } 265 | } 266 | ok &= FLAC__stream_encoder_finish(encoder); // closes outFile 267 | if(!ok) 268 | { 269 | fprintf(stderr, "encoding: %s\n", ok? "succeeded" : "FAILED"); 270 | fprintf(stderr, " state: %s\n", FLAC__StreamEncoderStateString[FLAC__stream_encoder_get_state(encoder)]); 271 | } 272 | 273 | writeFLACFile_cleanup: 274 | FLAC__stream_encoder_delete(encoder); 275 | } 276 | #endif 277 | 278 | // XML attribute stuff 279 | struct EntryAttributeCounters 280 | { 281 | std::map GMTOffs; 282 | std::map XAAttrib; 283 | std::map XAPerm; 284 | std::map GID; 285 | std::map UID; 286 | }; 287 | 288 | static void WriteOptionalXMLAttribs(tinyxml2::XMLElement* element, const cd::IsoDirEntries::Entry& entry, EntryType type, EntryAttributeCounters& attributeCounters) 289 | { 290 | element->SetAttribute(xml::attrib::GMT_OFFSET, entry.entry.entryDate.GMToffs); 291 | ++attributeCounters.GMTOffs[entry.entry.entryDate.GMToffs]; 292 | 293 | // xa_attrib only makes sense on XA files 294 | if (type == EntryType::EntryXA) 295 | { 296 | const auto XAAtrib = (entry.extData.attributes & cdxa::XA_ATTRIBUTES_MASK) >> 8; 297 | element->SetAttribute(xml::attrib::XA_ATTRIBUTES, XAAtrib); 298 | ++attributeCounters.XAAttrib[XAAtrib]; 299 | } 300 | const auto XAPerm = entry.extData.attributes & cdxa::XA_PERMISSIONS_MASK; 301 | element->SetAttribute(xml::attrib::XA_PERMISSIONS, XAPerm); 302 | ++attributeCounters.XAPerm[XAPerm]; 303 | 304 | element->SetAttribute(xml::attrib::XA_GID, entry.extData.ownergroupid); 305 | element->SetAttribute(xml::attrib::XA_UID, entry.extData.owneruserid); 306 | ++attributeCounters.GID[entry.extData.ownergroupid]; 307 | ++attributeCounters.UID[entry.extData.owneruserid]; 308 | } 309 | 310 | static EntryAttributes EstablishXMLAttributeDefaults(tinyxml2::XMLElement* defaultAttributesElement, const EntryAttributeCounters& attributeCounters) 311 | { 312 | // First establish "defaults" - that is, the most commonly occurring attributes 313 | auto findMaxElement = [](const auto& map) 314 | { 315 | return std::max_element(map.begin(), map.end(), [](const auto& left, const auto& right) { return left.second < right.second; })->first; 316 | }; 317 | 318 | EntryAttributes defaultAttributes; 319 | defaultAttributes.GMTOffs = static_cast(findMaxElement(attributeCounters.GMTOffs)); 320 | defaultAttributes.XAAttrib = static_cast(findMaxElement(attributeCounters.XAAttrib)); 321 | defaultAttributes.XAPerm = static_cast(findMaxElement(attributeCounters.XAPerm)); 322 | defaultAttributes.GID = static_cast(findMaxElement(attributeCounters.GID)); 323 | defaultAttributes.UID = static_cast(findMaxElement(attributeCounters.UID)); 324 | 325 | // Write them out to the XML 326 | defaultAttributesElement->SetAttribute(xml::attrib::GMT_OFFSET, defaultAttributes.GMTOffs); 327 | defaultAttributesElement->SetAttribute(xml::attrib::XA_ATTRIBUTES, defaultAttributes.XAAttrib); 328 | defaultAttributesElement->SetAttribute(xml::attrib::XA_PERMISSIONS, defaultAttributes.XAPerm); 329 | defaultAttributesElement->SetAttribute(xml::attrib::XA_GID, defaultAttributes.GID); 330 | defaultAttributesElement->SetAttribute(xml::attrib::XA_UID, defaultAttributes.UID); 331 | 332 | return defaultAttributes; 333 | } 334 | 335 | static void SimplifyDefaultXMLAttributes(tinyxml2::XMLElement* element, const EntryAttributes& defaults) 336 | { 337 | // DeleteAttribute can be safely called even if that attribute doesn't exist, so treating failure and default values 338 | // as equal simplifies logic 339 | auto deleteAttribute = [element](const char* name, auto defaultValue) 340 | { 341 | bool deleteAttribute = false; 342 | if constexpr (std::is_unsigned_v) 343 | { 344 | deleteAttribute = element->UnsignedAttribute(name, defaultValue) == defaultValue; 345 | } 346 | else 347 | { 348 | deleteAttribute = element->IntAttribute(name, defaultValue) == defaultValue; 349 | } 350 | if (deleteAttribute) 351 | { 352 | element->DeleteAttribute(name); 353 | } 354 | }; 355 | 356 | deleteAttribute(xml::attrib::GMT_OFFSET, defaults.GMTOffs); 357 | deleteAttribute(xml::attrib::XA_ATTRIBUTES, defaults.XAAttrib); 358 | deleteAttribute(xml::attrib::XA_PERMISSIONS, defaults.XAPerm); 359 | deleteAttribute(xml::attrib::XA_GID, defaults.GID); 360 | deleteAttribute(xml::attrib::XA_UID, defaults.UID); 361 | 362 | for (tinyxml2::XMLElement* child = element->FirstChildElement(); child != nullptr; child = child->NextSiblingElement()) 363 | { 364 | SimplifyDefaultXMLAttributes(child, defaults); 365 | } 366 | } 367 | 368 | static EntryType GetXAEntryType(unsigned short xa_attr) 369 | { 370 | // we try to guess the file type. Usually, the xa_attr should tell this, but there are many games 371 | // that do not follow the standard, and sometime leave some or all the attributes unset. 372 | if (xa_attr & 0x40) 373 | { 374 | // if the cddata flag is set, we assume this is the case 375 | return EntryType::EntryDA; 376 | } 377 | if (xa_attr & 0x80) 378 | { 379 | // if the directory flag is set, we assume this is the case 380 | return EntryType::EntryDir; 381 | } 382 | if ( (xa_attr & 0x08) && !(xa_attr & 0x10) ) 383 | { 384 | // if the mode 2 form 1 flag is set, and form 2 is not, we assume this is a regular file. 385 | return EntryType::EntryFile; 386 | } 387 | if ( (xa_attr & 0x10) && !(xa_attr & 0x08) ) 388 | { 389 | // if the mode 2 form 2 flag is set, and form 1 is not, we assume this is a pure audio xa file. 390 | return EntryType::EntryXA; 391 | } 392 | 393 | // here all flags are set to the same value. From what I could see until now, when both flags are the same, 394 | // this is interpreted in the following two ways, which both lead us to choose str/xa type. 395 | // 1. Both values are 1, which means there is an indication by the mode 2 form 2 flag that the data is not 396 | // regular mode 2 form 1 data (i.e., it is either mixed or just xa). 397 | // 2. Both values are 0. The fact that the mode 2 form 2 flag is 0 simply means that the data might not 398 | // be *pure* mode 2 form 2 data (i.e., xa), so, we do not conclude it is regular mode 2 form 1 data. 399 | // We thus give priority to the mode 2 form 1 flag, which is also zero, 400 | // and conclude that the data is not regular mode 2 form 1 data, and thus can be either mode 2 form 2 or mixed. 401 | 402 | // Remark: Some games (Legend of Mana), use a very strange STR+XA format that is stored in plain mode 2 form 1. 403 | // This is properly marked in the xa_attr, and there is nothing wrong in extracting them as data. 404 | return EntryType::EntryXA; 405 | } 406 | 407 | std::unique_ptr ParseSubdirectory(cd::IsoReader& reader, ListView view, int offs, int sectors, 408 | const fs::path& path) 409 | { 410 | auto dirEntries = std::make_unique(std::move(view)); 411 | dirEntries->ReadDirEntries(&reader, offs, sectors); 412 | 413 | for (auto& e : dirEntries->dirEntryList.GetView()) 414 | { 415 | auto& entry = e.get(); 416 | 417 | entry.virtualPath = path; 418 | if (entry.entry.flags & 0x2) 419 | { 420 | entry.subdir = ParseSubdirectory(reader, dirEntries->dirEntryList.NewView(), entry.entry.entryOffs.lsb, GetSizeInSectors(entry.entry.entrySize.lsb), 421 | path / CleanIdentifier(entry.identifier)); 422 | } 423 | } 424 | 425 | return dirEntries; 426 | } 427 | 428 | std::unique_ptr ParseRoot(cd::IsoReader& reader, ListView view, int offs) 429 | { 430 | auto dirEntries = std::make_unique(std::move(view)); 431 | dirEntries->ReadRootDir(&reader, offs); 432 | 433 | auto& entry = dirEntries->dirEntryList.GetView().front().get(); 434 | entry.subdir = ParseSubdirectory(reader, dirEntries->dirEntryList.NewView(), entry.entry.entryOffs.lsb, GetSizeInSectors(entry.entry.entrySize.lsb), 435 | CleanIdentifier(entry.identifier)); 436 | 437 | return dirEntries; 438 | } 439 | 440 | void ExtractFiles(cd::IsoReader& reader, const std::list& files, const fs::path& rootPath) 441 | { 442 | for (const auto& entry : files) 443 | { 444 | const fs::path outputPath = rootPath / entry.virtualPath / CleanIdentifier(entry.identifier); 445 | if (entry.subdir == nullptr) // Do not extract directories, they're already prepared 446 | { 447 | printf(" Extracting %s...\n%" PRFILESYSTEM_PATH "\n", entry.identifier.c_str(), outputPath.lexically_normal().c_str()); 448 | 449 | const EntryType type = GetXAEntryType((entry.extData.attributes & cdxa::XA_ATTRIBUTES_MASK) >> 8); 450 | if (type == EntryType::EntryXA) 451 | { 452 | // Extract XA or STR file. 453 | // For both XA and STR files, we need to extract the data 2336 bytes per sector. 454 | // When rebuilding the bin using mkpsxiso, either we mark the file with str or with xa 455 | // the source file will anyway be stored on our hard drive in raw form. 456 | 457 | // this is the data to be read 2336 bytes per sector, both if the file is an STR or XA, 458 | // because the STR contains audio. 459 | size_t sectorsToRead = GetSizeInSectors(entry.entry.entrySize.lsb); 460 | 461 | int bytesLeft = 2336*sectorsToRead; 462 | 463 | reader.SeekToSector(entry.entry.entryOffs.lsb); 464 | 465 | FILE* outFile = OpenFile(outputPath, "wb"); 466 | 467 | if (outFile == NULL) { 468 | printf("ERROR: Cannot create file %" PRFILESYSTEM_PATH "...", outputPath.lexically_normal().c_str()); 469 | return; 470 | } 471 | 472 | // Copy loop 473 | while(bytesLeft > 0) { 474 | 475 | u_char copyBuff[2336]; 476 | 477 | int bytesToRead = bytesLeft; 478 | 479 | if (bytesToRead > 2336) 480 | bytesToRead = 2336; 481 | 482 | reader.ReadBytesXA(copyBuff, 2336); 483 | 484 | fwrite(copyBuff, 1, 2336, outFile); 485 | 486 | bytesLeft -= bytesToRead; 487 | 488 | } 489 | 490 | fclose(outFile); 491 | 492 | } 493 | else if (type == EntryType::EntryDA) 494 | { 495 | // Extract CDDA file 496 | int result = reader.SeekToSector(entry.entry.entryOffs.lsb); 497 | 498 | if (result) { 499 | printf("WARNING: The CDDA file %" PRFILESYSTEM_PATH " is out of the iso file bounds.\n", outputPath.lexically_normal().c_str()); 500 | printf("This usually means that the game has audio tracks, and they are on separate files.\n"); 501 | printf("As DUMPSXISO does not support dumping from a cue file, you should use an iso file containing all tracks.\n\n"); 502 | printf("DUMPSXISO will write the file as a dummy (silent) cdda file.\n"); 503 | printf("This is generally fine, when the real CDDA file is also a dummy file.\n"); 504 | printf("If it is not dummy, you WILL lose this audio data in the rebuilt iso.\n"); 505 | } 506 | 507 | auto daOutPath = GetRealDAFilePath(outputPath); 508 | auto outFile = OpenScopedFile(daOutPath, "wb"); 509 | 510 | if (!outFile) { 511 | printf("ERROR: Cannot create file %" PRFILESYSTEM_PATH "...", daOutPath.lexically_normal().c_str()); 512 | return; 513 | } 514 | 515 | size_t sectorsToRead = GetSizeInSectors(entry.entry.entrySize.lsb); 516 | size_t cddaSize = 2352 * sectorsToRead; 517 | 518 | if(param::encodingFormat == EAF_WAV) 519 | { 520 | writeWaveFile(outFile.get(), reader, cddaSize, result); 521 | } 522 | #ifndef MKPSXISO_NO_LIBFLAC 523 | else if(param::encodingFormat == EAF_FLAC) 524 | { 525 | // libflac closes outFile 526 | writeFLACFile(outFile.release(), reader, cddaSize, result); 527 | } 528 | #endif 529 | else if(param::encodingFormat == EAF_PCM) 530 | { 531 | writePCMFile(outFile.get(), reader, cddaSize, result); 532 | } 533 | else 534 | { 535 | printf("ERROR: support for encoding format is not implemented\n"); 536 | return; 537 | } 538 | } 539 | else if (type == EntryType::EntryFile) 540 | { 541 | // Extract regular file 542 | 543 | reader.SeekToSector(entry.entry.entryOffs.lsb); 544 | 545 | FILE* outFile = OpenFile(outputPath, "wb"); 546 | 547 | if (outFile == NULL) { 548 | printf("ERROR: Cannot create file %" PRFILESYSTEM_PATH "...", outputPath.lexically_normal().c_str()); 549 | return; 550 | } 551 | 552 | size_t bytesLeft = entry.entry.entrySize.lsb; 553 | while(bytesLeft > 0) { 554 | 555 | u_char copyBuff[2048]; 556 | size_t bytesToRead = bytesLeft; 557 | 558 | if (bytesToRead > 2048) 559 | bytesToRead = 2048; 560 | 561 | reader.ReadBytes(copyBuff, bytesToRead); 562 | fwrite(copyBuff, 1, bytesToRead, outFile); 563 | 564 | bytesLeft -= bytesToRead; 565 | 566 | } 567 | 568 | fclose(outFile); 569 | 570 | } 571 | else 572 | { 573 | printf("ERROR: File %s is of invalid type", entry.identifier.c_str()); 574 | continue; 575 | } 576 | } 577 | } 578 | 579 | // Update timestamps AFTER all files have been extracted 580 | // else directories will have their timestamps discarded when files are being unpacked into them! 581 | for (const auto& entry : files) 582 | { 583 | fs::path toChange(rootPath / entry.virtualPath / CleanIdentifier(entry.identifier)); 584 | const EntryType type = GetXAEntryType((entry.extData.attributes & cdxa::XA_ATTRIBUTES_MASK) >> 8); 585 | if(type == EntryType::EntryDA) 586 | { 587 | toChange = GetRealDAFilePath(toChange); 588 | } 589 | UpdateTimestamps(toChange, entry.entry.entryDate); 590 | } 591 | } 592 | 593 | tinyxml2::XMLElement* WriteXMLEntry(const cd::IsoDirEntries::Entry& entry, tinyxml2::XMLElement* dirElement, fs::path* currentVirtualPath, 594 | const fs::path& sourcePath, const std::string& trackid, EntryAttributeCounters& attributeCounters) 595 | { 596 | tinyxml2::XMLElement* newelement; 597 | 598 | const fs::path outputPath = sourcePath / entry.virtualPath / CleanIdentifier(entry.identifier); 599 | const EntryType entryType = GetXAEntryType((entry.extData.attributes & cdxa::XA_ATTRIBUTES_MASK) >> 8); 600 | if (entryType == EntryType::EntryDir) 601 | { 602 | if (!entry.identifier.empty()) 603 | { 604 | newelement = dirElement->InsertNewChildElement("dir"); 605 | newelement->SetAttribute(xml::attrib::ENTRY_NAME, entry.identifier.c_str()); 606 | newelement->SetAttribute(xml::attrib::ENTRY_SOURCE, outputPath.lexically_normal().generic_u8string().c_str()); 607 | } 608 | else 609 | { 610 | // Root directory 611 | newelement = dirElement->InsertNewChildElement(xml::elem::DIRECTORY_TREE); 612 | } 613 | 614 | dirElement = newelement; 615 | if (currentVirtualPath != nullptr) 616 | { 617 | *currentVirtualPath /= entry.identifier; 618 | } 619 | } 620 | else 621 | { 622 | newelement = dirElement->InsertNewChildElement("file"); 623 | newelement->SetAttribute(xml::attrib::ENTRY_NAME, std::string(CleanIdentifier(entry.identifier)).c_str()); 624 | if(entryType != EntryType::EntryDA) 625 | { 626 | newelement->SetAttribute(xml::attrib::ENTRY_SOURCE, outputPath.lexically_normal().generic_u8string().c_str()); 627 | } 628 | else 629 | { 630 | newelement->SetAttribute(xml::attrib::TRACK_ID, trackid.c_str()); 631 | } 632 | 633 | if (entryType == EntryType::EntryXA) 634 | { 635 | newelement->SetAttribute(xml::attrib::ENTRY_TYPE, "mixed"); 636 | } 637 | else if (entryType == EntryType::EntryDA) 638 | { 639 | newelement->SetAttribute(xml::attrib::ENTRY_TYPE, "da"); 640 | } 641 | else if (entryType == EntryType::EntryFile) 642 | { 643 | newelement->SetAttribute(xml::attrib::ENTRY_TYPE, "data"); 644 | } 645 | } 646 | WriteOptionalXMLAttribs(newelement, entry, entryType, attributeCounters); 647 | return dirElement; 648 | } 649 | 650 | void WriteXMLGap(unsigned int numSectors, tinyxml2::XMLElement* dirElement) 651 | { 652 | // TODO: Detect if gap needs checksums and reflect it accordingly here 653 | tinyxml2::XMLElement* newelement = dirElement->InsertNewChildElement("dummy"); 654 | newelement->SetAttribute(xml::attrib::NUM_DUMMY_SECTORS, numSectors); 655 | } 656 | 657 | void WriteXMLByLBA(const std::list& files, tinyxml2::XMLElement* dirElement, const fs::path& sourcePath, unsigned int& expectedLBA, 658 | EntryAttributeCounters& attributeCounters) 659 | { 660 | fs::path currentVirtualPath; // Used to find out whether to traverse 'dir' up or down the chain 661 | unsigned tracknum = 2; 662 | for (const auto& entry : files) 663 | { 664 | const bool isDA = GetXAEntryType((entry.extData.attributes & cdxa::XA_ATTRIBUTES_MASK) >> 8) == EntryType::EntryDA; 665 | 666 | // only check for gaps, update LBA if it's inside the iso filesystem 667 | if(!isDA) 668 | { 669 | if (entry.entry.entryOffs.lsb > expectedLBA) 670 | { 671 | // if this is a DA file we are at the end of filesystem, flag to write the gap after DA files 672 | WriteXMLGap(entry.entry.entryOffs.lsb - expectedLBA, dirElement); 673 | } 674 | expectedLBA = entry.entry.entryOffs.lsb + GetSizeInSectors(entry.entry.entrySize.lsb); 675 | } 676 | 677 | // add a trackid to DA tracks 678 | std::string trackid; 679 | if (isDA) 680 | { 681 | char tidbuf[3]; 682 | snprintf(tidbuf, sizeof(tidbuf), "%02u", tracknum++); 683 | trackid = tidbuf; 684 | } 685 | 686 | // Work out the relative position between the current directory and the element to create 687 | const fs::path relative = entry.virtualPath.lexically_relative(currentVirtualPath); 688 | for (const fs::path& part : relative) 689 | { 690 | if (part == "..") 691 | { 692 | // Go up in XML 693 | dirElement = dirElement->Parent()->ToElement(); 694 | currentVirtualPath = currentVirtualPath.parent_path(); 695 | continue; 696 | } 697 | if (part == ".") 698 | { 699 | // Do nothing 700 | continue; 701 | } 702 | 703 | // "Enter" the directory 704 | dirElement = dirElement->InsertNewChildElement("dir"); 705 | dirElement->SetAttribute(xml::attrib::ENTRY_NAME, part.generic_u8string().c_str()); 706 | 707 | currentVirtualPath /= part; 708 | } 709 | 710 | dirElement = WriteXMLEntry(entry, dirElement, ¤tVirtualPath, sourcePath, trackid, attributeCounters); 711 | } 712 | } 713 | 714 | void WriteXMLByDirectories(const cd::IsoDirEntries* directory, tinyxml2::XMLElement* dirElement, const fs::path& sourcePath, unsigned int& expectedLBA, 715 | EntryAttributeCounters& attributeCounters) 716 | { 717 | unsigned tracknum = 2; 718 | for (const auto& e : directory->dirEntryList.GetView()) 719 | { 720 | const auto& entry = e.get(); 721 | 722 | std::string trackid; 723 | if (GetXAEntryType((entry.extData.attributes & cdxa::XA_ATTRIBUTES_MASK) >> 8) == EntryType::EntryDA) 724 | { 725 | char tidbuf[3]; 726 | snprintf(tidbuf, sizeof(tidbuf), "%02u", tracknum++); 727 | trackid = tidbuf; 728 | } 729 | else 730 | { 731 | // Update the LBA to the max encountered value 732 | expectedLBA = std::max(expectedLBA, entry.entry.entryOffs.lsb + GetSizeInSectors(entry.entry.entrySize.lsb)); 733 | } 734 | 735 | tinyxml2::XMLElement* child = WriteXMLEntry(entry, dirElement, nullptr, sourcePath, trackid, attributeCounters); 736 | // Recursively write children if there are any 737 | if (const cd::IsoDirEntries* subdir = entry.subdir.get(); subdir != nullptr) 738 | { 739 | WriteXMLByDirectories(subdir, child, sourcePath, expectedLBA, attributeCounters); 740 | } 741 | } 742 | } 743 | 744 | void ParseISO(cd::IsoReader& reader) { 745 | 746 | cd::ISO_DESCRIPTOR descriptor; 747 | auto license = ReadLicense(reader); 748 | 749 | reader.SeekToSector(16); 750 | reader.ReadBytes(&descriptor, 2048); 751 | 752 | 753 | printf("ISO descriptor:\n\n"); 754 | 755 | printf(" System ID : "); 756 | PrintId(descriptor.systemID); 757 | printf(" Volume ID : "); 758 | PrintId(descriptor.volumeID); 759 | printf(" Volume Set ID : "); 760 | PrintId(descriptor.volumeSetIdentifier); 761 | printf(" Publisher ID : "); 762 | PrintId(descriptor.publisherIdentifier); 763 | printf(" Data Prep. ID : "); 764 | PrintId(descriptor.dataPreparerIdentifier); 765 | printf(" Application ID : "); 766 | PrintId(descriptor.applicationIdentifier); 767 | printf("\n"); 768 | 769 | printf(" Volume Create Date : "); 770 | PrintDate(descriptor.volumeCreateDate); 771 | printf(" Volume Modify Date : "); 772 | PrintDate(descriptor.volumeModifyDate); 773 | printf(" Volume Expire Date : "); 774 | PrintDate(descriptor.volumeExpiryDate); 775 | printf("\n"); 776 | 777 | cd::IsoPathTable pathTable; 778 | 779 | size_t numEntries = pathTable.ReadPathTable(&reader, descriptor.pathTable1Offs); 780 | 781 | if (numEntries == 0) { 782 | printf(" No files to find.\n"); 783 | return; 784 | } 785 | 786 | // Prepare output directories 787 | for(size_t i=0; i entries; 799 | std::unique_ptr rootDir = ParseRoot(reader, 800 | ListView(entries), 801 | descriptor.rootDirRecord.entryOffs.lsb); 802 | 803 | // Sort files by LBA for "strict" output 804 | entries.sort([](const auto& left, const auto& right) 805 | { 806 | return left.entry.entryOffs.lsb < right.entry.entryOffs.lsb; 807 | }); 808 | 809 | ExtractFiles(reader, entries, param::outPath); 810 | SaveLicense(*license); 811 | 812 | if (!param::xmlFile.empty()) 813 | { 814 | if (FILE* file = OpenFile(param::xmlFile, "w"); file != nullptr) 815 | { 816 | tinyxml2::XMLDocument xmldoc; 817 | 818 | tinyxml2::XMLElement *baseElement = static_cast(xmldoc.InsertFirstChild(xmldoc.NewElement(xml::elem::ISO_PROJECT))); 819 | baseElement->SetAttribute(xml::attrib::IMAGE_NAME, "mkpsxiso.bin"); 820 | baseElement->SetAttribute(xml::attrib::CUE_SHEET, "mkpsxiso.cue"); 821 | 822 | tinyxml2::XMLElement *trackElement = baseElement->InsertNewChildElement(xml::elem::TRACK); 823 | trackElement->SetAttribute(xml::attrib::TRACK_TYPE, "data"); 824 | 825 | { 826 | tinyxml2::XMLElement *newElement = trackElement->InsertNewChildElement(xml::elem::IDENTIFIERS); 827 | auto setAttributeIfNotEmpty = [newElement](const char* name, std::string_view value) 828 | { 829 | if (!value.empty()) 830 | { 831 | newElement->SetAttribute(name, std::string(value).c_str()); 832 | } 833 | }; 834 | 835 | setAttributeIfNotEmpty(xml::attrib::SYSTEM_ID, CleanDescElement(descriptor.systemID)); 836 | setAttributeIfNotEmpty(xml::attrib::APPLICATION, CleanDescElement(descriptor.applicationIdentifier)); 837 | setAttributeIfNotEmpty(xml::attrib::VOLUME_ID, CleanDescElement(descriptor.volumeID)); 838 | setAttributeIfNotEmpty(xml::attrib::VOLUME_SET, CleanDescElement(descriptor.volumeSetIdentifier)); 839 | setAttributeIfNotEmpty(xml::attrib::PUBLISHER, CleanDescElement(descriptor.publisherIdentifier)); 840 | setAttributeIfNotEmpty(xml::attrib::DATA_PREPARER, CleanDescElement(descriptor.dataPreparerIdentifier)); 841 | setAttributeIfNotEmpty(xml::attrib::COPYRIGHT, CleanDescElement(descriptor.copyrightFileIdentifier)); 842 | newElement->SetAttribute(xml::attrib::CREATION_DATE, LongDateToString(descriptor.volumeCreateDate).c_str()); 843 | if (auto ZERO_DATE = GetUnspecifiedLongDate(); memcmp(&descriptor.volumeModifyDate, &ZERO_DATE, sizeof(descriptor.volumeModifyDate)) != 0) 844 | { 845 | // Set only if not zero 846 | newElement->SetAttribute(xml::attrib::MODIFICATION_DATE, LongDateToString(descriptor.volumeModifyDate).c_str()); 847 | } 848 | } 849 | 850 | const fs::path xmlPath = param::xmlFile.parent_path().lexically_normal(); 851 | 852 | { 853 | tinyxml2::XMLElement *newElement = trackElement->InsertNewChildElement(xml::elem::LICENSE); 854 | newElement->SetAttribute(xml::attrib::LICENSE_FILE, 855 | (param::outPath / "license_data.dat").lexically_proximate(xmlPath).generic_u8string().c_str()); 856 | } 857 | 858 | // Create now so it lands before the directory tree 859 | tinyxml2::XMLElement* defaultAttributesElement = trackElement->InsertNewChildElement(xml::elem::DEFAULT_ATTRIBUTES); 860 | 861 | const fs::path sourcePath = param::outPath.lexically_proximate(xmlPath); 862 | 863 | // process DA "files" to tracks and add to the dirs so the XML looks nicer 864 | std::vector::const_iterator> dafiles; 865 | for(auto it = entries.begin(); it != entries.end(); ++it) 866 | { 867 | if(GetXAEntryType(((*it).extData.attributes & cdxa::XA_ATTRIBUTES_MASK) >> 8) == EntryType::EntryDA) 868 | { 869 | dafiles.emplace_back(it); 870 | } 871 | } 872 | std::vector tracks; 873 | for(const auto& dafile : dafiles) 874 | { 875 | auto tracksource = GetRealDAFilePath(sourcePath / dafile->virtualPath / CleanIdentifier(dafile->identifier)).lexically_normal(); 876 | 877 | // add to make track element later 878 | tracks.emplace_back( 879 | dafile->entry.entryOffs.lsb, 880 | dafile->entry.entrySize.lsb, 881 | tracksource.generic_u8string() 882 | ); 883 | 884 | // add back in to the rest of the files 885 | for(auto it = entries.begin(); it != entries.end(); ++it) 886 | { 887 | fs::path vpath = (*it).virtualPath / CleanIdentifier((*it).identifier); 888 | if(dafile->virtualPath == vpath) 889 | { 890 | do { 891 | ++it; 892 | } while((it != entries.end()) && ((*it).virtualPath == dafile->virtualPath)); 893 | entries.splice(it, entries, dafile); 894 | break; 895 | } 896 | } 897 | } 898 | 899 | EntryAttributeCounters attributeCounters; 900 | unsigned currentLBA = descriptor.rootDirRecord.entryOffs.lsb; 901 | if (param::outputSortedByDir) 902 | { 903 | WriteXMLByDirectories(rootDir.get(), trackElement, sourcePath, currentLBA, attributeCounters); 904 | } 905 | else 906 | { 907 | WriteXMLByLBA(entries, trackElement, sourcePath, currentLBA, attributeCounters); 908 | } 909 | 910 | tinyxml2::XMLElement *dirtree = trackElement->FirstChildElement(xml::elem::DIRECTORY_TREE); 911 | SimplifyDefaultXMLAttributes(dirtree, EstablishXMLAttributeDefaults(defaultAttributesElement, attributeCounters)); 912 | 913 | // write CDDA tracks 914 | tinyxml2::XMLNode *modifyProject = trackElement->Parent(); 915 | tinyxml2::XMLElement *addAfter = trackElement; 916 | unsigned tracknum = 2; 917 | for(const auto& track : tracks) 918 | { 919 | unsigned pregap_sectors = 0; 920 | if(track.lba != currentLBA) 921 | { 922 | unsigned delta = (track.lba - currentLBA); 923 | // HACK add dummy sectors, assumes a 150 sector pregap if this a gap of more sectors 924 | if((delta > 150) && (tracknum == 2)) 925 | { 926 | WriteXMLGap(delta-150, dirtree); 927 | currentLBA += (delta-150); 928 | delta = 150; 929 | } 930 | currentLBA += delta; 931 | pregap_sectors = delta; 932 | } 933 | currentLBA += GetSizeInSectors(track.size); 934 | char tidbuf[3]; 935 | snprintf(tidbuf, sizeof(tidbuf), "%02u", tracknum); 936 | tinyxml2::XMLElement *newtrack = xmldoc.NewElement(xml::elem::TRACK); 937 | newtrack->SetAttribute(xml::attrib::TRACK_TYPE, "audio"); 938 | newtrack->SetAttribute(xml::attrib::TRACK_ID, tidbuf); 939 | newtrack->SetAttribute(xml::attrib::TRACK_SOURCE, track.source.c_str()); 940 | // only write the pregap element if it's non default 941 | if(pregap_sectors != 150) 942 | { 943 | tinyxml2::XMLElement *pregap = newtrack->InsertNewChildElement(xml::elem::TRACK_PREGAP); 944 | pregap->SetAttribute(xml::attrib::PREGAP_DURATION, SectorsToTimecode(pregap_sectors).c_str()); 945 | } 946 | 947 | modifyProject->InsertAfterChild(addAfter, newtrack); 948 | addAfter = newtrack; 949 | tracknum++; 950 | } 951 | 952 | xmldoc.SaveFile(file); 953 | fclose(file); 954 | } 955 | } 956 | } 957 | 958 | int Main(int argc, char *argv[]) 959 | { 960 | static constexpr const char* HELP_TEXT = 961 | "dumpsxiso [-h|--help] [-x ] [-s ] \n\n" 962 | " - File name of ISO file (supports any 2352 byte/sector images).\n" 963 | " -x - Specified destination directory of extracted files.\n" 964 | " -s - Outputs an MKPSXISO compatible XML script for later rebuilding.\n" 965 | " -S|--sort-by-dir - Outputs a \"pretty\" XML script where entries are grouped in directories, instead of strictly following their original order on the disc.\n" 966 | " -e|--encode - Codec to encode CDDA/DA audio. wave is default. Supported codecs: " SUPPORTED_CODEC_TEXT "\n" 967 | " -h|--help - Show this help text\n"; 968 | 969 | printf( "DUMPSXISO " VERSION " - PlayStation ISO dumping tool\n" 970 | "2017 Meido-Tek Productions (John \"Lameguy\" Wilbert Villamor/Lameguy64)\n" 971 | "2020 Phoenix (SadNES cITy)\n" 972 | "2021-2022 Silent, Chromaryu, G4Vi, and spicyjpeg\n\n" ); 973 | 974 | if (argc == 1) 975 | { 976 | printf(HELP_TEXT); 977 | return EXIT_SUCCESS; 978 | } 979 | 980 | for (char** args = argv+1; *args != nullptr; args++) 981 | { 982 | // Is it a switch? 983 | if ((*args)[0] == '-') 984 | { 985 | if (ParseArgument(args, "h", "help")) 986 | { 987 | printf(HELP_TEXT); 988 | return EXIT_SUCCESS; 989 | } 990 | if (auto outPath = ParsePathArgument(args, "x"); outPath.has_value()) 991 | { 992 | param::outPath = outPath->lexically_normal(); 993 | continue; 994 | } 995 | if (auto xmlPath = ParsePathArgument(args, "s"); xmlPath.has_value()) 996 | { 997 | param::xmlFile = *xmlPath; 998 | continue; 999 | } 1000 | if (ParseArgument(args, "S", "sort-by-dir")) 1001 | { 1002 | param::outputSortedByDir = true; 1003 | continue; 1004 | } 1005 | if(auto encodingStr = ParseStringArgument(args, "e", "encode"); encodingStr.has_value()) 1006 | { 1007 | unsigned i; 1008 | for(i = 0; i < std::size(EncodingCodecs); i++) 1009 | { 1010 | if(CompareICase(*encodingStr, EncodingCodecs[i].codec)) 1011 | { 1012 | break; 1013 | } 1014 | } 1015 | if(i == std::size(EncodingCodecs)) 1016 | { 1017 | printf("Unknown codec: %s\n", (*encodingStr).c_str()); 1018 | printf("Supported codecs: %s\n", SUPPORTED_CODEC_TEXT); 1019 | return EXIT_FAILURE; 1020 | } 1021 | if(EncodingCodecs[i].eaf & SUPPORTED_CODECS) 1022 | { 1023 | param::encodingFormat = EncodingCodecs[i].eaf; 1024 | continue; 1025 | } 1026 | printf("%s", EncodingCodecs[i].notcompiledmessage); 1027 | return EXIT_FAILURE; 1028 | } 1029 | 1030 | // If we reach this point, an unknown parameter was passed 1031 | printf("Unknown parameter: %s\n", *args); 1032 | return EXIT_FAILURE; 1033 | } 1034 | 1035 | if (param::isoFile.empty()) 1036 | { 1037 | param::isoFile = fs::u8path(*args); 1038 | } 1039 | else 1040 | { 1041 | printf("Only one ISO file is supported.\n"); 1042 | return EXIT_FAILURE; 1043 | } 1044 | } 1045 | 1046 | if (param::isoFile.empty()) 1047 | { 1048 | printf("No iso file specified.\n"); 1049 | return EXIT_FAILURE; 1050 | } 1051 | 1052 | 1053 | cd::IsoReader reader; 1054 | 1055 | if (!reader.Open(param::isoFile)) { 1056 | 1057 | printf("ERROR: Cannot open file %" PRFILESYSTEM_PATH "...\n", param::isoFile.lexically_normal().c_str()); 1058 | return EXIT_FAILURE; 1059 | 1060 | } 1061 | 1062 | // Check if file has a valid ISO9660 header 1063 | { 1064 | char sectbuff[2048]; 1065 | //char descid[] = { 0x01, 0x43, 0x44, 0x30, 0x30, 0x31, 0x01 }; 1066 | reader.SeekToSector(16); 1067 | reader.ReadBytes(§buff, 2048); 1068 | if( memcmp(sectbuff, "\1CD001\1", 7) ) 1069 | { 1070 | printf("ERROR: File does not contain a valid ISO9660 file system.\n"); 1071 | return EXIT_FAILURE; 1072 | } 1073 | } 1074 | 1075 | if (!param::outPath.empty()) 1076 | { 1077 | printf("Output directory : %" PRFILESYSTEM_PATH "\n", param::outPath.lexically_normal().c_str()); 1078 | } 1079 | 1080 | ParseISO(reader); 1081 | return EXIT_SUCCESS; 1082 | } 1083 | --------------------------------------------------------------------------------