diff --git a/NEWS.md b/NEWS.md index ecd03585f..00a790edb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,16 @@ ## v2.6 (released 2025/??/??) +* New parsers: + * ICO: Windows icons and cursors. Supports most icons and cursors designed + for Windows 3.x and later (including Windows Vista PNG-format icons), + plus the old Windows 1.x format. Only the "best" version for each icon + is selected for thumbnailing. (Largest size and highest color depth.) + * Icon thumbnailing is not actually enabled on Windows and Linux systems + at the moment, since it may conflict with system icon handling. + It's mostly only usable for rpcli and for use as a subclass elsewhere. + * Partially fixes #170: Icon files: ICNS, ICO, EXE + * New parser features: * Xbox360_STFS: Fix titles for some packages that were authored incorrectly and have mojibake titles. Specifically, the titles were originally encoded @@ -9,6 +19,14 @@ cp1252 when being converted to UTF-16BE. * Fixes #450: X360 - Non-Latin Titles appearing as mojibake * Reported by @Masamune3210. + * EXE: The application icon can now be extracted using rpcli. + * ISO: AUTORUN.INF is now parsed. This includes a tab showing the contents + of AUTORUN.INF, as well as the disc icon from a .ico or .exe/.dll file. + * Fixes #232: ISO: Parse autorun.inf + * ISO: Joliet file systems are now partially supported. This was added to + handle older Windows disc images that use a long filename for the icon, + and the disc is authored with Joliet for long filenames but an old version + of ISO-9660, resulting in 8.3 filenames in the ISO-9660 directories. * Bug fixes: * Windows: Work around a potential libpng crash when attempting to read @@ -19,6 +37,7 @@ * Other changes: * rpcli: Added more colorization for warning messages. * rpcli: Refactored console handling into a separate library, libgsvt. + * IsoPartition: Implemented readdir(). Not currently used by anything, though. ## v2.5.1 (released 2025/05/10) diff --git a/src/libromdata/CMakeLists.txt b/src/libromdata/CMakeLists.txt index b0cbed41b..5f00fe99c 100644 --- a/src/libromdata/CMakeLists.txt +++ b/src/libromdata/CMakeLists.txt @@ -126,7 +126,6 @@ SET(${PROJECT_NAME}_SRCS disc/GcnPartition_p.cpp disc/GczReader.cpp disc/GdiReader.cpp - disc/IResourceReader.cpp disc/IsoPartition.cpp disc/NASOSReader.cpp disc/NCCHReader.cpp @@ -298,7 +297,6 @@ SET(${PROJECT_NAME}_H Other/exe_mz_structs.h Other/exe_ne_structs.h Other/exe_pe_structs.h - Other/exe_res_structs.h Other/macho_structs.h Other/nfp_structs.h @@ -345,7 +343,6 @@ SET(${PROJECT_NAME}_H disc/GcnPartition_p.hpp disc/GczReader.hpp disc/GdiReader.hpp - disc/IResourceReader.hpp disc/IsoPartition.hpp disc/MultiTrackSparseDiscReader.hpp disc/NASOSReader.hpp diff --git a/src/libromdata/Console/GameCube.cpp b/src/libromdata/Console/GameCube.cpp index 70bdb1f3e..b9d22bdcd 100644 --- a/src/libromdata/Console/GameCube.cpp +++ b/src/libromdata/Console/GameCube.cpp @@ -1550,10 +1550,11 @@ int GameCube::loadFieldData(void) // - 21.29: IOS version. (21.29 == v5405) IFst::Dir *const dirp = d->updatePartition->opendir("/_sys/"); if (dirp) { - IFst::DirEnt *dirent; + const IFst::DirEnt *dirent; while ((dirent = d->updatePartition->readdir(dirp)) != nullptr) { - if (!dirent->name || dirent->type != DT_REG) + if (!dirent->name || dirent->type != DT_REG) { continue; + } // Check for a retail System Menu. if (dirent->name[0] == 'R') { diff --git a/src/libromdata/Console/PlayStationDisc.cpp b/src/libromdata/Console/PlayStationDisc.cpp index abd59b3f1..e507d098f 100644 --- a/src/libromdata/Console/PlayStationDisc.cpp +++ b/src/libromdata/Console/PlayStationDisc.cpp @@ -213,15 +213,16 @@ int PlayStationDiscPrivate::loadSystemCnf(const IsoPartitionPtr &pt) return ret; } - // CNF file should be less than 2048 bytes. + // CNF file should be 2048 bytes or less. + static constexpr size_t SYSTEM_CNF_SIZE_MAX = 2048; const off64_t fileSize = f_system_cnf->size(); - if (fileSize > 2048) { + if (fileSize > static_cast(SYSTEM_CNF_SIZE_MAX)) { return -ENOMEM; } // Read the entire file into memory. - char buf[2049]; - size_t size = f_system_cnf->read(buf, 2048); + char buf[SYSTEM_CNF_SIZE_MAX + 1]; + size_t size = f_system_cnf->read(buf, SYSTEM_CNF_SIZE_MAX); if (size != static_cast(fileSize)) { // Short read. return -EIO; diff --git a/src/libromdata/Media/ISO.cpp b/src/libromdata/Media/ISO.cpp index dd7487f50..3b35ad9dc 100644 --- a/src/libromdata/Media/ISO.cpp +++ b/src/libromdata/Media/ISO.cpp @@ -23,10 +23,20 @@ using namespace LibRpBase; using namespace LibRpFile; using namespace LibRpText; +// ISO-9660 file system access for AUTORUN.INF +#include "../disc/IsoPartition.hpp" +#include "ini.h" + +// Windows icon handler +#include "../Other/EXE.hpp" +#include "librptexture/fileformat/ICO.hpp" +using namespace LibRpTexture; + // C++ STL classes -#include using std::array; +using std::map; using std::string; +using std::unique_ptr; using std::vector; namespace LibRomData { @@ -79,9 +89,14 @@ public: // actual sector information. unsigned int sector_offset; - // UDF version - // TODO: Descriptors? - const char *s_udf_version; + // Joliet level + enum class JolietSVDType : uint8_t { + None = 0, + UCS2_Level1 = 1, // NOTE: UCS-2 BE + UCS2_Level2 = 2, // NOTE: UCS-2 BE + UCS2_Level3 = 3, // NOTE: UCS-2 BE + }; + JolietSVDType jolietSVDType; public: // El Torito boot catalog LBA. (present if non-zero) @@ -95,6 +110,11 @@ public: }; uint32_t boot_platforms; +public: + // UDF version + // TODO: Descriptors? + const char *s_udf_version; + public: /** * Check additional volume descirptors. @@ -189,6 +209,48 @@ public: { return (likely(discType != DiscType::CDi) ? lm32.he : be32_to_cpu(lm32.be)); } + +public: + // Icon + rp_image_ptr img_icon; + + // IsoPartition + IsoPartitionPtr isoPartition; + + /** + * Open the ISO-9660 partition. + * @return 0 on success; negative POSIX error code on error. + */ + int openIsoPartition(void); + + // AUTORUN.INF contents + // TODO: Concatenate the section and key names. + // For now, only handling the "[autorun]" section. + // Keys are stored as-is, without concatenation. + map autorun_inf; + + /** + * ini.h callback for parsing AUTORUN.INF. + * @param user [in] User data parameter (this) + * @param section [in] Section name + * @param name [in] Value name + * @param value [in] Value + * @return 0 to continue; 1 to stop. + */ + static int parse_autorun_inf(void *user, const char *section, const char *name, const char *value); + + /** + * Load AUTORUN.INF. + * AUTORUN.INF will be loaded into autorun_inf. + * @return 0 on success; negative POSIX error code on error. + */ + int loadAutorunInf(void); + + /** + * Load the icon. + * @return Icon, or nullptr on error. + */ + rp_image_const_ptr loadIcon(void); }; ROMDATA_IMPL(ISO) @@ -227,9 +289,10 @@ ISOPrivate::ISOPrivate(const IRpFilePtr &file) : super(file, &romDataInfo) , discType(DiscType::Unknown) , sector_offset(0) - , s_udf_version(nullptr) + , jolietSVDType(JolietSVDType::None) , boot_catalog_LBA(0) , boot_platforms(0) + , s_udf_version(nullptr) { // Clear the disc header structs. memset(&pvd, 0, sizeof(pvd)); @@ -274,6 +337,7 @@ void ISOPrivate::checkVolumeDescriptors(void) // Found the terminator. foundTerminator = true; break; + case ISO_VDT_BOOT_RECORD: if (boot_LBA != 0) break; @@ -283,6 +347,42 @@ void ISOPrivate::checkVolumeDescriptors(void) boot_LBA = le32_to_cpu(vd.boot.boot_catalog_addr); } break; + + case ISO_VDT_SUPPLEMENTARY: { + if (vd.header.version == ISO_VD_VERSION) { + // Check the escape sequences. + // Escape sequence format: '%', '/', x + const char *const p_end = &vd.pri.svd_escape_sequences[sizeof(vd.pri.svd_escape_sequences)-3]; + for (const char *p = vd.pri.svd_escape_sequences; p < p_end && *p != '\0'; p++) { + if (p[0] != '%' || p[1] != '/') { + continue; + } + + // Check if this is a valid UCS-2 level seqeunce. + // NOTE: Using the highest level specified. + JolietSVDType newType = JolietSVDType::None; + switch (p[2]) { + case '@': + newType = JolietSVDType::UCS2_Level1; + break; + case 'C': + newType = JolietSVDType::UCS2_Level2; + break; + case 'E': + newType = JolietSVDType::UCS2_Level3; + break; + default: + break; + } + + if (jolietSVDType < newType) { + jolietSVDType = newType; + } + } + } + break; + } + default: break; } @@ -573,6 +673,215 @@ void ISOPrivate::addPVDTimestamps_metaData(RomMetaData *metaData, const T *pvd) pvd_time_to_unix_time(&pvd->btime)); } +/** + * Open the ISO-9660 partition. + * @return 0 on success; negative POSIX error code on error. + */ +int ISOPrivate::openIsoPartition(void) +{ + if (isoPartition) { + // ISO-9660 partition is already open. + return 0; + } else if (!file) { + // File is not open. + return -EBADF; + } + + isoPartition = std::make_shared(file, 0, 0); + if (!isoPartition->isOpen()) { + // Unable to open the ISO-9660 file system. + // TODO: Better error code? + isoPartition.reset(); + return -EIO; + } + + return 0; +} + +/** + * ini.h callback for parsing AUTORUN.INF. + * @param user [in] User data parameter (this) + * @param section [in] Section name + * @param name [in] Value name + * @param value [in] Value + * @return 0 to continue; 1 to stop. + */ +int ISOPrivate::parse_autorun_inf(void *user, const char *section, const char *name, const char *value) +{ + // TODO: Character encoding? Assuming ASCII. + + // TODO: Concatenate the section and key names. + // For now, only handling the "[autorun]" section. +#if 0 + string s_name; + if (section[0] != '\0') { + s_name = section; + s_name += '|'; + } + s_name += name; +#endif + if (strcasecmp(section, "autorun") != 0) { + // Not "[autorun]". + return 0; + } + + // Convert the name to lowercase. + // TODO: Store the original case elsewhere? + string s_name = name; + std::transform(s_name.begin(), s_name.end(), s_name.begin(), + [](char c) noexcept -> char { return std::tolower(c); }); + + // Save the value for later. + ISOPrivate *const d = static_cast(user); + auto ret = d->autorun_inf.emplace(std::move(s_name), value); + // NOTE: This will stop processing if a duplicate key is found. + return (ret.second ? 0 : 1); +} + +/** + * Load AUTORUN.INF. + * AUTORUN.INF will be loaded into autorun_inf. + * @return 0 on success; negative POSIX error code on error. + */ +int ISOPrivate::loadAutorunInf(void) +{ + if (!autorun_inf.empty()) { + // AUTORUN.INF is already loaded. + return 0; + } + + // Make sure the ISO-9660 file system is open. + int ret = openIsoPartition(); + if (ret != 0) { + return ret; + } + + // Attempt to load AUTORUN.INF from the ISO-9660 file system. + IRpFilePtr f_autorun_inf = isoPartition->open("/AUTORUN.INF"); + if (!f_autorun_inf) { + // Unable to open AUTORUN.INF. + return -isoPartition->lastError(); + } + + // AUTORUN.INF should be 2048 bytes or less. + static constexpr size_t AUTORUN_INF_SIZE_MAX = 2048; + const off64_t autorun_inf_size = f_autorun_inf->size(); + if (autorun_inf_size > static_cast(AUTORUN_INF_SIZE_MAX)) { + // File is too big. + return -ENOMEM; + } + + // Read the entire file into memory. + char buf[AUTORUN_INF_SIZE_MAX + 1]; + size_t size = f_autorun_inf->read(buf, AUTORUN_INF_SIZE_MAX); + if (size != static_cast(autorun_inf_size)) { + // Short read. + return {}; + } + buf[static_cast(autorun_inf_size)] = '\0'; + + // Parse AUTORUN.INF. + // TODO: Save other AUTORUN data for a tab? + ret = ini_parse_string(buf, parse_autorun_inf, this); + if (ret < 0) { + // Failed to parse AUTORUN.INF. + autorun_inf.clear(); + return -EIO; + } + return 0; +} + +/** + * Load the icon. + * @return Icon, or nullptr on error. + */ +rp_image_const_ptr ISOPrivate::loadIcon(void) +{ + if (img_icon) { + // Icon has already been loaded. + return img_icon; + } else if (!this->isValid || static_cast(this->discType) < 0) { + // Can't load the icon. + return {}; + } + + // Make sure the ISO-9660 file system is open. + int ret = openIsoPartition(); + if (ret != 0) { + return {}; + } + + // Make sure AUTORUN.INF is loaded. + ret = loadAutorunInf(); + if (ret != 0 || autorun_inf.empty()) { + // Unable to load AUTORUN.INF. + return {}; + } + + // Get the icon filename. + // TODO: Concatenate the section and key names. + // For now, only handling the "[autorun]" section. + auto iter = autorun_inf.find("icon"); + if (iter == autorun_inf.end()) { + // No icon... + return {}; + } + string icon_filename = iter->second; + + // Check if there's an icon index specified. + // - Positive: Zero-based index + // - Negative: Resource ID + int iconindex = 0; // default is "first icon" + size_t dotpos = icon_filename.find_last_of('.'); + size_t commapos = icon_filename.find_last_of(','); + if (commapos < (icon_filename.size()-1) && dotpos < commapos) { + // Found an icon index. + char *endptr = nullptr; + iconindex = strtol(&icon_filename[commapos+1], &endptr, 10); + if (*endptr == '\0') { + // Icon index is valid. Use it. + icon_filename.resize(commapos); + } else { + // Icon index is invalid. + iconindex = 0; + } + } + + // Open the icon file from the disc. + IRpFilePtr f_file = isoPartition->open(icon_filename.c_str()); + if (!f_file) { + // Unable to open the icon file. + return {}; + } + + // Use the file extension to determine the reader. + // NOTE: May be better to check the file header... + rp_image_const_ptr icon; + + const size_t icon_filename_size = icon_filename.size(); + if (icon_filename_size > 4) { + if (!strncasecmp(&icon_filename[icon_filename_size-4], ".exe", 4) || + !strncasecmp(&icon_filename[icon_filename_size-4], ".dll", 4)) + { + // EXE or DLL. + unique_ptr exe(new EXE(f_file)); + if (exe->isValid()) { + icon = exe->loadSpecificIcon(iconindex); + } + } + } + + if (!icon) { + // No EXE or DLL. Try plain .ico. + unique_ptr ico(new ICO(f_file)); + if (ico->isValid()) { + icon = ico->image(); + } + } + + return icon; +} + /** ISO **/ /** @@ -662,7 +971,19 @@ ISO::ISO(const IRpFilePtr &file) } } -/** ROM detection functions. **/ +/** + * Close the opened file. + */ +void ISO::close(void) +{ + RP_D(ISO); + d->isoPartition.reset(); + + // Call the superclass function. + super::close(); +} + +/** ROM detection functions **/ /** * Check for a valid PVD. @@ -774,6 +1095,92 @@ const char *ISO::systemName(unsigned int type) const return sysNames[sysID][type & SYSNAME_TYPE_MASK]; } +/** + * Get a bitfield of image types this class can retrieve. + * @return Bitfield of supported image types. (ImageTypesBF) + */ +uint32_t ISO::supportedImageTypes_static(void) +{ + return IMGBF_INT_ICON; +} + +/** + * Get a bitfield of image types this object can retrieve. + * @return Bitfield of supported image types. (ImageTypesBF) + */ +uint32_t ISO::supportedImageTypes(void) const +{ + return supportedImageTypes_static(); +} + +/** + * Get a list of all available image sizes for the specified image type. + * @param imageType Image type. + * @return Vector of available image sizes, or empty vector if no images are available. + */ +vector ISO::supportedImageSizes_static(ImageType imageType) +{ + ASSERT_supportedImageSizes(imageType); + + switch (imageType) { + case IMG_INT_ICON: + // Assuming 32x32. + return {{nullptr, 32, 32, 0}}; + default: + break; + } + + // Unsupported image type. + return {}; +} + +/** + * Get a list of all available image sizes for the specified image type. + * @param imageType Image type. + * @return Vector of available image sizes, or empty vector if no images are available. + */ +vector ISO::supportedImageSizes(ImageType imageType) const +{ + ASSERT_supportedImageSizes(imageType); + + switch (imageType) { + case IMG_INT_ICON: + // Assuming 32x32. + // TODO: Load the icon and check? + return {{nullptr, 32, 32, 0}}; + default: + break; + } + + // Unsupported image type. + return {}; +} + +/** + * Get image processing flags. + * + * These specify post-processing operations for images, + * e.g. applying transparency masks. + * + * @param imageType Image type. + * @return Bitfield of ImageProcessingBF operations to perform. + */ +uint32_t ISO::imgpf(ImageType imageType) const +{ + ASSERT_imgpf(imageType); + + uint32_t ret = 0; + switch (imageType) { + case IMG_INT_ICON: + // TODO: Use nearest-neighbor for < 64x64. + break; + + default: + break; + } + return ret; +} + /** * Load field data. * Called by RomData::fields() if the field data hasn't been loaded yet. @@ -793,7 +1200,7 @@ int ISO::loadFieldData(void) return -EIO; } - d->fields.reserve(18); // Maximum of 18 fields. + d->fields.reserve(19); // Maximum of 19 fields. // NOTE: All fields are space-padded. (0x20, ' ') // TODO: ascii_to_utf8()? @@ -839,7 +1246,7 @@ int ISO::loadFieldData(void) d->fields.addField_string(C_("ISO", "Sector Format"), sector_format); switch (d->discType) { - case ISOPrivate::DiscType::ISO9660: + case ISOPrivate::DiscType::ISO9660: { // ISO-9660 d->fields.setTabName(0, C_("ISO", "ISO-9660 PVD")); @@ -867,7 +1274,17 @@ int ISO::loadFieldData(void) v_boot_platforms_names, 0, d->boot_platforms); } + + // Joliet SVD + const char *const s_joliet_title = C_("ISO", "Joliet"); + if (d->jolietSVDType == ISOPrivate::JolietSVDType::None) { + d->fields.addField_string(s_joliet_title, C_("ISO|JolietSVDType", "Not Present")); + } else { + d->fields.addField_string(s_joliet_title, + fmt::format(FRUN("UCS-2 Level {:d}"), static_cast(d->jolietSVDType))); + } break; + } case ISOPrivate::DiscType::HighSierra: // High Sierra @@ -911,6 +1328,16 @@ int ISO::loadFieldData(void) d->s_udf_version); } + // AUTORUN.INF + int ret = d->loadAutorunInf(); + if (ret == 0) { + // Add the AUTORUN.INF fields as-is. + d->fields.addTab("AUTORUN.INF"); + for (const auto &pair : d->autorun_inf) { + d->fields.addField_string(pair.first.c_str(), pair.second); + } + } + // Finished reading the field data. return static_cast(d->fields.count()); } @@ -955,6 +1382,26 @@ int ISO::loadMetaData(void) return static_cast(d->metaData.count()); } +/** + * Load an internal image. + * Called by RomData::image(). + * @param imageType [in] Image type to load. + * @param pImage [out] Reference to rp_image_const_ptr to store the image in. + * @return 0 on success; negative POSIX error code on error. + */ +int ISO::loadInternalImage(ImageType imageType, rp_image_const_ptr &pImage) +{ + ASSERT_loadInternalImage(imageType, pImage); + RP_D(ISO); + ROMDATA_loadInternalImage_single( + IMG_INT_ICON, // ourImageType + d->file, // file + d->isValid, // isValid + d->discType, // romType + d->img_icon, // imgCache + d->loadIcon); // func +} + /** * Check for "viewed" achievements. * diff --git a/src/libromdata/Media/ISO.hpp b/src/libromdata/Media/ISO.hpp index 3b07a9b4d..0fd569591 100644 --- a/src/libromdata/Media/ISO.hpp +++ b/src/libromdata/Media/ISO.hpp @@ -19,6 +19,7 @@ namespace LibRpBase { namespace LibRomData { ROMDATA_DECL_BEGIN(ISO) +ROMDATA_DECL_CLOSE() public: /** @@ -38,6 +39,9 @@ public: static void addMetaData_PVD(LibRpBase::RomMetaData *metaData, const struct _ISO_Primary_Volume_Descriptor *pvd); ROMDATA_DECL_METADATA() +ROMDATA_DECL_IMGSUPPORT() +ROMDATA_DECL_IMGPF() +ROMDATA_DECL_IMGINT() ROMDATA_DECL_VIEWED_ACHIEVEMENTS() ROMDATA_DECL_END() diff --git a/src/libromdata/Other/EXE.cpp b/src/libromdata/Other/EXE.cpp index de06b62bd..9be6a5dc3 100644 --- a/src/libromdata/Other/EXE.cpp +++ b/src/libromdata/Other/EXE.cpp @@ -16,9 +16,14 @@ using namespace LibRpBase; using namespace LibRpFile; using namespace LibRpText; +// Windows icon handler +#include "librptexture/fileformat/ICO.hpp" +using namespace LibRpTexture; + // C++ STL classes using std::array; using std::string; +using std::unique_ptr; using std::vector; // EXE data @@ -96,6 +101,37 @@ EXEPrivate::EXEPrivate(const IRpFilePtr &file) memset(&hdr, 0, sizeof(hdr)); } +/** + * Make sure the resource reader is loaded. + * @return 0 on success; negative POSIX error code on error. + */ +int EXEPrivate::loadResourceReader(void) +{ + if (rsrcReader) { + // Resource reader is already loaded. + return 0; + } + + int ret = -1; + switch (exeType) { + default: + // Unable to load resources from this type of executable. + return -ENOENT; + + case EXEPrivate::ExeType::NE: + case EXEPrivate::ExeType::COM_NE: + ret = loadNEResourceTable(); + break; + + case EXEPrivate::ExeType::PE: + case EXEPrivate::ExeType::PE32PLUS: + ret = loadPEResourceTypes(); + break; + } + + return ret; +} + /** * Add VS_VERSION_INFO fields. * @@ -342,6 +378,89 @@ void EXEPrivate::addFields_VS_VERSION_INFO(const VS_FIXEDFILEINFO *pVsFfi, const fields.addField_listData("StringFileInfo", ¶ms); } +/** + * Load a specific icon by index. + * @param iconindex Icon index (positive for zero-based index; negative for resource ID) + * @return Icon, or nullptr if not found. + */ +rp_image_const_ptr EXEPrivate::loadSpecificIcon(int iconindex) +{ + if (!this->isValid || static_cast(this->exeType) < 0) { + // Can't load the icon. + return {}; + } + + // Make sure the the resource reader is loaded. + int ret = loadResourceReader(); + if (ret != 0 || !rsrcReader) { + // No resources available. + return {}; + } + + uint16_t type = RT_GROUP_ICON; + if (exeType == ExeType::NE || exeType == ExeType::COM_NE) { + // Windows 1.x/2.x executables don't have RT_GROUP_ICON, + // but do have RT_ICON. If this is Win16, check for + // RT_GROUP_ICON first, then try RT_ICON. + // NOTE: Can't simply check based on if it's a 1.x/2.x + // executable because some EXEs converted to 3.x will + // still show up as 1.x/2.x. + if (rsrcReader->has_resource_type(RT_GROUP_ICON)) { + // We have RT_GROUP_ICON. + } else if (rsrcReader->has_resource_type(RT_ICON)) { + // We have RT_ICON. + type = RT_ICON; + } else { + // No icons... + return {}; + } + } + + // Get the resource ID. + int resID; + if (iconindex == 0) { + // Default icon + resID = -1; + } else if (iconindex > 0) { + // Positive icon index + // This is a zero-based index into the RT_GROUP_ICON table. + resID = rsrcReader->lookup_resource_ID(RT_GROUP_ICON, iconindex); + if (resID < 0) { + // Not found. + return {}; + } + } else { + // Negative icon index + // This is an actual resource ID. + resID = abs(iconindex); + } + + // Attempt to load the default icon. + unique_ptr ico(new ICO(rsrcReader, type, resID, -1)); + if (!ico->isValid()) { + // Unable to load the default icon. + return {}; + } + + // Return the icon's image. + return ico->image(); +} + +/** + * Load the icon. + * @return Icon, or nullptr on error. + */ +rp_image_const_ptr EXEPrivate::loadIcon(void) +{ + if (img_icon) { + // Icon has already been loaded. + return img_icon; + } + + // Load icon 0. + return loadSpecificIcon(0); +} + /** MZ-specific **/ /** @@ -936,6 +1055,92 @@ const char *EXE::systemName(unsigned int type) const return C_("EXE", "Unknown EXE"); } +/** + * Get a bitfield of image types this class can retrieve. + * @return Bitfield of supported image types. (ImageTypesBF) + */ +uint32_t EXE::supportedImageTypes_static(void) +{ + return IMGBF_INT_ICON; +} + +/** + * Get a bitfield of image types this object can retrieve. + * @return Bitfield of supported image types. (ImageTypesBF) + */ +uint32_t EXE::supportedImageTypes(void) const +{ + return supportedImageTypes_static(); +} + +/** + * Get a list of all available image sizes for the specified image type. + * @param imageType Image type. + * @return Vector of available image sizes, or empty vector if no images are available. + */ +vector EXE::supportedImageSizes_static(ImageType imageType) +{ + ASSERT_supportedImageSizes(imageType); + + switch (imageType) { + case IMG_INT_ICON: + // Assuming 32x32. + return {{nullptr, 32, 32, 0}}; + default: + break; + } + + // Unsupported image type. + return {}; +} + +/** + * Get a list of all available image sizes for the specified image type. + * @param imageType Image type. + * @return Vector of available image sizes, or empty vector if no images are available. + */ +vector EXE::supportedImageSizes(ImageType imageType) const +{ + ASSERT_supportedImageSizes(imageType); + + switch (imageType) { + case IMG_INT_ICON: + // Assuming 32x32. + // TODO: Load the icon and check? + return {{nullptr, 32, 32, 0}}; + default: + break; + } + + // Unsupported image type. + return {}; +} + +/** + * Get image processing flags. + * + * These specify post-processing operations for images, + * e.g. applying transparency masks. + * + * @param imageType Image type. + * @return Bitfield of ImageProcessingBF operations to perform. + */ +uint32_t EXE::imgpf(ImageType imageType) const +{ + ASSERT_imgpf(imageType); + + uint32_t ret = 0; + switch (imageType) { + case IMG_INT_ICON: + // TODO: Use nearest-neighbor for < 64x64. + break; + + default: + break; + } + return ret; +} + /** * Load field data. * Called by RomData::fields() if the field data hasn't been loaded yet. @@ -1047,23 +1252,7 @@ int EXE::loadMetaData(void) // We can parse fields for NE (Win16) and PE (Win32) executables, // if they have a resource section. - int ret = -1; - switch (d->exeType) { - default: - // Cannot load any metadata... - return 0; - - case EXEPrivate::ExeType::NE: - case EXEPrivate::ExeType::COM_NE: - ret = d->loadNEResourceTable(); - break; - - case EXEPrivate::ExeType::PE: - case EXEPrivate::ExeType::PE32PLUS: - ret = d->loadPEResourceTypes(); - break; - } - + int ret = d->loadResourceReader(); if (ret != 0 || !d->rsrcReader) { // No resources available. return 0; @@ -1164,6 +1353,51 @@ bool EXE::hasDangerousPermissions(void) const #endif /* ENABLE_XML */ } +/** + * Load an internal image. + * Called by RomData::image(). + * @param imageType [in] Image type to load. + * @param pImage [out] Reference to rp_image_const_ptr to store the image in. + * @return 0 on success; negative POSIX error code on error. + */ +int EXE::loadInternalImage(ImageType imageType, rp_image_const_ptr &pImage) +{ + ASSERT_loadInternalImage(imageType, pImage); + RP_D(EXE); + ROMDATA_loadInternalImage_single( + IMG_INT_ICON, // ourImageType + d->file, // file + d->isValid, // isValid + d->exeType, // romType + d->img_icon, // imgCache + d->loadIcon); // func +} + +/** + * Load a specific icon by index. + * @param iconindex Icon index (positive for zero-based index; negative for resource ID) + * @return Icon, or nullptr if not found. + */ +rp_image_const_ptr EXE::loadSpecificIcon(int iconindex) +{ + RP_D(EXE); + if (iconindex == 0) { + // Main icon. See if it's already loaded. + if (d->img_icon) { + // Icon has already been loaded. + return d->img_icon; + } + } + + rp_image_const_ptr icon = d->loadSpecificIcon(iconindex); + if (iconindex == 0) { + // Cache the main icon. + d->img_icon = icon; + } + + return icon; +} + /** * Check for "viewed" achievements. * diff --git a/src/libromdata/Other/EXE.hpp b/src/libromdata/Other/EXE.hpp index 668d3bdac..3a44aad82 100644 --- a/src/libromdata/Other/EXE.hpp +++ b/src/libromdata/Other/EXE.hpp @@ -2,7 +2,7 @@ * ROM Properties Page shell extension. (libromdata) * * EXE.hpp: DOS/Windows executable reader. * * * - * Copyright (c) 2016-2023 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -15,6 +15,18 @@ namespace LibRomData { ROMDATA_DECL_BEGIN(EXE) ROMDATA_DECL_DANGEROUS() ROMDATA_DECL_METADATA() +ROMDATA_DECL_IMGSUPPORT() +ROMDATA_DECL_IMGPF() +ROMDATA_DECL_IMGINT() + +public: + /** + * Load a specific icon by index. + * @param iconindex Icon index (positive for zero-based index; negative for resource ID) + * @return Icon, or nullptr if not found. + */ + LibRpTexture::rp_image_const_ptr loadSpecificIcon(int iconindex); + ROMDATA_DECL_VIEWED_ACHIEVEMENTS() ROMDATA_DECL_END() diff --git a/src/libromdata/Other/EXE_p.hpp b/src/libromdata/Other/EXE_p.hpp index 331766b81..6879e8a30 100644 --- a/src/libromdata/Other/EXE_p.hpp +++ b/src/libromdata/Other/EXE_p.hpp @@ -17,6 +17,8 @@ #include "exe_le_structs.h" #include "disc/PEResourceReader.hpp" +using LibRpBase::IResourceReader; +using LibRpBase::IResourceReaderPtr; // Uninitialized vector class #include "uvector.h" @@ -100,6 +102,12 @@ public: // Resource reader IResourceReaderPtr rsrcReader; + /** + * Make sure the resource reader is loaded. + * @return 0 on success; negative POSIX error code on error. + */ + int loadResourceReader(void); + /** * Add VS_VERSION_INFO fields. * @@ -111,6 +119,22 @@ public: */ void addFields_VS_VERSION_INFO(const VS_FIXEDFILEINFO *pVsFfi, const IResourceReader::StringFileInfo *pVsSfi); + // Icon + LibRpTexture::rp_image_const_ptr img_icon; + + /** + * Load a specific icon by index. + * @param iconindex Icon index (positive for zero-based index; negative for resource ID) + * @return Icon, or nullptr if not found. + */ + LibRpTexture::rp_image_const_ptr loadSpecificIcon(int iconindex); + + /** + * Load the icon. + * @return Icon, or nullptr on error. + */ + LibRpTexture::rp_image_const_ptr loadIcon(void); + /** MZ-specific **/ /** diff --git a/src/libromdata/RomDataFactory.cpp b/src/libromdata/RomDataFactory.cpp index 51ade7c2f..e36bd853e 100644 --- a/src/libromdata/RomDataFactory.cpp +++ b/src/libromdata/RomDataFactory.cpp @@ -306,7 +306,8 @@ static const array romDataFns_header = {{ // so they should go at the end of the address=0 section. #ifdef _WIN32 // NOTE: Windows provides its own thumbnail and metadata extraction for EXEs. - GetRomDataFns(EXE, ATTR_HAS_DPOVERLAY), + // NOTE 2: EXE class does support thumbnailing now, but it shouldn't be registered as such. + GetRomDataFns(EXE, /*ATTR_HAS_THUMBNAIL |*/ ATTR_HAS_DPOVERLAY), #else /* !_WIN32 */ GetRomDataFns(EXE, ATTR_HAS_DPOVERLAY | ATTR_HAS_METADATA), // TODO: Thumbnailing on non-Windows platforms. #endif /* _WIN32 */ @@ -1153,15 +1154,19 @@ static void init_supportedFileExtensions(void) static constexpr unsigned int FFF_ATTRS = ATTR_HAS_THUMBNAIL | ATTR_HAS_METADATA; const vector &vec_exts_fileFormat = FileFormatFactory::supportedFileExtensions(); for (const char *ext : vec_exts_fileFormat) { + // Explicitly prevent thumbnailing of ".ico" and ".cur" on Windows. + const bool block_thumbnail = !strcmp(ext, ".ico") || !strcmp(ext, ".cur"); + unsigned int attrs = (unlikely(block_thumbnail)) ? ATTR_HAS_METADATA : FFF_ATTRS; + auto iter = map_exts.find(ext); if (iter != map_exts.end()) { // We already had this extension. // Update its attributes. - iter->second |= FFF_ATTRS; + iter->second |= attrs; } else { // First time encountering this extension. - map_exts[ext] = FFF_ATTRS; - vec_exts.emplace_back(ext, FFF_ATTRS); + map_exts[ext] = attrs; + vec_exts.emplace_back(ext, attrs); } } diff --git a/src/libromdata/disc/GcnFst.cpp b/src/libromdata/disc/GcnFst.cpp index 93c0d04ce..2bc3be57b 100644 --- a/src/libromdata/disc/GcnFst.cpp +++ b/src/libromdata/disc/GcnFst.cpp @@ -422,14 +422,15 @@ IFst::Dir *GcnFst::opendir(const char *path) return nullptr; } - IFst::Dir *dirp = new IFst::Dir(this); - d->fstDirCount++; // TODO: Better way to get dir_idx? - dirp->dir_idx = static_cast(fst_entry - d->fstData); + const int dir_idx = static_cast(fst_entry - d->fstData); + IFst::Dir *dirp = new IFst::Dir(this, dir_idx); + d->fstDirCount++; // Initialize the entry to this directory. // readdir() will automatically seek to the next entry. - dirp->entry.ptnum = 0; // not used for GCN/Wii + dirp->entry.extra = nullptr; // not used for GCN/Wii + dirp->entry.ptnum = 0; // not used for GCN/Wii dirp->entry.idx = dirp->dir_idx; dirp->entry.type = DT_DIR; dirp->entry.name = d->entry_name(fst_entry); @@ -447,7 +448,7 @@ IFst::Dir *GcnFst::opendir(const char *path) * @return IFst::DirEnt*, or nullptr if end of directory or on error. * (End of directory does not set lastError; an error does.) */ -IFst::DirEnt *GcnFst::readdir(IFst::Dir *dirp) +const IFst::DirEnt *GcnFst::readdir(IFst::Dir *dirp) { assert(dirp != nullptr); assert(dirp->parent == this); diff --git a/src/libromdata/disc/GcnFst.hpp b/src/libromdata/disc/GcnFst.hpp index 38eec0cca..24c9f0503 100644 --- a/src/libromdata/disc/GcnFst.hpp +++ b/src/libromdata/disc/GcnFst.hpp @@ -68,7 +68,7 @@ public: * @return DirEnt*, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ - DirEnt *readdir(Dir *dirp) final; + const DirEnt *readdir(Dir *dirp) final; /** * Close an opened directory. diff --git a/src/libromdata/disc/GcnPartition.cpp b/src/libromdata/disc/GcnPartition.cpp index 61b1d79e9..26e7982dc 100644 --- a/src/libromdata/disc/GcnPartition.cpp +++ b/src/libromdata/disc/GcnPartition.cpp @@ -2,7 +2,7 @@ * ROM Properties Page shell extension. (libromdata) * * GcnPartition.cpp: GameCube partition reader. * * * - * Copyright (c) 2016-2024 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -217,7 +217,7 @@ IFst::Dir *GcnPartition::opendir(const char *path) * @return IFst::DirEnt*, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ -IFst::DirEnt *GcnPartition::readdir(IFst::Dir *dirp) +const IFst::DirEnt *GcnPartition::readdir(IFst::Dir *dirp) { RP_D(GcnPartition); if (!d->fst) { diff --git a/src/libromdata/disc/GcnPartition.hpp b/src/libromdata/disc/GcnPartition.hpp index f7beffa9b..ca38a080c 100644 --- a/src/libromdata/disc/GcnPartition.hpp +++ b/src/libromdata/disc/GcnPartition.hpp @@ -2,7 +2,7 @@ * ROM Properties Page shell extension. (libromdata) * * GcnPartition.hpp: GameCube partition reader. * * * - * Copyright (c) 2016-2023 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -107,7 +107,7 @@ public: * @return IFst::DirEnt, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ - LibRpBase::IFst::DirEnt *readdir(LibRpBase::IFst::Dir *dirp) final; + const LibRpBase::IFst::DirEnt *readdir(LibRpBase::IFst::Dir *dirp) final; /** * Close an opened directory. diff --git a/src/libromdata/disc/IsoPartition.cpp b/src/libromdata/disc/IsoPartition.cpp index 29f5bc8d5..c836ee856 100644 --- a/src/libromdata/disc/IsoPartition.cpp +++ b/src/libromdata/disc/IsoPartition.cpp @@ -19,12 +19,15 @@ using namespace LibRpText; using std::string; using std::unordered_map; +// TODO: HSFS/CDI support? + namespace LibRomData { class IsoPartitionPrivate { public: IsoPartitionPrivate(IsoPartition *q, off64_t partition_offset, int iso_start_offset); + ~IsoPartitionPrivate(); private: RP_DISABLE_COPY(IsoPartitionPrivate) @@ -36,8 +39,15 @@ public: off64_t partition_offset; off64_t partition_size; // Calculated partition size - // ISO primary volume descriptor - ISO_Primary_Volume_Descriptor pvd; + // ISO volume descriptors + ISO_Primary_Volume_Descriptor pvd, svd; + enum class JolietSVDType : uint8_t { + None = 0, + UCS2_Level1 = 1, // NOTE: UCS-2 BE + UCS2_Level2 = 2, // NOTE: UCS-2 BE + UCS2_Level3 = 3, // NOTE: UCS-2 BE + }; + JolietSVDType jolietSVDType; // Directories // - Key: Directory name, WITHOUT leading slash. (Root == empty string) [cp1252] @@ -51,23 +61,70 @@ public: // -1 == unknown int iso_start_offset; + // IFst::Dir* reference counter + int fstDirCount; + + /** + * Is this character a slash or backslash? + * @return True if it is; false if it isn't. + */ + static inline bool is_slash(char c) + { + return (c == '/') || (c == '\\'); + } + +private: /** * Find the last slash or backslash in a path. - * @param path Path. + * (Internal function!) + * @param path Path + * @param size Size of path * @return Last slash or backslash, or nullptr if not found. */ - inline const char *findLastSlash(const char *path) + static inline const char *findLastSlash(const char *path, size_t size) { - const char *sl = strrchr(path, '/'); - const char *const bs = strrchr(path, '\\'); - if (sl && bs) { - if (bs > sl) { - sl = bs; + const char *p = path + size - 1; + for (; size > 0; size--, p--) { + if (is_slash(*p)) { + return p; } } - return (sl ? sl : bs); + return nullptr; } +public: + /** + * Find the last slash or backslash in a path. + * @param path Path + * @return Last slash or backslash, or nullptr if not found. + */ + static inline const char *findLastSlash(const char *path) + { + return findLastSlash(path, strlen(path)); + } + + /** + * Find the last slash or backslash in a path. + * @param path Path + * @return Last slash or backslash, or nullptr if not found. + */ + static inline const char *findLastSlash(const string &path) + { + return findLastSlash(path.c_str(), path.size()); + } + + /** + * Sanitize an incoming path for ISO-9660 lookup. + * + * This function does the following: + * - Converts the path from UTF-8 to cp1252. + * - Removes leading and trailing slashes. + * + * @param path Path to sanitize + * @return Sanitized path (empty string for "/") + */ + static std::string sanitize_path(const char *path); + /** * Look up a directory entry from a base filename and directory. * @param pDir [in] Directory @@ -88,16 +145,16 @@ public: /** * Look up a directory entry from a filename. * @param filename Filename [UTF-8] - * @return ISO directory entry. + * @return ISO directory entry */ const ISO_DirEntry *lookup(const char *filename); /** * Parse an ISO-9660 timestamp. - * @param isofiletime File timestamp. - * @return Unix time. + * @param isofiletime File timestamp + * @return Unix time */ - time_t parseTimestamp(const ISO_Dir_DateTime_t *isofiletime); + static time_t parseTimestamp(const ISO_Dir_DateTime_t *isofiletime); }; /** IsoPartitionPrivate **/ @@ -107,10 +164,13 @@ IsoPartitionPrivate::IsoPartitionPrivate(IsoPartition *q, : q_ptr(q) , partition_offset(partition_offset) , partition_size(0) + , jolietSVDType(JolietSVDType::None) , iso_start_offset(iso_start_offset) + , fstDirCount(0) { - // Clear the PVD struct. + // Clear the Volume Descriptor structs. memset(&pvd, 0, sizeof(pvd)); + memset(&svd, 0, sizeof(svd)); if (!q->m_file) { q->m_lastError = EIO; @@ -146,10 +206,87 @@ IsoPartitionPrivate::IsoPartitionPrivate(IsoPartition *q, return; } + // Attempt to load the Supplementary Volume Descriptor. + // TODO: Keep loading VDs until we reach 0xFF? + size = q->m_file->seekAndRead(partition_offset + ISO_SVD_ADDRESS_2048, &svd, sizeof(svd)); + // Verify the signature and volume descriptor type. + if (size == sizeof(svd) && + svd.header.type == ISO_VDT_SUPPLEMENTARY && svd.header.version == ISO_VD_VERSION && + !memcmp(svd.header.identifier, ISO_VD_MAGIC, sizeof(svd.header.identifier))) + { + // This is a supplementary volume descriptor. + // Check the escape sequences. + // Escape sequence format: '%', '/', x + const char *const p_end = &svd.svd_escape_sequences[sizeof(svd.svd_escape_sequences)-3]; + for (const char *p = svd.svd_escape_sequences; p < p_end && *p != '\0'; p++) { + if (p[0] != '%' || p[1] != '/') { + continue; + } + + // Check if this is a valid UCS-2 level seqeunce. + // NOTE: Using the highest level specified. + JolietSVDType newType = JolietSVDType::None; + switch (p[2]) { + case '@': + newType = JolietSVDType::UCS2_Level1; + break; + case 'C': + newType = JolietSVDType::UCS2_Level2; + break; + case 'E': + newType = JolietSVDType::UCS2_Level3; + break; + default: + break; + } + + if (jolietSVDType < newType) { + jolietSVDType = newType; + } + } + } + // Load the root directory. getDirectory("/"); } +IsoPartitionPrivate::~IsoPartitionPrivate() +{ + assert(fstDirCount == 0); +} + +/** + * Sanitize an incoming path for ISO-9660 lookup. + * + * This function does the following: + * - Converts the path from UTF-8 to cp1252. + * - Removes leading and trailing slashes. + * + * @param path Path to sanitize + * @return Sanitized path (empty string for "/") + */ +std::string IsoPartitionPrivate::sanitize_path(const char *path) +{ + // Remove leading slashes. + while (is_slash(*path)) { + path++; + } + if (*path == '\0') { + // Nothing but slashes? + return {}; + } + + // Convert to cp1252, then remove trailing slashes. + string s_path = utf8_to_cp1252(path, -1); + size_t s_path_len = s_path.size(); + while (s_path_len > 0 && is_slash(s_path[s_path_len-1])) { + s_path_len--; + } + s_path.resize(s_path_len); + + return s_path; +} + /** * Look up a directory entry from a base filename and directory. * @param pDir [in] Directory @@ -167,23 +304,70 @@ const ISO_DirEntry *IsoPartitionPrivate::lookup_int(const DirData_t *pDir, const const ISO_DirEntry *dirEntry_found = nullptr; const uint8_t *p = pDir->data(); const uint8_t *const p_end = p + pDir->size(); - while (p < p_end) { + + // Temporary buffer for converting Joliet UCS-2 filenames to cp1252. + char joliet_cp1252_buf[128]; + + while ((p + sizeof(ISO_DirEntry)) < p_end) { const ISO_DirEntry *dirEntry = reinterpret_cast(p); - if (dirEntry->entry_length < sizeof(*dirEntry)) { - // End of directory. + if (dirEntry->entry_length == 0) { + // Directory entries cannot span multiple sectors in + // multi-sector directories, so if needed, the rest + // of the sector is padded with 00. + // Find the next non-zero byte. + for (p++; p < p_end; p++) { + if (*p != '\0') { + // Found a non-zero byte. + break; + } + } + if (p >= p_end) { + // No more non-zero bytes. + break; + } + continue; + } else if (dirEntry->entry_length < sizeof(*dirEntry)) { + // Invalid directory entry? break; } - const char *const entry_filename = reinterpret_cast(p) + sizeof(*dirEntry); + const char *entry_filename = reinterpret_cast(p) + sizeof(*dirEntry); if (entry_filename + dirEntry->filename_length > reinterpret_cast(p_end)) { // Filename is out of bounds. break; } + // Skip subdirectories with names "\x00" and "\x01". + // These are "special directory identifiers", representing "." and "..", respectively. + if ((dirEntry->flags & ISO_FLAG_DIRECTORY) && dirEntry->filename_length == 1) { + if (static_cast(entry_filename[0]) <= 0x01) { + // Skip this filename. + p += dirEntry->entry_length; + continue; + } + } + + // If using Joliet, the filename is encoded as UCS-2 (UTF-16). + // Use a quick-and-dirty (and not necessarily accurate) conversion to cp1252. + // FIXME: Proper conversion? + uint8_t dirEntry_filename_len = dirEntry->filename_length; + if (jolietSVDType > JolietSVDType::None) { + // dirEntry_filename_len is in bytes, which means it's double + // the number of UCS-2 code points. + // NOTE: UCS-2 *Big-Endian*. + dirEntry_filename_len /= 2; + unsigned int i = 0; + for (; i < dirEntry_filename_len; i++) { + joliet_cp1252_buf[i] = entry_filename[(i * 2) + 1]; + } + joliet_cp1252_buf[i] = '\0'; + entry_filename = joliet_cp1252_buf; + } + // Check the filename. // 1990s and early 2000s CD-ROM games usually have // ";1" filenames, so check for that first. - if (dirEntry->filename_length == filename_len + 2) { + if (dirEntry_filename_len == filename_len + 2) { // +2 length match. // This might have ";1". if (!strncasecmp(entry_filename, filename, filename_len)) { @@ -205,11 +389,19 @@ const ISO_DirEntry *IsoPartitionPrivate::lookup_int(const DirData_t *pDir, const break; } } - } else if (dirEntry->filename_length == filename_len) { + } else if (dirEntry_filename_len == filename_len) { // Exact length match. if (!strncasecmp(entry_filename, filename, filename_len)) { // Found it! - dirEntry_found = dirEntry; + // Verify directory vs. file. + const bool isDir = !!(dirEntry->flags & ISO_FLAG_DIRECTORY); + if (isDir == bFindDir) { + // Directory attribute matches. + dirEntry_found = dirEntry; + } else { + // Not a match. + err = (isDir ? EISDIR : ENOTDIR); + } break; } } @@ -273,7 +465,10 @@ const IsoPartitionPrivate::DirData_t *IsoPartitionPrivate::getDirectory(const ch // Loading the root directory. // Check the root directory entry. - const ISO_DirEntry *const rootdir = &pvd.dir_entry_root; + const ISO_DirEntry *const rootdir = (jolietSVDType > JolietSVDType::None) + ? &svd.dir_entry_root + : &pvd.dir_entry_root; + if (rootdir->size.he > 16*1024*1024) { // Root directory is too big. q->m_lastError = EIO; @@ -299,6 +494,7 @@ const IsoPartitionPrivate::DirData_t *IsoPartitionPrivate::getDirectory(const ch // in which case, we'll need to assume that the // root directory starts at block 20. // TODO: Better heuristics. + // TODO: Find the block that starts with "CD001" instead of this heuristic. if (rootdir->block.he < 20) { // Starting block is invalid. q->m_lastError = EIO; @@ -328,7 +524,7 @@ const IsoPartitionPrivate::DirData_t *IsoPartitionPrivate::getDirectory(const ch if (!pDir) { // Can't find the parent directory. - // getDirectory() already set q->lastError(). + // getDirectory() already set q->m_lastError(). return nullptr; } @@ -336,7 +532,11 @@ const IsoPartitionPrivate::DirData_t *IsoPartitionPrivate::getDirectory(const ch const ISO_DirEntry *const entry = lookup_int(pDir, path, true); if (!entry) { // Not found. - // lookup_int() already set q->lastError(). + // lookup_int() already set q->m_lastError(). + return nullptr; + } else if (!(entry->flags & ISO_FLAG_DIRECTORY)) { + // Entry found, but it's a directory. + q->m_lastError = ENOTDIR; return nullptr; } @@ -369,34 +569,27 @@ const IsoPartitionPrivate::DirData_t *IsoPartitionPrivate::getDirectory(const ch /** * Look up a directory entry from a filename. * @param filename Filename [UTF-8] - * @return ISO directory entry. + * @return ISO directory entry */ const ISO_DirEntry *IsoPartitionPrivate::lookup(const char *filename) { assert(filename != nullptr); assert(filename[0] != '\0'); - RP_Q(IsoPartition); - // Remove leading slashes. - while (*filename == '/') { - filename++; - } - if (filename[0] == 0) { - // Nothing but slashes... - q->m_lastError = EINVAL; - return nullptr; - } + // Sanitize the filename. + // If the return value is an empty string, that means root directory. + string s_filename = sanitize_path(filename); // TODO: Which encoding? // Assuming cp1252... const DirData_t *pDir; // Is this file in a subdirectory? - const char *const sl = findLastSlash(filename); + const char *const sl = findLastSlash(s_filename); if (sl) { // This file is in a subdirectory. - const string s_parentDir = utf8_to_cp1252(filename, static_cast(sl - filename)); - filename = sl + 1; + const string s_parentDir = s_filename.substr(0, sl - s_filename.c_str()); + s_filename.assign(sl + 1); pDir = getDirectory(s_parentDir.c_str()); } else { // Not in a subdirectory. @@ -406,19 +599,18 @@ const ISO_DirEntry *IsoPartitionPrivate::lookup(const char *filename) if (!pDir) { // Error getting the directory. - // getDirectory() has already set q->lastError. + // getDirectory() has already set q->m_lastError. return nullptr; } // Find the file in the directory. - const string s_filename = utf8_to_cp1252(filename, -1); return lookup_int(pDir, s_filename.c_str(), false); } /** * Parse an ISO-9660 timestamp. - * @param isofiletime File timestamp. - * @return Unix time. + * @param isofiletime File timestamp + * @return Unix time */ time_t IsoPartitionPrivate::parseTimestamp(const ISO_Dir_DateTime_t *isofiletime) { @@ -467,7 +659,7 @@ time_t IsoPartitionPrivate::parseTimestamp(const ISO_Dir_DateTime_t *isofiletime IsoPartition::IsoPartition(const IRpFilePtr &discReader, off64_t partition_offset, int iso_start_offset) : super(discReader) , d_ptr(new IsoPartitionPrivate(this, partition_offset, iso_start_offset)) -{} +{ } IsoPartition::~IsoPartition() { @@ -587,68 +779,205 @@ off64_t IsoPartition::partition_size_used(void) const /** IsoPartition **/ -/** GcnFst wrapper functions. **/ +/** IFst wrapper functions **/ -// TODO - -#if 0 /** * Open a directory. - * @param path [in] Directory path. + * @param path [in] Directory path * @return IFst::Dir*, or nullptr on error. */ IFst::Dir *IsoPartition::opendir(const char *path) { RP_D(IsoPartition); - if (!d->fst) { - // FST isn't loaded. - if (d->loadFst() != 0) { - // FST load failed. - // TODO: Errors? - return nullptr; - } + + // Sanitize the path. + // If the return value is an empty string, that means root directory. + string s_path = d->sanitize_path(path); + const IsoPartitionPrivate::DirData_t *const pDir = d->getDirectory(s_path.c_str()); + if (!pDir) { + // Path not found. + // TODO: Return an error code? + return nullptr; } - return d->fst->opendir(path); + // FIXME: Create an IsoFst class? Cannot pass `this` as IFst*. + IFst::Dir *const dirp = new IFst::Dir(nullptr, (void*)pDir); + d->fstDirCount++; + + // Initialize the entry to this directory. + // readdir() will automatically seek to the next entry. + dirp->entry.extra = nullptr; // temporary filename storage + dirp->entry.ptnum = 0; // not used for ISO + dirp->entry.idx = 0; + dirp->entry.type = DT_UNKNOWN; + dirp->entry.name = nullptr; + // offset and size are not valid for directories. + dirp->entry.offset = 0; + dirp->entry.size = 0; + + // Return the IFst::Dir*. + return dirp; } /** * Read a directory entry. - * @param dirp FstDir pointer. + * @param dirp FstDir pointer * @return IFst::DirEnt*, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ -IFst::DirEnt *IsoPartition::readdir(IFst::Dir *dirp) +const IFst::DirEnt *IsoPartition::readdir(IFst::Dir *dirp) { - RP_D(IsoPartition); - if (!d->fst) { - // TODO: Errors? + assert(dirp != nullptr); + //assert(dirp->parent == this); + if (!dirp /*|| dirp->parent != this*/) { + // No directory pointer, or the dirp + // doesn't belong to this IFst. return nullptr; } - return d->fst->readdir(dirp); + const IsoPartitionPrivate::DirData_t *const pDir = + reinterpret_cast(dirp->dir_idx); + const uint8_t *p = pDir->data(); + const uint8_t *const p_end = p + pDir->size(); + p += dirp->entry.idx; + + // NOTE: Using a loop in order to skip files that aren't really files. + const ISO_DirEntry *dirEntry = nullptr; + const char *entry_filename = nullptr; + while (p < p_end) { + dirEntry = reinterpret_cast(p); + if (dirEntry->entry_length == 0) { + // Directory entries cannot span multiple sectors in + // multi-sector directories, so if needed, the rest + // of the sector is padded with 00. + // Find the next non-zero byte. + for (p++; p < p_end; p++) { + if (*p != '\0') { + // Found a non-zero byte. + dirp->entry.idx = static_cast(p - pDir->data()); + break; + } + } + if (p >= p_end) { + // No more non-zero bytes. + dirp->entry.idx = static_cast(pDir->size()); + return nullptr; + } + continue; + } else if (dirEntry->entry_length < sizeof(*dirEntry)) { + // Invalid directory entry? + return nullptr; + } + + entry_filename = reinterpret_cast(p) + sizeof(*dirEntry); + if (entry_filename + dirEntry->filename_length > reinterpret_cast(p_end)) { + // Filename is out of bounds. + return nullptr; + } + + // Skip subdirectories with names "\x00" and "\x01". + // These are "special directory identifiers", representing "." and "..", respectively. + if ((dirEntry->flags & ISO_FLAG_DIRECTORY) && dirEntry->filename_length == 1) { + if (static_cast(entry_filename[0]) <= 0x01) { + // Skip this filename. + dirp->entry.idx += dirEntry->entry_length; + p += dirEntry->entry_length; + continue; + } + } + + // Found a valid file. + break; + } + if (!dirEntry) { + // Could not find a valid file. (End of directory?) + return nullptr; + } + + const bool isDir = !!(dirEntry->flags & ISO_FLAG_DIRECTORY); + if (isDir) { + // Technically, offset/size are valid for directories on ISO-9660, + // but we're going to set them to 0. + dirp->entry.type = DT_DIR; + dirp->entry.offset = 0; + dirp->entry.size = 0; + } else { + RP_D(IsoPartition); + const unsigned int block_size = d->pvd.logical_block_size.he; + dirp->entry.type = DT_REG; + dirp->entry.offset = static_cast(dirEntry->block.he) * block_size; + dirp->entry.size = dirEntry->size.he; + } + + // NOTE: Need to copy the filename in order to have NULL-termination. + // TODO: Remove ";1" from the filename, if present? + char *extra = static_cast(dirp->entry.extra); + delete[] extra; + + // If using Joliet, the filename is encoded as UCS-2 (UTF-16). + // Use a quick-and-dirty (and not necessarily accurate) conversion to cp1252. + // FIXME: Proper conversion? + // TODO: Convert to UTF-8 for readdir()? + RP_D(IsoPartition); + uint8_t dirEntry_filename_len = dirEntry->filename_length; + if (d->jolietSVDType > IsoPartitionPrivate::JolietSVDType::None) { + // dirEntry_filename_len is in bytes, which means it's double + // the number of UCS-2 code points. + // NOTE: UCS-2 *Big-Endian*. + dirEntry_filename_len /= 2; + extra = new char[dirEntry_filename_len + 1]; + unsigned int i = 0; + for (; i < dirEntry_filename_len; i++) { + extra[i] = entry_filename[(i * 2) + 1]; + } + extra[i] = '\0'; + } else { + // TODO: Convert from cp1252 to UTF-8 for readdir()? + extra = new char[dirEntry_filename_len + 1]; + memcpy(extra, entry_filename, dirEntry_filename_len); + extra[dirEntry_filename_len] = '\0'; + } + + dirp->entry.name = extra; + dirp->entry.extra = extra; + + // Next file entry. + dirp->entry.idx += dirEntry->entry_length; + + return &dirp->entry; } /** * Close an opened directory. - * @param dirp FstDir pointer. + * @param dirp FstDir pointer * @return 0 on success; negative POSIX error code on error. */ int IsoPartition::closedir(IFst::Dir *dirp) { - RP_D(IsoPartition); - if (!d->fst) { - // TODO: Errors? - return -EBADF; - } + assert(dirp != nullptr); + //assert(dirp->parent == this); + if (!dirp) { + // No directory pointer. + // In release builds, this is a no-op. + return 0; + } /*else if (dirp->parent != this) { + // The dirp doesn't belong to this IFst. + return -EINVAL; + }*/ - return d->fst->closedir(dirp); + RP_D(IsoPartition); + assert(d->fstDirCount > 0); + if (dirp->entry.extra) { + delete[] static_cast(dirp->entry.extra); + } + delete dirp; + d->fstDirCount--; + return 0; } -#endif /** * Open a file. (read-only) - * @param filename Filename. + * @param filename Filename * @return IRpFile*, or nullptr on error. */ IRpFilePtr IsoPartition::open(const char *filename) @@ -673,6 +1002,7 @@ IRpFilePtr IsoPartition::open(const char *filename) const ISO_DirEntry *const dirEntry = d->lookup(filename); if (!dirEntry) { // Not found. + m_lastError = ENOENT; return nullptr; } @@ -705,9 +1035,11 @@ IRpFilePtr IsoPartition::open(const char *filename) return std::make_shared(this->shared_from_this(), file_addr, dirEntry->size.he); } +/** IsoPartition-specific functions **/ + /** * Get a file's timestamp. - * @param filename Filename. + * @param filename Filename * @return Timestamp, or -1 on error. */ time_t IsoPartition::get_mtime(const char *filename) diff --git a/src/libromdata/disc/IsoPartition.hpp b/src/libromdata/disc/IsoPartition.hpp index 2fab39374..bc7de8e95 100644 --- a/src/libromdata/disc/IsoPartition.hpp +++ b/src/libromdata/disc/IsoPartition.hpp @@ -90,41 +90,41 @@ public: public: /** IFst wrapper functions **/ - // TODO -#if 0 /** * Open a directory. - * @param path [in] Directory path. + * @param path [in] Directory path * @return IFst::Dir*, or nullptr on error. */ LibRpBase::IFst::Dir *opendir(const char *path) final; /** * Read a directory entry. - * @param dirp IFst::Dir pointer. + * @param dirp IFst::Dir pointer * @return IFst::DirEnt, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ - LibRpBase::IFst::DirEnt *readdir(LibRpBase::IFst::Dir *dirp) final; + const LibRpBase::IFst::DirEnt *readdir(LibRpBase::IFst::Dir *dirp) final; /** * Close an opened directory. - * @param dirp IFst::Dir pointer. + * @param dirp IFst::Dir pointer * @return 0 on success; negative POSIX error code on error. */ int closedir(LibRpBase::IFst::Dir *dirp) final; -#endif /** * Open a file. (read-only) - * @param filename Filename. + * @param filename Filename * @return IRpFile*, or nullptr on error. */ LibRpFile::IRpFilePtr open(const char *filename) final; +public: + /** IsoPartition-specific functions **/ + /** * Get a file's timestamp. - * @param filename Filename. + * @param filename Filename * @return Timestamp, or -1 on error. */ time_t get_mtime(const char *filename); diff --git a/src/libromdata/disc/NEResourceReader.cpp b/src/libromdata/disc/NEResourceReader.cpp index 2ce6d74f3..5135755d2 100644 --- a/src/libromdata/disc/NEResourceReader.cpp +++ b/src/libromdata/disc/NEResourceReader.cpp @@ -190,13 +190,14 @@ int NEResourceReaderPrivate::loadResTbl(void) int ret = -EIO; while (pos < rsrc_tbl_size) { // Read the next type ID. - if ((pos + 2) >= rsrc_tbl_size) { + if ((pos + 2) > rsrc_tbl_size) { // I/O error; should be at least 2 bytes left... break; } const NE_TYPEINFO *typeInfo = reinterpret_cast(&rsrcTblData[pos]); const uint16_t rtTypeID = le16_to_cpu(typeInfo->rtTypeID); if (rtTypeID == 0) { + // typeInfo is actually pointing to rscEndTypes. // End of rscTypes[]. ret = 0; break; @@ -244,11 +245,27 @@ int NEResourceReaderPrivate::loadResTbl(void) const NE_NAMEINFO *nameInfo = reinterpret_cast(&rsrcTblData[pos]); pos += sizeof(NE_NAMEINFO); + /** + * NOTE: If the ID doesn't have 0x8000 set, the name is a string. + * The ID is the offset of the string, in bytes relative to the + * beginning of the resource table. The sting is a Pascal string; + * one byte indicating length, followed by the string data. + * ***NOT NULL-TERMINATED!*** + * + * We'll allow it for now as-is, since CALC.EXE, and probably + * other Windows 3.1 programs, have RT_GROUP_ICONs that use + * string names. + * + * TODO: How does Windows 3.1 select the application icon if + * the icons have names? + */ const uint16_t rnID = le16_to_cpu(nameInfo->rnID); +#if 0 if (!(rnID & 0x8000)) { // Resource name is a string. Not supported. continue; } +#endif // Add the resource information. auto &entry = dir[entriesRead]; @@ -260,8 +277,9 @@ int NEResourceReaderPrivate::loadResTbl(void) entry.len = le16_to_cpu(nameInfo->rnLength) << rscAlignShift; entriesRead++; } - if (isErr) + if (isErr) { break; + } // Shrink the vector in case we skipped some entries. dir.resize(entriesRead); @@ -646,11 +664,13 @@ IRpFilePtr NEResourceReader::open(uint16_t type, int id, int lang) // Get the directory for the specified type. auto iter_dir = d->res_types.find(type); - if (iter_dir == d->res_types.end()) + if (iter_dir == d->res_types.end()) { return nullptr; + } auto &dir = iter_dir->second; - if (dir.empty()) + if (dir.empty()) { return nullptr; + } const NEResourceReaderPrivate::ResTblEntry *entry = nullptr; if (id == -1) { @@ -781,4 +801,56 @@ int NEResourceReader::load_VS_VERSION_INFO(int id, int lang, VS_FIXEDFILEINFO *p return 0; } +/** + * Look up a resource ID given a zero-based index. + * Mostly useful for icon indexes. + * + * @param type [in] Resource type ID + * @param index [in] Zero-based index + * @param lang [in] Language ID (-1 for "first entry") + * @return Resource ID, or negative POSIX error code on error. + */ +int NEResourceReader::lookup_resource_ID(int type, int index) +{ + if (index < 0) { + return -EINVAL; + } + + // NOTE: Type and resource IDs have the high bit set for integers. + // We're only supporting integer IDs, so set the high bits here. + type |= 0x8000; + + // Get the resource directory for this type. + RP_D(const NEResourceReader); + auto iter_find = d->res_types.find(type); + if (iter_find == d->res_types.end()) { + // Not found. + return -ENOENT; + } + + const NEResourceReaderPrivate::rsrc_dir_t &type_dir = iter_find->second; + if (index >= static_cast(type_dir.size())) { + // Zero-based index is out of bounds. + return -ENOENT; + } + + // Return the ID at this index. + return type_dir[index].id; +} + +/** + * Do we have any resources of the specified type? + * @param type [in] Resource type ID + * @return True if we have these resources; false if we don't. + */ +bool NEResourceReader::has_resource_type(int type) +{ + // NOTE: Type and resource IDs have the high bit set for integers. + // We're only supporting integer IDs, so set the high bits here. + type |= 0x8000; + + RP_D(const NEResourceReader); + return (d->res_types.find(type) != d->res_types.end()); +} + } diff --git a/src/libromdata/disc/NEResourceReader.hpp b/src/libromdata/disc/NEResourceReader.hpp index d0bb442c3..b29976f8c 100644 --- a/src/libromdata/disc/NEResourceReader.hpp +++ b/src/libromdata/disc/NEResourceReader.hpp @@ -2,18 +2,18 @@ * ROM Properties Page shell extension. (libromdata) * * NEResourceReader.hpp: New Executable resource reader. * * * - * Copyright (c) 2016-2023 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ #pragma once -#include "IResourceReader.hpp" +#include "librpbase/disc/IResourceReader.hpp" namespace LibRomData { class NEResourceReaderPrivate; -class NEResourceReader final : public IResourceReader +class NEResourceReader final : public LibRpBase::IResourceReader { public: /** @@ -30,7 +30,7 @@ public: ~NEResourceReader() final; private: - typedef IResourceReader super; + typedef LibRpBase::IResourceReader super; RP_DISABLE_COPY(NEResourceReader) protected: friend class NEResourceReaderPrivate; @@ -92,9 +92,9 @@ public: /** * Open a resource. - * @param type Resource type ID. - * @param id Resource ID. (-1 for "first entry") - * @param lang Language ID. (-1 for "first entry") + * @param type [in] Resource type ID + * @param id [in] Resource ID (-1 for "first entry") + * @param lang [in] Language ID (-1 for "first entry") * @return IRpFile*, or nullptr on error. */ LibRpFile::IRpFilePtr open(uint16_t type, int id, int lang) final; @@ -116,6 +116,23 @@ public: * @return 0 on success; non-zero on error. */ int load_VS_VERSION_INFO(int id, int lang, VS_FIXEDFILEINFO *pVsFfi, StringFileInfo *pVsSfi) final; + + /** + * Look up a resource ID given a zero-based index. + * Mostly useful for icon indexes. + * + * @param type [in] Resource type ID + * @param index [in] Zero-based index + * @return Resource ID, or negative POSIX error code on error. + */ + int lookup_resource_ID(int type, int index) final; + + /** + * Do we have any resources of the specified type? + * @param type [in] Resource type ID + * @return True if we have these resources; false if we don't. + */ + bool has_resource_type(int type) final; }; } diff --git a/src/libromdata/disc/PEResourceReader.cpp b/src/libromdata/disc/PEResourceReader.cpp index 129a8da7f..26fe13233 100644 --- a/src/libromdata/disc/PEResourceReader.cpp +++ b/src/libromdata/disc/PEResourceReader.cpp @@ -861,4 +861,46 @@ int PEResourceReader::load_VS_VERSION_INFO(int id, int lang, VS_FIXEDFILEINFO *p return 0; } +/** + * Look up a resource ID given a zero-based index. + * Mostly useful for icon indexes. + * + * @param type [in] Resource type ID + * @param index [in] Zero-based index + * @param lang [in] Language ID (-1 for "first entry") + * @return Resource ID, or negative POSIX error code on error. + */ +int PEResourceReader::lookup_resource_ID(int type, int index) +{ + if (index < 0) { + return -EINVAL; + } + + // Get the resource directory for this type. + RP_D(PEResourceReader); + const PEResourceReaderPrivate::rsrc_dir_t *type_dir = d->getTypeDir(type); + if (!type_dir) { + return -ENOENT; + } + + if (index >= static_cast(type_dir->size())) { + // Zero-based index is out of bounds. + return -ENOENT; + } + + // Return the ID at this index. + return type_dir->at(index).id; +} + +/** + * Do we have any resources of the specified type? + * @param type [in] Resource type ID + * @return True if we have these resources; false if we don't. + */ +bool PEResourceReader::has_resource_type(int type) +{ + RP_D(PEResourceReader); + return (d->getTypeDir(type) != nullptr); +} + } diff --git a/src/libromdata/disc/PEResourceReader.hpp b/src/libromdata/disc/PEResourceReader.hpp index d3ff169cb..49c7b97e6 100644 --- a/src/libromdata/disc/PEResourceReader.hpp +++ b/src/libromdata/disc/PEResourceReader.hpp @@ -2,18 +2,18 @@ * ROM Properties Page shell extension. (libromdata) * * PEResourceReader.hpp: Portable Executable resource reader. * * * - * Copyright (c) 2016-2023 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ #pragma once -#include "IResourceReader.hpp" +#include "librpbase/disc/IResourceReader.hpp" namespace LibRomData { class PEResourceReaderPrivate; -class PEResourceReader final : public IResourceReader +class PEResourceReader final : public LibRpBase::IResourceReader { public: /** @@ -31,7 +31,7 @@ public: ~PEResourceReader() final; private: - typedef IResourceReader super; + typedef LibRpBase::IResourceReader super; RP_DISABLE_COPY(PEResourceReader) protected: friend class PEResourceReaderPrivate; @@ -93,9 +93,9 @@ public: /** * Open a resource. - * @param type Resource type ID. - * @param id Resource ID. (-1 for "first entry") - * @param lang Language ID. (-1 for "first entry") + * @param type [in] Resource type ID + * @param id [in] Resource ID (-1 for "first entry") + * @param lang [in] Language ID (-1 for "first entry") * @return IRpFile*, or nullptr on error. */ LibRpFile::IRpFilePtr open(uint16_t type, int id, int lang) final; @@ -110,13 +110,30 @@ public: /** * Load a VS_VERSION_INFO resource. * Data will be byteswapped to host-endian if necessary. - * @param id [in] Resource ID. (-1 for "first entry") - * @param lang [in] Language ID. (-1 for "first entry") + * @param id [in] Resource ID (-1 for "first entry") + * @param lang [in] Language ID (-1 for "first entry") * @param pVsFfi [out] VS_FIXEDFILEINFO (host-endian) * @param pVsSfi [out] StringFileInfo section. * @return 0 on success; non-zero on error. */ int load_VS_VERSION_INFO(int id, int lang, VS_FIXEDFILEINFO *pVsFfi, StringFileInfo *pVsSfi) final; + + /** + * Look up a resource ID given a zero-based index. + * Mostly useful for icon indexes. + * + * @param type [in] Resource type ID + * @param index [in] Zero-based index + * @return Resource ID, or negative POSIX error code on error. + */ + int lookup_resource_ID(int type, int index) final; + + /** + * Do we have any resources of the specified type? + * @param type [in] Resource type ID + * @return True if we have these resources; false if we don't. + */ + bool has_resource_type(int type) final; }; } diff --git a/src/libromdata/disc/WiiUFst.cpp b/src/libromdata/disc/WiiUFst.cpp index ab8f61c17..e0d71fc81 100644 --- a/src/libromdata/disc/WiiUFst.cpp +++ b/src/libromdata/disc/WiiUFst.cpp @@ -467,13 +467,14 @@ IFst::Dir *WiiUFst::opendir(const char *path) return nullptr; } - IFst::Dir *dirp = new IFst::Dir(this); - d->fstDirCount++; // TODO: Better way to get dir_idx? - dirp->dir_idx = static_cast(fst_entry - d->fstEntries); + const int dir_idx = static_cast(fst_entry - d->fstEntries); + IFst::Dir *dirp = new IFst::Dir(this, dir_idx); + d->fstDirCount++; // Initialize the entry to this directory. // readdir() will automatically seek to the next entry. + dirp->entry.extra = nullptr; // not used for Wii U dirp->entry.ptnum = be16_to_cpu(fst_entry->storage_cluster_index); dirp->entry.idx = dirp->dir_idx; dirp->entry.type = DT_DIR; @@ -492,7 +493,7 @@ IFst::Dir *WiiUFst::opendir(const char *path) * @return IFst::DirEnt*, or nullptr if end of directory or on error. * (End of directory does not set lastError; an error does.) */ -IFst::DirEnt *WiiUFst::readdir(IFst::Dir *dirp) +const IFst::DirEnt *WiiUFst::readdir(IFst::Dir *dirp) { RP_D(WiiUFst); assert(dirp != nullptr); diff --git a/src/libromdata/disc/WiiUFst.hpp b/src/libromdata/disc/WiiUFst.hpp index 717835dd6..ebe61aa58 100644 --- a/src/libromdata/disc/WiiUFst.hpp +++ b/src/libromdata/disc/WiiUFst.hpp @@ -67,7 +67,7 @@ public: * @return DirEnt*, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ - DirEnt *readdir(Dir *dirp) final; + const DirEnt *readdir(Dir *dirp) final; /** * Close an opened directory. diff --git a/src/libromdata/iso_structs.h b/src/libromdata/iso_structs.h index f5bae1dd9..942b6fb36 100644 --- a/src/libromdata/iso_structs.h +++ b/src/libromdata/iso_structs.h @@ -192,16 +192,22 @@ ASSERT_STRUCT(ISO_Boot_Volume_Descriptor, ISO_SECTOR_SIZE_MODE1_COOKED); * Primary volume descriptor. * * NOTE: All fields are space-padded. (0x20, ' ') + * + * NOTE 2: SVD fields are only valid in Supplementary Volume Descriptors. + * In PVDs, these fields should be all zero. */ typedef struct _ISO_Primary_Volume_Descriptor { ISO_Volume_Descriptor_Header header; - uint8_t reserved1; // [0x007] 0x00 + uint8_t svd_volume_flags; // [0x007] Bit 0, if clear, indicates escape sequences *only* has + // valid sequences from ISO 2375. If set, it has sequences + // that aren't in ISO 2375. + char sysID[32]; // [0x008] (strA) System identifier. char volID[32]; // [0x028] (strD) Volume identifier. uint8_t reserved2[8]; // [0x048] All zeroes. uint32_lsb_msb_t volume_space_size; // [0x050] Size of volume, in blocks. - uint8_t reserved3[32]; // [0x058] All zeroes. + char svd_escape_sequences[32]; // [0x058] SVD: Escape sequences (indicates character sets) uint16_lsb_msb_t volume_set_size; // [0x078] Size of the logical volume. (number of discs) uint16_lsb_msb_t volume_seq_number; // [0x07C] Disc number in the volume set. uint16_lsb_msb_t logical_block_size; // [0x080] Logical block size. (usually 2048) @@ -245,14 +251,22 @@ ASSERT_STRUCT(ISO_Primary_Volume_Descriptor, ISO_SECTOR_SIZE_MODE1_COOKED); /** * Volume descriptor. * - * Primary volume descriptor is located at sector 0x10. (0x8000) + * Primary Volume Descriptor is located at sector 0x10. (0x8000) + * Supplementary Volume Descriptor is usually located at sector 0x11, if present. (0x8800) */ #define ISO_VD_MAGIC "CD001" #define ISO_VD_VERSION 0x01 + #define ISO_PVD_LBA 0x10 #define ISO_PVD_ADDRESS_2048 (ISO_PVD_LBA * ISO_SECTOR_SIZE_MODE1_COOKED) #define ISO_PVD_ADDRESS_2352 (ISO_PVD_LBA * ISO_SECTOR_SIZE_MODE1_RAW) #define ISO_PVD_ADDRESS_2448 (ISO_PVD_LBA * ISO_SECTOR_SIZE_MODE1_RAW_SUBCHAN) + +#define ISO_SVD_LBA 0x11 +#define ISO_SVD_ADDRESS_2048 (ISO_SVD_LBA * ISO_SECTOR_SIZE_MODE1_COOKED) +#define ISO_SVD_ADDRESS_2352 (ISO_SVD_LBA * ISO_SECTOR_SIZE_MODE1_RAW) +#define ISO_SVD_ADDRESS_2448 (ISO_SVD_LBA * ISO_SECTOR_SIZE_MODE1_RAW_SUBCHAN) + typedef union _ISO_Volume_Descriptor { ISO_Volume_Descriptor_Header header; diff --git a/src/libromdata/tests/disc/FstPrint.cpp b/src/libromdata/tests/disc/FstPrint.cpp index 43cd78842..9f8efade0 100644 --- a/src/libromdata/tests/disc/FstPrint.cpp +++ b/src/libromdata/tests/disc/FstPrint.cpp @@ -71,7 +71,7 @@ static int fstPrint(IFst *fst, ostream &os, const string &path, } // Read the directory entries. - IFst::DirEnt *dirent = fst->readdir(dirp); + const IFst::DirEnt *dirent = fst->readdir(dirp); while (dirent != nullptr) { if (!dirent->name || dirent->name[0] == 0) { // Empty name... @@ -79,8 +79,7 @@ static int fstPrint(IFst *fst, ostream &os, const string &path, } // Print the tree lines. - for (int i = 0; i < level; i++) - { + for (int i = 0; i < level; i++) { if (tree_lines[i]) { // Directory tree exists for this segment. os << "\xE2\x94\x82 "; diff --git a/src/libromdata/tests/disc/GcnFstTest.cpp b/src/libromdata/tests/disc/GcnFstTest.cpp index 8415302b6..f2d112efa 100644 --- a/src/libromdata/tests/disc/GcnFstTest.cpp +++ b/src/libromdata/tests/disc/GcnFstTest.cpp @@ -357,7 +357,7 @@ void GcnFstTest::checkNoDuplicateFilenames(const char *subdir) ASSERT_TRUE(dirp != nullptr) << "Failed to open directory '" << subdir << "'."; - IFst::DirEnt *dirent = m_fst->readdir(dirp); + const IFst::DirEnt *dirent = m_fst->readdir(dirp); while (dirent != nullptr) { // Make sure we haven't seen this filename in // the current subdirectory yet. diff --git a/src/librpbase/CMakeLists.txt b/src/librpbase/CMakeLists.txt index ad4e771f5..37a52261f 100644 --- a/src/librpbase/CMakeLists.txt +++ b/src/librpbase/CMakeLists.txt @@ -47,6 +47,7 @@ SET(${PROJECT_NAME}_SRCS disc/PartitionFile.cpp disc/SparseDiscReader.cpp disc/CBCReader.cpp + disc/IResourceReader.cpp crypto/KeyManager.cpp config/ConfReader.cpp config/Config.cpp @@ -74,6 +75,8 @@ SET(${PROJECT_NAME}_H disc/SparseDiscReader.hpp disc/SparseDiscReader_p.hpp disc/CBCReader.hpp + disc/IResourceReader.hpp + disc/exe_res_structs.h crypto/KeyManager.hpp config/ConfReader.hpp config/Config.hpp diff --git a/src/librpbase/disc/IFst.hpp b/src/librpbase/disc/IFst.hpp index 8ca10f915..74405562e 100644 --- a/src/librpbase/disc/IFst.hpp +++ b/src/librpbase/disc/IFst.hpp @@ -2,7 +2,7 @@ * ROM Properties Page shell extension. (librpbase) * * IFst.hpp: File System Table interface. * * * - * Copyright (c) 2016-2024 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -25,92 +25,99 @@ namespace LibRpBase { class NOVTABLE IFst { - protected: - IFst() = default; - public: - virtual ~IFst() = 0; +protected: + IFst() = default; +public: + virtual ~IFst() = 0; - private: - RP_DISABLE_COPY(IFst) +private: + RP_DISABLE_COPY(IFst) - public: - // TODO: Base class? +public: + // TODO: Base class? - /** - * Is the FST open? - * @return True if open; false if not. - */ - virtual bool isOpen(void) const = 0; + /** + * Is the FST open? + * @return True if open; false if not. + */ + virtual bool isOpen(void) const = 0; - /** - * Have any errors been detected in the FST? - * @return True if yes; false if no. - */ - virtual bool hasErrors(void) const = 0; + /** + * Have any errors been detected in the FST? + * @return True if yes; false if no. + */ + virtual bool hasErrors(void) const = 0; - public: - /** opendir() interface **/ +public: + /** opendir() interface **/ - struct DirEnt { - off64_t offset; // Starting address - off64_t size; // File size - const char *name; // Filename + struct DirEnt { + off64_t offset; // Starting address + off64_t size; // File size + const char *name; // Filename - // TODO: Additional placeholders? - unsigned int ptnum; // Partition or content number - int idx; // File index - uint8_t type; // File type (See d_type.h) - }; + // TODO: Additional placeholders? + void *extra; // Extra data + unsigned int ptnum; // Partition or content number + int idx; // File index + uint8_t type; // File type (See d_type.h) + }; - struct Dir { - IFst *const parent; // IFst that owns this Dir - int dir_idx; // Directory index in the FST - DirEnt entry; // Current DirEnt + struct Dir { + IFst *const parent; // IFst that owns this Dir + intptr_t dir_idx; // Directory index in the FST + DirEnt entry; // Current DirEnt - explicit Dir(IFst *parent) - : parent(parent) - {} - }; + explicit Dir(IFst *parent, intptr_t dir_idx) + : parent(parent) + , dir_idx(dir_idx) + {} - /** - * Open a directory. - * @param path [in] Directory path. - * @return Dir*, or nullptr on error. - */ - virtual Dir *opendir(const char *path) = 0; + explicit Dir(IFst *parent, void *dir_idx) + : parent(parent) + , dir_idx(reinterpret_cast(dir_idx)) + {} + }; - /** - * Open a directory. - * @param path [in] Directory path. - * @return Dir*, or nullptr on error. - */ - inline Dir *opendir(const std::string &path) - { - return opendir(path.c_str()); - } + /** + * Open a directory. + * @param path [in] Directory path. + * @return Dir*, or nullptr on error. + */ + virtual Dir *opendir(const char *path) = 0; - /** - * Read a directory entry. - * @param dirp Dir pointer. - * @return DirEnt*, or nullptr if end of directory or on error. - * (TODO: Add lastError()?) - */ - virtual DirEnt *readdir(Dir *dirp) = 0; + /** + * Open a directory. + * @param path [in] Directory path. + * @return Dir*, or nullptr on error. + */ + inline Dir *opendir(const std::string &path) + { + return opendir(path.c_str()); + } - /** - * Close an opened directory. - * @param dirp Dir pointer. - * @return 0 on success; negative POSIX error code on error. - */ - virtual int closedir(Dir *dirp) = 0; + /** + * Read a directory entry. + * @param dirp Dir pointer. + * @return DirEnt*, or nullptr if end of directory or on error. + * (TODO: Add lastError()?) + */ + virtual const DirEnt *readdir(Dir *dirp) = 0; - /** - * Get the directory entry for the specified file. - * @param filename [in] Filename. - * @param dirent [out] Pointer to DirEnt buffer. - * @return 0 on success; negative POSIX error code on error. - */ - virtual int find_file(const char *filename, DirEnt *dirent) = 0; + /** + * Close an opened directory. + * @param dirp Dir pointer. + * @return 0 on success; negative POSIX error code on error. + */ + virtual int closedir(Dir *dirp) = 0; + + /** + * Get the directory entry for the specified file. + * @param filename [in] Filename. + * @param dirent [out] Pointer to DirEnt buffer. + * @return 0 on success; negative POSIX error code on error. + */ + virtual int find_file(const char *filename, DirEnt *dirent) = 0; }; /** diff --git a/src/librpbase/disc/IPartition.cpp b/src/librpbase/disc/IPartition.cpp index 05d1d8b4e..9e3f5d104 100644 --- a/src/librpbase/disc/IPartition.cpp +++ b/src/librpbase/disc/IPartition.cpp @@ -2,7 +2,7 @@ * ROM Properties Page shell extension. (librpbase) * * IPartition.cpp: Partition reader interface. * * * - * Copyright (c) 2016-2024 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -38,7 +38,7 @@ IFst::Dir *IPartition::opendir(const char *path) * @return IFst::DirEnt, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ -IFst::DirEnt *IPartition::readdir(IFst::Dir *dirp) +const IFst::DirEnt *IPartition::readdir(IFst::Dir *dirp) { RP_UNUSED(dirp); assert(!"IFst wrapper functions are not implemented for this class!"); diff --git a/src/librpbase/disc/IPartition.hpp b/src/librpbase/disc/IPartition.hpp index b07168798..e96feb49a 100644 --- a/src/librpbase/disc/IPartition.hpp +++ b/src/librpbase/disc/IPartition.hpp @@ -2,7 +2,7 @@ * ROM Properties Page shell extension. (librpbase) * * IPartition.hpp: Partition reader interface. * * * - * Copyright (c) 2016-2024 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -87,7 +87,7 @@ public: * @return IFst::DirEnt, or nullptr if end of directory or on error. * (TODO: Add lastError()?) */ - virtual IFst::DirEnt *readdir(IFst::Dir *dirp); + virtual const IFst::DirEnt *readdir(IFst::Dir *dirp); /** * Close an opened directory. diff --git a/src/libromdata/disc/IResourceReader.cpp b/src/librpbase/disc/IResourceReader.cpp similarity index 88% rename from src/libromdata/disc/IResourceReader.cpp rename to src/librpbase/disc/IResourceReader.cpp index 261d76af4..0162ba3b8 100644 --- a/src/libromdata/disc/IResourceReader.cpp +++ b/src/librpbase/disc/IResourceReader.cpp @@ -1,15 +1,15 @@ /*************************************************************************** - * ROM Properties Page shell extension. (libromdata) * + * ROM Properties Page shell extension. (librpbase) * * IResourceReader.cpp: Interface for Windows resource readers. * * * - * Copyright (c) 2016-2024 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ #include "stdafx.h" #include "IResourceReader.hpp" -namespace LibRomData { +namespace LibRpBase { /** * DWORD alignment function. diff --git a/src/libromdata/disc/IResourceReader.hpp b/src/librpbase/disc/IResourceReader.hpp similarity index 76% rename from src/libromdata/disc/IResourceReader.hpp rename to src/librpbase/disc/IResourceReader.hpp index 7223bb860..d950baea5 100644 --- a/src/libromdata/disc/IResourceReader.hpp +++ b/src/librpbase/disc/IResourceReader.hpp @@ -1,16 +1,15 @@ /*************************************************************************** - * ROM Properties Page shell extension. (libromdata) * + * ROM Properties Page shell extension. (librpbase) * * IResourceReader.hpp: Interface for Windows resource readers. * * * - * Copyright (c) 2016-2024 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ #pragma once -#include "../Other/exe_res_structs.h" +#include "exe_res_structs.h" -// librpbase #include "librpbase/disc/IPartition.hpp" // C++ includes @@ -29,9 +28,9 @@ typedef WCHAR *LPWSTR; typedef const WCHAR *LPCWSTR; #endif /* _WIN32 && !_WINNT_ */ -namespace LibRomData { +namespace LibRpBase { -class NOVTABLE IResourceReader : public LibRpBase::IPartition +class NOVTABLE IResourceReader : public IPartition { protected: IResourceReader(const LibRpFile::IRpFilePtr &file) @@ -41,7 +40,7 @@ public: ~IResourceReader() override = 0; private: - typedef LibRpBase::IPartition super; + typedef IPartition super; RP_DISABLE_COPY(IResourceReader) public: @@ -67,9 +66,9 @@ private: public: /** * Open a resource. - * @param type Resource type ID - * @param id Resource ID (-1 for "first entry") - * @param lang Language ID (-1 for "first entry") + * @param type [in] Resource type ID + * @param id [in] Resource ID (-1 for "first entry") + * @param lang [in] Language ID (-1 for "first entry") * @return IRpFile*, or nullptr on error. */ virtual LibRpFile::IRpFilePtr open(uint16_t type, int id, int lang) = 0; @@ -113,13 +112,30 @@ public: * Load a VS_VERSION_INFO resource. * Data will be byteswapped to host-endian if necessary. * - * @param id [in] Resource ID. (-1 for "first entry") - * @param lang [in] Language ID. (-1 for "first entry") + * @param id [in] Resource ID (-1 for "first entry") + * @param lang [in] Language ID (-1 for "first entry") * @param pVsFfi [out] VS_FIXEDFILEINFO (host-endian) * @param pVsSfi [out] StringFileInfo section. * @return 0 on success; non-zero on error. */ virtual int load_VS_VERSION_INFO(int id, int lang, VS_FIXEDFILEINFO *pVsFfi, StringFileInfo *pVsSfi) = 0; + + /** + * Look up a resource ID given a zero-based index. + * Mostly useful for icon indexes. + * + * @param type [in] Resource type ID + * @param index [in] Zero-based index + * @return Resource ID, or negative POSIX error code on error. + */ + virtual int lookup_resource_ID(int type, int index) = 0; + + /** + * Do we have any resources of the specified type? + * @param type [in] Resource type ID + * @return True if we have these resources; false if we don't. + */ + virtual bool has_resource_type(int type) = 0; }; typedef std::shared_ptr IResourceReaderPtr; diff --git a/src/libromdata/Other/exe_res_structs.h b/src/librpbase/disc/exe_res_structs.h similarity index 96% rename from src/libromdata/Other/exe_res_structs.h rename to src/librpbase/disc/exe_res_structs.h index f34dfeba5..c79915edf 100644 --- a/src/libromdata/Other/exe_res_structs.h +++ b/src/librpbase/disc/exe_res_structs.h @@ -1,8 +1,8 @@ /*************************************************************************** - * ROM Properties Page shell extension. (libromdata) * + * ROM Properties Page shell extension. (librpbase) * * exe_res_structs.h: DOS/Windows executable structures. (resources) * * * - * Copyright (c) 2017-2024 by David Korth. * + * Copyright (c) 2017-2025 by David Korth. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -57,6 +57,7 @@ typedef enum { /** Version resource **/ +#ifndef VER_H //#define VS_FILE_INFO RT_VERSION // TODO #define VS_VERSION_INFO 1 #define VS_USER_DEFINED 100 @@ -151,6 +152,7 @@ typedef struct _VS_FIXEDFILEINFO { uint32_t dwFileDateLS; } VS_FIXEDFILEINFO; ASSERT_STRUCT(VS_FIXEDFILEINFO, 13*sizeof(uint32_t)); +#endif /* VER_H */ #ifdef __cplusplus } diff --git a/src/librptexture/CMakeLists.txt b/src/librptexture/CMakeLists.txt index ae6821416..10bc85f5c 100644 --- a/src/librptexture/CMakeLists.txt +++ b/src/librptexture/CMakeLists.txt @@ -44,6 +44,7 @@ SET(${PROJECT_NAME}_SRCS fileformat/DidjTex.cpp fileformat/DirectDrawSurface.cpp fileformat/GodotSTEX.cpp + fileformat/ICO.cpp fileformat/KhronosKTX.cpp fileformat/KhronosKTX2.cpp fileformat/PalmOS_Tbmp.cpp @@ -91,6 +92,7 @@ SET(${PROJECT_NAME}_H fileformat/DidjTex.hpp fileformat/DirectDrawSurface.hpp fileformat/GodotSTEX.hpp + fileformat/ICO.hpp fileformat/KhronosKTX.hpp fileformat/KhronosKTX2.hpp fileformat/PalmOS_Tbmp.hpp @@ -106,6 +108,7 @@ SET(${PROJECT_NAME}_H fileformat/dds_structs.h fileformat/didj_tex_structs.h fileformat/godot_stex_structs.h + fileformat/ico_structs.h fileformat/ktx_structs.h fileformat/ktx_structs.h fileformat/palmos_tbmp_structs.h diff --git a/src/librptexture/FileFormatFactory.cpp b/src/librptexture/FileFormatFactory.cpp index bb37ce055..8b786de4d 100644 --- a/src/librptexture/FileFormatFactory.cpp +++ b/src/librptexture/FileFormatFactory.cpp @@ -32,6 +32,7 @@ using std::vector; #include "fileformat/DidjTex.hpp" #include "fileformat/DirectDrawSurface.hpp" #include "fileformat/GodotSTEX.hpp" +#include "fileformat/ICO.hpp" #include "fileformat/KhronosKTX.hpp" #include "fileformat/KhronosKTX2.hpp" #include "fileformat/PowerVR3.hpp" @@ -178,6 +179,10 @@ FileFormatPtr create(const IRpFilePtr &file) } } + // Check for specific file extensions. + // - ICO, CUR: Windows icons and cursors don't have a useful magic number. + // - TGA: There's no header magic number, but there *is* some information. + // Use some heuristics to check for TGA files. // Based on heuristics from `file`. // TGA 2.0 has an identifying footer as well. @@ -185,28 +190,45 @@ FileFormatPtr create(const IRpFilePtr &file) // conflicts with "WWF Raw" on SNES. const char *const filename = file->filename(); const char *const ext = FileSystem::file_ext(filename); - bool ext_ok = false; + bool is_ico = false, maybe_tga = false; if (!ext || ext[0] == '\0') { // No extension. Check for TGA anyway. - ext_ok = true; + maybe_tga = true; } else if (!strcasecmp(ext, ".tga")) { // TGA extension. - ext_ok = true; + maybe_tga = true; + } else if (!strcasecmp(ext, ".ico") || !strcasecmp(ext, ".cur")) { + // ICO or CUR extension. + is_ico = true; } else if (!strcasecmp(ext, ".gz")) { // Check if it's ".tga.gz". const size_t filename_len = strlen(filename); if (filename_len >= 7) { if (!strncasecmp(&filename[filename_len-7], ".tga", 4)) { // It's ".tga.gz". - ext_ok = true; + maybe_tga = true; + } else if (!strncasecmp(&filename[filename_len-7], ".ico", 4) || + !strncasecmp(&filename[filename_len-7], ".cur", 4)) + { + // It's ".ico.gz" or ".cur.gz". + is_ico = true; } } } + if (is_ico) { + // This might be a Windows icon or cursor. + FileFormatPtr fileFormat = std::make_shared(file); + if (fileFormat->isValid()) { + // FileFormat subclass obtained. + return fileFormat; + } + } + // test of Color Map Type 0~no 1~color map // and Image Type 1 2 3 9 10 11 32 33 // and Color Map Entry Size 0 15 16 24 32 - if (ext_ok && + if (maybe_tga && ((magic.u32[0] & be32_to_cpu(0x00FEC400)) == 0) && ((magic.u32[1] & be32_to_cpu(0x000000C0)) == 0)) { diff --git a/src/librptexture/decoder/ImageDecoder_Linear.cpp b/src/librptexture/decoder/ImageDecoder_Linear.cpp index 4e81d37dd..0fa402100 100644 --- a/src/librptexture/decoder/ImageDecoder_Linear.cpp +++ b/src/librptexture/decoder/ImageDecoder_Linear.cpp @@ -309,6 +309,7 @@ rp_image_ptr fromLinearCI8(PixelFormat px_format, } // Verify palette size. + // TODO: Table to map PixelFormat to bpp? switch (px_format) { case PixelFormat::RGB888: // 24-bit palette required. @@ -320,6 +321,9 @@ rp_image_ptr fromLinearCI8(PixelFormat px_format, case PixelFormat::BGR888_ABGR7888: case PixelFormat::Host_ARGB32: + case PixelFormat::Swap_ARGB32: + case PixelFormat::Host_xRGB32: + case PixelFormat::Swap_xRGB32: // 32-bit palette required. assert(pal_siz >= 256*4); if (pal_siz < 256*4) { diff --git a/src/librptexture/decoder/ImageDecoder_Linear_Gray.cpp b/src/librptexture/decoder/ImageDecoder_Linear_Gray.cpp index f807c2a97..dee1dd248 100644 --- a/src/librptexture/decoder/ImageDecoder_Linear_Gray.cpp +++ b/src/librptexture/decoder/ImageDecoder_Linear_Gray.cpp @@ -17,6 +17,9 @@ namespace LibRpTexture { namespace ImageDecoder { /** * Convert a linear monochrome image to rp_image. + * + * 0 == white; 1 == black + * * @param width [in] Image width * @param height [in] Image height * @param img_buf [in] Monochrome image buffer @@ -97,6 +100,9 @@ rp_image_ptr fromLinearMono(int width, int height, /** * Convert a linear 2-bpp grayscale image to rp_image. + * + * 0 == white; 3 == black + * * @param width [in] Image width * @param height [in] Image height * @param img_buf [in] Monochrome image buffer @@ -180,4 +186,109 @@ rp_image_ptr fromLinearGray2bpp(int width, int height, return img; } +/** + * Convert a linear monochrome image to rp_image. + * + * Windows icons are handled a bit different compared to "regular" monochrome images: + * - Two images are required: icon and mask + * - Transparency is supported using the mask. + * - 0 == black; 1 == white + * + * @param width [in] Image width + * @param height [in] Image height + * @param img_buf [in] Monochrome image buffer + * @param img_siz [in] Size of image data [must be >= ((w*h)/8)] + * @param mask_buf [in] Mask buffer + * @param mask_siz [in] Size of mask buffer [must be == img_siz] + * @param stride [in,opt] Stride, in bytes (if 0, assumes width*bytespp) + * @return rp_image, or nullptr on error. + */ +ATTR_ACCESS_SIZE(read_only, 3, 4) +rp_image_ptr fromLinearMono_WinIcon(int width, int height, + const uint8_t *RESTRICT img_buf, size_t img_siz, + const uint8_t *RESTRICT mask_buf, size_t mask_siz, int stride) +{ + // Verify parameters. + assert(img_buf != nullptr); + assert(width > 0); + assert(height > 0); + assert(img_siz >= (static_cast(width) * static_cast(height) / 8)); + assert(img_siz == mask_siz); + if (!img_buf || width <= 0 || height <= 0 || + img_siz < (static_cast(width) * static_cast(height) / 8) || + img_siz != mask_siz) + { + return {}; + } + + // Source stride adjustment. + int src_stride_adj = 0; + assert(stride >= 0); + if (stride > 0) { + // Set src_stride_adj to the number of bytes we need to + // add to the end of each line to get to the next row. + src_stride_adj = stride - ((width / 8) + ((width % 8) > 0)); + } + + // Create an rp_image. + rp_image_ptr img = std::make_shared(width, height, rp_image::Format::CI8); + if (!img->isValid()) { + // Could not allocate the image. + return {}; + } + const int dest_stride_adj = img->stride() - img->width(); + + // Set up a default monochrome palette. + // NOTE: Color 0 is used for transparency. + uint32_t *palette = img->palette(); + palette[0] = 0x00000000U; // transparent + palette[1] = 0xFF000000U; // black + palette[2] = 0xFFFFFFFFU; // white + img->set_tr_idx(0); + + // NOTE: rp_image initializes the palette to 0, + // so we don't need to clear the remaining colors. + + // Convert one line at a time. (monochrome -> CI8) + uint8_t *px_dest = static_cast(img->bits()); + for (unsigned int y = static_cast(height); y > 0; y--) { + for (int x = width; x > 0; x -= 8) { + uint8_t pxMask = *mask_buf++; + uint8_t pxIcon = *img_buf++; + + // For images where width is not a multiple of 8, + // we'll discard the remaining bits in the last byte. + const unsigned int max_bit = (x >= 8) ? 8 : static_cast(x); + + // TODO: Unroll this loop? + for (unsigned int bit = max_bit; bit > 0; bit--, px_dest++) { + // MSB == left-most pixel + if (pxMask & 0x80) { + // Mask bit is set: this is either screen or inverted. + // FIXME: Inverted doesn't work here. Will use white for inverted. + *px_dest = (pxIcon & 0x80) ? 2 : 0; + } else { + // Mask bit is clear: this is the image. + *px_dest = (pxIcon & 0x80) ? 2 : 1; + } + + pxMask <<= 1; + pxIcon <<= 1; + } + } + img_buf += src_stride_adj; + px_dest += dest_stride_adj; + } + + // Set the sBIT metadata. + // NOTE: Setting the grayscale value, though we're + // not saving grayscale PNGs at the moment. + // TODO: Don't set alpha if the icon mask doesn't have any set bits? + static const rp_image::sBIT_t sBIT = {1,1,1,1,1}; + img->set_sBIT(&sBIT); + + // Image has been converted. + return img; +} + } } diff --git a/src/librptexture/decoder/ImageDecoder_Linear_Gray.hpp b/src/librptexture/decoder/ImageDecoder_Linear_Gray.hpp index 98471d3eb..d2395c56d 100644 --- a/src/librptexture/decoder/ImageDecoder_Linear_Gray.hpp +++ b/src/librptexture/decoder/ImageDecoder_Linear_Gray.hpp @@ -15,6 +15,9 @@ namespace LibRpTexture { namespace ImageDecoder { /** * Convert a linear monochrome image to rp_image. + * + * 0 == white; 1 == black + * * @param width [in] Image width * @param height [in] Image height * @param img_buf [in] Monochrome image buffer @@ -28,6 +31,9 @@ rp_image_ptr fromLinearMono(int width, int height, /** * Convert a linear 2-bpp grayscale image to rp_image. + * + * 0 == white; 3 == black + * * @param width [in] Image width * @param height [in] Image height * @param img_buf [in] Monochrome image buffer @@ -39,4 +45,27 @@ ATTR_ACCESS_SIZE(read_only, 3, 4) rp_image_ptr fromLinearGray2bpp(int width, int height, const uint8_t *RESTRICT img_buf, size_t img_siz, int stride = 0); +/** + * Convert a linear monochrome image to rp_image. + * + * Windows icons are handled a bit different compared to "regular" monochrome images: + * - Two images are required: icon and mask + * - Transparency is supported using the mask. + * - 0 == black; 1 == white + * + * @param width [in] Image width + * @param height [in] Image height + * @param img_buf [in] Monochrome image buffer + * @param img_siz [in] Size of image data [must be >= ((w*h)/8)] + * @param mask_buf [in] Mask buffer + * @param mask_siz [in] Size of mask buffer [must be == img_siz] + * @param stride [in,opt] Stride, in bytes (if 0, assumes width*bytespp) + * @return rp_image, or nullptr on error. + */ +ATTR_ACCESS_SIZE(read_only, 3, 4) +ATTR_ACCESS_SIZE(read_only, 5, 6) +rp_image_ptr fromLinearMono_WinIcon(int width, int height, + const uint8_t *RESTRICT img_buf, size_t img_siz, + const uint8_t *RESTRICT mask_buf, size_t mask_siz, int stride = 0); + } } diff --git a/src/librptexture/fileformat/FileFormat_decl.hpp b/src/librptexture/fileformat/FileFormat_decl.hpp index c0327653a..14a0caa12 100644 --- a/src/librptexture/fileformat/FileFormat_decl.hpp +++ b/src/librptexture/fileformat/FileFormat_decl.hpp @@ -2,7 +2,7 @@ * ROM Properties Page shell extension. (librptexture) * * FileFormat_decl.hpp: Texture file format base class. (Subclass macros) * * * - * Copyright (c) 2016-2024 by David Korth. * + * Copyright (c) 2016-2025 by David Korth. * * Copyright (c) 2016-2018 by Egor. * * SPDX-License-Identifier: GPL-2.0-or-later * ***************************************************************************/ @@ -58,7 +58,7 @@ private: \ #define FILEFORMAT_DECL_CTOR_DEFAULT(klass) \ public: \ /** \ - * Read a texture file file. \ + * Read a texture file. \ * \ * A ROM image must be opened by the caller. The file handle \ * will be ref()'d and must be kept open in order to load \ diff --git a/src/librptexture/fileformat/ICO.cpp b/src/librptexture/fileformat/ICO.cpp new file mode 100644 index 000000000..9f1f855d3 --- /dev/null +++ b/src/librptexture/fileformat/ICO.cpp @@ -0,0 +1,1254 @@ +/*************************************************************************** + * ROM Properties Page shell extension. (librptexture) * + * ICO.cpp: Windows icon and cursor image reader. * + * * + * Copyright (c) 2017-2025 by David Korth. * + * SPDX-License-Identifier: GPL-2.0-or-later * + ***************************************************************************/ + +#include "stdafx.h" +#include "ICO.hpp" +#include "FileFormat_p.hpp" + +#include "ico_structs.h" + +// Other rom-properties libraries +#include "libi18n/i18n.h" +#include "librpbase/disc/DiscReader.hpp" +#include "librpbase/disc/PartitionFile.hpp" +#include "librpbase/img/RpPng.hpp" +using namespace LibRpBase; +using namespace LibRpFile; + +// librptexture +#include "img/rp_image.hpp" +#include "decoder/ImageDecoder_Linear.hpp" +#include "decoder/ImageDecoder_Linear_Gray.hpp" + +// C++ STL classes +using std::array; + +// Uninitialized vector class +#include "uvector.h" + +namespace LibRpTexture { + +class ICOPrivate final : public FileFormatPrivate +{ +public: + ICOPrivate(ICO *q, const IRpFilePtr &file); + ICOPrivate(ICO *q, const IResourceReaderPtr &resReader, uint16_t type, int id, int lang); + ~ICOPrivate(); + +private: + typedef FileFormatPrivate super; + RP_DISABLE_COPY(ICOPrivate) + +public: + /** TextureInfo **/ + static const array exts; + static const array mimeTypes; + static const TextureInfo textureInfo; + +public: + // Icon type + enum class IconType { + Unknown = -1, + + // Win1.x .ico/.cur + Icon_Win1 = 0, + Cursor_Win1 = 1, + + // Win3.x .ico/.cur + Icon_Win3 = 2, + Cursor_Win3 = 3, + + // Win1.x resources (RT_ICON, RT_CURSOR) + IconRes_Win1 = 4, + CursorRes_Win1 = 5, + + // Win3.x resources (RT_GROUP_ICON, RT_GROUP_CURSOR) + IconRes_Win3 = 6, + CursorRes_Win3 = 7, + + Max + }; + IconType iconType; + + // ICO header + union { + ICO_Win1_Header win1; + ICONDIR win3; // ICONDIR and GRPICONDIR are essentially the same + } icoHeader; + + /** Win3.x icon stuff **/ + + /** + * Is this an icon resource from a .exe/.dll? + * @return True if it's a resource; false if it's a .ico file. + */ + inline bool isResource(void) const + { + return (iconType >= IconType::IconRes_Win1) && (iconType <= IconType::CursorRes_Win3); + } + + /** + * Get the resource type for the individual icon or cursor. + * @return RT_ICON or RT_CURSOR, or 0 if this is a standalone .ico/.cur file. + */ + inline uint16_t imageResType(void) const + { + switch (iconType) { + case IconType::IconRes_Win1: + case IconType::IconRes_Win3: + return RT_ICON; + case IconType::CursorRes_Win1: + case IconType::CursorRes_Win3: + return RT_CURSOR; + default: + return 0; + } + } + + // NOTE: ICONDIRENTRY (for .ico files) and GRPICONDIRENTRY (for resources) + // are different sizes. Hence, we have to use this union of struct pointers hack. + struct icodir_ico { + // Icon directory + // NOTE: *Not* byteswapped. + rp::uvector iconDirectory; + + // "Best" icon in the icon directory + // NOTE: *Not* byteswapped. + const ICONDIRENTRY *pBestIcon; + + icodir_ico() + : pBestIcon(nullptr) + { } + }; + struct icodir_res { + // Icon directory + // NOTE: *Not* byteswapped. + // NOTE: ICONDIRENTRY and GRPICONDIRENTRY are different sizes, + // so this has to be interpreted based on IconType. + rp::uvector iconDirectory; + + // "Best" icon in the icon directory + // NOTE: *Not* byteswapped. + const GRPICONDIRENTRY *pBestIcon; + + // IResourceReader for loading icons from Windows executables + IResourceReaderPtr resReader; + + // Resource information + uint16_t type; + int id; + int lang; + + icodir_res(const IResourceReaderPtr &resReader, uint16_t type, int id, int lang) + : pBestIcon(nullptr) + , resReader(resReader) + , type(type) + , id(id) + , lang(lang) + { } + }; + + union { + void *v; + icodir_ico *ico; + icodir_res *res; + } dir; + + // Icon bitmap header + union IconBitmapHeader_t { + uint32_t size; + BITMAPCOREHEADER bch; + BITMAPINFOHEADER bih; + struct { + uint8_t magic[8]; + PNG_IHDR_full_t ihdr; + } png; + }; + + // All icon bitmap headers + // These all have to be loaded in order to + // determine which one is the "best" icon. + rp::uvector iconBitmapHeaders; + + // "Best" icon: Bitmap header + const IconBitmapHeader_t *pIconHeader; + + // Decoded image + rp_image_ptr img; + +public: + /** + * Load the icon directory. (Windows 3.x) + * This function will also select the "best" icon to use. + * @return 0 on success; negative POSIX error code on error. + */ + int loadIconDirectory_Win3(void); + +private: + /** + * Load the image. (Windows 1.0 icon format) + * @return Image, or nullptr on error. + */ + rp_image_const_ptr loadImage_Win1(void); + + /** + * Load the image. (Windows 3.x icon format) + * @return Image, or nullptr on error. + */ + rp_image_const_ptr loadImage_Win3(void); + + /** + * Load the image. (Windows Vista PNG format) + * @return Image, or nullptr on error. + */ + rp_image_const_ptr loadImage_WinVista_PNG(void); + +public: + /** + * Load the image. + * @return Image, or nullptr on error. + */ + rp_image_const_ptr loadImage(void); +}; + +FILEFORMAT_IMPL(ICO) + +/** ICOPrivate **/ + +/* TextureInfo */ +const array ICOPrivate::exts = {{ + ".ico", + ".cur", + + nullptr +}}; +const array ICOPrivate::mimeTypes = {{ + // Official MIME types. + "image/vnd.microsoft.icon", + + // Unofficial MIME types. + "application/ico", + "image/ico", // NOTE: Used by Microsoft + "image/icon", + "image/x-ico", + "image/x-icon", // NOTE: Used by Microsoft + "text/ico", + + // Unofficial MIME types. + // TODO: Get these upstreamed on FreeDesktop.org. + "image/vnd.microsoft.cursor", + "image/x-cursor", + + nullptr +}}; +const TextureInfo ICOPrivate::textureInfo = { + exts.data(), mimeTypes.data() +}; + +ICOPrivate::ICOPrivate(ICO *q, const IRpFilePtr &file) + : super(q, file, &textureInfo) + , iconType(IconType::Unknown) + , pIconHeader(nullptr) +{ + // Clear the ICO header union. + memset(&icoHeader, 0, sizeof(icoHeader)); + + // Initialize the icon directory union. + dir.ico = new icodir_ico(); +} + +ICOPrivate::ICOPrivate(ICO *q, const IResourceReaderPtr &resReader, uint16_t type, int id, int lang) + : super(q, resReader, &textureInfo) + , iconType(IconType::Unknown) + , pIconHeader(nullptr) +{ + // Clear the ICO header union. + memset(&icoHeader, 0, sizeof(icoHeader)); + + // Determine the icon type here. + switch (type) { + default: + assert(!"Unsupported resource type"); + dir.v = nullptr; + return; + + // NOTE: Assuming individual icon/cursor is Windows 1.x/2.x format. + // TODO: Check the header to verify? + case RT_ICON: + iconType = IconType::IconRes_Win1; + break; + case RT_CURSOR: + iconType = IconType::CursorRes_Win1; + break; + + case RT_GROUP_ICON: + iconType = IconType::IconRes_Win3; + break; + case RT_GROUP_CURSOR: + iconType = IconType::CursorRes_Win3; + break; + } + + // Initialize the icon directory union. + dir.res = new icodir_res(resReader, type, id, lang); +} + +ICOPrivate::~ICOPrivate() +{ + if (dir.v) { + if (isResource()) { + delete dir.res; + } else { + delete dir.ico; + } + } +} + +/** + * Load the icon directory. (Windows 3.x) + * This function will also select the "best" icon to use. + * @return 0 on success; negative POSIX error code on error. + */ +int ICOPrivate::loadIconDirectory_Win3(void) +{ + // TODO: Windows Vista uses BITMAPINFOHEADER to select an icon. + // Don't remember the reference for this, probably The Old New Thing... + + // Load the icon directory. + const unsigned int count = le16_to_cpu(icoHeader.win3.idCount); + if (count == 0) { + // No icons??? + return -ENOENT; + } + + const uint16_t rt = imageResType(); + if (rt != 0) { + // Icon/cursor resource from a Windows executable. + + // Open the RT_GROUP_ICON / RT_GROUP_CURSOR resource. + auto &res = *(dir.res); + IRpFilePtr f_icondir = res.resReader->open(res.type, res.id, res.lang); + if (!f_icondir) { + // Unable to open the RT_GROUP_ICON / RT_GROUP_CURSOR. + return -ENOENT; + } + + const size_t fullsize = count * sizeof(GRPICONDIRENTRY); + auto &iconDirectory = res.iconDirectory; + iconDirectory.resize(count); + size_t size = f_icondir->seekAndRead(sizeof(GRPICONDIR), iconDirectory.data(), fullsize); + if (size != fullsize) { + // Seek and/or read error. + iconDirectory.clear(); + return -EIO; + } + + // Load all of the icon image headers. + iconBitmapHeaders.resize(count); + IconBitmapHeader_t *p = iconBitmapHeaders.data(); + for (auto iter = iconDirectory.cbegin(); iter != iconDirectory.cend(); ++iter, p++) { + IRpFilePtr f_icon = res.resReader->open(rt, le16_to_cpu(iter->nID), res.lang); + if (!f_icon) { + // Unable to open the resource. + iconDirectory.clear(); + iconBitmapHeaders.clear(); + return -ENOENT; + } + + size_t size = f_icon->read(p, sizeof(*p)); + if (size != sizeof(*p)) { + // Short read. + iconDirectory.clear(); + iconBitmapHeaders.clear(); + return -EIO; + } + } + } else { + // Standalone .ico/.cur file. + const size_t fullsize = count * sizeof(ICONDIRENTRY); + auto &iconDirectory = dir.ico->iconDirectory; + iconDirectory.resize(count); + size_t size = file->seekAndRead(sizeof(ICONDIR), iconDirectory.data(), fullsize); + if (size != fullsize) { + // Seek and/or read error. + iconDirectory.clear(); + return -EIO; + } + + // Load all of the icon image headers. + iconBitmapHeaders.resize(count); + IconBitmapHeader_t *p = iconBitmapHeaders.data(); + for (auto iter = iconDirectory.cbegin(); iter != iconDirectory.cend(); ++iter, p++) { + unsigned int addr = le32_to_cpu(iter->dwImageOffset); + size_t size = file->seekAndRead(addr, p, sizeof(*p)); + if (size != sizeof(*p)) { + // Seek and/or read error. + iconDirectory.clear(); + iconBitmapHeaders.clear(); + return -EIO; + } + } + } + + // Go through the icon bitmap headers and figure out the "best" one. + unsigned int width_best = 0, height_best = 0, bitcount_best = 0; + int best_icon = -1; + for (unsigned int i = 0; i < count; i++) { + // Get the width, height, and color depth from this bitmap header. + const IconBitmapHeader_t *const p = &iconBitmapHeaders[i]; + unsigned int width, height, bitcount; + + switch (p->size) { + default: + // Not supported... + continue; + + case BITMAPCOREHEADER_SIZE: + if (le32_to_cpu(p->bch.bcPlanes) > 1) { + // Cannot handle planar bitmaps. + continue; + } + width = le16_to_cpu(p->bch.bcWidth); + height = le16_to_cpu(p->bch.bcHeight) / 2; + bitcount = le16_to_cpu(p->bch.bcBitCount); + break; + + case BITMAPINFOHEADER_SIZE: + case BITMAPV2INFOHEADER_SIZE: + case BITMAPV3INFOHEADER_SIZE: + case BITMAPV4HEADER_SIZE: + case BITMAPV5HEADER_SIZE: + if (le32_to_cpu(p->bih.biPlanes) > 1) { + // Cannot handle planar bitmaps. + continue; + } + width = le32_to_cpu(p->bih.biWidth); + height = le32_to_cpu(p->bih.biHeight) / 2; + bitcount = le16_to_cpu(p->bih.biBitCount); + break; + + case 0x474E5089: // "\x89PNG" + switch (p->png.ihdr.data.color_type) { + default: + // Not supported... + continue; + + case PNG_COLOR_TYPE_PALETTE: + bitcount = p->png.ihdr.data.bit_depth; + break; + + case PNG_COLOR_TYPE_GRAY: + case PNG_COLOR_TYPE_RGB: + // Handling as if it's RGB. + bitcount = p->png.ihdr.data.bit_depth * 3; + break; + + case PNG_COLOR_TYPE_GRAY_ALPHA: + case PNG_COLOR_TYPE_RGB_ALPHA: + // Handling as if it's ARGB. + bitcount = p->png.ihdr.data.bit_depth * 4; + break; + } + + width = be32_to_cpu(p->png.ihdr.data.width); + height = be32_to_cpu(p->png.ihdr.data.height); + break; + } + + // Check if the image is larger. + // TODO: Non-square icon handling. + bool icon_is_better = false; + if (width > width_best || height > height_best) { + // Image is larger. + icon_is_better = true; + } else if (width == width_best && height == height_best) { + // Image is the same size. + if (bitcount > bitcount_best) { + // Color depth is higher. + icon_is_better = true; + } + } + + if (icon_is_better) { + // This icon is better. + best_icon = static_cast(i); + pIconHeader = p; + width_best = width; + height_best = height; + bitcount_best = bitcount; + } + } + + if (best_icon >= 0) { + if (rt != 0) { + dir.res->pBestIcon = &dir.res->iconDirectory[best_icon]; + } else { + dir.ico->pBestIcon = &dir.ico->iconDirectory[best_icon]; + } + return 0; + } + + // No icons??? + return -ENOENT; +} + +/** + * Load the image. (Windows 1.0 icon format) + * @return Image, or nullptr on error. + */ +rp_image_const_ptr ICOPrivate::loadImage_Win1(void) +{ + // Icon data is located immediately after the header. + // Each icon is actually two icons: a 1bpp mask, then a 1bpp icon. + + // NOTE: If the file has *both* DIB and DDB, then the DIB is first, + // followed by the DDB, with its own icon header. Not handling this + // for now; only reading the DIB. + + const int width = le16_to_cpu(icoHeader.win1.width); + const int height = le16_to_cpu(icoHeader.win1.height); + const unsigned int stride = le16_to_cpu(icoHeader.win1.stride); + + // Single icon size + const size_t icon_size = static_cast(height) * stride; + + // Load the icon data. + rp::uvector icon_data; + icon_data.resize(icon_size * 2); + + // Is this from a file or a resource? + size_t size; + const uint16_t rt = imageResType(); + if (rt != 0) { + // Open the resource. + const auto &res = *(dir.res); + IRpFilePtr f_icon = res.resReader->open(rt, res.id, res.lang); + if (!f_icon) { + // Unable to open the resource. + return {}; + } + + // Read from the resource. + size = f_icon->seekAndRead(sizeof(icoHeader.win1), icon_data.data(), icon_size * 2); + } else { + // Read from the file. + size = file->seekAndRead(sizeof(icoHeader.win1), icon_data.data(), icon_size * 2); + } + + if (size != icon_size * 2) { + // Seek and/or read error. + return {}; + } + + // Convert the icon. + const uint8_t *const p_mask_data = icon_data.data(); + const uint8_t *const p_icon_data = p_mask_data + icon_size; + img = ImageDecoder::fromLinearMono_WinIcon(width, height, + p_icon_data, icon_size, p_mask_data, icon_size, stride); + return img; +} + +/** + * Load the image. (Windows 3.x icon format) + * @return Image, or nullptr on error. + */ +rp_image_const_ptr ICOPrivate::loadImage_Win3(void) +{ + // Icon image header was already loaded by loadIconDirectory_Win3(). + // TODO: Verify dwBytesInRes. + + // Check the header size. + const unsigned int header_size = le32_to_cpu(pIconHeader->size); + switch (header_size) { + default: + // Not supported... + return {}; + + case BITMAPCOREHEADER_SIZE: + // TODO: Convert to BITMAPINFOHEADER. + return {}; + + case BITMAPINFOHEADER_SIZE: + case BITMAPV2INFOHEADER_SIZE: + case BITMAPV3INFOHEADER_SIZE: + case BITMAPV4HEADER_SIZE: + case BITMAPV5HEADER_SIZE: + break; + + case 0x474E5089: // "\x89PNG" + // Load it as a PNG image. + return loadImage_WinVista_PNG(); + } + + // NOTE: For standard icons (non-alpha, not PNG), the height is + // actually doubled. The top of the bitmap is the icon image, + // and the bottom is the monochrome mask. + // NOTE 2: If height > 0, the entire bitmap is upside-down. + + const BITMAPINFOHEADER *bih = &pIconHeader->bih; + + // Make sure width and height are valid. + // Height cannot be 0 or an odd number. + // NOTE: Negative height is allowed for "right-side up". + const unsigned int width = le32_to_cpu((unsigned int)(bih->biWidth)); + const int orig_height = static_cast(le32_to_cpu(bih->biHeight)); + const unsigned int height = abs(orig_height); + if (width <= 0 || height == 0 || (height & 1)) { + // Invalid bitmap size. + return {}; + } + + const bool is_upside_down = (orig_height > 0); + const unsigned int half_height = height / 2; + + // Only supporting 16-color images for now. + // TODO: Handle BI_BITFIELDS? + if (le32_to_cpu(bih->biPlanes) > 1) { + // Cannot handle planar bitmaps. + return {}; + } + + // Row must be 32-bit aligned. + // FIXME: Including for 24-bit images? + const unsigned int bitcount = le32_to_cpu(bih->biBitCount); + unsigned int stride = width; + switch (bitcount) { + default: + // Unsupported bitcount. + return {}; + case 1: + stride /= 8; + break; + case 2: + stride /= 4; + break; + case 4: + stride /= 2; + break; + case 8: + break; + case 16: + stride *= 2; + break; + case 24: + stride *= 3; + break; + case 32: + stride *= 4; + break; + } + stride = ALIGN_BYTES(4, stride); + + // Mask row is 1bpp and must also be 32-bit aligned. + unsigned int mask_stride = ALIGN_BYTES(4, width / 8); + + // Icon file (this->file for .ico; IResourceReader::open() for .exe/.dll) + IRpFilePtr f_icon; + unsigned int addr; + + const uint16_t rt = imageResType(); + if (rt != 0) { + // Open the resource. + const auto &res = *(dir.res); + f_icon = res.resReader->open(rt, le16_to_cpu(res.pBestIcon->nID), res.lang); + if (!f_icon) { + // Unable to open the resource. + return {}; + } + addr = header_size; + } else { + // Get the icon's starting address within the .ico file. + const ICONDIRENTRY *const pBestIcon = dir.ico->pBestIcon; + f_icon = this->file; + addr = le32_to_cpu(pBestIcon->dwImageOffset) + header_size; + } + + // For 8bpp or less, a color table is present. + // NOTE: Need to manually set the alpha channel to 0xFF. + rp::uvector pal_data; + if (bitcount <= 8) { + const unsigned int palette_count = (1U << bitcount); + const unsigned int palette_size = palette_count * static_cast(sizeof(uint32_t)); + pal_data.resize(palette_count); + size_t size = f_icon->seekAndRead(addr, pal_data.data(), palette_size); + if (size != palette_size) { + // Seek and/or read error. + return {}; + } + // TODO: 32-bit alignment? + addr += palette_size; + + // Convert to host-endian and set the A channel to 0xFF. + for (uint32_t &p : pal_data) { + p = le32_to_cpu(p) | 0xFF000000U; + } + } + + // Calculate the icon, mask, and total image size. + unsigned int icon_size = stride * half_height; + unsigned int mask_size = mask_stride * half_height; + + size_t biSizeImage = icon_size + mask_size; + rp::uvector img_data; + img_data.resize(biSizeImage); + size_t size = f_icon->seekAndRead(addr, img_data.data(), biSizeImage); + if (size != biSizeImage) { + // Seek and/or read error. + return {}; + } + + // Convert the main image first. + // TODO: Apply the mask. + const uint8_t *icon_data, *mask_data; + if (is_upside_down) { + icon_data = img_data.data(); + mask_data = icon_data + icon_size; + } else { + // TODO: Need to test this. Might not be correct. + // I don't have any right-side up icons... + mask_data = img_data.data(); + icon_data = mask_data + mask_size; + } + + switch (bitcount) { + default: + // Not supported yet... + assert(!"This Win3.x icon format is not supported yet!"); + return {}; + + case 1: + // 1bpp (monochrome) + // NOTE: ImageDecoder::fromLinearMono_WinIcon() handles the mask. + img = ImageDecoder::fromLinearMono_WinIcon(width, half_height, + icon_data, icon_size, + mask_data, mask_size, stride); + break; + + case 4: + // 16-color + // NOTE: fromLinearCI4() doesn't support Host_xRGB32, + // and we're setting the alpha channel to 0xFF ourselves. + img = ImageDecoder::fromLinearCI4(ImageDecoder::PixelFormat::Host_ARGB32, true, + width, half_height, + icon_data, icon_size, + pal_data.data(), pal_data.size() * sizeof(uint32_t), + stride); + break; + + case 8: + // 256-color + img = ImageDecoder::fromLinearCI8(ImageDecoder::PixelFormat::Host_xRGB32, + width, half_height, + icon_data, icon_size, + pal_data.data(), pal_data.size() * sizeof(uint32_t), + stride); + break; + + case 32: { + // 32-bit ARGB + const uint32_t biCompression = le32_to_cpu(bih->biCompression); + if (biCompression != BI_RGB) { + // FIXME: BI_BITFIELDS is not supported right now. + return {}; + } + img = ImageDecoder::fromLinear32(ImageDecoder::PixelFormat::ARGB8888, + width, half_height, + reinterpret_cast(icon_data), icon_size, + stride); + break; + } + } + + // Apply the icon mask. + rp_image::sBIT_t sBIT; + img->get_sBIT(&sBIT); + if (bitcount == 1) { + // Monochrome icons are handled by ImageDecoder::fromLinearMono_WinIcon(). + } else if (bitcount < 8) { + // Keep the icon as CI8 and add a transparency color. + // TODO: Set sBIT. + assert(img->format() == rp_image::Format::CI8); + uint8_t tr_idx = (1U << bitcount); + img->palette()[tr_idx] = 0; + img->set_tr_idx(tr_idx); + + uint8_t *bits = static_cast(img->bits()); + const uint8_t *mask = mask_data; + int dest_stride_adj = img->stride() - width; + int mask_stride_adj = mask_stride - ((width / 8) + (width % 8 > 0)); + for (unsigned int y = half_height; y > 0; y--) { + // TODO: Mask stride adjustment? + unsigned int mask_bits_remain = 8; + uint8_t mask_byte = *mask++; + + for (unsigned int x = width; x > 0; x--, bits++, mask_byte <<= 1, mask_bits_remain--) { + if (mask_bits_remain == 0) { + // Get the next mask byte. + mask_byte = *mask++; + mask_bits_remain = 8; + } + + if (mask_byte & 0x80) { + // Mask the pixel. + // TODO: For 1bpp, if the destination pixel is white, don't mask it. + // This would be "invert" mode. + *bits = tr_idx; + } + } + + // Next row. + bits += dest_stride_adj; + mask += mask_stride_adj; + } + + // Update the sBIT metadata. + if (sBIT.alpha == 0) { + sBIT.alpha = 1; + img->set_sBIT(&sBIT); + } + } else { + // CI8 needs to be converted to ARGB32. + if (img->format() != rp_image::Format::ARGB32) { + rp_image_ptr convimg = img->dup_ARGB32(); + assert((bool)convimg); + if (!convimg) { + // Cannot convert the image for some reason... + // Flip it if necessary and then give up. + if (is_upside_down) { + rp_image_ptr flipimg = img->flip(rp_image::FLIP_V); + if (flipimg) { + img = std::move(flipimg); + } + } + return img; + } + img = std::move(convimg); + } + + assert(img->format() == rp_image::Format::ARGB32); + + uint32_t *bits = static_cast(img->bits()); + const uint8_t *mask = mask_data; + int dest_stride_adj = (img->stride() / sizeof(uint32_t)) - width; + int mask_stride_adj = mask_stride - ((width / 8) + (width % 8 > 0)); + for (unsigned int y = half_height; y > 0; y--) { + // TODO: Mask stride adjustment? + unsigned int mask_bits_remain = 8; + uint8_t mask_byte = *mask++; + + for (unsigned int x = width; x > 0; x--, bits++, mask_byte <<= 1, mask_bits_remain--) { + if (mask_bits_remain == 0) { + // Get the next mask byte. + mask_byte = *mask++; + mask_bits_remain = 8; + } + + if (mask_byte & 0x80) { + // Make the pixel transparent. + // NOTE: Complete transparency, without keeping the RGB. + *bits = 0; + } + } + + // Next row. + bits += dest_stride_adj; + mask += mask_stride_adj; + } + + // Update the sBIT metadata. + if (sBIT.alpha == 0) { + sBIT.alpha = 1; + img->set_sBIT(&sBIT); + } + } + + // Flip the icon after the mask is processed. + if (is_upside_down) { + rp_image_ptr flipimg = img->flip(rp_image::FLIP_V); + if (flipimg) { + img = std::move(flipimg); + } + } + + return img; +} + +/** + * Load the image. (Windows Vista PNG format) + * @return Image, or nullptr on error. + */ +rp_image_const_ptr ICOPrivate::loadImage_WinVista_PNG(void) +{ + // Use RpPng to load a PNG image. + IRpFilePtr f_png; + + const uint16_t rt = imageResType(); + if (rt != 0) { + // Load the PNG from a resource. + const auto &res = *(dir.res); + f_png = res.resReader->open(rt, res.id, res.lang); + } else { + // NOTE: PartitionFile only supports IDiscReader, so we'll need to + // create a dummy DiscReader object. + IDiscReaderPtr discReader = std::make_shared(file, 0, file->size()); + const ICONDIRENTRY *const pBestIcon = dir.ico->pBestIcon; + f_png = std::make_shared(discReader, pBestIcon->dwImageOffset, pBestIcon->dwBytesInRes); + } + + img = RpPng::load(f_png); + return img; +} + +/** + * Load the image. + * @return Image, or nullptr on error. + */ +rp_image_const_ptr ICOPrivate::loadImage(void) +{ + if (img) { + // Image has already been loaded. + return img; + } else if (!this->isValid || !this->file) { + // Can't load the image. + return {}; + } + + switch (iconType) { + default: + // Not supported... + return {}; + + case IconType::Icon_Win1: + case IconType::Cursor_Win1: + case IconType::IconRes_Win1: + case IconType::CursorRes_Win1: + // Windows 1.0 icon or cursor + return loadImage_Win1(); + + case IconType::Icon_Win3: + case IconType::Cursor_Win3: + case IconType::IconRes_Win3: + case IconType::CursorRes_Win3: + // Windows 3.x icon or cursor + return loadImage_Win3(); + } +} + +/** ICO **/ + +/** + * Read a Windows icon or cursor image file. + * + * A ROM image must be opened by the caller. The file handle + * will be ref()'d and must be kept open in order to load + * data from the ROM image. + * + * To close the file, either delete this object or call close(). + * + * NOTE: Check isValid() to determine if this is a valid ROM. + * + * @param file Open ROM image. + */ +ICO::ICO(const IRpFilePtr &file) + : super(new ICOPrivate(this, file)) +{ + init(false); +} + +/** + * Read an icon or cursor from a Windows executable. + * + * A ROM image must be opened by the caller. The file handle + * will be ref()'d and must be kept open in order to load + * data from the ROM image. + * + * To close the file, either delete this object or call close(). + * + * NOTE: Check isValid() to determine if this is a valid ROM. + * + * @param resReader [in] IResourceReader + * @param type [in] Resource type ID + * @param id [in] Resource ID (-1 for "first entry") + * @param lang [in] Language ID (-1 for "first entry") + */ +ICO::ICO(const LibRpBase::IResourceReaderPtr &resReader, uint16_t type, int id, int lang) + : super(new ICOPrivate(this, resReader, type, id, lang)) +{ + init(true); +} + +/** + * Common initialization function. + * @param res True if the icon is in a Windows executable; false if not. + */ +void ICO::init(bool res) +{ + RP_D(ICO); + + if (!d->file) { + // Could not ref() the file handle. + return; + } + + // Read the ICONDIR. + if (res) { + const auto &res = *(d->dir.res); + IRpFilePtr f_icondir = res.resReader->open(res.type, res.id, res.lang); + if (!f_icondir) { + // Unable to open the icon or cursor. + d->file.reset(); + delete d->dir.res; + d->dir.res = nullptr; + return; + } + + size_t size = f_icondir->read(&d->icoHeader, sizeof(d->icoHeader)); + if (size != sizeof(d->icoHeader)) { + d->file.reset(); + delete d->dir.res; + d->dir.res = nullptr; + return; + } + } else { + d->file->rewind(); + size_t size = d->file->read(&d->icoHeader, sizeof(d->icoHeader)); + if (size != sizeof(d->icoHeader)) { + d->file.reset(); + return; + } + } + + // Determine the icon type. + // NOTE: d->iconType is already set if loading from a Windows resource. + // Only set it if it's still ICOPrivate::IconType::Unknown. + switch (le16_to_cpu(d->icoHeader.win1.format)) { + default: + // Not recognized... + d->file.reset(); + if (res) { + delete d->dir.res; + d->dir.res = nullptr; + } + return; + + case ICO_WIN1_FORMAT_MAYBE_WIN3: { + switch (le16_to_cpu(d->icoHeader.win3.idType)) { + default: + // Not recognized... + d->file.reset(); + if (res) { + delete d->dir.res; + d->dir.res = nullptr; + } + return; + case ICO_WIN3_TYPE_ICON: + if (d->iconType == ICOPrivate::IconType::Unknown) { + d->iconType = ICOPrivate::IconType::Icon_Win3; + } + d->mimeType = "image/vnd.microsoft.icon"; + d->textureFormatName = "Windows 3.x Icon"; + break; + case ICO_WIN3_TYPE_CURSOR: + if (d->iconType == ICOPrivate::IconType::Unknown) { + d->iconType = ICOPrivate::IconType::Cursor_Win3; + } + d->mimeType = "image/vnd.microsoft.cursor"; + d->textureFormatName = "Windows 3.x Icon"; + break; + } + + // Load the icon directory and select the best image. + int ret = d->loadIconDirectory_Win3(); + if (ret != 0) { + // Failed to load the icon directory. + d->file.reset(); + if (res) { + delete d->dir.res; + d->dir.res = nullptr; + } + return; + } + break; + } + + case ICO_WIN1_FORMAT_ICON_DIB: + case ICO_WIN1_FORMAT_ICON_DDB: + case ICO_WIN1_FORMAT_ICON_BOTH: + if (d->iconType == ICOPrivate::IconType::Unknown) { + d->iconType = ICOPrivate::IconType::Icon_Win1; + } + // TODO: Different MIME type for Windows 1.x? + d->mimeType = "image/vnd.microsoft.icon"; + d->textureFormatName = "Windows 1.x Icon"; + break; + + case ICO_WIN1_FORMAT_CURSOR_DIB: + case ICO_WIN1_FORMAT_CURSOR_DDB: + case ICO_WIN1_FORMAT_CURSOR_BOTH: + if (d->iconType == ICOPrivate::IconType::Unknown) { + d->iconType = ICOPrivate::IconType::Cursor_Win1; + } + // TODO: Different MIME type for Windows 1.x? + d->mimeType = "image/vnd.microsoft.cursor"; + d->textureFormatName = "Windows 1.x Cursor"; + break; + } + + // Cache the dimensions for the FileFormat base class. + switch (d->iconType) { + default: + // Shouldn't get here... + assert(!"Invalid case!"); + d->file.reset(); + if (res) { + delete d->dir.res; + d->dir.res = nullptr; + } + return; + + case ICOPrivate::IconType::Icon_Win1: + case ICOPrivate::IconType::Cursor_Win1: + case ICOPrivate::IconType::IconRes_Win1: + case ICOPrivate::IconType::CursorRes_Win1: + d->dimensions[0] = le16_to_cpu(d->icoHeader.win1.width); + d->dimensions[1] = le16_to_cpu(d->icoHeader.win1.height); + break; + + case ICOPrivate::IconType::Icon_Win3: + case ICOPrivate::IconType::Cursor_Win3: + case ICOPrivate::IconType::IconRes_Win3: + case ICOPrivate::IconType::CursorRes_Win3: + if (!d->dir.ico->pBestIcon) { + // No "best" icon... + d->file.reset(); + if (res) { + delete d->dir.res; + d->dir.res = nullptr; + } + return; + } + + const ICOPrivate::IconBitmapHeader_t *const pIconHeader = d->pIconHeader; + switch (pIconHeader->size) { + default: + // Not supported... + d->file.reset(); + if (res) { + delete d->dir.res; + d->dir.res = nullptr; + } + return; + + case BITMAPCOREHEADER_SIZE: + d->dimensions[0] = le16_to_cpu(pIconHeader->bch.bcWidth); + d->dimensions[1] = le16_to_cpu(pIconHeader->bch.bcHeight) / 2; + break; + + case BITMAPINFOHEADER_SIZE: + case BITMAPV2INFOHEADER_SIZE: + case BITMAPV3INFOHEADER_SIZE: + case BITMAPV4HEADER_SIZE: + case BITMAPV5HEADER_SIZE: + d->dimensions[0] = le32_to_cpu(pIconHeader->bih.biWidth); + d->dimensions[1] = le32_to_cpu(pIconHeader->bih.biHeight) / 2; + break; + + case 0x474E5089: // "\x89PNG" + // TODO: Verify more IHDR fields? + d->dimensions[0] = be32_to_cpu(pIconHeader->png.ihdr.data.width); + d->dimensions[1] = be32_to_cpu(pIconHeader->png.ihdr.data.height); + break; + } + break; + } + + // File is valid. + d->isValid = true; +} + +/** Property accessors **/ + +/** + * Get the pixel format, e.g. "RGB888" or "DXT1". + * @return Pixel format, or nullptr if unavailable. + */ +const char *ICO::pixelFormat(void) const +{ + RP_D(const ICO); + if (!d->isValid) { + return nullptr; + } + + switch (d->iconType) { + default: + assert(!"Invalid icon type?"); + return nullptr; + + case ICOPrivate::IconType::Icon_Win1: + case ICOPrivate::IconType::Cursor_Win1: + // Windows 1.x only supports monochrome. + return "1bpp"; + + case ICOPrivate::IconType::Icon_Win3: + case ICOPrivate::IconType::Cursor_Win3: + // Check what the "best" icon is. + // TODO: Check BITMAPCOREHEADER, BITMAPINFOHEADER, or the PNG header. + return "Win3.x (TODO)"; + } +} + +#ifdef ENABLE_LIBRPBASE_ROMFIELDS +/** + * Get property fields for rom-properties. + * @param fields RomFields object to which fields should be added. + * @return Number of fields added, or 0 on error. + */ +int ICO::getFields(RomFields *fields) const +{ + assert(fields != nullptr); + if (!fields) + return 0; + + RP_D(const ICO); + if (!d->isValid) { + // Unknown file type. + return -EIO; + } + + const int initial_count = fields->count(); + fields->reserve(initial_count + 1); // Maximum of 1 field. + + // TODO: ICO/CUR fields? + // and "color" for Win1.x cursors + + // Finished reading the field data. + return (fields->count() - initial_count); +} +#endif /* ENABLE_LIBRPBASE_ROMFIELDS */ + +/** Image accessors **/ + +/** + * Get the image. + * For textures with mipmaps, this is the largest mipmap. + * The image is owned by this object. + * @return Image, or nullptr on error. + */ +rp_image_const_ptr ICO::image(void) const +{ + RP_D(const ICO); + if (!d->isValid) { + // Unknown file type. + return {}; + } + + // Load the image. + return const_cast(d)->loadImage(); +} + +} // namespace LibRpTexture diff --git a/src/librptexture/fileformat/ICO.hpp b/src/librptexture/fileformat/ICO.hpp new file mode 100644 index 000000000..0ef3e318d --- /dev/null +++ b/src/librptexture/fileformat/ICO.hpp @@ -0,0 +1,48 @@ +/*************************************************************************** + * ROM Properties Page shell extension. (librptexture) * + * ICO.hpp: Windows icon and cursor image reader. * + * * + * Copyright (c) 2017-2025 by David Korth. * + * SPDX-License-Identifier: GPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "FileFormat.hpp" + +// IResourceReader for loading icons from .exe/.dll files. +#include "librpbase/disc/IResourceReader.hpp" + +namespace LibRpTexture { + +FILEFORMAT_DECL_BEGIN(ICO) + +public: + /** + * Read an icon or cursor from a Windows executable. + * + * A ROM image must be opened by the caller. The file handle + * will be ref()'d and must be kept open in order to load + * data from the ROM image. + * + * To close the file, either delete this object or call close(). + * + * NOTE: Check isValid() to determine if this is a valid ROM. + * + * @param resReader [in] IResourceReader + * @param type [in] Resource type ID (RT_GROUP_ICON or RT_GROUP_CURSOR) + * @param id [in] Resource ID (-1 for "first entry") + * @param lang [in] Language ID (-1 for "first entry") + */ + explicit ICO(const LibRpBase::IResourceReaderPtr &resReader, uint16_t type, int id, int lang); + +private: + /** + * Common initialization function. + * @param res True if the icon is in a Windows executable; false if not. + */ + void init(bool res); + +FILEFORMAT_DECL_END() + +} diff --git a/src/librptexture/fileformat/ico_structs.h b/src/librptexture/fileformat/ico_structs.h new file mode 100644 index 000000000..bb13451e3 --- /dev/null +++ b/src/librptexture/fileformat/ico_structs.h @@ -0,0 +1,236 @@ +/*************************************************************************** + * ROM Properties Page shell extension. (librptexture) * + * ico_structs.h: Windows icon and cursor format data structures. * + * * + * Copyright (c) 2019-2025 by David Korth. * + * SPDX-License-Identifier: GPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include "common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * References: + * - http://justsolve.archiveteam.org/wiki/Windows_1.0_Icon + * - http://justsolve.archiveteam.org/wiki/Windows_1.0_Cursor + * - http://justsolve.archiveteam.org/wiki/ICO + * - http://justsolve.archiveteam.org/wiki/CUR + * - https://devblogs.microsoft.com/oldnewthing/20101018-00/?p=12513 + */ + +/** + * Windows 1.0: Icon (and cursor) + * + * All fields are in little-endian. + */ +typedef struct _ICO_Win1_Header { + uint16_t format; // [0x000] See ICO_Win1_Format_e + uint16_t hotX; // [0x002] Cursor hotspot X (cursors only) + uint16_t hotY; // [0x004] Cursor hotspot Y (cursors only) + uint16_t width; // [0x006] Width, in pixels + uint16_t height; // [0x008] Height, in pixels + uint16_t stride; // [0x00A] Row stride, in bytes + uint16_t color; // [0x00C] Cursor color +} ICO_Win1_Header; +ASSERT_STRUCT(ICO_Win1_Header, 14); + +/** + * Windows 1.0: Icon format + */ +typedef enum { + ICO_WIN1_FORMAT_MAYBE_WIN3 = 0x0000U, // may be a Win3 icon/cursor + + ICO_WIN1_FORMAT_ICON_DIB = 0x0001U, + ICO_WIN1_FORMAT_ICON_DDB = 0x0101U, + ICO_WIN1_FORMAT_ICON_BOTH = 0x0201U, + + ICO_WIN1_FORMAT_CURSOR_DIB = 0x0003U, + ICO_WIN1_FORMAT_CURSOR_DDB = 0x0103U, + ICO_WIN1_FORMAT_CURSOR_BOTH = 0x0203U, +} ICO_Win1_Format_e; + +/** + * Windows 3.x: Icon header + * Layout-compatible with the Windows 1.0 icon header. + * + * All fields are in little-endian. + */ +typedef struct _ICONDIR { + uint16_t idReserved; // [0x000] Zero for Win3.x icons + uint16_t idType; // [0x002] See ICO_Win3_idType_e + uint16_t idCount; // [0x004] Number of images +} ICONDIR, ICONHEADER; +ASSERT_STRUCT(ICONDIR, 6); + +/** + * Windows 3.x: Icon header, RES version (RT_GROUP_ICON) + * + * All fields are in little-endian. + */ +typedef struct _GRPICONDIR { + uint16_t idReserved; // [0x000] Zero for Win3.x icons + uint16_t idType; // [0x002] See ICO_Win3_idType_e + uint16_t idCount; // [0x004] Number of images +} GRPICONDIR; +ASSERT_STRUCT(GRPICONDIR, 6); + +/** + * Windows 3.x: Icon types + */ +typedef enum { + ICO_WIN3_TYPE_ICON = 0x0001U, + ICO_WIN3_TYPE_CURSOR = 0x0002U, +} ICO_Win3_IdType_e; + +/** + * Windows 3.x: Icon directory entry + * + * All fields are in little-endian. + */ +typedef struct _ICONDIRENTRY { + uint8_t bWidth; // [0x000] Width (0 is actually 256) + uint8_t bHeight; // [0x001] Height (0 is actually 256) + uint8_t bColorCount; // [0x002] Color count + uint8_t bReserved; // [0x003] + uint16_t wPlanes; // [0x004] Bitplanes (if >1, multiply by wBitCount) + uint16_t wBitCount; // [0x006] Bitcount + uint32_t dwBytesInRes; // [0x008] Size + uint32_t dwImageOffset; // [0x00C] Offset +} ICONDIRENTRY; +ASSERT_STRUCT(ICONDIRENTRY, 16); + +/** + * Windows 3.x: Icon directory entry, RES version (RT_GROUP_ICON) + * + * All fields are in little-endian. + */ +#pragma pack(2) +typedef struct RP_PACKED _GRPICONDIRENTRY { + uint8_t bWidth; // [0x000] Width (0 is actually 256) + uint8_t bHeight; // [0x001] Height (0 is actually 256) + uint8_t bColorCount; // [0x002] Color count + uint8_t bReserved; // [0x003] + uint16_t wPlanes; // [0x004] Bitplanes (if >1, multiply by wBitCount) + uint16_t wBitCount; // [0x006] Bitcount + uint32_t dwBytesInRes; // [0x008] Size + uint16_t nID; // [0x00C] RT_ICON ID +} GRPICONDIRENTRY; +ASSERT_STRUCT(GRPICONDIRENTRY, 14); +#pragma pack() + +// Windows 3.x icons can either have BITMAPCOREHEADER, BITMAPINFOHEADER, +// or a raw PNG image (supported by Windows Vista and later). + +// TODO: Combine with librpbase/tests/bmp.h? + +/** + * BITMAPCOREHEADER + * All fields are little-endian. + * Reference: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader + */ +#define BITMAPCOREHEADER_SIZE 12U +typedef struct tagBITMAPCOREHEADER { + uint32_t bcSize; + uint16_t bcWidth; + uint16_t bcHeight; + uint16_t bcPlanes; + uint16_t bcBitCount; +} BITMAPCOREHEADER; +ASSERT_STRUCT(BITMAPCOREHEADER, BITMAPCOREHEADER_SIZE); + +/** + * BITMAPINFOHEADER + * All fields are little-endian. + * Reference: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader + */ +#define BITMAPINFOHEADER_SIZE 40U +#define BITMAPV2INFOHEADER_SIZE 52U +#define BITMAPV3INFOHEADER_SIZE 56U +#define BITMAPV4HEADER_SIZE 108U +#define BITMAPV5HEADER_SIZE 124U +typedef struct tagBITMAPINFOHEADER { + uint32_t biSize; // [0x000] sizeof(BITMAPINFOHEADER) + int32_t biWidth; // [0x004] Width, in pixels + int32_t biHeight; // [0x008] Height, in pixels + uint16_t biPlanes; // [0x00C] Bitplanes + uint16_t biBitCount; // [0x00E] Bitcount + uint32_t biCompression; // [0x010] Compression (see BMP_Compression_e) + uint32_t biSizeImage; + int32_t biXPelsPerMeter; + int32_t biYPelsPerMeter; + uint32_t biClrUsed; + uint32_t biClrImportant; +} BITMAPINFOHEADER; +ASSERT_STRUCT(BITMAPINFOHEADER, BITMAPINFOHEADER_SIZE); + +/** + * Bitmap compression + * + * NOTE: For Windows icons, only BI_RGB and BI_BITFIELDS are valid. + * For PNG, use a raw PNG without BITMAPINFOHEADER. + */ +typedef enum { + BI_RGB = 0U, + BI_RLE8 = 1U, + BI_RLE4 = 2U, + BI_BITFIELDS = 3U, + BI_JPEG = 4U, + BI_PNG = 5U, +} BMP_Compression_e; + +/** PNG chunks (TODO: Combine with librpbase/tests/bmp.h?) **/ + +/* These describe the color_type field in png_info. */ +/* color type masks */ +#define PNG_COLOR_MASK_PALETTE 1 +#define PNG_COLOR_MASK_COLOR 2 +#define PNG_COLOR_MASK_ALPHA 4 + +/* color types. Note that not all combinations are legal */ +#define PNG_COLOR_TYPE_GRAY 0 +#define PNG_COLOR_TYPE_PALETTE (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_PALETTE) +#define PNG_COLOR_TYPE_RGB (PNG_COLOR_MASK_COLOR) +#define PNG_COLOR_TYPE_RGB_ALPHA (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_ALPHA) +#define PNG_COLOR_TYPE_GRAY_ALPHA (PNG_COLOR_MASK_ALPHA) +/* aliases */ +#define PNG_COLOR_TYPE_RGBA PNG_COLOR_TYPE_RGB_ALPHA +#define PNG_COLOR_TYPE_GA PNG_COLOR_TYPE_GRAY_ALPHA + +/** + * PNG IHDR chunk + */ +#pragma pack(1) +typedef struct RP_PACKED _PNG_IHDR_t { + uint32_t width; // BE32 + uint32_t height; // BE32 + uint8_t bit_depth; + uint8_t color_type; + uint8_t compression_method; + uint8_t filter_method; + uint8_t interlace_method; +} PNG_IHDR_t; +ASSERT_STRUCT(PNG_IHDR_t, 13); +#pragma pack() + +/** + * PNG IHDR struct, with length, name, and CRC32. + */ +#pragma pack(1) +typedef struct RP_PACKED _PNG_IHDR_full_t { + uint32_t chunk_size; // BE32 + char chunk_name[4]; // "IHDR" + PNG_IHDR_t data; + uint32_t crc32; +} PNG_IHDR_full_t; +ASSERT_STRUCT(PNG_IHDR_full_t, 25); +#pragma pack() + +#ifdef __cplusplus +} +#endif diff --git a/xdg/mime.no-thumbnail.types b/xdg/mime.no-thumbnail.types index e8f04b9be..9e8db0311 100644 --- a/xdg/mime.no-thumbnail.types +++ b/xdg/mime.no-thumbnail.types @@ -89,6 +89,15 @@ application/x-ms-wim # Wim # Texture # Formats listed here are widely supported by Linux desktop # environments, but not other systems. +application/ico # ICO +image/ico # ICO +image/icon # ICO +image/x-cursor # ICO +image/x-ico # ICO +image/x-icon # ICO +image/vnd.microsoft.cursor # ICO +image/vnd.microsoft.icon # ICO +text/ico # ICO application/tga # TGA application/x-targa # TGA application/x-tga # TGA