├── pkgrename.c ├── include │ ├── onlinesearch.h │ ├── characters.h │ ├── strings.h │ ├── checksums.h │ ├── terminal.h │ ├── options.h │ ├── releaselists.h │ ├── pkg.h │ ├── common.h │ ├── colors.h │ ├── scan.h │ ├── sha256.h │ └── getopt.h ├── src │ ├── checksums.c │ ├── common.c │ ├── characters.c │ ├── onlinesearch.c │ ├── terminal.c │ ├── releaselists.c │ ├── strings.c │ ├── scan.c │ ├── sha256.c │ ├── pkg.c │ └── options.c └── pkgrename.c ├── pkgrename └── README.md /pkgrename.c/include/onlinesearch.h: -------------------------------------------------------------------------------- 1 | #define MAX_FILENAME_LEN 256 2 | 3 | void search_online(char *content_id, char *title, int silent); 4 | -------------------------------------------------------------------------------- /pkgrename.c/include/characters.h: -------------------------------------------------------------------------------- 1 | #ifndef CHARACTERS_H 2 | #define CHARACTERS_H 3 | 4 | extern char *illegal_characters; 5 | extern char placeholder_char; 6 | 7 | int is_in_set(char c, char *set); 8 | int count_spec_chars(char *string); 9 | void replace_illegal_characters(char *string); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /pkgrename.c/include/strings.h: -------------------------------------------------------------------------------- 1 | #ifndef STRINGS_H 2 | #define STRINGS_H 3 | 4 | void trim_string(char *string, char *ltrim, char *rtrim); 5 | char *strwrd(const char *string, char *word); 6 | char *strreplace(char *string, char *search, char *replace); 7 | void mixed_case(char *string); 8 | int lower_strcmp(char *string1, char *string2); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /pkgrename.c/include/checksums.h: -------------------------------------------------------------------------------- 1 | #ifndef CHECKSUMS_H 2 | #define CHECKSUMS_H 3 | 4 | #include 5 | 6 | // Prints a checksum stored in a buffer to the provided file descriptor. 7 | void print_checksum(FILE *stream, unsigned char *buf, size_t buf_size); 8 | 9 | // Calculates a buffer's checksum. 10 | void sha256(void *to, void *from, size_t from_size); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /pkgrename.c/src/checksums.c: -------------------------------------------------------------------------------- 1 | #include "../include/checksums.h" 2 | #include "../include/sha256.h" 3 | 4 | void print_checksum(FILE *stream, unsigned char *buf, size_t buf_size) 5 | { 6 | for (size_t i = 0; i < buf_size; i++) 7 | fprintf(stream, "%02x", buf[i]); 8 | } 9 | 10 | void sha256(void *to, void *from, size_t from_size) 11 | { 12 | Sha256Context context; 13 | Sha256Initialise(&context); 14 | Sha256Update(&context, from, from_size); 15 | Sha256Finalise(&context, to); 16 | } 17 | -------------------------------------------------------------------------------- /pkgrename.c/src/common.c: -------------------------------------------------------------------------------- 1 | #include "../include/common.h" 2 | 3 | #include 4 | #include 5 | 6 | char *tag_separator = ","; 7 | char *BACKPORT_STRING = "Backport"; 8 | char *FAKE_STRING = "Fake"; 9 | char *RETAIL_STRING = "Retail"; 10 | 11 | void exit_err(int err, const char *function_name, int line) 12 | { 13 | fprintf(stderr, "Unrecoverable error %d in function %s, line %d.\n", 14 | err, function_name, line); 15 | fprintf(stderr, "Please report this bug at \"%s\".\n", SUPPORT_LINK); 16 | exit(err); 17 | } 18 | -------------------------------------------------------------------------------- /pkgrename.c/include/terminal.h: -------------------------------------------------------------------------------- 1 | #ifndef TERMINAL_H 2 | #define TERMINAL_H 3 | 4 | // Needs to be called before other functions 5 | // Parameter is the function to be called on SIGINT 6 | void initialize_terminal(); 7 | 8 | // Resets terminal 9 | void reset_terminal(); 10 | 11 | // Sets terminal to noncanonical input mode (= no need to press enter) 12 | void raw_terminal(); 13 | 14 | // Reads user input and stores it in buffer "string" 15 | void scan_string(char *string, size_t max_size, char *default_string, char *(*f)(char *)); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /pkgrename.c/include/options.h: -------------------------------------------------------------------------------- 1 | #ifndef OPTIONS_H 2 | #define OPTIONS_H 3 | 4 | extern int option_override_tags; 5 | extern int option_compact; 6 | extern int option_disable_colors; 7 | extern int option_force; 8 | extern int option_force_backup; 9 | extern int option_mixed_case; 10 | extern int option_no_placeholder; 11 | extern int option_no_to_all; 12 | extern char option_language_number[3]; 13 | extern int option_leading_zeros; 14 | extern int option_online; 15 | extern int option_query; 16 | extern int option_recursive; 17 | extern char *option_tag_separator; 18 | extern int option_underscores; 19 | extern int option_verbose; 20 | extern int option_yes_to_all; 21 | 22 | void print_usage(void); 23 | void print_prompt_help(void); 24 | void parse_options(int *argc, char **argv[]); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /pkgrename.c/include/releaselists.h: -------------------------------------------------------------------------------- 1 | #ifndef RELEASELISTS_H 2 | #define RELEASELISTS_H 3 | 4 | // Searches the argument for known release groups and returns the first match. 5 | char *get_release_group(char *string); 6 | 7 | // Searches the argument for known releases and returns the first match. 8 | int get_release(char **release, const char *string); 9 | 10 | // Used as autocomplete function for scan_string() (in terminal.c). 11 | // Returns the name of a tag if it is found in "string". 12 | char *get_tag(char *string); 13 | 14 | // Replaces all commas in a release tag with a custom string. 15 | // The buffer is stored in must be of length MAX_TAG_LEN. 16 | void replace_commas_in_tag(char *tag, const char *string); 17 | 18 | // Searches a changelog buffer for all known release tags and prints them. 19 | void print_changelog_tags(const char *changelog_buf); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /pkgrename.c/include/pkg.h: -------------------------------------------------------------------------------- 1 | #ifndef PKG_H 2 | #define PKG_H 3 | 4 | // Loads PKG data into dynamically allocated buffers and passes their pointers. 5 | // Returns 0 on success and -1 on error. 6 | int load_pkg_data(unsigned char **param_sfo, char **changelog, 7 | _Bool *fake_status, const char *filename); 8 | 9 | // Searches a buffered param.sfo file for a key/value pair and returns a pointer 10 | // to the value. Returns NULL if the key is not found. 11 | void *get_param_sfo_value(const unsigned char *param_sfo_buf, const char *key); 12 | 13 | // Prints a buffered param.sfo file's keys and values. 14 | void print_param_sfo(const unsigned char *param_sfo_buf); 15 | 16 | // Loads the true patch version from a string and stores it in a buffer; 17 | // the buffer must be of size 6. 18 | // Returns 1 if the patch version has been found, otherwise 0. 19 | int store_patch_version(char *version_buf, const char *changelog); 20 | 21 | // Get a PKG file's compatibility checksum. 22 | int get_checksum(char msum[7], const char *filename); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /pkgrename.c/include/common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | #include 5 | 6 | #ifdef _WIN32 7 | #define DIR_SEPARATOR '\\' 8 | #else 9 | #define DIR_SEPARATOR '/' 10 | #endif 11 | 12 | #define MAX_FILENAME_LEN 256 // exFAT file name limit (+1) 13 | #define MAX_FORMAT_STRING_LEN 512 14 | #define MAX_TAG_LEN 50 15 | #define MAX_TAGS 100 16 | #define MAX_TITLE_LEN 128 // https://www.psdevwiki.com/ps4/Param.sfo#TITLE 17 | 18 | #define HOMEPAGE_LINK "https://github.com/hippie68/pkgrename" 19 | #define SUPPORT_LINK "https://github.com/hippie68/pkgrename/issues" 20 | 21 | struct custom_category { 22 | char *game; 23 | char *patch; 24 | char *dlc; 25 | char *app; 26 | char *other; 27 | }; 28 | 29 | extern char *BACKPORT_STRING; 30 | extern struct custom_category custom_category; 31 | extern char *FAKE_STRING; 32 | extern char *RETAIL_STRING; 33 | extern char format_string[MAX_FORMAT_STRING_LEN]; 34 | extern char placeholder_char; 35 | extern char *tags[MAX_TAGS]; 36 | extern int tagc; 37 | extern char *tag_separator; 38 | 39 | void exit_err(int err, const char *function_name, int line) 40 | __attribute__ ((noreturn)); 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /pkgrename.c/include/colors.h: -------------------------------------------------------------------------------- 1 | #ifndef COLORS_H 2 | #define COLORS_H 3 | 4 | #define RESET "\033[0m" 5 | 6 | #define BLACK "\033[0;30m" 7 | #define RED "\033[0;31m" 8 | #define GREEN "\033[0;32m" 9 | #define YELLOW "\033[0;33m" 10 | #define BLUE "\033[0;34m" 11 | #define PURPLE "\033[0;35m" 12 | #define CYAN "\033[0;36m" 13 | #define GRAY "\033[0;37m" 14 | 15 | #define BRIGHT_BLACK "\033[0;90m" 16 | #define BRIGHT_RED "\033[0;91m" 17 | #define BRIGHT_GREEN "\033[0;92m" 18 | #define BRIGHT_YELLOW "\033[0;93m" 19 | #define BRIGHT_BLUE "\033[0;94m" 20 | #define BRIGHT_PURPLE "\033[0;95m" 21 | #define BRIGHT_CYAN "\033[0;96m" 22 | #define BRIGHT_GRAY "\033[0;97m" 23 | 24 | #define BG_BLACK "\033[0;40m" 25 | #define BG_RED "\033[0;41m" 26 | #define BG_GREEN "\033[0;42m" 27 | #define BG_YELLOW "\033[0;43m" 28 | #define BG_BLUE "\033[0;44m" 29 | #define BG_PURPLE "\033[0;45m" 30 | #define BG_CYAN "\033[0;46m" 31 | #define BG_GRAY "\033[0;47m" 32 | 33 | #define BG_BRIGHT_BLACK "\033[0;100m" 34 | #define BG_BRIGHT_RED "\033[0;101m" 35 | #define BG_BRIGHT_GREEN "\033[0;102m" 36 | #define BG_BRIGHT_YELLOW "\033[0;103m" 37 | #define BG_BRIGHT_BLUE "\033[0;104m" 38 | #define BG_BRIGHT_PURPLE "\033[0;105m" 39 | #define BG_BRIGHT_CYAN "\033[0;106m" 40 | #define BG_BRIGHT_GRAY "\033[0;107m" 41 | 42 | #define set_color(color, stream) \ 43 | do { \ 44 | if (option_disable_colors == 0) \ 45 | fputs(color, stream); \ 46 | } while (0) 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /pkgrename.c/src/characters.c: -------------------------------------------------------------------------------- 1 | #include "../include/characters.h" 2 | #include "../include/options.h" 3 | 4 | #include 5 | #include 6 | 7 | char *illegal_characters = "\"*/:<>?\\|"; // https://www.ntfs.com/exfat-filename-dentry.htm 8 | char placeholder_char = '_'; 9 | 10 | // Returns 1 if char c is illegal on an exFAT file system. 11 | int is_in_set(char c, char *set) 12 | { 13 | for (size_t i = 0; i < strlen(set); i++) { 14 | if (c == set[i]) 15 | return 1; 16 | } 17 | return 0; 18 | } 19 | 20 | // Returns number of special characters in a string. 21 | int count_spec_chars(char *string) 22 | { 23 | int count = 0; 24 | 25 | for (size_t i = 0; i < strlen(string); i++) { 26 | if (!isprint(string[i])) 27 | count++; 28 | } 29 | 30 | return count; 31 | } 32 | 33 | // Replaces or hides (skips) illegal characters. 34 | void replace_illegal_characters(char *string) 35 | { 36 | char buffer[strlen(string)]; 37 | char *p = buffer; 38 | 39 | for (size_t i = 0; i < strlen(string); i++) { 40 | // Printable character 41 | if (isprint(string[i])) { 42 | if (is_in_set(string[i], illegal_characters)) { 43 | // Replace illegal character. 44 | if (option_no_placeholder) 45 | *(p++) = ' '; // Replace with space. 46 | else 47 | *(p++) = placeholder_char; // Replace with placeholder. 48 | } else { 49 | *(p++) = string[i]; 50 | } 51 | // Non-printable character 52 | } else { // TODO: Skip user-provided non-printable characters 53 | *(p++) = string[i]; 54 | } 55 | } 56 | 57 | *p = '\0'; 58 | strcpy(string, buffer); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /pkgrename.c/include/scan.h: -------------------------------------------------------------------------------- 1 | #ifndef SCAN_H 2 | #define SCAN_H 3 | 4 | #include 5 | 6 | // Linked list node that stores a PS4 PKG file scan result. 7 | struct scan { 8 | char *filename; 9 | unsigned char *param_sfo; 10 | char *changelog; 11 | _Bool fake_status; 12 | _Bool filename_allocated; 13 | enum { 14 | SCAN_ERROR_OPEN_FILE = 1, 15 | SCAN_ERROR_READ_FILE, 16 | SCAN_ERROR_NOT_A_PKG, 17 | SCAN_ERROR_OUT_OF_MEMORY, 18 | SCAN_ERROR_PARAM_SFO_INVALID_DATA, 19 | SCAN_ERROR_PARAM_SFO_INVALID_FORMAT, 20 | SCAN_ERROR_PARAM_SFO_INVALID_SIZE, 21 | SCAN_ERROR_PARAM_SFO_NOT_FOUND, 22 | SCAN_ERROR_CHANGELOG_INVALID_SIZE, 23 | } error; 24 | struct scan *prev; 25 | struct scan *next; 26 | }; 27 | 28 | // This is a linked list that does not allocate individual nodes but instead 29 | // larger memory chunks on demand when it runs out of node slots (see scan.c). 30 | #define SCAN_LIST_CHUNK_SIZE 1024 // Number of nodes per chunk. 31 | struct scan_list { 32 | struct scan *head; 33 | struct scan *tail; 34 | _Bool finished; // True when scanning is complete. 35 | short n_slots; // Number of remaining slots in the current chunk. 36 | }; 37 | 38 | struct scan_job { 39 | struct scan_list scan_list; 40 | pthread_mutex_t mutex; 41 | pthread_cond_t cond; // "A new scan result is ready." 42 | char **filenames; // May contain both files and directories. 43 | int n_filenames; 44 | }; 45 | 46 | // Adds a scan result to a job's scan list. 47 | void add_scan_result(struct scan_job *job, char *filename, 48 | _Bool filename_allocated); 49 | 50 | // Prints a message that describes the value of struct scan's .error member. 51 | void print_scan_error(struct scan *scan); 52 | 53 | // Finds all .pkg files in a directory and runs a scan on them. 54 | // Returns 0 on success and -1 on error. 55 | int parse_directory(char *directory_name, struct scan_job *job); 56 | 57 | // Initializes a scan job. 58 | // Returns 0 on success and -1 on error. 59 | int initialize_scan_job(struct scan_job *job, char **filenames, 60 | int n_filenames); 61 | 62 | // Destroys a scan job. 63 | void destroy_scan_job(struct scan_job *job); 64 | 65 | #ifdef DEBUG 66 | // Debug function. 67 | void print_scan(struct scan *scan); 68 | #endif 69 | 70 | #endif 71 | -------------------------------------------------------------------------------- /pkgrename.c/include/sha256.h: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // WjCryptLib_Sha256 3 | // 4 | // Implementation of SHA256 hash function. 5 | // Original author: Tom St Denis, tomstdenis@gmail.com, http://libtom.org 6 | // Modified by WaterJuice retaining Public Domain license. 7 | // 8 | // This is free and unencumbered software released into the public domain - June 2013 waterjuice.org 9 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 10 | 11 | #ifndef SHA256_H 12 | #define SHA256_H 13 | 14 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 15 | // IMPORTS 16 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | #include 19 | #include 20 | 21 | typedef struct 22 | { 23 | uint64_t length; 24 | uint32_t state[8]; 25 | uint32_t curlen; 26 | uint8_t buf[64]; 27 | } Sha256Context; 28 | 29 | #define SHA256_HASH_SIZE ( 256 / 8 ) 30 | 31 | typedef struct 32 | { 33 | uint8_t bytes [SHA256_HASH_SIZE]; 34 | } SHA256_HASH; 35 | 36 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 37 | // PUBLIC FUNCTIONS 38 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 39 | 40 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 41 | // Sha256Initialise 42 | // 43 | // Initialises a SHA256 Context. Use this to initialise/reset a context. 44 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 45 | void 46 | Sha256Initialise 47 | ( 48 | Sha256Context* Context // [out] 49 | ); 50 | 51 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 52 | // Sha256Update 53 | // 54 | // Adds data to the SHA256 context. This will process the data and update the internal state of the context. Keep on 55 | // calling this function until all the data has been added. Then call Sha256Finalise to calculate the hash. 56 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 57 | void 58 | Sha256Update 59 | ( 60 | Sha256Context* Context, // [in out] 61 | void const* Buffer, // [in] 62 | uint32_t BufferSize // [in] 63 | ); 64 | 65 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 66 | // Sha256Finalise 67 | // 68 | // Performs the final calculation of the hash and returns the digest (32 byte buffer containing 256bit hash). After 69 | // calling this, Sha256Initialised must be used to reuse the context. 70 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 71 | void 72 | Sha256Finalise 73 | ( 74 | Sha256Context* Context, // [in out] 75 | SHA256_HASH* Digest // [out] 76 | ); 77 | 78 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 79 | // Sha256Calculate 80 | // 81 | // Combines Sha256Initialise, Sha256Update, and Sha256Finalise into one function. Calculates the SHA256 hash of the 82 | // buffer. 83 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 84 | void 85 | Sha256Calculate 86 | ( 87 | void const* Buffer, // [in] 88 | uint32_t BufferSize, // [in] 89 | SHA256_HASH* Digest // [in] 90 | ); 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /pkgrename.c/src/onlinesearch.c: -------------------------------------------------------------------------------- 1 | #include "../include/colors.h" 2 | #include "../include/common.h" 3 | #include "../include/onlinesearch.h" 4 | #include "../include/options.h" 5 | 6 | #ifndef _WIN32 7 | #include 8 | #endif 9 | #include 10 | #include 11 | #include 12 | 13 | #define URL_LEN 128 14 | 15 | static int create_url(char url[URL_LEN], char *content_id) 16 | { 17 | char *prefix; 18 | switch (content_id[0]) { 19 | case 'U': 20 | prefix = "https://store.playstation.com/en-us/product/"; 21 | break; 22 | case 'E': 23 | prefix = "https://store.playstation.com/en-gb/product/"; 24 | break; 25 | case 'H': 26 | prefix = "https://store.playstation.com/en-hk/product/"; 27 | break; 28 | case 'J': 29 | prefix = "https://store.playstation.com/ja-jp/product/"; 30 | break; 31 | default: 32 | printf( 33 | "Online search not supported for this Content ID (\"%s\").\n", 34 | content_id); 35 | return 1; 36 | } 37 | strcpy(url, prefix); 38 | strcat(url, content_id); 39 | return 0; 40 | } 41 | 42 | #ifdef _WIN32 43 | void search_online(char *content_id, char *title, int silent) 44 | { 45 | char url[URL_LEN]; 46 | char cmd[128]; 47 | 48 | if (create_url(url, content_id) != 0) 49 | return; 50 | 51 | if (!silent) 52 | printf("Searching online, please wait...\n"); 53 | 54 | // Create cURL command 55 | strcpy(cmd, "curl.exe -Ls --connect-timeout 5 "); 56 | strcat(cmd, url); 57 | //printf("cmd: %s\n", cmd); // DEBUG 58 | 59 | // Run cURL 60 | FILE *pipe = _popen(cmd, "rb"); 61 | if (pipe == NULL) { 62 | fprintf(stderr, "Error while calling curl.exe.\n"); 63 | return; 64 | } 65 | char c; 66 | char curl_output[65536]; 67 | size_t index = 0; 68 | while ((c = fgetc(pipe)) != EOF && index < sizeof(curl_output) - 1) { 69 | curl_output[index] = c; 70 | index++; 71 | } 72 | curl_output[index] = '\0'; 73 | int result = _pclose(pipe); 74 | if (result != 0 && result != 23) { // 23: pipe aborted (curl_output is full) 75 | fprintf(stderr, "An error occured (error code \"%d\").\n" 76 | "See \"https://curl.se/libcurl/c/libcurl-errors.html\".\n", result); 77 | } 78 | 79 | //printf("cURL output: %s\n", curl_output); // DEBUG 80 | 81 | // Search cURL's output for the title 82 | char *start = strstr(curl_output, "@type"); 83 | if (start != NULL && strlen(start) > 25) { 84 | start += 25; 85 | char *end = strchr(start, '"'); 86 | if (end != NULL) { 87 | end[0] = '\0'; 88 | if (strcmp(start, "") != 0) { 89 | if (!silent) 90 | printf("Online title: \"%s\"\n", start); 91 | strncpy(title, start, MAX_FILENAME_LEN); 92 | title[MAX_FILENAME_LEN - 1] = '\0'; 93 | } else goto fail; // String is empty 94 | } else { 95 | set_color(BRIGHT_RED, stderr); 96 | fprintf(stderr, 97 | "Error while searching online. Please contact the developer at" 98 | " \"https://github.com/hippie68/pkgrename/issues\"" 99 | " and show him this link: \"%s\".\n", url); 100 | set_color(RESET, stderr); 101 | } 102 | } else { 103 | fail: 104 | if (!silent) 105 | printf("No online information found.\n"); 106 | } 107 | } 108 | 109 | #else 110 | static char *curl_output = NULL; 111 | 112 | // libcurl callback function 113 | #pragma GCC diagnostic push 114 | #pragma GCC diagnostic ignored "-Wunused-parameter" 115 | static size_t write_callback(char *ptr, size_t size, size_t nmemb, 116 | void *userdata) 117 | { 118 | #pragma GCC diagnostic pop 119 | if (curl_output == NULL) { 120 | curl_output = malloc(1); 121 | curl_output[0] = '\0'; 122 | } 123 | curl_output = realloc(curl_output, strlen(curl_output) + nmemb + 1); 124 | strcat(curl_output, ptr); 125 | return(nmemb); 126 | } 127 | 128 | void search_online(char *content_id, char *title, int silent) 129 | { 130 | char url[URL_LEN]; 131 | 132 | CURL *curl = curl_easy_init(); 133 | if (curl == NULL) { 134 | fprintf(stderr, "Error while initializing cURL.\n"); 135 | return; 136 | } 137 | 138 | if (create_url(url, content_id) != 0) 139 | goto exit; 140 | 141 | if (!silent) 142 | printf("Searching online, please wait...\n"); 143 | 144 | CURLcode result; 145 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5); 146 | curl_easy_setopt(curl, CURLOPT_URL, url); 147 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 148 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); 149 | result = curl_easy_perform(curl); 150 | if (result != CURLE_OK) { 151 | fprintf(stderr, "An error occured (error code \"%d\").\n" 152 | "See https://curl.se/libcurl/c/libcurl-errors.html\n", result); 153 | goto exit; 154 | } 155 | 156 | //printf("cURL output: %s\n", curl_output); // DEBUG 157 | 158 | // Search cURL's output for the title 159 | char *start = strstr(curl_output, "@type"); 160 | if (start != NULL && strlen(start) > 25) { 161 | start += 25; 162 | char *end = strchr(start, '"'); 163 | if (end != NULL) { 164 | end[0] = '\0'; 165 | if (strcmp(start, "") != 0) { 166 | if (!silent) 167 | printf("Online title: \"%s\"\n", start); 168 | strncpy(title, start, MAX_FILENAME_LEN); 169 | title[MAX_FILENAME_LEN - 1] = '\0'; 170 | } else goto fail; // String is empty 171 | } else { 172 | set_color(BRIGHT_RED, stderr); 173 | fprintf(stderr, 174 | "Error while searching online. Please contact the developer at" 175 | " \"https://github.com/hippie68/pkgrename/issues\"" 176 | " and show him this link: \"%s\".\n", url); 177 | set_color(RESET, stderr); 178 | } 179 | } else { 180 | fail: 181 | if (!silent) 182 | printf("No online information found.\n"); 183 | } 184 | 185 | exit: 186 | curl_easy_cleanup(curl); 187 | if (curl_output != NULL) { 188 | free(curl_output); 189 | curl_output = NULL; 190 | } 191 | } 192 | #endif 193 | -------------------------------------------------------------------------------- /pkgrename.c/src/terminal.c: -------------------------------------------------------------------------------- 1 | #include "../include/common.h" 2 | #include "../include/terminal.h" 3 | 4 | #include 5 | #include 6 | 7 | #ifdef _WIN32 8 | static unsigned int win_orig_codepage; 9 | #include 10 | #else 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | static struct termios terminal; 17 | static struct termios terminal_backup; 18 | #endif 19 | 20 | // For scan_string() 21 | #ifdef _WIN32 22 | #include 23 | #define getchar getkey 24 | static HANDLE hStdin; 25 | static DWORD fdwSaveOldMode; 26 | static char WIN_KEY; 27 | #endif 28 | 29 | // Sets terminal to noncanonical input mode (= no need to press enter) 30 | void raw_terminal() { 31 | #ifndef _WIN32 32 | terminal.c_iflag &= ~(ICRNL); 33 | terminal.c_lflag &= ~(ECHO | ICANON); 34 | tcsetattr(STDIN_FILENO, TCSANOW, &terminal); 35 | #endif 36 | } 37 | 38 | // Resets terminal 39 | void reset_terminal() { 40 | #ifndef _WIN32 41 | tcsetattr(STDIN_FILENO, TCSANOW, &terminal_backup); 42 | #endif 43 | } 44 | 45 | // Resets terminal automatically on exit 46 | void atexit_reset_terminal() { 47 | #ifdef _WIN32 48 | SetConsoleOutputCP(win_orig_codepage); 49 | SetConsoleMode(hStdin, fdwSaveOldMode); 50 | #endif 51 | reset_terminal(); 52 | } 53 | 54 | // Needs to be called before other functions 55 | void initialize_terminal() { 56 | atexit(atexit_reset_terminal); 57 | 58 | // Windows 59 | #ifdef _WIN32 60 | win_orig_codepage = GetConsoleOutputCP(); // Store current codepage 61 | SetConsoleOutputCP(65001); // UTF-8 codepage 62 | 63 | hStdin = GetStdHandle(STD_INPUT_HANDLE); 64 | 65 | if (hStdin == INVALID_HANDLE_VALUE) { 66 | fprintf(stderr, "Invalid input handle.\n"); 67 | exit(1); 68 | } 69 | 70 | if (! GetConsoleMode(hStdin, &fdwSaveOldMode) ) { 71 | fprintf(stderr, "Error while calling GetConsoleMode().\n"); 72 | exit(1); 73 | } 74 | 75 | // Other OS 76 | #else 77 | tcgetattr(STDIN_FILENO, &terminal); 78 | terminal_backup = terminal; // Store current terminal attributes 79 | signal(SIGINT, exit); 80 | #endif 81 | } 82 | 83 | #ifdef _WIN32 84 | // Windows replacement for "getchar() in raw mode" 85 | int getkey() { 86 | DWORD cNumRead; 87 | INPUT_RECORD irInBuf[128]; 88 | 89 | // Repeat until key pressed 90 | while (ReadConsoleInput(hStdin, irInBuf, 128, &cNumRead)) { 91 | for (size_t i = 0; i < cNumRead; i++) { 92 | switch(irInBuf[i].EventType) { 93 | case KEY_EVENT: 94 | if (irInBuf[i].Event.KeyEvent.bKeyDown) { 95 | WIN_KEY = irInBuf[i].Event.KeyEvent.uChar.AsciiChar; 96 | return irInBuf[i].Event.KeyEvent.wVirtualKeyCode; 97 | } else { 98 | WIN_KEY = 0; 99 | } 100 | } 101 | } 102 | } 103 | 104 | fprintf(stderr, "Error while calling ReadConsoleInput().\n"); 105 | exit(1); 106 | } 107 | #endif 108 | 109 | // Erase rest of the line 110 | static void clear_line() { 111 | #ifdef _WIN32 112 | for (int i = 0; i <= MAX_TAG_LEN + 4; i++) printf(" "); 113 | for (int i = 0; i <= MAX_TAG_LEN + 4; i++) printf("\b"); 114 | #else 115 | printf("\033[0J"); 116 | #endif 117 | } 118 | 119 | // User input function with editing capabilities; for use with raw_terminal(). 120 | // Function f() is optional; its return value is used for auto-completion. 121 | // Currently not working with Unicode characters. 122 | void scan_string(char *string, size_t max_size, char *default_string, char *(*f)(char *)) 123 | { 124 | #ifdef _WIN32 125 | #define KEY_BACKSPACE 8 126 | #define KEY_DELETE 46 127 | #define KEY_ENTER 13 128 | #define KEY_LEFT_ARROW 37 129 | #define KEY_RIGHT_ARROW 39 130 | #define KEY_TAB 9 131 | #else 132 | #define KEY_BACKSPACE 127 133 | #define KEY_DELETE 126 134 | #define KEY_ENTER 13 135 | #define KEY_LEFT_ARROW 68 136 | #define KEY_RIGHT_ARROW 67 137 | #define KEY_TAB 9 138 | #endif 139 | 140 | char key; 141 | char buffer[max_size * 2]; 142 | char *autocomplete = NULL; 143 | 144 | strncpy(buffer, default_string, max_size); 145 | printf("%s", buffer); 146 | size_t i = strlen(buffer); 147 | 148 | while ((key = getchar()) != KEY_ENTER) { // Enter key 149 | //printf("Number: %d\n", key); // DEBUG 150 | switch (key) { 151 | #ifndef _WIN32 152 | case 27: 153 | while ((key = getchar()) == 27) // Ignore any follow-up escapes 154 | ; 155 | if (key == 91) { // ANSI escape sequence 156 | switch (getchar()) { 157 | #endif 158 | case KEY_LEFT_ARROW: 159 | if (f == NULL && i > 0) { 160 | i--; 161 | printf("\b"); // Move cursor left 162 | } 163 | break; 164 | case KEY_RIGHT_ARROW: 165 | if (i < strlen(buffer)) { 166 | #ifdef _WIN32 167 | printf("%c", buffer[i]); 168 | #else 169 | printf("\033[1C"); // Move cursor right 170 | #endif 171 | i++; 172 | } 173 | break; 174 | #ifndef _WIN32 175 | } 176 | } else goto Default; // Regular key 177 | break; 178 | #endif 179 | case KEY_DELETE: // Delete 180 | memmove(&buffer[i], &buffer[i + 1], max_size); 181 | printf("%s \b", &buffer[i]); 182 | for (size_t count = strlen(buffer); count > i; count--) 183 | printf("\b"); 184 | break; 185 | case KEY_BACKSPACE: // Backspace 186 | if (i > 0) { 187 | printf("\b \b"); 188 | memmove(&buffer[i - 1], &buffer[i], max_size); 189 | i--; 190 | printf("%s \b", &buffer[i]); 191 | for (size_t count = strlen(buffer); count > i; count--) 192 | printf("\b"); 193 | } 194 | break; 195 | case KEY_TAB: // Tabulator 196 | if (autocomplete != NULL) { 197 | for (size_t count = strlen(buffer); count > 0; count--) 198 | printf("\b"); 199 | 200 | char *last_comma = strrchr(buffer, ','); 201 | if (last_comma != NULL) 202 | strncpy(last_comma + 1, autocomplete, max_size - (last_comma - buffer)); 203 | else 204 | strncpy(buffer, autocomplete, max_size); 205 | clear_line(); 206 | printf("%s", buffer); 207 | i = strlen(buffer); 208 | } 209 | break; 210 | default: // Regular key 211 | #ifndef _WIN32 212 | Default: 213 | #endif 214 | #ifdef _WIN32 215 | key = WIN_KEY; 216 | #endif 217 | if (isprint(key) && i < max_size - 1) { 218 | printf("%c", key); 219 | memmove(&buffer[i + 1], &buffer[i], max_size); 220 | buffer[i] = key; 221 | i++; 222 | printf("%s", &buffer[i]); 223 | for (size_t count = strlen(buffer); count > i; count--) 224 | printf("\b"); 225 | } 226 | } 227 | 228 | // Call provided autocomplete function and print its result 229 | if (f != NULL) { 230 | clear_line(); 231 | autocomplete = f(buffer); 232 | if (autocomplete != NULL) { 233 | printf(" [%s]", autocomplete); 234 | for (size_t i = 0; i < strlen(autocomplete) + 4; i++) 235 | printf("\b"); 236 | } 237 | } 238 | } 239 | 240 | // Return result... 241 | if (f != NULL && autocomplete != NULL) { // ...of auto-completion 242 | clear_line(); 243 | char *last_comma = strrchr(buffer, ','); 244 | if (last_comma != NULL) { 245 | strncpy(last_comma + 1, autocomplete, max_size - (last_comma - buffer)); 246 | strncpy(string, buffer, max_size); 247 | } else 248 | strncpy(string, autocomplete, max_size); 249 | } else { 250 | strncpy(string, buffer, max_size); // ..of regular input 251 | } 252 | 253 | printf("\n"); 254 | } 255 | 256 | -------------------------------------------------------------------------------- /pkgrename.c/src/releaselists.c: -------------------------------------------------------------------------------- 1 | #ifndef _WIN32 2 | #define _GNU_SOURCE // For strcasecmp(). 3 | #endif 4 | 5 | #include "../include/colors.h" 6 | #include "../include/common.h" 7 | #include "../include/options.h" 8 | #include "../include/releaselists.h" 9 | #include "../include/strings.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | struct rls_list { 17 | char *name; 18 | char *alt_name; 19 | }; 20 | 21 | // List of release groups; [0]: Name, [1]: Additional search string 22 | // (Additional search strings commonly appear in original file names.) 23 | #pragma GCC diagnostic push 24 | #pragma GCC diagnostic ignored "-Wmissing-field-initializers" 25 | static struct rls_list release_groups[] = { 26 | {"AUGETY"}, 27 | {"BigBlueBox"}, 28 | {"BlaZe", "blz"}, 29 | {"CAF"}, 30 | {"DarKmooN"}, 31 | {"DUPLEX"}, 32 | {"GCMR"}, 33 | {"HOODLUM"}, 34 | {"HR"}, 35 | {"iNTERNAL"}, 36 | {"JRP"}, 37 | {"KOTF"}, 38 | {"LeveLUp"}, 39 | {"LiGHTFORCE", "lfc"}, 40 | {"MarvTM"}, 41 | {"MOEMOE", "moe-"}, 42 | {"PiKMiN"}, 43 | {"Playable"}, 44 | {"PRELUDE"}, 45 | {"PROTOCOL"}, 46 | {"RESPAWN"}, 47 | {"SharpHD"}, 48 | {"TCD"}, 49 | {"UNLiMiTED"}, 50 | {"WaLMaRT"}, 51 | {"WaYsTeD"}, 52 | {NULL} 53 | }; 54 | 55 | // List of releases 56 | static struct rls_list releases[] = { 57 | {"Arczi"}, 58 | {"CyB1K", "rayku22"}, 59 | {"Fugazi", "mrboot"}, 60 | {"High Speed", "highspeed33"}, 61 | {"OPOISSO893", "opoisso"}, 62 | {"SeanP2500", "seanp"}, 63 | {"TKJ13"}, 64 | {"TRIFECTA"}, 65 | {"VikaCaptive"}, 66 | {"Whitehawkx"}, 67 | {"xmrallx"}, 68 | {NULL} 69 | }; 70 | #pragma GCC diagnostic pop 71 | 72 | #ifdef _WIN32 73 | // MinGW is missing strcasestr(). 74 | static char *strcasestr(const char *haystack, const char *needle) 75 | { 76 | if (*haystack == '\0' || *needle == '\0') 77 | return NULL; 78 | 79 | size_t haystack_len = strlen(haystack); 80 | size_t needle_len = strlen(needle); 81 | if (haystack_len < needle_len) 82 | return NULL; 83 | size_t diff_len = haystack_len - needle_len; 84 | 85 | do { 86 | if (tolower(*haystack) == tolower(*needle)) { 87 | const char *a = haystack; 88 | const char *b = needle; 89 | do { 90 | ++a; 91 | ++b; 92 | if (*b == '\0') 93 | return (char *) haystack; 94 | } while (tolower(*a) == tolower(*b)); 95 | } 96 | ++haystack; 97 | } while (diff_len-- > 0); 98 | 99 | return NULL; 100 | } 101 | #endif 102 | 103 | // Detects release group in a string and returns a pointer containing the group. 104 | char *get_release_group(char *string) 105 | { 106 | struct rls_list *p = release_groups; 107 | 108 | while (p->name != NULL) { 109 | if (strwrd(string, p->name)) 110 | return p->name; 111 | if (p->alt_name != NULL && strstr(string, p->alt_name)) 112 | return p->name; 113 | p++; 114 | } 115 | 116 | return NULL; 117 | } 118 | 119 | static int compar_func(const void *s1, const void *s2) 120 | { 121 | return strcasecmp(*(char **) s1, *(char **) s2); 122 | } 123 | 124 | // Companion function for get_release(). 125 | // Returns 1 if a tag matches one of the user-provided tags. 126 | static int matches_user_tag(const char *tag) 127 | { 128 | for (int i = 0; i < tagc; i++) { 129 | if (strcasecmp(tags[i], tag) == 0) 130 | return 1; 131 | } 132 | 133 | return 0; 134 | } 135 | 136 | // Detects one or multiple releases in a string and stores a pointer to the 137 | // resulting comma-separated string in . Returns the number of found 138 | // unique matches. 139 | int get_release(char **release, const char *string) 140 | { 141 | static char *found[MAX_TAGS + 1]; 142 | int n_found = 0; 143 | static char *retval; 144 | 145 | // Check user-specified tags first, so they can override built-in tags. 146 | for (int i = 0; i < tagc; i++) 147 | if (strwrd(string, tags[i])) 148 | found[n_found++] = tags[i]; 149 | 150 | // Check built-in tags. 151 | struct rls_list *p = releases; 152 | while (p->name != NULL) { 153 | if (!matches_user_tag(p->name) && (strwrd(string, p->name) 154 | || (p->alt_name != NULL && strwrd(string, p->alt_name)) 155 | || (strcmp(p->name, "Fugazi") == 0 && strcasestr(string, "fxd")))) 156 | found[n_found++] = p->name; 157 | p++; 158 | } 159 | 160 | if (n_found == 1) { 161 | *release = found[0]; 162 | } else if (n_found > 1) { 163 | // Return multiple releases as a comma-separated string. 164 | qsort(found, n_found, sizeof(char *), compar_func); 165 | if (retval == NULL) 166 | retval = calloc(MAX_TAG_LEN, 1); 167 | strncpy(retval, found[0], MAX_TAG_LEN); 168 | for (int i = 1; i < n_found; i++) { 169 | strncat(retval, tag_separator, MAX_TAG_LEN - strlen(retval)); 170 | strncat(retval, found[i], MAX_TAG_LEN - strlen(retval)); 171 | } 172 | *release = retval; 173 | } 174 | 175 | return n_found; 176 | } 177 | 178 | // Returns 1 if str2 fully matches the beginning of str1 (case-insensitive). 179 | static int strings_match(char *str1, char *str2) 180 | { 181 | if (strlen(str2) == 0) 182 | return 0; 183 | if (strlen(str2) > strlen(str1)) 184 | return 0; 185 | 186 | for (size_t i = 0; i < strlen(str2); i++) { 187 | if (tolower(str2[i]) != tolower(str1[i])) 188 | return 0; 189 | } 190 | 191 | return 1; 192 | } 193 | 194 | // Used as autocomplete function for scan_string() (in terminal.c). 195 | // Returns the name of a tag if it is found in "string". 196 | char *get_tag(char *string) 197 | { 198 | struct rls_list *p; 199 | 200 | // Ignore already entered tags that are separated by commas. 201 | char *last_comma = strrchr(string, ','); 202 | if (last_comma != NULL) { 203 | string = last_comma + 1; 204 | while (*string == ' ') 205 | string++; 206 | } 207 | 208 | p = release_groups; 209 | while (p->name != NULL) { 210 | if (strings_match(p->name, string)) 211 | return p->name; 212 | p++; 213 | } 214 | 215 | // Check user-specified tags first, so they can override built-in tags. 216 | for (int i = 0; i < tagc; i++) { 217 | if (strings_match(tags[i], string)) 218 | return tags[i]; 219 | } 220 | 221 | // Check built-in tags. 222 | p = releases; 223 | while (p->name != NULL) { 224 | if (strings_match(p->name, string)) 225 | return p->name; 226 | p++; 227 | } 228 | 229 | if (strings_match("Backport", string)) 230 | return "Backport"; 231 | 232 | return NULL; 233 | } 234 | 235 | // Replaces all commas in a release tag with a custom string. 236 | // The buffer is stored in must be of length MAX_TAG_LEN. 237 | void replace_commas_in_tag(char *tag, const char *string) 238 | { 239 | size_t stringlen = strlen(string); 240 | 241 | for (size_t i = 0; i < strlen(tag); i++) { 242 | if (tag[i] == ',') { 243 | // Only do it if there's enough space left in the tag buffer. 244 | if (strlen(tag) + stringlen - 1 >= MAX_TAG_LEN) 245 | return; 246 | 247 | memmove(tag + i + stringlen, tag + i + 1, strlen(tag + i)); 248 | memcpy(tag + i, string, stringlen); 249 | i += stringlen; 250 | } 251 | } 252 | } 253 | 254 | // Searches a changelog buffer for all known release tags and prints them. 255 | void print_changelog_tags(const char *changelog_buf) 256 | { 257 | int first_match = 0; 258 | 259 | // User-specified tags. 260 | for (int i = 0; i < tagc; i++) { 261 | if (strwrd(changelog_buf, tags[i])) { 262 | if (first_match == 0) { 263 | fputs("Release tags found:\n", stdout); 264 | first_match = 1; 265 | set_color(BRIGHT_YELLOW, stdout); 266 | } 267 | printf("%s (user tag)\n", tags[i]); 268 | } 269 | } 270 | 271 | // Built-in tags. 272 | struct rls_list *p = releases; 273 | while (p->name != NULL) { 274 | char *name; 275 | if (strcasestr(changelog_buf, p->name) 276 | || (p->alt_name && strcasestr(changelog_buf, p->alt_name))) 277 | name = p->name; 278 | else 279 | name = NULL; 280 | if (name) { 281 | if (first_match == 0) { 282 | fputs("Release tags found:\n", stdout); 283 | first_match = 1; 284 | set_color(BRIGHT_YELLOW, stdout); 285 | } 286 | printf("%s (built-in tag)\n", name); 287 | } 288 | p++; 289 | } 290 | 291 | if (first_match) 292 | set_color(RESET, stdout); 293 | } 294 | 295 | // Prints a list of all built-in release group tags and release tags. 296 | void print_database() 297 | { 298 | struct rls_list *p; 299 | 300 | p = release_groups; 301 | printf("Known release groups:\n"); 302 | while (p->name != NULL) { 303 | printf(" - %s\n", p->name); 304 | p++; 305 | } 306 | 307 | p = releases; 308 | printf("\nKnown releases:\n"); 309 | while (p->name != NULL) { 310 | printf(" - %s\n", p->name); 311 | p++; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /pkgrename.c/src/strings.c: -------------------------------------------------------------------------------- 1 | #include "../include/characters.h" 2 | #include "../include/common.h" 3 | #include "../include/strings.h" 4 | 5 | #ifdef _WIN32 6 | #include 7 | #define strcasestr StrStrIA 8 | #else 9 | #define _GNU_SOURCE // For strcasestr(), which is not standard 10 | #endif 11 | 12 | #include 13 | #include 14 | 15 | // Removes leading and/or trailing characters from a string. 16 | void trim_string(char *string, char *ltrim, char *rtrim) 17 | { 18 | size_t len = strlen(string); 19 | 20 | if (ltrim) { 21 | size_t spn = strspn(string, ltrim); 22 | if (spn) 23 | memmove(string, string + spn, len + 1 - spn); 24 | len -= spn; 25 | } 26 | 27 | if (rtrim) { 28 | size_t rtrimlen = strlen(rtrim); 29 | for (size_t i = len - 1; i > 0; i--) { 30 | for (size_t j = 0; j < rtrimlen; j++) 31 | if (rtrim[j] == string[i]) { 32 | string[i] = '\0'; 33 | goto next; 34 | } 35 | break; 36 | 37 | next: 38 | continue; 39 | } 40 | } 41 | } 42 | 43 | // Case-insensitively tests if a string contains a word 44 | inline char *strwrd(const char *string, char *word) 45 | { 46 | char *p = strcasestr(string, word); 47 | if (p == NULL) 48 | return NULL; 49 | 50 | size_t word_len = strlen(word); 51 | size_t string_len = strlen(string); 52 | 53 | do { 54 | if ((p != string && isalnum(p[-1])) 55 | || (p != string + (string_len - word_len) && isalnum(p[word_len]))) { 56 | continue; 57 | } 58 | return p; 59 | } while ((p = strcasestr(++p, word))); 60 | 61 | return NULL; 62 | } 63 | 64 | // Case-insensitively replaces all occurences of a word in a character array 65 | // "string" must be of length MAX_FILENAME_LEN 66 | static void replace_word(char *string, char *word, char *replace) 67 | { 68 | char *p = string; 69 | size_t word_len = strlen(word); 70 | size_t replace_len = strlen(replace); 71 | 72 | while ((p = strwrd(p, word))) { 73 | // Abort if new string length would be too large 74 | if (strlen(string) - word_len + replace_len > MAX_FILENAME_LEN) return; 75 | 76 | memmove(p + replace_len, p + word_len, strlen(p + word_len) + 1); 77 | memcpy(p, replace, replace_len); 78 | 79 | p++; 80 | } 81 | } 82 | 83 | // Removes unused curly braces expressions; returns 0 on success 84 | static int curlycrunch(char *string, int position) 85 | { 86 | char temp[MAX_FILENAME_LEN] = ""; 87 | 88 | // Search left 89 | for (int i = position; i >= 0; i--) { 90 | if (string[i] == '{') { 91 | strncpy(temp, string, i); 92 | //printf("%s left : %s\n", __func__, temp); // DEBUG 93 | break; 94 | } 95 | if (string[i] == '}') { 96 | return 1; 97 | } 98 | } 99 | 100 | // Search right 101 | for (size_t i = position; i < strlen(string); i++) { 102 | if (string[i] == '}') { 103 | strcat(temp, &string[i + 1]); 104 | //printf("%s right: %s\n", __func__, temp); // DEBUG 105 | break; 106 | } 107 | if (string[i] == '{' || i == strlen(string) - 1) { 108 | return 1; 109 | } 110 | } 111 | 112 | strcpy(string, temp); 113 | return 0; 114 | } 115 | 116 | // Replaces all occurences of "search" in "string" (an array of char), e.g.: 117 | // strreplace(temp, "%title%", title) 118 | // "string" must be of length MAX_FILENAME_LEN 119 | // "search" and "replace" must not be equal 120 | inline char *strreplace(char *string, char *search, char *replace) 121 | { 122 | char *p; 123 | 124 | while ((p = strstr(string, search))) { 125 | char temp[MAX_FILENAME_LEN] = ""; 126 | int position; // Position of first character of "search" in "format_string" 127 | if (replace == NULL) replace = ""; 128 | position = p - string; 129 | 130 | //printf("Search string: %s\n", search); // DEBUG 131 | //printf("Replace string: %s\n", replace); // DEBUG 132 | 133 | // If replace string is an empty pattern variable, remove associated 134 | // curly braces expression 135 | if (search[0] == '%' && strcmp(replace, "") == 0 && 136 | curlycrunch(string, position) == 0) return NULL; 137 | 138 | strncpy(temp, string, position); 139 | //printf("Current string (step 1): \"%s\"\n", temp); // DEBUG 140 | strcat(temp, replace); 141 | //printf("Current string (step 2): \"%s\"\n", temp); // DEBUG 142 | strcat(temp, string + position + strlen(search)); 143 | //printf("Current string (step 3): \"%s\"\n\n", temp); //DEBUG 144 | strcpy(string, temp); 145 | } 146 | 147 | return p; 148 | } 149 | 150 | // Converts a string (title, to be specific) to mixed-case style 151 | void mixed_case(char *title) 152 | { 153 | int len = strlen(title); 154 | 155 | // Apply mixed-case style 156 | title[0] = toupper(title[0]); 157 | for (int i = 1; i < len; i++) { 158 | if (isspace(title[i - 1]) || is_in_set(title[i - 1], ":-;~_1234567890")) { 159 | title[i] = toupper(title[i]); 160 | } else { 161 | title[i] = tolower(title[i]); 162 | } 163 | } 164 | 165 | // Make sure certain words are spelled correctly 166 | char *special_words[] = { 167 | // Roman numerals 168 | "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", 169 | "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XVIX", "XX", 170 | // Games 171 | "20XX", 172 | "2Dark", 173 | "2K", 174 | "2K14", 175 | "2K15", 176 | "2K16", 177 | "2K17", 178 | "2K18", 179 | "2K19", 180 | "2K20", 181 | "2K21", 182 | "2K22", 183 | "2X", 184 | "3D", 185 | "4K", 186 | "ABC", 187 | "ACA", 188 | "ADR1FT", 189 | "AER", 190 | "AI", 191 | "AO", 192 | "ARK", 193 | "ATV", 194 | "AVICII", 195 | "AdVenture", 196 | "AereA", 197 | "AeternoBlade", 198 | "AnywhereVR", 199 | "ArmaGallant", 200 | "Avenger iX", 201 | "BMX", 202 | "BaZooka", 203 | "BioHazard", 204 | "BioShock", 205 | "BlazBlue", 206 | "BlazeRush", 207 | "BloodRayne", 208 | "BoxVR", 209 | "CastleStorm", 210 | "ChromaGun", 211 | "CrossCode", 212 | "CruisinMix", 213 | "DC", 214 | "DCL", 215 | "DEX", 216 | "DG2", 217 | "DX", 218 | "DJMax", 219 | "DLC", 220 | "DS", 221 | "DUB", 222 | "DWVR", 223 | "DarkWatch", 224 | "DayZ", 225 | "DmC", 226 | "DreamMix", 227 | "DreamWorks", 228 | "EA", 229 | "EBKore", 230 | "ECHO", 231 | "EFootball", 232 | "EP", 233 | "ESP", 234 | "ESPN", 235 | "EVE", 236 | "EX", 237 | "EXA", 238 | "EarthNight", 239 | "FEZ", 240 | "FIA", 241 | "FIFA", 242 | "F.I.S.T.", 243 | "FX2", 244 | "FX3", 245 | "FantaVision", 246 | "Fate/Extella", 247 | "FightN", 248 | "FighterZ", 249 | "FlOw", 250 | "FlatOut", 251 | "GI", 252 | "GODS", 253 | "GP", 254 | "Gris", 255 | "GU", 256 | "GoldenEye", 257 | "GreedFall", 258 | "HD", 259 | "HOA", 260 | "HiQ", 261 | "Hitman GO", 262 | "ICO", 263 | "IF", 264 | "InFamous", 265 | "IxSHE", 266 | "JJ", 267 | "JoJos", 268 | "JoyRide", 269 | "JumpJet", 270 | "KO", 271 | "KOI", 272 | "KeyWe", 273 | "KickBeat", 274 | "LA Cops", 275 | "LittleBigPlanet", 276 | "LocoRoco", 277 | "LoveR Kiss", 278 | "MLB", 279 | "MS", 280 | "MV", 281 | "MX", 282 | "MXGP", 283 | "MalFunction", 284 | "MasterCube", 285 | "McIlroy", 286 | "McMorris", 287 | "MechWarrior", 288 | "MediEvil", 289 | "MegaDrive", 290 | "MotoGP", 291 | "MudRunner", 292 | "NASCAR", 293 | "NBA", 294 | "N.E.R.O.", 295 | "NESTS", 296 | "NFL", 297 | "NG", 298 | "NHL", 299 | "NT", 300 | "NY", 301 | "NecroDancer", 302 | "NeoGeo", 303 | "NeoWave", 304 | "NeuroVoider", 305 | "NieR", 306 | "OG", 307 | "OK", 308 | "OMG", 309 | "OhShape", 310 | "OlliOlli", 311 | "OutRun", 312 | "OwlBoy", 313 | "PAW", 314 | "PES", 315 | "PGA", 316 | "PS2", 317 | "PS4", 318 | "PSN", 319 | "PaRappa", 320 | "PixARK", 321 | "PixelJunk", 322 | "PlayStation", 323 | "Project CARS", 324 | "ProStreet", 325 | "QuiVr", 326 | "RBI", 327 | "REV", 328 | "RICO", 329 | "RIGS", 330 | "RiME", 331 | "RPG", 332 | "RemiLore", 333 | "RiMS", 334 | "RollerCoaster", 335 | "Romancing SaGa", 336 | "RyoRaiRai", 337 | "SD", 338 | "SG/ZH", 339 | "SH1FT3R", 340 | "SNES", 341 | "SNK", 342 | "SSX", 343 | "SVC", 344 | "SaGa Frontier", 345 | "SaGa Scarlet", 346 | "SkullGirls", 347 | "SkyScrappers", 348 | "SmackDown", 349 | "SnowRunner", 350 | "SoulCalibur", 351 | "SpeedRunners", 352 | "SpinMaster", 353 | "SquarePants", 354 | "SteamWorld", 355 | "SuperChargers", 356 | "SuperEpic", 357 | "TMNT", 358 | "Tron RUN/r", 359 | "TT", 360 | "TV", 361 | "ToeJam", 362 | "TowerFall", 363 | "TrackMania", 364 | "TrainerVR", 365 | "UEFA", 366 | "UFC", 367 | "UN", 368 | "UNO", 369 | "UglyDolls", 370 | "UnMetal", 371 | "VA", 372 | "VFR", 373 | "VIIR", 374 | "VR", 375 | "VRobot", 376 | "VRog", 377 | "VirZOOM", 378 | "WMD", 379 | "WRC", 380 | "WWE", 381 | "WWII", 382 | "WindJammers", 383 | "XCOM", 384 | "XD", 385 | "XL", 386 | "XXL", 387 | "YU-NO", 388 | "YoRHa", 389 | "ZX", 390 | "eSports", 391 | "eX+", 392 | "pFBA", 393 | "pNES", 394 | "pSNES", 395 | "reQuest", 396 | "tRrLM();", 397 | "theHunter", 398 | "vs", 399 | NULL 400 | }; 401 | for (int i = 0; special_words[i]; i++) { 402 | replace_word(title, special_words[i], special_words[i]); 403 | } 404 | } 405 | 406 | int lower_strcmp(char *string1, char *string2) 407 | { 408 | if (strlen(string1) == strlen(string2)) { 409 | for (size_t i = 0; i < strlen(string1); i++) { 410 | if (tolower(string1[i]) != tolower(string2[i])) { 411 | return 1; 412 | } 413 | } 414 | } else { 415 | return 1; 416 | } 417 | 418 | return 0; 419 | } 420 | -------------------------------------------------------------------------------- /pkgrename.c/src/scan.c: -------------------------------------------------------------------------------- 1 | #include "../include/colors.h" 2 | #include "../include/common.h" 3 | #include "../include/scan.h" 4 | #include "../include/options.h" 5 | #include "../include/pkg.h" 6 | 7 | #ifdef _WIN32 8 | #include 9 | #endif 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define SCAN_LIST_MIN_SIZE 1024 17 | 18 | #ifdef DEBUG 19 | // Debug function. 20 | void print_scan(struct scan *scan) 21 | { 22 | fprintf(stderr, "node pointer: %p\n", (void *) scan ); 23 | if (scan == NULL) 24 | return; 25 | fprintf(stderr, "filename: %s\n", scan->filename); 26 | fprintf(stderr, "filename_allocated: %d\n", scan->filename_allocated); 27 | fprintf(stderr, "prev: %p\n", (void *) scan->prev); 28 | fprintf(stderr, "next: %p\n", (void *) scan->next); 29 | } 30 | #endif 31 | 32 | // Companion function for initialize_scan_job(). 33 | // Returns 0 on success and -1 on error. 34 | static inline int initialize_scan_list(struct scan_list *list) 35 | { 36 | list->tail = calloc(1, SCAN_LIST_CHUNK_SIZE * sizeof(struct scan)); 37 | if (list->tail == NULL) 38 | return -1; 39 | 40 | list->head = NULL; 41 | list->finished = 0; 42 | list->n_slots = SCAN_LIST_CHUNK_SIZE; 43 | 44 | return 0; 45 | } 46 | 47 | // Initializes a scan job. 48 | // Returns 0 on success and -1 on error. 49 | int initialize_scan_job(struct scan_job *job, char **filenames, int n_filenames) 50 | { 51 | if (initialize_scan_list(&job->scan_list)) 52 | return -1; 53 | if (pthread_mutex_init(&job->mutex, NULL)) 54 | return -1; 55 | if (pthread_cond_init(&job->cond, NULL)) { 56 | pthread_mutex_destroy(&job->mutex); 57 | return -1; 58 | } 59 | job->filenames = filenames; 60 | job->n_filenames = n_filenames; 61 | 62 | return 0; 63 | } 64 | 65 | // Companion function for destroy_scan_list(); must not be used elsewhere. 66 | // Deletes a node and returns a pointer to the next node. 67 | static inline struct scan *delete_scan(struct scan *scan) 68 | { 69 | if (scan->filename_allocated && scan->filename) 70 | free(scan->filename); 71 | if (scan->param_sfo) 72 | free(scan->param_sfo); 73 | if (scan->changelog) 74 | free(scan->changelog); 75 | 76 | if (scan->next) 77 | return scan->next; 78 | else 79 | return NULL; 80 | } 81 | 82 | // Companion function for destroy_scan_job() that frees a scan list's allocated 83 | // memory. The list must not be in use by other threads anymore. 84 | static inline void destroy_scan_list(struct scan_list *scan_list) 85 | { 86 | struct scan *scan = scan_list->head; 87 | if (scan == NULL) 88 | return; 89 | 90 | size_t n_nodes = 0; 91 | do { 92 | scan = delete_scan(scan); 93 | 94 | n_nodes++; 95 | if (n_nodes == SCAN_LIST_CHUNK_SIZE) { 96 | free(scan_list->head); 97 | scan_list->head = scan; 98 | n_nodes = 0; 99 | } 100 | } while (scan); 101 | } 102 | 103 | // Destroys a scan job. 104 | void destroy_scan_job(struct scan_job *job) 105 | { 106 | destroy_scan_list(&job->scan_list); 107 | pthread_mutex_destroy(&job->mutex); 108 | pthread_cond_destroy(&job->cond); 109 | } 110 | 111 | // Adds a scan result to a job's scan list. 112 | void add_scan_result(struct scan_job *job, char *filename, 113 | _Bool filename_allocated) 114 | { 115 | pthread_mutex_lock(&job->mutex); 116 | 117 | struct scan_list *list = &job->scan_list; 118 | struct scan *scan; 119 | 120 | // Assemble new node, without linking it yet. 121 | if (list->head == NULL) { 122 | scan = list->tail; 123 | } else { 124 | if (list->n_slots > 0) { // Chunk slots left? Use next slot. 125 | scan = list->tail + 1; 126 | } else { // Use a new chunk. 127 | scan = malloc(SCAN_LIST_CHUNK_SIZE * sizeof(struct scan)); 128 | if (scan == NULL) 129 | exit(EXIT_FAILURE); // TODO: better error handling. 130 | list->n_slots = SCAN_LIST_CHUNK_SIZE; 131 | } 132 | } 133 | list->n_slots--; 134 | 135 | pthread_mutex_unlock(&job->mutex); 136 | 137 | scan->filename = filename; 138 | scan->filename_allocated = filename_allocated; 139 | scan->param_sfo = NULL; 140 | scan->changelog = NULL; 141 | scan->error = load_pkg_data(&scan->param_sfo, &scan->changelog, 142 | &scan->fake_status, filename); 143 | scan->next = NULL; 144 | 145 | // Link new node. 146 | pthread_mutex_lock(&job->mutex); 147 | if (list->head == NULL) { 148 | list->tail = scan; 149 | list->head = scan; 150 | } else { 151 | list->tail->next = scan; 152 | scan->prev = list->tail; 153 | list->tail = scan; 154 | } 155 | 156 | pthread_mutex_unlock(&job->mutex); 157 | 158 | pthread_cond_signal(&job->cond); 159 | } 160 | 161 | // Prints a message that describes the value of struct scan's .error member. 162 | void print_scan_error(struct scan *scan) 163 | { 164 | set_color(BRIGHT_RED, stderr); 165 | fprintf(stderr, "Error while scanning file \"%s\": ", scan->filename); 166 | switch (scan->error) { 167 | case SCAN_ERROR_NOT_A_PKG: 168 | fputs("File is not a PS4 PKG file.\n", stderr); 169 | break; 170 | case SCAN_ERROR_OPEN_FILE: 171 | fputs("Could not open file.\n", stderr); 172 | break; 173 | case SCAN_ERROR_READ_FILE: 174 | fputs("Could not read data.\n", stderr); 175 | break; 176 | case SCAN_ERROR_OUT_OF_MEMORY: 177 | fputs("Could not allocate memory for PKG content.\n", stderr); 178 | break; 179 | case SCAN_ERROR_PARAM_SFO_INVALID_DATA: 180 | fputs("Invalid data in PKG content \"param.sfo\".\n", stderr); 181 | break; 182 | case SCAN_ERROR_PARAM_SFO_INVALID_FORMAT: 183 | fputs("Invalid file type of PKG content \"param.sfo\".\n", stderr); 184 | break; 185 | case SCAN_ERROR_PARAM_SFO_INVALID_SIZE: 186 | fputs("Invalid size of PKG content \"param.sfo\".\n", stderr); 187 | break; 188 | case SCAN_ERROR_PARAM_SFO_NOT_FOUND: 189 | fputs("PKG content \"param.sfo\" not found.\n", stderr); 190 | break; 191 | case SCAN_ERROR_CHANGELOG_INVALID_SIZE: 192 | fputs("Invalid size of PKG content \"changelog.xml\".\n", stderr); 193 | break; 194 | default: 195 | fputs("Unkown error.\n", stderr); 196 | } 197 | set_color(RESET, stderr); 198 | } 199 | 200 | // Companion function for qsort in parse_directory(). 201 | static int qsort_compare_strings(const void *p, const void *q) 202 | { 203 | return strcmp(*(const char **)p, *(const char **)q); 204 | } 205 | 206 | inline static int is_root(const char *dir) 207 | { 208 | if (dir[0] == DIR_SEPARATOR && dir[1] == '\0') 209 | return 1; 210 | return 0; 211 | } 212 | 213 | // Finds all .pkg files in a directory and runs a scan on them. 214 | // Returns 0 on success and -1 on error. 215 | int parse_directory(char *cur_dir, struct scan_job *job) 216 | { 217 | int retval = 0; 218 | 219 | DIR *dir; 220 | struct dirent *dir_entry; 221 | 222 | size_t dir_count_max = 100; 223 | size_t file_count_max = 100; 224 | char **dir_names = malloc(sizeof(void *) * dir_count_max); 225 | char **filenames = malloc(sizeof(void *) * file_count_max); 226 | size_t dir_count = 0, file_count = 0; 227 | 228 | // Remove trailing directory separators. 229 | { 230 | int len = strlen(cur_dir); 231 | while (len > 1 && cur_dir[len - 1] == DIR_SEPARATOR) { 232 | cur_dir[len - 1] = '\0'; 233 | len--; 234 | } 235 | } 236 | 237 | dir = opendir(cur_dir); // TODO: better error handling. 238 | if (dir == NULL) { 239 | retval = -1; 240 | goto cleanup; 241 | } 242 | 243 | // Read all directory entries to put them in lists. 244 | while ((dir_entry = readdir(dir)) != NULL) { 245 | // Entry is a directory. 246 | #ifdef _WIN32 // MinGW does not know .d_type. 247 | struct stat statbuf; 248 | char path[PATH_MAX]; 249 | snprintf(path, sizeof(path), "%s%c%s", is_root(cur_dir) ? "" : cur_dir, 250 | DIR_SEPARATOR, dir_entry->d_name); 251 | if (stat(path, &statbuf) == -1) { 252 | set_color(BRIGHT_RED, stderr); 253 | fprintf(stderr, "Could not read file system information: \"%s\".\n", 254 | path); 255 | set_color(RESET, stderr); 256 | continue; 257 | } 258 | if (S_ISDIR(statbuf.st_mode)) { 259 | #else 260 | if (dir_entry->d_type == DT_DIR) { 261 | #endif 262 | // Save name in the directory list. 263 | if (option_recursive == 1 264 | && dir_entry->d_name[0] != '.' 265 | && dir_entry->d_name[0] != '$') // Exclude system dirs. 266 | { 267 | size_t size; 268 | if (is_root(cur_dir)) 269 | size = 1 + strlen(dir_entry->d_name) + 1; 270 | else 271 | size = strlen(cur_dir) + 1 + strlen(dir_entry->d_name) + 1; 272 | if ((dir_names[dir_count] = malloc(size)) == NULL) { 273 | retval = -1; 274 | goto cleanup; 275 | } 276 | 277 | sprintf(dir_names[dir_count], "%s%c%s", is_root(cur_dir) ? "" 278 | : cur_dir, DIR_SEPARATOR, dir_entry->d_name); 279 | dir_count++; 280 | if (dir_count == dir_count_max) { 281 | dir_count_max *= 2; 282 | dir_names = realloc(dir_names, sizeof(void *) 283 | * dir_count_max); 284 | } 285 | } 286 | // Entry is .pkg file. 287 | } else { 288 | char *file_extension = strrchr(dir_entry->d_name, '.'); 289 | if (file_extension != NULL 290 | && strcasecmp(file_extension, ".pkg") == 0) 291 | { 292 | // Save name in the file list. 293 | size_t size; 294 | if (is_root(cur_dir)) 295 | size = 1 + strlen(dir_entry->d_name) + 1; 296 | else 297 | size = strlen(cur_dir) + 1 + strlen(dir_entry->d_name) + 1; 298 | if ((filenames[file_count] = malloc(size)) == NULL) { 299 | retval = -1; 300 | goto cleanup; 301 | } 302 | 303 | sprintf(filenames[file_count], "%s%c%s", is_root(cur_dir) ? "" 304 | : cur_dir, DIR_SEPARATOR, dir_entry->d_name); 305 | file_count++; 306 | if (file_count == file_count_max) { 307 | file_count_max *= 2; 308 | filenames = realloc(filenames, sizeof(void *) 309 | * file_count_max); 310 | } 311 | } 312 | } 313 | } 314 | 315 | // Sort the final lists. 316 | qsort(dir_names, dir_count, sizeof(char *), 317 | qsort_compare_strings); 318 | qsort(filenames, file_count, sizeof(char *), qsort_compare_strings); 319 | 320 | // Use the filenames to create new scan results. 321 | for (size_t i = 0; i < file_count; i++) 322 | add_scan_result(job, filenames[i], 1); 323 | 324 | // Parse sorted directories recursively. 325 | if (option_recursive == 1) { 326 | for (size_t i = 0; i < dir_count; i++) { 327 | parse_directory(dir_names[i], job); 328 | free(dir_names[i]); 329 | } 330 | } 331 | 332 | closedir(dir); 333 | 334 | cleanup: 335 | free(dir_names); 336 | free(filenames); 337 | 338 | return retval; 339 | } 340 | -------------------------------------------------------------------------------- /pkgrename.c/src/sha256.c: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // WjCryptLib_Sha256 3 | // 4 | // Implementation of SHA256 hash function. 5 | // Original author: Tom St Denis, tomstdenis@gmail.com, http://libtom.org 6 | // Modified by WaterJuice retaining Public Domain license. 7 | // 8 | // This is free and unencumbered software released into the public domain - June 2013 waterjuice.org 9 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 10 | 11 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 12 | // IMPORTS 13 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 14 | 15 | #include "../include/sha256.h" 16 | #include 17 | 18 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 19 | // MACROS 20 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | #define ror(value, bits) (((value) >> (bits)) | ((value) << (32 - (bits)))) 23 | 24 | #define MIN(x, y) ( ((x)<(y))?(x):(y) ) 25 | 26 | #define STORE32H(x, y) \ 27 | { (y)[0] = (uint8_t)(((x)>>24)&255); (y)[1] = (uint8_t)(((x)>>16)&255); \ 28 | (y)[2] = (uint8_t)(((x)>>8)&255); (y)[3] = (uint8_t)((x)&255); } 29 | 30 | #define LOAD32H(x, y) \ 31 | { x = ((uint32_t)((y)[0] & 255)<<24) | \ 32 | ((uint32_t)((y)[1] & 255)<<16) | \ 33 | ((uint32_t)((y)[2] & 255)<<8) | \ 34 | ((uint32_t)((y)[3] & 255)); } 35 | 36 | #define STORE64H(x, y) \ 37 | { (y)[0] = (uint8_t)(((x)>>56)&255); (y)[1] = (uint8_t)(((x)>>48)&255); \ 38 | (y)[2] = (uint8_t)(((x)>>40)&255); (y)[3] = (uint8_t)(((x)>>32)&255); \ 39 | (y)[4] = (uint8_t)(((x)>>24)&255); (y)[5] = (uint8_t)(((x)>>16)&255); \ 40 | (y)[6] = (uint8_t)(((x)>>8)&255); (y)[7] = (uint8_t)((x)&255); } 41 | 42 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 43 | // CONSTANTS 44 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 45 | 46 | // The K array 47 | static const uint32_t K[64] = { 48 | 0x428a2f98UL, 0x71374491UL, 0xb5c0fbcfUL, 0xe9b5dba5UL, 0x3956c25bUL, 49 | 0x59f111f1UL, 0x923f82a4UL, 0xab1c5ed5UL, 0xd807aa98UL, 0x12835b01UL, 50 | 0x243185beUL, 0x550c7dc3UL, 0x72be5d74UL, 0x80deb1feUL, 0x9bdc06a7UL, 51 | 0xc19bf174UL, 0xe49b69c1UL, 0xefbe4786UL, 0x0fc19dc6UL, 0x240ca1ccUL, 52 | 0x2de92c6fUL, 0x4a7484aaUL, 0x5cb0a9dcUL, 0x76f988daUL, 0x983e5152UL, 53 | 0xa831c66dUL, 0xb00327c8UL, 0xbf597fc7UL, 0xc6e00bf3UL, 0xd5a79147UL, 54 | 0x06ca6351UL, 0x14292967UL, 0x27b70a85UL, 0x2e1b2138UL, 0x4d2c6dfcUL, 55 | 0x53380d13UL, 0x650a7354UL, 0x766a0abbUL, 0x81c2c92eUL, 0x92722c85UL, 56 | 0xa2bfe8a1UL, 0xa81a664bUL, 0xc24b8b70UL, 0xc76c51a3UL, 0xd192e819UL, 57 | 0xd6990624UL, 0xf40e3585UL, 0x106aa070UL, 0x19a4c116UL, 0x1e376c08UL, 58 | 0x2748774cUL, 0x34b0bcb5UL, 0x391c0cb3UL, 0x4ed8aa4aUL, 0x5b9cca4fUL, 59 | 0x682e6ff3UL, 0x748f82eeUL, 0x78a5636fUL, 0x84c87814UL, 0x8cc70208UL, 60 | 0x90befffaUL, 0xa4506cebUL, 0xbef9a3f7UL, 0xc67178f2UL 61 | }; 62 | 63 | #define BLOCK_SIZE 64 64 | 65 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 66 | // INTERNAL FUNCTIONS 67 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 68 | 69 | // Various logical functions 70 | #define Ch( x, y, z ) (z ^ (x & (y ^ z))) 71 | #define Maj( x, y, z ) (((x | y) & z) | (x & y)) 72 | #define S( x, n ) ror((x),(n)) 73 | #define R( x, n ) (((x)&0xFFFFFFFFUL)>>(n)) 74 | #define Sigma0( x ) (S(x, 2) ^ S(x, 13) ^ S(x, 22)) 75 | #define Sigma1( x ) (S(x, 6) ^ S(x, 11) ^ S(x, 25)) 76 | #define Gamma0( x ) (S(x, 7) ^ S(x, 18) ^ R(x, 3)) 77 | #define Gamma1( x ) (S(x, 17) ^ S(x, 19) ^ R(x, 10)) 78 | 79 | #define Sha256Round( a, b, c, d, e, f, g, h, i ) \ 80 | t0 = h + Sigma1(e) + Ch(e, f, g) + K[i] + W[i]; \ 81 | t1 = Sigma0(a) + Maj(a, b, c); \ 82 | d += t0; \ 83 | h = t0 + t1; 84 | 85 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 86 | // TransformFunction 87 | // 88 | // Compress 512-bits 89 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 90 | static 91 | void 92 | TransformFunction 93 | ( 94 | Sha256Context* Context, 95 | uint8_t const* Buffer 96 | ) 97 | { 98 | uint32_t S[8]; 99 | uint32_t W[64]; 100 | uint32_t t0; 101 | uint32_t t1; 102 | uint32_t t; 103 | int i; 104 | 105 | // Copy state into S 106 | for( i=0; i<8; i++ ) 107 | { 108 | S[i] = Context->state[i]; 109 | } 110 | 111 | // Copy the state into 512-bits into W[0..15] 112 | for( i=0; i<16; i++ ) 113 | { 114 | LOAD32H( W[i], Buffer + (4*i) ); 115 | } 116 | 117 | // Fill W[16..63] 118 | for( i=16; i<64; i++ ) 119 | { 120 | W[i] = Gamma1( W[i-2]) + W[i-7] + Gamma0( W[i-15] ) + W[i-16]; 121 | } 122 | 123 | // Compress 124 | for( i=0; i<64; i++ ) 125 | { 126 | Sha256Round( S[0], S[1], S[2], S[3], S[4], S[5], S[6], S[7], i ); 127 | t = S[7]; 128 | S[7] = S[6]; 129 | S[6] = S[5]; 130 | S[5] = S[4]; 131 | S[4] = S[3]; 132 | S[3] = S[2]; 133 | S[2] = S[1]; 134 | S[1] = S[0]; 135 | S[0] = t; 136 | } 137 | 138 | // Feedback 139 | for( i=0; i<8; i++ ) 140 | { 141 | Context->state[i] = Context->state[i] + S[i]; 142 | } 143 | } 144 | 145 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 146 | // PUBLIC FUNCTIONS 147 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 148 | 149 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 150 | // Sha256Initialise 151 | // 152 | // Initialises a SHA256 Context. Use this to initialise/reset a context. 153 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 154 | void 155 | Sha256Initialise 156 | ( 157 | Sha256Context* Context // [out] 158 | ) 159 | { 160 | Context->curlen = 0; 161 | Context->length = 0; 162 | Context->state[0] = 0x6A09E667UL; 163 | Context->state[1] = 0xBB67AE85UL; 164 | Context->state[2] = 0x3C6EF372UL; 165 | Context->state[3] = 0xA54FF53AUL; 166 | Context->state[4] = 0x510E527FUL; 167 | Context->state[5] = 0x9B05688CUL; 168 | Context->state[6] = 0x1F83D9ABUL; 169 | Context->state[7] = 0x5BE0CD19UL; 170 | } 171 | 172 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 173 | // Sha256Update 174 | // 175 | // Adds data to the SHA256 context. This will process the data and update the internal state of the context. Keep on 176 | // calling this function until all the data has been added. Then call Sha256Finalise to calculate the hash. 177 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 178 | void 179 | Sha256Update 180 | ( 181 | Sha256Context* Context, // [in out] 182 | void const* Buffer, // [in] 183 | uint32_t BufferSize // [in] 184 | ) 185 | { 186 | uint32_t n; 187 | 188 | if( Context->curlen > sizeof(Context->buf) ) 189 | { 190 | return; 191 | } 192 | 193 | while( BufferSize > 0 ) 194 | { 195 | if( Context->curlen == 0 && BufferSize >= BLOCK_SIZE ) 196 | { 197 | TransformFunction( Context, (uint8_t*)Buffer ); 198 | Context->length += BLOCK_SIZE * 8; 199 | Buffer = (uint8_t*)Buffer + BLOCK_SIZE; 200 | BufferSize -= BLOCK_SIZE; 201 | } 202 | else 203 | { 204 | n = MIN( BufferSize, (BLOCK_SIZE - Context->curlen) ); 205 | memcpy( Context->buf + Context->curlen, Buffer, (size_t)n ); 206 | Context->curlen += n; 207 | Buffer = (uint8_t*)Buffer + n; 208 | BufferSize -= n; 209 | if( Context->curlen == BLOCK_SIZE ) 210 | { 211 | TransformFunction( Context, Context->buf ); 212 | Context->length += 8*BLOCK_SIZE; 213 | Context->curlen = 0; 214 | } 215 | } 216 | } 217 | } 218 | 219 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 220 | // Sha256Finalise 221 | // 222 | // Performs the final calculation of the hash and returns the digest (32 byte buffer containing 256bit hash). After 223 | // calling this, Sha256Initialised must be used to reuse the context. 224 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 225 | void 226 | Sha256Finalise 227 | ( 228 | Sha256Context* Context, // [in out] 229 | SHA256_HASH* Digest // [out] 230 | ) 231 | { 232 | int i; 233 | 234 | if( Context->curlen >= sizeof(Context->buf) ) 235 | { 236 | return; 237 | } 238 | 239 | // Increase the length of the message 240 | Context->length += Context->curlen * 8; 241 | 242 | // Append the '1' bit 243 | Context->buf[Context->curlen++] = (uint8_t)0x80; 244 | 245 | // if the length is currently above 56 bytes we append zeros 246 | // then compress. Then we can fall back to padding zeros and length 247 | // encoding like normal. 248 | if( Context->curlen > 56 ) 249 | { 250 | while( Context->curlen < 64 ) 251 | { 252 | Context->buf[Context->curlen++] = (uint8_t)0; 253 | } 254 | TransformFunction(Context, Context->buf); 255 | Context->curlen = 0; 256 | } 257 | 258 | // Pad up to 56 bytes of zeroes 259 | while( Context->curlen < 56 ) 260 | { 261 | Context->buf[Context->curlen++] = (uint8_t)0; 262 | } 263 | 264 | // Store length 265 | STORE64H( Context->length, Context->buf+56 ); 266 | TransformFunction( Context, Context->buf ); 267 | 268 | // Copy output 269 | for( i=0; i<8; i++ ) 270 | { 271 | STORE32H( Context->state[i], Digest->bytes+(4*i) ); 272 | } 273 | } 274 | 275 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 276 | // Sha256Calculate 277 | // 278 | // Combines Sha256Initialise, Sha256Update, and Sha256Finalise into one function. Calculates the SHA256 hash of the 279 | // buffer. 280 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 281 | void 282 | Sha256Calculate 283 | ( 284 | void const* Buffer, // [in] 285 | uint32_t BufferSize, // [in] 286 | SHA256_HASH* Digest // [in] 287 | ) 288 | { 289 | Sha256Context context; 290 | 291 | Sha256Initialise( &context ); 292 | Sha256Update( &context, Buffer, BufferSize ); 293 | Sha256Finalise( &context, Digest ); 294 | } 295 | -------------------------------------------------------------------------------- /pkgrename: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://github.com/hippie68/pkgrename 3 | # Renames PS4 PKG files based on param.sfo information and predefined patterns. 4 | # Requires script/program "sfo" (https://github.com/hippie68/sfo) in $PATH. 5 | 6 | # If you have renamed "sfo", enter the correct name here: 7 | sfo_script_name=sfo 8 | 9 | ######################### 10 | # Customization section: 11 | 12 | # How the filename should look like: 13 | pattern='$title [$category] [$title_id] [$releasegroup] [$release] [$backport].pkg' 14 | # Possible variables: 15 | # title, category, sdk, firmware, releasegroup, release, backport 16 | # Plus original SFO strings: app_ver, content_id, title_id, version 17 | 18 | # Replacement for characters that would be invalid on an exFAT file system: 19 | exfat_placeholder=_ 20 | 21 | # Do title customization here: 22 | # e.g. title=${title//SEARCH/REPLACE} 23 | customize_title() { 24 | # Replace various characters with less exotic alternatives 25 | title=${title//[®™]/} 26 | title=${title//–/-} 27 | title=${title//’/\'} 28 | title=${title//&/&} 29 | # Replace irregular whitespace with regular one: 30 | title=${title// / } 31 | # Uncomment these lines if you want placeholders for : and & to disappear: 32 | #title=${title//:/}; title=${title// / } 33 | #if [[ $title == *[a-z]* ]]; then 34 | # title=${title// & / and } 35 | #else 36 | # title=${title// & / AND } 37 | #fi 38 | } 39 | 40 | # Do all other file name customization here: 41 | customize() { 42 | # How it should look like when a backport is detected: 43 | backport_string="BACKPORT" 44 | # How different categories should look like: 45 | category_game="BASE GAME" 46 | category_patch="UPDATE ${app_ver#0}" 47 | category_dlc="DLC" 48 | # Optional: characters to remove should a variable be empty: 49 | outer_shell="[]" 50 | } 51 | 52 | ####################### 53 | # Script starts below: 54 | 55 | show_usage() { 56 | echo "Usage: ${0##*/} [-fhor] [file/directory ...]" >&2 57 | } 58 | 59 | show_help() { 60 | show_usage 2>&1 61 | echo " 62 | Automatically renames PKG files based on customizable patterns. 63 | Customization takes place inside the script file. 64 | If no files or directories are specified, the current directory will be used. 65 | 66 | Options: 67 | -f Force prompt when file name matches pattern 68 | -h Display this help info 69 | -o Default to online search 70 | -r Traverse directories recursively" 71 | exit 72 | } 73 | 74 | find_option="-maxdepth 1" 75 | 76 | # Parse command line options 77 | while [[ $1 == "-"?* ]]; do 78 | for ((i=1;i<${#1};i++)); do 79 | case ${1:$i:1} in 80 | f) force_prompt=true ;; 81 | h) show_help ;; 82 | o) search_ps_store=true ;; 83 | r) find_option= ;; 84 | *) show_usage; exit 1 ;; 85 | esac 86 | done 87 | shift 88 | done 89 | 90 | [[ $1 == "" ]] && set -- '.' # Use current directory if no arguments given 91 | 92 | cleanup() { 93 | echo "Script aborted." 94 | exit 95 | } 96 | 97 | trap '{ echo; cleanup; }' SIGINT 98 | 99 | # Prints a message and reads a keystroke until input is valid 100 | # $1: Message, $2: Allowed characters 101 | read_userinput() { 102 | local char 103 | while read -e -t 0.01; do true; done # Clear buffer 104 | while [[ ${char,,} != [$2] ]]; do 105 | read -p "$1 " -n 1 char 106 | while read -e -t 0.01; do true; done 107 | echo 108 | done 109 | userinput=${char,,} 110 | } 111 | 112 | # Optionally searches the PS Store for better titles (option -s) 113 | online_search() { 114 | case $content_id in 115 | UP*) url="https://store.playstation.com/en-us/product/$content_id" ;; 116 | EP*) url="https://store.playstation.com/en-gb/product/$content_id" ;; 117 | JP*) url="https://store.playstation.com/ja-jp/product/$content_id" ;; 118 | *) return ;; 119 | esac 120 | online_search_result=$(curl --silent "$url" \ 121 | | grep -Po -m1 '"@type":"Product","name":"\K[^"]*(?=")') 122 | if [[ $online_search_result == "" ]]; then 123 | echo "Online search failed." >&2 124 | else 125 | title=$online_search_result 126 | echo "Online search successful." 127 | fi 128 | } 129 | 130 | # Moves file "$1" to new file name "$2" 131 | rename_file() { 132 | # Skip if filenames are equal 133 | if [[ $1 == "$2" ]]; then 134 | echo "Nothing to do." 135 | return 0 # Skip to next PKG 136 | fi 137 | 138 | # Skip if new file name is an existing directory 139 | if [[ -d "$2" ]]; then 140 | echo "A directory with the same name already exists." >&2 141 | echo "Skipped file $1" >&2 142 | rename_all=false 143 | return 1 # Return to interactive loop with current PKG 144 | fi 145 | 146 | if [[ -f "$2" ]]; then 147 | # Filesystem is case-insensitive (e.g. exFAT) 148 | if [[ ${1,,} == "${2,,}" ]]; then 149 | if [[ -f "$1.pkgrename" ]]; then 150 | echo "The old temporary file \"$1.pkgrename\" prevents renaming." >&2 151 | echo "Please fix this and then try again." >&2 152 | return 1 153 | else 154 | # Workaround for mv on case-insensitive filesystems 155 | mv "$1" "$1.pkgrename" && mv "$1.pkgrename" "$2" 156 | fi 157 | # Filesystem is case-sensitive 158 | else 159 | echo "File \"$2\" already exists." 160 | read_userinput "Overwrite? [Y]es [N]o" yn 161 | case $userinput in 162 | y) mv "$1" "$2" ;; 163 | n) return 1 ;; 164 | esac 165 | fi 166 | else 167 | mv -i "$1" "$2" 168 | fi 169 | } 170 | 171 | # Accepts a PKG file and renames it 172 | pkgrename() { 173 | local app_ver backport backport_detected category content_id firmware \ 174 | newfilename online_search_result pubtoolinfo release releasegroup sdk \ 175 | title title_id version 176 | local filename=${1##*/} # Just the file name without path 177 | local filename_lowercase=${filename,,} 178 | 179 | echo "${filename}" 180 | 181 | # Run external "sfo" script to get param.sfo variables 182 | while IFS= read -r line; do 183 | case "${line%%=*}" in 184 | APP_VER) app_ver=${line#*=} ;; # Patch version 185 | CATEGORY) category=${line#*=} ;; # ac: DLC, gd: Game, gp: Patch 186 | CONTENT_ID) content_id=${line#*=} ;; # https://www.psdevwiki.com/ps4/Content_ID 187 | PUBTOOLINFO) pubtoolinfo=${line#*=} ;; 188 | SYSTEM_VER) 189 | firmware=${line#*0x} 190 | firmware=${firmware:0:2}.${firmware:2:2} 191 | firmware=${firmware#0} 192 | (( ${firmware/./} )) || firmware= # Set empty if not a number 193 | ;; 194 | TITLE) title=${line#*=} ;; # Game/Patch/DLC name 195 | TITLE_ID) title_id=${line#*=} ;; # CUSAXXXXX 196 | VERSION) version=${line#*=} ;; # Master version 197 | esac 198 | done < <("$sfo_script_name" "$1") 199 | 200 | # Create "sdk" variable (stays empty if PKG is DLC) 201 | if [[ $category == +(gd|gp) ]]; then 202 | sdk=${pubtoolinfo#*sdk_ver=} 203 | sdk=${sdk%%,*} 204 | sdk=${sdk:0:2}.${sdk:2:2} 205 | sdk=${sdk#0} 206 | (( ${sdk/./} )) || sdk= # Set empty if not a number 207 | fi 208 | 209 | # Check file name for release groups 210 | case ${filename_lowercase} in 211 | *[^a-z]bigbluebox[^a-z]*) releasegroup="BigBlueBox" ;; 212 | *[^a-z]blz[^a-z]*|*[^a-z]blaze[^a-z]*) releasegroup="BlaZe" ;; 213 | *[^a-z]caf[^a-z]*) releasegroup="CAF" ;; 214 | *[^a-z]darkmoon[^a-z]*) releasegroup="DarKmooN" ;; 215 | *[^a-z]duplex[^a-z]*) releasegroup="DUPLEX" ;; 216 | *[^a-z]gcmr[^a-z]*) releasegroup="GCMR" ;; 217 | *[^a-z]hoodlum[^a-z]*) releasegroup="HOODLUM" ;; 218 | *[^a-z]hr[^a-z]*) releasegroup="HR" ;; 219 | *[^a-z]internal[^a-z]*) releasegroup="iNTERNAL" ;; 220 | *[^a-z]jrp[^a-z]*) releasegroup="JRP" ;; 221 | *[^a-z]kotf[^a-z]*) releasegroup="KOTF" ;; 222 | *[^a-z]levelup[^a-z]*) releasegroup="LevelUp" ;; 223 | *[^a-z]lfc[^a-z]*|*[^a-z]lightforce[^a-z]*) releasegroup="LiGHTFORCE" ;; 224 | *[^a-z]marvtm[^a-z]*) releasegroup="MarvTM" ;; 225 | *[^a-z]moemoe[^a-z]*|moe[^a-z]*) releasegroup="MOEMOE" ;; 226 | *[^a-z]playable[^a-z]*) releasegroup="Playable" ;; 227 | *[^a-z]prelude[^a-z]*) releasegroup="PRELUDE" ;; 228 | *[^a-z]protocol[^a-z]*) releasegroup="PROTOCOL" ;; 229 | *[^a-z]respawn[^a-z]*) releasegroup="RESPAWN" ;; 230 | *[^a-z]sharphd[^a-z]*) releasegroup="SharpHD" ;; 231 | *[^a-z]tcd[^a-z]*) releasegroup="TCD" ;; 232 | *[^a-z]unlimited[^a-z]*) releasegroup="UNLiMiTED" ;; 233 | *[^a-z]walmart[^a-z]*) releasegroup="WaLMaRT" ;; 234 | *[^a-z]waysted[^a-z]*) releasegroup="WaYsTeD" ;; 235 | esac 236 | 237 | # Other releases 238 | case ${filename_lowercase} in 239 | *[^a-z]arczi[^a-z]*) release="Arczi" ;; 240 | *[^a-z]cyb1k[^a-z]*) release="CyB1K" ;; 241 | *[^a-z]opoisso893[^a-z]*) release="OPOISSO893" ;; 242 | esac 243 | 244 | # This line is option "-o", must be run before title customization 245 | [[ $search_ps_store == true ]] && online_search 246 | 247 | # Apply user's customization 248 | customize_title 249 | title_backup=$title # Backup for possible use of [R]eset 250 | customize 251 | 252 | # Checks following customization 253 | # Check for backport; could generate false positives (hopefully not) 254 | if [[ $sdk == 5.05 && ${firmware/./} -gt 600 \ 255 | || ${filename_lowercase} == *[^a-z]backport[^a-z]* \ 256 | || ${filename_lowercase} == *[^a-z]"$backport_string"[^a-z]* ]]; then 257 | backport=$backport_string 258 | fi 259 | case "$category" in 260 | ac) category="$category_dlc" ;; 261 | gd) category="$category_game" ;; 262 | gp) category="$category_patch" ;; 263 | *) category= ;; 264 | esac 265 | 266 | # Interactive loop 267 | while true; do 268 | # Apply user's pattern 269 | newfilename=$(eval echo "$pattern") 270 | 271 | # Remove outer shell characters and whitespace caused by empty variables 272 | newfilename=${newfilename//$outer_shell/} 273 | shopt -s extglob 274 | newfilename=${newfilename// +( )/ } 275 | shopt -u extglob 276 | newfilename=${newfilename/ ./.} 277 | 278 | # Encforce exFAT compatibility 279 | # (https://www.ntfs.com/exfat-filename-dentry.htm) 280 | newfilename=${newfilename//[&:\\|\/\"<>*]/$exfat_placeholder} 281 | 282 | echo "$newfilename" 283 | if [[ $newfilename == "$filename" && $force_prompt != true ]]; then 284 | echo "Nothing to do." 285 | break 286 | elif [[ $rename_all == true ]]; then 287 | if rename_file "$1" "$(dirname "$1")/$newfilename"; then 288 | break 289 | else 290 | continue 291 | fi 292 | else 293 | read_userinput "Rename? [Y]es [N]o [A]ll [E]dit [M]ix [O]nline [R]eset [C]hars [S]FO [Q]uit:" ynaemorcsq 294 | case $userinput in 295 | y) 296 | if rename_file "$1" "$(dirname "$1")/$newfilename"; then 297 | break 298 | else 299 | continue 300 | fi 301 | ;; 302 | n) break ;; 303 | a) 304 | if rename_file "$1" "$(dirname "$1")/$newfilename"; then 305 | rename_all=true 306 | break 307 | else 308 | continue 309 | fi 310 | ;; 311 | e) echo "Enter new title:"; read -i "$title" -e title ;; 312 | m) 313 | title=${title,,} 314 | array=($title) 315 | title=${array[@]~} # Uppercase for first letter of every word 316 | echo "Converted title to mixed-case style." 317 | ;; 318 | o) online_search; customize_title ;; 319 | r) title=$title_backup; echo "All changes reset." ;; 320 | c) 321 | special_chars=$(echo -n "$newfilename" | cat -A); 322 | if [[ $special_chars != "$newfilename" ]]; then 323 | echo "Special characters found:" 324 | echo "$special_chars" 325 | else 326 | echo "No special characters found." 327 | fi 328 | ;; 329 | s) echo; "$sfo_script_name" "$1"; echo ;; 330 | q) cleanup ;; 331 | esac 332 | fi 333 | done 334 | echo 335 | } 336 | 337 | # File loop 338 | while [[ $1 != "" ]]; do 339 | if [[ -d "$1" ]]; then 340 | # File descriptor 3 because of nested reads (function "read_userinput") 341 | while IFS= read -r -d $'\0' -u 3 pkg; do 342 | pkgrename "$pkg" 343 | done 3< <(find "$1" $find_option -iname '*.pkg' -type f -print0 | sort -z) 344 | elif [[ -f "$1" && ${1,,} == *.pkg ]]; then 345 | pkgrename "$1" 346 | fi 347 | shift 348 | done 349 | -------------------------------------------------------------------------------- /pkgrename.c/src/pkg.c: -------------------------------------------------------------------------------- 1 | #include "../include/checksums.h" 2 | #include "../include/common.h" 3 | #include "../include/pkg.h" 4 | #include "../include/scan.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define MAGIC_NUMBER_PKG 0x7f434e54 12 | #define MAGIC_NUMBER_PARAM_SFO 0x46535000 13 | #define MAX_SIZE_PARAM_SFO 65536 14 | #define MAX_SIZE_CHANGELOG 65536 15 | 16 | struct pkg_header { 17 | uint32_t magic; 18 | uint32_t type; 19 | uint32_t unknown_data; 20 | uint32_t file_count; 21 | uint32_t entry_count; 22 | uint32_t garbage_data; 23 | uint32_t table_offset; 24 | uint32_t entry_data_size; 25 | uint64_t body_offset; 26 | uint64_t body_size; 27 | uint64_t content_offset; 28 | uint64_t content_size; 29 | unsigned char content_id[36]; 30 | unsigned char padding[12]; 31 | uint32_t drm_type; 32 | uint32_t content_type; 33 | uint32_t content_flags; 34 | } __attribute__ ((packed, scalar_storage_order("big-endian"))); // Requires GCC. 35 | 36 | struct pkg_table_entry { 37 | uint32_t id; 38 | uint32_t filename_offset; 39 | uint32_t flags1; 40 | uint32_t flags2; 41 | uint32_t offset; 42 | uint32_t size; 43 | uint64_t padding; 44 | } __attribute__ ((packed, scalar_storage_order("big-endian"))); 45 | 46 | struct param_sfo_header { 47 | uint32_t magic; 48 | uint32_t version; 49 | uint32_t keytable_offset; 50 | uint32_t datatable_offset; 51 | uint32_t n_entries; 52 | } __attribute__ ((packed, scalar_storage_order("little-endian"))); 53 | 54 | struct param_sfo_entry { 55 | uint16_t key_offset; 56 | uint16_t param_type; 57 | uint32_t param_len; 58 | uint32_t param_max_len; 59 | uint32_t data_offset; 60 | } __attribute__ ((packed, scalar_storage_order("little-endian"))); 61 | 62 | // Checks a buffered param.sfo file's integrity. 63 | // Returns 0 on success or -1 on failure. 64 | static int check_param_sfo(const unsigned char *param_sfo_buf, size_t buf_size) 65 | { 66 | if (buf_size < sizeof(struct param_sfo_header)) 67 | return -1; 68 | 69 | struct param_sfo_header *header = (struct param_sfo_header *) param_sfo_buf; 70 | if (buf_size < sizeof(struct param_sfo_header) + header->n_entries 71 | * sizeof(struct param_sfo_entry) 72 | || header->keytable_offset >= buf_size 73 | || header->datatable_offset >= buf_size) 74 | return -1; 75 | 76 | struct param_sfo_entry *entries = (struct param_sfo_entry *) 77 | ¶m_sfo_buf[sizeof(struct param_sfo_header)]; 78 | for (uint32_t i = 0; i < header->n_entries; i++) { 79 | if (header->keytable_offset + entries[i].key_offset >= buf_size 80 | || header->datatable_offset + entries[i].data_offset 81 | + entries[i].param_len >= buf_size) 82 | return -1; 83 | } 84 | 85 | return 0; 86 | } 87 | 88 | static void gen_key(void *key, char *content_id, char *passcode, 89 | int index) 90 | { 91 | unsigned char index_buf[4] = { 0, 0, 0, index }; 92 | unsigned char content_id_buf[48] = { 0 }; 93 | memcpy(content_id_buf, content_id, strlen(content_id)); 94 | 95 | unsigned char checksum1[32]; 96 | unsigned char checksum2[32]; 97 | sha256(checksum1, index_buf, sizeof(index_buf)); 98 | sha256(checksum2, content_id_buf, sizeof(content_id_buf)); 99 | /* print_checksum(stdout, checksum1, 32); */ 100 | /* printf("\n"); */ 101 | /* print_checksum(stdout, checksum2, 32); */ 102 | /* printf("\n"); */ 103 | 104 | unsigned char data[96]; 105 | memcpy(data, checksum1, 32); 106 | memcpy(data + 32, checksum2, 32); 107 | memcpy(data + 64, passcode, 32); 108 | 109 | sha256(key, data, sizeof(data)); 110 | } 111 | 112 | static _Bool is_fake(char *content_id, unsigned char key_checksum[32]) 113 | { 114 | static char *passcode = "00000000000000000000000000000000"; 115 | 116 | unsigned char key[32]; 117 | gen_key(key, content_id, passcode, 0); 118 | unsigned char checksum[32]; 119 | sha256(checksum, key, sizeof(key)); 120 | for (int i = 0; i < 32; i++) 121 | checksum[i] = key[i] ^ checksum[i]; 122 | 123 | // Debug 124 | /* print_checksum(stdout, key_checksum, 32); */ 125 | /* print_checksum(stdout, checksum, 32); */ 126 | /* printf("\n"); */ 127 | 128 | int ret = memcmp(checksum, key_checksum, 32); 129 | 130 | if (ret) 131 | return false; 132 | return true; 133 | } 134 | 135 | // Loads PKG data into dynamically allocated buffers and passes their pointers. 136 | // Returns 0 on success and -1 on error. 137 | int load_pkg_data(unsigned char **param_sfo, char **changelog, 138 | _Bool *fake_status, const char *filename) 139 | { 140 | #pragma GCC diagnostic push 141 | #pragma GCC diagnostic ignored "-Wscalar-storage-order" 142 | int retval; 143 | 144 | FILE *file = fopen(filename, "rb"); 145 | if (file == NULL) 146 | return SCAN_ERROR_OPEN_FILE; 147 | 148 | struct pkg_header pkg_header; 149 | if (fread(&pkg_header, sizeof(pkg_header), 1, file) != 1) 150 | goto read_error; 151 | if (pkg_header.magic != MAGIC_NUMBER_PKG) { 152 | retval = SCAN_ERROR_NOT_A_PKG; 153 | goto error; 154 | } 155 | 156 | // Get offsets and file sizes first, not to drop read-ahead cache. 157 | uint32_t param_sfo_offset, param_sfo_size, changelog_offset, keys_offset, 158 | changelog_size; 159 | int keys_offset_found = 0; 160 | int param_sfo_found = 0; 161 | int changelog_found = 0; 162 | if (fseek(file, pkg_header.table_offset, SEEK_SET)) 163 | goto read_error; 164 | for (uint32_t i = 0; i < pkg_header.entry_count; i++) { 165 | struct pkg_table_entry entry; 166 | if (fread(&entry, sizeof(entry), 1, file) != 1) 167 | goto read_error; 168 | 169 | if (entry.id == 0x10) { 170 | keys_offset = entry.offset; 171 | keys_offset_found = 1; 172 | } else if (entry.id == 0x1000) { // param.sfo 173 | param_sfo_offset = entry.offset; 174 | if (entry.size > MAX_SIZE_PARAM_SFO) { 175 | retval = SCAN_ERROR_PARAM_SFO_INVALID_SIZE; 176 | goto error; 177 | } 178 | param_sfo_size = entry.size; 179 | param_sfo_found = 1; 180 | } else if (entry.id == 0x1260) { // changeinfo.xml 181 | changelog_offset = entry.offset; 182 | if (entry.size > MAX_SIZE_CHANGELOG) { 183 | retval = SCAN_ERROR_CHANGELOG_INVALID_SIZE; 184 | goto error; 185 | } 186 | changelog_size = entry.size; 187 | changelog_found = 1; 188 | } 189 | } 190 | 191 | // Load param.sfo. 192 | #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" 193 | if (param_sfo_found) { 194 | if (fseek(file, param_sfo_offset, SEEK_SET)) 195 | goto read_error; 196 | 197 | unsigned char *param_sfo_buf = malloc(param_sfo_size + 1); 198 | if (param_sfo_buf == NULL) { 199 | retval = SCAN_ERROR_OUT_OF_MEMORY; 200 | goto read_error; 201 | } 202 | 203 | if (fread(param_sfo_buf, param_sfo_size, 1, file) != 1) 204 | goto read_error; 205 | 206 | if ((*(struct param_sfo_header *) param_sfo_buf).magic 207 | != MAGIC_NUMBER_PARAM_SFO) { 208 | retval = SCAN_ERROR_PARAM_SFO_INVALID_FORMAT; 209 | goto error; 210 | } 211 | 212 | if (check_param_sfo(param_sfo_buf, param_sfo_size)) { 213 | retval = SCAN_ERROR_PARAM_SFO_INVALID_DATA; 214 | goto error; 215 | } 216 | 217 | // Guard against non-terminated keytable. 218 | param_sfo_buf[param_sfo_size] = '\0'; 219 | 220 | *param_sfo = param_sfo_buf; 221 | } else { 222 | retval = SCAN_ERROR_PARAM_SFO_NOT_FOUND; 223 | goto error; 224 | } 225 | 226 | // Load changelog. 227 | if (changelog_found) { 228 | if (fseek(file, changelog_offset, SEEK_SET)) 229 | goto read_error; 230 | 231 | char *changelog_buf = malloc(changelog_size + 1); 232 | if (changelog_buf == NULL) { 233 | retval = SCAN_ERROR_OUT_OF_MEMORY; 234 | goto error; 235 | } 236 | if (fread(changelog_buf, changelog_size, 1, file) != 1) 237 | goto read_error; 238 | changelog_buf[changelog_size] = '\0'; // Make it a C string. 239 | 240 | *changelog = changelog_buf; 241 | } else { 242 | *changelog = NULL; 243 | } 244 | 245 | // Check for FPKG. 246 | if (keys_offset_found) { 247 | if (fseek(file, keys_offset + 32, SEEK_SET)) 248 | goto read_error; 249 | 250 | unsigned char key_checksum[32]; 251 | if (fread(key_checksum, 32, 1, file) != 1) 252 | goto read_error; 253 | 254 | char *content_id = get_param_sfo_value(*param_sfo, "CONTENT_ID"); 255 | if (content_id == NULL) { 256 | retval = SCAN_ERROR_PARAM_SFO_INVALID_DATA; 257 | goto error; 258 | } 259 | 260 | *fake_status = is_fake(content_id, key_checksum); 261 | } 262 | 263 | fclose(file); 264 | return 0; 265 | 266 | read_error: 267 | retval = SCAN_ERROR_READ_FILE; 268 | error: 269 | fclose(file); 270 | return retval; 271 | #pragma GCC diagnostic pop 272 | } 273 | 274 | // Loads the true patch version from a string and stores it in a buffer; 275 | // the buffer must be of size 6. 276 | // Returns 1 if the patch version has been found, otherwise 0. 277 | int store_patch_version(char *version_buf, const char *changelog) 278 | { 279 | // Grab the highest patch version. 280 | const char *bufp = changelog; 281 | char *next_patch; 282 | char current_patch[6]; 283 | while ((next_patch = strstr(bufp, "app_ver=\"")) != NULL) { 284 | next_patch += 9; 285 | if (version_buf[0] == '\0') { 286 | strncpy(version_buf, next_patch, 5); 287 | version_buf[5] = '\0'; 288 | } else { 289 | strncpy(current_patch, next_patch, 5); 290 | current_patch[5] = '\0'; 291 | if (strcmp(version_buf, current_patch) < 0) 292 | memcpy(version_buf, current_patch, 6); 293 | } 294 | 295 | bufp = next_patch; 296 | } 297 | 298 | return version_buf[0] != '\0'; 299 | } 300 | 301 | // Searches a buffered param.sfo file for a key/value pair and returns a pointer 302 | // to the value. Returns NULL if the key is not found. 303 | void *get_param_sfo_value(const unsigned char *param_sfo_buf, const char *key) 304 | { 305 | struct param_sfo_header *header = (struct param_sfo_header *) param_sfo_buf; 306 | struct param_sfo_entry *entries = (struct param_sfo_entry *) 307 | ¶m_sfo_buf[sizeof(struct param_sfo_header)]; 308 | 309 | for (uint32_t i = 0; i < header->n_entries; i++) { 310 | char *current_key = (char *) ¶m_sfo_buf[header->keytable_offset 311 | + entries[i].key_offset]; 312 | if (strcmp(current_key, key) == 0) 313 | return (void *) ¶m_sfo_buf[header->datatable_offset 314 | + entries[i].data_offset]; 315 | } 316 | 317 | return NULL; 318 | } 319 | 320 | // Prints a buffered param.sfo file's keys and values. 321 | void print_param_sfo(const unsigned char *param_sfo_buf) 322 | { 323 | struct param_sfo_header *header = (struct param_sfo_header *) param_sfo_buf; 324 | struct param_sfo_entry *entries = (struct param_sfo_entry *) 325 | ¶m_sfo_buf[sizeof(struct param_sfo_header)]; 326 | 327 | for (uint32_t i = 0; i < header->n_entries; i++) { 328 | char *key = (char *) ¶m_sfo_buf[header->keytable_offset 329 | + entries[i].key_offset]; 330 | void *val = (void *) ¶m_sfo_buf[header->datatable_offset 331 | + entries[i].data_offset]; 332 | 333 | switch (entries[i].param_type) { 334 | case 0x0004: 335 | case 0x0204: 336 | printf("%s=\"%s\"\n", key, (char *) val); 337 | break; 338 | case 0x0404: 339 | printf("%s=0x%08X\n", key, *(uint32_t *) val); 340 | break; 341 | } 342 | } 343 | } 344 | 345 | int get_checksum(char msum[7], const char *filename) 346 | { 347 | unsigned char buf[32]; 348 | 349 | FILE *file = fopen(filename, "rb"); 350 | if (file == NULL) { 351 | fprintf(stderr, "Could not open file \"%s\".\n", filename); 352 | return -1; 353 | } 354 | 355 | struct pkg_header header; 356 | if (fread((void *) &header, sizeof(header), 1, file) != 1) 357 | goto read_error; 358 | 359 | if (header.content_type == 27) // DLC 360 | goto error; 361 | 362 | uint32_t target_id; 363 | switch (header.content_flags & 0x0F000000) { 364 | case 0x0A000000: 365 | target_id = 0x1001; 366 | break; 367 | case 0x02000000: 368 | target_id = 0x1008; 369 | break; 370 | default: 371 | goto error; 372 | } 373 | 374 | if (fseek(file, header.table_offset, SEEK_SET)) 375 | goto read_error; 376 | struct pkg_table_entry entry; 377 | if (fread((void *) &entry, sizeof(entry), 1, file) != 1) 378 | goto read_error; 379 | uint32_t digests_offset = entry.offset; 380 | for (uint32_t i = 1; i < header.entry_count; i++) { 381 | if (fread((void *) &entry, sizeof(entry), 1, file) != 1) 382 | goto read_error; 383 | if (entry.id == target_id) { 384 | if (fseek(file, digests_offset + i * 32, SEEK_SET)) 385 | goto read_error; 386 | if (fread(buf, 32, 1, file) != 1) 387 | goto read_error; 388 | 389 | fclose(file); 390 | for (int c = 0; c < 3; c++) 391 | sprintf(msum + c * 2, "%02X", buf[c]); 392 | msum[6] = '\0'; 393 | return 0; 394 | } 395 | } 396 | goto error; 397 | 398 | read_error: 399 | fprintf(stderr, "Could not read from file \"%s\".\n", filename); 400 | error: 401 | fclose(file); 402 | return -1; 403 | } 404 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pkgrename.c 2 | 3 | pkgrename.c is a standalone, advanced version of the original Bash script, written in C. It currently works on Linux and Windows, possibly on other systems, too. 4 | 5 | 6 | - [Manual](#manual) 7 | - [Tagging](#tagging) 8 | - [Querying (for scripts/tools)](#querying) 9 | - [How to compile](#how-to-compile) 10 | - [For Windows users](#for-windows-users) 11 | - [How to run pkgrename.exe from anywhere](#how-to-run-pkgrenameexe-from-anywhere-with-modified-arguments) 12 | - [Original Bash script](#pkgrename-original-bash-script-superseded-by-pkgrenamec) 13 | 14 | # Manual 15 | 16 | [Please note that this manual reflects the current code and not necessarily the latest release.] 17 | 18 | The program in action looks like this: 19 | 20 | $ pkgrename 21 | "totally_not_helpful_filename.pkg" 22 | => "Baldur's Gate and Baldur's Gate II_ Enhanced Editions [v1.02] [CUSA15671].pkg" 23 | [Y/N/A] [E]dit [T]ag [M]ix [O]nline [R]eset [C]hars [S]FO [L]og [H]elp [Q]uit: y 24 | 25 | The program's help screen ("pkgrename --help"): 26 | 27 | ``` 28 | Usage: pkgrename [OPTIONS] [FILE|DIRECTORY ...] 29 | 30 | Renames PS4 PKGs to match a file name pattern. The default pattern is: 31 | "%title% [%dlc%] [{v%app_ver%}{ + v%merged_ver%}] [%title_id%] [%release_group%] [%release%] [%backport%]" 32 | 33 | Pattern variables: 34 | ------------------ 35 | Name Example 36 | ---------------------------------------------------------------------- 37 | %app% "App" 38 | %app_ver% "4.03" 39 | %backport% "Backport" (1) 40 | %category% "gp" 41 | %content_id% "EP4497-CUSA05571_00-00000000000GOTY1" 42 | %dlc% "DLC" 43 | %fake% "Fake" (5) 44 | %fake_status% "Fake" (5) 45 | %file_id% "EP4497-CUSA05571_00-00000000000GOTY1-A0403-V0100" 46 | %firmware% "10.01" 47 | %game% "Game" 48 | %merged_ver% "" (2) 49 | %msum% "3E57B0" (3) 50 | %other% "Other" 51 | %patch% "Update" 52 | %region% "EU" 53 | %release_group% "PRELUDE" (1) 54 | %release% "John Doe" (1) 55 | %retail% "" (5) 56 | %sdk% "4.50" 57 | %size% "19.34 GiB" 58 | %title% "The Witcher 3: Wild Hunt – Game of the Year Edition" 59 | %title_id% "CUSA05571" 60 | %true_ver% "4.03" (2) 61 | %type% "Update" (4) 62 | %version% "1.00" 63 | 64 | (1) Backports not targeting 5.05 are detected by searching file names for the 65 | words "BP" and "Backport" (case-insensitive). The same principle applies to 66 | release groups and releases. 67 | 68 | (2) Patches and apps merged with patches are detected by searching PKG files 69 | for changelog information. If a patch is found, both %merged_ver% and 70 | %true_ver% are the patch version. If no patch is found or if patch detection 71 | is disabled (command [P]), %merged_ver% is empty and %true_ver% is %app_ver%. 72 | %merged_ver% is always empty for non-app PKGs. 73 | 74 | (3) A checksum that indicates whether game and update PKGs that have the 75 | same Title ID are compatible with each other ("married"). This pattern 76 | variable will be empty for PKGs of other types. 77 | 78 | (4) %type% is %category% mapped to "Game,Update,DLC,App,Other". 79 | These five default strings can be changed via option "--set-type", e.g.: 80 | --set-type "Game,Patch %app_ver%,DLC,-,-" (no spaces before or after commas) 81 | Each string must have a value. To hide a category, use the value "-". 82 | %app%, %dlc%, %game%, %other%, and %patch% are mapped to their corresponding 83 | %type% values. They will be displayed if the PKG is of that specific category. 84 | 85 | (5) These pattern variables depend on the type of the PKG: 86 | PKG type %fake% %retail% %fake_status% 87 | "Fake" PKG (FPKG) Fake Fake 88 | Retail PKG Retail Retail 89 | 90 | After parsing, empty pairs of brackets, empty pairs of parentheses, and any 91 | remaining curly braces ("[]", "()", "{", "}") will be removed. 92 | 93 | Curly braces expressions: 94 | ------------------------- 95 | Pattern variables and other strings can be grouped together by surrounding 96 | them with curly braces. If an inner pattern variable turns out to be empty, 97 | the whole curly braces expression will be removed. 98 | 99 | Example 1 - %firmware% is empty: 100 | "%title% [FW %firmware%]" => "Example DLC [FW ].pkg" WRONG 101 | "%title% [{FW %firmware%}]" => "Example DLC.pkg" CORRECT 102 | 103 | Example 2 - %firmware% has a value: 104 | "%title% [{FW %firmware%}]" => "Example Game [FW 7.55].pkg" 105 | 106 | Handling of special characters: 107 | ------------------------------- 108 | - For exFAT compatibility, some characters are replaced by a placeholder 109 | character (default: underscore). 110 | - Some special characters like copyright symbols are automatically removed 111 | or replaced by more common alternatives. 112 | - Numbers appearing in parentheses behind a file name indicate the presence 113 | of non-ASCII characters. 114 | 115 | Interactive prompt: 116 | ------------------- 117 | - [Y]es Rename the file as seen. 118 | - [N]o Skip the file and drop all changes. 119 | - [A]ll Same as yes, but also for all future files. 120 | - [E]dit Prompt to manually edit the title. 121 | - [T]ag Prompt to enter a release group or a release. 122 | - [M]ix Convert the letter case to mixed-case style. 123 | - [O]nline Search the PS Store online for title information. 124 | - [R]eset Undo all changes. 125 | - [C]hars Reveal special characters in the title. 126 | - [S]FO Show file's param.sfo information. 127 | - [L]og Print existing changelog data. 128 | - [H]elp Print help. 129 | - [Q]uit Exit the program. 130 | - [B] Toggle the "Backport" tag. 131 | - [P] Toggle changelog patch detection for the current PKG. 132 | - Shift-[T] Remove all release tags. 133 | - Backspace Go back to the previous PKG. 134 | - Space Return to the current PKG. 135 | 136 | Options: 137 | -------- 138 | -c, --compact Hide files that are already renamed. 139 | --disable-colors Disable colored text output. 140 | -f, --force Force-prompt even when file names match. 141 | -h, --help Print this help screen. 142 | -l, --language LANG If the PKG supports it, use the language specified 143 | by language code LANG (see --print-languages) to 144 | retrieve the PKG's title. 145 | -0, --leading-zeros Show leading zeros in pattern variables %app_ver%, 146 | %firmware%, %merged_ver%, %sdk%, %true_ver%, 147 | %version%. 148 | -m, --mixed-case Automatically apply mixed-case letter style. 149 | --no-placeholder Hide characters instead of using placeholders. 150 | -n, --no-to-all Do not prompt; do not actually rename any files. 151 | This can be used to do a test run. 152 | -o, --online Automatically search online for %title%. 153 | --override-tags Make changelog release tags take precedence over 154 | existing file name tags. 155 | -p, --pattern PATTERN Set the file name pattern to string PATTERN. 156 | --placeholder X Set the placeholder character to X. 157 | --print-languages Print available language codes. 158 | --print-tags Print all built-in release tags. 159 | -q, --query For scripts/tools: print file name suggestions, one 160 | per line, without renaming the files. A successful 161 | query returns exit code 0. 162 | -r, --recursive Traverse subdirectories recursively. 163 | --set-backport STRING Set %backport% mapping to STRING. 164 | --set-fake STRINGS Set %fake%, %fake_status%, and %retail% mappings to 165 | two comma-separated STRINGS. The first string 166 | replaces %fake%, the second one %retail%. 167 | --set-type CATEGORIES Set %type% mapping to comma-separated string 168 | CATEGORIES (see section "Pattern variables"). 169 | --tagfile FILE Load additional %release% tags from text file FILE, 170 | one tag per line. 171 | --tags TAGS Load additional %release% tags from comma-separated 172 | string TAGS (no spaces before or after commas). 173 | --tag-separator SEP Use the string SEP instead of commas to separate 174 | multiple release tags. 175 | -u, --underscores Use underscores instead of spaces in file names. 176 | -v, --verbose Display additional infos. 177 | --version Print the current pkgrename version. 178 | -y, --yes-to-all Do not prompt; rename all files automatically. 179 | ``` 180 | 181 | ## Tagging 182 | 183 | You can organize your PKGs by tagging them: 184 | 185 | "unnamed.pkg" 186 | => "Assassin's Creed Valhalla [v1.00] [CUSA18534].pkg" 187 | [Y/N/A] [E]dit [T]ag [M]ix [O]nline [R]eset [C]hars [S]FO [L]og [H]elp [Q]uit: t 188 | 189 | Enter new tag: dup [DUPLEX] 190 | 191 | Pressing Tab at this point will use word completion to apply the suggested value. You can enter multiple tags by separating them with commas. Press Enter to apply the changes and any remaining word suggestion: 192 | 193 | => "Assassin's Creed Valhalla [v1.00] [CUSA18534] [DUPLEX].pkg" 194 | [Y/N/A] [E]dit [T]ag [M]ix [O]nline [R]eset [C]hars [S]FO [L]og [H]elp [Q]uit: 195 | 196 | The next time pkgrename is run on this file, it will recognize and preserve the tag. 197 | You can add your own tag values, by using options --tags and/or --tagfile: 198 | 199 | pkgrename --tags "user500,Umbrella Corp.,john_wayne" 200 | pkgrename --tagfile tags.txt 201 | 202 | If you use a text file, each line must contain a single tag: 203 | 204 | user500 205 | Umbrella Corp. 206 | john_wayne 207 | 208 | ## Querying 209 | Use querying to receive name suggestions for your scripts/tools, for example: 210 | 211 | $ pkgrename -p '%title% [%true_ver%]' --query ps4.pkg ps3.pkg flower.gif subdirectory/ 212 | Super Mario Bros. [1.00].pkg 213 | ps3.pkg 214 | flower.gif 215 | subdirectory/ 216 | $ echo $? 217 | 0 218 | 219 | Files that can't be renamed (are not PKGs, are broken, etc.) and directories are returned unchanged. 220 | A successful query returns exit code 0. On error, the list is incomplete and a non-zero value is returned to indicate failure. 221 | 222 | ## How to compile... 223 | 224 | ...for Linux/Unix (requires libcurl development headers; for Debian-based distros "libcurl4-xxx-dev"): 225 | 226 | gcc -Wall -Wextra -pedantic pkgrename.c src/*.c -o pkgrename -lcurl -pthread -s -O3 227 | 228 | ...for Windows: 229 | 230 | x86_64-w64-mingw32-gcc-win32 -Wall -Wextra -pedantic pkgrename.c src/*.c -o pkgrename.exe -static -pthread -s -O3 231 | 232 | Or download a compiled Windows release at https://github.com/hippie68/pkgrename/releases. 233 | 234 | Please report bugs, make feature requests, or add missing data at https://github.com/hippie68/pkgrename/issues. 235 | 236 | # For Windows users 237 | 238 | On Windows 10/11, it is **strongly recommended** to activate the UTF-8 beta feature: Settings - Time & Language - Language - Administrative language settings - Change system locale... - Beta: Use Unicode UTF-8 for worldwide language support. 239 | For Windows 10 users it is recommended to use the new Windows Terminal application (which is now the default terminal in Windows 11) instead of the standard cmd.exe command prompt. 240 | When using both the UTF-8 beta feature and Windows Terminal, pkgrename should work as intended. 241 | 242 | If the UTF-8 beta feature is not used, file names that contain multibyte characters may cause this error: 243 | 244 | Could not read file system information: "weird_characters_????.pkg". 245 | 246 | Such a PKG file can't be renamed then and will be skipped. 247 | 248 | ## How to run pkgrename.exe from anywhere with modified arguments 249 | 250 | Put pkgrename.exe in a folder (you can also put other command line programs there). 251 | Inside that folder, create a new batch file named "pkgrename.bat" and open it with Notepad. 252 | Write the following lines, while replacing ARGUMENTS with your preferred arguments: 253 | 254 | @echo off 255 | pkgrename.exe ARGUMENTS %* 256 | 257 | For example: 258 | 259 | @echo off 260 | pkgrename.exe --pattern "%%title%% [%%title_id%%]" --tagfile "C:\Users\Luigi\pkgrename_tags.txt" %* 261 | 262 | Note: As seen above, if the batch file contains pattern variables, their percent signs need to be escaped by doubling them. For example, %title% must be changed to %%title%%. 263 | 264 | Now click Start, type "env" and select "Edit environment variables for your account". Select "Path" and click edit. Select "New" and enter the folder where you put pkgrename.bat into. Close and reopen any opened command line windows for the changes to apply. 265 | 266 | # pkgrename (original Bash script, superseded by pkgrename.c) 267 | Renames PS4 PKG files based on local param.sfo information and predefined patterns. 268 | Requires Bash script or program "sfo" (https://github.com/hippie68/sfo) in your $PATH environment variable. 269 | 270 | Usage: `pkgrename [options] [file|directory ...]` 271 | 272 | Options: 273 | 274 | -f Force prompt when file name matches pattern 275 | -h Display this help info 276 | -o Default to online search 277 | -r Traverse directories recursively 278 | 279 | The script in action looks like this: 280 | 281 | $ pkgrename 282 | totally_not_helpful_filename.pkg 283 | Baldur's Gate and Baldur's Gate II_ Enhanced Editions [UPDATE 1.02] [CUSA15671].pkg 284 | Rename? [Y]es [N]o [A]ll [E]dit [M]ix [O]nline [R]eset [C]hars [S]FO [Q]uit: y 285 | 286 | - `[Y]es` Renames the file as seen 287 | - `[N]o` Skips the file and drops all changes 288 | - `[A]ll` Same as yes, but also for all future files 289 | - `[E]dit` Prompts to manually edit the title 290 | - `[M]ix` Converts the title to mixed case format 291 | - `[O]nline` Searches the PS Store online for the current file's title information 292 | - `[R]eset` Reverts all title changes 293 | - `[C]hars` Shows special characters, if still present 294 | - `[S]FO` Shows file's param.sfo information 295 | - `[Q]uit` Exits the script immediately 296 | 297 | You can easily customize the naming scheme in the customization section at the top of the script: 298 | 299 | pattern='$title [$category] [$title_id] [$releasegroup] [$backport].pkg' 300 | 301 | Possible variables: title, category, backport, sdk, firmware, releasegroup 302 | Plus original SFO strings: app_ver, content_id, title_id, version 303 | 304 | You can fully customize every aspect of the file name. 305 | Further information is found inside the script's customization section. 306 | 307 | For exFAT compatibility, by default certain characters are replaced with underscores (which is also customizable). 308 | -------------------------------------------------------------------------------- /pkgrename.c/include/getopt.h: -------------------------------------------------------------------------------- 1 | // A thread-safe, more intuitive alternative to getopt_long() that features 2 | // word-wrapping help output and nested subcommands. 3 | // https://github.com/hippie68/getopt 4 | 5 | #ifndef GETOPT_H 6 | #define GETOPT_H 7 | 8 | /// Interface ------------------------------------------------------------------ 9 | 10 | #include 11 | 12 | struct option { 13 | int index; // The option's unique identifier. 14 | // A single alphanumeric ASCII character literal means 15 | // this is the option's short name. Any other positive 16 | // value means the option does not have a short name. 17 | // A negative value means the option is a subcommand. 18 | char *name; // The option's unique long name (without leading "--") 19 | // or the subcommand's name. 20 | char *arg; // A single option-argument or the subcommand's operands, 21 | // as they are supposed to appear in the help screen 22 | // (e.g. "ARG", "[ARG]", "", ...). 23 | // Square brackets mean the argument is optional. 24 | char *description; // The option's or the subcommand's description. 25 | }; 26 | 27 | #define HIDEOPT &var_HIDEOPT // Hides options if used as value for .description. 28 | 29 | // Return values: 0 when done, '?' on error, otherwise an option's .index value. 30 | int getopt(int *argc, char **argv[], char **optarg, const struct option opts[]); 31 | 32 | // Prints the specified option array's options. 33 | // Returns 1 if the array has subcommands, otherwise 0. 34 | int print_options(FILE *stream, const struct option opts[]); 35 | 36 | // Prints the specified option array's subcommands. 37 | void print_subcommands(FILE *stream, const struct option opts[]); 38 | 39 | /* Quick example: 40 | int main(int argc, char *argv[]) 41 | { 42 | static struct option opts[] = { 43 | { 'h', "help", NULL, "Print help information and quit." }, 44 | { 0 } 45 | }; 46 | 47 | int opt; 48 | char *optarg; 49 | while ((opt = getopt(&argc, &argv, &optarg, opts)) != 0) { 50 | switch (opt) { 51 | case 'h': 52 | print_options(stdout, opts); 53 | return 0; 54 | case '?': // Error 55 | return 1; 56 | } 57 | } 58 | } 59 | */ 60 | 61 | /// Implementation ------------------------------------------------------------- 62 | 63 | #include 64 | 65 | // Customizable values used by print_options() and print_subcommands(). 66 | #ifndef GETOPT_LINE_MAXLEN 67 | #define GETOPT_LINE_MAXLEN 80 // Max. length of lines of text output. 68 | #endif 69 | #ifndef GETOPT_BLCK_MINLEN 70 | #define GETOPT_BLCK_MINLEN 30 // Min. length of an option's description block. 71 | #endif 72 | 73 | // Error messages printed by getopt(). 74 | #define ERR_SHRTOPT_NEEDARG "Option -%c requires an argument.\n" 75 | #define ERR_LONGOPT_NEEDARG "Option --%s requires an argument.\n" 76 | #define ERR_SHRTOPT_UNKNOWN "Unknown option: -%c\n" 77 | #define ERR_LONGOPT_UNKNOWN "Unknown option: --%s\n" 78 | #define ERR_COMMAND_UNKNOWN "Unknown subcommand: %s\n" 79 | #define ERR_SHRTOPT_HATEARG "Option -%c doesn't allow an argument.\n" 80 | #define ERR_LONGOPT_HATEARG "Option --%s doesn't allow an argument.\n" 81 | 82 | static char var_HIDEOPT; // Dummy variable to make the HIDEOPT pointer unique. 83 | 84 | // Left-shifts array elements by 1, moving the first element to the end. 85 | static void shift(char *argv[]) 86 | { 87 | char *first_element = argv[0]; 88 | while (argv[1] != NULL) { 89 | argv[0] = argv[1]; 90 | argv++; 91 | } 92 | argv[0] = first_element; 93 | } 94 | 95 | // Return values: 0 when done, '?' on error, otherwise an option's .index value. 96 | int getopt(int *argc, char **argv[], char **optarg, const struct option opts[]) 97 | { 98 | if (*argc <= 0 || argv == NULL || *argv == NULL || **argv == NULL 99 | || optarg == NULL || opts == NULL) 100 | goto parsing_finished; 101 | 102 | if (*optarg) 103 | *optarg = NULL; 104 | 105 | // Advance parsing. 106 | if (*argc > 0) { 107 | (*argv)++; 108 | (*argc)--; 109 | } 110 | 111 | if (*argc == 0 || **argv == NULL) 112 | goto parsing_finished; 113 | 114 | char *argp; // Pointer used to probe the command line arguments. 115 | 116 | start: 117 | argp = **argv; 118 | if (*argp == '-') { // Option 119 | argp++; 120 | if (*argp == '-') { // Long option 121 | argp++; 122 | 123 | // Handle "end of options" (--). 124 | if (*argp == '\0') { 125 | (*argv)++; 126 | (*argc)--; 127 | 128 | // Shift-hide all remaining operands. 129 | if ((*argv)[*argc] != NULL) 130 | while (*argc) { 131 | shift(*argv); 132 | (*argc)--; 133 | } 134 | 135 | goto parsing_finished; 136 | } 137 | 138 | // Save attached option-argument, if it exists. 139 | *optarg = strchr(argp, '='); 140 | if (*optarg) { 141 | **optarg = '\0'; 142 | (*optarg)++; 143 | } 144 | 145 | const struct option *opt = opts; 146 | while (opt->index) { 147 | if (opt->name != NULL && strcmp(opt->name, argp) == 0) { 148 | if (*optarg) { 149 | if (opt->arg == NULL) { 150 | fprintf(stderr, ERR_LONGOPT_HATEARG, opt->name); 151 | return '?'; 152 | } 153 | } else if (opt->arg && opt->arg[0] != '[') { 154 | (*argv)++; 155 | (*argc)--; 156 | if (*argc == 0 || (*optarg = **argv) == NULL) { 157 | fprintf(stderr, ERR_LONGOPT_NEEDARG, opt->name); 158 | return '?'; 159 | } 160 | } 161 | return opt->index; 162 | } 163 | opt++; 164 | } 165 | fprintf(stderr, ERR_LONGOPT_UNKNOWN, argp); 166 | return '?'; 167 | } else if (*argp == '\0') { // A single "-" 168 | goto not_an_option; // Treat it as an operand. 169 | } else { // Short option group 170 | const struct option *opt = opts; 171 | while (opt->index) { 172 | if (opt->index == *argp) { // Short option is known. 173 | if (*(argp + 1) == '\0') { // No characters are attached. 174 | if (opt->arg && opt->arg[0] != '[') { 175 | (*argv)++; 176 | (*argc)--; 177 | if (*argc == 0 || (*optarg = **argv) == NULL) { 178 | fprintf(stderr, ERR_SHRTOPT_NEEDARG, *argp); 179 | return '?'; 180 | } 181 | } 182 | } else { // Option has attached characters. 183 | if (opt->arg) { 184 | *optarg = argp + 1; 185 | } else { 186 | if (*(argp + 1) == '-') { // Unwanted argument. 187 | fprintf(stderr, ERR_SHRTOPT_HATEARG, *argp); 188 | return '?'; 189 | } 190 | *argp = '-'; 191 | **argv = argp; // Scan here again next round. 192 | (*argv)--; 193 | (*argc)++; 194 | } 195 | } 196 | return opt->index; 197 | } 198 | opt++; 199 | } 200 | fprintf(stderr, ERR_SHRTOPT_UNKNOWN, *argp); 201 | return '?'; 202 | } 203 | } else { // Either operand or subcommand 204 | not_an_option: 205 | ; 206 | 207 | // Return if operand is a subcommand. 208 | const struct option *opt = opts; 209 | int subcommands_exist = 0; 210 | while (opt->index) { 211 | if (opt->index < 0) { 212 | if (opt->name && strcmp(opt->name, argp) == 0) 213 | return opt->index; 214 | subcommands_exist = 1; 215 | } 216 | opt++; 217 | } 218 | 219 | // Don't move operand if subcommands exist. 220 | if (subcommands_exist) { 221 | fprintf(stderr, ERR_COMMAND_UNKNOWN, argp); 222 | return '?'; 223 | } 224 | 225 | // Move operand to the end of argv[] and hide it for now. 226 | shift(*argv); 227 | (*argc)--; // Hide it. 228 | 229 | if (*argc == 0) 230 | goto parsing_finished; 231 | 232 | goto start; 233 | } 234 | 235 | parsing_finished: 236 | // Unhide any previously hidden operands. 237 | while ((*argv)[*argc] != NULL) 238 | (*argc)++; 239 | 240 | return 0; 241 | } 242 | 243 | // Helper function for print_block(). 244 | static int next_word_size(char *p) 245 | { 246 | char *word_end = p; 247 | do { 248 | word_end++; 249 | } while (*word_end != ' ' && *word_end != '\0'); 250 | return word_end - p; 251 | } 252 | 253 | // Uses word-wrapping to print an indented string over multiple lines. 254 | static void print_block(FILE *stream, char *str, int indent, int start) 255 | { 256 | if (start >= GETOPT_LINE_MAXLEN) { 257 | putc('\n', stderr); 258 | start = 0; 259 | } 260 | 261 | while (*str) { 262 | // Print line indentation. 263 | for (int i = 0, n = indent - start; i < n; i++) 264 | putc(' ', stream); 265 | 266 | int chars_left = GETOPT_LINE_MAXLEN - (start > indent ? start : indent); 267 | 268 | // Skip non-intended first characters. 269 | if (str[0] == ' ' || str[0] == '\n') 270 | str++; 271 | 272 | // Print all words that fit in the current line. 273 | int next_len; 274 | while (*str && (next_len = next_word_size(str)) <= chars_left) { 275 | for (int i = 0; i < next_len; i++) { 276 | if (*str == '\n') 277 | goto next_line; 278 | 279 | putc(*str, stream); 280 | str++; 281 | } 282 | chars_left -= next_len; 283 | } 284 | 285 | next_line: 286 | putc('\n', stream); 287 | start = 0; 288 | } 289 | } 290 | 291 | // Returns 1 if an option index is a valid short option character, otherwise 0. 292 | static inline int is_short_name(int index) 293 | { 294 | char *range = "abcdefghijklmnopqrstuvwxyz" 295 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 296 | "0123456789"; 297 | for (int i = 0; i < 62; i++) { 298 | if (index == range[i]) 299 | return 1; 300 | } 301 | 302 | return 0; 303 | } 304 | 305 | // Prints a single option. 306 | // Adjustments here must be done in print_opts(), too. 307 | static void print_opt(FILE *stream, const struct option *opt, int indent) 308 | { 309 | if (opt->description == HIDEOPT) 310 | return; 311 | 312 | if (opt->index > 0) { 313 | int short_name = is_short_name(opt->index); 314 | 315 | // 1 2 3 4 5 6 7 8 9 0 1 2 3 316 | int len = fprintf(stream, " %c%c%s%s%s%s%s%s%s%s%s%s%s", 317 | /* 1 */ short_name ? '-' : ' ', 318 | /* 2 */ short_name ? opt->index : ' ', 319 | /* 3 */ short_name && !opt->name && opt->arg ? opt->arg[0] == '[' ? opt->arg : " " : "", 320 | /* 4 */ short_name && !opt->name && opt->arg && opt->arg[0] != '[' ? opt->arg : "", 321 | /* 5 */ short_name && opt->name ? "," : short_name ? "" : " ", 322 | /* 6 */ opt->name ? " --" : "", 323 | /* 7 */ opt->name ? opt->name : "", 324 | /* 8 */ opt->name && opt->arg && opt->arg[0] != '[' ? " " : "", 325 | /* 9 */ opt->name && opt->arg && opt->arg[0] != '[' ? opt->arg : "", 326 | /* 0 */ opt->name && opt->arg && opt->arg[0] == '[' ? "[" : "", 327 | /* 1 */ opt->name && opt->arg && opt->arg[0] == '[' ? "=" : "", 328 | /* 2 */ opt->name && opt->arg && opt->arg[0] == '[' ? opt->arg + 1 : "", 329 | /* 3 */ opt->description ? " " : ""); 330 | 331 | if (opt->description) 332 | print_block(stream, opt->description, indent, len); 333 | else 334 | putc('\n', stream); 335 | } 336 | } 337 | 338 | // Prints the specified option array's options. 339 | // Returns 1 if the array has subcommands, otherwise 0. 340 | int print_options(FILE *stream, const struct option opts[]) 341 | { 342 | const struct option *opt = opts; 343 | int has_subcommands = 0; 344 | 345 | // Calculate option description indentation. 346 | // Adjustments here must be done in print_opt(), too. 347 | int indent = 0; 348 | while (opt->index) { 349 | if (opt->index > 0) { 350 | int len = 6; // Minimum indentation, " -x" and " .description". 351 | if (opt->name) 352 | len += 4 + strlen(opt->name); // 4: ", --" 353 | if (opt->arg) { 354 | len += strlen(opt->arg); 355 | if (opt->name || opt->arg[0] == '[') 356 | len++; 357 | } 358 | if (len > indent && len <= GETOPT_LINE_MAXLEN - GETOPT_BLCK_MINLEN) 359 | indent = len; 360 | } 361 | opt++; 362 | } 363 | 364 | // Print options. 365 | opt = opts; 366 | while (opt->index) { 367 | if (opt->index < 0) 368 | has_subcommands = 1; 369 | else 370 | print_opt(stream, opt, indent); 371 | opt++; 372 | } 373 | 374 | return has_subcommands; 375 | } 376 | 377 | // Prints a single subcommand. 378 | // Adjustments here must be done in print_subcommands(), too. 379 | static void print_subcmd(FILE *stream, const struct option *opt, int indent) 380 | { 381 | if (opt->index == 0 || opt->description == HIDEOPT) 382 | return; 383 | 384 | if (opt->index < 0) { 385 | int len = fprintf(stream, " %s%s%s%s", 386 | opt->name ? opt->name : "", 387 | opt->arg ? " " : "", 388 | opt->arg ? opt->arg : "", 389 | opt->description ? " ": ""); 390 | if (opt->description) 391 | print_block(stream, opt->description, indent, len); 392 | else 393 | putc('\n', stream); 394 | } 395 | } 396 | 397 | // Prints the specified option array's subcommands. 398 | void print_subcommands(FILE *stream, const struct option opts[]) 399 | { 400 | const struct option *opt = opts; 401 | 402 | // Calculate subcommand description indentation. 403 | // Adjustments here must be done in print_subcmd(), too. 404 | int indent = 0; 405 | while (opt->index) { 406 | if (opt->index < 0) { 407 | int len = 4; // Minimum indentation, " .name" and " .description". 408 | if (opt->name) 409 | len += strlen(opt->name); 410 | if (opt->arg) 411 | len += 1 + strlen(opt->arg); 412 | if (len > indent && len <= GETOPT_LINE_MAXLEN - GETOPT_BLCK_MINLEN) 413 | indent = len; 414 | } 415 | opt++; 416 | } 417 | 418 | // Print subcommands. 419 | opt = opts; 420 | while (opt->index) { 421 | if (opt->index < 0) 422 | print_subcmd(stream, opt, indent); 423 | opt++; 424 | } 425 | } 426 | 427 | #endif 428 | -------------------------------------------------------------------------------- /pkgrename.c/src/options.c: -------------------------------------------------------------------------------- 1 | #include "../include/common.h" 2 | #include "../include/colors.h" 3 | #include "../include/getopt.h" 4 | #include "../include/options.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | int option_compact; 11 | int option_disable_colors; 12 | int option_force; 13 | int option_force_backup; 14 | int option_mixed_case; 15 | int option_no_placeholder; 16 | int option_no_to_all; 17 | char option_language_number[3]; 18 | int option_leading_zeros; 19 | int option_online; 20 | int option_override_tags; 21 | int option_query; 22 | int option_recursive; 23 | char *option_tag_separator; 24 | int option_underscores; 25 | int option_verbose; 26 | int option_yes_to_all; 27 | 28 | enum long_only_options { 29 | OPT_DISABLE_COLORS = 256, 30 | OPT_NO_PLACEHOLDER, 31 | OPT_OVERRIDE_TAGS, 32 | OPT_PLACEHOLDER, 33 | OPT_PRINT_LANGS, 34 | OPT_PRINT_TAGS, 35 | OPT_SET_BACKPORT, 36 | OPT_SET_FAKE, 37 | OPT_SET_TYPE, 38 | OPT_TAGFILE, 39 | OPT_TAGS, 40 | OPT_TAG_SEPARATOR, 41 | OPT_VERSION, 42 | }; 43 | 44 | static struct option opts[] = { 45 | { 'c', "compact", NULL, "Hide files that are already renamed." }, 46 | #ifndef _WIN32 47 | { OPT_DISABLE_COLORS, "disable-colors", NULL, "Disable colored text output." }, 48 | #endif 49 | { 'f', "force", NULL, "Force-prompt even when file names match." }, 50 | { 'h', "help", NULL, "Print this help screen." }, 51 | { 'l', "language", "LANG", "If the PKG supports it, use the language specified by language code LANG (see --print-languages) to retrieve the PKG's title." }, 52 | { '0', "leading-zeros", NULL, "Show leading zeros in pattern variables %app_ver%, %firmware%, %merged_ver%, %sdk%, %true_ver%, %version%." }, 53 | { 'm', "mixed-case", NULL, "Automatically apply mixed-case letter style." }, 54 | { OPT_NO_PLACEHOLDER, "no-placeholder", NULL, "Hide characters instead of using placeholders." }, 55 | { 'n', "no-to-all", NULL, "Do not prompt; do not actually rename any files. This can be used to do a test run." }, 56 | { 'o', "online", NULL, "Automatically search online for %title%." }, 57 | { OPT_OVERRIDE_TAGS, "override-tags", NULL, "Make changelog release tags take precedence over existing file name tags." }, 58 | { 'p', "pattern", "PATTERN", "Set the file name pattern to string PATTERN." }, 59 | { OPT_PLACEHOLDER, "placeholder", "X", "Set the placeholder character to X." }, 60 | { OPT_PRINT_LANGS, "print-languages", NULL, "Print available language codes." }, 61 | { OPT_PRINT_TAGS, "print-tags", NULL, "Print all built-in release tags." }, 62 | { 'q', "query", NULL, "For scripts/tools: print file name suggestions, one per line, without renaming the files. A successful query returns exit code 0." }, 63 | { 'r', "recursive", NULL, "Traverse subdirectories recursively." }, 64 | { OPT_SET_BACKPORT, "set-backport", "STRING", "Set %backport% mapping to STRING." }, 65 | { OPT_SET_FAKE, "set-fake", "STRINGS", "Set %fake%, %fake_status%, and %retail% mappings to two comma-separated STRINGS. The first string replaces %fake%, the second one %retail%." }, 66 | { OPT_SET_TYPE, "set-type", "CATEGORIES", "Set %type% mapping to comma-separated string CATEGORIES (see section \"Pattern variables\")." }, 67 | { OPT_TAGFILE, "tagfile", "FILE", "Load additional %release% tags from text file FILE, one tag per line." }, 68 | { OPT_TAGS, "tags", "TAGS", "Load additional %release% tags from comma-separated string TAGS (no spaces before or after commas)." }, 69 | { OPT_TAG_SEPARATOR, "tag-separator", "SEP", "Use the string SEP instead of commas to separate multiple release tags." }, 70 | { 'u', "underscores", NULL, "Use underscores instead of spaces in file names." }, 71 | { 'v', "verbose", NULL, "Display additional infos." }, 72 | { OPT_VERSION, "version", NULL, "Print the current pkgrename version." }, 73 | { 'y', "yes-to-all", NULL, "Do not prompt; rename all files automatically." }, 74 | { 0 } 75 | }; 76 | 77 | void print_version(void) 78 | { 79 | printf("Version 1.09, build date: %s\n", __DATE__); 80 | printf("Get the latest version at " 81 | "\"%s\".\n", HOMEPAGE_LINK); 82 | printf("Report bugs, request features, or add missing tags at " 83 | "\"%s\".\n", SUPPORT_LINK); 84 | } 85 | 86 | void print_prompt_help(void) 87 | { 88 | fputs( 89 | " - [Y]es Rename the file as seen.\n" 90 | " - [N]o Skip the file and drop all changes.\n" 91 | " - [A]ll Same as yes, but also for all future files.\n" 92 | " - [E]dit Prompt to manually edit the title.\n" 93 | " - [T]ag Prompt to enter a release group or a release.\n" 94 | " - [M]ix Convert the letter case to mixed-case style.\n" 95 | " - [O]nline Search the PS Store online for title information.\n" 96 | " - [R]eset Undo all changes.\n" 97 | " - [C]hars Reveal special characters in the title.\n" 98 | " - [S]FO Show file's param.sfo information.\n" 99 | " - [L]og Print existing changelog data.\n" 100 | " - [H]elp Print help.\n" 101 | " - [Q]uit Exit the program.\n" 102 | " - [B] Toggle the \"Backport\" tag.\n" 103 | " - [P] Toggle changelog patch detection for the current PKG.\n" 104 | " - Shift-[T] Remove all release tags.\n" 105 | " - Backspace Go back to the previous PKG.\n" 106 | " - Space Return to the current PKG.\n" 107 | , stdout); 108 | } 109 | 110 | void print_usage(void) 111 | { 112 | fputs( 113 | "Usage: pkgrename [OPTIONS] [FILE|DIRECTORY ...]\n" 114 | "\n" 115 | "Renames PS4 PKGs to match a file name pattern. The default pattern is:\n" 116 | "\"%title% [%dlc%] [{v%app_ver%}{ + v%merged_ver%}] [%title_id%] [%release_group%] [%release%] [%backport%]\"\n" 117 | "\n" 118 | "Pattern variables:\n" 119 | "------------------\n" 120 | " Name Example\n" 121 | " ----------------------------------------------------------------------\n" 122 | " %app% \"App\"\n" 123 | " %app_ver% \"4.03\"\n" 124 | " %backport% \"Backport\" (1)\n" 125 | " %category% \"gp\"\n" 126 | " %content_id% \"EP4497-CUSA05571_00-00000000000GOTY1\"\n" 127 | " %dlc% \"DLC\"\n" 128 | " %fake% \"Fake\" (5)\n" 129 | " %fake_status% \"Fake\" (5)\n" 130 | " %file_id% \"EP4497-CUSA05571_00-00000000000GOTY1-A0403-V0100\"\n" 131 | " %firmware% \"10.01\"\n" 132 | " %game% \"Game\"\n" 133 | " %merged_ver% \"\" (2)\n" 134 | " %msum% \"3E57B0\" (3)\n" 135 | " %other% \"Other\"\n" 136 | " %patch% \"Update\"\n" 137 | " %region% \"EU\"\n" 138 | " %release_group% \"PRELUDE\" (1)\n" 139 | " %release% \"John Doe\" (1)\n" 140 | " %retail% \"\" (5)\n" 141 | " %sdk% \"4.50\"\n" 142 | " %size% \"19.34 GiB\"\n" 143 | " %title% \"The Witcher 3: Wild Hunt – Game of the Year Edition\"\n" 144 | " %title_id% \"CUSA05571\"\n" 145 | " %true_ver% \"4.03\" (2)\n" 146 | " %type% \"Update\" (4)\n" 147 | " %version% \"1.00\"\n" 148 | "\n" 149 | " (1) Backports not targeting 5.05 are detected by searching file names for the\n" 150 | " words \"BP\" and \"Backport\" (case-insensitive). The same principle applies to\n" 151 | " release groups and releases.\n" 152 | "\n" 153 | " (2) Patches and apps merged with patches are detected by searching PKG files\n" 154 | " for changelog information. If a patch is found, both %merged_ver% and\n" 155 | " %true_ver% are the patch version. If no patch is found or if patch detection\n" 156 | " is disabled (command [P]), %merged_ver% is empty and %true_ver% is %app_ver%.\n" 157 | " %merged_ver% is always empty for non-app PKGs.\n" 158 | "\n" 159 | " (3) A checksum that indicates whether game and update PKGs that have the\n" 160 | " same Title ID are compatible with each other (\"married\"). This pattern\n" 161 | " variable will be empty for PKGs of other types.\n" 162 | "\n" 163 | " (4) %type% is %category% mapped to \"Game,Update,DLC,App,Other\".\n" 164 | " These five default strings can be changed via option \"--set-type\", e.g.:\n" 165 | " --set-type \"Game,Patch %app_ver%,DLC,-,-\" (no spaces before or after commas)\n" 166 | " Each string must have a value. To hide a category, use the value \"-\".\n" 167 | " %app%, %dlc%, %game%, %other%, and %patch% are mapped to their corresponding\n" 168 | " %type% values. They will be displayed if the PKG is of that specific category.\n" 169 | "\n" 170 | " (5) These pattern variables depend on the type of the PKG:\n" 171 | " PKG type %fake% %retail% %fake_status%\n" 172 | " \"Fake\" PKG (FPKG) Fake Fake\n" 173 | " Retail PKG Retail Retail\n" 174 | "\n" 175 | " After parsing, empty pairs of brackets, empty pairs of parentheses, and any\n" 176 | " remaining curly braces (\"[]\", \"()\", \"{\", \"}\") will be removed.\n" 177 | "\n" 178 | "Curly braces expressions:\n" 179 | "-------------------------\n" 180 | " Pattern variables and other strings can be grouped together by surrounding\n" 181 | " them with curly braces. If an inner pattern variable turns out to be empty,\n" 182 | " the whole curly braces expression will be removed.\n" 183 | "\n" 184 | " Example 1 - %firmware% is empty:\n" 185 | " \"%title% [FW %firmware%]\" => \"Example DLC [FW ].pkg\" WRONG\n" 186 | " \"%title% [{FW %firmware%}]\" => \"Example DLC.pkg\" CORRECT\n" 187 | "\n" 188 | " Example 2 - %firmware% has a value:\n" 189 | " \"%title% [{FW %firmware%}]\" => \"Example Game [FW 7.55].pkg\"\n" 190 | "\n" 191 | "Handling of special characters:\n" 192 | "-------------------------------\n" 193 | " - For exFAT compatibility, some characters are replaced by a placeholder\n" 194 | " character (default: underscore).\n" 195 | " - Some special characters like copyright symbols are automatically removed\n" 196 | " or replaced by more common alternatives.\n" 197 | " - Numbers appearing in parentheses behind a file name indicate the presence\n" 198 | " of non-ASCII characters.\n" 199 | "\n" 200 | "Interactive prompt:\n" 201 | "-------------------\n" 202 | , stdout); 203 | print_prompt_help(); 204 | fputs("\nOptions:\n" 205 | "--------\n", stdout); 206 | print_options(stdout, opts); 207 | } 208 | 209 | // Option functions ------------------------------------------------------------ 210 | 211 | static inline void optf_pattern(char *pattern) 212 | { 213 | size_t len = strlen(pattern); 214 | if (len >= MAX_FORMAT_STRING_LEN) { 215 | #ifdef _WIN64 216 | fprintf(stderr, "Pattern too long (%llu/%d characters).\n", len, 217 | #elif defined(_WIN32) 218 | fprintf(stderr, "Pattern too long (%u/%d characters).\n", len, 219 | #else 220 | fprintf(stderr, "Pattern too long (%lu/%d characters).\n", len, 221 | #endif 222 | MAX_FORMAT_STRING_LEN - 1); 223 | exit(EXIT_FAILURE); 224 | } 225 | strcpy(format_string, pattern); 226 | } 227 | 228 | static struct lang { 229 | unsigned char number; 230 | char *name; 231 | char *identifier; 232 | } langs[] = { 233 | { 0, "Japanese", "jp" }, 234 | { 1, "English (United States)", "en" }, 235 | { 2, "French (France)", "fr" }, 236 | { 3, "Spanish (Spain)", "es" }, 237 | { 4, "German", "de" }, 238 | { 5, "Italian", "it" }, 239 | { 6, "Dutch", "nl" }, 240 | { 7, "Portuguese (Portugal)", "pt" }, 241 | { 8, "Russian", "ru" }, 242 | { 9, "Korean", "ko" }, 243 | { 10, "Chinese (traditional)", "zh_t" }, 244 | { 11, "Chinese (simplified)", "zh_s" }, 245 | { 12, "Finnish", "fi" }, 246 | { 13, "Swedish", "sv" }, 247 | { 14, "Danish", "da" }, 248 | { 15, "Norwegian", "no" }, 249 | { 16, "Polish", "pl" }, 250 | { 17, "Portuguese (Brazil)", "pt-br" }, 251 | { 18, "English (United Kingdom)", "en-gb" }, 252 | { 19, "Turkish", "tr" }, 253 | { 20, "Spanish (Latin America)", "es-la" }, 254 | { 21, "Arabic", "ar" }, 255 | { 22, "French (Canada)", "fr-ca" }, 256 | { 23, "Czech", "cs" }, 257 | { 24, "Hungarian", "hu" }, 258 | { 25, "Greek", "el" }, 259 | { 26, "Romanian", "ro" }, 260 | { 27, "Thai", "th" }, 261 | { 28, "Vietnamese", "vi" }, 262 | { 29, "Indonesian", "in" }, 263 | }; 264 | 265 | static int compar_lang(const void *l1, const void *l2) 266 | { 267 | return strcmp(((struct lang *) l1)->name, ((struct lang *) l2)->name); 268 | } 269 | 270 | static inline void optf_print_languages(void) 271 | { 272 | qsort(langs, sizeof(langs) / sizeof(langs[0]), sizeof(langs[0]), 273 | compar_lang); 274 | printf("Code\tLanguage\n\n"); 275 | for (size_t i = 0; i < sizeof(langs) / sizeof(struct lang); i++) { 276 | printf("%s\t%s\n", langs[i].identifier, langs[i].name); 277 | } 278 | } 279 | 280 | // Returns n_pieces on success and 0 on error. 281 | static int split_string(char *string, char *delims, char *array[], int n_pieces) 282 | { 283 | char *next = strtok(string, delims); 284 | int i; 285 | for (i = 0; i < n_pieces; i++) { 286 | if (next == NULL) 287 | return 0; 288 | 289 | array[i] = next; 290 | next = strtok(NULL, delims); 291 | } 292 | 293 | if (next) 294 | return 0; 295 | return i; 296 | } 297 | 298 | static inline void optf_set_fake(char *arg) 299 | { 300 | char *input[2]; 301 | if (split_string(arg, ",", input, 2) == 0) { 302 | fprintf(stderr, "The argument to option --set-fake must consist of exactly 2 comma-separated parts.\n"); 303 | exit(EXIT_FAILURE); 304 | } 305 | 306 | FAKE_STRING = input[0]; 307 | RETAIL_STRING = input[1]; 308 | } 309 | 310 | static inline void optf_set_type(char *arg) 311 | { 312 | char *input[5]; 313 | if (split_string(arg, ",", input, 5) == 0) { 314 | fprintf(stderr, 315 | "The argument to option --set-type must consist of exactly 5 comma-separated parts.\n"); 316 | exit(EXIT_FAILURE); 317 | } 318 | 319 | for (int i = 0; i < 5; i++) 320 | if (strcmp(input[i], "-") == 0) 321 | input[i] = ""; 322 | 323 | custom_category.game = input[0]; 324 | custom_category.patch = input[1]; 325 | custom_category.dlc = input[2]; 326 | custom_category.app = input[3]; 327 | custom_category.other = input[4]; 328 | } 329 | 330 | static inline void optf_tagfile(char *file_name) 331 | { 332 | FILE *tagfile = fopen(file_name, "r"); 333 | if (!tagfile) { 334 | fprintf(stderr, "Option --tagfile: File not found: \"%s\".\n", 335 | file_name); 336 | exit(EXIT_FAILURE); 337 | } 338 | 339 | char buf[MAX_TAG_LEN + 1]; 340 | while (fgets(buf, sizeof buf, tagfile) && tagc < MAX_TAGS) { 341 | buf[strcspn(buf, "\r\n")] = '\0'; 342 | if (buf[0] == '\0') 343 | continue; 344 | tags[tagc] = realloc(tags[tagc], strlen(buf) + 1); 345 | memcpy(tags[tagc], buf, strlen(buf) + 1); 346 | tagc++; 347 | } 348 | 349 | fclose(tagfile); 350 | } 351 | 352 | static inline void optf_tags(char *taglist) 353 | { 354 | char *p = strtok(taglist, ","); 355 | while (p && tagc < MAX_TAGS) { 356 | tags[tagc] = realloc(tags[tagc], strlen(p) + 1); 357 | memcpy(tags[tagc], p, strlen(p) + 1); 358 | tagc++; 359 | p = strtok(NULL, ","); 360 | } 361 | } 362 | 363 | // ----------------------------------------------------------------------------- 364 | 365 | void parse_options(int *argc, char **argv[]) 366 | { 367 | int opt; 368 | char *optarg; 369 | while ((opt = getopt(argc, argv, &optarg, opts)) != 0) { 370 | switch (opt) { 371 | case 'c': 372 | option_compact = 1; 373 | break; 374 | #ifndef _WIN32 375 | case OPT_DISABLE_COLORS: 376 | option_disable_colors = 1; 377 | break; 378 | #endif 379 | case 'f': 380 | option_force = 1; 381 | option_force_backup = 1; 382 | break; 383 | case 'h': 384 | print_usage(); 385 | exit(EXIT_SUCCESS); 386 | case 'l': 387 | for (size_t i = 0; i < sizeof(langs) / sizeof(langs[0]); i++) { 388 | if (strcmp(optarg, langs[i].identifier) == 0) { 389 | #pragma GCC diagnostic push 390 | #pragma GCC diagnostic ignored "-Wformat-truncation" 391 | snprintf(option_language_number, 3, "%02d", langs[i].number); 392 | #pragma GCC diagnostic pop 393 | goto language_found; 394 | } 395 | } 396 | 397 | fprintf(stderr, "Unknown language code: %s\n", optarg); 398 | exit(EXIT_FAILURE); 399 | language_found: 400 | break; 401 | case '0': 402 | option_leading_zeros = 1; 403 | break; 404 | case 'm': 405 | option_mixed_case = 1; 406 | break; 407 | case OPT_NO_PLACEHOLDER: 408 | option_no_placeholder = 1; 409 | break; 410 | case 'n': 411 | option_no_to_all = 1; 412 | break; 413 | case 'o': 414 | option_online = 1; 415 | break; 416 | case OPT_OVERRIDE_TAGS: 417 | option_override_tags = 1; 418 | break; 419 | case 'p': 420 | optf_pattern(optarg); 421 | break; 422 | case OPT_PLACEHOLDER: 423 | placeholder_char = optarg[0]; 424 | break; 425 | case OPT_PRINT_LANGS: 426 | optf_print_languages(); 427 | exit(EXIT_SUCCESS); 428 | case OPT_PRINT_TAGS: 429 | ; 430 | extern void print_database(); 431 | print_database(); 432 | exit(EXIT_SUCCESS); 433 | case 'q': 434 | option_query = 1; 435 | break; 436 | case 'r': 437 | option_recursive = 1; 438 | break; 439 | case OPT_SET_BACKPORT: 440 | BACKPORT_STRING = optarg; 441 | break; 442 | case OPT_SET_FAKE: 443 | optf_set_fake(optarg); 444 | break; 445 | case OPT_SET_TYPE: 446 | optf_set_type(optarg); 447 | break; 448 | case OPT_TAGFILE: 449 | optf_tagfile(optarg); 450 | break; 451 | case OPT_TAGS: 452 | optf_tags(optarg); 453 | break; 454 | case OPT_TAG_SEPARATOR: 455 | option_tag_separator = optarg; 456 | break; 457 | case 'u': 458 | option_underscores = 1; 459 | break; 460 | case 'v': 461 | option_verbose = 1; 462 | break; 463 | case OPT_VERSION: 464 | print_version(); 465 | exit(EXIT_SUCCESS); 466 | case 'y': 467 | option_yes_to_all = 1; 468 | break; 469 | default: 470 | exit(EXIT_FAILURE); 471 | } 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /pkgrename.c/pkgrename.c: -------------------------------------------------------------------------------- 1 | #define _FILE_OFFSET_BITS 64 2 | 3 | #ifndef _WIN32 4 | #define _GNU_SOURCE // For strcasestr(), which is not standard. 5 | #endif 6 | 7 | #include "include/characters.h" 8 | #include "include/colors.h" 9 | #include "include/common.h" 10 | #include "include/onlinesearch.h" 11 | #include "include/options.h" 12 | #include "include/pkg.h" 13 | #include "include/releaselists.h" 14 | #include "include/scan.h" 15 | #include "include/strings.h" 16 | #include "include/terminal.h" 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #ifdef _WIN32 29 | #include // For getch(). 30 | #include 31 | #define strcasestr StrStrIA 32 | #define DIR_SEPARATOR '\\' 33 | #else 34 | #include // For __fpurge(); requires the GNU C Library. 35 | #define DIR_SEPARATOR '/' 36 | #endif 37 | 38 | char format_string[MAX_FORMAT_STRING_LEN] = 39 | "%title% [%dlc%] [{v%app_ver%}{ + v%merged_ver%}] [%title_id%] [%release_group%] [%release%] [%backport%]"; 40 | struct custom_category custom_category = 41 | {"Game", "Update", "DLC", "App", "Other"}; 42 | char *tags[MAX_TAGS]; 43 | int tagc; 44 | int multiple_directories; // If 1, pkgrename() prints dir names on dir change. 45 | 46 | static void rename_file(char *filename, char *new_basename, char *path) 47 | { 48 | FILE *file; 49 | char new_filename[MAX_FILENAME_LEN]; 50 | 51 | strcpy(new_filename, path); 52 | strcat(new_filename, new_basename); 53 | 54 | file = fopen(new_filename, "rb"); 55 | 56 | // File already exists. 57 | if (file != NULL) { 58 | fclose(file); 59 | // Use temporary file to force-rename on (case-insensitive) exFAT. 60 | if (lower_strcmp(filename, new_filename) == 0) { 61 | char temp[MAX_FILENAME_LEN]; 62 | strcpy(temp, new_filename); 63 | strcat(temp, ".pkgrename"); 64 | if (rename(filename, temp)) goto error; 65 | if (rename(temp, new_filename)) goto error; 66 | } else { 67 | fprintf(stderr, "File already exists: \"%s\".\n", new_filename); 68 | exit(EXIT_FAILURE); 69 | } 70 | // File does not exist yet. 71 | } else { 72 | if (rename(filename, new_filename)) goto error; 73 | } 74 | 75 | return; 76 | 77 | error: 78 | fprintf(stderr, "Could not rename file %s.\n", filename); 79 | exit(EXIT_FAILURE); 80 | } 81 | 82 | // Returns the size of a file in bytes or -1 on error. 83 | static ssize_t get_file_size(const char *filename) 84 | { 85 | FILE *file = fopen(filename, "rb"); 86 | if (file == NULL) 87 | return -1; 88 | if (fseek(file, 0, SEEK_END)) { 89 | fclose(file); 90 | return -1; 91 | } 92 | ssize_t ret = ftello(file); 93 | fclose(file); 94 | return ret; 95 | } 96 | 97 | // Companion function for pkgrename(). 98 | // Prints a message if a path has changed during pkgrename() calls. 99 | static void print_dir_change(const char *path) 100 | { 101 | #ifdef _WIN32 102 | #define realpath(name, resolved) _fullpath(resolved, name, PATH_MAX) 103 | #endif 104 | static char old_realpath[PATH_MAX]; 105 | static char new_realpath[PATH_MAX]; 106 | 107 | if (realpath(*path ? path : ".", new_realpath) == NULL) 108 | return; 109 | 110 | if (strcmp(new_realpath, old_realpath) == 0) 111 | return; 112 | 113 | set_color(GRAY, stdout); 114 | printf("Current directory: %s\n\n", new_realpath); 115 | set_color(RESET, stdout); 116 | 117 | strcpy(old_realpath, new_realpath); 118 | #ifdef _WIN32 119 | #undef realpath 120 | #endif 121 | } 122 | 123 | // Uses information retreived by a previous scan to rename a PS4 PKG file. 124 | // The .next member may not be accessed without a mutex lock. 125 | // Returns NULL or a pointer to a scan it needs to be called again with. 126 | static struct scan *pkgrename(struct scan *scan) 127 | { 128 | static int first_run = 1; // Used to decide when to print newlines. 129 | static struct scan *scan_backup; // Used to return to current scan (space). 130 | static int error_streak = 0; 131 | 132 | // Don't proceed if the scan describes an error. 133 | if (scan->error) { 134 | if (option_query == 1) { 135 | // Ignore error and print the original file name. 136 | printf("%s\n", scan->filename); 137 | return NULL; 138 | } 139 | 140 | // Make sure to print consecutive errors as a block. 141 | if (error_streak == 0) { 142 | error_streak = 1; 143 | if (first_run == 1) 144 | first_run = 0; 145 | else 146 | putchar('\n'); 147 | } 148 | 149 | print_scan_error(scan); 150 | 151 | // Mark error scan as seen so that it won't be displayed again. 152 | if (scan->filename_allocated) 153 | free(scan->filename); 154 | scan->filename = NULL; 155 | 156 | return NULL; 157 | } else { 158 | error_streak = 0; 159 | } 160 | 161 | // Reset option_force if the current PKG is reached again. 162 | if (scan_backup && scan == scan_backup) { 163 | scan_backup = NULL; 164 | option_force = option_force_backup; 165 | } 166 | 167 | if (option_query == 0 && option_compact == 0) { 168 | if (first_run == 1) 169 | first_run = 0; 170 | else 171 | putchar('\n'); 172 | } 173 | 174 | char new_basename[MAX_FORMAT_STRING_LEN]; // Used to build the new filename. 175 | char *filename = scan->filename; 176 | char *basename; // "filename" without path. 177 | char lowercase_basename[MAX_FILENAME_LEN]; 178 | char path[PATH_MAX]; // "filename" without file. 179 | char tag_release_group[MAX_TAG_LEN + 1] = ""; 180 | char tag_release[MAX_TAG_LEN + 1] = ""; 181 | int spec_chars_current, spec_chars_total; 182 | int prompted_once = 0; 183 | int changelog_patch_detection = 1; 184 | int print_ambiguity_warning = 0; 185 | 186 | // Internal pattern variables. 187 | char *app = NULL; 188 | char *app_ver = NULL; 189 | char *backport = NULL; 190 | char *category = NULL; 191 | char *content_id = NULL; 192 | char *dlc = NULL; 193 | char *fake = NULL; 194 | char *fake_status = NULL; 195 | char file_id_suffix[13] = "-A0000-V0000"; 196 | char firmware[9] = ""; 197 | char *game = NULL; 198 | char true_ver_buf[5] = ""; 199 | char *merged_ver = NULL; 200 | char msum[7] = ""; 201 | char *other = NULL; 202 | char *patch = NULL; 203 | char *region = NULL; 204 | char *release_group = NULL; 205 | char *release = NULL; 206 | char *retail = NULL; 207 | char sdk[6] = ""; 208 | char size[10]; 209 | char title[MAX_TITLE_LEN] = ""; 210 | char *title_backup = NULL; 211 | char *title_id = NULL; 212 | char *true_ver = NULL; 213 | char *type = NULL; 214 | char *version = NULL; 215 | 216 | // Define the file's basename and path. 217 | basename = strrchr(filename, DIR_SEPARATOR); 218 | if (basename == NULL) { 219 | basename = filename; 220 | path[0] = '.'; 221 | path[1] = DIR_SEPARATOR; 222 | path[2] = '\0'; 223 | } else { 224 | basename++; 225 | strncpy(path, filename, (basename - filename)); 226 | path[basename - filename] = '\0'; 227 | } 228 | 229 | // Print directory if it's different (early). 230 | if (multiple_directories && option_compact == 0) 231 | print_dir_change(path); 232 | 233 | // Create a lowercase copy of "basename". 234 | strcpy(lowercase_basename, basename); 235 | for (size_t i = 0; i < strlen(lowercase_basename); i++) 236 | lowercase_basename[i] = tolower(lowercase_basename[i]); 237 | 238 | // Print current basename (early). 239 | if (option_query == 0 && option_compact == 0) 240 | printf(" \"%s\"\n", basename); 241 | 242 | // Load PKG data. 243 | unsigned char *param_sfo = scan->param_sfo; 244 | char *changelog = scan->changelog; 245 | // APP_VER 246 | app_ver = (char *) get_param_sfo_value(param_sfo, "APP_VER"); 247 | if (app_ver && strlen(app_ver) >= 3) { 248 | if (app_ver[2] == '.' && strlen(app_ver) >= 5) { 249 | file_id_suffix[2] = app_ver[0]; 250 | file_id_suffix[3] = app_ver[1]; 251 | file_id_suffix[4] = app_ver[3]; 252 | file_id_suffix[5] = app_ver[4]; 253 | } else if (app_ver[1] == '.') { // Some homebrew apps got it wrong. 254 | file_id_suffix[2] = '0'; 255 | file_id_suffix[3] = app_ver[0]; 256 | file_id_suffix[4] = app_ver[2]; 257 | if (app_ver[3]) 258 | file_id_suffix[5] = app_ver[3]; 259 | else 260 | file_id_suffix[5] = '0'; 261 | } 262 | } 263 | if (app_ver && option_leading_zeros == 0 && app_ver[0] == '0') 264 | app_ver++; 265 | // CATEGORY 266 | category = (char *) get_param_sfo_value(param_sfo, "CATEGORY"); 267 | if (category) { 268 | if (strcmp(category, "gd") == 0) { 269 | type = custom_category.game; 270 | game = type; 271 | } else if (strstr(category, "gp") != NULL) { 272 | type = custom_category.patch; 273 | patch = type; 274 | } else if (strcmp(category, "ac") == 0) { 275 | type = custom_category.dlc; 276 | dlc = type; 277 | } else if (category[0] == 'g' && category[1] == 'd') { 278 | type = custom_category.app; 279 | app = type; 280 | } else { 281 | type = custom_category.other; 282 | other = type; 283 | } 284 | } 285 | // CONTENT_ID 286 | content_id = (char *) get_param_sfo_value(param_sfo, "CONTENT_ID"); 287 | if (content_id) { 288 | switch (content_id[0]) { 289 | case 'E': region = "EU"; break; 290 | case 'H': region = "AS"; break; 291 | case 'I': region = "IN"; break; 292 | case 'J': region = "JP"; break; 293 | case 'U': region = "US"; break; 294 | } 295 | } 296 | // PUBTOOLINFO 297 | char *pubtoolinfo = (char *) get_param_sfo_value(param_sfo, "PUBTOOLINFO"); 298 | if (pubtoolinfo) { 299 | char *p = strstr(pubtoolinfo, "sdk_ver="); 300 | if (p) { 301 | p += 8; 302 | memcpy(sdk, p, 4); 303 | if (sdk[0] == '0' && option_leading_zeros == 0) { 304 | sdk[0] = sdk[1]; 305 | sdk[1] = '.'; 306 | sdk[4] = '\0'; 307 | } else { 308 | sdk[4] = sdk[3]; 309 | sdk[3] = sdk[2]; 310 | sdk[2] = '.'; 311 | } 312 | } 313 | } 314 | // SYSTEM_VER 315 | uint32_t *system_ver = (uint32_t *) get_param_sfo_value(param_sfo, 316 | "SYSTEM_VER"); 317 | if (system_ver) { 318 | sprintf(firmware, "%08x", *system_ver); 319 | if (firmware[0] == '0' && option_leading_zeros == 0) { 320 | firmware[0] = firmware[1]; 321 | firmware[1] = '.'; 322 | firmware[4] = '\0'; 323 | } else { 324 | firmware[5] = '\0'; 325 | firmware[4] = firmware[3]; 326 | firmware[3] = firmware[2]; 327 | firmware[2] = '.'; 328 | } 329 | } 330 | // TITLE 331 | if (option_language_number[0] != '\0') { 332 | char query[9]; 333 | snprintf(query, sizeof(query), "TITLE_%s", option_language_number); 334 | title_backup = (char *) get_param_sfo_value(param_sfo, query); 335 | if (title_backup) 336 | goto title_found; 337 | } 338 | title_backup = (char *) get_param_sfo_value(param_sfo, "TITLE"); 339 | if (title_backup) { 340 | title_found: 341 | strncpy(title, title_backup, MAX_TITLE_LEN); 342 | title[MAX_TITLE_LEN - 1] = '\0'; 343 | } 344 | // TITLE_ID 345 | title_id = (char *) get_param_sfo_value(param_sfo, "TITLE_ID"); 346 | // VERSION 347 | version = (char *) get_param_sfo_value(param_sfo, "VERSION"); 348 | if (version && strlen(version) >= 3) { 349 | if (version[2] == '.' && strlen(version) >= 5) { 350 | file_id_suffix[8] = version[0]; 351 | file_id_suffix[9] = version[1]; 352 | file_id_suffix[10] = version[3]; 353 | file_id_suffix[11] = version[4]; 354 | } else if (version[1] == '.') { // Some homebrew apps got it wrong. 355 | file_id_suffix[8] = '0'; 356 | file_id_suffix[9] = version[0]; 357 | file_id_suffix[10] = version[2]; 358 | if (version[3]) 359 | file_id_suffix[11] = version[3]; 360 | else 361 | file_id_suffix[11] = '0'; 362 | } 363 | } 364 | if (option_leading_zeros == 0 && version[0] == '0') 365 | version++; 366 | 367 | // Handle fake status. 368 | if (scan->fake_status) { 369 | fake = FAKE_STRING; 370 | retail = ""; 371 | fake_status = FAKE_STRING; 372 | } else { 373 | fake = ""; 374 | retail = RETAIL_STRING; 375 | fake_status = RETAIL_STRING; 376 | } 377 | 378 | // Get compatibility checksum. 379 | if (strstr(format_string, "%msum%")) 380 | get_checksum(msum, filename); 381 | 382 | // Detect changelog patch level. 383 | if (changelog && store_patch_version(true_ver_buf, changelog)) { 384 | if (option_leading_zeros == 0 && true_ver_buf[0] == '0') 385 | true_ver = true_ver_buf + 1; 386 | else 387 | true_ver = true_ver_buf; 388 | if (category[1] == 'd' && strcmp(true_ver_buf, "01.00") != 0) 389 | merged_ver = true_ver; 390 | } else { 391 | true_ver = app_ver; 392 | } 393 | 394 | // Detect backport. 395 | if ((category && category[0] == 'g' && category[1] == 'p' 396 | && ((option_leading_zeros == 0 && strcmp(sdk, "5.05") == 0 ) 397 | || (option_leading_zeros == 1 && strcmp(sdk, "05.05") == 0))) 398 | || strwrd(basename, BACKPORT_STRING) 399 | || strstr(lowercase_basename, "backport") 400 | || strwrd(lowercase_basename, "bp") 401 | || (changelog && changelog[0] ? strcasestr(changelog, "backport") : 0)) 402 | { 403 | backport = BACKPORT_STRING; 404 | } 405 | 406 | // Detect releases. 407 | if (strstr(format_string, "%release_group%")) 408 | release_group = get_release_group(lowercase_basename); 409 | if (strstr(format_string, "%release%")) { 410 | int n = get_release(&release, lowercase_basename); 411 | if (changelog && (release == NULL || option_override_tags == 1)) { 412 | n = get_release(&release, changelog); 413 | if (n > 1) { 414 | // Remove all tags but the 1st. 415 | // Note: if there ever is demand, this line can be removed to 416 | // automatically retreive all tags from the changelog. 417 | *(strchr(release, ',')) = '\0'; 418 | 419 | if (! option_query) 420 | print_ambiguity_warning = 1; 421 | } 422 | } 423 | 424 | if (release && n > 1 && option_tag_separator) 425 | replace_commas_in_tag(release, option_tag_separator); 426 | } 427 | 428 | // Get file size in GiB. 429 | if (strstr(format_string, "%size%")) { 430 | ssize_t file_size = get_file_size(filename); 431 | if (file_size == -1) { 432 | fprintf(stderr, "Error while getting the size of file \"%s\".\n", 433 | filename); 434 | exit(EXIT_FAILURE); 435 | } 436 | 437 | snprintf(size, sizeof(size), "%.2f GiB", file_size / 1073741824.0); 438 | } 439 | 440 | // Option "online". 441 | if (option_online == 1) { 442 | if (option_compact) 443 | search_online(content_id, title, 1); // Silent search. 444 | else 445 | search_online(content_id, title, 0); 446 | } 447 | 448 | // Option "mixed-case". 449 | if (option_mixed_case == 1) 450 | mixed_case(title); 451 | 452 | // User input loop. 453 | int first_loop = 1; 454 | char c; 455 | while(1) { 456 | /*********************************************************************** 457 | * Build new file name 458 | ***********************************************************************/ 459 | 460 | // Replace pattern variables. 461 | strncpy(new_basename, format_string, sizeof(new_basename) - 1); 462 | new_basename[sizeof(new_basename) - 1] = '\0'; 463 | // First, variables that do or may contain other pattern variables. 464 | strreplace(new_basename, "%type%", type); 465 | strreplace(new_basename, "%app%", app); 466 | strreplace(new_basename, "%dlc%", dlc); 467 | strreplace(new_basename, "%game%", game); 468 | strreplace(new_basename, "%other%", other); 469 | strreplace(new_basename, "%patch%", patch); 470 | strreplace(new_basename, "%file_id%", "%content_id%%file_id_suffix%"); 471 | 472 | strreplace(new_basename, "%app_ver%", app_ver); 473 | strreplace(new_basename, "%backport%", backport); 474 | strreplace(new_basename, "%category%", category); 475 | strreplace(new_basename, "%content_id%", content_id); 476 | strreplace(new_basename, "%fake%", fake); 477 | strreplace(new_basename, "%fake_status%", fake_status); 478 | strreplace(new_basename, "%file_id_suffix%", file_id_suffix); 479 | strreplace(new_basename, "%firmware%", firmware); 480 | strreplace(new_basename, "%merged_ver%", merged_ver); 481 | strreplace(new_basename, "%msum%", msum); 482 | strreplace(new_basename, "%region%", region); 483 | if (tag_release_group[0] != '\0') 484 | strreplace(new_basename, "%release_group%", tag_release_group); 485 | else 486 | strreplace(new_basename, "%release_group%", release_group); 487 | if (tag_release[0] != '\0') 488 | strreplace(new_basename, "%release%", tag_release); 489 | else 490 | strreplace(new_basename, "%release%", release); 491 | strreplace(new_basename, "%retail%", retail); 492 | strreplace(new_basename, "%sdk%", sdk); 493 | if (strstr(format_string, "%size%")) 494 | strreplace(new_basename, "%size%", size); 495 | strreplace(new_basename, "%title%", title); 496 | strreplace(new_basename, "%title_id%", title_id); 497 | strreplace(new_basename, "%true_ver%", true_ver); 498 | strreplace(new_basename, "%version%", version); 499 | 500 | // Remove empty brackets and parentheses, and curly braces. 501 | while (strreplace(new_basename, "[]", "") != NULL) 502 | ; 503 | while (strreplace(new_basename, "()", "") != NULL) 504 | ; 505 | while (strreplace(new_basename, "{", "") != NULL) 506 | ; 507 | while (strreplace(new_basename, "}", "") != NULL) 508 | ; 509 | 510 | // Replace illegal characters. 511 | replace_illegal_characters(new_basename); 512 | 513 | spec_chars_total = count_spec_chars(new_basename); 514 | 515 | // Replace misused special characters. 516 | if (strreplace(new_basename, "&", "&")) spec_chars_current--; 517 | if (strreplace(new_basename, "’", "'")) spec_chars_current--; 518 | if (strreplace(new_basename, " ", " ")) spec_chars_current--; 519 | if (strreplace(new_basename, "Ⅲ", "III")) spec_chars_current--; 520 | 521 | // Replace potentially annoying special characters. 522 | if (strreplace(new_basename, "™_", "_")) spec_chars_current--; 523 | if (strreplace(new_basename, "™", " ")) spec_chars_current--; 524 | if (strreplace(new_basename, "®_", "_")) spec_chars_current--; 525 | if (strreplace(new_basename, "®", " ")) spec_chars_current--; 526 | if (strreplace(new_basename, "–", "-")) spec_chars_current--; 527 | 528 | spec_chars_current = count_spec_chars(new_basename); 529 | 530 | // Remove any number of repeated spaces. 531 | while (strreplace(new_basename, " ", " ") != NULL) 532 | ; 533 | 534 | // Remove leading whitespace. 535 | char *p; 536 | p = new_basename; 537 | while (isspace(p[0])) 538 | p++; 539 | memmove(new_basename, p, MAX_FILENAME_LEN); 540 | 541 | // Remove trailing whitespace. 542 | p = new_basename + strlen(new_basename) - 1; 543 | while (isspace(p[0])) 544 | p[0] = '\0'; 545 | 546 | // Option --underscores: replace all whitespace with underscores. 547 | if (option_underscores == 1) { 548 | p = new_basename; 549 | while (*p != '\0') { 550 | if (isspace(*p)) 551 | *p = '_'; 552 | p++; 553 | } 554 | } 555 | 556 | strcat(new_basename, ".pkg"); 557 | 558 | /**********************************************************************/ 559 | 560 | // Print current basename (late). 561 | if (option_query == 0 && option_compact == 1 && first_loop == 1) { 562 | if (option_force == 0 && strcmp(basename, new_basename) == 0) { 563 | goto exit; 564 | } else { 565 | if (first_run) 566 | first_run = 0; 567 | else 568 | putchar('\n'); 569 | 570 | // Print directory if it's different (late). 571 | if (multiple_directories) 572 | print_dir_change(path); 573 | 574 | printf(" \"%s\"\n", basename); 575 | } 576 | first_loop = 0; 577 | } 578 | 579 | // Print new basename. 580 | if (option_query == 1) { 581 | printf("%s\n", new_basename); 582 | return NULL; 583 | } 584 | printf("=> \"%s\"", new_basename); 585 | 586 | // Print number of special characters. 587 | if (option_verbose) { 588 | set_color(BRIGHT_YELLOW, stdout); 589 | printf(" (%d/%d)", spec_chars_current, spec_chars_total); 590 | set_color(RESET, stdout); 591 | } else if (spec_chars_current) { 592 | set_color(BRIGHT_YELLOW, stdout); 593 | printf(" (%d)", spec_chars_current); 594 | set_color(RESET, stdout); 595 | } 596 | putchar('\n'); 597 | 598 | // Warn if new basename too long. 599 | if (strlen(new_basename) > MAX_FILENAME_LEN - 1) { 600 | set_color(YELLOW, stderr); 601 | fprintf(stderr, 602 | "Warning: new filename too long for exFAT partitions (%" 603 | #ifdef _WIN64 604 | "llu" 605 | #elif defined(_WIN32) 606 | "u" 607 | #else 608 | "lu" 609 | #endif 610 | "/%d characters).\n", 611 | strlen(new_basename) + 4, MAX_FILENAME_LEN - 1); // + 4: ".pkg" 612 | set_color(RESET, stderr); 613 | } 614 | 615 | // Warn if multiple release tags were found. 616 | if (print_ambiguity_warning) { 617 | set_color(BRIGHT_YELLOW, stderr); 618 | fputs("Warning: release tag ambiguous (press [L] to verify).\n", 619 | stderr); 620 | set_color(RESET, stderr); 621 | print_ambiguity_warning = 0; 622 | } 623 | 624 | // Option -n: don't do anything else. 625 | if (option_no_to_all == 1) 626 | goto exit; 627 | 628 | // Quit if already renamed. 629 | if (prompted_once == 0 && option_force == 0 630 | && strcmp(basename, new_basename) == 0) { 631 | puts("Nothing to do."); 632 | goto exit; 633 | } else { 634 | prompted_once = 1; 635 | } 636 | 637 | // Rename now if option_yes_to_all enabled. 638 | if (option_yes_to_all == 1) { 639 | rename_file(filename, new_basename, path); 640 | goto exit; 641 | } 642 | 643 | /*********************************************************************** 644 | * Interactive prompt 645 | **********************************************************************/ 646 | 647 | // Clear stdin immediately. 648 | #ifndef _WIN32 649 | __fpurge(stdin); 650 | #endif 651 | 652 | // Read user input. 653 | fputs("[Y/N/A] [E]dit [T]ag [M]ix [O]nline [R]eset [C]hars [S]FO [L]og [H]elp [Q]uit: ", stdout); 654 | do { 655 | #ifdef _WIN32 656 | c = getch(); 657 | #else 658 | c = getchar(); 659 | #endif 660 | 661 | // Try to go back to the previous scan if backspace was pressed. 662 | #ifdef _WIN32 663 | if (c == 8) { 664 | #else 665 | if (c == 127) { 666 | #endif 667 | // Find the previous non-error scan. 668 | struct scan *prev = scan->prev; 669 | while (prev && prev->error) 670 | prev = prev->prev; 671 | if (prev == NULL) 672 | continue; 673 | 674 | // Go back. 675 | if (scan_backup == NULL) 676 | scan_backup = scan; 677 | option_force = 1; 678 | putchar('\n'); 679 | return prev; 680 | } 681 | 682 | // Return to the current scan if space was pressed. 683 | if (c == 32 && scan_backup != NULL) { 684 | scan = scan_backup; 685 | scan_backup = NULL; 686 | option_force = option_force_backup; 687 | putchar('\n'); 688 | return scan; 689 | } 690 | } while (strchr("ynaAetmorcslhqbpT", c) == NULL); 691 | printf("%c\n", c); 692 | 693 | static int a_primed; 694 | if (c != 'a' && c != 'A') 695 | a_primed = 0; 696 | 697 | // Evaluate user input, 698 | switch (c) { 699 | case 'y': // [Y]es: rename the file. 700 | rename_file(filename, new_basename, path); 701 | goto exit; 702 | case 'n': // [No]: skip file 703 | goto exit; 704 | case 'a': // [A]ll: rename files automatically. 705 | a_primed = 1; 706 | set_color(BRIGHT_YELLOW, stdout); 707 | puts("\nPress Shift-[A] now if you really want to automatically rename all remaining files.\n"); 708 | set_color(RESET, stdout); 709 | break; 710 | case 'A': 711 | if (a_primed) { 712 | option_yes_to_all = 1; 713 | rename_file(filename, new_basename, path); 714 | goto exit; 715 | } else { 716 | set_color(BRIGHT_YELLOW, stdout); 717 | puts("\nPress [A] to unlock this command.\n"); 718 | set_color(RESET, stdout); 719 | } 720 | break; 721 | case 'e': // [E]dit: let user manually enter a new title. 722 | ; 723 | char backup[MAX_TITLE_LEN]; 724 | strcpy(backup, title); 725 | reset_terminal(); 726 | printf("\nEnter new title: "); 727 | fgets(title, MAX_TITLE_LEN, stdin); 728 | title[strlen(title) - 1] = '\0'; // Remove Enter character. 729 | // Remove entered control characters. 730 | for (size_t i = 0; i < strlen(title); i++) { 731 | if (iscntrl(title[i])) { 732 | memmove(&title[i], &title[i + 1], strlen(title) - i); 733 | i--; 734 | } 735 | } 736 | // Restore title if nothing has been entered. 737 | if (title[0] == '\0') { 738 | strcpy(title, backup); 739 | printf("Using title \"%s\".\n", title); 740 | } 741 | printf("\n"); 742 | raw_terminal(); 743 | break; 744 | case 't': // [T]ag: let user enter release groups or releases. 745 | ; 746 | char tag[MAX_TAG_LEN] = ""; 747 | printf("\nEnter new tag: "); 748 | scan_string(tag, MAX_TAG_LEN, "", get_tag); 749 | trim_string(tag, " ,", " ,"); 750 | char *result; 751 | int n_results; 752 | if (tag[0] != '\0') { 753 | // Get entered known release groups. 754 | if ((result = get_release_group(tag)) != NULL) { 755 | printf("Using \"%s\" as release group.\n", result); 756 | strncpy(tag_release_group, result, MAX_TAG_LEN); 757 | tag_release_group[MAX_TAG_LEN] = '\0'; 758 | } 759 | 760 | // Get entered known releases. 761 | if ((n_results = get_release(&result, tag)) != 0) { 762 | printf("Using \"%s\" as release.\n", result); 763 | strncpy(tag_release, result, MAX_TAG_LEN); 764 | tag_release[MAX_TAG_LEN] = '\0'; 765 | } 766 | 767 | // Get Backport tags and unknown tags. 768 | int unknown_tag_entered = 0; 769 | int show_database_hint = 0; 770 | char *tok = strtok(tag, ","); 771 | while (tok) { 772 | trim_string(tok, " ", " "); 773 | if (strcasecmp(tok, BACKPORT_STRING) == 0) { 774 | if (backport) { 775 | printf("Backport tag disabled.\n"); 776 | backport = NULL; 777 | } else { 778 | printf("Backport tag enabled.\n"); 779 | backport = BACKPORT_STRING; 780 | } 781 | } else { 782 | if (tag_release_group[0] 783 | && get_release_group(tok)) { 784 | if (strcasecmp(tag_release_group, tok) != 0) 785 | printf("Cannot enter multiple release groups (\"%s\").\n", tok); 786 | } else { 787 | // Make sure existing releases reset when a new 788 | // unknown tag was entered. 789 | if (unknown_tag_entered == 0) { 790 | unknown_tag_entered = 1; 791 | if (n_results == 0) 792 | tag_release[0] = '\0'; 793 | } 794 | 795 | if (strwrd(tag_release, tok) == NULL) { // Ignore duplicates. 796 | printf("Using \"%s\" as release. ", tok); 797 | set_color(BRIGHT_YELLOW, stdout); 798 | printf("%s", "<- MISSING FROM DATABASE\n"); 799 | set_color(RESET, stdout); 800 | show_database_hint = 1; 801 | 802 | if (tag_release[0]) { 803 | // Add current tok in alphabetic order. 804 | char buf[MAX_TAG_LEN] = ""; 805 | char *next_tag_start = tag_release; 806 | while(1) { 807 | char *next_tag_end = strchr(next_tag_start, ','); 808 | if (next_tag_end == NULL) 809 | next_tag_end = tag_release + strlen(tag_release); 810 | memcpy(buf, next_tag_start, next_tag_end - next_tag_start); 811 | buf[next_tag_end - next_tag_start] = '\0'; 812 | 813 | // Found correct order -> insert. 814 | if (strcasecmp(buf, tok) > 0) { 815 | memset(buf, 0, MAX_TAG_LEN); 816 | strncpy(buf, tag_release, next_tag_start - tag_release); 817 | strncat(buf, tok, MAX_TAG_LEN - 1 - strlen(buf)); 818 | if (next_tag_start != 0) 819 | strcat(buf, ","); 820 | #pragma GCC diagnostic push 821 | #pragma GCC diagnostic ignored "-Wstringop-truncation" 822 | strncat(buf, next_tag_start, MAX_TAG_LEN - 1 - strlen(buf)); 823 | #pragma GCC diagnostic pop 824 | memcpy(tag_release, buf, MAX_TAG_LEN); 825 | break; 826 | } 827 | 828 | // Reached the end -> append. 829 | if (*next_tag_end == '\0') { 830 | size_t len = strlen(tag_release); 831 | snprintf(tag_release + len, MAX_TAG_LEN - len, ",%s", tok); 832 | break; 833 | } 834 | 835 | next_tag_start = next_tag_end + 1; 836 | } 837 | } else { 838 | // First value; simply copy it. 839 | strncpy(tag_release + strlen(tag_release), tok, MAX_TAG_LEN); 840 | tag_release[MAX_TAG_LEN] = '\0'; 841 | } 842 | } 843 | } 844 | } 845 | 846 | tok = strtok(NULL, ","); 847 | } 848 | 849 | if (show_database_hint) { 850 | set_color(BRIGHT_YELLOW, stdout); 851 | puts("\nUse command line option --tags or --tagfile to add missing tags to the database."); 852 | set_color(RESET, stdout); 853 | } 854 | 855 | if (option_tag_separator) 856 | replace_commas_in_tag(tag_release, option_tag_separator); 857 | } 858 | printf("\n"); 859 | break; 860 | case 'T': // Shift-t: remove all release tags. 861 | printf("\n"); 862 | if (tag_release[0] || tag_release_group[0] 863 | || release || release_group) 864 | puts("Release tags have been removed."); 865 | else 866 | puts("No release tags to remove."); 867 | printf("\n"); 868 | tag_release[0] = '\0'; 869 | tag_release_group[0] = '\0'; 870 | release = NULL; 871 | release_group = NULL; 872 | break; 873 | case 'm': // [M]ix: convert title to mixed-case letter format. 874 | mixed_case(title); 875 | printf("\nConverted letter case to mixed-case style.\n\n"); 876 | break; 877 | case 'o': // [O]nline: search the PlayStation store for metadata. 878 | printf("\n"); 879 | search_online(content_id, title, 0); 880 | printf("\n"); 881 | break; 882 | case 'r': // [R]eset: undo all changes. 883 | strcpy(title, title_backup); 884 | printf("\nTitle has been reset to \"%s\".\n", title_backup); 885 | if (tag_release_group[0] != '\0') { 886 | printf("Tagged release group \"%s\" has been reset.\n", 887 | tag_release_group); 888 | tag_release_group[0] = '\0'; 889 | } 890 | if (tag_release[0] != '\0') { 891 | printf("Tagged release \"%s\" has been reset.\n", 892 | tag_release); 893 | tag_release[0] = '\0'; 894 | } 895 | printf("\n"); 896 | break; 897 | case 'c': // [C]hars: reveal all non-printable characters. 898 | printf("\nOriginal: \"%s\"\nRevealed: \"", title); 899 | int count = 0; 900 | for (size_t i = 0; i < strlen(title); i++) { 901 | if (isprint(title[i])) { 902 | printf("%c", title[i]); 903 | } else { 904 | count++; 905 | set_color(BRIGHT_YELLOW, stdout); 906 | printf("#%u", (unsigned char) title[i]); 907 | set_color(RESET, stdout); 908 | } 909 | } 910 | printf("\"\n%d special character%c found.\n\n", count, 911 | count == 1 ? 0 : 's'); 912 | break; 913 | case 's': // [S]FO: print param.sfo information. 914 | printf("\n"); 915 | print_param_sfo(param_sfo); 916 | printf("\n"); 917 | break; 918 | case 'h': // [H]elp: show help. 919 | printf("\n"); 920 | print_prompt_help(); 921 | printf("\n"); 922 | break; 923 | case 'b': // [B]ackport: toggle backport tag. 924 | if (backport) { 925 | backport = NULL; 926 | printf("\nBackport tag disabled.\n\n"); 927 | } else { 928 | backport = BACKPORT_STRING; 929 | printf("\nBackport tag enabled.\n\n"); 930 | } 931 | break; 932 | case 'l': // Change[l]og: print existing changelog data. 933 | if (changelog) { 934 | printf("\n%s\n\n", changelog); 935 | print_changelog_tags(changelog); 936 | putchar('\n'); 937 | } else { 938 | printf("\nThis file does not contain changelog data.\n\n"); 939 | } 940 | break; 941 | case 'p': // [P]atch: toggle changelog patch detection for app PKGs. 942 | if (changelog_patch_detection) { 943 | merged_ver = NULL; 944 | true_ver = app_ver; 945 | changelog_patch_detection = 0; 946 | printf("\nChangelog patch detection disabled for the" 947 | " current file.\n\n"); 948 | } else { 949 | if (true_ver_buf[0]) { 950 | if (option_leading_zeros == 0 && true_ver_buf[0] == '0') 951 | true_ver = true_ver_buf + 1; 952 | else 953 | true_ver = true_ver_buf; 954 | if (category && category[1] == 'd' 955 | && strcmp(true_ver_buf, "01.00") != 0) 956 | merged_ver = true_ver; 957 | } 958 | changelog_patch_detection = 1; 959 | printf("\nChangelog patch detection enabled for the current" 960 | " file.\n\n"); 961 | } 962 | break; 963 | case 'q': // [Q]uit: exit the program. 964 | exit(EXIT_SUCCESS); 965 | } 966 | } 967 | 968 | exit: 969 | return NULL; 970 | } 971 | 972 | // Background thread that scans PS4 PKG files for data required for renaming. 973 | static void *scan_files(void *param) 974 | { 975 | struct scan_job *job = (struct scan_job *) param; 976 | 977 | if (job->n_filenames == 0) { // Use current directory. 978 | if (option_query == 1) 979 | goto done; 980 | if (parse_directory(".", job)) 981 | exit(EXIT_FAILURE); 982 | } else { // Find PKGs and run pkgrename() on them. 983 | DIR *dir; 984 | for (int i = 0; i < job->n_filenames; i++) { 985 | // Directory 986 | if ((dir = opendir(job->filenames[i])) != NULL) { 987 | closedir(dir); 988 | if (option_query == 1) { 989 | puts(job->filenames[i]); 990 | continue; 991 | } 992 | if (parse_directory(job->filenames[i], job)) 993 | exit(EXIT_FAILURE); 994 | // File 995 | } else { 996 | add_scan_result(job, job->filenames[i], 0); 997 | } 998 | } 999 | } 1000 | 1001 | done: 1002 | ; 1003 | int err; 1004 | 1005 | if ((err = pthread_mutex_lock(&job->mutex)) != 0) 1006 | goto error; 1007 | job->scan_list.finished = 1; 1008 | if ((err = pthread_mutex_unlock(&job->mutex)) != 0) 1009 | goto error; 1010 | 1011 | if ((err = pthread_cond_signal(&job->cond)) != 0) 1012 | goto error; 1013 | 1014 | return NULL; 1015 | 1016 | error: 1017 | exit_err(err, __func__, __LINE__); 1018 | } 1019 | 1020 | // Runs pkgrename() on scan results as they become available. 1021 | static void parse_scan_results(struct scan_job *job) 1022 | { 1023 | struct scan *scan, *known_tail; 1024 | 1025 | // Wait until the list has at least 1 node. 1026 | pthread_mutex_lock(&job->mutex); 1027 | while (job->scan_list.head == NULL && job->scan_list.finished == 0) 1028 | pthread_cond_wait(&job->cond, &job->mutex); 1029 | known_tail = job->scan_list.tail; // Store tail to skip mutex locks below. 1030 | scan = job->scan_list.head; 1031 | pthread_mutex_unlock(&job->mutex); 1032 | 1033 | if (scan == NULL) // Scan finished without adding scans. 1034 | return; 1035 | 1036 | 1037 | while (1) { 1038 | // Skip previously seen error scans. 1039 | if (scan->filename == NULL) 1040 | goto next; 1041 | 1042 | while (1) { 1043 | struct scan *ret = pkgrename(scan); 1044 | if (ret == NULL) 1045 | break; 1046 | scan = ret; 1047 | } // Call pkgrename() as long as requested. 1048 | 1049 | next: 1050 | // Wait until a new scan becomes available. 1051 | if (scan == known_tail) { 1052 | pthread_mutex_lock(&job->mutex); 1053 | while (scan->next == NULL && job->scan_list.finished == 0) 1054 | pthread_cond_wait(&job->cond, &job->mutex); 1055 | known_tail = job->scan_list.tail; 1056 | pthread_mutex_unlock(&job->mutex); 1057 | if (scan->next == NULL) // Scanning finished. 1058 | return; 1059 | } 1060 | 1061 | scan = scan->next; 1062 | } 1063 | } 1064 | 1065 | inline static int is_dir(const char *filename) 1066 | { 1067 | struct stat sb; 1068 | if (stat(filename, &sb)) 1069 | return 0; 1070 | 1071 | return S_ISDIR(sb.st_mode); 1072 | } 1073 | 1074 | int main(int argc, char *argv[]) 1075 | { 1076 | initialize_terminal(); 1077 | raw_terminal(); 1078 | 1079 | #ifdef _WIN32 1080 | // Disable colors if the terminal is not Windows Terminal. 1081 | if (getenv("WT_SESSION") == NULL) 1082 | option_disable_colors = 1; 1083 | #endif 1084 | 1085 | parse_options(&argc, &argv); 1086 | 1087 | struct scan_job job; 1088 | if (initialize_scan_job(&job, argv, argc)) 1089 | exit(EXIT_FAILURE); 1090 | 1091 | // Check if operands contain directories. 1092 | if (option_query == 0) { 1093 | if (option_recursive == 1) 1094 | multiple_directories = 1; 1095 | else if (argc > 1) { 1096 | for (int i = 0; i < argc; i++) { 1097 | if (is_dir(argv[i])) { 1098 | multiple_directories = 1; 1099 | break; 1100 | } 1101 | } 1102 | } 1103 | } 1104 | 1105 | // Run file scans in a separate thread. 1106 | pthread_t file_thread; 1107 | int err; 1108 | if ((err = pthread_create(&file_thread, NULL, scan_files, &job)) != 0) 1109 | exit_err(err, __func__, __LINE__); 1110 | 1111 | // Parse the scan results in the main thread. 1112 | parse_scan_results(&job); 1113 | 1114 | destroy_scan_job(&job); 1115 | 1116 | exit(EXIT_SUCCESS); 1117 | } 1118 | --------------------------------------------------------------------------------