2
0

PackageTool.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. // Copyright (c) 2008-2023 the Urho3D project
  2. // License: MIT
  3. #include <Urho3D/Core/Context.h>
  4. #include <Urho3D/Container/ArrayPtr.h>
  5. #include <Urho3D/Core/ProcessUtils.h>
  6. #include <Urho3D/IO/File.h>
  7. #include <Urho3D/IO/FileSystem.h>
  8. #include <Urho3D/IO/PackageFile.h>
  9. #ifdef WIN32
  10. #include <Urho3D/Engine/WinWrapped.h>
  11. #endif
  12. #include <LZ4/lz4.h>
  13. #include <LZ4/lz4hc.h>
  14. #include <Urho3D/DebugNew.h>
  15. using namespace Urho3D;
  16. static const unsigned COMPRESSED_BLOCK_SIZE = 32768;
  17. struct FileEntry
  18. {
  19. String name_;
  20. unsigned offset_{};
  21. unsigned size_{};
  22. hash32 checksum_{};
  23. };
  24. SharedPtr<Context> context_(new Context());
  25. SharedPtr<FileSystem> fileSystem_(new FileSystem(context_));
  26. String basePath_;
  27. Vector<FileEntry> entries_;
  28. hash32 checksum_ = 0;
  29. bool compress_ = false;
  30. bool quiet_ = false;
  31. unsigned blockSize_ = COMPRESSED_BLOCK_SIZE;
  32. String ignoreExtensions_[] = {
  33. ".bak",
  34. ".rule",
  35. ""
  36. };
  37. int main(int argc, char** argv);
  38. void Run(const Vector<String>& arguments);
  39. void Pack(const Vector<String>& arguments);
  40. void Unpack(const Vector<String>& arguments);
  41. void ProcessFile(const String& fileName, const String& rootDir);
  42. void WritePackageFile(const String& fileName, const String& rootDir);
  43. void WriteHeader(File& dest);
  44. int main(int argc, char** argv)
  45. {
  46. Vector<String> arguments;
  47. #ifdef WIN32
  48. arguments = ParseArguments(GetCommandLineW());
  49. #else
  50. arguments = ParseArguments(argc, argv);
  51. #endif
  52. Run(arguments);
  53. return 0;
  54. }
  55. static const String USAGE_STR =
  56. "Usage:\n"
  57. "1) Packing: PackageTool -p<options> <input directory name> <output package name> [base path]\n"
  58. " Options:\n"
  59. " q - enable quiet mode\n"
  60. " c - enable LZ4 compression\n"
  61. " Base path is an optional prefix that will be added to the file entries.\n"
  62. " Example: PackageTool -pqc CoreData CoreData.pak\n"
  63. "2) Unpacking: PackageTool -u<options> <input package name> <output directory name>\n"
  64. " Options:\n"
  65. " q - enable quiet mode\n"
  66. " Example: PackageTool -uq CoreData.pak CoreData\n"
  67. "3) Print info: PackageTool -<mode> <package name>\n"
  68. " Modes:\n"
  69. " i - print package file information\n"
  70. " l - print file names (including their paths) contained in the package\n"
  71. " L - similar to l but also output compression ratio (compressed package file only)\n"
  72. " Example: PackageTool -i CoreData.pak";
  73. void Pack(const Vector<String>& arguments)
  74. {
  75. if (arguments.Size() != 3 && arguments.Size() != 4)
  76. ErrorExit(USAGE_STR);
  77. const String& mode = arguments[0]; // Starts with -p
  78. for (unsigned i = 2; i < mode.Length(); ++i)
  79. {
  80. if (mode[i] == 'q')
  81. quiet_ = true;
  82. else if (mode[i] == 'c')
  83. compress_ = true;
  84. else
  85. ErrorExit("Unrecognized option");
  86. }
  87. const String& dirName = arguments[1];
  88. const String& packageName = arguments[2];
  89. if (arguments.Size() == 4)
  90. basePath_ = arguments[3];
  91. if (!quiet_)
  92. PrintLine("Scanning directory " + dirName + " for files");
  93. // Get the file list recursively
  94. Vector<String> fileNames;
  95. fileSystem_->ScanDir(fileNames, dirName, "*.*", SCAN_FILES, true);
  96. if (!fileNames.Size())
  97. ErrorExit("No files found");
  98. // Check for extensions to ignore
  99. for (i32 i = fileNames.Size() - 1; i >= 0; --i)
  100. {
  101. String extension = GetExtension(fileNames[i]);
  102. for (unsigned j = 0; ignoreExtensions_[j].Length(); ++j)
  103. {
  104. if (extension == ignoreExtensions_[j])
  105. {
  106. fileNames.Erase(fileNames.Begin() + i);
  107. break;
  108. }
  109. }
  110. }
  111. for (unsigned i = 0; i < fileNames.Size(); ++i)
  112. ProcessFile(fileNames[i], dirName);
  113. WritePackageFile(packageName, dirName);
  114. }
  115. void Unpack(const Vector<String>& arguments)
  116. {
  117. if (arguments.Size() != 3)
  118. ErrorExit(USAGE_STR);
  119. const String& mode = arguments[0]; // Starts with -u
  120. for (unsigned i = 2; i < mode.Length(); ++i)
  121. {
  122. if (mode[i] == 'q')
  123. quiet_ = true;
  124. else
  125. ErrorExit("Unrecognized option");
  126. }
  127. const String& packageName = arguments[1];
  128. const String& dirName = arguments[2];
  129. SharedPtr<PackageFile> packageFile(new PackageFile(context_, packageName));
  130. char buffer[1024];
  131. const HashMap<String, PackageEntry>& entries = packageFile->GetEntries();
  132. for (HashMap<String, PackageEntry>::ConstIterator i = entries.Begin(); i != entries.End();)
  133. {
  134. HashMap<String, PackageEntry>::ConstIterator current = i++;
  135. String outFilePath(dirName + "/" + current->first_);
  136. i32 pos = outFilePath.FindLast('/');
  137. if (pos == String::NPOS)
  138. ErrorExit("pos == String::NPOS");
  139. fileSystem_->CreateDir(outFilePath.Substring(0, pos));
  140. File packedFile(context_, packageFile, current->first_);
  141. if (!packedFile.IsOpen())
  142. ErrorExit("packedFile open failed " + current->first_);
  143. File outFile(context_, outFilePath, FILE_WRITE);
  144. if (!outFile.IsOpen())
  145. ErrorExit("outFile open failed " + current->first_);
  146. if (!quiet_)
  147. PrintLine("Write file: " + outFilePath);
  148. unsigned numRead, numWrite;
  149. while (true)
  150. {
  151. numRead = packedFile.Read(buffer, sizeof(buffer));
  152. if (!numRead)
  153. {
  154. packedFile.Close();
  155. outFile.Close();
  156. break;
  157. }
  158. numWrite = outFile.Write(buffer, numRead);
  159. if (numWrite != numRead)
  160. ErrorExit("numWrite != numRead");
  161. }
  162. }
  163. if (!quiet_)
  164. PrintLine("Done");
  165. }
  166. void PrintInfo(const Vector<String>& arguments)
  167. {
  168. if (arguments.Size() != 2)
  169. ErrorExit(USAGE_STR);
  170. const String& packageName = arguments[1];
  171. SharedPtr<PackageFile> packageFile(new PackageFile(context_, packageName));
  172. bool outputCompressionRatio = false;
  173. switch (arguments[0][1])
  174. {
  175. case 'i':
  176. PrintLine("Number of files: " + String(packageFile->GetNumFiles()));
  177. PrintLine("File data size: " + String(packageFile->GetTotalDataSize()));
  178. PrintLine("Package size: " + String(packageFile->GetTotalSize()));
  179. PrintLine("Checksum: " + String(packageFile->GetChecksum()));
  180. PrintLine("Compressed: " + String(packageFile->IsCompressed() ? "yes" : "no"));
  181. break;
  182. case 'L':
  183. if (!packageFile->IsCompressed())
  184. ErrorExit("Invalid output option: -L is applicable for compressed package file only");
  185. outputCompressionRatio = true;
  186. [[fallthrough]];
  187. case 'l':
  188. {
  189. const HashMap<String, PackageEntry>& entries = packageFile->GetEntries();
  190. for (HashMap<String, PackageEntry>::ConstIterator i = entries.Begin(); i != entries.End();)
  191. {
  192. HashMap<String, PackageEntry>::ConstIterator current = i++;
  193. String fileEntry(current->first_);
  194. if (outputCompressionRatio)
  195. {
  196. unsigned compressedSize =
  197. (i == entries.End() ? packageFile->GetTotalSize() - sizeof(unsigned) : i->second_.offset_) -
  198. current->second_.offset_;
  199. fileEntry.AppendWithFormat("\tin: %u\tout: %u\tratio: %f", current->second_.size_, compressedSize,
  200. compressedSize ? 1.f * current->second_.size_ / compressedSize : 0.f);
  201. }
  202. PrintLine(fileEntry);
  203. }
  204. }
  205. break;
  206. default:
  207. ErrorExit("Unrecognized output option");
  208. }
  209. }
  210. void Run(const Vector<String>& arguments)
  211. {
  212. if (arguments.Size() < 2)
  213. ErrorExit(USAGE_STR);
  214. const String& mode = arguments[0];
  215. if (mode.Length() < 2 || mode[0] != '-')
  216. ErrorExit(USAGE_STR);
  217. if (mode[1] == 'p')
  218. Pack(arguments);
  219. else if (mode[1] == 'u')
  220. Unpack(arguments);
  221. else if (mode[1] == 'i' || mode[1] == 'l' || mode[1] == 'L')
  222. PrintInfo(arguments);
  223. else
  224. ErrorExit(USAGE_STR);
  225. }
  226. void ProcessFile(const String& fileName, const String& rootDir)
  227. {
  228. String fullPath = rootDir + "/" + fileName;
  229. File file(context_);
  230. if (!file.Open(fullPath))
  231. ErrorExit("Could not open file " + fileName);
  232. if (!file.GetSize())
  233. return;
  234. FileEntry newEntry;
  235. newEntry.name_ = fileName;
  236. newEntry.offset_ = 0; // Offset not yet known
  237. newEntry.size_ = file.GetSize();
  238. newEntry.checksum_ = 0; // Will be calculated later
  239. entries_.Push(newEntry);
  240. }
  241. void WritePackageFile(const String& fileName, const String& rootDir)
  242. {
  243. if (!quiet_)
  244. PrintLine("Writing package");
  245. File dest(context_);
  246. if (!dest.Open(fileName, FILE_WRITE))
  247. ErrorExit("Could not open output file " + fileName);
  248. // Write ID, number of files & placeholder for checksum
  249. WriteHeader(dest);
  250. for (unsigned i = 0; i < entries_.Size(); ++i)
  251. {
  252. // Write entry (correct offset is still unknown, will be filled in later)
  253. dest.WriteString(basePath_ + entries_[i].name_);
  254. dest.WriteU32(entries_[i].offset_);
  255. dest.WriteU32(entries_[i].size_);
  256. dest.WriteU32(entries_[i].checksum_);
  257. }
  258. unsigned totalDataSize = 0;
  259. unsigned lastOffset;
  260. // Write file data, calculate checksums & correct offsets
  261. for (unsigned i = 0; i < entries_.Size(); ++i)
  262. {
  263. lastOffset = entries_[i].offset_ = dest.GetSize();
  264. String fileFullPath = rootDir + "/" + entries_[i].name_;
  265. File srcFile(context_, fileFullPath);
  266. if (!srcFile.IsOpen())
  267. ErrorExit("Could not open file " + fileFullPath);
  268. unsigned dataSize = entries_[i].size_;
  269. totalDataSize += dataSize;
  270. SharedArrayPtr<u8> buffer(new u8[dataSize]);
  271. if (srcFile.Read(&buffer[0], dataSize) != dataSize)
  272. ErrorExit("Could not read file " + fileFullPath);
  273. srcFile.Close();
  274. for (unsigned j = 0; j < dataSize; ++j)
  275. {
  276. checksum_ = SDBMHash(checksum_, buffer[j]);
  277. entries_[i].checksum_ = SDBMHash(entries_[i].checksum_, buffer[j]);
  278. }
  279. if (!compress_)
  280. {
  281. if (!quiet_)
  282. PrintLine(entries_[i].name_ + " size " + String(dataSize));
  283. dest.Write(&buffer[0], entries_[i].size_);
  284. }
  285. else // Compress
  286. {
  287. SharedArrayPtr<u8> compressBuffer(new u8[LZ4_compressBound(blockSize_)]);
  288. unsigned pos = 0;
  289. while (pos < dataSize)
  290. {
  291. unsigned unpackedSize = blockSize_;
  292. if (pos + unpackedSize > dataSize)
  293. unpackedSize = dataSize - pos;
  294. auto packedSize = (unsigned)LZ4_compress_HC((const char*)&buffer[pos], (char*)compressBuffer.Get(), unpackedSize, LZ4_compressBound(unpackedSize), 0);
  295. if (!packedSize)
  296. ErrorExit("LZ4 compression failed for file " + entries_[i].name_ + " at offset " + String(pos));
  297. dest.WriteU16((unsigned short)unpackedSize);
  298. dest.WriteU16((unsigned short)packedSize);
  299. dest.Write(compressBuffer.Get(), packedSize);
  300. pos += unpackedSize;
  301. }
  302. if (!quiet_)
  303. {
  304. unsigned totalPackedBytes = dest.GetSize() - lastOffset;
  305. String fileEntry(entries_[i].name_);
  306. fileEntry.AppendWithFormat("\tin: %u\tout: %u\tratio: %f", dataSize, totalPackedBytes,
  307. totalPackedBytes ? 1.f * dataSize / totalPackedBytes : 0.f);
  308. PrintLine(fileEntry);
  309. }
  310. }
  311. }
  312. // Write package size to the end of file to allow finding it linked to an executable file
  313. unsigned currentSize = dest.GetSize();
  314. dest.WriteU32(currentSize + sizeof(unsigned));
  315. // Write header again with correct offsets & checksums
  316. dest.Seek(0);
  317. WriteHeader(dest);
  318. for (unsigned i = 0; i < entries_.Size(); ++i)
  319. {
  320. dest.WriteString(basePath_ + entries_[i].name_);
  321. dest.WriteU32(entries_[i].offset_);
  322. dest.WriteU32(entries_[i].size_);
  323. dest.WriteU32(entries_[i].checksum_);
  324. }
  325. if (!quiet_)
  326. {
  327. PrintLine("Number of files: " + String(entries_.Size()));
  328. PrintLine("File data size: " + String(totalDataSize));
  329. PrintLine("Package size: " + String(dest.GetSize()));
  330. PrintLine("Checksum: " + String(checksum_));
  331. PrintLine("Compressed: " + String(compress_ ? "yes" : "no"));
  332. }
  333. }
  334. void WriteHeader(File& dest)
  335. {
  336. if (!compress_)
  337. dest.WriteFileID("UPAK");
  338. else
  339. dest.WriteFileID("ULZ4");
  340. dest.WriteU32(entries_.Size());
  341. dest.WriteU32(checksum_);
  342. }