diff --git a/tools/resgen/src/main.cpp b/tools/resgen/src/main.cpp index 8186ef7254..ccad6d8e2b 100644 --- a/tools/resgen/src/main.cpp +++ b/tools/resgen/src/main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,28 +19,37 @@ #include #include +#include #include +#include #include #include #include +#include #include #include #include #include -using namespace std; using namespace utils; -static const char* g_jsonMagicString = "__RESGEN__"; -static const char* g_packageName = "resources"; -static const char* g_deployDir = "."; -static bool g_keepExtension = false; -static bool g_appendNull = false; -static bool g_generateC = false; -static bool g_quietMode = false; -static bool g_embedJson = false; +// Holds all command-line configuration for the resource generator. +struct AppConfig { + const char* packageName = "resources"; + const char* deployDir = "."; + bool keepExtension = false; + bool appendNull = false; + bool generateC = false; + bool quietMode = false; + bool embedJson = false; + std::vector inputPaths; +}; +// A special marker used to identify where to insert the JSON summary blob. +static const char* g_jsonMagicString = "__RESGEN__"; + +// Defines the help message text displayed to the user. static const char* USAGE = R"TXT( RESGEN aggregates a sequence of binary blobs, each of which becomes a "resource" whose id is the basename of the input file. It produces the following set of files: @@ -82,6 +91,7 @@ Examples: TEXTURES_BEACH_DATA, TEXTURES_BEACH_SIZE )TXT"; +// Template for the Apple-specific assembly file. static const char* APPLE_ASM_TEMPLATE = R"ASM( .global _{RESOURCES}PACKAGE .section __TEXT,__const @@ -89,6 +99,7 @@ _{RESOURCES}PACKAGE: .incbin "{resources}.bin" )ASM"; +// Template for the standard assembly file. static const char* ASM_TEMPLATE = R"ASM( .global {RESOURCES}PACKAGE .section .rodata @@ -96,287 +107,304 @@ static const char* ASM_TEMPLATE = R"ASM( .incbin "{resources}.bin" )ASM"; +// Prints the usage string, replacing the placeholder with the actual executable name. static void printUsage(const char* name) { - std::string execName(Path(name).getName()); + std::string const execName(Path(name).getName()); const std::string from("RESGEN"); std::string usage(USAGE); - for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) { + std::size_t pos = 0; + while ((pos = usage.find(from, pos)) != std::string::npos) { usage.replace(pos, from.length(), execName); + pos += execName.length(); } - puts(usage.c_str()); + std::cout << usage; } +// Prints the license text to the console. static void license() { static const char *license[] = { #include "licenses/licenses.inc" nullptr }; - const char **p = &license[0]; - while (*p) - std::cout << *p++ << std::endl; + for (const char **p = &license[0]; *p; ++p) { + std::cout << *p << std::endl; + } } -static int handleArguments(int argc, char* argv[]) { +// A helper function to replace all occurrences of a substring within a string. +static std::string& replaceAll(std::string& context, const std::string& from, const std::string& to) { + std::size_t pos = 0; + while ((pos = context.find(from, pos)) != std::string::npos) { + context.replace(pos, from.length(), to); + pos += to.length(); + } + return context; +} + +// Parses command-line arguments using getopt and populates the AppConfig struct. +static AppConfig handleArguments(int const argc, char* argv[]) { static constexpr const char* OPTSTR = "hLp:x:ktcqj"; - static const struct option OPTIONS[] = { - { "help", no_argument, 0, 'h' }, - { "license", no_argument, 0, 'L' }, - { "package", required_argument, 0, 'p' }, - { "deploy", required_argument, 0, 'x' }, - { "keep", no_argument, 0, 'k' }, - { "text", no_argument, 0, 't' }, - { "cfile", no_argument, 0, 'c' }, - { "quiet", no_argument, 0, 'q' }, - { "json", no_argument, 0, 'j' }, - { 0, 0, 0, 0 } // termination of the option list + static const option OPTIONS[] = { + { "help", no_argument, nullptr, 'h' }, + { "license", no_argument, nullptr, 'L' }, + { "package", required_argument, nullptr, 'p' }, + { "deploy", required_argument, nullptr, 'x' }, + { "keep", no_argument, nullptr, 'k' }, + { "text", no_argument, nullptr, 't' }, + { "cfile", no_argument, nullptr, 'c' }, + { "quiet", no_argument, nullptr, 'q' }, + { "json", no_argument, nullptr, 'j' }, + { nullptr, 0, nullptr, 0 } }; + AppConfig config; int opt; - int optionIndex = 0; - - while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &optionIndex)) >= 0) { - std::string arg(optarg ? optarg : ""); + while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, nullptr)) >= 0) { switch (opt) { - default: case 'h': printUsage(argv[0]); - exit(0); + std::exit(0); case 'L': license(); - exit(0); + std::exit(0); case 'p': - g_packageName = optarg; + config.packageName = optarg; break; case 'x': - g_deployDir = optarg; + config.deployDir = optarg; break; case 'k': - g_keepExtension = true; + config.keepExtension = true; break; case 't': - g_appendNull = true; + config.appendNull = true; break; case 'c': - g_generateC = true; + config.generateC = true; break; case 'q': - g_quietMode = true; + config.quietMode = true; break; case 'j': - g_embedJson = true; + config.embedJson = true; break; + default: + printUsage(argv[0]); + std::exit(1); } } - return optind; + // Treat remaining arguments as input file paths. + for (int i = optind; i < argc; ++i) { + config.inputPaths.emplace_back(argv[i]); + } + + return config; +} + +// Opens a file for writing, printing an error and exiting on failure. +static std::ofstream openOutputFile(const Path& path, std::ios_base::openmode const mode = std::ios_base::out) { + std::ofstream stream(path.getPath(), mode); + if (!stream) { + std::cerr << "Unable to open " << path << std::endl; + std::exit(1); + } + return stream; +} + +// Reads an entire binary file into a vector of bytes. +static std::vector readFile(const Path& path) { + std::ifstream inStream(path.getPath(), std::ios::binary); + if (!inStream) { + std::cerr << "Unable to open " << path << std::endl; + std::exit(1); + } + return std::vector((std::istreambuf_iterator(inStream)), + (std::istreambuf_iterator())); +} + +// Writes the generated header file, but only if its content has changed. +// This helps avoid unnecessary recompilation in build systems. +static void writeHeaderIfChanged(const Path& path, const std::string& newContents) { + std::ifstream headerInStream(path.getPath(), std::ifstream::ate); + if (headerInStream) { + long const fileSize = headerInStream.tellg(); + if (fileSize == newContents.size()) { + std::vector previous(fileSize); + headerInStream.seekg(0); + headerInStream.read(previous.data(), fileSize); + if (0 == memcmp(previous.data(), newContents.c_str(), fileSize)) { + return; + } + } + } + std::ofstream headerOutStream = openOutputFile(path); + headerOutStream << newContents; +} + +// Writes a resource's data to a C-style char array (similar to 'xxd -i'). +static void writeXxdEntry(std::ofstream& xxdStream, const std::string& resourceName, + const std::vector& content) { + xxdStream << "// " << resourceName << "\n"; + xxdStream << std::setfill('0') << std::hex; + for (std::size_t i = 0; i < content.size(); i++) { + if (i > 0 && i % 20 == 0) { + xxdStream << "\n"; + } + xxdStream << "0x" << std::setw(2) << int(content[i]) << ", "; + } + if (!content.empty() && content.size() % 20 != 0) { + xxdStream << "\n"; + } + xxdStream << "\n"; } int main(int argc, char* argv[]) { - const int optionIndex = handleArguments(argc, argv); - const int numArgs = argc - optionIndex; - if (numArgs < 1) { + // Parse arguments and check for input files. + const AppConfig config = handleArguments(argc, argv); + if (config.inputPaths.empty()) { printUsage(argv[0]); return 1; } - vector inputPaths; - for (int argIndex = optionIndex; argIndex < argc; ++argIndex) { - inputPaths.emplace_back(argv[argIndex]); - } - if (g_embedJson) { - inputPaths.push_back(g_jsonMagicString); + // If JSON embedding is enabled, add a placeholder to the input list. + std::vector inputPaths = config.inputPaths; + if (config.embedJson) { + inputPaths.emplace_back(g_jsonMagicString); } - std::string packageFile = g_packageName; - std::string packagePrefix = std::string(g_packageName) + "_"; - transform(packagePrefix.begin(), packagePrefix.end(), packagePrefix.begin(), ::toupper); - std::string package = packagePrefix + "PACKAGE"; + // Generate names and symbols based on the package name. + const std::string packageFile = config.packageName; + std::string packagePrefix = std::string(config.packageName) + "_"; + std::transform(packagePrefix.begin(), packagePrefix.end(), packagePrefix.begin(), [](unsigned char c) { return std::toupper(c); }); + const std::string packageSymbol = packagePrefix + "PACKAGE"; - const Path deployDir(g_deployDir); + // Create the deployment directory if it doesn't exist. + const Path deployDir(config.deployDir); if (!deployDir.exists()) { deployDir.mkdirRecursive(); } + // Define all output file paths. const Path appleAsmPath(deployDir + (packageFile + ".apple.S")); const Path asmPath(deployDir + (packageFile + ".S")); const Path binPath(deployDir + (packageFile + ".bin")); const Path headerPath(deployDir + (packageFile + ".h")); const Path xxdPath(deployDir + (packageFile + ".c")); - // In the assembly language templates, replace {RESOURCES} with packagePrefix and replace - // {resources} with packageFile. - const std::string k1("{RESOURCES}"); - const std::string k2("{resources}"); + // Prepare assembly file content by replacing placeholders. std::string aasmstr(APPLE_ASM_TEMPLATE); + replaceAll(aasmstr, "{RESOURCES}", packagePrefix); + replaceAll(aasmstr, "{resources}", packageFile); + std::string asmstr(ASM_TEMPLATE); - for (size_t pos = aasmstr.find(k1); pos != std::string::npos; pos = aasmstr.find(k1, pos)) - aasmstr.replace(pos, k1.length(), packagePrefix); - for (size_t pos = aasmstr.find(k2); pos != std::string::npos; pos = aasmstr.find(k2, pos)) - aasmstr.replace(pos, k2.length(), packageFile); - for (size_t pos = asmstr.find(k1); pos != std::string::npos; pos = asmstr.find(k1, pos)) - asmstr.replace(pos, k1.length(), packagePrefix); - for (size_t pos = asmstr.find(k2); pos != std::string::npos; pos = asmstr.find(k2, pos)) - asmstr.replace(pos, k2.length(), packageFile); + replaceAll(asmstr, "{RESOURCES}", packagePrefix); + replaceAll(asmstr, "{resources}", packageFile); - // Open the Apple-friendly assembly language file. - ofstream appleAsmStream(appleAsmPath.getPath()); - if (!appleAsmStream) { - cerr << "Unable to open " << appleAsmPath << endl; - exit(1); - } + // Open all output file streams. + auto appleAsmStream = openOutputFile(appleAsmPath); + auto asmStream = openOutputFile(asmPath); + auto binStream = openOutputFile(binPath, std::ios::binary); - // Open the non-Apple assembly language file. - ofstream asmStream(asmPath.getPath()); - if (!asmStream) { - cerr << "Unable to open " << asmPath << endl; - exit(1); - } + // Begin constructing the C header file content. + std::ostringstream headerStream; + headerStream << "#ifndef " << packagePrefix << "H_\n" + << "#define " << packagePrefix << "H_\n\n" + << "#include \n\n" + << "extern \"C\" {\n" + << " extern const uint8_t " << packageSymbol << "[];\n"; - // Open the bin file for writing. - ofstream binStream(binPath.getPath(), ios::binary); - if (!binStream) { - cerr << "Unable to open " << binPath << endl; - exit(1); - } + // String streams to accumulate header macros and the JSON summary. + std::ostringstream headerMacros; + std::ostringstream jsonStream; - // Open the header file stream for writing. - ostringstream headerStream; - headerStream << "#ifndef " << packagePrefix << "H_" << endl - << "#define " << packagePrefix << "H_" << endl << endl - << "#include \n" << endl - << "extern \"C\" {" << endl - << " extern const uint8_t " << package << "[];" << endl; - - ostringstream headerMacros; - ostringstream appleDataAsmStream; - ostringstream dataAsmStream; - ostringstream jsonStream; - - // Open the generated C file for writing. - ofstream xxdStream; - if (g_generateC) { - xxdStream = ofstream(xxdPath.getPath()); - if (!xxdStream) { - cerr << "Unable to open " << xxdPath << endl; - exit(1); - } + // If generating a C file, open the stream and write the initial boilerplate. + std::ofstream xxdStream; + if (config.generateC) { + xxdStream = openOutputFile(xxdPath); xxdStream << "#include \n" - << "const uint8_t " << package << "[] = {\n"; + << "const uint8_t " << packageSymbol << "[] = {\n"; } - // Consume each input file and write it back out into the various output streams. + // Process each input file to build the resource collection. jsonStream << "{"; - size_t offset = 0; + std::size_t offset = 0; for (const auto& inPath : inputPaths) { - vector content; + std::vector content; if (inPath != g_jsonMagicString) { - ifstream inStream(inPath.getPath(), ios::binary); - if (!inStream) { - cerr << "Unable to open " << inPath << endl; - exit(1); - } - content = vector((istreambuf_iterator(inStream)), {}); + // For a regular file, read its binary content. + content = readFile(inPath); } else { - // To finalize the JSON string, we replace the trailing comma with an end bracket and - // prefix it with the magic identifier and string size. + // For the JSON placeholder, finalize and embed the JSON summary blob. + // The blob is formatted as: __RESGEN__\0\0{...json...} std::string jsonString = jsonStream.str(); - jsonString[jsonString.size()-1] = '}'; - ostringstream jsonBlob; - jsonBlob << g_jsonMagicString << "\0"; - jsonBlob << jsonString.size() << "\0"; + jsonString[jsonString.size() - 1] = '}'; + std::ostringstream jsonBlob; + jsonBlob << g_jsonMagicString << '\0'; + jsonBlob << jsonString.size() << '\0'; jsonBlob << jsonString; jsonString = jsonBlob.str(); - uint8_t const* jsonPtr = (uint8_t const*) jsonString.c_str(); - content = vector(jsonPtr, jsonPtr + jsonBlob.str().size()); + const auto* jsonPtr = reinterpret_cast(jsonString.c_str()); + content.assign(jsonPtr, jsonPtr + jsonBlob.str().size()); } - if (g_appendNull) { + + // Optionally append a null terminator, useful for text resources. + if (config.appendNull) { content.push_back(0); } - // Formulate the resource name and the prefixed resource name. - std::string rname = g_keepExtension ? inPath.getName() : inPath.getNameWithoutExtension(); - replace(rname.begin(), rname.end(), '.', '_'); - transform(rname.begin(), rname.end(), rname.begin(), ::toupper); + // Generate the resource's symbol name from its file name. + std::string rname = config.keepExtension ? inPath.getName() : inPath.getNameWithoutExtension(); + replaceAll(rname, ".", "_"); + std::transform(rname.begin(), rname.end(), rname.begin(), [](unsigned char c) { return std::toupper(c); }); const std::string prname = packagePrefix + rname; - // Write the binary blob into the bin file. - binStream.write((const char*) content.data(), content.size()); + // Write the resource content to the aggregate binary file. + binStream.write(reinterpret_cast(content.data()), content.size()); - // Write the offsets and sizes. - headerMacros - << "#define " << prname << "_OFFSET " << offset << "\n" - << "#define " << prname << "_SIZE " << content.size() << "\n" - << "#define " << prname << "_DATA (" << package << " + " << prname << "_OFFSET)\n\n"; + // Generate C preprocessor macros for the resource's offset, size, and data pointer. + headerMacros << "#define " << prname << "_OFFSET " << offset << "\n" + << "#define " << prname << "_SIZE " << content.size() << "\n" + << "#define " << prname << "_DATA (" << packageSymbol << " + " << prname << "_OFFSET)\n\n"; - // Write the xxd-style ASCII array, followed by a blank line. - if (g_generateC) { - xxdStream << "// " << rname << "\n"; - xxdStream << setfill('0') << hex; - size_t i = 0; - for (; i < content.size(); i++) { - if (i > 0 && i % 20 == 0) { - xxdStream << "\n"; - } - xxdStream << "0x" << setw(2) << (int) content[i] << ", "; - } - if (i % 20 != 0) xxdStream << "\n"; - xxdStream << "\n"; + // If enabled, write the content to the C source file. + if (config.generateC) { + writeXxdEntry(xxdStream, rname, content); } + // Add an entry to the JSON summary and update the running offset. jsonStream << "\"" << rname << "\":" << content.size() << ","; offset += content.size(); } + // Finalize the header file content. headerStream << "}\n\n"; headerStream << headerMacros.str(); headerStream << "#endif\n"; - // To optimize builds, avoid overwriting the header file if nothing has changed. - bool headerIsDirty = true; - ifstream headerInStream(headerPath.getPath(), std::ifstream::ate); - string headerContents = headerStream.str(); - if (headerInStream) { - long fileSize = static_cast(headerInStream.tellg()); - if (fileSize == headerContents.size()) { - vector previous(fileSize); - headerInStream.seekg(0); - headerInStream.read(previous.data(), fileSize); - headerIsDirty = 0 != memcmp(previous.data(), headerContents.c_str(), fileSize); - } + // Write the header and assembly files. + writeHeaderIfChanged(headerPath, headerStream.str()); + asmStream << asmstr << std::endl; + appleAsmStream << aasmstr << std::endl; + + // Report generated files to the console unless in quiet mode. + if (!config.quietMode) { + std::cout << "Generated files: " << headerPath << " " << asmPath << " " << appleAsmPath << " " + << binPath; } - if (headerIsDirty) { - ofstream headerOutStream(headerPath.getPath()); - if (!headerOutStream) { - cerr << "Unable to open " << headerPath << endl; - exit(1); - } - headerOutStream << headerContents; - } - - asmStream << asmstr << dataAsmStream.str() << endl; - asmStream.close(); - - appleAsmStream << aasmstr << appleDataAsmStream.str() << endl; - appleAsmStream.close(); - - if (!g_quietMode) { - cout << "Generated files: " - << headerPath << " " - << asmPath << " " - << appleAsmPath << " " - << binPath; - } - - if (g_generateC) { + // Finalize the C file and report it. + if (config.generateC) { xxdStream << "};\n\n"; - if (!g_quietMode) { - cout << " " << xxdPath; + if (!config.quietMode) { + std::cout << " " << xxdPath; } } - if (!g_quietMode) { - cout << endl; + if (!config.quietMode) { + std::cout << std::endl; } -} + + return 0; +} \ No newline at end of file